github.com/hugh712/snapd@v0.0.0-20200910133618-1a99902bd583/overlord/devicestate/devicestate_gadget_test.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2016-2019 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package devicestate_test
    21  
    22  import (
    23  	"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/dirs"
    33  	"github.com/snapcore/snapd/gadget"
    34  	"github.com/snapcore/snapd/osutil"
    35  	"github.com/snapcore/snapd/overlord/auth"
    36  	"github.com/snapcore/snapd/overlord/devicestate"
    37  	"github.com/snapcore/snapd/overlord/devicestate/devicestatetest"
    38  	"github.com/snapcore/snapd/overlord/snapstate"
    39  	"github.com/snapcore/snapd/overlord/snapstate/snapstatetest"
    40  	"github.com/snapcore/snapd/overlord/state"
    41  	"github.com/snapcore/snapd/release"
    42  	"github.com/snapcore/snapd/snap"
    43  	"github.com/snapcore/snapd/snap/snaptest"
    44  	"github.com/snapcore/snapd/testutil"
    45  )
    46  
    47  type deviceMgrGadgetSuite struct {
    48  	deviceMgrBaseSuite
    49  }
    50  
    51  var _ = Suite(&deviceMgrGadgetSuite{})
    52  
    53  var snapYaml = `
    54  name: foo-gadget
    55  type: gadget
    56  `
    57  
    58  var gadgetYaml = `
    59  volumes:
    60    pc:
    61      bootloader: grub
    62  `
    63  
    64  var uc20gadgetYaml = `
    65  volumes:
    66    pc:
    67      bootloader: grub
    68      structure:
    69        - name: ubuntu-seed
    70          role: system-seed
    71          type: 21686148-6449-6E6F-744E-656564454649
    72          size: 20M
    73        - name: ubuntu-boot
    74          role: system-boot
    75          type: 21686148-6449-6E6F-744E-656564454649
    76          size: 10M
    77        - name: ubuntu-data
    78          role: system-data
    79          type: 21686148-6449-6E6F-744E-656564454649
    80          size: 50M
    81  `
    82  
    83  func (s *deviceMgrGadgetSuite) setupModelWithGadget(c *C, gadget string) {
    84  	s.makeModelAssertionInState(c, "canonical", "pc-model", map[string]interface{}{
    85  		"architecture": "amd64",
    86  		"kernel":       "pc-kernel",
    87  		"gadget":       gadget,
    88  		"base":         "core18",
    89  	})
    90  	devicestatetest.SetDevice(s.state, &auth.DeviceState{
    91  		Brand:  "canonical",
    92  		Model:  "pc-model",
    93  		Serial: "serial",
    94  	})
    95  }
    96  
    97  func (s *deviceMgrGadgetSuite) setupUC20ModelWithGadget(c *C, gadget string) {
    98  	s.makeModelAssertionInState(c, "canonical", "pc20-model", map[string]interface{}{
    99  		"display-name": "UC20 pc model",
   100  		"architecture": "amd64",
   101  		"base":         "core20",
   102  		// enough to have a grade set
   103  		"grade": "dangerous",
   104  		"snaps": []interface{}{
   105  			map[string]interface{}{
   106  				"name":            "pc-kernel",
   107  				"id":              "pckernelidididididididididididid",
   108  				"type":            "kernel",
   109  				"default-channel": "20",
   110  			},
   111  			map[string]interface{}{
   112  				"name":            gadget,
   113  				"id":              "pcididididididididididididididid",
   114  				"type":            "gadget",
   115  				"default-channel": "20",
   116  			}},
   117  	})
   118  	devicestatetest.SetDevice(s.state, &auth.DeviceState{
   119  		Brand:  "canonical",
   120  		Model:  "pc20-model",
   121  		Serial: "serial",
   122  	})
   123  }
   124  
   125  func (s *deviceMgrGadgetSuite) setupGadgetUpdate(c *C, modelGrade string) (chg *state.Change, tsk *state.Task) {
   126  	siCurrent := &snap.SideInfo{
   127  		RealName: "foo-gadget",
   128  		Revision: snap.R(33),
   129  		SnapID:   "foo-id",
   130  	}
   131  	si := &snap.SideInfo{
   132  		RealName: "foo-gadget",
   133  		Revision: snap.R(34),
   134  		SnapID:   "foo-id",
   135  	}
   136  	gadgetYamlContent := gadgetYaml
   137  	if modelGrade != "" {
   138  		gadgetYamlContent = uc20gadgetYaml
   139  	}
   140  	snaptest.MockSnapWithFiles(c, snapYaml, siCurrent, [][]string{
   141  		{"meta/gadget.yaml", gadgetYamlContent},
   142  	})
   143  	snaptest.MockSnapWithFiles(c, snapYaml, si, [][]string{
   144  		{"meta/gadget.yaml", gadgetYamlContent},
   145  	})
   146  
   147  	s.state.Lock()
   148  	defer s.state.Unlock()
   149  
   150  	if modelGrade == "" {
   151  		s.setupModelWithGadget(c, "foo-gadget")
   152  	} else {
   153  		s.setupUC20ModelWithGadget(c, "foo-gadget")
   154  	}
   155  
   156  	snapstate.Set(s.state, "foo-gadget", &snapstate.SnapState{
   157  		SnapType: "gadget",
   158  		Sequence: []*snap.SideInfo{siCurrent},
   159  		Current:  siCurrent.Revision,
   160  		Active:   true,
   161  	})
   162  
   163  	tsk = s.state.NewTask("update-gadget-assets", "update gadget")
   164  	tsk.Set("snap-setup", &snapstate.SnapSetup{
   165  		SideInfo: si,
   166  		Type:     snap.TypeGadget,
   167  	})
   168  	chg = s.state.NewChange("dummy", "...")
   169  	chg.AddTask(tsk)
   170  
   171  	return chg, tsk
   172  }
   173  
   174  func (s *deviceMgrGadgetSuite) testUpdateGadgetOnCoreSimple(c *C, grade string) {
   175  	var updateCalled bool
   176  	var passedRollbackDir string
   177  	restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, observer gadget.ContentUpdateObserver) error {
   178  		updateCalled = true
   179  		passedRollbackDir = path
   180  		st, err := os.Stat(path)
   181  		c.Assert(err, IsNil)
   182  		m := st.Mode()
   183  		c.Assert(m.IsDir(), Equals, true)
   184  		c.Check(m.Perm(), Equals, os.FileMode(0750))
   185  		if grade == "" {
   186  			// non UC20 model
   187  			c.Check(observer, IsNil)
   188  		} else {
   189  			c.Check(observer, NotNil)
   190  			// expecting a very specific observer
   191  			trustedUpdateObserver, ok := observer.(*boot.TrustedAssetsUpdateObserver)
   192  			c.Assert(ok, Equals, true, Commentf("unexpected type: %T", observer))
   193  			c.Assert(trustedUpdateObserver, NotNil)
   194  		}
   195  		return nil
   196  	})
   197  	defer restore()
   198  
   199  	chg, t := s.setupGadgetUpdate(c, grade)
   200  
   201  	// procure modeenv
   202  	if grade != "" {
   203  		// state after mark-seeded ran
   204  		modeenv := boot.Modeenv{
   205  			Mode:           "run",
   206  			RecoverySystem: "",
   207  		}
   208  		err := modeenv.WriteTo("")
   209  		c.Assert(err, IsNil)
   210  	}
   211  	devicestate.SetBootOkRan(s.mgr, true)
   212  
   213  	s.state.Lock()
   214  	s.state.Set("seeded", true)
   215  	s.state.Unlock()
   216  
   217  	s.settle(c)
   218  
   219  	s.state.Lock()
   220  	defer s.state.Unlock()
   221  	c.Assert(chg.IsReady(), Equals, true)
   222  	c.Check(chg.Err(), IsNil)
   223  	c.Check(t.Status(), Equals, state.DoneStatus)
   224  	c.Check(updateCalled, Equals, true)
   225  	rollbackDir := filepath.Join(dirs.SnapRollbackDir, "foo-gadget_34")
   226  	c.Check(rollbackDir, Equals, passedRollbackDir)
   227  	// should have been removed right after update
   228  	c.Check(osutil.IsDirectory(rollbackDir), Equals, false)
   229  	c.Check(s.restartRequests, DeepEquals, []state.RestartType{state.RestartSystem})
   230  
   231  }
   232  
   233  func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreSimple(c *C) {
   234  	// unset grade
   235  	s.testUpdateGadgetOnCoreSimple(c, "")
   236  }
   237  
   238  func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnUC20CoreSimple(c *C) {
   239  	s.testUpdateGadgetOnCoreSimple(c, "dangerous")
   240  }
   241  
   242  func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreNoUpdateNeeded(c *C) {
   243  	var called bool
   244  	restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error {
   245  		called = true
   246  		return gadget.ErrNoUpdate
   247  	})
   248  	defer restore()
   249  
   250  	chg, t := s.setupGadgetUpdate(c, "")
   251  
   252  	s.se.Ensure()
   253  	s.se.Wait()
   254  
   255  	s.state.Lock()
   256  	defer s.state.Unlock()
   257  	c.Assert(chg.IsReady(), Equals, true)
   258  	c.Check(chg.Err(), IsNil)
   259  	c.Check(t.Status(), Equals, state.DoneStatus)
   260  	c.Check(t.Log(), HasLen, 1)
   261  	c.Check(t.Log()[0], Matches, ".* INFO No gadget assets update needed")
   262  	c.Check(called, Equals, true)
   263  	c.Check(s.restartRequests, HasLen, 0)
   264  }
   265  
   266  func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreRollbackDirCreateFailed(c *C) {
   267  	if os.Geteuid() == 0 {
   268  		c.Skip("this test cannot run as root (permissions are not honored)")
   269  	}
   270  
   271  	restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error {
   272  		return errors.New("unexpected call")
   273  	})
   274  	defer restore()
   275  
   276  	chg, t := s.setupGadgetUpdate(c, "")
   277  
   278  	rollbackDir := filepath.Join(dirs.SnapRollbackDir, "foo-gadget_34")
   279  	err := os.MkdirAll(dirs.SnapRollbackDir, 0000)
   280  	c.Assert(err, IsNil)
   281  
   282  	s.state.Lock()
   283  	s.state.Set("seeded", true)
   284  	s.state.Unlock()
   285  
   286  	s.settle(c)
   287  
   288  	s.state.Lock()
   289  	defer s.state.Unlock()
   290  	c.Assert(chg.IsReady(), Equals, true)
   291  	c.Check(chg.Err(), ErrorMatches, `(?s).*cannot prepare update rollback directory: .*`)
   292  	c.Check(t.Status(), Equals, state.ErrorStatus)
   293  	c.Check(osutil.IsDirectory(rollbackDir), Equals, false)
   294  	c.Check(s.restartRequests, HasLen, 0)
   295  }
   296  
   297  func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreUpdateFailed(c *C) {
   298  	restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error {
   299  		return errors.New("gadget exploded")
   300  	})
   301  	defer restore()
   302  	chg, t := s.setupGadgetUpdate(c, "")
   303  
   304  	s.state.Lock()
   305  	s.state.Set("seeded", true)
   306  	s.state.Unlock()
   307  
   308  	s.settle(c)
   309  
   310  	s.state.Lock()
   311  	defer s.state.Unlock()
   312  	c.Assert(chg.IsReady(), Equals, true)
   313  	c.Check(chg.Err(), ErrorMatches, `(?s).*update gadget \(gadget exploded\).*`)
   314  	c.Check(t.Status(), Equals, state.ErrorStatus)
   315  	rollbackDir := filepath.Join(dirs.SnapRollbackDir, "foo-gadget_34")
   316  	// update rollback left for inspection
   317  	c.Check(osutil.IsDirectory(rollbackDir), Equals, true)
   318  	c.Check(s.restartRequests, HasLen, 0)
   319  }
   320  
   321  func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreNotDuringFirstboot(c *C) {
   322  	restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error {
   323  		return errors.New("unexpected call")
   324  	})
   325  	defer restore()
   326  
   327  	// simulate first-boot/seeding, there is no existing snap state information
   328  
   329  	si := &snap.SideInfo{
   330  		RealName: "foo-gadget",
   331  		Revision: snap.R(34),
   332  		SnapID:   "foo-id",
   333  	}
   334  	snaptest.MockSnapWithFiles(c, snapYaml, si, [][]string{
   335  		{"meta/gadget.yaml", gadgetYaml},
   336  	})
   337  
   338  	s.state.Lock()
   339  	s.state.Set("seeded", true)
   340  
   341  	s.setupModelWithGadget(c, "foo-gadget")
   342  
   343  	t := s.state.NewTask("update-gadget-assets", "update gadget")
   344  	t.Set("snap-setup", &snapstate.SnapSetup{
   345  		SideInfo: si,
   346  		Type:     snap.TypeGadget,
   347  	})
   348  	chg := s.state.NewChange("dummy", "...")
   349  	chg.AddTask(t)
   350  
   351  	s.state.Unlock()
   352  
   353  	s.settle(c)
   354  
   355  	s.state.Lock()
   356  	defer s.state.Unlock()
   357  	c.Assert(chg.IsReady(), Equals, true)
   358  	c.Check(chg.Err(), IsNil)
   359  	c.Check(t.Status(), Equals, state.DoneStatus)
   360  	rollbackDir := filepath.Join(dirs.SnapRollbackDir, "foo-gadget")
   361  	c.Check(osutil.IsDirectory(rollbackDir), Equals, false)
   362  	c.Check(s.restartRequests, HasLen, 0)
   363  }
   364  
   365  func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreBadGadgetYaml(c *C) {
   366  	restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error {
   367  		return errors.New("unexpected call")
   368  	})
   369  	defer restore()
   370  	siCurrent := &snap.SideInfo{
   371  		RealName: "foo-gadget",
   372  		Revision: snap.R(33),
   373  		SnapID:   "foo-id",
   374  	}
   375  	si := &snap.SideInfo{
   376  		RealName: "foo-gadget",
   377  		Revision: snap.R(34),
   378  		SnapID:   "foo-id",
   379  	}
   380  	snaptest.MockSnapWithFiles(c, snapYaml, siCurrent, [][]string{
   381  		{"meta/gadget.yaml", gadgetYaml},
   382  	})
   383  	// invalid gadget.yaml data
   384  	snaptest.MockSnapWithFiles(c, snapYaml, si, [][]string{
   385  		{"meta/gadget.yaml", "foobar"},
   386  	})
   387  
   388  	s.state.Lock()
   389  	s.state.Set("seeded", true)
   390  
   391  	s.setupModelWithGadget(c, "foo-gadget")
   392  
   393  	snapstate.Set(s.state, "foo-gadget", &snapstate.SnapState{
   394  		SnapType: "gadget",
   395  		Sequence: []*snap.SideInfo{siCurrent},
   396  		Current:  siCurrent.Revision,
   397  		Active:   true,
   398  	})
   399  
   400  	t := s.state.NewTask("update-gadget-assets", "update gadget")
   401  	t.Set("snap-setup", &snapstate.SnapSetup{
   402  		SideInfo: si,
   403  		Type:     snap.TypeGadget,
   404  	})
   405  	chg := s.state.NewChange("dummy", "...")
   406  	chg.AddTask(t)
   407  
   408  	s.state.Unlock()
   409  
   410  	s.settle(c)
   411  
   412  	s.state.Lock()
   413  	defer s.state.Unlock()
   414  	c.Assert(chg.IsReady(), Equals, true)
   415  	c.Check(chg.Err(), ErrorMatches, `(?s).*update gadget \(cannot read candidate snap gadget metadata: .*\).*`)
   416  	c.Check(t.Status(), Equals, state.ErrorStatus)
   417  	rollbackDir := filepath.Join(dirs.SnapRollbackDir, "foo-gadget")
   418  	c.Check(osutil.IsDirectory(rollbackDir), Equals, false)
   419  	c.Check(s.restartRequests, HasLen, 0)
   420  }
   421  
   422  func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreParanoidChecks(c *C) {
   423  	restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error {
   424  		return errors.New("unexpected call")
   425  	})
   426  	defer restore()
   427  	siCurrent := &snap.SideInfo{
   428  		RealName: "foo-gadget",
   429  		Revision: snap.R(33),
   430  		SnapID:   "foo-id",
   431  	}
   432  	si := &snap.SideInfo{
   433  		RealName: "foo-gadget-unexpected",
   434  		Revision: snap.R(34),
   435  		SnapID:   "foo-id",
   436  	}
   437  
   438  	s.state.Lock()
   439  
   440  	s.state.Set("seeded", true)
   441  
   442  	s.setupModelWithGadget(c, "foo-gadget")
   443  
   444  	snapstate.Set(s.state, "foo-gadget", &snapstate.SnapState{
   445  		SnapType: "gadget",
   446  		Sequence: []*snap.SideInfo{siCurrent},
   447  		Current:  siCurrent.Revision,
   448  		Active:   true,
   449  	})
   450  
   451  	t := s.state.NewTask("update-gadget-assets", "update gadget")
   452  	t.Set("snap-setup", &snapstate.SnapSetup{
   453  		SideInfo: si,
   454  		Type:     snap.TypeGadget,
   455  	})
   456  	chg := s.state.NewChange("dummy", "...")
   457  	chg.AddTask(t)
   458  
   459  	s.state.Unlock()
   460  
   461  	s.settle(c)
   462  
   463  	s.state.Lock()
   464  	defer s.state.Unlock()
   465  	c.Assert(chg.IsReady(), Equals, true)
   466  	c.Assert(chg.Err(), ErrorMatches, `(?s).*\(cannot apply gadget assets update from non-model gadget snap "foo-gadget-unexpected", expected "foo-gadget" snap\)`)
   467  	c.Check(t.Status(), Equals, state.ErrorStatus)
   468  	c.Check(s.restartRequests, HasLen, 0)
   469  }
   470  
   471  func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnClassicErrorsOut(c *C) {
   472  	restore := release.MockOnClassic(true)
   473  	defer restore()
   474  
   475  	restore = devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error {
   476  		return errors.New("unexpected call")
   477  	})
   478  	defer restore()
   479  
   480  	s.state.Lock()
   481  
   482  	s.state.Set("seeded", true)
   483  
   484  	t := s.state.NewTask("update-gadget-assets", "update gadget")
   485  	chg := s.state.NewChange("dummy", "...")
   486  	chg.AddTask(t)
   487  
   488  	s.state.Unlock()
   489  
   490  	s.settle(c)
   491  
   492  	s.state.Lock()
   493  	defer s.state.Unlock()
   494  	c.Assert(chg.IsReady(), Equals, true)
   495  	c.Check(chg.Err(), ErrorMatches, `(?s).*update gadget \(cannot run update gadget assets task on a classic system\).*`)
   496  	c.Check(t.Status(), Equals, state.ErrorStatus)
   497  }
   498  
   499  type mockUpdater struct{}
   500  
   501  func (m *mockUpdater) Backup() error { return nil }
   502  
   503  func (m *mockUpdater) Rollback() error { return nil }
   504  
   505  func (m *mockUpdater) Update() error { return nil }
   506  
   507  func (s *deviceMgrGadgetSuite) TestUpdateGadgetCallsToGadget(c *C) {
   508  	siCurrent := &snap.SideInfo{
   509  		RealName: "foo-gadget",
   510  		Revision: snap.R(33),
   511  		SnapID:   "foo-id",
   512  	}
   513  	si := &snap.SideInfo{
   514  		RealName: "foo-gadget",
   515  		Revision: snap.R(34),
   516  		SnapID:   "foo-id",
   517  	}
   518  	var gadgetCurrentYaml = `
   519  volumes:
   520    pc:
   521      bootloader: grub
   522      structure:
   523         - name: foo
   524           size: 10M
   525           type: bare
   526           content:
   527              - image: content.img
   528  `
   529  	var gadgetUpdateYaml = `
   530  volumes:
   531    pc:
   532      bootloader: grub
   533      structure:
   534         - name: foo
   535           size: 10M
   536           type: bare
   537           content:
   538              - image: content.img
   539           update:
   540             edition: 2
   541  `
   542  	snaptest.MockSnapWithFiles(c, snapYaml, siCurrent, [][]string{
   543  		{"meta/gadget.yaml", gadgetCurrentYaml},
   544  		{"content.img", "some content"},
   545  	})
   546  	updateInfo := snaptest.MockSnapWithFiles(c, snapYaml, si, [][]string{
   547  		{"meta/gadget.yaml", gadgetUpdateYaml},
   548  		{"content.img", "updated content"},
   549  	})
   550  
   551  	expectedRollbackDir := filepath.Join(dirs.SnapRollbackDir, "foo-gadget_34")
   552  	updaterForStructureCalls := 0
   553  	restore := gadget.MockUpdaterForStructure(func(ps *gadget.LaidOutStructure, rootDir, rollbackDir string, _ gadget.ContentUpdateObserver) (gadget.Updater, error) {
   554  		updaterForStructureCalls++
   555  
   556  		c.Assert(ps.Name, Equals, "foo")
   557  		c.Assert(rootDir, Equals, updateInfo.MountDir())
   558  		c.Assert(filepath.Join(rootDir, "content.img"), testutil.FileEquals, "updated content")
   559  		c.Assert(strings.HasPrefix(rollbackDir, expectedRollbackDir), Equals, true)
   560  		c.Assert(osutil.IsDirectory(rollbackDir), Equals, true)
   561  		return &mockUpdater{}, nil
   562  	})
   563  	defer restore()
   564  
   565  	s.state.Lock()
   566  	s.state.Set("seeded", true)
   567  
   568  	s.setupModelWithGadget(c, "foo-gadget")
   569  
   570  	snapstate.Set(s.state, "foo-gadget", &snapstate.SnapState{
   571  		SnapType: "gadget",
   572  		Sequence: []*snap.SideInfo{siCurrent},
   573  		Current:  siCurrent.Revision,
   574  		Active:   true,
   575  	})
   576  
   577  	t := s.state.NewTask("update-gadget-assets", "update gadget")
   578  	t.Set("snap-setup", &snapstate.SnapSetup{
   579  		SideInfo: si,
   580  		Type:     snap.TypeGadget,
   581  	})
   582  	chg := s.state.NewChange("dummy", "...")
   583  	chg.AddTask(t)
   584  
   585  	s.state.Unlock()
   586  
   587  	s.settle(c)
   588  
   589  	s.state.Lock()
   590  	defer s.state.Unlock()
   591  	c.Assert(chg.IsReady(), Equals, true)
   592  	c.Check(t.Status(), Equals, state.DoneStatus)
   593  	c.Check(s.restartRequests, HasLen, 1)
   594  	c.Check(updaterForStructureCalls, Equals, 1)
   595  }
   596  
   597  func (s *deviceMgrGadgetSuite) TestCurrentAndUpdateInfo(c *C) {
   598  	siCurrent := &snap.SideInfo{
   599  		RealName: "foo-gadget",
   600  		Revision: snap.R(33),
   601  		SnapID:   "foo-id",
   602  	}
   603  	si := &snap.SideInfo{
   604  		RealName: "foo-gadget",
   605  		Revision: snap.R(34),
   606  		SnapID:   "foo-id",
   607  	}
   608  
   609  	s.state.Lock()
   610  	defer s.state.Unlock()
   611  
   612  	snapsup := &snapstate.SnapSetup{
   613  		SideInfo: si,
   614  		Type:     snap.TypeGadget,
   615  	}
   616  
   617  	model := s.brands.Model("canonical", "pc-model", map[string]interface{}{
   618  		"architecture": "amd64",
   619  		"kernel":       "pc-kernel",
   620  		"gadget":       "foo-gadget",
   621  		"base":         "core18",
   622  	})
   623  	deviceCtx := &snapstatetest.TrivialDeviceContext{DeviceModel: model}
   624  
   625  	current, err := devicestate.CurrentGadgetInfo(s.state, deviceCtx)
   626  	c.Assert(current, IsNil)
   627  	c.Check(err, IsNil)
   628  
   629  	snapstate.Set(s.state, "foo-gadget", &snapstate.SnapState{
   630  		SnapType: "gadget",
   631  		Sequence: []*snap.SideInfo{siCurrent},
   632  		Current:  siCurrent.Revision,
   633  		Active:   true,
   634  	})
   635  
   636  	// mock current first, but gadget.yaml is still missing
   637  	ci := snaptest.MockSnapWithFiles(c, snapYaml, siCurrent, nil)
   638  
   639  	current, err = devicestate.CurrentGadgetInfo(s.state, deviceCtx)
   640  
   641  	c.Assert(current, IsNil)
   642  	c.Assert(err, ErrorMatches, "cannot read current gadget snap details: .*/33/meta/gadget.yaml: no such file or directory")
   643  
   644  	// drop gadget.yaml for current snap
   645  	ioutil.WriteFile(filepath.Join(ci.MountDir(), "meta/gadget.yaml"), []byte(gadgetYaml), 0644)
   646  
   647  	current, err = devicestate.CurrentGadgetInfo(s.state, deviceCtx)
   648  	c.Assert(err, IsNil)
   649  	c.Assert(current, DeepEquals, &gadget.GadgetData{
   650  		Info: &gadget.Info{
   651  			Volumes: map[string]gadget.Volume{
   652  				"pc": {
   653  					Bootloader: "grub",
   654  				},
   655  			},
   656  		},
   657  		RootDir: ci.MountDir(),
   658  	})
   659  
   660  	// pending update
   661  	update, err := devicestate.PendingGadgetInfo(snapsup, deviceCtx)
   662  	c.Assert(update, IsNil)
   663  	c.Assert(err, ErrorMatches, "cannot read candidate gadget snap details: cannot find installed snap .* .*/34/meta/snap.yaml")
   664  
   665  	ui := snaptest.MockSnapWithFiles(c, snapYaml, si, nil)
   666  
   667  	update, err = devicestate.PendingGadgetInfo(snapsup, deviceCtx)
   668  	c.Assert(update, IsNil)
   669  	c.Assert(err, ErrorMatches, "cannot read candidate snap gadget metadata: .*/34/meta/gadget.yaml: no such file or directory")
   670  
   671  	var updateGadgetYaml = `
   672  volumes:
   673    pc:
   674      bootloader: grub
   675      id: 123
   676  `
   677  
   678  	// drop gadget.yaml for update snap
   679  	ioutil.WriteFile(filepath.Join(ui.MountDir(), "meta/gadget.yaml"), []byte(updateGadgetYaml), 0644)
   680  
   681  	update, err = devicestate.PendingGadgetInfo(snapsup, deviceCtx)
   682  	c.Assert(err, IsNil)
   683  	c.Assert(update, DeepEquals, &gadget.GadgetData{
   684  		Info: &gadget.Info{
   685  			Volumes: map[string]gadget.Volume{
   686  				"pc": {
   687  					Bootloader: "grub",
   688  					ID:         "123",
   689  				},
   690  			},
   691  		},
   692  		RootDir: ui.MountDir(),
   693  	})
   694  }
   695  
   696  func (s *deviceMgrGadgetSuite) TestGadgetUpdateBlocksWhenOtherTasks(c *C) {
   697  	restore := release.MockOnClassic(true)
   698  	defer restore()
   699  
   700  	s.state.Lock()
   701  	defer s.state.Unlock()
   702  
   703  	tUpdate := s.state.NewTask("update-gadget-assets", "update gadget")
   704  	t1 := s.state.NewTask("other-task-1", "other 1")
   705  	t2 := s.state.NewTask("other-task-2", "other 2")
   706  
   707  	// no other running tasks, does not block
   708  	c.Assert(devicestate.GadgetUpdateBlocked(tUpdate, nil), Equals, false)
   709  
   710  	// list of running tasks actually contains ones that are in the 'running' state
   711  	t1.SetStatus(state.DoingStatus)
   712  	t2.SetStatus(state.UndoingStatus)
   713  	// block on any other running tasks
   714  	c.Assert(devicestate.GadgetUpdateBlocked(tUpdate, []*state.Task{t1, t2}), Equals, true)
   715  }
   716  
   717  func (s *deviceMgrGadgetSuite) TestGadgetUpdateBlocksOtherTasks(c *C) {
   718  	restore := release.MockOnClassic(true)
   719  	defer restore()
   720  
   721  	s.state.Lock()
   722  	defer s.state.Unlock()
   723  
   724  	tUpdate := s.state.NewTask("update-gadget-assets", "update gadget")
   725  	tUpdate.SetStatus(state.DoingStatus)
   726  	t1 := s.state.NewTask("other-task-1", "other 1")
   727  	t2 := s.state.NewTask("other-task-2", "other 2")
   728  
   729  	// block on any other running tasks
   730  	c.Assert(devicestate.GadgetUpdateBlocked(t1, []*state.Task{tUpdate}), Equals, true)
   731  	c.Assert(devicestate.GadgetUpdateBlocked(t2, []*state.Task{tUpdate}), Equals, true)
   732  
   733  	t2.SetStatus(state.UndoingStatus)
   734  	// update-gadget should be the only running task, for the sake of
   735  	// completeness pretend it's one of many running tasks
   736  	c.Assert(devicestate.GadgetUpdateBlocked(t1, []*state.Task{tUpdate, t2}), Equals, true)
   737  
   738  	// not blocking without gadget update task
   739  	c.Assert(devicestate.GadgetUpdateBlocked(t1, []*state.Task{t2}), Equals, false)
   740  }