github.com/david-imola/snapd@v0.0.0-20210611180407-2de8ddeece6d/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  	"fmt"
    25  	"io/ioutil"
    26  	"os"
    27  	"path/filepath"
    28  	"reflect"
    29  	"strings"
    30  
    31  	. "gopkg.in/check.v1"
    32  
    33  	"github.com/snapcore/snapd/asserts"
    34  	"github.com/snapcore/snapd/boot"
    35  	"github.com/snapcore/snapd/bootloader"
    36  	"github.com/snapcore/snapd/bootloader/bootloadertest"
    37  	"github.com/snapcore/snapd/dirs"
    38  	"github.com/snapcore/snapd/gadget"
    39  	"github.com/snapcore/snapd/osutil"
    40  	"github.com/snapcore/snapd/overlord/auth"
    41  	"github.com/snapcore/snapd/overlord/devicestate"
    42  	"github.com/snapcore/snapd/overlord/devicestate/devicestatetest"
    43  	"github.com/snapcore/snapd/overlord/snapstate"
    44  	"github.com/snapcore/snapd/overlord/snapstate/snapstatetest"
    45  	"github.com/snapcore/snapd/overlord/state"
    46  	"github.com/snapcore/snapd/release"
    47  	"github.com/snapcore/snapd/snap"
    48  	"github.com/snapcore/snapd/snap/snaptest"
    49  	"github.com/snapcore/snapd/testutil"
    50  )
    51  
    52  type deviceMgrGadgetSuite struct {
    53  	deviceMgrBaseSuite
    54  
    55  	managedbl *bootloadertest.MockTrustedAssetsBootloader
    56  }
    57  
    58  var _ = Suite(&deviceMgrGadgetSuite{})
    59  
    60  const pcGadgetSnapYaml = `
    61  name: pc
    62  type: gadget
    63  `
    64  
    65  var snapYaml = `
    66  name: foo-gadget
    67  type: gadget
    68  `
    69  
    70  var gadgetYaml = `
    71  volumes:
    72    pc:
    73      bootloader: grub
    74  `
    75  
    76  var uc20gadgetYaml = `
    77  volumes:
    78    pc:
    79      bootloader: grub
    80      structure:
    81        - name: ubuntu-seed
    82          role: system-seed
    83          type: 21686148-6449-6E6F-744E-656564454649
    84          size: 20M
    85        - name: ubuntu-boot
    86          role: system-boot
    87          type: 21686148-6449-6E6F-744E-656564454649
    88          size: 10M
    89        - name: ubuntu-data
    90          role: system-data
    91          type: 21686148-6449-6E6F-744E-656564454649
    92          size: 50M
    93  `
    94  
    95  var uc20gadgetYamlWithSave = uc20gadgetYaml + `
    96        - name: ubuntu-save
    97          role: system-save
    98          type: 21686148-6449-6E6F-744E-656564454649
    99          size: 50M
   100  `
   101  
   102  // this is the kind of volumes setup recommended to be prepared for a possible
   103  // UC18 -> UC20 transition
   104  var hybridGadgetYaml = `
   105  volumes:
   106    hybrid:
   107      bootloader: grub
   108      structure:
   109        - name: mbr
   110          type: mbr
   111          size: 440
   112          content:
   113            - image: pc-boot.img
   114        - name: BIOS Boot
   115          type: DA,21686148-6449-6E6F-744E-656564454649
   116          size: 1M
   117          offset: 1M
   118          offset-write: mbr+92
   119          content:
   120            - image: pc-core.img
   121        - name: EFI System
   122          type: EF,C12A7328-F81F-11D2-BA4B-00A0C93EC93B
   123          filesystem: vfat
   124          filesystem-label: system-boot
   125          size: 1200M
   126          content:
   127            - source: grubx64.efi
   128              target: EFI/boot/grubx64.efi
   129            - source: shim.efi.signed
   130              target: EFI/boot/bootx64.efi
   131            - source: mmx64.efi
   132              target: EFI/boot/mmx64.efi
   133            - source: grub.cfg
   134              target: EFI/ubuntu/grub.cfg
   135        - name: Ubuntu Boot
   136          type: 0FC63DAF-8483-4772-8E79-3D69D8477DE4
   137          filesystem: ext4
   138          filesystem-label: ubuntu-boot
   139          size: 750M
   140  `
   141  
   142  func (s *deviceMgrGadgetSuite) SetUpTest(c *C) {
   143  	s.deviceMgrBaseSuite.SetUpTest(c)
   144  
   145  	s.managedbl = bootloadertest.Mock("mock", c.MkDir()).WithTrustedAssets()
   146  	s.managedbl.StaticCommandLine = "console=ttyS0 console=tty1 panic=-1"
   147  	s.managedbl.CandidateStaticCommandLine = "console=ttyS0 console=tty1 panic=-1 candidate"
   148  
   149  	s.state.Lock()
   150  	defer s.state.Unlock()
   151  }
   152  
   153  func (s *deviceMgrGadgetSuite) mockModeenvForMode(c *C, mode string) {
   154  	// mock minimal modeenv
   155  	modeenv := boot.Modeenv{
   156  		Mode:           mode,
   157  		RecoverySystem: "",
   158  		CurrentKernelCommandLines: []string{
   159  			"snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1",
   160  		},
   161  	}
   162  	err := modeenv.WriteTo("")
   163  	c.Assert(err, IsNil)
   164  }
   165  
   166  func (s *deviceMgrGadgetSuite) setupModelWithGadget(c *C, gadget string) {
   167  	s.makeModelAssertionInState(c, "canonical", "pc-model", map[string]interface{}{
   168  		"architecture": "amd64",
   169  		"kernel":       "pc-kernel",
   170  		"gadget":       gadget,
   171  		"base":         "core18",
   172  	})
   173  	devicestatetest.SetDevice(s.state, &auth.DeviceState{
   174  		Brand:  "canonical",
   175  		Model:  "pc-model",
   176  		Serial: "serial",
   177  	})
   178  }
   179  
   180  func (s *deviceMgrGadgetSuite) setupUC20ModelWithGadget(c *C, gadget string) {
   181  	s.makeModelAssertionInState(c, "canonical", "pc20-model", map[string]interface{}{
   182  		"display-name": "UC20 pc model",
   183  		"architecture": "amd64",
   184  		"base":         "core20",
   185  		// enough to have a grade set
   186  		"grade": "dangerous",
   187  		"snaps": []interface{}{
   188  			map[string]interface{}{
   189  				"name":            "pc-kernel",
   190  				"id":              "pckernelidididididididididididid",
   191  				"type":            "kernel",
   192  				"default-channel": "20",
   193  			},
   194  			map[string]interface{}{
   195  				"name":            gadget,
   196  				"id":              "pcididididididididididididididid",
   197  				"type":            "gadget",
   198  				"default-channel": "20",
   199  			}},
   200  	})
   201  	devicestatetest.SetDevice(s.state, &auth.DeviceState{
   202  		Brand:  "canonical",
   203  		Model:  "pc20-model",
   204  		Serial: "serial",
   205  	})
   206  }
   207  
   208  func (s *deviceMgrGadgetSuite) setupGadgetUpdate(c *C, modelGrade, gadgetYamlContent, gadgetYamlContentNext string) (chg *state.Change, tsk *state.Task) {
   209  	siCurrent := &snap.SideInfo{
   210  		RealName: "foo-gadget",
   211  		Revision: snap.R(33),
   212  		SnapID:   "foo-id",
   213  	}
   214  	si := &snap.SideInfo{
   215  		RealName: "foo-gadget",
   216  		Revision: snap.R(34),
   217  		SnapID:   "foo-id",
   218  	}
   219  	snaptest.MockSnapWithFiles(c, snapYaml, siCurrent, [][]string{
   220  		{"meta/gadget.yaml", gadgetYamlContent},
   221  		{"managed-asset", "managed asset rev 33"},
   222  		{"trusted-asset", "trusted asset rev 33"},
   223  	})
   224  	if gadgetYamlContentNext == "" {
   225  		gadgetYamlContentNext = gadgetYamlContent
   226  	}
   227  	snaptest.MockSnapWithFiles(c, snapYaml, si, [][]string{
   228  		{"meta/gadget.yaml", gadgetYamlContentNext},
   229  		{"managed-asset", "managed asset rev 34"},
   230  		// SHA3-384: 88478d8afe6925b348b9cd00085f3535959fde7029a64d7841b031acc39415c690796757afab1852a9e09da913a0151b
   231  		{"trusted-asset", "trusted asset rev 34"},
   232  	})
   233  
   234  	s.state.Lock()
   235  	defer s.state.Unlock()
   236  
   237  	if modelGrade == "" {
   238  		s.setupModelWithGadget(c, "foo-gadget")
   239  	} else {
   240  		s.setupUC20ModelWithGadget(c, "foo-gadget")
   241  	}
   242  
   243  	snapstate.Set(s.state, "foo-gadget", &snapstate.SnapState{
   244  		SnapType: "gadget",
   245  		Sequence: []*snap.SideInfo{siCurrent},
   246  		Current:  siCurrent.Revision,
   247  		Active:   true,
   248  	})
   249  
   250  	tsk = s.state.NewTask("update-gadget-assets", "update gadget")
   251  	tsk.Set("snap-setup", &snapstate.SnapSetup{
   252  		SideInfo: si,
   253  		Type:     snap.TypeGadget,
   254  	})
   255  	chg = s.state.NewChange("dummy", "...")
   256  	chg.AddTask(tsk)
   257  
   258  	return chg, tsk
   259  }
   260  
   261  func (s *deviceMgrGadgetSuite) testUpdateGadgetOnCoreSimple(c *C, grade string, encryption bool, gadgetYamlCont, gadgetYamlContNext string) {
   262  	var updateCalled bool
   263  	var passedRollbackDir string
   264  
   265  	if grade != "" {
   266  		bootDir := c.MkDir()
   267  		tbl := bootloadertest.Mock("trusted", bootDir).WithTrustedAssets()
   268  		tbl.TrustedAssetsList = []string{"trusted-asset"}
   269  		tbl.ManagedAssetsList = []string{"managed-asset"}
   270  		bootloader.Force(tbl)
   271  		defer func() { bootloader.Force(nil) }()
   272  	}
   273  
   274  	restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, observer gadget.ContentUpdateObserver) error {
   275  		updateCalled = true
   276  		passedRollbackDir = path
   277  		st, err := os.Stat(path)
   278  		c.Assert(err, IsNil)
   279  		m := st.Mode()
   280  		c.Assert(m.IsDir(), Equals, true)
   281  		c.Check(m.Perm(), Equals, os.FileMode(0750))
   282  		if grade == "" {
   283  			// non UC20 model
   284  			c.Check(observer, IsNil)
   285  		} else {
   286  			c.Check(observer, NotNil)
   287  			// expecting a very specific observer
   288  			trustedUpdateObserver, ok := observer.(*boot.TrustedAssetsUpdateObserver)
   289  			c.Assert(ok, Equals, true, Commentf("unexpected type: %T", observer))
   290  			c.Assert(trustedUpdateObserver, NotNil)
   291  
   292  			// check that observer is behaving correctly with
   293  			// respect to trusted and managed assets
   294  			targetDir := c.MkDir()
   295  			sourceStruct := &gadget.LaidOutStructure{
   296  				VolumeStructure: &gadget.VolumeStructure{
   297  					Role: gadget.SystemSeed,
   298  				},
   299  			}
   300  			act, err := observer.Observe(gadget.ContentUpdate, sourceStruct, targetDir, "managed-asset",
   301  				&gadget.ContentChange{After: filepath.Join(update.RootDir, "managed-asset")})
   302  			c.Assert(err, IsNil)
   303  			c.Check(act, Equals, gadget.ChangeIgnore)
   304  			act, err = observer.Observe(gadget.ContentUpdate, sourceStruct, targetDir, "trusted-asset",
   305  				&gadget.ContentChange{After: filepath.Join(update.RootDir, "trusted-asset")})
   306  			c.Assert(err, IsNil)
   307  			c.Check(act, Equals, gadget.ChangeApply)
   308  			// check that the behavior is correct
   309  			m, err := boot.ReadModeenv("")
   310  			c.Assert(err, IsNil)
   311  			if encryption {
   312  				// with encryption enabled, trusted asset would
   313  				// have been picked up by the the observer and
   314  				// added to modenv
   315  				c.Assert(m.CurrentTrustedRecoveryBootAssets, NotNil)
   316  				c.Check(m.CurrentTrustedRecoveryBootAssets["trusted-asset"], DeepEquals,
   317  					[]string{"88478d8afe6925b348b9cd00085f3535959fde7029a64d7841b031acc39415c690796757afab1852a9e09da913a0151b"})
   318  			} else {
   319  				c.Check(m.CurrentTrustedRecoveryBootAssets, HasLen, 0)
   320  			}
   321  		}
   322  		return nil
   323  	})
   324  	defer restore()
   325  
   326  	chg, t := s.setupGadgetUpdate(c, grade, gadgetYamlCont, gadgetYamlContNext)
   327  
   328  	// procure modeenv and stamp that we sealed keys
   329  	if grade != "" {
   330  		// state after mark-seeded ran
   331  		modeenv := boot.Modeenv{
   332  			Mode:           "run",
   333  			RecoverySystem: "",
   334  		}
   335  		err := modeenv.WriteTo("")
   336  		c.Assert(err, IsNil)
   337  
   338  		if encryption {
   339  			// sealed keys stamp
   340  			stamp := filepath.Join(dirs.SnapFDEDir, "sealed-keys")
   341  			c.Assert(os.MkdirAll(filepath.Dir(stamp), 0755), IsNil)
   342  			err = ioutil.WriteFile(stamp, nil, 0644)
   343  			c.Assert(err, IsNil)
   344  		}
   345  	}
   346  	devicestate.SetBootOkRan(s.mgr, true)
   347  
   348  	s.state.Lock()
   349  	s.state.Set("seeded", true)
   350  	s.state.Unlock()
   351  
   352  	s.settle(c)
   353  
   354  	s.state.Lock()
   355  	defer s.state.Unlock()
   356  	c.Assert(chg.IsReady(), Equals, true)
   357  	c.Check(chg.Err(), IsNil)
   358  	c.Check(t.Status(), Equals, state.DoneStatus)
   359  	c.Check(updateCalled, Equals, true)
   360  	rollbackDir := filepath.Join(dirs.SnapRollbackDir, "foo-gadget_34")
   361  	c.Check(rollbackDir, Equals, passedRollbackDir)
   362  	// should have been removed right after update
   363  	c.Check(osutil.IsDirectory(rollbackDir), Equals, false)
   364  	c.Check(s.restartRequests, DeepEquals, []state.RestartType{state.RestartSystem})
   365  }
   366  
   367  func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreSimple(c *C) {
   368  	// unset grade
   369  	encryption := false
   370  	s.testUpdateGadgetOnCoreSimple(c, "", encryption, gadgetYaml, "")
   371  }
   372  
   373  func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnUC20CoreSimpleWithEncryption(c *C) {
   374  	encryption := true
   375  	s.testUpdateGadgetOnCoreSimple(c, "dangerous", encryption, uc20gadgetYaml, "")
   376  }
   377  
   378  func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnUC20CoreSimpleNoEncryption(c *C) {
   379  	encryption := false
   380  	s.testUpdateGadgetOnCoreSimple(c, "dangerous", encryption, uc20gadgetYaml, "")
   381  }
   382  
   383  func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreNoUpdateNeeded(c *C) {
   384  	var called bool
   385  	restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error {
   386  		called = true
   387  		return gadget.ErrNoUpdate
   388  	})
   389  	defer restore()
   390  
   391  	chg, t := s.setupGadgetUpdate(c, "", gadgetYaml, "")
   392  
   393  	s.se.Ensure()
   394  	s.se.Wait()
   395  
   396  	s.state.Lock()
   397  	defer s.state.Unlock()
   398  	c.Assert(chg.IsReady(), Equals, true)
   399  	c.Check(chg.Err(), IsNil)
   400  	c.Check(t.Status(), Equals, state.DoneStatus)
   401  	c.Check(t.Log(), HasLen, 1)
   402  	c.Check(t.Log()[0], Matches, ".* INFO No gadget assets update needed")
   403  	c.Check(called, Equals, true)
   404  	c.Check(s.restartRequests, HasLen, 0)
   405  }
   406  
   407  func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreRollbackDirCreateFailed(c *C) {
   408  	if os.Geteuid() == 0 {
   409  		c.Skip("this test cannot run as root (permissions are not honored)")
   410  	}
   411  
   412  	restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error {
   413  		return errors.New("unexpected call")
   414  	})
   415  	defer restore()
   416  
   417  	chg, t := s.setupGadgetUpdate(c, "", gadgetYaml, "")
   418  
   419  	rollbackDir := filepath.Join(dirs.SnapRollbackDir, "foo-gadget_34")
   420  	err := os.MkdirAll(dirs.SnapRollbackDir, 0000)
   421  	c.Assert(err, IsNil)
   422  
   423  	s.state.Lock()
   424  	s.state.Set("seeded", true)
   425  	s.state.Unlock()
   426  
   427  	s.settle(c)
   428  
   429  	s.state.Lock()
   430  	defer s.state.Unlock()
   431  	c.Assert(chg.IsReady(), Equals, true)
   432  	c.Check(chg.Err(), ErrorMatches, `(?s).*cannot prepare update rollback directory: .*`)
   433  	c.Check(t.Status(), Equals, state.ErrorStatus)
   434  	c.Check(osutil.IsDirectory(rollbackDir), Equals, false)
   435  	c.Check(s.restartRequests, HasLen, 0)
   436  }
   437  
   438  func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreUpdateFailed(c *C) {
   439  	restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error {
   440  		return errors.New("gadget exploded")
   441  	})
   442  	defer restore()
   443  	chg, t := s.setupGadgetUpdate(c, "", gadgetYaml, "")
   444  
   445  	s.state.Lock()
   446  	s.state.Set("seeded", true)
   447  	s.state.Unlock()
   448  
   449  	s.settle(c)
   450  
   451  	s.state.Lock()
   452  	defer s.state.Unlock()
   453  	c.Assert(chg.IsReady(), Equals, true)
   454  	c.Check(chg.Err(), ErrorMatches, `(?s).*update gadget \(gadget exploded\).*`)
   455  	c.Check(t.Status(), Equals, state.ErrorStatus)
   456  	rollbackDir := filepath.Join(dirs.SnapRollbackDir, "foo-gadget_34")
   457  	// update rollback left for inspection
   458  	c.Check(osutil.IsDirectory(rollbackDir), Equals, true)
   459  	c.Check(s.restartRequests, HasLen, 0)
   460  }
   461  
   462  func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreNotDuringFirstboot(c *C) {
   463  	restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error {
   464  		return errors.New("unexpected call")
   465  	})
   466  	defer restore()
   467  
   468  	// simulate first-boot/seeding, there is no existing snap state information
   469  
   470  	si := &snap.SideInfo{
   471  		RealName: "foo-gadget",
   472  		Revision: snap.R(34),
   473  		SnapID:   "foo-id",
   474  	}
   475  	snaptest.MockSnapWithFiles(c, snapYaml, si, [][]string{
   476  		{"meta/gadget.yaml", gadgetYaml},
   477  	})
   478  
   479  	s.state.Lock()
   480  	s.state.Set("seeded", true)
   481  
   482  	s.setupModelWithGadget(c, "foo-gadget")
   483  
   484  	t := s.state.NewTask("update-gadget-assets", "update gadget")
   485  	t.Set("snap-setup", &snapstate.SnapSetup{
   486  		SideInfo: si,
   487  		Type:     snap.TypeGadget,
   488  	})
   489  	chg := s.state.NewChange("dummy", "...")
   490  	chg.AddTask(t)
   491  
   492  	s.state.Unlock()
   493  
   494  	s.settle(c)
   495  
   496  	s.state.Lock()
   497  	defer s.state.Unlock()
   498  	c.Assert(chg.IsReady(), Equals, true)
   499  	c.Check(chg.Err(), IsNil)
   500  	c.Check(t.Status(), Equals, state.DoneStatus)
   501  	rollbackDir := filepath.Join(dirs.SnapRollbackDir, "foo-gadget")
   502  	c.Check(osutil.IsDirectory(rollbackDir), Equals, false)
   503  	c.Check(s.restartRequests, HasLen, 0)
   504  }
   505  
   506  func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreBadGadgetYaml(c *C) {
   507  	restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error {
   508  		return errors.New("unexpected call")
   509  	})
   510  	defer restore()
   511  	siCurrent := &snap.SideInfo{
   512  		RealName: "foo-gadget",
   513  		Revision: snap.R(33),
   514  		SnapID:   "foo-id",
   515  	}
   516  	si := &snap.SideInfo{
   517  		RealName: "foo-gadget",
   518  		Revision: snap.R(34),
   519  		SnapID:   "foo-id",
   520  	}
   521  	snaptest.MockSnapWithFiles(c, snapYaml, siCurrent, [][]string{
   522  		{"meta/gadget.yaml", gadgetYaml},
   523  	})
   524  	// invalid gadget.yaml data
   525  	snaptest.MockSnapWithFiles(c, snapYaml, si, [][]string{
   526  		{"meta/gadget.yaml", "foobar"},
   527  	})
   528  
   529  	s.state.Lock()
   530  	s.state.Set("seeded", true)
   531  
   532  	s.setupModelWithGadget(c, "foo-gadget")
   533  
   534  	snapstate.Set(s.state, "foo-gadget", &snapstate.SnapState{
   535  		SnapType: "gadget",
   536  		Sequence: []*snap.SideInfo{siCurrent},
   537  		Current:  siCurrent.Revision,
   538  		Active:   true,
   539  	})
   540  
   541  	t := s.state.NewTask("update-gadget-assets", "update gadget")
   542  	t.Set("snap-setup", &snapstate.SnapSetup{
   543  		SideInfo: si,
   544  		Type:     snap.TypeGadget,
   545  	})
   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 read candidate snap gadget metadata: .*\).*`)
   557  	c.Check(t.Status(), Equals, state.ErrorStatus)
   558  	rollbackDir := filepath.Join(dirs.SnapRollbackDir, "foo-gadget")
   559  	c.Check(osutil.IsDirectory(rollbackDir), Equals, false)
   560  	c.Check(s.restartRequests, HasLen, 0)
   561  }
   562  
   563  func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreParanoidChecks(c *C) {
   564  	restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error {
   565  		return errors.New("unexpected call")
   566  	})
   567  	defer restore()
   568  	siCurrent := &snap.SideInfo{
   569  		RealName: "foo-gadget",
   570  		Revision: snap.R(33),
   571  		SnapID:   "foo-id",
   572  	}
   573  	si := &snap.SideInfo{
   574  		RealName: "foo-gadget-unexpected",
   575  		Revision: snap.R(34),
   576  		SnapID:   "foo-id",
   577  	}
   578  
   579  	s.state.Lock()
   580  
   581  	s.state.Set("seeded", true)
   582  
   583  	s.setupModelWithGadget(c, "foo-gadget")
   584  
   585  	snapstate.Set(s.state, "foo-gadget", &snapstate.SnapState{
   586  		SnapType: "gadget",
   587  		Sequence: []*snap.SideInfo{siCurrent},
   588  		Current:  siCurrent.Revision,
   589  		Active:   true,
   590  	})
   591  
   592  	t := s.state.NewTask("update-gadget-assets", "update gadget")
   593  	t.Set("snap-setup", &snapstate.SnapSetup{
   594  		SideInfo: si,
   595  		Type:     snap.TypeGadget,
   596  	})
   597  	chg := s.state.NewChange("dummy", "...")
   598  	chg.AddTask(t)
   599  
   600  	s.state.Unlock()
   601  
   602  	s.settle(c)
   603  
   604  	s.state.Lock()
   605  	defer s.state.Unlock()
   606  	c.Assert(chg.IsReady(), Equals, true)
   607  	c.Assert(chg.Err(), ErrorMatches, `(?s).*\(cannot apply gadget assets update from non-model gadget snap "foo-gadget-unexpected", expected "foo-gadget" snap\)`)
   608  	c.Check(t.Status(), Equals, state.ErrorStatus)
   609  	c.Check(s.restartRequests, HasLen, 0)
   610  }
   611  
   612  func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnClassicErrorsOut(c *C) {
   613  	restore := release.MockOnClassic(true)
   614  	defer restore()
   615  
   616  	restore = devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error {
   617  		return errors.New("unexpected call")
   618  	})
   619  	defer restore()
   620  
   621  	s.state.Lock()
   622  
   623  	s.state.Set("seeded", true)
   624  
   625  	t := s.state.NewTask("update-gadget-assets", "update gadget")
   626  	chg := s.state.NewChange("dummy", "...")
   627  	chg.AddTask(t)
   628  
   629  	s.state.Unlock()
   630  
   631  	s.settle(c)
   632  
   633  	s.state.Lock()
   634  	defer s.state.Unlock()
   635  	c.Assert(chg.IsReady(), Equals, true)
   636  	c.Check(chg.Err(), ErrorMatches, `(?s).*update gadget \(cannot run update gadget assets task on a classic system\).*`)
   637  	c.Check(t.Status(), Equals, state.ErrorStatus)
   638  }
   639  
   640  type mockUpdater struct{}
   641  
   642  func (m *mockUpdater) Backup() error { return nil }
   643  
   644  func (m *mockUpdater) Rollback() error { return nil }
   645  
   646  func (m *mockUpdater) Update() error { return nil }
   647  
   648  func (s *deviceMgrGadgetSuite) TestUpdateGadgetCallsToGadget(c *C) {
   649  	siCurrent := &snap.SideInfo{
   650  		RealName: "foo-gadget",
   651  		Revision: snap.R(33),
   652  		SnapID:   "foo-id",
   653  	}
   654  	si := &snap.SideInfo{
   655  		RealName: "foo-gadget",
   656  		Revision: snap.R(34),
   657  		SnapID:   "foo-id",
   658  	}
   659  	var gadgetCurrentYaml = `
   660  volumes:
   661    pc:
   662      bootloader: grub
   663      structure:
   664         - name: foo
   665           size: 10M
   666           type: bare
   667           content:
   668              - image: content.img
   669  `
   670  	var gadgetUpdateYaml = `
   671  volumes:
   672    pc:
   673      bootloader: grub
   674      structure:
   675         - name: foo
   676           size: 10M
   677           type: bare
   678           content:
   679              - image: content.img
   680           update:
   681             edition: 2
   682  `
   683  	snaptest.MockSnapWithFiles(c, snapYaml, siCurrent, [][]string{
   684  		{"meta/gadget.yaml", gadgetCurrentYaml},
   685  		{"content.img", "some content"},
   686  	})
   687  	updateInfo := snaptest.MockSnapWithFiles(c, snapYaml, si, [][]string{
   688  		{"meta/gadget.yaml", gadgetUpdateYaml},
   689  		{"content.img", "updated content"},
   690  	})
   691  
   692  	expectedRollbackDir := filepath.Join(dirs.SnapRollbackDir, "foo-gadget_34")
   693  	updaterForStructureCalls := 0
   694  	restore := gadget.MockUpdaterForStructure(func(ps *gadget.LaidOutStructure, rootDir, rollbackDir string, _ gadget.ContentUpdateObserver) (gadget.Updater, error) {
   695  		updaterForStructureCalls++
   696  
   697  		c.Assert(ps.Name, Equals, "foo")
   698  		c.Assert(rootDir, Equals, updateInfo.MountDir())
   699  		c.Assert(filepath.Join(rootDir, "content.img"), testutil.FileEquals, "updated content")
   700  		c.Assert(strings.HasPrefix(rollbackDir, expectedRollbackDir), Equals, true)
   701  		c.Assert(osutil.IsDirectory(rollbackDir), Equals, true)
   702  		return &mockUpdater{}, nil
   703  	})
   704  	defer restore()
   705  
   706  	s.state.Lock()
   707  	s.state.Set("seeded", true)
   708  
   709  	s.setupModelWithGadget(c, "foo-gadget")
   710  
   711  	snapstate.Set(s.state, "foo-gadget", &snapstate.SnapState{
   712  		SnapType: "gadget",
   713  		Sequence: []*snap.SideInfo{siCurrent},
   714  		Current:  siCurrent.Revision,
   715  		Active:   true,
   716  	})
   717  
   718  	t := s.state.NewTask("update-gadget-assets", "update gadget")
   719  	t.Set("snap-setup", &snapstate.SnapSetup{
   720  		SideInfo: si,
   721  		Type:     snap.TypeGadget,
   722  	})
   723  	chg := s.state.NewChange("dummy", "...")
   724  	chg.AddTask(t)
   725  
   726  	s.state.Unlock()
   727  
   728  	s.settle(c)
   729  
   730  	s.state.Lock()
   731  	defer s.state.Unlock()
   732  	c.Assert(chg.IsReady(), Equals, true)
   733  	c.Check(t.Status(), Equals, state.DoneStatus)
   734  	c.Check(s.restartRequests, HasLen, 1)
   735  	c.Check(updaterForStructureCalls, Equals, 1)
   736  }
   737  
   738  func (s *deviceMgrGadgetSuite) TestCurrentAndUpdateInfo(c *C) {
   739  	siCurrent := &snap.SideInfo{
   740  		RealName: "foo-gadget",
   741  		Revision: snap.R(33),
   742  		SnapID:   "foo-id",
   743  	}
   744  	si := &snap.SideInfo{
   745  		RealName: "foo-gadget",
   746  		Revision: snap.R(34),
   747  		SnapID:   "foo-id",
   748  	}
   749  
   750  	s.state.Lock()
   751  	defer s.state.Unlock()
   752  
   753  	snapsup := &snapstate.SnapSetup{
   754  		SideInfo: si,
   755  		Type:     snap.TypeGadget,
   756  	}
   757  
   758  	model := s.brands.Model("canonical", "pc-model", map[string]interface{}{
   759  		"architecture": "amd64",
   760  		"kernel":       "pc-kernel",
   761  		"gadget":       "foo-gadget",
   762  		"base":         "core18",
   763  	})
   764  	deviceCtx := &snapstatetest.TrivialDeviceContext{DeviceModel: model}
   765  
   766  	current, err := devicestate.CurrentGadgetInfo(s.state, deviceCtx)
   767  	c.Assert(current, IsNil)
   768  	c.Check(err, IsNil)
   769  
   770  	snapstate.Set(s.state, "foo-gadget", &snapstate.SnapState{
   771  		SnapType: "gadget",
   772  		Sequence: []*snap.SideInfo{siCurrent},
   773  		Current:  siCurrent.Revision,
   774  		Active:   true,
   775  	})
   776  
   777  	// mock current first, but gadget.yaml is still missing
   778  	ci := snaptest.MockSnapWithFiles(c, snapYaml, siCurrent, nil)
   779  
   780  	current, err = devicestate.CurrentGadgetInfo(s.state, deviceCtx)
   781  
   782  	c.Assert(current, IsNil)
   783  	c.Assert(err, ErrorMatches, "cannot read current gadget snap details: .*/33/meta/gadget.yaml: no such file or directory")
   784  
   785  	// drop gadget.yaml for current snap
   786  	ioutil.WriteFile(filepath.Join(ci.MountDir(), "meta/gadget.yaml"), []byte(gadgetYaml), 0644)
   787  
   788  	current, err = devicestate.CurrentGadgetInfo(s.state, deviceCtx)
   789  	c.Assert(err, IsNil)
   790  	c.Assert(current, DeepEquals, &gadget.GadgetData{
   791  		Info: &gadget.Info{
   792  			Volumes: map[string]*gadget.Volume{
   793  				"pc": {
   794  					Bootloader: "grub",
   795  					Schema:     "gpt",
   796  				},
   797  			},
   798  		},
   799  		RootDir: ci.MountDir(),
   800  	})
   801  
   802  	// pending update
   803  	update, err := devicestate.PendingGadgetInfo(snapsup, deviceCtx)
   804  	c.Assert(update, IsNil)
   805  	c.Assert(err, ErrorMatches, "cannot read candidate gadget snap details: cannot find installed snap .* .*/34/meta/snap.yaml")
   806  
   807  	ui := snaptest.MockSnapWithFiles(c, snapYaml, si, nil)
   808  
   809  	update, err = devicestate.PendingGadgetInfo(snapsup, deviceCtx)
   810  	c.Assert(update, IsNil)
   811  	c.Assert(err, ErrorMatches, "cannot read candidate snap gadget metadata: .*/34/meta/gadget.yaml: no such file or directory")
   812  
   813  	var updateGadgetYaml = `
   814  volumes:
   815    pc:
   816      bootloader: grub
   817      id: 123
   818  `
   819  
   820  	// drop gadget.yaml for update snap
   821  	ioutil.WriteFile(filepath.Join(ui.MountDir(), "meta/gadget.yaml"), []byte(updateGadgetYaml), 0644)
   822  
   823  	update, err = devicestate.PendingGadgetInfo(snapsup, deviceCtx)
   824  	c.Assert(err, IsNil)
   825  	c.Assert(update, DeepEquals, &gadget.GadgetData{
   826  		Info: &gadget.Info{
   827  			Volumes: map[string]*gadget.Volume{
   828  				"pc": {
   829  					Bootloader: "grub",
   830  					Schema:     "gpt",
   831  					ID:         "123",
   832  				},
   833  			},
   834  		},
   835  		RootDir: ui.MountDir(),
   836  	})
   837  }
   838  
   839  func (s *deviceMgrGadgetSuite) TestGadgetUpdateBlocksWhenOtherTasks(c *C) {
   840  	restore := release.MockOnClassic(true)
   841  	defer restore()
   842  
   843  	s.state.Lock()
   844  	defer s.state.Unlock()
   845  
   846  	tUpdate := s.state.NewTask("update-gadget-assets", "update gadget")
   847  	t1 := s.state.NewTask("other-task-1", "other 1")
   848  	t2 := s.state.NewTask("other-task-2", "other 2")
   849  
   850  	// no other running tasks, does not block
   851  	c.Assert(devicestate.GadgetUpdateBlocked(tUpdate, nil), Equals, false)
   852  
   853  	// list of running tasks actually contains ones that are in the 'running' state
   854  	t1.SetStatus(state.DoingStatus)
   855  	t2.SetStatus(state.UndoingStatus)
   856  	// block on any other running tasks
   857  	c.Assert(devicestate.GadgetUpdateBlocked(tUpdate, []*state.Task{t1, t2}), Equals, true)
   858  }
   859  
   860  func (s *deviceMgrGadgetSuite) TestGadgetUpdateBlocksOtherTasks(c *C) {
   861  	restore := release.MockOnClassic(true)
   862  	defer restore()
   863  
   864  	s.state.Lock()
   865  	defer s.state.Unlock()
   866  
   867  	tUpdate := s.state.NewTask("update-gadget-assets", "update gadget")
   868  	tUpdate.SetStatus(state.DoingStatus)
   869  	t1 := s.state.NewTask("other-task-1", "other 1")
   870  	t2 := s.state.NewTask("other-task-2", "other 2")
   871  
   872  	// block on any other running tasks
   873  	c.Assert(devicestate.GadgetUpdateBlocked(t1, []*state.Task{tUpdate}), Equals, true)
   874  	c.Assert(devicestate.GadgetUpdateBlocked(t2, []*state.Task{tUpdate}), Equals, true)
   875  
   876  	t2.SetStatus(state.UndoingStatus)
   877  	// update-gadget should be the only running task, for the sake of
   878  	// completeness pretend it's one of many running tasks
   879  	c.Assert(devicestate.GadgetUpdateBlocked(t1, []*state.Task{tUpdate, t2}), Equals, true)
   880  
   881  	// not blocking without gadget update task
   882  	c.Assert(devicestate.GadgetUpdateBlocked(t1, []*state.Task{t2}), Equals, false)
   883  }
   884  
   885  func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreHybridFirstboot(c *C) {
   886  	restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error {
   887  		return errors.New("unexpected call")
   888  	})
   889  	defer restore()
   890  
   891  	// simulate first-boot/seeding, there is no existing snap state information
   892  
   893  	si := &snap.SideInfo{
   894  		RealName: "foo-gadget",
   895  		Revision: snap.R(34),
   896  		SnapID:   "foo-id",
   897  	}
   898  	snaptest.MockSnapWithFiles(c, snapYaml, si, [][]string{
   899  		{"meta/gadget.yaml", hybridGadgetYaml},
   900  	})
   901  
   902  	s.state.Lock()
   903  	s.state.Set("seeded", true)
   904  
   905  	s.setupModelWithGadget(c, "foo-gadget")
   906  
   907  	t := s.state.NewTask("update-gadget-assets", "update gadget")
   908  	t.Set("snap-setup", &snapstate.SnapSetup{
   909  		SideInfo: si,
   910  		Type:     snap.TypeGadget,
   911  	})
   912  	chg := s.state.NewChange("dummy", "...")
   913  	chg.AddTask(t)
   914  
   915  	s.state.Unlock()
   916  
   917  	s.settle(c)
   918  
   919  	s.state.Lock()
   920  	defer s.state.Unlock()
   921  	c.Assert(chg.IsReady(), Equals, true)
   922  	c.Check(chg.Err(), IsNil)
   923  	c.Check(t.Status(), Equals, state.DoneStatus)
   924  	rollbackDir := filepath.Join(dirs.SnapRollbackDir, "foo-gadget")
   925  	c.Check(osutil.IsDirectory(rollbackDir), Equals, false)
   926  	c.Check(s.restartRequests, HasLen, 0)
   927  }
   928  
   929  func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreHybridShouldWork(c *C) {
   930  	encryption := false
   931  	s.testUpdateGadgetOnCoreSimple(c, "", encryption, hybridGadgetYaml, "")
   932  }
   933  
   934  func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreOldIsInvalidNowButShouldWork(c *C) {
   935  	encryption := false
   936  	// this is not gadget yaml that we should support, by the UC16/18
   937  	// rules it actually has two system-boot role partitions,
   938  	hybridGadgetYamlBroken := hybridGadgetYaml + `
   939          role: system-boot
   940  `
   941  	s.testUpdateGadgetOnCoreSimple(c, "", encryption, hybridGadgetYamlBroken, hybridGadgetYaml)
   942  }
   943  
   944  func (s *deviceMgrGadgetSuite) makeMinimalKernelAssetsUpdateChange(c *C) (chg *state.Change, tsk *state.Task) {
   945  	s.state.Lock()
   946  	defer s.state.Unlock()
   947  
   948  	siGadget := &snap.SideInfo{
   949  		RealName: "foo-gadget",
   950  		Revision: snap.R(1),
   951  		SnapID:   "foo-gadget-id",
   952  	}
   953  	gadgetSnapYaml := "name: foo-gadget\nversion: 1.0\ntype: gadget"
   954  	gadgetYamlContent := `
   955  volumes:
   956    pi:
   957      bootloader: grub`
   958  	snaptest.MockSnapWithFiles(c, gadgetSnapYaml, siGadget, [][]string{
   959  		{"meta/gadget.yaml", gadgetYamlContent},
   960  	})
   961  	s.setupModelWithGadget(c, "foo-gadget")
   962  	snapstate.Set(s.state, "foo-gadget", &snapstate.SnapState{
   963  		SnapType: "gadget",
   964  		Sequence: []*snap.SideInfo{siGadget},
   965  		Current:  siGadget.Revision,
   966  		Active:   true,
   967  	})
   968  
   969  	snapKernelYaml := "name: pc-kernel\nversion: 1.0\ntype: kernel"
   970  	siCurrent := &snap.SideInfo{
   971  		RealName: "pc-kernel",
   972  		Revision: snap.R(33),
   973  		SnapID:   "foo-id",
   974  	}
   975  	snaptest.MockSnapWithFiles(c, snapKernelYaml, siCurrent, nil)
   976  	siNext := &snap.SideInfo{
   977  		RealName: "pc-kernel",
   978  		Revision: snap.R(34),
   979  		SnapID:   "foo-id",
   980  	}
   981  	snaptest.MockSnapWithFiles(c, snapKernelYaml, siNext, nil)
   982  	snapstate.Set(s.state, "pc-kernel", &snapstate.SnapState{
   983  		SnapType: "kernel",
   984  		Sequence: []*snap.SideInfo{siNext, siCurrent},
   985  		Current:  siCurrent.Revision,
   986  		Active:   true,
   987  	})
   988  
   989  	s.bootloader.SetBootVars(map[string]string{
   990  		"snap_core":   "core_1.snap",
   991  		"snap_kernel": "pc-kernel_33.snap",
   992  	})
   993  
   994  	tsk = s.state.NewTask("update-gadget-assets", "update gadget")
   995  	tsk.Set("snap-setup", &snapstate.SnapSetup{
   996  		SideInfo: siNext,
   997  		Type:     snap.TypeKernel,
   998  	})
   999  	chg = s.state.NewChange("dummy", "...")
  1000  	chg.AddTask(tsk)
  1001  
  1002  	return chg, tsk
  1003  }
  1004  
  1005  func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreFromKernel(c *C) {
  1006  	var updateCalled int
  1007  	var passedRollbackDir string
  1008  
  1009  	restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, observer gadget.ContentUpdateObserver) error {
  1010  		updateCalled++
  1011  		passedRollbackDir = path
  1012  
  1013  		c.Check(strings.HasSuffix(current.RootDir, "/snap/foo-gadget/1"), Equals, true)
  1014  		c.Check(strings.HasSuffix(update.RootDir, "/snap/foo-gadget/1"), Equals, true)
  1015  		c.Check(strings.HasSuffix(current.KernelRootDir, "/snap/pc-kernel/33"), Equals, true)
  1016  		c.Check(strings.HasSuffix(update.KernelRootDir, "/snap/pc-kernel/34"), Equals, true)
  1017  
  1018  		// KernelUpdatePolicy is used
  1019  		c.Check(reflect.ValueOf(policy), DeepEquals, reflect.ValueOf(gadget.UpdatePolicyFunc(gadget.KernelUpdatePolicy)))
  1020  		return nil
  1021  	})
  1022  	defer restore()
  1023  
  1024  	chg, t := s.makeMinimalKernelAssetsUpdateChange(c)
  1025  	devicestate.SetBootOkRan(s.mgr, true)
  1026  
  1027  	s.state.Lock()
  1028  	s.state.Set("seeded", true)
  1029  	s.state.Unlock()
  1030  
  1031  	s.settle(c)
  1032  
  1033  	s.state.Lock()
  1034  	defer s.state.Unlock()
  1035  	c.Assert(chg.IsReady(), Equals, true)
  1036  	c.Check(chg.Err(), IsNil)
  1037  	c.Check(t.Status(), Equals, state.DoneStatus)
  1038  	c.Check(updateCalled, Equals, 1)
  1039  	rollbackDir := filepath.Join(dirs.SnapRollbackDir, "pc-kernel_34")
  1040  	c.Check(rollbackDir, Equals, passedRollbackDir)
  1041  }
  1042  
  1043  func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreFromKernelRemodel(c *C) {
  1044  	var updateCalled int
  1045  	var passedRollbackDir string
  1046  
  1047  	restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, observer gadget.ContentUpdateObserver) error {
  1048  		updateCalled++
  1049  		passedRollbackDir = path
  1050  
  1051  		c.Check(strings.HasSuffix(current.RootDir, "/snap/foo-gadget/1"), Equals, true)
  1052  		c.Check(strings.HasSuffix(update.RootDir, "/snap/foo-gadget/1"), Equals, true)
  1053  		c.Check(strings.HasSuffix(current.KernelRootDir, "/snap/pc-kernel/33"), Equals, true)
  1054  		c.Check(strings.HasSuffix(update.KernelRootDir, "/snap/pc-kernel/34"), Equals, true)
  1055  
  1056  		// KernelUpdatePolicy is used even when we remodel
  1057  		c.Check(reflect.ValueOf(policy), DeepEquals, reflect.ValueOf(gadget.UpdatePolicyFunc(gadget.KernelUpdatePolicy)))
  1058  		return nil
  1059  	})
  1060  	defer restore()
  1061  
  1062  	chg, t := s.makeMinimalKernelAssetsUpdateChange(c)
  1063  	devicestate.SetBootOkRan(s.mgr, true)
  1064  
  1065  	newModel := s.brands.Model("canonical", "pc-model", map[string]interface{}{
  1066  		"architecture": "amd64",
  1067  		"kernel":       "pc-kernel",
  1068  		"gadget":       "foo-gadget",
  1069  		"base":         "core18",
  1070  		"revision":     "1",
  1071  	})
  1072  
  1073  	s.state.Lock()
  1074  	// pretend we are remodeling
  1075  	chg.Set("new-model", string(asserts.Encode(newModel)))
  1076  	s.state.Set("seeded", true)
  1077  	s.state.Unlock()
  1078  
  1079  	s.settle(c)
  1080  
  1081  	s.state.Lock()
  1082  	defer s.state.Unlock()
  1083  	c.Assert(chg.IsReady(), Equals, true)
  1084  	c.Check(chg.Err(), IsNil)
  1085  	c.Check(t.Status(), Equals, state.DoneStatus)
  1086  	c.Check(updateCalled, Equals, 1)
  1087  	rollbackDir := filepath.Join(dirs.SnapRollbackDir, "pc-kernel_34")
  1088  	c.Check(rollbackDir, Equals, passedRollbackDir)
  1089  }
  1090  
  1091  func (s *deviceMgrGadgetSuite) testGadgetCommandlineUpdateRun(c *C, fromFiles, toFiles [][]string, errMatch, logMatch string, updated bool) {
  1092  	restore := release.MockOnClassic(false)
  1093  	defer restore()
  1094  
  1095  	s.state.Lock()
  1096  
  1097  	currentSi := &snap.SideInfo{
  1098  		RealName: "pc",
  1099  		Revision: snap.R(33),
  1100  		SnapID:   "foo-id",
  1101  	}
  1102  	snapstate.Set(s.state, "pc", &snapstate.SnapState{
  1103  		SnapType: "gadget",
  1104  		Sequence: []*snap.SideInfo{currentSi},
  1105  		Current:  currentSi.Revision,
  1106  		Active:   true,
  1107  	})
  1108  	snaptest.MockSnapWithFiles(c, pcGadgetSnapYaml, currentSi, fromFiles)
  1109  	updateSi := *currentSi
  1110  	updateSi.Revision = snap.R(34)
  1111  	snaptest.MockSnapWithFiles(c, pcGadgetSnapYaml, &updateSi, toFiles)
  1112  
  1113  	tsk := s.state.NewTask("update-gadget-cmdline", "update gadget command line")
  1114  	tsk.Set("snap-setup", &snapstate.SnapSetup{
  1115  		SideInfo: &updateSi,
  1116  		Type:     snap.TypeGadget,
  1117  	})
  1118  	chg := s.state.NewChange("dummy", "...")
  1119  	chg.AddTask(tsk)
  1120  	s.state.Unlock()
  1121  
  1122  	s.settle(c)
  1123  
  1124  	s.state.Lock()
  1125  	defer s.state.Unlock()
  1126  
  1127  	c.Assert(chg.IsReady(), Equals, true)
  1128  	if errMatch == "" {
  1129  		c.Check(chg.Err(), IsNil)
  1130  		c.Check(tsk.Status(), Equals, state.DoneStatus)
  1131  		// we log on success
  1132  		log := tsk.Log()
  1133  		if logMatch != "" {
  1134  			c.Assert(log, HasLen, 1)
  1135  			c.Check(log[0], Matches, fmt.Sprintf(".* %v", logMatch))
  1136  		} else {
  1137  			c.Check(log, HasLen, 0)
  1138  		}
  1139  		if updated {
  1140  			// update was applied, thus a restart was requested
  1141  			c.Check(s.restartRequests, DeepEquals, []state.RestartType{state.RestartSystem})
  1142  		} else {
  1143  			// update was not applied or failed
  1144  			c.Check(s.restartRequests, HasLen, 0)
  1145  		}
  1146  	} else {
  1147  		c.Check(chg.Err(), ErrorMatches, errMatch)
  1148  		c.Check(tsk.Status(), Equals, state.ErrorStatus)
  1149  	}
  1150  }
  1151  
  1152  func (s *deviceMgrGadgetSuite) TestUpdateGadgetCommandlineWithExistingArgs(c *C) {
  1153  	// arguments change
  1154  	bootloader.Force(s.managedbl)
  1155  	s.state.Lock()
  1156  	s.setupUC20ModelWithGadget(c, "pc")
  1157  	s.mockModeenvForMode(c, "run")
  1158  	devicestate.SetBootOkRan(s.mgr, true)
  1159  	s.state.Set("seeded", true)
  1160  
  1161  	// update the modeenv to have the gadget arguments included to mimic the
  1162  	// state we would have in the system
  1163  	m, err := boot.ReadModeenv("")
  1164  	c.Assert(err, IsNil)
  1165  	m.CurrentKernelCommandLines = []string{
  1166  		"snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1 args from old gadget",
  1167  	}
  1168  	c.Assert(m.Write(), IsNil)
  1169  	err = s.managedbl.SetBootVars(map[string]string{
  1170  		"snapd_extra_cmdline_args": "args from old gadget",
  1171  	})
  1172  	c.Assert(err, IsNil)
  1173  	s.managedbl.SetBootVarsCalls = 0
  1174  
  1175  	s.state.Unlock()
  1176  
  1177  	const update = true
  1178  	s.testGadgetCommandlineUpdateRun(c,
  1179  		[][]string{
  1180  			{"meta/gadget.yaml", gadgetYaml},
  1181  			{"cmdline.extra", "args from old gadget"},
  1182  		},
  1183  		[][]string{
  1184  			{"meta/gadget.yaml", gadgetYaml},
  1185  			{"cmdline.extra", "args from updated gadget"},
  1186  		},
  1187  		"", "Updated kernel command line", update)
  1188  
  1189  	m, err = boot.ReadModeenv("")
  1190  	c.Assert(err, IsNil)
  1191  	c.Check([]string(m.CurrentKernelCommandLines), DeepEquals, []string{
  1192  		"snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1 args from old gadget",
  1193  		// gadget arguments are picked up for the candidate command line
  1194  		"snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1 args from updated gadget",
  1195  	})
  1196  	c.Check(s.managedbl.SetBootVarsCalls, Equals, 1)
  1197  	vars, err := s.managedbl.GetBootVars("snapd_extra_cmdline_args")
  1198  	c.Assert(err, IsNil)
  1199  	// bootenv was cleared
  1200  	c.Assert(vars, DeepEquals, map[string]string{
  1201  		"snapd_extra_cmdline_args": "args from updated gadget",
  1202  	})
  1203  }
  1204  
  1205  func (s *deviceMgrGadgetSuite) TestUpdateGadgetCommandlineWithNewArgs(c *C) {
  1206  	// no command line arguments prior to the gadget update
  1207  	bootloader.Force(s.managedbl)
  1208  	s.state.Lock()
  1209  	s.setupUC20ModelWithGadget(c, "pc")
  1210  	s.mockModeenvForMode(c, "run")
  1211  	devicestate.SetBootOkRan(s.mgr, true)
  1212  	s.state.Set("seeded", true)
  1213  
  1214  	// mimic system state
  1215  	m, err := boot.ReadModeenv("")
  1216  	c.Assert(err, IsNil)
  1217  	m.CurrentKernelCommandLines = []string{
  1218  		"snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1",
  1219  	}
  1220  	c.Assert(m.Write(), IsNil)
  1221  	err = s.managedbl.SetBootVars(map[string]string{
  1222  		"snapd_extra_cmdline_args": "",
  1223  	})
  1224  	c.Assert(err, IsNil)
  1225  	s.managedbl.SetBootVarsCalls = 0
  1226  
  1227  	s.state.Unlock()
  1228  
  1229  	const update = true
  1230  	s.testGadgetCommandlineUpdateRun(c,
  1231  		[][]string{
  1232  			{"meta/gadget.yaml", gadgetYaml},
  1233  			// old gadget does not carry command line arguments
  1234  		},
  1235  		[][]string{
  1236  			{"meta/gadget.yaml", gadgetYaml},
  1237  			{"cmdline.extra", "args from new gadget"},
  1238  		},
  1239  		"", "Updated kernel command line", update)
  1240  
  1241  	m, err = boot.ReadModeenv("")
  1242  	c.Assert(err, IsNil)
  1243  	c.Check([]string(m.CurrentKernelCommandLines), DeepEquals, []string{
  1244  		"snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1",
  1245  		// gadget arguments are picked up for the candidate command line
  1246  		"snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1 args from new gadget",
  1247  	})
  1248  	c.Check(s.managedbl.SetBootVarsCalls, Equals, 1)
  1249  	vars, err := s.managedbl.GetBootVars("snapd_extra_cmdline_args")
  1250  	c.Assert(err, IsNil)
  1251  	// bootenv was cleared
  1252  	c.Assert(vars, DeepEquals, map[string]string{
  1253  		"snapd_extra_cmdline_args": "args from new gadget",
  1254  	})
  1255  }
  1256  
  1257  func (s *deviceMgrGadgetSuite) TestUpdateGadgetCommandlineDroppedArgs(c *C) {
  1258  	// no command line arguments prior to the gadget up
  1259  	s.state.Lock()
  1260  	bootloader.Force(s.managedbl)
  1261  	s.setupUC20ModelWithGadget(c, "pc")
  1262  	s.mockModeenvForMode(c, "run")
  1263  	devicestate.SetBootOkRan(s.mgr, true)
  1264  	s.state.Set("seeded", true)
  1265  
  1266  	// mimic system state
  1267  	m, err := boot.ReadModeenv("")
  1268  	c.Assert(err, IsNil)
  1269  	m.CurrentKernelCommandLines = []string{
  1270  		"snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1 args from gadget",
  1271  	}
  1272  	c.Assert(m.Write(), IsNil)
  1273  	err = s.managedbl.SetBootVars(map[string]string{
  1274  		"snapd_extra_cmdline_args": "args from gadget",
  1275  	})
  1276  	c.Assert(err, IsNil)
  1277  	s.managedbl.SetBootVarsCalls = 0
  1278  
  1279  	s.state.Unlock()
  1280  
  1281  	const update = true
  1282  	s.testGadgetCommandlineUpdateRun(c,
  1283  		[][]string{
  1284  			{"meta/gadget.yaml", gadgetYaml},
  1285  			// old gadget carries command line arguments
  1286  			{"cmdline.extra", "args from gadget"},
  1287  		},
  1288  		[][]string{
  1289  			{"meta/gadget.yaml", gadgetYaml},
  1290  			// new one does not
  1291  		},
  1292  		"", "Updated kernel command line", update)
  1293  
  1294  	m, err = boot.ReadModeenv("")
  1295  	c.Assert(err, IsNil)
  1296  	c.Check([]string(m.CurrentKernelCommandLines), DeepEquals, []string{
  1297  		"snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1 args from gadget",
  1298  		// this is the expected new command line
  1299  		"snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1",
  1300  	})
  1301  	c.Check(s.managedbl.SetBootVarsCalls, Equals, 1)
  1302  	vars, err := s.managedbl.GetBootVars("snapd_extra_cmdline_args")
  1303  	c.Assert(err, IsNil)
  1304  	// bootenv was cleared
  1305  	c.Assert(vars, DeepEquals, map[string]string{
  1306  		"snapd_extra_cmdline_args": "",
  1307  	})
  1308  }
  1309  
  1310  func (s *deviceMgrGadgetSuite) TestUpdateGadgetCommandlineUnchanged(c *C) {
  1311  	// no command line arguments prior to the gadget update
  1312  	bootloader.Force(s.managedbl)
  1313  	s.state.Lock()
  1314  	s.setupUC20ModelWithGadget(c, "pc")
  1315  	s.mockModeenvForMode(c, "run")
  1316  	devicestate.SetBootOkRan(s.mgr, true)
  1317  	s.state.Set("seeded", true)
  1318  
  1319  	// mimic system state
  1320  	m, err := boot.ReadModeenv("")
  1321  	c.Assert(err, IsNil)
  1322  	m.CurrentKernelCommandLines = []string{
  1323  		"snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1 args from gadget",
  1324  	}
  1325  	c.Assert(m.Write(), IsNil)
  1326  	err = s.managedbl.SetBootVars(map[string]string{
  1327  		"snapd_extra_cmdline_args": "args from gadget",
  1328  	})
  1329  	c.Assert(err, IsNil)
  1330  	s.managedbl.SetBootVarsCalls = 0
  1331  
  1332  	s.state.Unlock()
  1333  
  1334  	sameFiles := [][]string{
  1335  		{"meta/gadget.yaml", gadgetYaml},
  1336  		{"cmdline.extra", "args from gadget"},
  1337  	}
  1338  	// old and new gadget have the same command line arguments, nothing changes
  1339  	const update = false
  1340  	s.testGadgetCommandlineUpdateRun(c, sameFiles, sameFiles,
  1341  		"", "", update)
  1342  
  1343  	m, err = boot.ReadModeenv("")
  1344  	c.Assert(err, IsNil)
  1345  	c.Check([]string(m.CurrentKernelCommandLines), DeepEquals, []string{
  1346  		"snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1 args from gadget",
  1347  	})
  1348  	c.Check(s.managedbl.SetBootVarsCalls, Equals, 0)
  1349  }
  1350  
  1351  func (s *deviceMgrGadgetSuite) TestUpdateGadgetCommandlineNonUC20(c *C) {
  1352  	// arguments are ignored on non UC20
  1353  	s.state.Lock()
  1354  	s.setupModelWithGadget(c, "pc")
  1355  	devicestate.SetBootOkRan(s.mgr, true)
  1356  	s.state.Set("seeded", true)
  1357  
  1358  	// there is no modeenv either
  1359  
  1360  	s.state.Unlock()
  1361  	const update = false
  1362  	s.testGadgetCommandlineUpdateRun(c,
  1363  		[][]string{
  1364  			{"meta/gadget.yaml", gadgetYaml},
  1365  			// old gadget does not carry command line arguments
  1366  		},
  1367  		[][]string{
  1368  			{"meta/gadget.yaml", gadgetYaml},
  1369  			{"cmdline.extra", "args from new gadget"},
  1370  		},
  1371  		"", "", update)
  1372  }
  1373  
  1374  func (s *deviceMgrGadgetSuite) TestGadgetCommandlineUpdateUndo(c *C) {
  1375  	restore := release.MockOnClassic(false)
  1376  	defer restore()
  1377  
  1378  	bootloader.Force(s.managedbl)
  1379  	s.state.Lock()
  1380  	s.setupUC20ModelWithGadget(c, "pc")
  1381  	s.mockModeenvForMode(c, "run")
  1382  	devicestate.SetBootOkRan(s.mgr, true)
  1383  	s.state.Set("seeded", true)
  1384  
  1385  	// mimic system state
  1386  	m, err := boot.ReadModeenv("")
  1387  	c.Assert(err, IsNil)
  1388  	m.CurrentKernelCommandLines = []string{
  1389  		"snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1 args from old gadget",
  1390  	}
  1391  	c.Assert(m.Write(), IsNil)
  1392  
  1393  	err = s.managedbl.SetBootVars(map[string]string{
  1394  		"snapd_extra_cmdline_args": "args from old gadget",
  1395  	})
  1396  	c.Assert(err, IsNil)
  1397  	s.managedbl.SetBootVarsCalls = 0
  1398  
  1399  	currentSi := &snap.SideInfo{
  1400  		RealName: "pc",
  1401  		Revision: snap.R(33),
  1402  		SnapID:   "foo-id",
  1403  	}
  1404  	snapstate.Set(s.state, "pc", &snapstate.SnapState{
  1405  		SnapType: "gadget",
  1406  		Sequence: []*snap.SideInfo{currentSi},
  1407  		Current:  currentSi.Revision,
  1408  		Active:   true,
  1409  	})
  1410  	snaptest.MockSnapWithFiles(c, pcGadgetSnapYaml, currentSi, [][]string{
  1411  		{"meta/gadget.yaml", gadgetYaml},
  1412  		{"cmdline.extra", "args from old gadget"},
  1413  	})
  1414  	updateSi := *currentSi
  1415  	updateSi.Revision = snap.R(34)
  1416  	snaptest.MockSnapWithFiles(c, pcGadgetSnapYaml, &updateSi, [][]string{
  1417  		{"meta/gadget.yaml", gadgetYaml},
  1418  		{"cmdline.extra", "args from new gadget"},
  1419  	})
  1420  
  1421  	tsk := s.state.NewTask("update-gadget-cmdline", "update gadget command line")
  1422  	tsk.Set("snap-setup", &snapstate.SnapSetup{
  1423  		SideInfo: &updateSi,
  1424  		Type:     snap.TypeGadget,
  1425  	})
  1426  	terr := s.state.NewTask("error-trigger", "provoking total undo")
  1427  	terr.WaitFor(tsk)
  1428  	chg := s.state.NewChange("dummy", "...")
  1429  	chg.AddTask(tsk)
  1430  	chg.AddTask(terr)
  1431  	s.state.Unlock()
  1432  
  1433  	restartCount := 0
  1434  	s.restartObserve = func() {
  1435  		// we want to observe restarts and mangle modeenv like
  1436  		// devicemanager boot handling would do
  1437  		restartCount++
  1438  		m, err := boot.ReadModeenv("")
  1439  		c.Assert(err, IsNil)
  1440  		switch restartCount {
  1441  		case 1:
  1442  			c.Check([]string(m.CurrentKernelCommandLines), DeepEquals, []string{
  1443  				"snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1 args from old gadget",
  1444  				"snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1 args from new gadget",
  1445  			})
  1446  			m.CurrentKernelCommandLines = []string{"snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1 args from new gadget"}
  1447  		case 2:
  1448  			c.Check([]string(m.CurrentKernelCommandLines), DeepEquals, []string{
  1449  				"snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1 args from new gadget",
  1450  				"snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1 args from old gadget",
  1451  			})
  1452  			m.CurrentKernelCommandLines = []string{"snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1 args from old gadget"}
  1453  		default:
  1454  			c.Fatalf("unexpected restart %v", restartCount)
  1455  		}
  1456  		c.Assert(m.Write(), IsNil)
  1457  	}
  1458  
  1459  	s.settle(c)
  1460  
  1461  	s.state.Lock()
  1462  	defer s.state.Unlock()
  1463  
  1464  	c.Assert(chg.IsReady(), Equals, true)
  1465  	c.Check(chg.Err(), ErrorMatches, "(?s)cannot perform the following tasks.*total undo.*")
  1466  	c.Check(tsk.Status(), Equals, state.UndoneStatus)
  1467  	log := tsk.Log()
  1468  	c.Assert(log, HasLen, 2)
  1469  	c.Check(log[0], Matches, ".* Updated kernel command line")
  1470  	c.Check(log[1], Matches, ".* Reverted kernel command line change")
  1471  	// update was applied and then undone
  1472  	c.Check(s.restartRequests, DeepEquals, []state.RestartType{state.RestartSystem, state.RestartSystem})
  1473  	c.Check(restartCount, Equals, 2)
  1474  	vars, err := s.managedbl.GetBootVars("snapd_extra_cmdline_args")
  1475  	c.Assert(err, IsNil)
  1476  	c.Assert(vars, DeepEquals, map[string]string{
  1477  		"snapd_extra_cmdline_args": "args from old gadget",
  1478  	})
  1479  	// 2 calls, one to set the new arguments, and one to reset them back
  1480  	c.Check(s.managedbl.SetBootVarsCalls, Equals, 2)
  1481  }
  1482  
  1483  func (s *deviceMgrGadgetSuite) TestGadgetCommandlineUpdateNoChangeNoRebootsUndo(c *C) {
  1484  	restore := release.MockOnClassic(false)
  1485  	defer restore()
  1486  
  1487  	bootloader.Force(s.managedbl)
  1488  	s.state.Lock()
  1489  	s.setupUC20ModelWithGadget(c, "pc")
  1490  	s.mockModeenvForMode(c, "run")
  1491  	devicestate.SetBootOkRan(s.mgr, true)
  1492  	s.state.Set("seeded", true)
  1493  
  1494  	// mimic system state
  1495  	m, err := boot.ReadModeenv("")
  1496  	c.Assert(err, IsNil)
  1497  	m.CurrentKernelCommandLines = []string{
  1498  		"snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1 args from gadget",
  1499  	}
  1500  	c.Assert(m.Write(), IsNil)
  1501  
  1502  	err = s.managedbl.SetBootVars(map[string]string{
  1503  		"snapd_extra_cmdline_args": "args from gadget",
  1504  	})
  1505  	c.Assert(err, IsNil)
  1506  	s.managedbl.SetBootVarsCalls = 0
  1507  
  1508  	currentSi := &snap.SideInfo{
  1509  		RealName: "pc",
  1510  		Revision: snap.R(33),
  1511  		SnapID:   "foo-id",
  1512  	}
  1513  	snapstate.Set(s.state, "pc", &snapstate.SnapState{
  1514  		SnapType: "gadget",
  1515  		Sequence: []*snap.SideInfo{currentSi},
  1516  		Current:  currentSi.Revision,
  1517  		Active:   true,
  1518  	})
  1519  	sameFiles := [][]string{
  1520  		{"meta/gadget.yaml", gadgetYaml},
  1521  		{"cmdline.extra", "args from gadget"},
  1522  	}
  1523  	snaptest.MockSnapWithFiles(c, pcGadgetSnapYaml, currentSi, sameFiles)
  1524  	updateSi := *currentSi
  1525  	updateSi.Revision = snap.R(34)
  1526  	// identical content, just a revision bump
  1527  	snaptest.MockSnapWithFiles(c, pcGadgetSnapYaml, &updateSi, sameFiles)
  1528  
  1529  	tsk := s.state.NewTask("update-gadget-cmdline", "update gadget command line")
  1530  	tsk.Set("snap-setup", &snapstate.SnapSetup{
  1531  		SideInfo: &updateSi,
  1532  		Type:     snap.TypeGadget,
  1533  	})
  1534  	terr := s.state.NewTask("error-trigger", "provoking total undo")
  1535  	terr.WaitFor(tsk)
  1536  	chg := s.state.NewChange("dummy", "...")
  1537  	chg.AddTask(tsk)
  1538  	chg.AddTask(terr)
  1539  	s.state.Unlock()
  1540  
  1541  	s.settle(c)
  1542  
  1543  	s.state.Lock()
  1544  	defer s.state.Unlock()
  1545  
  1546  	c.Assert(chg.IsReady(), Equals, true)
  1547  	c.Check(chg.Err(), ErrorMatches, "(?s)cannot perform the following tasks.*total undo.*")
  1548  	c.Check(tsk.Status(), Equals, state.UndoneStatus)
  1549  	// there was nothing to update and thus nothing to undo
  1550  	c.Check(s.restartRequests, HasLen, 0)
  1551  	c.Check(s.managedbl.SetBootVarsCalls, Equals, 0)
  1552  	// modeenv wasn't changed
  1553  	m, err = boot.ReadModeenv("")
  1554  	c.Assert(err, IsNil)
  1555  	c.Check([]string(m.CurrentKernelCommandLines), DeepEquals, []string{
  1556  		"snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1 args from gadget",
  1557  	})
  1558  }
  1559  
  1560  func (s *deviceMgrGadgetSuite) TestUpdateGadgetCommandlineWithFullArgs(c *C) {
  1561  	bootloader.Force(s.managedbl)
  1562  	s.state.Lock()
  1563  	s.setupUC20ModelWithGadget(c, "pc")
  1564  	s.mockModeenvForMode(c, "run")
  1565  	devicestate.SetBootOkRan(s.mgr, true)
  1566  	s.state.Set("seeded", true)
  1567  
  1568  	// mimic system state
  1569  	m, err := boot.ReadModeenv("")
  1570  	c.Assert(err, IsNil)
  1571  	m.CurrentKernelCommandLines = []string{
  1572  		"snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1 extra args",
  1573  	}
  1574  	c.Assert(m.Write(), IsNil)
  1575  	err = s.managedbl.SetBootVars(map[string]string{
  1576  		"snapd_extra_cmdline_args": "extra args",
  1577  		"snapd_full_cmdline_args":  "",
  1578  	})
  1579  	c.Assert(err, IsNil)
  1580  	s.managedbl.SetBootVarsCalls = 0
  1581  
  1582  	s.state.Unlock()
  1583  
  1584  	const update = true
  1585  	s.testGadgetCommandlineUpdateRun(c,
  1586  		[][]string{
  1587  			{"meta/gadget.yaml", gadgetYaml},
  1588  			{"cmdline.extra", "extra args"},
  1589  		},
  1590  		[][]string{
  1591  			{"meta/gadget.yaml", gadgetYaml},
  1592  			{"cmdline.full", "full args"},
  1593  		},
  1594  		"", "Updated kernel command line", update)
  1595  
  1596  	m, err = boot.ReadModeenv("")
  1597  	c.Assert(err, IsNil)
  1598  	c.Check([]string(m.CurrentKernelCommandLines), DeepEquals, []string{
  1599  		"snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1 extra args",
  1600  		// gadget arguments are picked up for the candidate command line
  1601  		"snapd_recovery_mode=run full args",
  1602  	})
  1603  	c.Check(s.managedbl.SetBootVarsCalls, Equals, 1)
  1604  	vars, err := s.managedbl.GetBootVars("snapd_extra_cmdline_args", "snapd_full_cmdline_args")
  1605  	c.Assert(err, IsNil)
  1606  	// bootenv was cleared
  1607  	c.Assert(vars, DeepEquals, map[string]string{
  1608  		"snapd_extra_cmdline_args": "",
  1609  		"snapd_full_cmdline_args":  "full args",
  1610  	})
  1611  }