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