github.com/david-imola/snapd@v0.0.0-20210611180407-2de8ddeece6d/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 "fmt" 25 "io/ioutil" 26 "os" 27 "path/filepath" 28 "reflect" 29 "strings" 30 31 . "gopkg.in/check.v1" 32 33 "github.com/snapcore/snapd/asserts" 34 "github.com/snapcore/snapd/boot" 35 "github.com/snapcore/snapd/bootloader" 36 "github.com/snapcore/snapd/bootloader/bootloadertest" 37 "github.com/snapcore/snapd/dirs" 38 "github.com/snapcore/snapd/gadget" 39 "github.com/snapcore/snapd/osutil" 40 "github.com/snapcore/snapd/overlord/auth" 41 "github.com/snapcore/snapd/overlord/devicestate" 42 "github.com/snapcore/snapd/overlord/devicestate/devicestatetest" 43 "github.com/snapcore/snapd/overlord/snapstate" 44 "github.com/snapcore/snapd/overlord/snapstate/snapstatetest" 45 "github.com/snapcore/snapd/overlord/state" 46 "github.com/snapcore/snapd/release" 47 "github.com/snapcore/snapd/snap" 48 "github.com/snapcore/snapd/snap/snaptest" 49 "github.com/snapcore/snapd/testutil" 50 ) 51 52 type deviceMgrGadgetSuite struct { 53 deviceMgrBaseSuite 54 55 managedbl *bootloadertest.MockTrustedAssetsBootloader 56 } 57 58 var _ = Suite(&deviceMgrGadgetSuite{}) 59 60 const pcGadgetSnapYaml = ` 61 name: pc 62 type: gadget 63 ` 64 65 var snapYaml = ` 66 name: foo-gadget 67 type: gadget 68 ` 69 70 var gadgetYaml = ` 71 volumes: 72 pc: 73 bootloader: grub 74 ` 75 76 var uc20gadgetYaml = ` 77 volumes: 78 pc: 79 bootloader: grub 80 structure: 81 - name: ubuntu-seed 82 role: system-seed 83 type: 21686148-6449-6E6F-744E-656564454649 84 size: 20M 85 - name: ubuntu-boot 86 role: system-boot 87 type: 21686148-6449-6E6F-744E-656564454649 88 size: 10M 89 - name: ubuntu-data 90 role: system-data 91 type: 21686148-6449-6E6F-744E-656564454649 92 size: 50M 93 ` 94 95 var uc20gadgetYamlWithSave = uc20gadgetYaml + ` 96 - name: ubuntu-save 97 role: system-save 98 type: 21686148-6449-6E6F-744E-656564454649 99 size: 50M 100 ` 101 102 // this is the kind of volumes setup recommended to be prepared for a possible 103 // UC18 -> UC20 transition 104 var hybridGadgetYaml = ` 105 volumes: 106 hybrid: 107 bootloader: grub 108 structure: 109 - name: mbr 110 type: mbr 111 size: 440 112 content: 113 - image: pc-boot.img 114 - name: BIOS Boot 115 type: DA,21686148-6449-6E6F-744E-656564454649 116 size: 1M 117 offset: 1M 118 offset-write: mbr+92 119 content: 120 - image: pc-core.img 121 - name: EFI System 122 type: EF,C12A7328-F81F-11D2-BA4B-00A0C93EC93B 123 filesystem: vfat 124 filesystem-label: system-boot 125 size: 1200M 126 content: 127 - source: grubx64.efi 128 target: EFI/boot/grubx64.efi 129 - source: shim.efi.signed 130 target: EFI/boot/bootx64.efi 131 - source: mmx64.efi 132 target: EFI/boot/mmx64.efi 133 - source: grub.cfg 134 target: EFI/ubuntu/grub.cfg 135 - name: Ubuntu Boot 136 type: 0FC63DAF-8483-4772-8E79-3D69D8477DE4 137 filesystem: ext4 138 filesystem-label: ubuntu-boot 139 size: 750M 140 ` 141 142 func (s *deviceMgrGadgetSuite) SetUpTest(c *C) { 143 s.deviceMgrBaseSuite.SetUpTest(c) 144 145 s.managedbl = bootloadertest.Mock("mock", c.MkDir()).WithTrustedAssets() 146 s.managedbl.StaticCommandLine = "console=ttyS0 console=tty1 panic=-1" 147 s.managedbl.CandidateStaticCommandLine = "console=ttyS0 console=tty1 panic=-1 candidate" 148 149 s.state.Lock() 150 defer s.state.Unlock() 151 } 152 153 func (s *deviceMgrGadgetSuite) mockModeenvForMode(c *C, mode string) { 154 // mock minimal modeenv 155 modeenv := boot.Modeenv{ 156 Mode: mode, 157 RecoverySystem: "", 158 CurrentKernelCommandLines: []string{ 159 "snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1", 160 }, 161 } 162 err := modeenv.WriteTo("") 163 c.Assert(err, IsNil) 164 } 165 166 func (s *deviceMgrGadgetSuite) setupModelWithGadget(c *C, gadget string) { 167 s.makeModelAssertionInState(c, "canonical", "pc-model", map[string]interface{}{ 168 "architecture": "amd64", 169 "kernel": "pc-kernel", 170 "gadget": gadget, 171 "base": "core18", 172 }) 173 devicestatetest.SetDevice(s.state, &auth.DeviceState{ 174 Brand: "canonical", 175 Model: "pc-model", 176 Serial: "serial", 177 }) 178 } 179 180 func (s *deviceMgrGadgetSuite) setupUC20ModelWithGadget(c *C, gadget string) { 181 s.makeModelAssertionInState(c, "canonical", "pc20-model", map[string]interface{}{ 182 "display-name": "UC20 pc model", 183 "architecture": "amd64", 184 "base": "core20", 185 // enough to have a grade set 186 "grade": "dangerous", 187 "snaps": []interface{}{ 188 map[string]interface{}{ 189 "name": "pc-kernel", 190 "id": "pckernelidididididididididididid", 191 "type": "kernel", 192 "default-channel": "20", 193 }, 194 map[string]interface{}{ 195 "name": gadget, 196 "id": "pcididididididididididididididid", 197 "type": "gadget", 198 "default-channel": "20", 199 }}, 200 }) 201 devicestatetest.SetDevice(s.state, &auth.DeviceState{ 202 Brand: "canonical", 203 Model: "pc20-model", 204 Serial: "serial", 205 }) 206 } 207 208 func (s *deviceMgrGadgetSuite) setupGadgetUpdate(c *C, modelGrade, gadgetYamlContent, gadgetYamlContentNext string) (chg *state.Change, tsk *state.Task) { 209 siCurrent := &snap.SideInfo{ 210 RealName: "foo-gadget", 211 Revision: snap.R(33), 212 SnapID: "foo-id", 213 } 214 si := &snap.SideInfo{ 215 RealName: "foo-gadget", 216 Revision: snap.R(34), 217 SnapID: "foo-id", 218 } 219 snaptest.MockSnapWithFiles(c, snapYaml, siCurrent, [][]string{ 220 {"meta/gadget.yaml", gadgetYamlContent}, 221 {"managed-asset", "managed asset rev 33"}, 222 {"trusted-asset", "trusted asset rev 33"}, 223 }) 224 if gadgetYamlContentNext == "" { 225 gadgetYamlContentNext = gadgetYamlContent 226 } 227 snaptest.MockSnapWithFiles(c, snapYaml, si, [][]string{ 228 {"meta/gadget.yaml", gadgetYamlContentNext}, 229 {"managed-asset", "managed asset rev 34"}, 230 // SHA3-384: 88478d8afe6925b348b9cd00085f3535959fde7029a64d7841b031acc39415c690796757afab1852a9e09da913a0151b 231 {"trusted-asset", "trusted asset rev 34"}, 232 }) 233 234 s.state.Lock() 235 defer s.state.Unlock() 236 237 if modelGrade == "" { 238 s.setupModelWithGadget(c, "foo-gadget") 239 } else { 240 s.setupUC20ModelWithGadget(c, "foo-gadget") 241 } 242 243 snapstate.Set(s.state, "foo-gadget", &snapstate.SnapState{ 244 SnapType: "gadget", 245 Sequence: []*snap.SideInfo{siCurrent}, 246 Current: siCurrent.Revision, 247 Active: true, 248 }) 249 250 tsk = s.state.NewTask("update-gadget-assets", "update gadget") 251 tsk.Set("snap-setup", &snapstate.SnapSetup{ 252 SideInfo: si, 253 Type: snap.TypeGadget, 254 }) 255 chg = s.state.NewChange("dummy", "...") 256 chg.AddTask(tsk) 257 258 return chg, tsk 259 } 260 261 func (s *deviceMgrGadgetSuite) testUpdateGadgetOnCoreSimple(c *C, grade string, encryption bool, gadgetYamlCont, gadgetYamlContNext string) { 262 var updateCalled bool 263 var passedRollbackDir string 264 265 if grade != "" { 266 bootDir := c.MkDir() 267 tbl := bootloadertest.Mock("trusted", bootDir).WithTrustedAssets() 268 tbl.TrustedAssetsList = []string{"trusted-asset"} 269 tbl.ManagedAssetsList = []string{"managed-asset"} 270 bootloader.Force(tbl) 271 defer func() { bootloader.Force(nil) }() 272 } 273 274 restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, observer gadget.ContentUpdateObserver) error { 275 updateCalled = true 276 passedRollbackDir = path 277 st, err := os.Stat(path) 278 c.Assert(err, IsNil) 279 m := st.Mode() 280 c.Assert(m.IsDir(), Equals, true) 281 c.Check(m.Perm(), Equals, os.FileMode(0750)) 282 if grade == "" { 283 // non UC20 model 284 c.Check(observer, IsNil) 285 } else { 286 c.Check(observer, NotNil) 287 // expecting a very specific observer 288 trustedUpdateObserver, ok := observer.(*boot.TrustedAssetsUpdateObserver) 289 c.Assert(ok, Equals, true, Commentf("unexpected type: %T", observer)) 290 c.Assert(trustedUpdateObserver, NotNil) 291 292 // check that observer is behaving correctly with 293 // respect to trusted and managed assets 294 targetDir := c.MkDir() 295 sourceStruct := &gadget.LaidOutStructure{ 296 VolumeStructure: &gadget.VolumeStructure{ 297 Role: gadget.SystemSeed, 298 }, 299 } 300 act, err := observer.Observe(gadget.ContentUpdate, sourceStruct, targetDir, "managed-asset", 301 &gadget.ContentChange{After: filepath.Join(update.RootDir, "managed-asset")}) 302 c.Assert(err, IsNil) 303 c.Check(act, Equals, gadget.ChangeIgnore) 304 act, err = observer.Observe(gadget.ContentUpdate, sourceStruct, targetDir, "trusted-asset", 305 &gadget.ContentChange{After: filepath.Join(update.RootDir, "trusted-asset")}) 306 c.Assert(err, IsNil) 307 c.Check(act, Equals, gadget.ChangeApply) 308 // check that the behavior is correct 309 m, err := boot.ReadModeenv("") 310 c.Assert(err, IsNil) 311 if encryption { 312 // with encryption enabled, trusted asset would 313 // have been picked up by the the observer and 314 // added to modenv 315 c.Assert(m.CurrentTrustedRecoveryBootAssets, NotNil) 316 c.Check(m.CurrentTrustedRecoveryBootAssets["trusted-asset"], DeepEquals, 317 []string{"88478d8afe6925b348b9cd00085f3535959fde7029a64d7841b031acc39415c690796757afab1852a9e09da913a0151b"}) 318 } else { 319 c.Check(m.CurrentTrustedRecoveryBootAssets, HasLen, 0) 320 } 321 } 322 return nil 323 }) 324 defer restore() 325 326 chg, t := s.setupGadgetUpdate(c, grade, gadgetYamlCont, gadgetYamlContNext) 327 328 // procure modeenv and stamp that we sealed keys 329 if grade != "" { 330 // state after mark-seeded ran 331 modeenv := boot.Modeenv{ 332 Mode: "run", 333 RecoverySystem: "", 334 } 335 err := modeenv.WriteTo("") 336 c.Assert(err, IsNil) 337 338 if encryption { 339 // sealed keys stamp 340 stamp := filepath.Join(dirs.SnapFDEDir, "sealed-keys") 341 c.Assert(os.MkdirAll(filepath.Dir(stamp), 0755), IsNil) 342 err = ioutil.WriteFile(stamp, nil, 0644) 343 c.Assert(err, IsNil) 344 } 345 } 346 devicestate.SetBootOkRan(s.mgr, true) 347 348 s.state.Lock() 349 s.state.Set("seeded", true) 350 s.state.Unlock() 351 352 s.settle(c) 353 354 s.state.Lock() 355 defer s.state.Unlock() 356 c.Assert(chg.IsReady(), Equals, true) 357 c.Check(chg.Err(), IsNil) 358 c.Check(t.Status(), Equals, state.DoneStatus) 359 c.Check(updateCalled, Equals, true) 360 rollbackDir := filepath.Join(dirs.SnapRollbackDir, "foo-gadget_34") 361 c.Check(rollbackDir, Equals, passedRollbackDir) 362 // should have been removed right after update 363 c.Check(osutil.IsDirectory(rollbackDir), Equals, false) 364 c.Check(s.restartRequests, DeepEquals, []state.RestartType{state.RestartSystem}) 365 } 366 367 func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreSimple(c *C) { 368 // unset grade 369 encryption := false 370 s.testUpdateGadgetOnCoreSimple(c, "", encryption, gadgetYaml, "") 371 } 372 373 func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnUC20CoreSimpleWithEncryption(c *C) { 374 encryption := true 375 s.testUpdateGadgetOnCoreSimple(c, "dangerous", encryption, uc20gadgetYaml, "") 376 } 377 378 func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnUC20CoreSimpleNoEncryption(c *C) { 379 encryption := false 380 s.testUpdateGadgetOnCoreSimple(c, "dangerous", encryption, uc20gadgetYaml, "") 381 } 382 383 func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreNoUpdateNeeded(c *C) { 384 var called bool 385 restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error { 386 called = true 387 return gadget.ErrNoUpdate 388 }) 389 defer restore() 390 391 chg, t := s.setupGadgetUpdate(c, "", gadgetYaml, "") 392 393 s.se.Ensure() 394 s.se.Wait() 395 396 s.state.Lock() 397 defer s.state.Unlock() 398 c.Assert(chg.IsReady(), Equals, true) 399 c.Check(chg.Err(), IsNil) 400 c.Check(t.Status(), Equals, state.DoneStatus) 401 c.Check(t.Log(), HasLen, 1) 402 c.Check(t.Log()[0], Matches, ".* INFO No gadget assets update needed") 403 c.Check(called, Equals, true) 404 c.Check(s.restartRequests, HasLen, 0) 405 } 406 407 func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreRollbackDirCreateFailed(c *C) { 408 if os.Geteuid() == 0 { 409 c.Skip("this test cannot run as root (permissions are not honored)") 410 } 411 412 restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error { 413 return errors.New("unexpected call") 414 }) 415 defer restore() 416 417 chg, t := s.setupGadgetUpdate(c, "", gadgetYaml, "") 418 419 rollbackDir := filepath.Join(dirs.SnapRollbackDir, "foo-gadget_34") 420 err := os.MkdirAll(dirs.SnapRollbackDir, 0000) 421 c.Assert(err, IsNil) 422 423 s.state.Lock() 424 s.state.Set("seeded", true) 425 s.state.Unlock() 426 427 s.settle(c) 428 429 s.state.Lock() 430 defer s.state.Unlock() 431 c.Assert(chg.IsReady(), Equals, true) 432 c.Check(chg.Err(), ErrorMatches, `(?s).*cannot prepare update rollback directory: .*`) 433 c.Check(t.Status(), Equals, state.ErrorStatus) 434 c.Check(osutil.IsDirectory(rollbackDir), Equals, false) 435 c.Check(s.restartRequests, HasLen, 0) 436 } 437 438 func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreUpdateFailed(c *C) { 439 restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error { 440 return errors.New("gadget exploded") 441 }) 442 defer restore() 443 chg, t := s.setupGadgetUpdate(c, "", gadgetYaml, "") 444 445 s.state.Lock() 446 s.state.Set("seeded", true) 447 s.state.Unlock() 448 449 s.settle(c) 450 451 s.state.Lock() 452 defer s.state.Unlock() 453 c.Assert(chg.IsReady(), Equals, true) 454 c.Check(chg.Err(), ErrorMatches, `(?s).*update gadget \(gadget exploded\).*`) 455 c.Check(t.Status(), Equals, state.ErrorStatus) 456 rollbackDir := filepath.Join(dirs.SnapRollbackDir, "foo-gadget_34") 457 // update rollback left for inspection 458 c.Check(osutil.IsDirectory(rollbackDir), Equals, true) 459 c.Check(s.restartRequests, HasLen, 0) 460 } 461 462 func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreNotDuringFirstboot(c *C) { 463 restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error { 464 return errors.New("unexpected call") 465 }) 466 defer restore() 467 468 // simulate first-boot/seeding, there is no existing snap state information 469 470 si := &snap.SideInfo{ 471 RealName: "foo-gadget", 472 Revision: snap.R(34), 473 SnapID: "foo-id", 474 } 475 snaptest.MockSnapWithFiles(c, snapYaml, si, [][]string{ 476 {"meta/gadget.yaml", gadgetYaml}, 477 }) 478 479 s.state.Lock() 480 s.state.Set("seeded", true) 481 482 s.setupModelWithGadget(c, "foo-gadget") 483 484 t := s.state.NewTask("update-gadget-assets", "update gadget") 485 t.Set("snap-setup", &snapstate.SnapSetup{ 486 SideInfo: si, 487 Type: snap.TypeGadget, 488 }) 489 chg := s.state.NewChange("dummy", "...") 490 chg.AddTask(t) 491 492 s.state.Unlock() 493 494 s.settle(c) 495 496 s.state.Lock() 497 defer s.state.Unlock() 498 c.Assert(chg.IsReady(), Equals, true) 499 c.Check(chg.Err(), IsNil) 500 c.Check(t.Status(), Equals, state.DoneStatus) 501 rollbackDir := filepath.Join(dirs.SnapRollbackDir, "foo-gadget") 502 c.Check(osutil.IsDirectory(rollbackDir), Equals, false) 503 c.Check(s.restartRequests, HasLen, 0) 504 } 505 506 func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreBadGadgetYaml(c *C) { 507 restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error { 508 return errors.New("unexpected call") 509 }) 510 defer restore() 511 siCurrent := &snap.SideInfo{ 512 RealName: "foo-gadget", 513 Revision: snap.R(33), 514 SnapID: "foo-id", 515 } 516 si := &snap.SideInfo{ 517 RealName: "foo-gadget", 518 Revision: snap.R(34), 519 SnapID: "foo-id", 520 } 521 snaptest.MockSnapWithFiles(c, snapYaml, siCurrent, [][]string{ 522 {"meta/gadget.yaml", gadgetYaml}, 523 }) 524 // invalid gadget.yaml data 525 snaptest.MockSnapWithFiles(c, snapYaml, si, [][]string{ 526 {"meta/gadget.yaml", "foobar"}, 527 }) 528 529 s.state.Lock() 530 s.state.Set("seeded", true) 531 532 s.setupModelWithGadget(c, "foo-gadget") 533 534 snapstate.Set(s.state, "foo-gadget", &snapstate.SnapState{ 535 SnapType: "gadget", 536 Sequence: []*snap.SideInfo{siCurrent}, 537 Current: siCurrent.Revision, 538 Active: true, 539 }) 540 541 t := s.state.NewTask("update-gadget-assets", "update gadget") 542 t.Set("snap-setup", &snapstate.SnapSetup{ 543 SideInfo: si, 544 Type: snap.TypeGadget, 545 }) 546 chg := s.state.NewChange("dummy", "...") 547 chg.AddTask(t) 548 549 s.state.Unlock() 550 551 s.settle(c) 552 553 s.state.Lock() 554 defer s.state.Unlock() 555 c.Assert(chg.IsReady(), Equals, true) 556 c.Check(chg.Err(), ErrorMatches, `(?s).*update gadget \(cannot read candidate snap gadget metadata: .*\).*`) 557 c.Check(t.Status(), Equals, state.ErrorStatus) 558 rollbackDir := filepath.Join(dirs.SnapRollbackDir, "foo-gadget") 559 c.Check(osutil.IsDirectory(rollbackDir), Equals, false) 560 c.Check(s.restartRequests, HasLen, 0) 561 } 562 563 func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreParanoidChecks(c *C) { 564 restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error { 565 return errors.New("unexpected call") 566 }) 567 defer restore() 568 siCurrent := &snap.SideInfo{ 569 RealName: "foo-gadget", 570 Revision: snap.R(33), 571 SnapID: "foo-id", 572 } 573 si := &snap.SideInfo{ 574 RealName: "foo-gadget-unexpected", 575 Revision: snap.R(34), 576 SnapID: "foo-id", 577 } 578 579 s.state.Lock() 580 581 s.state.Set("seeded", true) 582 583 s.setupModelWithGadget(c, "foo-gadget") 584 585 snapstate.Set(s.state, "foo-gadget", &snapstate.SnapState{ 586 SnapType: "gadget", 587 Sequence: []*snap.SideInfo{siCurrent}, 588 Current: siCurrent.Revision, 589 Active: true, 590 }) 591 592 t := s.state.NewTask("update-gadget-assets", "update gadget") 593 t.Set("snap-setup", &snapstate.SnapSetup{ 594 SideInfo: si, 595 Type: snap.TypeGadget, 596 }) 597 chg := s.state.NewChange("dummy", "...") 598 chg.AddTask(t) 599 600 s.state.Unlock() 601 602 s.settle(c) 603 604 s.state.Lock() 605 defer s.state.Unlock() 606 c.Assert(chg.IsReady(), Equals, true) 607 c.Assert(chg.Err(), ErrorMatches, `(?s).*\(cannot apply gadget assets update from non-model gadget snap "foo-gadget-unexpected", expected "foo-gadget" snap\)`) 608 c.Check(t.Status(), Equals, state.ErrorStatus) 609 c.Check(s.restartRequests, HasLen, 0) 610 } 611 612 func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnClassicErrorsOut(c *C) { 613 restore := release.MockOnClassic(true) 614 defer restore() 615 616 restore = devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error { 617 return errors.New("unexpected call") 618 }) 619 defer restore() 620 621 s.state.Lock() 622 623 s.state.Set("seeded", true) 624 625 t := s.state.NewTask("update-gadget-assets", "update gadget") 626 chg := s.state.NewChange("dummy", "...") 627 chg.AddTask(t) 628 629 s.state.Unlock() 630 631 s.settle(c) 632 633 s.state.Lock() 634 defer s.state.Unlock() 635 c.Assert(chg.IsReady(), Equals, true) 636 c.Check(chg.Err(), ErrorMatches, `(?s).*update gadget \(cannot run update gadget assets task on a classic system\).*`) 637 c.Check(t.Status(), Equals, state.ErrorStatus) 638 } 639 640 type mockUpdater struct{} 641 642 func (m *mockUpdater) Backup() error { return nil } 643 644 func (m *mockUpdater) Rollback() error { return nil } 645 646 func (m *mockUpdater) Update() error { return nil } 647 648 func (s *deviceMgrGadgetSuite) TestUpdateGadgetCallsToGadget(c *C) { 649 siCurrent := &snap.SideInfo{ 650 RealName: "foo-gadget", 651 Revision: snap.R(33), 652 SnapID: "foo-id", 653 } 654 si := &snap.SideInfo{ 655 RealName: "foo-gadget", 656 Revision: snap.R(34), 657 SnapID: "foo-id", 658 } 659 var gadgetCurrentYaml = ` 660 volumes: 661 pc: 662 bootloader: grub 663 structure: 664 - name: foo 665 size: 10M 666 type: bare 667 content: 668 - image: content.img 669 ` 670 var gadgetUpdateYaml = ` 671 volumes: 672 pc: 673 bootloader: grub 674 structure: 675 - name: foo 676 size: 10M 677 type: bare 678 content: 679 - image: content.img 680 update: 681 edition: 2 682 ` 683 snaptest.MockSnapWithFiles(c, snapYaml, siCurrent, [][]string{ 684 {"meta/gadget.yaml", gadgetCurrentYaml}, 685 {"content.img", "some content"}, 686 }) 687 updateInfo := snaptest.MockSnapWithFiles(c, snapYaml, si, [][]string{ 688 {"meta/gadget.yaml", gadgetUpdateYaml}, 689 {"content.img", "updated content"}, 690 }) 691 692 expectedRollbackDir := filepath.Join(dirs.SnapRollbackDir, "foo-gadget_34") 693 updaterForStructureCalls := 0 694 restore := gadget.MockUpdaterForStructure(func(ps *gadget.LaidOutStructure, rootDir, rollbackDir string, _ gadget.ContentUpdateObserver) (gadget.Updater, error) { 695 updaterForStructureCalls++ 696 697 c.Assert(ps.Name, Equals, "foo") 698 c.Assert(rootDir, Equals, updateInfo.MountDir()) 699 c.Assert(filepath.Join(rootDir, "content.img"), testutil.FileEquals, "updated content") 700 c.Assert(strings.HasPrefix(rollbackDir, expectedRollbackDir), Equals, true) 701 c.Assert(osutil.IsDirectory(rollbackDir), Equals, true) 702 return &mockUpdater{}, nil 703 }) 704 defer restore() 705 706 s.state.Lock() 707 s.state.Set("seeded", true) 708 709 s.setupModelWithGadget(c, "foo-gadget") 710 711 snapstate.Set(s.state, "foo-gadget", &snapstate.SnapState{ 712 SnapType: "gadget", 713 Sequence: []*snap.SideInfo{siCurrent}, 714 Current: siCurrent.Revision, 715 Active: true, 716 }) 717 718 t := s.state.NewTask("update-gadget-assets", "update gadget") 719 t.Set("snap-setup", &snapstate.SnapSetup{ 720 SideInfo: si, 721 Type: snap.TypeGadget, 722 }) 723 chg := s.state.NewChange("dummy", "...") 724 chg.AddTask(t) 725 726 s.state.Unlock() 727 728 s.settle(c) 729 730 s.state.Lock() 731 defer s.state.Unlock() 732 c.Assert(chg.IsReady(), Equals, true) 733 c.Check(t.Status(), Equals, state.DoneStatus) 734 c.Check(s.restartRequests, HasLen, 1) 735 c.Check(updaterForStructureCalls, Equals, 1) 736 } 737 738 func (s *deviceMgrGadgetSuite) TestCurrentAndUpdateInfo(c *C) { 739 siCurrent := &snap.SideInfo{ 740 RealName: "foo-gadget", 741 Revision: snap.R(33), 742 SnapID: "foo-id", 743 } 744 si := &snap.SideInfo{ 745 RealName: "foo-gadget", 746 Revision: snap.R(34), 747 SnapID: "foo-id", 748 } 749 750 s.state.Lock() 751 defer s.state.Unlock() 752 753 snapsup := &snapstate.SnapSetup{ 754 SideInfo: si, 755 Type: snap.TypeGadget, 756 } 757 758 model := s.brands.Model("canonical", "pc-model", map[string]interface{}{ 759 "architecture": "amd64", 760 "kernel": "pc-kernel", 761 "gadget": "foo-gadget", 762 "base": "core18", 763 }) 764 deviceCtx := &snapstatetest.TrivialDeviceContext{DeviceModel: model} 765 766 current, err := devicestate.CurrentGadgetInfo(s.state, deviceCtx) 767 c.Assert(current, IsNil) 768 c.Check(err, IsNil) 769 770 snapstate.Set(s.state, "foo-gadget", &snapstate.SnapState{ 771 SnapType: "gadget", 772 Sequence: []*snap.SideInfo{siCurrent}, 773 Current: siCurrent.Revision, 774 Active: true, 775 }) 776 777 // mock current first, but gadget.yaml is still missing 778 ci := snaptest.MockSnapWithFiles(c, snapYaml, siCurrent, nil) 779 780 current, err = devicestate.CurrentGadgetInfo(s.state, deviceCtx) 781 782 c.Assert(current, IsNil) 783 c.Assert(err, ErrorMatches, "cannot read current gadget snap details: .*/33/meta/gadget.yaml: no such file or directory") 784 785 // drop gadget.yaml for current snap 786 ioutil.WriteFile(filepath.Join(ci.MountDir(), "meta/gadget.yaml"), []byte(gadgetYaml), 0644) 787 788 current, err = devicestate.CurrentGadgetInfo(s.state, deviceCtx) 789 c.Assert(err, IsNil) 790 c.Assert(current, DeepEquals, &gadget.GadgetData{ 791 Info: &gadget.Info{ 792 Volumes: map[string]*gadget.Volume{ 793 "pc": { 794 Bootloader: "grub", 795 Schema: "gpt", 796 }, 797 }, 798 }, 799 RootDir: ci.MountDir(), 800 }) 801 802 // pending update 803 update, err := devicestate.PendingGadgetInfo(snapsup, deviceCtx) 804 c.Assert(update, IsNil) 805 c.Assert(err, ErrorMatches, "cannot read candidate gadget snap details: cannot find installed snap .* .*/34/meta/snap.yaml") 806 807 ui := snaptest.MockSnapWithFiles(c, snapYaml, si, nil) 808 809 update, err = devicestate.PendingGadgetInfo(snapsup, deviceCtx) 810 c.Assert(update, IsNil) 811 c.Assert(err, ErrorMatches, "cannot read candidate snap gadget metadata: .*/34/meta/gadget.yaml: no such file or directory") 812 813 var updateGadgetYaml = ` 814 volumes: 815 pc: 816 bootloader: grub 817 id: 123 818 ` 819 820 // drop gadget.yaml for update snap 821 ioutil.WriteFile(filepath.Join(ui.MountDir(), "meta/gadget.yaml"), []byte(updateGadgetYaml), 0644) 822 823 update, err = devicestate.PendingGadgetInfo(snapsup, deviceCtx) 824 c.Assert(err, IsNil) 825 c.Assert(update, DeepEquals, &gadget.GadgetData{ 826 Info: &gadget.Info{ 827 Volumes: map[string]*gadget.Volume{ 828 "pc": { 829 Bootloader: "grub", 830 Schema: "gpt", 831 ID: "123", 832 }, 833 }, 834 }, 835 RootDir: ui.MountDir(), 836 }) 837 } 838 839 func (s *deviceMgrGadgetSuite) TestGadgetUpdateBlocksWhenOtherTasks(c *C) { 840 restore := release.MockOnClassic(true) 841 defer restore() 842 843 s.state.Lock() 844 defer s.state.Unlock() 845 846 tUpdate := s.state.NewTask("update-gadget-assets", "update gadget") 847 t1 := s.state.NewTask("other-task-1", "other 1") 848 t2 := s.state.NewTask("other-task-2", "other 2") 849 850 // no other running tasks, does not block 851 c.Assert(devicestate.GadgetUpdateBlocked(tUpdate, nil), Equals, false) 852 853 // list of running tasks actually contains ones that are in the 'running' state 854 t1.SetStatus(state.DoingStatus) 855 t2.SetStatus(state.UndoingStatus) 856 // block on any other running tasks 857 c.Assert(devicestate.GadgetUpdateBlocked(tUpdate, []*state.Task{t1, t2}), Equals, true) 858 } 859 860 func (s *deviceMgrGadgetSuite) TestGadgetUpdateBlocksOtherTasks(c *C) { 861 restore := release.MockOnClassic(true) 862 defer restore() 863 864 s.state.Lock() 865 defer s.state.Unlock() 866 867 tUpdate := s.state.NewTask("update-gadget-assets", "update gadget") 868 tUpdate.SetStatus(state.DoingStatus) 869 t1 := s.state.NewTask("other-task-1", "other 1") 870 t2 := s.state.NewTask("other-task-2", "other 2") 871 872 // block on any other running tasks 873 c.Assert(devicestate.GadgetUpdateBlocked(t1, []*state.Task{tUpdate}), Equals, true) 874 c.Assert(devicestate.GadgetUpdateBlocked(t2, []*state.Task{tUpdate}), Equals, true) 875 876 t2.SetStatus(state.UndoingStatus) 877 // update-gadget should be the only running task, for the sake of 878 // completeness pretend it's one of many running tasks 879 c.Assert(devicestate.GadgetUpdateBlocked(t1, []*state.Task{tUpdate, t2}), Equals, true) 880 881 // not blocking without gadget update task 882 c.Assert(devicestate.GadgetUpdateBlocked(t1, []*state.Task{t2}), Equals, false) 883 } 884 885 func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreHybridFirstboot(c *C) { 886 restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error { 887 return errors.New("unexpected call") 888 }) 889 defer restore() 890 891 // simulate first-boot/seeding, there is no existing snap state information 892 893 si := &snap.SideInfo{ 894 RealName: "foo-gadget", 895 Revision: snap.R(34), 896 SnapID: "foo-id", 897 } 898 snaptest.MockSnapWithFiles(c, snapYaml, si, [][]string{ 899 {"meta/gadget.yaml", hybridGadgetYaml}, 900 }) 901 902 s.state.Lock() 903 s.state.Set("seeded", true) 904 905 s.setupModelWithGadget(c, "foo-gadget") 906 907 t := s.state.NewTask("update-gadget-assets", "update gadget") 908 t.Set("snap-setup", &snapstate.SnapSetup{ 909 SideInfo: si, 910 Type: snap.TypeGadget, 911 }) 912 chg := s.state.NewChange("dummy", "...") 913 chg.AddTask(t) 914 915 s.state.Unlock() 916 917 s.settle(c) 918 919 s.state.Lock() 920 defer s.state.Unlock() 921 c.Assert(chg.IsReady(), Equals, true) 922 c.Check(chg.Err(), IsNil) 923 c.Check(t.Status(), Equals, state.DoneStatus) 924 rollbackDir := filepath.Join(dirs.SnapRollbackDir, "foo-gadget") 925 c.Check(osutil.IsDirectory(rollbackDir), Equals, false) 926 c.Check(s.restartRequests, HasLen, 0) 927 } 928 929 func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreHybridShouldWork(c *C) { 930 encryption := false 931 s.testUpdateGadgetOnCoreSimple(c, "", encryption, hybridGadgetYaml, "") 932 } 933 934 func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreOldIsInvalidNowButShouldWork(c *C) { 935 encryption := false 936 // this is not gadget yaml that we should support, by the UC16/18 937 // rules it actually has two system-boot role partitions, 938 hybridGadgetYamlBroken := hybridGadgetYaml + ` 939 role: system-boot 940 ` 941 s.testUpdateGadgetOnCoreSimple(c, "", encryption, hybridGadgetYamlBroken, hybridGadgetYaml) 942 } 943 944 func (s *deviceMgrGadgetSuite) makeMinimalKernelAssetsUpdateChange(c *C) (chg *state.Change, tsk *state.Task) { 945 s.state.Lock() 946 defer s.state.Unlock() 947 948 siGadget := &snap.SideInfo{ 949 RealName: "foo-gadget", 950 Revision: snap.R(1), 951 SnapID: "foo-gadget-id", 952 } 953 gadgetSnapYaml := "name: foo-gadget\nversion: 1.0\ntype: gadget" 954 gadgetYamlContent := ` 955 volumes: 956 pi: 957 bootloader: grub` 958 snaptest.MockSnapWithFiles(c, gadgetSnapYaml, siGadget, [][]string{ 959 {"meta/gadget.yaml", gadgetYamlContent}, 960 }) 961 s.setupModelWithGadget(c, "foo-gadget") 962 snapstate.Set(s.state, "foo-gadget", &snapstate.SnapState{ 963 SnapType: "gadget", 964 Sequence: []*snap.SideInfo{siGadget}, 965 Current: siGadget.Revision, 966 Active: true, 967 }) 968 969 snapKernelYaml := "name: pc-kernel\nversion: 1.0\ntype: kernel" 970 siCurrent := &snap.SideInfo{ 971 RealName: "pc-kernel", 972 Revision: snap.R(33), 973 SnapID: "foo-id", 974 } 975 snaptest.MockSnapWithFiles(c, snapKernelYaml, siCurrent, nil) 976 siNext := &snap.SideInfo{ 977 RealName: "pc-kernel", 978 Revision: snap.R(34), 979 SnapID: "foo-id", 980 } 981 snaptest.MockSnapWithFiles(c, snapKernelYaml, siNext, nil) 982 snapstate.Set(s.state, "pc-kernel", &snapstate.SnapState{ 983 SnapType: "kernel", 984 Sequence: []*snap.SideInfo{siNext, siCurrent}, 985 Current: siCurrent.Revision, 986 Active: true, 987 }) 988 989 s.bootloader.SetBootVars(map[string]string{ 990 "snap_core": "core_1.snap", 991 "snap_kernel": "pc-kernel_33.snap", 992 }) 993 994 tsk = s.state.NewTask("update-gadget-assets", "update gadget") 995 tsk.Set("snap-setup", &snapstate.SnapSetup{ 996 SideInfo: siNext, 997 Type: snap.TypeKernel, 998 }) 999 chg = s.state.NewChange("dummy", "...") 1000 chg.AddTask(tsk) 1001 1002 return chg, tsk 1003 } 1004 1005 func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreFromKernel(c *C) { 1006 var updateCalled int 1007 var passedRollbackDir string 1008 1009 restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, observer gadget.ContentUpdateObserver) error { 1010 updateCalled++ 1011 passedRollbackDir = path 1012 1013 c.Check(strings.HasSuffix(current.RootDir, "/snap/foo-gadget/1"), Equals, true) 1014 c.Check(strings.HasSuffix(update.RootDir, "/snap/foo-gadget/1"), Equals, true) 1015 c.Check(strings.HasSuffix(current.KernelRootDir, "/snap/pc-kernel/33"), Equals, true) 1016 c.Check(strings.HasSuffix(update.KernelRootDir, "/snap/pc-kernel/34"), Equals, true) 1017 1018 // KernelUpdatePolicy is used 1019 c.Check(reflect.ValueOf(policy), DeepEquals, reflect.ValueOf(gadget.UpdatePolicyFunc(gadget.KernelUpdatePolicy))) 1020 return nil 1021 }) 1022 defer restore() 1023 1024 chg, t := s.makeMinimalKernelAssetsUpdateChange(c) 1025 devicestate.SetBootOkRan(s.mgr, true) 1026 1027 s.state.Lock() 1028 s.state.Set("seeded", true) 1029 s.state.Unlock() 1030 1031 s.settle(c) 1032 1033 s.state.Lock() 1034 defer s.state.Unlock() 1035 c.Assert(chg.IsReady(), Equals, true) 1036 c.Check(chg.Err(), IsNil) 1037 c.Check(t.Status(), Equals, state.DoneStatus) 1038 c.Check(updateCalled, Equals, 1) 1039 rollbackDir := filepath.Join(dirs.SnapRollbackDir, "pc-kernel_34") 1040 c.Check(rollbackDir, Equals, passedRollbackDir) 1041 } 1042 1043 func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreFromKernelRemodel(c *C) { 1044 var updateCalled int 1045 var passedRollbackDir string 1046 1047 restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, observer gadget.ContentUpdateObserver) error { 1048 updateCalled++ 1049 passedRollbackDir = path 1050 1051 c.Check(strings.HasSuffix(current.RootDir, "/snap/foo-gadget/1"), Equals, true) 1052 c.Check(strings.HasSuffix(update.RootDir, "/snap/foo-gadget/1"), Equals, true) 1053 c.Check(strings.HasSuffix(current.KernelRootDir, "/snap/pc-kernel/33"), Equals, true) 1054 c.Check(strings.HasSuffix(update.KernelRootDir, "/snap/pc-kernel/34"), Equals, true) 1055 1056 // KernelUpdatePolicy is used even when we remodel 1057 c.Check(reflect.ValueOf(policy), DeepEquals, reflect.ValueOf(gadget.UpdatePolicyFunc(gadget.KernelUpdatePolicy))) 1058 return nil 1059 }) 1060 defer restore() 1061 1062 chg, t := s.makeMinimalKernelAssetsUpdateChange(c) 1063 devicestate.SetBootOkRan(s.mgr, true) 1064 1065 newModel := s.brands.Model("canonical", "pc-model", map[string]interface{}{ 1066 "architecture": "amd64", 1067 "kernel": "pc-kernel", 1068 "gadget": "foo-gadget", 1069 "base": "core18", 1070 "revision": "1", 1071 }) 1072 1073 s.state.Lock() 1074 // pretend we are remodeling 1075 chg.Set("new-model", string(asserts.Encode(newModel))) 1076 s.state.Set("seeded", true) 1077 s.state.Unlock() 1078 1079 s.settle(c) 1080 1081 s.state.Lock() 1082 defer s.state.Unlock() 1083 c.Assert(chg.IsReady(), Equals, true) 1084 c.Check(chg.Err(), IsNil) 1085 c.Check(t.Status(), Equals, state.DoneStatus) 1086 c.Check(updateCalled, Equals, 1) 1087 rollbackDir := filepath.Join(dirs.SnapRollbackDir, "pc-kernel_34") 1088 c.Check(rollbackDir, Equals, passedRollbackDir) 1089 } 1090 1091 func (s *deviceMgrGadgetSuite) testGadgetCommandlineUpdateRun(c *C, fromFiles, toFiles [][]string, errMatch, logMatch string, updated bool) { 1092 restore := release.MockOnClassic(false) 1093 defer restore() 1094 1095 s.state.Lock() 1096 1097 currentSi := &snap.SideInfo{ 1098 RealName: "pc", 1099 Revision: snap.R(33), 1100 SnapID: "foo-id", 1101 } 1102 snapstate.Set(s.state, "pc", &snapstate.SnapState{ 1103 SnapType: "gadget", 1104 Sequence: []*snap.SideInfo{currentSi}, 1105 Current: currentSi.Revision, 1106 Active: true, 1107 }) 1108 snaptest.MockSnapWithFiles(c, pcGadgetSnapYaml, currentSi, fromFiles) 1109 updateSi := *currentSi 1110 updateSi.Revision = snap.R(34) 1111 snaptest.MockSnapWithFiles(c, pcGadgetSnapYaml, &updateSi, toFiles) 1112 1113 tsk := s.state.NewTask("update-gadget-cmdline", "update gadget command line") 1114 tsk.Set("snap-setup", &snapstate.SnapSetup{ 1115 SideInfo: &updateSi, 1116 Type: snap.TypeGadget, 1117 }) 1118 chg := s.state.NewChange("dummy", "...") 1119 chg.AddTask(tsk) 1120 s.state.Unlock() 1121 1122 s.settle(c) 1123 1124 s.state.Lock() 1125 defer s.state.Unlock() 1126 1127 c.Assert(chg.IsReady(), Equals, true) 1128 if errMatch == "" { 1129 c.Check(chg.Err(), IsNil) 1130 c.Check(tsk.Status(), Equals, state.DoneStatus) 1131 // we log on success 1132 log := tsk.Log() 1133 if logMatch != "" { 1134 c.Assert(log, HasLen, 1) 1135 c.Check(log[0], Matches, fmt.Sprintf(".* %v", logMatch)) 1136 } else { 1137 c.Check(log, HasLen, 0) 1138 } 1139 if updated { 1140 // update was applied, thus a restart was requested 1141 c.Check(s.restartRequests, DeepEquals, []state.RestartType{state.RestartSystem}) 1142 } else { 1143 // update was not applied or failed 1144 c.Check(s.restartRequests, HasLen, 0) 1145 } 1146 } else { 1147 c.Check(chg.Err(), ErrorMatches, errMatch) 1148 c.Check(tsk.Status(), Equals, state.ErrorStatus) 1149 } 1150 } 1151 1152 func (s *deviceMgrGadgetSuite) TestUpdateGadgetCommandlineWithExistingArgs(c *C) { 1153 // arguments change 1154 bootloader.Force(s.managedbl) 1155 s.state.Lock() 1156 s.setupUC20ModelWithGadget(c, "pc") 1157 s.mockModeenvForMode(c, "run") 1158 devicestate.SetBootOkRan(s.mgr, true) 1159 s.state.Set("seeded", true) 1160 1161 // update the modeenv to have the gadget arguments included to mimic the 1162 // state we would have in the system 1163 m, err := boot.ReadModeenv("") 1164 c.Assert(err, IsNil) 1165 m.CurrentKernelCommandLines = []string{ 1166 "snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1 args from old gadget", 1167 } 1168 c.Assert(m.Write(), IsNil) 1169 err = s.managedbl.SetBootVars(map[string]string{ 1170 "snapd_extra_cmdline_args": "args from old gadget", 1171 }) 1172 c.Assert(err, IsNil) 1173 s.managedbl.SetBootVarsCalls = 0 1174 1175 s.state.Unlock() 1176 1177 const update = true 1178 s.testGadgetCommandlineUpdateRun(c, 1179 [][]string{ 1180 {"meta/gadget.yaml", gadgetYaml}, 1181 {"cmdline.extra", "args from old gadget"}, 1182 }, 1183 [][]string{ 1184 {"meta/gadget.yaml", gadgetYaml}, 1185 {"cmdline.extra", "args from updated gadget"}, 1186 }, 1187 "", "Updated kernel command line", update) 1188 1189 m, err = boot.ReadModeenv("") 1190 c.Assert(err, IsNil) 1191 c.Check([]string(m.CurrentKernelCommandLines), DeepEquals, []string{ 1192 "snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1 args from old gadget", 1193 // gadget arguments are picked up for the candidate command line 1194 "snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1 args from updated gadget", 1195 }) 1196 c.Check(s.managedbl.SetBootVarsCalls, Equals, 1) 1197 vars, err := s.managedbl.GetBootVars("snapd_extra_cmdline_args") 1198 c.Assert(err, IsNil) 1199 // bootenv was cleared 1200 c.Assert(vars, DeepEquals, map[string]string{ 1201 "snapd_extra_cmdline_args": "args from updated gadget", 1202 }) 1203 } 1204 1205 func (s *deviceMgrGadgetSuite) TestUpdateGadgetCommandlineWithNewArgs(c *C) { 1206 // no command line arguments prior to the gadget update 1207 bootloader.Force(s.managedbl) 1208 s.state.Lock() 1209 s.setupUC20ModelWithGadget(c, "pc") 1210 s.mockModeenvForMode(c, "run") 1211 devicestate.SetBootOkRan(s.mgr, true) 1212 s.state.Set("seeded", true) 1213 1214 // mimic system state 1215 m, err := boot.ReadModeenv("") 1216 c.Assert(err, IsNil) 1217 m.CurrentKernelCommandLines = []string{ 1218 "snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1", 1219 } 1220 c.Assert(m.Write(), IsNil) 1221 err = s.managedbl.SetBootVars(map[string]string{ 1222 "snapd_extra_cmdline_args": "", 1223 }) 1224 c.Assert(err, IsNil) 1225 s.managedbl.SetBootVarsCalls = 0 1226 1227 s.state.Unlock() 1228 1229 const update = true 1230 s.testGadgetCommandlineUpdateRun(c, 1231 [][]string{ 1232 {"meta/gadget.yaml", gadgetYaml}, 1233 // old gadget does not carry command line arguments 1234 }, 1235 [][]string{ 1236 {"meta/gadget.yaml", gadgetYaml}, 1237 {"cmdline.extra", "args from new gadget"}, 1238 }, 1239 "", "Updated kernel command line", update) 1240 1241 m, err = boot.ReadModeenv("") 1242 c.Assert(err, IsNil) 1243 c.Check([]string(m.CurrentKernelCommandLines), DeepEquals, []string{ 1244 "snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1", 1245 // gadget arguments are picked up for the candidate command line 1246 "snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1 args from new gadget", 1247 }) 1248 c.Check(s.managedbl.SetBootVarsCalls, Equals, 1) 1249 vars, err := s.managedbl.GetBootVars("snapd_extra_cmdline_args") 1250 c.Assert(err, IsNil) 1251 // bootenv was cleared 1252 c.Assert(vars, DeepEquals, map[string]string{ 1253 "snapd_extra_cmdline_args": "args from new gadget", 1254 }) 1255 } 1256 1257 func (s *deviceMgrGadgetSuite) TestUpdateGadgetCommandlineDroppedArgs(c *C) { 1258 // no command line arguments prior to the gadget up 1259 s.state.Lock() 1260 bootloader.Force(s.managedbl) 1261 s.setupUC20ModelWithGadget(c, "pc") 1262 s.mockModeenvForMode(c, "run") 1263 devicestate.SetBootOkRan(s.mgr, true) 1264 s.state.Set("seeded", true) 1265 1266 // mimic system state 1267 m, err := boot.ReadModeenv("") 1268 c.Assert(err, IsNil) 1269 m.CurrentKernelCommandLines = []string{ 1270 "snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1 args from gadget", 1271 } 1272 c.Assert(m.Write(), IsNil) 1273 err = s.managedbl.SetBootVars(map[string]string{ 1274 "snapd_extra_cmdline_args": "args from gadget", 1275 }) 1276 c.Assert(err, IsNil) 1277 s.managedbl.SetBootVarsCalls = 0 1278 1279 s.state.Unlock() 1280 1281 const update = true 1282 s.testGadgetCommandlineUpdateRun(c, 1283 [][]string{ 1284 {"meta/gadget.yaml", gadgetYaml}, 1285 // old gadget carries command line arguments 1286 {"cmdline.extra", "args from gadget"}, 1287 }, 1288 [][]string{ 1289 {"meta/gadget.yaml", gadgetYaml}, 1290 // new one does not 1291 }, 1292 "", "Updated kernel command line", update) 1293 1294 m, err = boot.ReadModeenv("") 1295 c.Assert(err, IsNil) 1296 c.Check([]string(m.CurrentKernelCommandLines), DeepEquals, []string{ 1297 "snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1 args from gadget", 1298 // this is the expected new command line 1299 "snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1", 1300 }) 1301 c.Check(s.managedbl.SetBootVarsCalls, Equals, 1) 1302 vars, err := s.managedbl.GetBootVars("snapd_extra_cmdline_args") 1303 c.Assert(err, IsNil) 1304 // bootenv was cleared 1305 c.Assert(vars, DeepEquals, map[string]string{ 1306 "snapd_extra_cmdline_args": "", 1307 }) 1308 } 1309 1310 func (s *deviceMgrGadgetSuite) TestUpdateGadgetCommandlineUnchanged(c *C) { 1311 // no command line arguments prior to the gadget update 1312 bootloader.Force(s.managedbl) 1313 s.state.Lock() 1314 s.setupUC20ModelWithGadget(c, "pc") 1315 s.mockModeenvForMode(c, "run") 1316 devicestate.SetBootOkRan(s.mgr, true) 1317 s.state.Set("seeded", true) 1318 1319 // mimic system state 1320 m, err := boot.ReadModeenv("") 1321 c.Assert(err, IsNil) 1322 m.CurrentKernelCommandLines = []string{ 1323 "snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1 args from gadget", 1324 } 1325 c.Assert(m.Write(), IsNil) 1326 err = s.managedbl.SetBootVars(map[string]string{ 1327 "snapd_extra_cmdline_args": "args from gadget", 1328 }) 1329 c.Assert(err, IsNil) 1330 s.managedbl.SetBootVarsCalls = 0 1331 1332 s.state.Unlock() 1333 1334 sameFiles := [][]string{ 1335 {"meta/gadget.yaml", gadgetYaml}, 1336 {"cmdline.extra", "args from gadget"}, 1337 } 1338 // old and new gadget have the same command line arguments, nothing changes 1339 const update = false 1340 s.testGadgetCommandlineUpdateRun(c, sameFiles, sameFiles, 1341 "", "", update) 1342 1343 m, err = boot.ReadModeenv("") 1344 c.Assert(err, IsNil) 1345 c.Check([]string(m.CurrentKernelCommandLines), DeepEquals, []string{ 1346 "snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1 args from gadget", 1347 }) 1348 c.Check(s.managedbl.SetBootVarsCalls, Equals, 0) 1349 } 1350 1351 func (s *deviceMgrGadgetSuite) TestUpdateGadgetCommandlineNonUC20(c *C) { 1352 // arguments are ignored on non UC20 1353 s.state.Lock() 1354 s.setupModelWithGadget(c, "pc") 1355 devicestate.SetBootOkRan(s.mgr, true) 1356 s.state.Set("seeded", true) 1357 1358 // there is no modeenv either 1359 1360 s.state.Unlock() 1361 const update = false 1362 s.testGadgetCommandlineUpdateRun(c, 1363 [][]string{ 1364 {"meta/gadget.yaml", gadgetYaml}, 1365 // old gadget does not carry command line arguments 1366 }, 1367 [][]string{ 1368 {"meta/gadget.yaml", gadgetYaml}, 1369 {"cmdline.extra", "args from new gadget"}, 1370 }, 1371 "", "", update) 1372 } 1373 1374 func (s *deviceMgrGadgetSuite) TestGadgetCommandlineUpdateUndo(c *C) { 1375 restore := release.MockOnClassic(false) 1376 defer restore() 1377 1378 bootloader.Force(s.managedbl) 1379 s.state.Lock() 1380 s.setupUC20ModelWithGadget(c, "pc") 1381 s.mockModeenvForMode(c, "run") 1382 devicestate.SetBootOkRan(s.mgr, true) 1383 s.state.Set("seeded", true) 1384 1385 // mimic system state 1386 m, err := boot.ReadModeenv("") 1387 c.Assert(err, IsNil) 1388 m.CurrentKernelCommandLines = []string{ 1389 "snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1 args from old gadget", 1390 } 1391 c.Assert(m.Write(), IsNil) 1392 1393 err = s.managedbl.SetBootVars(map[string]string{ 1394 "snapd_extra_cmdline_args": "args from old gadget", 1395 }) 1396 c.Assert(err, IsNil) 1397 s.managedbl.SetBootVarsCalls = 0 1398 1399 currentSi := &snap.SideInfo{ 1400 RealName: "pc", 1401 Revision: snap.R(33), 1402 SnapID: "foo-id", 1403 } 1404 snapstate.Set(s.state, "pc", &snapstate.SnapState{ 1405 SnapType: "gadget", 1406 Sequence: []*snap.SideInfo{currentSi}, 1407 Current: currentSi.Revision, 1408 Active: true, 1409 }) 1410 snaptest.MockSnapWithFiles(c, pcGadgetSnapYaml, currentSi, [][]string{ 1411 {"meta/gadget.yaml", gadgetYaml}, 1412 {"cmdline.extra", "args from old gadget"}, 1413 }) 1414 updateSi := *currentSi 1415 updateSi.Revision = snap.R(34) 1416 snaptest.MockSnapWithFiles(c, pcGadgetSnapYaml, &updateSi, [][]string{ 1417 {"meta/gadget.yaml", gadgetYaml}, 1418 {"cmdline.extra", "args from new gadget"}, 1419 }) 1420 1421 tsk := s.state.NewTask("update-gadget-cmdline", "update gadget command line") 1422 tsk.Set("snap-setup", &snapstate.SnapSetup{ 1423 SideInfo: &updateSi, 1424 Type: snap.TypeGadget, 1425 }) 1426 terr := s.state.NewTask("error-trigger", "provoking total undo") 1427 terr.WaitFor(tsk) 1428 chg := s.state.NewChange("dummy", "...") 1429 chg.AddTask(tsk) 1430 chg.AddTask(terr) 1431 s.state.Unlock() 1432 1433 restartCount := 0 1434 s.restartObserve = func() { 1435 // we want to observe restarts and mangle modeenv like 1436 // devicemanager boot handling would do 1437 restartCount++ 1438 m, err := boot.ReadModeenv("") 1439 c.Assert(err, IsNil) 1440 switch restartCount { 1441 case 1: 1442 c.Check([]string(m.CurrentKernelCommandLines), DeepEquals, []string{ 1443 "snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1 args from old gadget", 1444 "snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1 args from new gadget", 1445 }) 1446 m.CurrentKernelCommandLines = []string{"snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1 args from new gadget"} 1447 case 2: 1448 c.Check([]string(m.CurrentKernelCommandLines), DeepEquals, []string{ 1449 "snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1 args from new gadget", 1450 "snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1 args from old gadget", 1451 }) 1452 m.CurrentKernelCommandLines = []string{"snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1 args from old gadget"} 1453 default: 1454 c.Fatalf("unexpected restart %v", restartCount) 1455 } 1456 c.Assert(m.Write(), IsNil) 1457 } 1458 1459 s.settle(c) 1460 1461 s.state.Lock() 1462 defer s.state.Unlock() 1463 1464 c.Assert(chg.IsReady(), Equals, true) 1465 c.Check(chg.Err(), ErrorMatches, "(?s)cannot perform the following tasks.*total undo.*") 1466 c.Check(tsk.Status(), Equals, state.UndoneStatus) 1467 log := tsk.Log() 1468 c.Assert(log, HasLen, 2) 1469 c.Check(log[0], Matches, ".* Updated kernel command line") 1470 c.Check(log[1], Matches, ".* Reverted kernel command line change") 1471 // update was applied and then undone 1472 c.Check(s.restartRequests, DeepEquals, []state.RestartType{state.RestartSystem, state.RestartSystem}) 1473 c.Check(restartCount, Equals, 2) 1474 vars, err := s.managedbl.GetBootVars("snapd_extra_cmdline_args") 1475 c.Assert(err, IsNil) 1476 c.Assert(vars, DeepEquals, map[string]string{ 1477 "snapd_extra_cmdline_args": "args from old gadget", 1478 }) 1479 // 2 calls, one to set the new arguments, and one to reset them back 1480 c.Check(s.managedbl.SetBootVarsCalls, Equals, 2) 1481 } 1482 1483 func (s *deviceMgrGadgetSuite) TestGadgetCommandlineUpdateNoChangeNoRebootsUndo(c *C) { 1484 restore := release.MockOnClassic(false) 1485 defer restore() 1486 1487 bootloader.Force(s.managedbl) 1488 s.state.Lock() 1489 s.setupUC20ModelWithGadget(c, "pc") 1490 s.mockModeenvForMode(c, "run") 1491 devicestate.SetBootOkRan(s.mgr, true) 1492 s.state.Set("seeded", true) 1493 1494 // mimic system state 1495 m, err := boot.ReadModeenv("") 1496 c.Assert(err, IsNil) 1497 m.CurrentKernelCommandLines = []string{ 1498 "snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1 args from gadget", 1499 } 1500 c.Assert(m.Write(), IsNil) 1501 1502 err = s.managedbl.SetBootVars(map[string]string{ 1503 "snapd_extra_cmdline_args": "args from gadget", 1504 }) 1505 c.Assert(err, IsNil) 1506 s.managedbl.SetBootVarsCalls = 0 1507 1508 currentSi := &snap.SideInfo{ 1509 RealName: "pc", 1510 Revision: snap.R(33), 1511 SnapID: "foo-id", 1512 } 1513 snapstate.Set(s.state, "pc", &snapstate.SnapState{ 1514 SnapType: "gadget", 1515 Sequence: []*snap.SideInfo{currentSi}, 1516 Current: currentSi.Revision, 1517 Active: true, 1518 }) 1519 sameFiles := [][]string{ 1520 {"meta/gadget.yaml", gadgetYaml}, 1521 {"cmdline.extra", "args from gadget"}, 1522 } 1523 snaptest.MockSnapWithFiles(c, pcGadgetSnapYaml, currentSi, sameFiles) 1524 updateSi := *currentSi 1525 updateSi.Revision = snap.R(34) 1526 // identical content, just a revision bump 1527 snaptest.MockSnapWithFiles(c, pcGadgetSnapYaml, &updateSi, sameFiles) 1528 1529 tsk := s.state.NewTask("update-gadget-cmdline", "update gadget command line") 1530 tsk.Set("snap-setup", &snapstate.SnapSetup{ 1531 SideInfo: &updateSi, 1532 Type: snap.TypeGadget, 1533 }) 1534 terr := s.state.NewTask("error-trigger", "provoking total undo") 1535 terr.WaitFor(tsk) 1536 chg := s.state.NewChange("dummy", "...") 1537 chg.AddTask(tsk) 1538 chg.AddTask(terr) 1539 s.state.Unlock() 1540 1541 s.settle(c) 1542 1543 s.state.Lock() 1544 defer s.state.Unlock() 1545 1546 c.Assert(chg.IsReady(), Equals, true) 1547 c.Check(chg.Err(), ErrorMatches, "(?s)cannot perform the following tasks.*total undo.*") 1548 c.Check(tsk.Status(), Equals, state.UndoneStatus) 1549 // there was nothing to update and thus nothing to undo 1550 c.Check(s.restartRequests, HasLen, 0) 1551 c.Check(s.managedbl.SetBootVarsCalls, Equals, 0) 1552 // modeenv wasn't changed 1553 m, err = boot.ReadModeenv("") 1554 c.Assert(err, IsNil) 1555 c.Check([]string(m.CurrentKernelCommandLines), DeepEquals, []string{ 1556 "snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1 args from gadget", 1557 }) 1558 } 1559 1560 func (s *deviceMgrGadgetSuite) TestUpdateGadgetCommandlineWithFullArgs(c *C) { 1561 bootloader.Force(s.managedbl) 1562 s.state.Lock() 1563 s.setupUC20ModelWithGadget(c, "pc") 1564 s.mockModeenvForMode(c, "run") 1565 devicestate.SetBootOkRan(s.mgr, true) 1566 s.state.Set("seeded", true) 1567 1568 // mimic system state 1569 m, err := boot.ReadModeenv("") 1570 c.Assert(err, IsNil) 1571 m.CurrentKernelCommandLines = []string{ 1572 "snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1 extra args", 1573 } 1574 c.Assert(m.Write(), IsNil) 1575 err = s.managedbl.SetBootVars(map[string]string{ 1576 "snapd_extra_cmdline_args": "extra args", 1577 "snapd_full_cmdline_args": "", 1578 }) 1579 c.Assert(err, IsNil) 1580 s.managedbl.SetBootVarsCalls = 0 1581 1582 s.state.Unlock() 1583 1584 const update = true 1585 s.testGadgetCommandlineUpdateRun(c, 1586 [][]string{ 1587 {"meta/gadget.yaml", gadgetYaml}, 1588 {"cmdline.extra", "extra args"}, 1589 }, 1590 [][]string{ 1591 {"meta/gadget.yaml", gadgetYaml}, 1592 {"cmdline.full", "full args"}, 1593 }, 1594 "", "Updated kernel command line", update) 1595 1596 m, err = boot.ReadModeenv("") 1597 c.Assert(err, IsNil) 1598 c.Check([]string(m.CurrentKernelCommandLines), DeepEquals, []string{ 1599 "snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1 extra args", 1600 // gadget arguments are picked up for the candidate command line 1601 "snapd_recovery_mode=run full args", 1602 }) 1603 c.Check(s.managedbl.SetBootVarsCalls, Equals, 1) 1604 vars, err := s.managedbl.GetBootVars("snapd_extra_cmdline_args", "snapd_full_cmdline_args") 1605 c.Assert(err, IsNil) 1606 // bootenv was cleared 1607 c.Assert(vars, DeepEquals, map[string]string{ 1608 "snapd_extra_cmdline_args": "", 1609 "snapd_full_cmdline_args": "full args", 1610 }) 1611 }