github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/overlord/devicestate/systems_test.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2021 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  	"bytes"
    24  	"fmt"
    25  	"io/ioutil"
    26  	"os"
    27  	"path/filepath"
    28  	"sort"
    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/logger"
    38  	"github.com/snapcore/snapd/osutil"
    39  	"github.com/snapcore/snapd/overlord/devicestate"
    40  	"github.com/snapcore/snapd/seed/seedtest"
    41  	"github.com/snapcore/snapd/snap"
    42  	"github.com/snapcore/snapd/snap/snaptest"
    43  	"github.com/snapcore/snapd/testutil"
    44  )
    45  
    46  type createSystemSuite struct {
    47  	deviceMgrBaseSuite
    48  
    49  	ss *seedtest.SeedSnaps
    50  
    51  	logbuf *bytes.Buffer
    52  }
    53  
    54  var _ = Suite(&createSystemSuite{})
    55  
    56  var (
    57  	genericSnapYaml = "name: %s\nversion: 1.0\n%s"
    58  	snapYamls       = map[string]string{
    59  		"pc-kernel":        "name: pc-kernel\nversion: 1.0\ntype: kernel",
    60  		"pc":               "name: pc\nversion: 1.0\ntype: gadget\nbase: core20",
    61  		"core20":           "name: core20\nversion: 20.1\ntype: base",
    62  		"core18":           "name: core18\nversion: 18.1\ntype: base",
    63  		"snapd":            "name: snapd\nversion: 2.2.2\ntype: snapd",
    64  		"other-required":   fmt.Sprintf(genericSnapYaml, "other-required", "base: core20"),
    65  		"other-present":    fmt.Sprintf(genericSnapYaml, "other-present", "base: core20"),
    66  		"other-core18":     fmt.Sprintf(genericSnapYaml, "other-present", "base: core18"),
    67  		"other-unasserted": fmt.Sprintf(genericSnapYaml, "other-unasserted", "base: core20"),
    68  	}
    69  	snapFiles = map[string][][]string{
    70  		"pc": {
    71  			{"meta/gadget.yaml", gadgetYaml},
    72  			{"cmdline.extra", "args from gadget"},
    73  		},
    74  	}
    75  )
    76  
    77  func (s *createSystemSuite) SetUpTest(c *C) {
    78  	s.deviceMgrBaseSuite.SetUpTest(c)
    79  
    80  	s.ss = &seedtest.SeedSnaps{
    81  		StoreSigning: s.storeSigning,
    82  		Brands:       s.brands,
    83  	}
    84  	s.AddCleanup(func() { bootloader.Force(nil) })
    85  
    86  	buf, restore := logger.MockLogger()
    87  	s.AddCleanup(restore)
    88  	s.logbuf = buf
    89  }
    90  
    91  func (s *createSystemSuite) makeSnap(c *C, name string, rev snap.Revision) *snap.Info {
    92  	snapID := s.ss.AssertedSnapID(name)
    93  	if rev.Unset() || rev.Local() {
    94  		snapID = ""
    95  	}
    96  	si := &snap.SideInfo{
    97  		RealName: name,
    98  		SnapID:   snapID,
    99  		Revision: rev,
   100  	}
   101  	where, info := snaptest.MakeTestSnapInfoWithFiles(c, snapYamls[name], snapFiles[name], si)
   102  	c.Assert(os.MkdirAll(filepath.Dir(info.MountFile()), 0755), IsNil)
   103  	c.Assert(os.Rename(where, info.MountFile()), IsNil)
   104  	if !rev.Unset() && !rev.Local() {
   105  		// snap is non local, generate relevant assertions
   106  		s.setupSnapDecl(c, info, "my-brand")
   107  		s.setupSnapRevision(c, info, "my-brand", rev)
   108  	}
   109  	return info
   110  }
   111  
   112  func (s *createSystemSuite) makeEssentialSnapInfos(c *C) map[string]*snap.Info {
   113  	infos := map[string]*snap.Info{}
   114  	infos["pc-kernel"] = s.makeSnap(c, "pc-kernel", snap.R(1))
   115  	infos["pc"] = s.makeSnap(c, "pc", snap.R(2))
   116  	infos["core20"] = s.makeSnap(c, "core20", snap.R(3))
   117  	infos["snapd"] = s.makeSnap(c, "snapd", snap.R(4))
   118  	return infos
   119  }
   120  
   121  func validateCore20Seed(c *C, name string, expectedModel *asserts.Model, trusted []asserts.Assertion, runModeSnapNames ...string) {
   122  	const usesSnapd = true
   123  	sd := seedtest.ValidateSeed(c, boot.InitramfsUbuntuSeedDir, name, usesSnapd, trusted)
   124  
   125  	snaps, err := sd.ModeSnaps(boot.ModeRun)
   126  	c.Assert(err, IsNil)
   127  	seenSnaps := []string{}
   128  	for _, sn := range snaps {
   129  		seenSnaps = append(seenSnaps, sn.SnapName())
   130  	}
   131  	sort.Strings(seenSnaps)
   132  	sort.Strings(runModeSnapNames)
   133  	if len(runModeSnapNames) != 0 {
   134  		c.Check(seenSnaps, DeepEquals, runModeSnapNames)
   135  	} else {
   136  		c.Check(seenSnaps, HasLen, 0)
   137  	}
   138  
   139  	c.Assert(sd.Model(), DeepEquals, expectedModel)
   140  }
   141  
   142  func (s *createSystemSuite) TestCreateSystemFromAssertedSnaps(c *C) {
   143  	bl := bootloadertest.Mock("trusted", c.MkDir()).WithRecoveryAwareTrustedAssets()
   144  	// make it simple for now, no assets
   145  	bl.TrustedAssetsList = nil
   146  	bl.StaticCommandLine = "mock static"
   147  	bl.CandidateStaticCommandLine = "unused"
   148  	bootloader.Force(bl)
   149  
   150  	s.state.Lock()
   151  	defer s.state.Unlock()
   152  	s.setupBrands(c)
   153  	infos := s.makeEssentialSnapInfos(c)
   154  	infos["other-present"] = s.makeSnap(c, "other-present", snap.R(5))
   155  	infos["other-required"] = s.makeSnap(c, "other-required", snap.R(6))
   156  	infos["other-core18"] = s.makeSnap(c, "other-core18", snap.R(7))
   157  	infos["core18"] = s.makeSnap(c, "core18", snap.R(8))
   158  
   159  	model := s.makeModelAssertionInState(c, "my-brand", "pc", map[string]interface{}{
   160  		"architecture": "amd64",
   161  		"grade":        "dangerous",
   162  		"base":         "core20",
   163  		"snaps": []interface{}{
   164  			map[string]interface{}{
   165  				"name":            "pc-kernel",
   166  				"id":              s.ss.AssertedSnapID("pc-kernel"),
   167  				"type":            "kernel",
   168  				"default-channel": "20",
   169  			},
   170  			map[string]interface{}{
   171  				"name":            "pc",
   172  				"id":              s.ss.AssertedSnapID("pc"),
   173  				"type":            "gadget",
   174  				"default-channel": "20",
   175  			},
   176  			map[string]interface{}{
   177  				"name": "snapd",
   178  				"id":   s.ss.AssertedSnapID("snapd"),
   179  				"type": "snapd",
   180  			},
   181  			// optional but not present
   182  			map[string]interface{}{
   183  				"name":     "other-not-present",
   184  				"id":       s.ss.AssertedSnapID("other-not-present"),
   185  				"presence": "optional",
   186  			},
   187  			// optional and present
   188  			map[string]interface{}{
   189  				"name":     "other-present",
   190  				"id":       s.ss.AssertedSnapID("other-present"),
   191  				"presence": "optional",
   192  			},
   193  			// required
   194  			map[string]interface{}{
   195  				"name":     "other-required",
   196  				"id":       s.ss.AssertedSnapID("other-required"),
   197  				"presence": "required",
   198  			},
   199  			// different base
   200  			map[string]interface{}{
   201  				"name": "other-core18",
   202  				"id":   s.ss.AssertedSnapID("other-core18"),
   203  			},
   204  			// and the actual base for that snap
   205  			map[string]interface{}{
   206  				"name": "core18",
   207  				"id":   s.ss.AssertedSnapID("core18"),
   208  				"type": "base",
   209  			},
   210  		},
   211  	})
   212  	expectedDir := filepath.Join(boot.InitramfsUbuntuSeedDir, "systems/1234")
   213  
   214  	infoGetter := func(name string) (*snap.Info, bool, error) {
   215  		c.Logf("called for: %q", name)
   216  		info, present := infos[name]
   217  		return info, present, nil
   218  	}
   219  	var newFiles []string
   220  	snapWriteObserver := func(dir, where string) error {
   221  		c.Check(dir, Equals, expectedDir)
   222  		c.Check(where, testutil.FileAbsent)
   223  		newFiles = append(newFiles, where)
   224  		return nil
   225  	}
   226  
   227  	dir, err := devicestate.CreateSystemForModelFromValidatedSnaps(model, "1234", s.db, infoGetter, snapWriteObserver)
   228  	c.Assert(err, IsNil)
   229  	c.Check(newFiles, DeepEquals, []string{
   230  		filepath.Join(boot.InitramfsUbuntuSeedDir, "snaps/snapd_4.snap"),
   231  		filepath.Join(boot.InitramfsUbuntuSeedDir, "snaps/pc-kernel_1.snap"),
   232  		filepath.Join(boot.InitramfsUbuntuSeedDir, "snaps/core20_3.snap"),
   233  		filepath.Join(boot.InitramfsUbuntuSeedDir, "snaps/pc_2.snap"),
   234  		filepath.Join(boot.InitramfsUbuntuSeedDir, "snaps/other-present_5.snap"),
   235  		filepath.Join(boot.InitramfsUbuntuSeedDir, "snaps/other-required_6.snap"),
   236  		filepath.Join(boot.InitramfsUbuntuSeedDir, "snaps/other-core18_7.snap"),
   237  		filepath.Join(boot.InitramfsUbuntuSeedDir, "snaps/core18_8.snap"),
   238  	})
   239  	c.Check(dir, Equals, expectedDir)
   240  	// naive check for files being present
   241  	for _, info := range infos {
   242  		c.Check(filepath.Join(boot.InitramfsUbuntuSeedDir, "snaps", filepath.Base(info.MountFile())),
   243  			testutil.FileEquals,
   244  			testutil.FileContentRef(info.MountFile()))
   245  	}
   246  	// recovery system bootenv was set
   247  	c.Check(bl.RecoverySystemDir, Equals, "/systems/1234")
   248  	c.Check(bl.RecoverySystemBootVars, DeepEquals, map[string]string{
   249  		"snapd_full_cmdline_args":  "",
   250  		"snapd_extra_cmdline_args": "args from gadget",
   251  		"snapd_recovery_kernel":    "/snaps/pc-kernel_1.snap",
   252  	})
   253  	// load the seed
   254  	validateCore20Seed(c, "1234", model, s.storeSigning.Trusted,
   255  		"other-core18", "core18", "other-present", "other-required")
   256  }
   257  
   258  func (s *createSystemSuite) TestCreateSystemFromUnassertedSnaps(c *C) {
   259  	bl := bootloadertest.Mock("trusted", c.MkDir()).WithRecoveryAwareTrustedAssets()
   260  	// make it simple for now, no assets
   261  	bl.TrustedAssetsList = nil
   262  	bl.StaticCommandLine = "mock static"
   263  	bl.CandidateStaticCommandLine = "unused"
   264  	bootloader.Force(bl)
   265  
   266  	s.state.Lock()
   267  	defer s.state.Unlock()
   268  	s.setupBrands(c)
   269  	infos := s.makeEssentialSnapInfos(c)
   270  	// unasserted with local revision
   271  	infos["other-unasserted"] = s.makeSnap(c, "other-unasserted", snap.R(-1))
   272  
   273  	model := s.makeModelAssertionInState(c, "my-brand", "pc", map[string]interface{}{
   274  		"architecture": "amd64",
   275  		"grade":        "dangerous",
   276  		"base":         "core20",
   277  		"snaps": []interface{}{
   278  			map[string]interface{}{
   279  				"name":            "pc-kernel",
   280  				"id":              s.ss.AssertedSnapID("pc-kernel"),
   281  				"type":            "kernel",
   282  				"default-channel": "20",
   283  			},
   284  			map[string]interface{}{
   285  				"name":            "pc",
   286  				"id":              s.ss.AssertedSnapID("pc"),
   287  				"type":            "gadget",
   288  				"default-channel": "20",
   289  			},
   290  			map[string]interface{}{
   291  				"name": "snapd",
   292  				"id":   s.ss.AssertedSnapID("snapd"),
   293  				"type": "snapd",
   294  			},
   295  			// required
   296  			map[string]interface{}{
   297  				"name":     "other-unasserted",
   298  				"presence": "required",
   299  			},
   300  		},
   301  	})
   302  	expectedDir := filepath.Join(boot.InitramfsUbuntuSeedDir, "systems/1234")
   303  
   304  	infoGetter := func(name string) (*snap.Info, bool, error) {
   305  		c.Logf("called for: %q", name)
   306  		info, present := infos[name]
   307  		return info, present, nil
   308  	}
   309  	var newFiles []string
   310  	snapWriteObserver := func(dir, where string) error {
   311  		c.Check(dir, Equals, expectedDir)
   312  		c.Check(where, testutil.FileAbsent)
   313  		newFiles = append(newFiles, where)
   314  		return nil
   315  	}
   316  
   317  	dir, err := devicestate.CreateSystemForModelFromValidatedSnaps(model, "1234", s.db, infoGetter, snapWriteObserver)
   318  	c.Assert(err, IsNil)
   319  	c.Check(newFiles, DeepEquals, []string{
   320  		filepath.Join(boot.InitramfsUbuntuSeedDir, "snaps/snapd_4.snap"),
   321  		filepath.Join(boot.InitramfsUbuntuSeedDir, "snaps/pc-kernel_1.snap"),
   322  		filepath.Join(boot.InitramfsUbuntuSeedDir, "snaps/core20_3.snap"),
   323  		filepath.Join(boot.InitramfsUbuntuSeedDir, "snaps/pc_2.snap"),
   324  		// this snap unasserted and lands under the system
   325  		filepath.Join(boot.InitramfsUbuntuSeedDir, "systems/1234/snaps/other-unasserted_1.0.snap"),
   326  	})
   327  	c.Check(dir, Equals, filepath.Join(boot.InitramfsUbuntuSeedDir, "systems/1234"))
   328  	// naive check for files being present
   329  	for _, info := range infos {
   330  		if info.Revision.Store() {
   331  			c.Check(filepath.Join(boot.InitramfsUbuntuSeedDir, "snaps", filepath.Base(info.MountFile())),
   332  				testutil.FileEquals,
   333  				testutil.FileContentRef(info.MountFile()))
   334  		} else {
   335  			fileName := fmt.Sprintf("%s_%s.snap", info.SnapName(), info.Version)
   336  			c.Check(filepath.Join(boot.InitramfsUbuntuSeedDir, "systems/1234/snaps", fileName),
   337  				testutil.FileEquals,
   338  				testutil.FileContentRef(info.MountFile()))
   339  		}
   340  	}
   341  	// load the seed
   342  	validateCore20Seed(c, "1234", model, s.storeSigning.Trusted, "other-unasserted")
   343  	// we have unasserted snaps, so a warning should have been logged
   344  	c.Check(s.logbuf.String(), testutil.Contains, `system "1234" contains unasserted snaps "other-unasserted"`)
   345  }
   346  
   347  func (s *createSystemSuite) TestCreateSystemWithSomeSnapsAlreadyExisting(c *C) {
   348  	bl := bootloadertest.Mock("trusted", c.MkDir()).WithRecoveryAwareTrustedAssets()
   349  	bootloader.Force(bl)
   350  
   351  	s.state.Lock()
   352  	defer s.state.Unlock()
   353  	s.setupBrands(c)
   354  	infos := s.makeEssentialSnapInfos(c)
   355  	model := s.makeModelAssertionInState(c, "my-brand", "pc", map[string]interface{}{
   356  		"architecture": "amd64",
   357  		"grade":        "dangerous",
   358  		"base":         "core20",
   359  		"snaps": []interface{}{
   360  			map[string]interface{}{
   361  				"name":            "pc-kernel",
   362  				"id":              s.ss.AssertedSnapID("pc-kernel"),
   363  				"type":            "kernel",
   364  				"default-channel": "20",
   365  			},
   366  			map[string]interface{}{
   367  				"name":            "pc",
   368  				"id":              s.ss.AssertedSnapID("pc"),
   369  				"type":            "gadget",
   370  				"default-channel": "20",
   371  			},
   372  			map[string]interface{}{
   373  				"name": "snapd",
   374  				"id":   s.ss.AssertedSnapID("snapd"),
   375  				"type": "snapd",
   376  			},
   377  		},
   378  	})
   379  	expectedDir := filepath.Join(boot.InitramfsUbuntuSeedDir, "systems/1234")
   380  
   381  	infoGetter := func(name string) (*snap.Info, bool, error) {
   382  		c.Logf("called for: %q", name)
   383  		info, present := infos[name]
   384  		return info, present, nil
   385  	}
   386  	var newFiles []string
   387  	snapWriteObserver := func(dir, where string) error {
   388  		c.Check(dir, Equals, expectedDir)
   389  		// we are not called for the snap which already exists
   390  		c.Check(where, testutil.FileAbsent)
   391  		newFiles = append(newFiles, where)
   392  		return nil
   393  	}
   394  
   395  	assertedSnapsDir := filepath.Join(boot.InitramfsUbuntuSeedDir, "snaps")
   396  	c.Assert(os.MkdirAll(assertedSnapsDir, 0755), IsNil)
   397  	// procure the file in place
   398  	err := osutil.CopyFile(infos["core20"].MountFile(), filepath.Join(assertedSnapsDir, "core20_3.snap"), 0)
   399  	c.Assert(err, IsNil)
   400  
   401  	// when a given snap in asserted snaps directory already exists, it is
   402  	// not copied over
   403  	dir, err := devicestate.CreateSystemForModelFromValidatedSnaps(model, "1234", s.db, infoGetter, snapWriteObserver)
   404  	c.Assert(err, IsNil)
   405  	c.Check(newFiles, DeepEquals, []string{
   406  		filepath.Join(boot.InitramfsUbuntuSeedDir, "snaps/snapd_4.snap"),
   407  		filepath.Join(boot.InitramfsUbuntuSeedDir, "snaps/pc-kernel_1.snap"),
   408  		filepath.Join(boot.InitramfsUbuntuSeedDir, "snaps/pc_2.snap"),
   409  	})
   410  	c.Check(dir, Equals, filepath.Join(boot.InitramfsUbuntuSeedDir, "systems/1234"))
   411  	// naive check for files being present
   412  	for _, info := range infos {
   413  		c.Check(filepath.Join(boot.InitramfsUbuntuSeedDir, "snaps", filepath.Base(info.MountFile())),
   414  			testutil.FileEquals,
   415  			testutil.FileContentRef(info.MountFile()))
   416  	}
   417  	// recovery system bootenv was set
   418  	c.Check(bl.RecoverySystemDir, Equals, "/systems/1234")
   419  	c.Check(bl.RecoverySystemBootVars, DeepEquals, map[string]string{
   420  		"snapd_full_cmdline_args":  "",
   421  		"snapd_extra_cmdline_args": "args from gadget",
   422  		"snapd_recovery_kernel":    "/snaps/pc-kernel_1.snap",
   423  	})
   424  	// load the seed
   425  	validateCore20Seed(c, "1234", model, s.storeSigning.Trusted)
   426  
   427  	// add an unasserted snap
   428  	infos["other-unasserted"] = s.makeSnap(c, "other-unasserted", snap.R(-1))
   429  	modelWithUnasserted := s.makeModelAssertionInState(c, "my-brand", "pc-with-unasserted", map[string]interface{}{
   430  		"architecture": "amd64",
   431  		"grade":        "dangerous",
   432  		"base":         "core20",
   433  		"snaps": []interface{}{
   434  			map[string]interface{}{
   435  				"name":            "pc-kernel",
   436  				"id":              s.ss.AssertedSnapID("pc-kernel"),
   437  				"type":            "kernel",
   438  				"default-channel": "20",
   439  			},
   440  			map[string]interface{}{
   441  				"name":            "pc",
   442  				"id":              s.ss.AssertedSnapID("pc"),
   443  				"type":            "gadget",
   444  				"default-channel": "20",
   445  			},
   446  			map[string]interface{}{
   447  				"name": "snapd",
   448  				"id":   s.ss.AssertedSnapID("snapd"),
   449  				"type": "snapd",
   450  			},
   451  			// required
   452  			map[string]interface{}{
   453  				"name":     "other-unasserted",
   454  				"presence": "required",
   455  			},
   456  		},
   457  	})
   458  
   459  	unassertedSnapsDir := filepath.Join(boot.InitramfsUbuntuSeedDir, "systems/1234unasserted/snaps")
   460  	c.Assert(os.MkdirAll(unassertedSnapsDir, 0755), IsNil)
   461  	err = osutil.CopyFile(infos["other-unasserted"].MountFile(),
   462  		filepath.Join(unassertedSnapsDir, "other-unasserted_1.0.snap"), 0)
   463  	c.Assert(err, IsNil)
   464  
   465  	newFiles = nil
   466  	// the unasserted snap goes into the snaps directory under the system
   467  	// directory, which triggers the error in creating the directory by
   468  	// seed writer
   469  	dir, err = devicestate.CreateSystemForModelFromValidatedSnaps(modelWithUnasserted, "1234unasserted", s.db,
   470  		infoGetter, snapWriteObserver)
   471  
   472  	c.Assert(err, ErrorMatches, `system "1234unasserted" already exists`)
   473  	// we failed early, no files were written yet
   474  	c.Check(dir, Equals, "")
   475  	c.Check(newFiles, IsNil)
   476  }
   477  
   478  func (s *createSystemSuite) TestCreateSystemInfoAndAssertsChecks(c *C) {
   479  	bl := bootloadertest.Mock("trusted", c.MkDir()).WithRecoveryAwareTrustedAssets()
   480  	bootloader.Force(bl)
   481  	infos := map[string]*snap.Info{}
   482  
   483  	s.state.Lock()
   484  	defer s.state.Unlock()
   485  	s.setupBrands(c)
   486  	// missing info for the pc snap
   487  	infos["pc-kernel"] = s.makeSnap(c, "pc-kernel", snap.R(1))
   488  	infos["core20"] = s.makeSnap(c, "core20", snap.R(3))
   489  	infos["snapd"] = s.makeSnap(c, "snapd", snap.R(4))
   490  	model := s.makeModelAssertionInState(c, "my-brand", "pc", map[string]interface{}{
   491  		"architecture": "amd64",
   492  		"grade":        "dangerous",
   493  		"base":         "core20",
   494  		"snaps": []interface{}{
   495  			map[string]interface{}{
   496  				"name":            "pc-kernel",
   497  				"id":              s.ss.AssertedSnapID("pc-kernel"),
   498  				"type":            "kernel",
   499  				"default-channel": "20",
   500  			},
   501  			// pc snap is the gadget, but we have no info for it
   502  			map[string]interface{}{
   503  				"name":            "pc",
   504  				"id":              s.ss.AssertedSnapID("pc"),
   505  				"type":            "gadget",
   506  				"default-channel": "20",
   507  			},
   508  			map[string]interface{}{
   509  				"name": "snapd",
   510  				"id":   s.ss.AssertedSnapID("snapd"),
   511  				"type": "snapd",
   512  			},
   513  			map[string]interface{}{
   514  				"name":     "other-required",
   515  				"id":       s.ss.AssertedSnapID("other-required"),
   516  				"presence": "required",
   517  			},
   518  		},
   519  	})
   520  
   521  	infoGetter := func(name string) (*snap.Info, bool, error) {
   522  		c.Logf("called for: %q", name)
   523  		info, present := infos[name]
   524  		return info, present, nil
   525  	}
   526  	var observerCalls int
   527  	snapWriteObserver := func(dir, where string) error {
   528  		observerCalls++
   529  		return fmt.Errorf("unexpected call")
   530  	}
   531  
   532  	systemDir := filepath.Join(boot.InitramfsUbuntuSeedDir, "systems/1234")
   533  
   534  	// when a given snap in asserted snaps directory already exists, it is
   535  	// not copied over
   536  	dir, err := devicestate.CreateSystemForModelFromValidatedSnaps(model, "1234", s.db,
   537  		infoGetter, snapWriteObserver)
   538  	c.Assert(err, ErrorMatches, `internal error: essential snap "pc" not present`)
   539  	c.Check(dir, Equals, "")
   540  	c.Check(observerCalls, Equals, 0)
   541  
   542  	// the directory shouldn't be there, as we haven't written anything yet
   543  	c.Check(osutil.IsDirectory(systemDir), Equals, false)
   544  
   545  	// create the info now
   546  	infos["pc"] = s.makeSnap(c, "pc", snap.R(2))
   547  
   548  	// and try with with a non essential snap
   549  	dir, err = devicestate.CreateSystemForModelFromValidatedSnaps(model, "1234", s.db,
   550  		infoGetter, snapWriteObserver)
   551  	c.Assert(err, ErrorMatches, `internal error: non-essential but required snap "other-required" not present`)
   552  	c.Check(dir, Equals, "")
   553  	c.Check(observerCalls, Equals, 0)
   554  	// the directory shouldn't be there, as we haven't written anything yet
   555  	c.Check(osutil.IsDirectory(systemDir), Equals, false)
   556  
   557  	// create the info now
   558  	infos["other-required"] = s.makeSnap(c, "other-required", snap.R(5))
   559  
   560  	// but change the file contents of 'pc' snap so that deriving side info fails
   561  	c.Assert(ioutil.WriteFile(infos["pc"].MountFile(), []byte("canary"), 0644), IsNil)
   562  	dir, err = devicestate.CreateSystemForModelFromValidatedSnaps(model, "1234", s.db,
   563  		infoGetter, snapWriteObserver)
   564  	c.Assert(err, ErrorMatches, `internal error: no assertions for asserted snap with ID: pcididididididididididididididid`)
   565  	// we're past the start, so the system directory is there
   566  	c.Check(dir, Equals, systemDir)
   567  	c.Check(osutil.IsDirectory(systemDir), Equals, true)
   568  	// but no files were copied
   569  	c.Check(observerCalls, Equals, 0)
   570  }
   571  
   572  func (s *createSystemSuite) TestCreateSystemGetInfoErr(c *C) {
   573  	bl := bootloadertest.Mock("trusted", c.MkDir()).WithRecoveryAwareTrustedAssets()
   574  	bootloader.Force(bl)
   575  
   576  	s.state.Lock()
   577  	defer s.state.Unlock()
   578  	s.setupBrands(c)
   579  	// missing info for the pc snap
   580  	infos := s.makeEssentialSnapInfos(c)
   581  	infos["other-required"] = s.makeSnap(c, "other-required", snap.R(5))
   582  	model := s.makeModelAssertionInState(c, "my-brand", "pc", map[string]interface{}{
   583  		"architecture": "amd64",
   584  		"grade":        "dangerous",
   585  		"base":         "core20",
   586  		"snaps": []interface{}{
   587  			map[string]interface{}{
   588  				"name":            "pc-kernel",
   589  				"id":              s.ss.AssertedSnapID("pc-kernel"),
   590  				"type":            "kernel",
   591  				"default-channel": "20",
   592  			},
   593  			// pc snap is the gadget, but we have no info for it
   594  			map[string]interface{}{
   595  				"name":            "pc",
   596  				"id":              s.ss.AssertedSnapID("pc"),
   597  				"type":            "gadget",
   598  				"default-channel": "20",
   599  			},
   600  			map[string]interface{}{
   601  				"name": "snapd",
   602  				"id":   s.ss.AssertedSnapID("snapd"),
   603  				"type": "snapd",
   604  			},
   605  			map[string]interface{}{
   606  				"name":     "other-required",
   607  				"id":       s.ss.AssertedSnapID("other-required"),
   608  				"presence": "required",
   609  			},
   610  		},
   611  	})
   612  
   613  	failOn := map[string]bool{}
   614  
   615  	infoGetter := func(name string) (*snap.Info, bool, error) {
   616  		c.Logf("called for: %q", name)
   617  		if failOn[name] {
   618  			return nil, false, fmt.Errorf("mock failure for snap %q", name)
   619  		}
   620  		info, present := infos[name]
   621  		return info, present, nil
   622  	}
   623  	var observerCalls int
   624  	snapWriteObserver := func(dir, where string) error {
   625  		observerCalls++
   626  		return fmt.Errorf("unexpected call")
   627  	}
   628  
   629  	systemDir := filepath.Join(boot.InitramfsUbuntuSeedDir, "systems/1234")
   630  
   631  	// when a given snap in asserted snaps directory already exists, it is
   632  	// not copied over
   633  
   634  	failOn["pc"] = true
   635  	dir, err := devicestate.CreateSystemForModelFromValidatedSnaps(model, "1234", s.db,
   636  		infoGetter, snapWriteObserver)
   637  	c.Assert(err, ErrorMatches, `cannot obtain essential snap information: mock failure for snap "pc"`)
   638  	c.Check(dir, Equals, "")
   639  	c.Check(observerCalls, Equals, 0)
   640  	c.Check(osutil.IsDirectory(systemDir), Equals, false)
   641  
   642  	failOn["pc"] = false
   643  	failOn["other-required"] = true
   644  	dir, err = devicestate.CreateSystemForModelFromValidatedSnaps(model, "1234", s.db,
   645  		infoGetter, snapWriteObserver)
   646  	c.Assert(err, ErrorMatches, `cannot obtain non-essential but required snap information: mock failure for snap "other-required"`)
   647  	c.Check(dir, Equals, "")
   648  	c.Check(observerCalls, Equals, 0)
   649  	c.Check(osutil.IsDirectory(systemDir), Equals, false)
   650  }
   651  
   652  func (s *createSystemSuite) TestCreateSystemNonUC20(c *C) {
   653  	bl := bootloadertest.Mock("trusted", c.MkDir()).WithRecoveryAwareTrustedAssets()
   654  	bootloader.Force(bl)
   655  
   656  	s.state.Lock()
   657  	defer s.state.Unlock()
   658  	s.setupBrands(c)
   659  	model := s.makeModelAssertionInState(c, "my-brand", "pc", map[string]interface{}{
   660  		"architecture": "amd64",
   661  		"base":         "core18",
   662  		"kernel":       "pc-kernel",
   663  		"gadget":       "pc",
   664  	})
   665  
   666  	infoGetter := func(name string) (*snap.Info, bool, error) {
   667  		c.Fatalf("unexpected call")
   668  		return nil, false, fmt.Errorf("unexpected call")
   669  	}
   670  	snapWriteObserver := func(dir, where string) error {
   671  		c.Fatalf("unexpected call")
   672  		return fmt.Errorf("unexpected call")
   673  	}
   674  	dir, err := devicestate.CreateSystemForModelFromValidatedSnaps(model, "1234", s.db,
   675  		infoGetter, snapWriteObserver)
   676  	c.Assert(err, ErrorMatches, `cannot create a system for non UC20 model`)
   677  	c.Check(dir, Equals, "")
   678  }
   679  
   680  func (s *createSystemSuite) TestCreateSystemImplicitSnaps(c *C) {
   681  	bl := bootloadertest.Mock("trusted", c.MkDir()).WithRecoveryAwareTrustedAssets()
   682  	bootloader.Force(bl)
   683  
   684  	s.state.Lock()
   685  	defer s.state.Unlock()
   686  	s.setupBrands(c)
   687  	infos := s.makeEssentialSnapInfos(c)
   688  
   689  	// snapd snap is implicitly required
   690  	model := s.makeModelAssertionInState(c, "my-brand", "pc", map[string]interface{}{
   691  		"architecture": "amd64",
   692  		"grade":        "dangerous",
   693  		// base does not need to be listed among snaps
   694  		"base": "core20",
   695  		"snaps": []interface{}{
   696  			map[string]interface{}{
   697  				"name":            "pc-kernel",
   698  				"id":              s.ss.AssertedSnapID("pc-kernel"),
   699  				"type":            "kernel",
   700  				"default-channel": "20",
   701  			},
   702  			map[string]interface{}{
   703  				"name":            "pc",
   704  				"id":              s.ss.AssertedSnapID("pc"),
   705  				"type":            "gadget",
   706  				"default-channel": "20",
   707  			},
   708  		},
   709  	})
   710  	expectedDir := filepath.Join(boot.InitramfsUbuntuSeedDir, "systems/1234")
   711  
   712  	infoGetter := func(name string) (*snap.Info, bool, error) {
   713  		c.Logf("called for: %q", name)
   714  		info, present := infos[name]
   715  		return info, present, nil
   716  	}
   717  	var newFiles []string
   718  	snapWriteObserver := func(dir, where string) error {
   719  		c.Check(dir, Equals, expectedDir)
   720  		newFiles = append(newFiles, where)
   721  		return nil
   722  	}
   723  
   724  	dir, err := devicestate.CreateSystemForModelFromValidatedSnaps(model, "1234", s.db,
   725  		infoGetter, snapWriteObserver)
   726  	c.Assert(err, IsNil)
   727  	c.Check(newFiles, DeepEquals, []string{
   728  		filepath.Join(boot.InitramfsUbuntuSeedDir, "snaps/snapd_4.snap"),
   729  		filepath.Join(boot.InitramfsUbuntuSeedDir, "snaps/pc-kernel_1.snap"),
   730  		filepath.Join(boot.InitramfsUbuntuSeedDir, "snaps/core20_3.snap"),
   731  		filepath.Join(boot.InitramfsUbuntuSeedDir, "snaps/pc_2.snap"),
   732  	})
   733  	c.Check(dir, Equals, filepath.Join(boot.InitramfsUbuntuSeedDir, "systems/1234"))
   734  	// validate the seed
   735  	validateCore20Seed(c, "1234", model, s.ss.StoreSigning.Trusted)
   736  }
   737  
   738  func (s *createSystemSuite) TestCreateSystemObserverErr(c *C) {
   739  	bl := bootloadertest.Mock("trusted", c.MkDir()).WithRecoveryAwareTrustedAssets()
   740  	bootloader.Force(bl)
   741  
   742  	s.state.Lock()
   743  	defer s.state.Unlock()
   744  	s.setupBrands(c)
   745  	infos := s.makeEssentialSnapInfos(c)
   746  
   747  	// snapd snap is implicitly required
   748  	model := s.makeModelAssertionInState(c, "my-brand", "pc", map[string]interface{}{
   749  		"architecture": "amd64",
   750  		"grade":        "dangerous",
   751  		// base does not need to be listed among snaps
   752  		"base": "core20",
   753  		"snaps": []interface{}{
   754  			map[string]interface{}{
   755  				"name":            "pc-kernel",
   756  				"id":              s.ss.AssertedSnapID("pc-kernel"),
   757  				"type":            "kernel",
   758  				"default-channel": "20",
   759  			},
   760  			map[string]interface{}{
   761  				"name":            "pc",
   762  				"id":              s.ss.AssertedSnapID("pc"),
   763  				"type":            "gadget",
   764  				"default-channel": "20",
   765  			},
   766  		},
   767  	})
   768  
   769  	infoGetter := func(name string) (*snap.Info, bool, error) {
   770  		info, present := infos[name]
   771  		return info, present, nil
   772  	}
   773  	var newFiles []string
   774  	snapWriteObserver := func(dir, where string) error {
   775  		newFiles = append(newFiles, where)
   776  		if strings.HasSuffix(where, "/core20_3.snap") {
   777  			return fmt.Errorf("mocked observer failure")
   778  		}
   779  		return nil
   780  	}
   781  
   782  	dir, err := devicestate.CreateSystemForModelFromValidatedSnaps(model, "1234", s.db,
   783  		infoGetter, snapWriteObserver)
   784  	c.Assert(err, ErrorMatches, "mocked observer failure")
   785  	c.Check(newFiles, DeepEquals, []string{
   786  		filepath.Join(boot.InitramfsUbuntuSeedDir, "snaps/snapd_4.snap"),
   787  		filepath.Join(boot.InitramfsUbuntuSeedDir, "snaps/pc-kernel_1.snap"),
   788  		// we failed on this one
   789  		filepath.Join(boot.InitramfsUbuntuSeedDir, "snaps/core20_3.snap"),
   790  	})
   791  	c.Check(dir, Equals, filepath.Join(boot.InitramfsUbuntuSeedDir, "systems/1234"))
   792  }