gitee.com/mysnapcore/mysnapd@v0.1.0/boot/flags_test.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2022 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 boot_test
    21  
    22  import (
    23  	"errors"
    24  	"io/ioutil"
    25  	"os"
    26  	"path/filepath"
    27  
    28  	. "gopkg.in/check.v1"
    29  
    30  	"gitee.com/mysnapcore/mysnapd/boot"
    31  	"gitee.com/mysnapcore/mysnapd/boot/boottest"
    32  	"gitee.com/mysnapcore/mysnapd/bootloader"
    33  	"gitee.com/mysnapcore/mysnapd/bootloader/grubenv"
    34  	"gitee.com/mysnapcore/mysnapd/dirs"
    35  	"gitee.com/mysnapcore/mysnapd/snap"
    36  	"gitee.com/mysnapcore/mysnapd/testutil"
    37  )
    38  
    39  type bootFlagsSuite struct {
    40  	baseBootenvSuite
    41  }
    42  
    43  var _ = Suite(&bootFlagsSuite{})
    44  
    45  func (s *bootFlagsSuite) TestBootFlagsFamilyClassic(c *C) {
    46  	classicDev := boottest.MockDevice("")
    47  
    48  	// make bootloader.Find fail but shouldn't matter
    49  	bootloader.ForceError(errors.New("broken bootloader"))
    50  	defer bootloader.ForceError(nil)
    51  
    52  	_, err := boot.NextBootFlags(classicDev)
    53  	c.Assert(err, ErrorMatches, `cannot get boot flags on pre-UC20 device`)
    54  
    55  	err = boot.SetNextBootFlags(classicDev, "", []string{"foo"})
    56  	c.Assert(err, ErrorMatches, `cannot get boot flags on pre-UC20 device`)
    57  
    58  	_, err = boot.BootFlags(classicDev)
    59  	c.Assert(err, ErrorMatches, `cannot get boot flags on pre-UC20 device`)
    60  }
    61  
    62  func (s *bootFlagsSuite) TestBootFlagsFamilyUC16(c *C) {
    63  	coreDev := boottest.MockDevice("some-snap")
    64  
    65  	// make bootloader.Find fail but shouldn't matter
    66  	bootloader.ForceError(errors.New("broken bootloader"))
    67  	defer bootloader.ForceError(nil)
    68  
    69  	_, err := boot.NextBootFlags(coreDev)
    70  	c.Assert(err, ErrorMatches, `cannot get boot flags on pre-UC20 device`)
    71  
    72  	err = boot.SetNextBootFlags(coreDev, "", []string{"foo"})
    73  	c.Assert(err, ErrorMatches, `cannot get boot flags on pre-UC20 device`)
    74  
    75  	_, err = boot.BootFlags(coreDev)
    76  	c.Assert(err, ErrorMatches, `cannot get boot flags on pre-UC20 device`)
    77  }
    78  
    79  func setupRealGrub(c *C, rootDir, baseDir string, opts *bootloader.Options) bootloader.Bootloader {
    80  	if rootDir == "" {
    81  		rootDir = dirs.GlobalRootDir
    82  	}
    83  	grubCfg := filepath.Join(rootDir, baseDir, "grub.cfg")
    84  	err := os.MkdirAll(filepath.Dir(grubCfg), 0755)
    85  	c.Assert(err, IsNil)
    86  
    87  	err = ioutil.WriteFile(grubCfg, nil, 0644)
    88  	c.Assert(err, IsNil)
    89  
    90  	genv := grubenv.NewEnv(filepath.Join(rootDir, baseDir, "grubenv"))
    91  	err = genv.Save()
    92  	c.Assert(err, IsNil)
    93  
    94  	grubBl, err := bootloader.Find(rootDir, opts)
    95  	c.Assert(err, IsNil)
    96  	c.Assert(grubBl.Name(), Equals, "grub")
    97  
    98  	return grubBl
    99  }
   100  
   101  func (s *bootFlagsSuite) TestInitramfsActiveBootFlagsUC20InstallModeHappy(c *C) {
   102  	dir := c.MkDir()
   103  
   104  	dirs.SetRootDir(dir)
   105  	defer func() { dirs.SetRootDir("") }()
   106  
   107  	blDir := boot.InitramfsUbuntuSeedDir
   108  
   109  	setupRealGrub(c, blDir, "EFI/ubuntu", &bootloader.Options{Role: bootloader.RoleRecovery})
   110  
   111  	flags, err := boot.InitramfsActiveBootFlags(boot.ModeInstall, filepath.Join(dirs.GlobalRootDir, "/run/mnt/data/system-data"))
   112  	c.Assert(err, IsNil)
   113  	c.Assert(flags, HasLen, 0)
   114  
   115  	// if we set some flags via ubuntu-image customizations then we get them
   116  	// back
   117  
   118  	err = boot.SetBootFlagsInBootloader([]string{"factory"}, blDir)
   119  	c.Assert(err, IsNil)
   120  
   121  	flags, err = boot.InitramfsActiveBootFlags(boot.ModeInstall, filepath.Join(dirs.GlobalRootDir, "/run/mnt/data/system-data"))
   122  	c.Assert(err, IsNil)
   123  	c.Assert(flags, DeepEquals, []string{"factory"})
   124  }
   125  
   126  func (s *bootFlagsSuite) TestInitramfsActiveBootFlagsUC20FactoryResetModeHappy(c *C) {
   127  	// FactoryReset and Install run identical code, as their condition match pretty closely
   128  	// so this unit test is to reconfirm that we expect same behavior as we see in the unit
   129  	// test for install mode.
   130  	dir := c.MkDir()
   131  
   132  	dirs.SetRootDir(dir)
   133  	defer func() { dirs.SetRootDir("") }()
   134  
   135  	blDir := boot.InitramfsUbuntuSeedDir
   136  
   137  	setupRealGrub(c, blDir, "EFI/ubuntu", &bootloader.Options{Role: bootloader.RoleRecovery})
   138  
   139  	flags, err := boot.InitramfsActiveBootFlags(boot.ModeFactoryReset, filepath.Join(dirs.GlobalRootDir, "/run/mnt/data/system-data"))
   140  	c.Assert(err, IsNil)
   141  	c.Assert(flags, HasLen, 0)
   142  
   143  	// if we set some flags via ubuntu-image customizations then we get them
   144  	// back
   145  
   146  	err = boot.SetBootFlagsInBootloader([]string{"factory"}, blDir)
   147  	c.Assert(err, IsNil)
   148  
   149  	flags, err = boot.InitramfsActiveBootFlags(boot.ModeFactoryReset, filepath.Join(dirs.GlobalRootDir, "/run/mnt/data/system-data"))
   150  	c.Assert(err, IsNil)
   151  	c.Assert(flags, DeepEquals, []string{"factory"})
   152  }
   153  
   154  func (s *bootFlagsSuite) TestSetImageBootFlagsVerification(c *C) {
   155  	longVal := "longer-than-256-char-value"
   156  	for i := 0; i < 256; i++ {
   157  		longVal += "X"
   158  	}
   159  
   160  	r := boot.MockAdditionalBootFlags([]string{longVal})
   161  	defer r()
   162  
   163  	blVars := make(map[string]string)
   164  
   165  	err := boot.SetImageBootFlags([]string{"not-a-real-flag"}, blVars)
   166  	c.Assert(err, ErrorMatches, `unknown boot flags \[not-a-real-flag\] not allowed`)
   167  
   168  	err = boot.SetImageBootFlags([]string{longVal}, blVars)
   169  	c.Assert(err, ErrorMatches, "internal error: boot flags too large to fit inside bootenv value")
   170  }
   171  
   172  func (s *bootFlagsSuite) TestInitramfsActiveBootFlagsUC20RecoverModeNoop(c *C) {
   173  	dir := c.MkDir()
   174  
   175  	dirs.SetRootDir(dir)
   176  	defer func() { dirs.SetRootDir("") }()
   177  
   178  	blDir := boot.InitramfsUbuntuSeedDir
   179  
   180  	// create a grubenv to ensure that we don't return any values from there
   181  	grubBl := setupRealGrub(c, blDir, "EFI/ubuntu", &bootloader.Options{Role: bootloader.RoleRecovery})
   182  
   183  	// also create the modeenv to make sure we don't peek there either
   184  	m := boot.Modeenv{
   185  		Mode:      boot.ModeRun,
   186  		BootFlags: []string{},
   187  	}
   188  
   189  	err := os.MkdirAll(filepath.Join(dirs.GlobalRootDir, "/run/mnt/data/system-data"), 0755)
   190  	c.Assert(err, IsNil)
   191  
   192  	err = m.WriteTo(filepath.Join(dirs.GlobalRootDir, "/run/mnt/data/system-data"))
   193  	c.Assert(err, IsNil)
   194  
   195  	flags, err := boot.InitramfsActiveBootFlags(boot.ModeRecover, filepath.Join(dirs.GlobalRootDir, "/run/mnt/data/system-data"))
   196  	c.Assert(err, IsNil)
   197  	c.Assert(flags, HasLen, 0)
   198  
   199  	err = grubBl.SetBootVars(map[string]string{"snapd_boot_flags": "factory"})
   200  	c.Assert(err, IsNil)
   201  
   202  	m.BootFlags = []string{"modeenv-boot-flag"}
   203  	err = m.WriteTo(filepath.Join(dirs.GlobalRootDir, "/run/mnt/data/system-data"))
   204  	c.Assert(err, IsNil)
   205  
   206  	// still no flags since we are in recovery mode
   207  	flags, err = boot.InitramfsActiveBootFlags(boot.ModeRecover, filepath.Join(dirs.GlobalRootDir, "/run/mnt/data/system-data"))
   208  	c.Assert(err, IsNil)
   209  	c.Assert(flags, HasLen, 0)
   210  }
   211  
   212  func (s *bootFlagsSuite) testInitramfsActiveBootFlagsUC20RRunModeHappy(c *C, flagsDir string) {
   213  	dir := c.MkDir()
   214  
   215  	dirs.SetRootDir(dir)
   216  	defer func() { dirs.SetRootDir("") }()
   217  
   218  	// setup a basic empty modeenv
   219  	m := boot.Modeenv{
   220  		Mode:      boot.ModeRun,
   221  		BootFlags: []string{},
   222  	}
   223  
   224  	err := os.MkdirAll(flagsDir, 0755)
   225  	c.Assert(err, IsNil)
   226  
   227  	err = m.WriteTo(flagsDir)
   228  	c.Assert(err, IsNil)
   229  
   230  	flags, err := boot.InitramfsActiveBootFlags(boot.ModeRun, flagsDir)
   231  	c.Assert(err, IsNil)
   232  	c.Assert(flags, HasLen, 0)
   233  
   234  	m.BootFlags = []string{"factory", "other-flag"}
   235  	err = m.WriteTo(flagsDir)
   236  	c.Assert(err, IsNil)
   237  
   238  	// now some flags after we set them in the modeenv
   239  	flags, err = boot.InitramfsActiveBootFlags(boot.ModeRun, flagsDir)
   240  	c.Assert(err, IsNil)
   241  	c.Assert(flags, DeepEquals, []string{"factory", "other-flag"})
   242  }
   243  
   244  func (s *bootFlagsSuite) TestInitramfsActiveBootFlagsUC20RRunModeHappy(c *C) {
   245  	s.testInitramfsActiveBootFlagsUC20RRunModeHappy(c, filepath.Join(dirs.GlobalRootDir, "/run/mnt/data/system-data"))
   246  	s.testInitramfsActiveBootFlagsUC20RRunModeHappy(c, c.MkDir())
   247  }
   248  
   249  func (s *bootFlagsSuite) TestInitramfsSetBootFlags(c *C) {
   250  	tt := []struct {
   251  		flags               []string
   252  		expFlags            []string
   253  		expFlagFile         string
   254  		bootFlagsErr        string
   255  		bootFlagsErrUnknown bool
   256  	}{
   257  		{
   258  			flags:       []string{"factory"},
   259  			expFlags:    []string{"factory"},
   260  			expFlagFile: "factory",
   261  		},
   262  		{
   263  			flags:               []string{"factory", "unknown-new-flag"},
   264  			expFlagFile:         "factory,unknown-new-flag",
   265  			expFlags:            []string{"factory"},
   266  			bootFlagsErr:        `unknown boot flags \[unknown-new-flag\] not allowed`,
   267  			bootFlagsErrUnknown: true,
   268  		},
   269  		{
   270  			flags:       []string{"", "", "", "factory"},
   271  			expFlags:    []string{"factory"},
   272  			expFlagFile: "factory",
   273  		},
   274  		{
   275  			flags:    []string{},
   276  			expFlags: []string{},
   277  		},
   278  	}
   279  
   280  	uc20Dev := boottest.MockUC20Device("run", nil)
   281  
   282  	for _, t := range tt {
   283  		err := boot.InitramfsExposeBootFlagsForSystem(t.flags)
   284  		c.Assert(err, IsNil)
   285  		c.Assert(filepath.Join(dirs.SnapRunDir, "boot-flags"), testutil.FileEquals, t.expFlagFile)
   286  
   287  		// also read the flags as if from user space to make sure they match
   288  		flags, err := boot.BootFlags(uc20Dev)
   289  		if t.bootFlagsErr != "" {
   290  			c.Assert(err, ErrorMatches, t.bootFlagsErr)
   291  			if t.bootFlagsErrUnknown {
   292  				c.Assert(boot.IsUnknownBootFlagError(err), Equals, true)
   293  			}
   294  		} else {
   295  			c.Assert(err, IsNil)
   296  		}
   297  		c.Assert(flags, DeepEquals, t.expFlags)
   298  	}
   299  }
   300  
   301  func (s *bootFlagsSuite) TestUserspaceBootFlagsUC20(c *C) {
   302  	tt := []struct {
   303  		beforeFlags []string
   304  		flags       []string
   305  		expFlags    []string
   306  		err         string
   307  	}{
   308  		{
   309  			beforeFlags: []string{},
   310  			flags:       []string{"factory"},
   311  			expFlags:    []string{"factory"},
   312  		},
   313  		{
   314  			flags: []string{"factory", "new-unsupported-flag"},
   315  			err:   `unknown boot flags \[new-unsupported-flag\] not allowed`,
   316  		},
   317  		{
   318  			flags: []string{""},
   319  			err:   `unknown boot flags \[\"\"\] not allowed`,
   320  		},
   321  		{
   322  			beforeFlags: []string{},
   323  			flags:       []string{},
   324  		},
   325  		{
   326  			beforeFlags: []string{"factory"},
   327  			flags:       []string{},
   328  		},
   329  		{
   330  			beforeFlags: []string{"foobar"},
   331  			flags:       []string{"factory"},
   332  			expFlags:    []string{"factory"},
   333  		},
   334  	}
   335  
   336  	uc20Dev := boottest.MockUC20Device("run", nil)
   337  
   338  	m := boot.Modeenv{
   339  		Mode:      boot.ModeInstall,
   340  		BootFlags: []string{},
   341  	}
   342  
   343  	for _, t := range tt {
   344  		m.BootFlags = t.beforeFlags
   345  		err := m.WriteTo("")
   346  		c.Assert(err, IsNil)
   347  
   348  		err = boot.SetNextBootFlags(uc20Dev, "", t.flags)
   349  		if t.err != "" {
   350  			c.Assert(err, ErrorMatches, t.err)
   351  			continue
   352  		}
   353  
   354  		c.Assert(err, IsNil)
   355  
   356  		// re-read modeenv
   357  		m2, err := boot.ReadModeenv("")
   358  		c.Assert(err, IsNil)
   359  		c.Assert(m2.BootFlags, DeepEquals, t.expFlags)
   360  
   361  		// get the next boot flags with NextBootFlags and compare with expected
   362  		flags, err := boot.NextBootFlags(uc20Dev)
   363  		c.Assert(flags, DeepEquals, t.expFlags)
   364  		c.Assert(err, IsNil)
   365  	}
   366  }
   367  
   368  func (s *bootFlagsSuite) TestRunModeRootfs(c *C) {
   369  	uc20Dev := boottest.MockUC20Device("run", nil)
   370  	classicModesDev := boottest.MockClassicWithModesDevice("run", nil)
   371  
   372  	tt := []struct {
   373  		mode               string
   374  		dev                snap.Device
   375  		createExpDirs      bool
   376  		expDirs            []string
   377  		noExpDirRootPrefix bool
   378  		degradedJSON       string
   379  		err                string
   380  		comment            string
   381  	}{
   382  		{
   383  			mode:    boot.ModeRun,
   384  			dev:     uc20Dev,
   385  			expDirs: []string{"/run/mnt/data", ""},
   386  			comment: "run mode",
   387  		},
   388  		{
   389  			mode:    boot.ModeRun,
   390  			dev:     classicModesDev,
   391  			expDirs: []string{"/run/mnt/data", ""},
   392  			comment: "run mode (classic)",
   393  		},
   394  		{
   395  			mode:    boot.ModeInstall,
   396  			dev:     uc20Dev,
   397  			comment: "install mode before partition creation",
   398  		},
   399  		{
   400  			mode:    boot.ModeInstall,
   401  			dev:     classicModesDev,
   402  			comment: "install mode before partition creation (classic)",
   403  		},
   404  		{
   405  			mode:    boot.ModeFactoryReset,
   406  			dev:     uc20Dev,
   407  			comment: "factory-reset mode before partition is recreated",
   408  		},
   409  		{
   410  			mode:    boot.ModeFactoryReset,
   411  			dev:     uc20Dev,
   412  			comment: "factory-reset mode before partition is recreated (classic)",
   413  		},
   414  		{
   415  			mode:          boot.ModeInstall,
   416  			dev:           uc20Dev,
   417  			expDirs:       []string{"/run/mnt/ubuntu-data"},
   418  			createExpDirs: true,
   419  			comment:       "install mode after partition creation",
   420  		},
   421  		{
   422  			mode:          boot.ModeInstall,
   423  			dev:           classicModesDev,
   424  			expDirs:       []string{"/run/mnt/ubuntu-data"},
   425  			createExpDirs: true,
   426  			comment:       "install mode after partition creation (classic)",
   427  		},
   428  		{
   429  			mode:          boot.ModeFactoryReset,
   430  			dev:           uc20Dev,
   431  			expDirs:       []string{"/run/mnt/ubuntu-data"},
   432  			createExpDirs: true,
   433  			comment:       "factory-reset mode after partition creation",
   434  		},
   435  		{
   436  			mode:          boot.ModeFactoryReset,
   437  			dev:           classicModesDev,
   438  			expDirs:       []string{"/run/mnt/ubuntu-data"},
   439  			createExpDirs: true,
   440  			comment:       "factory-reset mode after partition creation (classic)",
   441  		},
   442  		{
   443  			mode: boot.ModeRecover,
   444  			dev:  uc20Dev,
   445  			degradedJSON: `
   446  			{
   447  				"ubuntu-data": {
   448  					"mount-state": "mounted",
   449  					"mount-location": "/host/ubuntu-data"
   450  				}
   451  			}
   452  			`,
   453  			expDirs:            []string{"/host/ubuntu-data"},
   454  			noExpDirRootPrefix: true,
   455  			comment:            "recover degraded.json default mounted location",
   456  		},
   457  		{
   458  			mode: boot.ModeRecover,
   459  			dev:  uc20Dev,
   460  			degradedJSON: `
   461  			{
   462  				"ubuntu-data": {
   463  					"mount-state": "mounted",
   464  					"mount-location": "/host/elsewhere/ubuntu-data"
   465  				}
   466  			}
   467  			`,
   468  			expDirs:            []string{"/host/elsewhere/ubuntu-data"},
   469  			noExpDirRootPrefix: true,
   470  			comment:            "recover degraded.json alternative mounted location",
   471  		},
   472  		{
   473  			mode: boot.ModeRecover,
   474  			dev:  uc20Dev,
   475  			degradedJSON: `
   476  			{
   477  				"ubuntu-data": {
   478  					"mount-state": "error-mounting"
   479  				}
   480  			}
   481  			`,
   482  			comment: "recover degraded.json error-mounting",
   483  		},
   484  		{
   485  			mode: boot.ModeRecover,
   486  			dev:  uc20Dev,
   487  			degradedJSON: `
   488  			{
   489  				"ubuntu-data": {
   490  					"mount-state": "mounted-untrusted"
   491  				}
   492  			}
   493  			`,
   494  			comment: "recover degraded.json mounted-untrusted",
   495  		},
   496  		{
   497  			mode: boot.ModeRecover,
   498  			dev:  uc20Dev,
   499  			degradedJSON: `
   500  			{
   501  				"ubuntu-data": {
   502  					"mount-state": "absent-but-optional"
   503  				}
   504  			}
   505  			`,
   506  			comment: "recover degraded.json absent-but-optional",
   507  		},
   508  		{
   509  			mode: boot.ModeRecover,
   510  			dev:  uc20Dev,
   511  			degradedJSON: `
   512  			{
   513  				"ubuntu-data": {
   514  					"mount-state": "new-wild-unknown-state"
   515  				}
   516  			}
   517  			`,
   518  			comment: "recover degraded.json new-wild-unknown-state",
   519  		},
   520  		{
   521  			mode:    "",
   522  			dev:     uc20Dev,
   523  			err:     "system mode is unsupported",
   524  			comment: "unsupported system mode",
   525  		},
   526  	}
   527  	for _, t := range tt {
   528  		comment := Commentf(t.comment)
   529  		if t.degradedJSON != "" {
   530  			rootdir := c.MkDir()
   531  			dirs.SetRootDir(rootdir)
   532  			defer func() { dirs.SetRootDir("") }()
   533  
   534  			degradedJSON := filepath.Join(dirs.SnapBootstrapRunDir, "degraded.json")
   535  			err := os.MkdirAll(dirs.SnapBootstrapRunDir, 0755)
   536  			c.Assert(err, IsNil, comment)
   537  
   538  			err = ioutil.WriteFile(degradedJSON, []byte(t.degradedJSON), 0644)
   539  			c.Assert(err, IsNil, comment)
   540  		}
   541  
   542  		if t.createExpDirs {
   543  			for _, dir := range t.expDirs {
   544  				err := os.MkdirAll(filepath.Join(dirs.GlobalRootDir, dir), 0755)
   545  				c.Assert(err, IsNil, comment)
   546  			}
   547  		}
   548  
   549  		dataMountDirs, err := boot.HostUbuntuDataForMode(t.mode, t.dev.Model())
   550  		if t.err != "" {
   551  			c.Assert(err, ErrorMatches, t.err, comment)
   552  			c.Assert(dataMountDirs, IsNil)
   553  			continue
   554  		}
   555  		c.Assert(err, IsNil, comment)
   556  
   557  		if t.expDirs != nil && !t.noExpDirRootPrefix {
   558  			// prefix all the dirs in expDirs with dirs.GlobalRootDir for easier
   559  			// test case writing above
   560  			prefixedDir := make([]string, len(t.expDirs))
   561  			for i, dir := range t.expDirs {
   562  				prefixedDir[i] = filepath.Join(dirs.GlobalRootDir, dir)
   563  			}
   564  			c.Assert(dataMountDirs, DeepEquals, prefixedDir, comment)
   565  		} else {
   566  			c.Assert(dataMountDirs, DeepEquals, t.expDirs, comment)
   567  		}
   568  	}
   569  }