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  }