github.com/chipaca/snappy@v0.0.0-20210104084008-1f06296fe8ad/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  	"io/ioutil"
    25  	"os"
    26  	"path/filepath"
    27  	"strings"
    28  
    29  	. "gopkg.in/check.v1"
    30  
    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/gadget"
    36  	"github.com/snapcore/snapd/osutil"
    37  	"github.com/snapcore/snapd/overlord/auth"
    38  	"github.com/snapcore/snapd/overlord/devicestate"
    39  	"github.com/snapcore/snapd/overlord/devicestate/devicestatetest"
    40  	"github.com/snapcore/snapd/overlord/snapstate"
    41  	"github.com/snapcore/snapd/overlord/snapstate/snapstatetest"
    42  	"github.com/snapcore/snapd/overlord/state"
    43  	"github.com/snapcore/snapd/release"
    44  	"github.com/snapcore/snapd/snap"
    45  	"github.com/snapcore/snapd/snap/snaptest"
    46  	"github.com/snapcore/snapd/testutil"
    47  )
    48  
    49  type deviceMgrGadgetSuite struct {
    50  	deviceMgrBaseSuite
    51  }
    52  
    53  var _ = Suite(&deviceMgrGadgetSuite{})
    54  
    55  var snapYaml = `
    56  name: foo-gadget
    57  type: gadget
    58  `
    59  
    60  var gadgetYaml = `
    61  volumes:
    62    pc:
    63      bootloader: grub
    64  `
    65  
    66  var uc20gadgetYaml = `
    67  volumes:
    68    pc:
    69      bootloader: grub
    70      structure:
    71        - name: ubuntu-seed
    72          role: system-seed
    73          type: 21686148-6449-6E6F-744E-656564454649
    74          size: 20M
    75        - name: ubuntu-boot
    76          role: system-boot
    77          type: 21686148-6449-6E6F-744E-656564454649
    78          size: 10M
    79        - name: ubuntu-data
    80          role: system-data
    81          type: 21686148-6449-6E6F-744E-656564454649
    82          size: 50M
    83  `
    84  
    85  var uc20gadgetYamlWithSave = uc20gadgetYaml + `
    86        - name: ubuntu-save
    87          role: system-save
    88          type: 21686148-6449-6E6F-744E-656564454649
    89          size: 50M
    90  `
    91  
    92  // this is the kind of volumes setup recommended to be prepared for a possible
    93  // UC18 -> UC20 transition
    94  var hybridGadgetYaml = `
    95  volumes:
    96    hybrid:
    97      bootloader: grub
    98      structure:
    99        - name: mbr
   100          type: mbr
   101          size: 440
   102          content:
   103            - image: pc-boot.img
   104        - name: BIOS Boot
   105          type: DA,21686148-6449-6E6F-744E-656564454649
   106          size: 1M
   107          offset: 1M
   108          offset-write: mbr+92
   109          content:
   110            - image: pc-core.img
   111        - name: EFI System
   112          type: EF,C12A7328-F81F-11D2-BA4B-00A0C93EC93B
   113          filesystem: vfat
   114          filesystem-label: system-boot
   115          size: 1200M
   116          content:
   117            - source: grubx64.efi
   118              target: EFI/boot/grubx64.efi
   119            - source: shim.efi.signed
   120              target: EFI/boot/bootx64.efi
   121            - source: mmx64.efi
   122              target: EFI/boot/mmx64.efi
   123            - source: grub.cfg
   124              target: EFI/ubuntu/grub.cfg
   125        - name: Ubuntu Boot
   126          type: 0FC63DAF-8483-4772-8E79-3D69D8477DE4
   127          filesystem: ext4
   128          filesystem-label: ubuntu-boot
   129          size: 750M
   130  `
   131  
   132  func (s *deviceMgrGadgetSuite) setupModelWithGadget(c *C, gadget string) {
   133  	s.makeModelAssertionInState(c, "canonical", "pc-model", map[string]interface{}{
   134  		"architecture": "amd64",
   135  		"kernel":       "pc-kernel",
   136  		"gadget":       gadget,
   137  		"base":         "core18",
   138  	})
   139  	devicestatetest.SetDevice(s.state, &auth.DeviceState{
   140  		Brand:  "canonical",
   141  		Model:  "pc-model",
   142  		Serial: "serial",
   143  	})
   144  }
   145  
   146  func (s *deviceMgrGadgetSuite) setupUC20ModelWithGadget(c *C, gadget string) {
   147  	s.makeModelAssertionInState(c, "canonical", "pc20-model", map[string]interface{}{
   148  		"display-name": "UC20 pc model",
   149  		"architecture": "amd64",
   150  		"base":         "core20",
   151  		// enough to have a grade set
   152  		"grade": "dangerous",
   153  		"snaps": []interface{}{
   154  			map[string]interface{}{
   155  				"name":            "pc-kernel",
   156  				"id":              "pckernelidididididididididididid",
   157  				"type":            "kernel",
   158  				"default-channel": "20",
   159  			},
   160  			map[string]interface{}{
   161  				"name":            gadget,
   162  				"id":              "pcididididididididididididididid",
   163  				"type":            "gadget",
   164  				"default-channel": "20",
   165  			}},
   166  	})
   167  	devicestatetest.SetDevice(s.state, &auth.DeviceState{
   168  		Brand:  "canonical",
   169  		Model:  "pc20-model",
   170  		Serial: "serial",
   171  	})
   172  }
   173  
   174  func (s *deviceMgrGadgetSuite) setupGadgetUpdate(c *C, modelGrade, gadgetYamlContent, gadgetYamlContentNext string) (chg *state.Change, tsk *state.Task) {
   175  	siCurrent := &snap.SideInfo{
   176  		RealName: "foo-gadget",
   177  		Revision: snap.R(33),
   178  		SnapID:   "foo-id",
   179  	}
   180  	si := &snap.SideInfo{
   181  		RealName: "foo-gadget",
   182  		Revision: snap.R(34),
   183  		SnapID:   "foo-id",
   184  	}
   185  	snaptest.MockSnapWithFiles(c, snapYaml, siCurrent, [][]string{
   186  		{"meta/gadget.yaml", gadgetYamlContent},
   187  		{"managed-asset", "managed asset rev 33"},
   188  		{"trusted-asset", "trusted asset rev 33"},
   189  	})
   190  	if gadgetYamlContentNext == "" {
   191  		gadgetYamlContentNext = gadgetYamlContent
   192  	}
   193  	snaptest.MockSnapWithFiles(c, snapYaml, si, [][]string{
   194  		{"meta/gadget.yaml", gadgetYamlContentNext},
   195  		{"managed-asset", "managed asset rev 34"},
   196  		// SHA3-384: 88478d8afe6925b348b9cd00085f3535959fde7029a64d7841b031acc39415c690796757afab1852a9e09da913a0151b
   197  		{"trusted-asset", "trusted asset rev 34"},
   198  	})
   199  
   200  	s.state.Lock()
   201  	defer s.state.Unlock()
   202  
   203  	if modelGrade == "" {
   204  		s.setupModelWithGadget(c, "foo-gadget")
   205  	} else {
   206  		s.setupUC20ModelWithGadget(c, "foo-gadget")
   207  	}
   208  
   209  	snapstate.Set(s.state, "foo-gadget", &snapstate.SnapState{
   210  		SnapType: "gadget",
   211  		Sequence: []*snap.SideInfo{siCurrent},
   212  		Current:  siCurrent.Revision,
   213  		Active:   true,
   214  	})
   215  
   216  	tsk = s.state.NewTask("update-gadget-assets", "update gadget")
   217  	tsk.Set("snap-setup", &snapstate.SnapSetup{
   218  		SideInfo: si,
   219  		Type:     snap.TypeGadget,
   220  	})
   221  	chg = s.state.NewChange("dummy", "...")
   222  	chg.AddTask(tsk)
   223  
   224  	return chg, tsk
   225  }
   226  
   227  func (s *deviceMgrGadgetSuite) testUpdateGadgetOnCoreSimple(c *C, grade string, encryption bool, gadgetYamlCont, gadgetYamlContNext string) {
   228  	var updateCalled bool
   229  	var passedRollbackDir string
   230  
   231  	if grade != "" {
   232  		bootDir := c.MkDir()
   233  		tbl := bootloadertest.Mock("trusted", bootDir).WithTrustedAssets()
   234  		tbl.TrustedAssetsList = []string{"trusted-asset"}
   235  		tbl.ManagedAssetsList = []string{"managed-asset"}
   236  		bootloader.Force(tbl)
   237  		defer func() { bootloader.Force(nil) }()
   238  	}
   239  
   240  	restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, observer gadget.ContentUpdateObserver) error {
   241  		updateCalled = true
   242  		passedRollbackDir = path
   243  		st, err := os.Stat(path)
   244  		c.Assert(err, IsNil)
   245  		m := st.Mode()
   246  		c.Assert(m.IsDir(), Equals, true)
   247  		c.Check(m.Perm(), Equals, os.FileMode(0750))
   248  		if grade == "" {
   249  			// non UC20 model
   250  			c.Check(observer, IsNil)
   251  		} else {
   252  			c.Check(observer, NotNil)
   253  			// expecting a very specific observer
   254  			trustedUpdateObserver, ok := observer.(*boot.TrustedAssetsUpdateObserver)
   255  			c.Assert(ok, Equals, true, Commentf("unexpected type: %T", observer))
   256  			c.Assert(trustedUpdateObserver, NotNil)
   257  
   258  			// check that observer is behaving correctly with
   259  			// respect to trusted and managed assets
   260  			targetDir := c.MkDir()
   261  			sourceStruct := &gadget.LaidOutStructure{
   262  				VolumeStructure: &gadget.VolumeStructure{
   263  					Role: gadget.SystemSeed,
   264  				},
   265  			}
   266  			act, err := observer.Observe(gadget.ContentUpdate, sourceStruct, targetDir, "managed-asset",
   267  				&gadget.ContentChange{After: filepath.Join(update.RootDir, "managed-asset")})
   268  			c.Assert(err, IsNil)
   269  			c.Check(act, Equals, gadget.ChangeIgnore)
   270  			act, err = observer.Observe(gadget.ContentUpdate, sourceStruct, targetDir, "trusted-asset",
   271  				&gadget.ContentChange{After: filepath.Join(update.RootDir, "trusted-asset")})
   272  			c.Assert(err, IsNil)
   273  			c.Check(act, Equals, gadget.ChangeApply)
   274  			// check that the behavior is correct
   275  			m, err := boot.ReadModeenv("")
   276  			c.Assert(err, IsNil)
   277  			if encryption {
   278  				// with encryption enabled, trusted asset would
   279  				// have been picked up by the the observer and
   280  				// added to modenv
   281  				c.Assert(m.CurrentTrustedRecoveryBootAssets, NotNil)
   282  				c.Check(m.CurrentTrustedRecoveryBootAssets["trusted-asset"], DeepEquals,
   283  					[]string{"88478d8afe6925b348b9cd00085f3535959fde7029a64d7841b031acc39415c690796757afab1852a9e09da913a0151b"})
   284  			} else {
   285  				c.Check(m.CurrentTrustedRecoveryBootAssets, HasLen, 0)
   286  			}
   287  		}
   288  		return nil
   289  	})
   290  	defer restore()
   291  
   292  	chg, t := s.setupGadgetUpdate(c, grade, gadgetYamlCont, gadgetYamlContNext)
   293  
   294  	// procure modeenv and stamp that we sealed keys
   295  	if grade != "" {
   296  		// state after mark-seeded ran
   297  		modeenv := boot.Modeenv{
   298  			Mode:           "run",
   299  			RecoverySystem: "",
   300  		}
   301  		err := modeenv.WriteTo("")
   302  		c.Assert(err, IsNil)
   303  
   304  		if encryption {
   305  			// sealed keys stamp
   306  			stamp := filepath.Join(dirs.SnapFDEDir, "sealed-keys")
   307  			c.Assert(os.MkdirAll(filepath.Dir(stamp), 0755), IsNil)
   308  			err = ioutil.WriteFile(stamp, nil, 0644)
   309  			c.Assert(err, IsNil)
   310  		}
   311  	}
   312  	devicestate.SetBootOkRan(s.mgr, true)
   313  
   314  	s.state.Lock()
   315  	s.state.Set("seeded", true)
   316  	s.state.Unlock()
   317  
   318  	s.settle(c)
   319  
   320  	s.state.Lock()
   321  	defer s.state.Unlock()
   322  	c.Assert(chg.IsReady(), Equals, true)
   323  	c.Check(chg.Err(), IsNil)
   324  	c.Check(t.Status(), Equals, state.DoneStatus)
   325  	c.Check(updateCalled, Equals, true)
   326  	rollbackDir := filepath.Join(dirs.SnapRollbackDir, "foo-gadget_34")
   327  	c.Check(rollbackDir, Equals, passedRollbackDir)
   328  	// should have been removed right after update
   329  	c.Check(osutil.IsDirectory(rollbackDir), Equals, false)
   330  	c.Check(s.restartRequests, DeepEquals, []state.RestartType{state.RestartSystem})
   331  }
   332  
   333  func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreSimple(c *C) {
   334  	// unset grade
   335  	encryption := false
   336  	s.testUpdateGadgetOnCoreSimple(c, "", encryption, gadgetYaml, "")
   337  }
   338  
   339  func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnUC20CoreSimpleWithEncryption(c *C) {
   340  	encryption := true
   341  	s.testUpdateGadgetOnCoreSimple(c, "dangerous", encryption, uc20gadgetYaml, "")
   342  }
   343  
   344  func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnUC20CoreSimpleNoEncryption(c *C) {
   345  	encryption := false
   346  	s.testUpdateGadgetOnCoreSimple(c, "dangerous", encryption, uc20gadgetYaml, "")
   347  }
   348  
   349  func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreNoUpdateNeeded(c *C) {
   350  	var called bool
   351  	restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error {
   352  		called = true
   353  		return gadget.ErrNoUpdate
   354  	})
   355  	defer restore()
   356  
   357  	chg, t := s.setupGadgetUpdate(c, "", gadgetYaml, "")
   358  
   359  	s.se.Ensure()
   360  	s.se.Wait()
   361  
   362  	s.state.Lock()
   363  	defer s.state.Unlock()
   364  	c.Assert(chg.IsReady(), Equals, true)
   365  	c.Check(chg.Err(), IsNil)
   366  	c.Check(t.Status(), Equals, state.DoneStatus)
   367  	c.Check(t.Log(), HasLen, 1)
   368  	c.Check(t.Log()[0], Matches, ".* INFO No gadget assets update needed")
   369  	c.Check(called, Equals, true)
   370  	c.Check(s.restartRequests, HasLen, 0)
   371  }
   372  
   373  func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreRollbackDirCreateFailed(c *C) {
   374  	if os.Geteuid() == 0 {
   375  		c.Skip("this test cannot run as root (permissions are not honored)")
   376  	}
   377  
   378  	restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error {
   379  		return errors.New("unexpected call")
   380  	})
   381  	defer restore()
   382  
   383  	chg, t := s.setupGadgetUpdate(c, "", gadgetYaml, "")
   384  
   385  	rollbackDir := filepath.Join(dirs.SnapRollbackDir, "foo-gadget_34")
   386  	err := os.MkdirAll(dirs.SnapRollbackDir, 0000)
   387  	c.Assert(err, IsNil)
   388  
   389  	s.state.Lock()
   390  	s.state.Set("seeded", true)
   391  	s.state.Unlock()
   392  
   393  	s.settle(c)
   394  
   395  	s.state.Lock()
   396  	defer s.state.Unlock()
   397  	c.Assert(chg.IsReady(), Equals, true)
   398  	c.Check(chg.Err(), ErrorMatches, `(?s).*cannot prepare update rollback directory: .*`)
   399  	c.Check(t.Status(), Equals, state.ErrorStatus)
   400  	c.Check(osutil.IsDirectory(rollbackDir), Equals, false)
   401  	c.Check(s.restartRequests, HasLen, 0)
   402  }
   403  
   404  func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreUpdateFailed(c *C) {
   405  	restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error {
   406  		return errors.New("gadget exploded")
   407  	})
   408  	defer restore()
   409  	chg, t := s.setupGadgetUpdate(c, "", gadgetYaml, "")
   410  
   411  	s.state.Lock()
   412  	s.state.Set("seeded", true)
   413  	s.state.Unlock()
   414  
   415  	s.settle(c)
   416  
   417  	s.state.Lock()
   418  	defer s.state.Unlock()
   419  	c.Assert(chg.IsReady(), Equals, true)
   420  	c.Check(chg.Err(), ErrorMatches, `(?s).*update gadget \(gadget exploded\).*`)
   421  	c.Check(t.Status(), Equals, state.ErrorStatus)
   422  	rollbackDir := filepath.Join(dirs.SnapRollbackDir, "foo-gadget_34")
   423  	// update rollback left for inspection
   424  	c.Check(osutil.IsDirectory(rollbackDir), Equals, true)
   425  	c.Check(s.restartRequests, HasLen, 0)
   426  }
   427  
   428  func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreNotDuringFirstboot(c *C) {
   429  	restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error {
   430  		return errors.New("unexpected call")
   431  	})
   432  	defer restore()
   433  
   434  	// simulate first-boot/seeding, there is no existing snap state information
   435  
   436  	si := &snap.SideInfo{
   437  		RealName: "foo-gadget",
   438  		Revision: snap.R(34),
   439  		SnapID:   "foo-id",
   440  	}
   441  	snaptest.MockSnapWithFiles(c, snapYaml, si, [][]string{
   442  		{"meta/gadget.yaml", gadgetYaml},
   443  	})
   444  
   445  	s.state.Lock()
   446  	s.state.Set("seeded", true)
   447  
   448  	s.setupModelWithGadget(c, "foo-gadget")
   449  
   450  	t := s.state.NewTask("update-gadget-assets", "update gadget")
   451  	t.Set("snap-setup", &snapstate.SnapSetup{
   452  		SideInfo: si,
   453  		Type:     snap.TypeGadget,
   454  	})
   455  	chg := s.state.NewChange("dummy", "...")
   456  	chg.AddTask(t)
   457  
   458  	s.state.Unlock()
   459  
   460  	s.settle(c)
   461  
   462  	s.state.Lock()
   463  	defer s.state.Unlock()
   464  	c.Assert(chg.IsReady(), Equals, true)
   465  	c.Check(chg.Err(), IsNil)
   466  	c.Check(t.Status(), Equals, state.DoneStatus)
   467  	rollbackDir := filepath.Join(dirs.SnapRollbackDir, "foo-gadget")
   468  	c.Check(osutil.IsDirectory(rollbackDir), Equals, false)
   469  	c.Check(s.restartRequests, HasLen, 0)
   470  }
   471  
   472  func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreBadGadgetYaml(c *C) {
   473  	restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error {
   474  		return errors.New("unexpected call")
   475  	})
   476  	defer restore()
   477  	siCurrent := &snap.SideInfo{
   478  		RealName: "foo-gadget",
   479  		Revision: snap.R(33),
   480  		SnapID:   "foo-id",
   481  	}
   482  	si := &snap.SideInfo{
   483  		RealName: "foo-gadget",
   484  		Revision: snap.R(34),
   485  		SnapID:   "foo-id",
   486  	}
   487  	snaptest.MockSnapWithFiles(c, snapYaml, siCurrent, [][]string{
   488  		{"meta/gadget.yaml", gadgetYaml},
   489  	})
   490  	// invalid gadget.yaml data
   491  	snaptest.MockSnapWithFiles(c, snapYaml, si, [][]string{
   492  		{"meta/gadget.yaml", "foobar"},
   493  	})
   494  
   495  	s.state.Lock()
   496  	s.state.Set("seeded", true)
   497  
   498  	s.setupModelWithGadget(c, "foo-gadget")
   499  
   500  	snapstate.Set(s.state, "foo-gadget", &snapstate.SnapState{
   501  		SnapType: "gadget",
   502  		Sequence: []*snap.SideInfo{siCurrent},
   503  		Current:  siCurrent.Revision,
   504  		Active:   true,
   505  	})
   506  
   507  	t := s.state.NewTask("update-gadget-assets", "update gadget")
   508  	t.Set("snap-setup", &snapstate.SnapSetup{
   509  		SideInfo: si,
   510  		Type:     snap.TypeGadget,
   511  	})
   512  	chg := s.state.NewChange("dummy", "...")
   513  	chg.AddTask(t)
   514  
   515  	s.state.Unlock()
   516  
   517  	s.settle(c)
   518  
   519  	s.state.Lock()
   520  	defer s.state.Unlock()
   521  	c.Assert(chg.IsReady(), Equals, true)
   522  	c.Check(chg.Err(), ErrorMatches, `(?s).*update gadget \(cannot read candidate snap gadget metadata: .*\).*`)
   523  	c.Check(t.Status(), Equals, state.ErrorStatus)
   524  	rollbackDir := filepath.Join(dirs.SnapRollbackDir, "foo-gadget")
   525  	c.Check(osutil.IsDirectory(rollbackDir), Equals, false)
   526  	c.Check(s.restartRequests, HasLen, 0)
   527  }
   528  
   529  func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreParanoidChecks(c *C) {
   530  	restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error {
   531  		return errors.New("unexpected call")
   532  	})
   533  	defer restore()
   534  	siCurrent := &snap.SideInfo{
   535  		RealName: "foo-gadget",
   536  		Revision: snap.R(33),
   537  		SnapID:   "foo-id",
   538  	}
   539  	si := &snap.SideInfo{
   540  		RealName: "foo-gadget-unexpected",
   541  		Revision: snap.R(34),
   542  		SnapID:   "foo-id",
   543  	}
   544  
   545  	s.state.Lock()
   546  
   547  	s.state.Set("seeded", true)
   548  
   549  	s.setupModelWithGadget(c, "foo-gadget")
   550  
   551  	snapstate.Set(s.state, "foo-gadget", &snapstate.SnapState{
   552  		SnapType: "gadget",
   553  		Sequence: []*snap.SideInfo{siCurrent},
   554  		Current:  siCurrent.Revision,
   555  		Active:   true,
   556  	})
   557  
   558  	t := s.state.NewTask("update-gadget-assets", "update gadget")
   559  	t.Set("snap-setup", &snapstate.SnapSetup{
   560  		SideInfo: si,
   561  		Type:     snap.TypeGadget,
   562  	})
   563  	chg := s.state.NewChange("dummy", "...")
   564  	chg.AddTask(t)
   565  
   566  	s.state.Unlock()
   567  
   568  	s.settle(c)
   569  
   570  	s.state.Lock()
   571  	defer s.state.Unlock()
   572  	c.Assert(chg.IsReady(), Equals, true)
   573  	c.Assert(chg.Err(), ErrorMatches, `(?s).*\(cannot apply gadget assets update from non-model gadget snap "foo-gadget-unexpected", expected "foo-gadget" snap\)`)
   574  	c.Check(t.Status(), Equals, state.ErrorStatus)
   575  	c.Check(s.restartRequests, HasLen, 0)
   576  }
   577  
   578  func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnClassicErrorsOut(c *C) {
   579  	restore := release.MockOnClassic(true)
   580  	defer restore()
   581  
   582  	restore = devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error {
   583  		return errors.New("unexpected call")
   584  	})
   585  	defer restore()
   586  
   587  	s.state.Lock()
   588  
   589  	s.state.Set("seeded", true)
   590  
   591  	t := s.state.NewTask("update-gadget-assets", "update gadget")
   592  	chg := s.state.NewChange("dummy", "...")
   593  	chg.AddTask(t)
   594  
   595  	s.state.Unlock()
   596  
   597  	s.settle(c)
   598  
   599  	s.state.Lock()
   600  	defer s.state.Unlock()
   601  	c.Assert(chg.IsReady(), Equals, true)
   602  	c.Check(chg.Err(), ErrorMatches, `(?s).*update gadget \(cannot run update gadget assets task on a classic system\).*`)
   603  	c.Check(t.Status(), Equals, state.ErrorStatus)
   604  }
   605  
   606  type mockUpdater struct{}
   607  
   608  func (m *mockUpdater) Backup() error { return nil }
   609  
   610  func (m *mockUpdater) Rollback() error { return nil }
   611  
   612  func (m *mockUpdater) Update() error { return nil }
   613  
   614  func (s *deviceMgrGadgetSuite) TestUpdateGadgetCallsToGadget(c *C) {
   615  	siCurrent := &snap.SideInfo{
   616  		RealName: "foo-gadget",
   617  		Revision: snap.R(33),
   618  		SnapID:   "foo-id",
   619  	}
   620  	si := &snap.SideInfo{
   621  		RealName: "foo-gadget",
   622  		Revision: snap.R(34),
   623  		SnapID:   "foo-id",
   624  	}
   625  	var gadgetCurrentYaml = `
   626  volumes:
   627    pc:
   628      bootloader: grub
   629      structure:
   630         - name: foo
   631           size: 10M
   632           type: bare
   633           content:
   634              - image: content.img
   635  `
   636  	var gadgetUpdateYaml = `
   637  volumes:
   638    pc:
   639      bootloader: grub
   640      structure:
   641         - name: foo
   642           size: 10M
   643           type: bare
   644           content:
   645              - image: content.img
   646           update:
   647             edition: 2
   648  `
   649  	snaptest.MockSnapWithFiles(c, snapYaml, siCurrent, [][]string{
   650  		{"meta/gadget.yaml", gadgetCurrentYaml},
   651  		{"content.img", "some content"},
   652  	})
   653  	updateInfo := snaptest.MockSnapWithFiles(c, snapYaml, si, [][]string{
   654  		{"meta/gadget.yaml", gadgetUpdateYaml},
   655  		{"content.img", "updated content"},
   656  	})
   657  
   658  	expectedRollbackDir := filepath.Join(dirs.SnapRollbackDir, "foo-gadget_34")
   659  	updaterForStructureCalls := 0
   660  	restore := gadget.MockUpdaterForStructure(func(ps *gadget.LaidOutStructure, rootDir, rollbackDir string, _ gadget.ContentUpdateObserver) (gadget.Updater, error) {
   661  		updaterForStructureCalls++
   662  
   663  		c.Assert(ps.Name, Equals, "foo")
   664  		c.Assert(rootDir, Equals, updateInfo.MountDir())
   665  		c.Assert(filepath.Join(rootDir, "content.img"), testutil.FileEquals, "updated content")
   666  		c.Assert(strings.HasPrefix(rollbackDir, expectedRollbackDir), Equals, true)
   667  		c.Assert(osutil.IsDirectory(rollbackDir), Equals, true)
   668  		return &mockUpdater{}, nil
   669  	})
   670  	defer restore()
   671  
   672  	s.state.Lock()
   673  	s.state.Set("seeded", true)
   674  
   675  	s.setupModelWithGadget(c, "foo-gadget")
   676  
   677  	snapstate.Set(s.state, "foo-gadget", &snapstate.SnapState{
   678  		SnapType: "gadget",
   679  		Sequence: []*snap.SideInfo{siCurrent},
   680  		Current:  siCurrent.Revision,
   681  		Active:   true,
   682  	})
   683  
   684  	t := s.state.NewTask("update-gadget-assets", "update gadget")
   685  	t.Set("snap-setup", &snapstate.SnapSetup{
   686  		SideInfo: si,
   687  		Type:     snap.TypeGadget,
   688  	})
   689  	chg := s.state.NewChange("dummy", "...")
   690  	chg.AddTask(t)
   691  
   692  	s.state.Unlock()
   693  
   694  	s.settle(c)
   695  
   696  	s.state.Lock()
   697  	defer s.state.Unlock()
   698  	c.Assert(chg.IsReady(), Equals, true)
   699  	c.Check(t.Status(), Equals, state.DoneStatus)
   700  	c.Check(s.restartRequests, HasLen, 1)
   701  	c.Check(updaterForStructureCalls, Equals, 1)
   702  }
   703  
   704  func (s *deviceMgrGadgetSuite) TestCurrentAndUpdateInfo(c *C) {
   705  	siCurrent := &snap.SideInfo{
   706  		RealName: "foo-gadget",
   707  		Revision: snap.R(33),
   708  		SnapID:   "foo-id",
   709  	}
   710  	si := &snap.SideInfo{
   711  		RealName: "foo-gadget",
   712  		Revision: snap.R(34),
   713  		SnapID:   "foo-id",
   714  	}
   715  
   716  	s.state.Lock()
   717  	defer s.state.Unlock()
   718  
   719  	snapsup := &snapstate.SnapSetup{
   720  		SideInfo: si,
   721  		Type:     snap.TypeGadget,
   722  	}
   723  
   724  	model := s.brands.Model("canonical", "pc-model", map[string]interface{}{
   725  		"architecture": "amd64",
   726  		"kernel":       "pc-kernel",
   727  		"gadget":       "foo-gadget",
   728  		"base":         "core18",
   729  	})
   730  	deviceCtx := &snapstatetest.TrivialDeviceContext{DeviceModel: model}
   731  
   732  	current, err := devicestate.CurrentGadgetInfo(s.state, deviceCtx)
   733  	c.Assert(current, IsNil)
   734  	c.Check(err, IsNil)
   735  
   736  	snapstate.Set(s.state, "foo-gadget", &snapstate.SnapState{
   737  		SnapType: "gadget",
   738  		Sequence: []*snap.SideInfo{siCurrent},
   739  		Current:  siCurrent.Revision,
   740  		Active:   true,
   741  	})
   742  
   743  	// mock current first, but gadget.yaml is still missing
   744  	ci := snaptest.MockSnapWithFiles(c, snapYaml, siCurrent, nil)
   745  
   746  	current, err = devicestate.CurrentGadgetInfo(s.state, deviceCtx)
   747  
   748  	c.Assert(current, IsNil)
   749  	c.Assert(err, ErrorMatches, "cannot read current gadget snap details: .*/33/meta/gadget.yaml: no such file or directory")
   750  
   751  	// drop gadget.yaml for current snap
   752  	ioutil.WriteFile(filepath.Join(ci.MountDir(), "meta/gadget.yaml"), []byte(gadgetYaml), 0644)
   753  
   754  	current, err = devicestate.CurrentGadgetInfo(s.state, deviceCtx)
   755  	c.Assert(err, IsNil)
   756  	c.Assert(current, DeepEquals, &gadget.GadgetData{
   757  		Info: &gadget.Info{
   758  			Volumes: map[string]*gadget.Volume{
   759  				"pc": {
   760  					Bootloader: "grub",
   761  					Schema:     "gpt",
   762  				},
   763  			},
   764  		},
   765  		RootDir: ci.MountDir(),
   766  	})
   767  
   768  	// pending update
   769  	update, err := devicestate.PendingGadgetInfo(snapsup, deviceCtx)
   770  	c.Assert(update, IsNil)
   771  	c.Assert(err, ErrorMatches, "cannot read candidate gadget snap details: cannot find installed snap .* .*/34/meta/snap.yaml")
   772  
   773  	ui := snaptest.MockSnapWithFiles(c, snapYaml, si, nil)
   774  
   775  	update, err = devicestate.PendingGadgetInfo(snapsup, deviceCtx)
   776  	c.Assert(update, IsNil)
   777  	c.Assert(err, ErrorMatches, "cannot read candidate snap gadget metadata: .*/34/meta/gadget.yaml: no such file or directory")
   778  
   779  	var updateGadgetYaml = `
   780  volumes:
   781    pc:
   782      bootloader: grub
   783      id: 123
   784  `
   785  
   786  	// drop gadget.yaml for update snap
   787  	ioutil.WriteFile(filepath.Join(ui.MountDir(), "meta/gadget.yaml"), []byte(updateGadgetYaml), 0644)
   788  
   789  	update, err = devicestate.PendingGadgetInfo(snapsup, deviceCtx)
   790  	c.Assert(err, IsNil)
   791  	c.Assert(update, DeepEquals, &gadget.GadgetData{
   792  		Info: &gadget.Info{
   793  			Volumes: map[string]*gadget.Volume{
   794  				"pc": {
   795  					Bootloader: "grub",
   796  					Schema:     "gpt",
   797  					ID:         "123",
   798  				},
   799  			},
   800  		},
   801  		RootDir: ui.MountDir(),
   802  	})
   803  }
   804  
   805  func (s *deviceMgrGadgetSuite) TestGadgetUpdateBlocksWhenOtherTasks(c *C) {
   806  	restore := release.MockOnClassic(true)
   807  	defer restore()
   808  
   809  	s.state.Lock()
   810  	defer s.state.Unlock()
   811  
   812  	tUpdate := s.state.NewTask("update-gadget-assets", "update gadget")
   813  	t1 := s.state.NewTask("other-task-1", "other 1")
   814  	t2 := s.state.NewTask("other-task-2", "other 2")
   815  
   816  	// no other running tasks, does not block
   817  	c.Assert(devicestate.GadgetUpdateBlocked(tUpdate, nil), Equals, false)
   818  
   819  	// list of running tasks actually contains ones that are in the 'running' state
   820  	t1.SetStatus(state.DoingStatus)
   821  	t2.SetStatus(state.UndoingStatus)
   822  	// block on any other running tasks
   823  	c.Assert(devicestate.GadgetUpdateBlocked(tUpdate, []*state.Task{t1, t2}), Equals, true)
   824  }
   825  
   826  func (s *deviceMgrGadgetSuite) TestGadgetUpdateBlocksOtherTasks(c *C) {
   827  	restore := release.MockOnClassic(true)
   828  	defer restore()
   829  
   830  	s.state.Lock()
   831  	defer s.state.Unlock()
   832  
   833  	tUpdate := s.state.NewTask("update-gadget-assets", "update gadget")
   834  	tUpdate.SetStatus(state.DoingStatus)
   835  	t1 := s.state.NewTask("other-task-1", "other 1")
   836  	t2 := s.state.NewTask("other-task-2", "other 2")
   837  
   838  	// block on any other running tasks
   839  	c.Assert(devicestate.GadgetUpdateBlocked(t1, []*state.Task{tUpdate}), Equals, true)
   840  	c.Assert(devicestate.GadgetUpdateBlocked(t2, []*state.Task{tUpdate}), Equals, true)
   841  
   842  	t2.SetStatus(state.UndoingStatus)
   843  	// update-gadget should be the only running task, for the sake of
   844  	// completeness pretend it's one of many running tasks
   845  	c.Assert(devicestate.GadgetUpdateBlocked(t1, []*state.Task{tUpdate, t2}), Equals, true)
   846  
   847  	// not blocking without gadget update task
   848  	c.Assert(devicestate.GadgetUpdateBlocked(t1, []*state.Task{t2}), Equals, false)
   849  }
   850  
   851  func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreHybridFirstboot(c *C) {
   852  	restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error {
   853  		return errors.New("unexpected call")
   854  	})
   855  	defer restore()
   856  
   857  	// simulate first-boot/seeding, there is no existing snap state information
   858  
   859  	si := &snap.SideInfo{
   860  		RealName: "foo-gadget",
   861  		Revision: snap.R(34),
   862  		SnapID:   "foo-id",
   863  	}
   864  	snaptest.MockSnapWithFiles(c, snapYaml, si, [][]string{
   865  		{"meta/gadget.yaml", hybridGadgetYaml},
   866  	})
   867  
   868  	s.state.Lock()
   869  	s.state.Set("seeded", true)
   870  
   871  	s.setupModelWithGadget(c, "foo-gadget")
   872  
   873  	t := s.state.NewTask("update-gadget-assets", "update gadget")
   874  	t.Set("snap-setup", &snapstate.SnapSetup{
   875  		SideInfo: si,
   876  		Type:     snap.TypeGadget,
   877  	})
   878  	chg := s.state.NewChange("dummy", "...")
   879  	chg.AddTask(t)
   880  
   881  	s.state.Unlock()
   882  
   883  	s.settle(c)
   884  
   885  	s.state.Lock()
   886  	defer s.state.Unlock()
   887  	c.Assert(chg.IsReady(), Equals, true)
   888  	c.Check(chg.Err(), IsNil)
   889  	c.Check(t.Status(), Equals, state.DoneStatus)
   890  	rollbackDir := filepath.Join(dirs.SnapRollbackDir, "foo-gadget")
   891  	c.Check(osutil.IsDirectory(rollbackDir), Equals, false)
   892  	c.Check(s.restartRequests, HasLen, 0)
   893  }
   894  
   895  func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreHybridShouldWork(c *C) {
   896  	encryption := false
   897  	s.testUpdateGadgetOnCoreSimple(c, "", encryption, hybridGadgetYaml, "")
   898  }
   899  
   900  func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreOldIsInvalidNowButShouldWork(c *C) {
   901  	encryption := false
   902  	// this is not gadget yaml that we should support, by the UC16/18
   903  	// rules it actually has two system-boot role partitions,
   904  	hybridGadgetYamlBroken := hybridGadgetYaml + `
   905          role: system-boot
   906  `
   907  	s.testUpdateGadgetOnCoreSimple(c, "", encryption, hybridGadgetYamlBroken, hybridGadgetYaml)
   908  }