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