github.com/chipaca/snappy@v0.0.0-20210104084008-1f06296fe8ad/overlord/devicestate/firstboot20_test.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2019 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 "os" 24 "path/filepath" 25 "strings" 26 "time" 27 28 . "gopkg.in/check.v1" 29 30 "github.com/snapcore/snapd/asserts" 31 "github.com/snapcore/snapd/boot" 32 "github.com/snapcore/snapd/bootloader" 33 "github.com/snapcore/snapd/bootloader/bootloadertest" 34 "github.com/snapcore/snapd/dirs" 35 "github.com/snapcore/snapd/osutil" 36 "github.com/snapcore/snapd/overlord/devicestate" 37 "github.com/snapcore/snapd/overlord/ifacestate" 38 "github.com/snapcore/snapd/overlord/snapstate" 39 "github.com/snapcore/snapd/overlord/state" 40 "github.com/snapcore/snapd/seed/seedtest" 41 "github.com/snapcore/snapd/snap" 42 "github.com/snapcore/snapd/strutil" 43 "github.com/snapcore/snapd/systemd" 44 "github.com/snapcore/snapd/testutil" 45 ) 46 47 type firstBoot20Suite struct { 48 firstBootBaseTest 49 50 snapYaml map[string]string 51 52 // TestingSeed20 helps populating seeds (it provides 53 // MakeAssertedSnap, MakeSeed) for tests. 54 *seedtest.TestingSeed20 55 } 56 57 var ( 58 allGrades = []asserts.ModelGrade{ 59 asserts.ModelDangerous, 60 } 61 ) 62 63 var _ = Suite(&firstBoot20Suite{}) 64 65 func (s *firstBoot20Suite) SetUpTest(c *C) { 66 s.snapYaml = seedtest.SampleSnapYaml 67 68 s.TestingSeed20 = &seedtest.TestingSeed20{} 69 70 s.setupBaseTest(c, &s.TestingSeed20.SeedSnaps) 71 72 // don't start the overlord here so that we can mock different modeenvs 73 // later, which is needed by devicestart manager startup with uc20 booting 74 75 s.SeedDir = dirs.SnapSeedDir 76 77 // mock the snap mapper as snapd here 78 s.AddCleanup(ifacestate.MockSnapMapper(&ifacestate.CoreSnapdSystemMapper{})) 79 } 80 81 func (s *firstBoot20Suite) setupCore20Seed(c *C, sysLabel string, modelGrade asserts.ModelGrade, extraDevModeSnaps ...string) *asserts.Model { 82 gadgetYaml := ` 83 volumes: 84 volume-id: 85 bootloader: grub 86 structure: 87 - name: ubuntu-seed 88 role: system-seed 89 type: EF,C12A7328-F81F-11D2-BA4B-00A0C93EC93B 90 size: 1G 91 - name: ubuntu-data 92 role: system-data 93 type: 83,0FC63DAF-8483-4772-8E79-3D69D8477DE4 94 size: 2G 95 ` 96 97 makeSnap := func(yamlKey string) { 98 var files [][]string 99 if yamlKey == "pc=20" { 100 files = append(files, []string{"meta/gadget.yaml", gadgetYaml}) 101 } 102 s.MakeAssertedSnap(c, s.snapYaml[yamlKey], files, snap.R(1), "canonical", s.StoreSigning.Database) 103 } 104 105 makeSnap("snapd") 106 makeSnap("pc-kernel=20") 107 makeSnap("core20") 108 makeSnap("pc=20") 109 for _, sn := range extraDevModeSnaps { 110 makeSnap(sn) 111 } 112 113 model := map[string]interface{}{ 114 "display-name": "my model", 115 "architecture": "amd64", 116 "base": "core20", 117 "grade": string(modelGrade), 118 "snaps": []interface{}{ 119 map[string]interface{}{ 120 "name": "pc-kernel", 121 "id": s.AssertedSnapID("pc-kernel"), 122 "type": "kernel", 123 "default-channel": "20", 124 }, 125 map[string]interface{}{ 126 "name": "pc", 127 "id": s.AssertedSnapID("pc"), 128 "type": "gadget", 129 "default-channel": "20", 130 }, 131 map[string]interface{}{ 132 "name": "snapd", 133 "id": s.AssertedSnapID("snapd"), 134 "type": "snapd", 135 }, 136 map[string]interface{}{ 137 "name": "core20", 138 "id": s.AssertedSnapID("core20"), 139 "type": "base", 140 }, 141 }, 142 } 143 144 for _, sn := range extraDevModeSnaps { 145 name, channel := splitSnapNameWithChannel(sn) 146 model["snaps"] = append(model["snaps"].([]interface{}), map[string]interface{}{ 147 "name": name, 148 "type": "app", 149 "id": s.AssertedSnapID(name), 150 "default-channel": channel, 151 }) 152 } 153 154 return s.MakeSeed(c, sysLabel, "my-brand", "my-model", model, nil) 155 } 156 157 func splitSnapNameWithChannel(sn string) (name, channel string) { 158 nameParts := strings.SplitN(sn, "=", 2) 159 name = nameParts[0] 160 channel = "" 161 if len(nameParts) == 2 { 162 channel = nameParts[1] 163 } 164 return name, channel 165 } 166 167 func stripSnapNamesWithChannels(snaps []string) []string { 168 names := []string{} 169 for _, sn := range snaps { 170 name, _ := splitSnapNameWithChannel(sn) 171 names = append(names, name) 172 } 173 return names 174 } 175 176 func checkSnapstateDevModeFlags(c *C, tsAll []*state.TaskSet, snapsWithDevModeFlag ...string) { 177 allDevModeSnaps := stripSnapNamesWithChannels(snapsWithDevModeFlag) 178 179 // XXX: mostly same code from checkOrder helper in firstboot_test.go, maybe 180 // combine someday? 181 matched := 0 182 var prevTask *state.Task 183 for i, ts := range tsAll { 184 task0 := ts.Tasks()[0] 185 waitTasks := task0.WaitTasks() 186 if i == 0 { 187 c.Check(waitTasks, HasLen, 0) 188 } else { 189 c.Check(waitTasks, testutil.Contains, prevTask) 190 } 191 prevTask = task0 192 if task0.Kind() != "prerequisites" { 193 continue 194 } 195 snapsup, err := snapstate.TaskSnapSetup(task0) 196 c.Assert(err, IsNil, Commentf("%#v", task0)) 197 if strutil.ListContains(allDevModeSnaps, snapsup.InstanceName()) { 198 c.Assert(snapsup.DevMode, Equals, true) 199 matched++ 200 } else { 201 // it should not have DevMode true 202 c.Assert(snapsup.DevMode, Equals, false) 203 } 204 } 205 c.Check(matched, Equals, len(snapsWithDevModeFlag)) 206 } 207 208 func (s *firstBoot20Suite) testPopulateFromSeedCore20Happy(c *C, m *boot.Modeenv, modelGrade asserts.ModelGrade, extraDevModeSnaps ...string) { 209 c.Assert(m, NotNil, Commentf("missing modeenv test data")) 210 err := m.WriteTo("") 211 c.Assert(err, IsNil) 212 213 // restart overlord to pick up the modeenv 214 s.startOverlord(c) 215 216 // XXX some things are not yet completely final/realistic 217 var sysdLog [][]string 218 systemctlRestorer := systemd.MockSystemctl(func(cmd ...string) ([]byte, error) { 219 sysdLog = append(sysdLog, cmd) 220 return []byte("ActiveState=inactive\n"), nil 221 }) 222 defer systemctlRestorer() 223 224 sysLabel := m.RecoverySystem 225 model := s.setupCore20Seed(c, sysLabel, modelGrade, extraDevModeSnaps...) 226 // sanity check that our returned model has the expected grade 227 c.Assert(model.Grade(), Equals, modelGrade) 228 229 bloader := bootloadertest.Mock("mock", c.MkDir()).WithExtractedRunKernelImage() 230 bootloader.Force(bloader) 231 defer bootloader.Force(nil) 232 233 // since we are in runmode, MakeBootable will already have run from install 234 // mode, and extracted the kernel assets for the kernel snap into the 235 // bootloader, so set the current kernel there 236 kernel, err := snap.ParsePlaceInfoFromSnapFileName("pc-kernel_1.snap") 237 c.Assert(err, IsNil) 238 r := bloader.SetEnabledKernel(kernel) 239 defer r() 240 241 opts := devicestate.PopulateStateFromSeedOptions{ 242 Label: sysLabel, 243 Mode: m.Mode, 244 } 245 246 // run the firstboot stuff 247 st := s.overlord.State() 248 st.Lock() 249 defer st.Unlock() 250 tsAll, err := devicestate.PopulateStateFromSeedImpl(st, &opts, s.perfTimings) 251 c.Assert(err, IsNil) 252 253 snaps := []string{"snapd", "pc-kernel", "core20", "pc"} 254 allDevModeSnaps := stripSnapNamesWithChannels(extraDevModeSnaps) 255 if len(extraDevModeSnaps) != 0 { 256 snaps = append(snaps, allDevModeSnaps...) 257 } 258 checkOrder(c, tsAll, snaps...) 259 260 // if the model is dangerous check that the devmode snaps in the model have 261 // the flag set in snapstate for DevMode confinement 262 // XXX: eventually we may need more complicated checks here and for 263 // non-dangerous models only specific snaps may have this flag set 264 if modelGrade == asserts.ModelDangerous { 265 checkSnapstateDevModeFlags(c, tsAll, allDevModeSnaps...) 266 } 267 268 // now run the change and check the result 269 // use the expected kind otherwise settle with start another one 270 chg := st.NewChange("seed", "run the populate from seed changes") 271 for _, ts := range tsAll { 272 chg.AddAll(ts) 273 } 274 c.Assert(st.Changes(), HasLen, 1) 275 276 c.Assert(chg.Err(), IsNil) 277 278 // avoid device reg 279 chg1 := st.NewChange("become-operational", "init device") 280 chg1.SetStatus(state.DoingStatus) 281 282 // run change until it wants to restart 283 st.Unlock() 284 err = s.overlord.Settle(settleTimeout) 285 st.Lock() 286 c.Assert(err, IsNil) 287 288 // at this point the system is "restarting", pretend the restart has 289 // happened 290 c.Assert(chg.Status(), Equals, state.DoingStatus) 291 state.MockRestarting(st, state.RestartUnset) 292 st.Unlock() 293 err = s.overlord.Settle(settleTimeout) 294 st.Lock() 295 c.Assert(err, IsNil) 296 c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("%s", chg.Err())) 297 298 // verify 299 f, err := os.Open(dirs.SnapStateFile) 300 c.Assert(err, IsNil) 301 state, err := state.ReadState(nil, f) 302 c.Assert(err, IsNil) 303 304 state.Lock() 305 defer state.Unlock() 306 // check snapd, core20, kernel, gadget 307 _, err = snapstate.CurrentInfo(state, "snapd") 308 c.Check(err, IsNil) 309 _, err = snapstate.CurrentInfo(state, "core20") 310 c.Check(err, IsNil) 311 _, err = snapstate.CurrentInfo(state, "pc-kernel") 312 c.Check(err, IsNil) 313 _, err = snapstate.CurrentInfo(state, "pc") 314 c.Check(err, IsNil) 315 316 // ensure required flag is set on all essential snaps 317 var snapst snapstate.SnapState 318 for _, reqName := range []string{"snapd", "core20", "pc-kernel", "pc"} { 319 err = snapstate.Get(state, reqName, &snapst) 320 c.Assert(err, IsNil) 321 c.Assert(snapst.Required, Equals, true, Commentf("required not set for %v", reqName)) 322 323 if m.Mode == "run" { 324 // also ensure that in run mode none of the snaps are installed as 325 // symlinks, they must be copied onto ubuntu-data 326 files, err := filepath.Glob(filepath.Join(dirs.SnapBlobDir, reqName+"_*.snap")) 327 c.Assert(err, IsNil) 328 c.Assert(files, HasLen, 1) 329 c.Assert(osutil.IsSymlink(files[0]), Equals, false) 330 } 331 } 332 333 // the right systemd commands were run 334 c.Check(sysdLog, testutil.DeepContains, []string{"start", "usr-lib-snapd.mount"}) 335 336 // and ensure state is now considered seeded 337 var seeded bool 338 err = state.Get("seeded", &seeded) 339 c.Assert(err, IsNil) 340 c.Check(seeded, Equals, true) 341 342 // check we set seed-time 343 var seedTime time.Time 344 err = state.Get("seed-time", &seedTime) 345 c.Assert(err, IsNil) 346 c.Check(seedTime.IsZero(), Equals, false) 347 348 // check that we removed recovery_system from modeenv 349 m2, err := boot.ReadModeenv("") 350 c.Assert(err, IsNil) 351 if m.Mode == "run" { 352 // recovery system is cleared in run mode 353 c.Assert(m2.RecoverySystem, Equals, "") 354 } else { 355 // but kept intact in other modes 356 c.Assert(m2.RecoverySystem, Equals, m.RecoverySystem) 357 } 358 c.Assert(m2.Base, Equals, m.Base) 359 c.Assert(m2.Mode, Equals, m.Mode) 360 // Note that we don't check CurrentKernels in the modeenv, even though in a 361 // real first boot that would also be set here, because setting that is done 362 // in the snapstate manager, not the devicestate manager 363 364 // check that the default device ctx has a Modeenv 365 dev, err := devicestate.DeviceCtx(s.overlord.State(), nil, nil) 366 c.Assert(err, IsNil) 367 c.Assert(dev.HasModeenv(), Equals, true) 368 369 // check that we marked the boot successful with bootstate20 methods, namely 370 // that we called SetNext, which since it was called on the kernel we 371 // already booted from, we should only have checked what the current kernel 372 // is 373 374 if m.Mode == "run" { 375 // only relevant in run mode 376 377 // the 3 calls here are : 378 // * 1 from MarkBootSuccessful() from ensureBootOk() before we restart 379 // * 1 from boot.SetNextBoot() from LinkSnap() from doInstall() from InstallPath() from 380 // installSeedSnap() after restart 381 // * 1 from boot.GetCurrentBoot() from FinishRestart after restart 382 _, numKernelCalls := bloader.GetRunKernelImageFunctionSnapCalls("Kernel") 383 c.Assert(numKernelCalls, Equals, 3) 384 } 385 actual, _ := bloader.GetRunKernelImageFunctionSnapCalls("EnableKernel") 386 c.Assert(actual, HasLen, 0) 387 actual, _ = bloader.GetRunKernelImageFunctionSnapCalls("DisableTryKernel") 388 c.Assert(actual, HasLen, 0) 389 actual, _ = bloader.GetRunKernelImageFunctionSnapCalls("EnableTryKernel") 390 c.Assert(actual, HasLen, 0) 391 392 var whatseeded []devicestate.SeededSystem 393 err = state.Get("seeded-systems", &whatseeded) 394 if m.Mode == "run" { 395 c.Assert(err, IsNil) 396 c.Assert(whatseeded, DeepEquals, []devicestate.SeededSystem{{ 397 System: m.RecoverySystem, 398 Model: "my-model", 399 BrandID: "my-brand", 400 Revision: model.Revision(), 401 Timestamp: model.Timestamp(), 402 SeedTime: seedTime, 403 }}) 404 } else { 405 c.Assert(err, NotNil) 406 } 407 } 408 409 func (s *firstBoot20Suite) TestPopulateFromSeedCore20RunModeDangerousWithDevmode(c *C) { 410 m := boot.Modeenv{ 411 Mode: "run", 412 RecoverySystem: "20191018", 413 Base: "core20_1.snap", 414 } 415 s.testPopulateFromSeedCore20Happy(c, &m, asserts.ModelDangerous, "test-devmode=20") 416 } 417 418 func (s *firstBoot20Suite) TestPopulateFromSeedCore20RunMode(c *C) { 419 m := boot.Modeenv{ 420 Mode: "run", 421 RecoverySystem: "20191018", 422 Base: "core20_1.snap", 423 } 424 for _, grade := range allGrades { 425 s.testPopulateFromSeedCore20Happy(c, &m, grade) 426 } 427 } 428 429 func (s *firstBoot20Suite) TestPopulateFromSeedCore20InstallMode(c *C) { 430 m := boot.Modeenv{ 431 Mode: "install", 432 RecoverySystem: "20191019", 433 Base: "core20_1.snap", 434 } 435 for _, grade := range allGrades { 436 s.testPopulateFromSeedCore20Happy(c, &m, grade) 437 } 438 } 439 440 func (s *firstBoot20Suite) TestPopulateFromSeedCore20RecoverMode(c *C) { 441 m := boot.Modeenv{ 442 Mode: "recover", 443 RecoverySystem: "20191020", 444 Base: "core20_1.snap", 445 } 446 for _, grade := range allGrades { 447 s.testPopulateFromSeedCore20Happy(c, &m, grade) 448 } 449 }