gitee.com/mysnapcore/mysnapd@v0.1.0/boot/flags_test.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2022 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 boot_test 21 22 import ( 23 "errors" 24 "io/ioutil" 25 "os" 26 "path/filepath" 27 28 . "gopkg.in/check.v1" 29 30 "gitee.com/mysnapcore/mysnapd/boot" 31 "gitee.com/mysnapcore/mysnapd/boot/boottest" 32 "gitee.com/mysnapcore/mysnapd/bootloader" 33 "gitee.com/mysnapcore/mysnapd/bootloader/grubenv" 34 "gitee.com/mysnapcore/mysnapd/dirs" 35 "gitee.com/mysnapcore/mysnapd/snap" 36 "gitee.com/mysnapcore/mysnapd/testutil" 37 ) 38 39 type bootFlagsSuite struct { 40 baseBootenvSuite 41 } 42 43 var _ = Suite(&bootFlagsSuite{}) 44 45 func (s *bootFlagsSuite) TestBootFlagsFamilyClassic(c *C) { 46 classicDev := boottest.MockDevice("") 47 48 // make bootloader.Find fail but shouldn't matter 49 bootloader.ForceError(errors.New("broken bootloader")) 50 defer bootloader.ForceError(nil) 51 52 _, err := boot.NextBootFlags(classicDev) 53 c.Assert(err, ErrorMatches, `cannot get boot flags on pre-UC20 device`) 54 55 err = boot.SetNextBootFlags(classicDev, "", []string{"foo"}) 56 c.Assert(err, ErrorMatches, `cannot get boot flags on pre-UC20 device`) 57 58 _, err = boot.BootFlags(classicDev) 59 c.Assert(err, ErrorMatches, `cannot get boot flags on pre-UC20 device`) 60 } 61 62 func (s *bootFlagsSuite) TestBootFlagsFamilyUC16(c *C) { 63 coreDev := boottest.MockDevice("some-snap") 64 65 // make bootloader.Find fail but shouldn't matter 66 bootloader.ForceError(errors.New("broken bootloader")) 67 defer bootloader.ForceError(nil) 68 69 _, err := boot.NextBootFlags(coreDev) 70 c.Assert(err, ErrorMatches, `cannot get boot flags on pre-UC20 device`) 71 72 err = boot.SetNextBootFlags(coreDev, "", []string{"foo"}) 73 c.Assert(err, ErrorMatches, `cannot get boot flags on pre-UC20 device`) 74 75 _, err = boot.BootFlags(coreDev) 76 c.Assert(err, ErrorMatches, `cannot get boot flags on pre-UC20 device`) 77 } 78 79 func setupRealGrub(c *C, rootDir, baseDir string, opts *bootloader.Options) bootloader.Bootloader { 80 if rootDir == "" { 81 rootDir = dirs.GlobalRootDir 82 } 83 grubCfg := filepath.Join(rootDir, baseDir, "grub.cfg") 84 err := os.MkdirAll(filepath.Dir(grubCfg), 0755) 85 c.Assert(err, IsNil) 86 87 err = ioutil.WriteFile(grubCfg, nil, 0644) 88 c.Assert(err, IsNil) 89 90 genv := grubenv.NewEnv(filepath.Join(rootDir, baseDir, "grubenv")) 91 err = genv.Save() 92 c.Assert(err, IsNil) 93 94 grubBl, err := bootloader.Find(rootDir, opts) 95 c.Assert(err, IsNil) 96 c.Assert(grubBl.Name(), Equals, "grub") 97 98 return grubBl 99 } 100 101 func (s *bootFlagsSuite) TestInitramfsActiveBootFlagsUC20InstallModeHappy(c *C) { 102 dir := c.MkDir() 103 104 dirs.SetRootDir(dir) 105 defer func() { dirs.SetRootDir("") }() 106 107 blDir := boot.InitramfsUbuntuSeedDir 108 109 setupRealGrub(c, blDir, "EFI/ubuntu", &bootloader.Options{Role: bootloader.RoleRecovery}) 110 111 flags, err := boot.InitramfsActiveBootFlags(boot.ModeInstall, filepath.Join(dirs.GlobalRootDir, "/run/mnt/data/system-data")) 112 c.Assert(err, IsNil) 113 c.Assert(flags, HasLen, 0) 114 115 // if we set some flags via ubuntu-image customizations then we get them 116 // back 117 118 err = boot.SetBootFlagsInBootloader([]string{"factory"}, blDir) 119 c.Assert(err, IsNil) 120 121 flags, err = boot.InitramfsActiveBootFlags(boot.ModeInstall, filepath.Join(dirs.GlobalRootDir, "/run/mnt/data/system-data")) 122 c.Assert(err, IsNil) 123 c.Assert(flags, DeepEquals, []string{"factory"}) 124 } 125 126 func (s *bootFlagsSuite) TestInitramfsActiveBootFlagsUC20FactoryResetModeHappy(c *C) { 127 // FactoryReset and Install run identical code, as their condition match pretty closely 128 // so this unit test is to reconfirm that we expect same behavior as we see in the unit 129 // test for install mode. 130 dir := c.MkDir() 131 132 dirs.SetRootDir(dir) 133 defer func() { dirs.SetRootDir("") }() 134 135 blDir := boot.InitramfsUbuntuSeedDir 136 137 setupRealGrub(c, blDir, "EFI/ubuntu", &bootloader.Options{Role: bootloader.RoleRecovery}) 138 139 flags, err := boot.InitramfsActiveBootFlags(boot.ModeFactoryReset, filepath.Join(dirs.GlobalRootDir, "/run/mnt/data/system-data")) 140 c.Assert(err, IsNil) 141 c.Assert(flags, HasLen, 0) 142 143 // if we set some flags via ubuntu-image customizations then we get them 144 // back 145 146 err = boot.SetBootFlagsInBootloader([]string{"factory"}, blDir) 147 c.Assert(err, IsNil) 148 149 flags, err = boot.InitramfsActiveBootFlags(boot.ModeFactoryReset, filepath.Join(dirs.GlobalRootDir, "/run/mnt/data/system-data")) 150 c.Assert(err, IsNil) 151 c.Assert(flags, DeepEquals, []string{"factory"}) 152 } 153 154 func (s *bootFlagsSuite) TestSetImageBootFlagsVerification(c *C) { 155 longVal := "longer-than-256-char-value" 156 for i := 0; i < 256; i++ { 157 longVal += "X" 158 } 159 160 r := boot.MockAdditionalBootFlags([]string{longVal}) 161 defer r() 162 163 blVars := make(map[string]string) 164 165 err := boot.SetImageBootFlags([]string{"not-a-real-flag"}, blVars) 166 c.Assert(err, ErrorMatches, `unknown boot flags \[not-a-real-flag\] not allowed`) 167 168 err = boot.SetImageBootFlags([]string{longVal}, blVars) 169 c.Assert(err, ErrorMatches, "internal error: boot flags too large to fit inside bootenv value") 170 } 171 172 func (s *bootFlagsSuite) TestInitramfsActiveBootFlagsUC20RecoverModeNoop(c *C) { 173 dir := c.MkDir() 174 175 dirs.SetRootDir(dir) 176 defer func() { dirs.SetRootDir("") }() 177 178 blDir := boot.InitramfsUbuntuSeedDir 179 180 // create a grubenv to ensure that we don't return any values from there 181 grubBl := setupRealGrub(c, blDir, "EFI/ubuntu", &bootloader.Options{Role: bootloader.RoleRecovery}) 182 183 // also create the modeenv to make sure we don't peek there either 184 m := boot.Modeenv{ 185 Mode: boot.ModeRun, 186 BootFlags: []string{}, 187 } 188 189 err := os.MkdirAll(filepath.Join(dirs.GlobalRootDir, "/run/mnt/data/system-data"), 0755) 190 c.Assert(err, IsNil) 191 192 err = m.WriteTo(filepath.Join(dirs.GlobalRootDir, "/run/mnt/data/system-data")) 193 c.Assert(err, IsNil) 194 195 flags, err := boot.InitramfsActiveBootFlags(boot.ModeRecover, filepath.Join(dirs.GlobalRootDir, "/run/mnt/data/system-data")) 196 c.Assert(err, IsNil) 197 c.Assert(flags, HasLen, 0) 198 199 err = grubBl.SetBootVars(map[string]string{"snapd_boot_flags": "factory"}) 200 c.Assert(err, IsNil) 201 202 m.BootFlags = []string{"modeenv-boot-flag"} 203 err = m.WriteTo(filepath.Join(dirs.GlobalRootDir, "/run/mnt/data/system-data")) 204 c.Assert(err, IsNil) 205 206 // still no flags since we are in recovery mode 207 flags, err = boot.InitramfsActiveBootFlags(boot.ModeRecover, filepath.Join(dirs.GlobalRootDir, "/run/mnt/data/system-data")) 208 c.Assert(err, IsNil) 209 c.Assert(flags, HasLen, 0) 210 } 211 212 func (s *bootFlagsSuite) testInitramfsActiveBootFlagsUC20RRunModeHappy(c *C, flagsDir string) { 213 dir := c.MkDir() 214 215 dirs.SetRootDir(dir) 216 defer func() { dirs.SetRootDir("") }() 217 218 // setup a basic empty modeenv 219 m := boot.Modeenv{ 220 Mode: boot.ModeRun, 221 BootFlags: []string{}, 222 } 223 224 err := os.MkdirAll(flagsDir, 0755) 225 c.Assert(err, IsNil) 226 227 err = m.WriteTo(flagsDir) 228 c.Assert(err, IsNil) 229 230 flags, err := boot.InitramfsActiveBootFlags(boot.ModeRun, flagsDir) 231 c.Assert(err, IsNil) 232 c.Assert(flags, HasLen, 0) 233 234 m.BootFlags = []string{"factory", "other-flag"} 235 err = m.WriteTo(flagsDir) 236 c.Assert(err, IsNil) 237 238 // now some flags after we set them in the modeenv 239 flags, err = boot.InitramfsActiveBootFlags(boot.ModeRun, flagsDir) 240 c.Assert(err, IsNil) 241 c.Assert(flags, DeepEquals, []string{"factory", "other-flag"}) 242 } 243 244 func (s *bootFlagsSuite) TestInitramfsActiveBootFlagsUC20RRunModeHappy(c *C) { 245 s.testInitramfsActiveBootFlagsUC20RRunModeHappy(c, filepath.Join(dirs.GlobalRootDir, "/run/mnt/data/system-data")) 246 s.testInitramfsActiveBootFlagsUC20RRunModeHappy(c, c.MkDir()) 247 } 248 249 func (s *bootFlagsSuite) TestInitramfsSetBootFlags(c *C) { 250 tt := []struct { 251 flags []string 252 expFlags []string 253 expFlagFile string 254 bootFlagsErr string 255 bootFlagsErrUnknown bool 256 }{ 257 { 258 flags: []string{"factory"}, 259 expFlags: []string{"factory"}, 260 expFlagFile: "factory", 261 }, 262 { 263 flags: []string{"factory", "unknown-new-flag"}, 264 expFlagFile: "factory,unknown-new-flag", 265 expFlags: []string{"factory"}, 266 bootFlagsErr: `unknown boot flags \[unknown-new-flag\] not allowed`, 267 bootFlagsErrUnknown: true, 268 }, 269 { 270 flags: []string{"", "", "", "factory"}, 271 expFlags: []string{"factory"}, 272 expFlagFile: "factory", 273 }, 274 { 275 flags: []string{}, 276 expFlags: []string{}, 277 }, 278 } 279 280 uc20Dev := boottest.MockUC20Device("run", nil) 281 282 for _, t := range tt { 283 err := boot.InitramfsExposeBootFlagsForSystem(t.flags) 284 c.Assert(err, IsNil) 285 c.Assert(filepath.Join(dirs.SnapRunDir, "boot-flags"), testutil.FileEquals, t.expFlagFile) 286 287 // also read the flags as if from user space to make sure they match 288 flags, err := boot.BootFlags(uc20Dev) 289 if t.bootFlagsErr != "" { 290 c.Assert(err, ErrorMatches, t.bootFlagsErr) 291 if t.bootFlagsErrUnknown { 292 c.Assert(boot.IsUnknownBootFlagError(err), Equals, true) 293 } 294 } else { 295 c.Assert(err, IsNil) 296 } 297 c.Assert(flags, DeepEquals, t.expFlags) 298 } 299 } 300 301 func (s *bootFlagsSuite) TestUserspaceBootFlagsUC20(c *C) { 302 tt := []struct { 303 beforeFlags []string 304 flags []string 305 expFlags []string 306 err string 307 }{ 308 { 309 beforeFlags: []string{}, 310 flags: []string{"factory"}, 311 expFlags: []string{"factory"}, 312 }, 313 { 314 flags: []string{"factory", "new-unsupported-flag"}, 315 err: `unknown boot flags \[new-unsupported-flag\] not allowed`, 316 }, 317 { 318 flags: []string{""}, 319 err: `unknown boot flags \[\"\"\] not allowed`, 320 }, 321 { 322 beforeFlags: []string{}, 323 flags: []string{}, 324 }, 325 { 326 beforeFlags: []string{"factory"}, 327 flags: []string{}, 328 }, 329 { 330 beforeFlags: []string{"foobar"}, 331 flags: []string{"factory"}, 332 expFlags: []string{"factory"}, 333 }, 334 } 335 336 uc20Dev := boottest.MockUC20Device("run", nil) 337 338 m := boot.Modeenv{ 339 Mode: boot.ModeInstall, 340 BootFlags: []string{}, 341 } 342 343 for _, t := range tt { 344 m.BootFlags = t.beforeFlags 345 err := m.WriteTo("") 346 c.Assert(err, IsNil) 347 348 err = boot.SetNextBootFlags(uc20Dev, "", t.flags) 349 if t.err != "" { 350 c.Assert(err, ErrorMatches, t.err) 351 continue 352 } 353 354 c.Assert(err, IsNil) 355 356 // re-read modeenv 357 m2, err := boot.ReadModeenv("") 358 c.Assert(err, IsNil) 359 c.Assert(m2.BootFlags, DeepEquals, t.expFlags) 360 361 // get the next boot flags with NextBootFlags and compare with expected 362 flags, err := boot.NextBootFlags(uc20Dev) 363 c.Assert(flags, DeepEquals, t.expFlags) 364 c.Assert(err, IsNil) 365 } 366 } 367 368 func (s *bootFlagsSuite) TestRunModeRootfs(c *C) { 369 uc20Dev := boottest.MockUC20Device("run", nil) 370 classicModesDev := boottest.MockClassicWithModesDevice("run", nil) 371 372 tt := []struct { 373 mode string 374 dev snap.Device 375 createExpDirs bool 376 expDirs []string 377 noExpDirRootPrefix bool 378 degradedJSON string 379 err string 380 comment string 381 }{ 382 { 383 mode: boot.ModeRun, 384 dev: uc20Dev, 385 expDirs: []string{"/run/mnt/data", ""}, 386 comment: "run mode", 387 }, 388 { 389 mode: boot.ModeRun, 390 dev: classicModesDev, 391 expDirs: []string{"/run/mnt/data", ""}, 392 comment: "run mode (classic)", 393 }, 394 { 395 mode: boot.ModeInstall, 396 dev: uc20Dev, 397 comment: "install mode before partition creation", 398 }, 399 { 400 mode: boot.ModeInstall, 401 dev: classicModesDev, 402 comment: "install mode before partition creation (classic)", 403 }, 404 { 405 mode: boot.ModeFactoryReset, 406 dev: uc20Dev, 407 comment: "factory-reset mode before partition is recreated", 408 }, 409 { 410 mode: boot.ModeFactoryReset, 411 dev: uc20Dev, 412 comment: "factory-reset mode before partition is recreated (classic)", 413 }, 414 { 415 mode: boot.ModeInstall, 416 dev: uc20Dev, 417 expDirs: []string{"/run/mnt/ubuntu-data"}, 418 createExpDirs: true, 419 comment: "install mode after partition creation", 420 }, 421 { 422 mode: boot.ModeInstall, 423 dev: classicModesDev, 424 expDirs: []string{"/run/mnt/ubuntu-data"}, 425 createExpDirs: true, 426 comment: "install mode after partition creation (classic)", 427 }, 428 { 429 mode: boot.ModeFactoryReset, 430 dev: uc20Dev, 431 expDirs: []string{"/run/mnt/ubuntu-data"}, 432 createExpDirs: true, 433 comment: "factory-reset mode after partition creation", 434 }, 435 { 436 mode: boot.ModeFactoryReset, 437 dev: classicModesDev, 438 expDirs: []string{"/run/mnt/ubuntu-data"}, 439 createExpDirs: true, 440 comment: "factory-reset mode after partition creation (classic)", 441 }, 442 { 443 mode: boot.ModeRecover, 444 dev: uc20Dev, 445 degradedJSON: ` 446 { 447 "ubuntu-data": { 448 "mount-state": "mounted", 449 "mount-location": "/host/ubuntu-data" 450 } 451 } 452 `, 453 expDirs: []string{"/host/ubuntu-data"}, 454 noExpDirRootPrefix: true, 455 comment: "recover degraded.json default mounted location", 456 }, 457 { 458 mode: boot.ModeRecover, 459 dev: uc20Dev, 460 degradedJSON: ` 461 { 462 "ubuntu-data": { 463 "mount-state": "mounted", 464 "mount-location": "/host/elsewhere/ubuntu-data" 465 } 466 } 467 `, 468 expDirs: []string{"/host/elsewhere/ubuntu-data"}, 469 noExpDirRootPrefix: true, 470 comment: "recover degraded.json alternative mounted location", 471 }, 472 { 473 mode: boot.ModeRecover, 474 dev: uc20Dev, 475 degradedJSON: ` 476 { 477 "ubuntu-data": { 478 "mount-state": "error-mounting" 479 } 480 } 481 `, 482 comment: "recover degraded.json error-mounting", 483 }, 484 { 485 mode: boot.ModeRecover, 486 dev: uc20Dev, 487 degradedJSON: ` 488 { 489 "ubuntu-data": { 490 "mount-state": "mounted-untrusted" 491 } 492 } 493 `, 494 comment: "recover degraded.json mounted-untrusted", 495 }, 496 { 497 mode: boot.ModeRecover, 498 dev: uc20Dev, 499 degradedJSON: ` 500 { 501 "ubuntu-data": { 502 "mount-state": "absent-but-optional" 503 } 504 } 505 `, 506 comment: "recover degraded.json absent-but-optional", 507 }, 508 { 509 mode: boot.ModeRecover, 510 dev: uc20Dev, 511 degradedJSON: ` 512 { 513 "ubuntu-data": { 514 "mount-state": "new-wild-unknown-state" 515 } 516 } 517 `, 518 comment: "recover degraded.json new-wild-unknown-state", 519 }, 520 { 521 mode: "", 522 dev: uc20Dev, 523 err: "system mode is unsupported", 524 comment: "unsupported system mode", 525 }, 526 } 527 for _, t := range tt { 528 comment := Commentf(t.comment) 529 if t.degradedJSON != "" { 530 rootdir := c.MkDir() 531 dirs.SetRootDir(rootdir) 532 defer func() { dirs.SetRootDir("") }() 533 534 degradedJSON := filepath.Join(dirs.SnapBootstrapRunDir, "degraded.json") 535 err := os.MkdirAll(dirs.SnapBootstrapRunDir, 0755) 536 c.Assert(err, IsNil, comment) 537 538 err = ioutil.WriteFile(degradedJSON, []byte(t.degradedJSON), 0644) 539 c.Assert(err, IsNil, comment) 540 } 541 542 if t.createExpDirs { 543 for _, dir := range t.expDirs { 544 err := os.MkdirAll(filepath.Join(dirs.GlobalRootDir, dir), 0755) 545 c.Assert(err, IsNil, comment) 546 } 547 } 548 549 dataMountDirs, err := boot.HostUbuntuDataForMode(t.mode, t.dev.Model()) 550 if t.err != "" { 551 c.Assert(err, ErrorMatches, t.err, comment) 552 c.Assert(dataMountDirs, IsNil) 553 continue 554 } 555 c.Assert(err, IsNil, comment) 556 557 if t.expDirs != nil && !t.noExpDirRootPrefix { 558 // prefix all the dirs in expDirs with dirs.GlobalRootDir for easier 559 // test case writing above 560 prefixedDir := make([]string, len(t.expDirs)) 561 for i, dir := range t.expDirs { 562 prefixedDir[i] = filepath.Join(dirs.GlobalRootDir, dir) 563 } 564 c.Assert(dataMountDirs, DeepEquals, prefixedDir, comment) 565 } else { 566 c.Assert(dataMountDirs, DeepEquals, t.expDirs, comment) 567 } 568 } 569 }