github.com/ubuntu-core/snappy@v0.0.0-20210827154228-9e584df982bb/boot/modeenv_test.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2019-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 boot_test
    21  
    22  import (
    23  	"bytes"
    24  	"encoding/json"
    25  	"fmt"
    26  	"io/ioutil"
    27  	"os"
    28  	"path/filepath"
    29  	"strings"
    30  
    31  	"github.com/mvo5/goconfigparser"
    32  	. "gopkg.in/check.v1"
    33  
    34  	"github.com/snapcore/snapd/asserts"
    35  	"github.com/snapcore/snapd/boot"
    36  	"github.com/snapcore/snapd/dirs"
    37  	"github.com/snapcore/snapd/testutil"
    38  )
    39  
    40  // baseBootSuite is used to setup the common test environment
    41  type modeenvSuite struct {
    42  	testutil.BaseTest
    43  
    44  	tmpdir          string
    45  	mockModeenvPath string
    46  }
    47  
    48  var _ = Suite(&modeenvSuite{})
    49  
    50  func (s *modeenvSuite) SetUpTest(c *C) {
    51  	s.tmpdir = c.MkDir()
    52  	s.mockModeenvPath = filepath.Join(s.tmpdir, dirs.SnapModeenvFile)
    53  }
    54  
    55  func (s *modeenvSuite) TestKnownKnown(c *C) {
    56  	// double check keys as found with reflect
    57  	c.Check(boot.ModeenvKnownKeys, DeepEquals, map[string]bool{
    58  		"mode":                     true,
    59  		"recovery_system":          true,
    60  		"current_recovery_systems": true,
    61  		"good_recovery_systems":    true,
    62  		"boot_flags":               true,
    63  		// keep this comment to make old go fmt happy
    64  		"base":                  true,
    65  		"try_base":              true,
    66  		"base_status":           true,
    67  		"current_kernels":       true,
    68  		"model":                 true,
    69  		"grade":                 true,
    70  		"model_sign_key_id":     true,
    71  		"try_model":             true,
    72  		"try_grade":             true,
    73  		"try_model_sign_key_id": true,
    74  		// keep this comment to make old go fmt happy
    75  		"current_kernel_command_lines":         true,
    76  		"current_trusted_boot_assets":          true,
    77  		"current_trusted_recovery_boot_assets": true,
    78  	})
    79  }
    80  
    81  func (s *modeenvSuite) TestReadEmptyErrors(c *C) {
    82  	modeenv, err := boot.ReadModeenv("/no/such/file")
    83  	c.Assert(os.IsNotExist(err), Equals, true)
    84  	c.Assert(modeenv, IsNil)
    85  }
    86  
    87  func (s *modeenvSuite) makeMockModeenvFile(c *C, content string) {
    88  	err := os.MkdirAll(filepath.Dir(s.mockModeenvPath), 0755)
    89  	c.Assert(err, IsNil)
    90  	err = ioutil.WriteFile(s.mockModeenvPath, []byte(content), 0644)
    91  	c.Assert(err, IsNil)
    92  }
    93  
    94  func (s *modeenvSuite) TestWasReadSanity(c *C) {
    95  	modeenv := &boot.Modeenv{}
    96  	c.Check(modeenv.WasRead(), Equals, false)
    97  }
    98  
    99  func (s *modeenvSuite) TestReadEmpty(c *C) {
   100  	s.makeMockModeenvFile(c, "")
   101  
   102  	modeenv, err := boot.ReadModeenv(s.tmpdir)
   103  	c.Assert(err, ErrorMatches, "internal error: mode is unset")
   104  	c.Assert(modeenv, IsNil)
   105  }
   106  
   107  func (s *modeenvSuite) TestReadMode(c *C) {
   108  	s.makeMockModeenvFile(c, "mode=run")
   109  
   110  	modeenv, err := boot.ReadModeenv(s.tmpdir)
   111  	c.Assert(err, IsNil)
   112  	c.Check(modeenv.Mode, Equals, "run")
   113  	c.Check(modeenv.RecoverySystem, Equals, "")
   114  	c.Check(modeenv.Base, Equals, "")
   115  }
   116  
   117  func (s *modeenvSuite) TestDeepEqualDiskVsMemoryInvariant(c *C) {
   118  	s.makeMockModeenvFile(c, `mode=recovery
   119  recovery_system=20191126
   120  base=core20_123.snap
   121  try_base=core20_124.snap
   122  base_status=try
   123  `)
   124  
   125  	diskModeenv, err := boot.ReadModeenv(s.tmpdir)
   126  	c.Assert(err, IsNil)
   127  	inMemoryModeenv := &boot.Modeenv{
   128  		Mode:           "recovery",
   129  		RecoverySystem: "20191126",
   130  		Base:           "core20_123.snap",
   131  		TryBase:        "core20_124.snap",
   132  		BaseStatus:     "try",
   133  	}
   134  	c.Assert(inMemoryModeenv.DeepEqual(diskModeenv), Equals, true)
   135  	c.Assert(diskModeenv.DeepEqual(inMemoryModeenv), Equals, true)
   136  }
   137  
   138  func (s *modeenvSuite) TestCopyDeepEquals(c *C) {
   139  	s.makeMockModeenvFile(c, `mode=recovery
   140  recovery_system=20191126
   141  base=core20_123.snap
   142  try_base=core20_124.snap
   143  base_status=try
   144  current_trusted_boot_assets={"thing1":["hash1","hash2"],"thing2":["hash3"]}
   145  current_kernel_command_lines=["foo", "bar"]
   146  `)
   147  
   148  	diskModeenv, err := boot.ReadModeenv(s.tmpdir)
   149  	c.Assert(err, IsNil)
   150  	inMemoryModeenv := &boot.Modeenv{
   151  		Mode:           "recovery",
   152  		RecoverySystem: "20191126",
   153  		Base:           "core20_123.snap",
   154  		TryBase:        "core20_124.snap",
   155  		BaseStatus:     "try",
   156  		CurrentTrustedBootAssets: boot.BootAssetsMap{
   157  			"thing1": {"hash1", "hash2"},
   158  			"thing2": {"hash3"},
   159  		},
   160  		CurrentKernelCommandLines: boot.BootCommandLines{
   161  			"foo", "bar",
   162  		},
   163  	}
   164  
   165  	c.Assert(inMemoryModeenv.DeepEqual(diskModeenv), Equals, true)
   166  	c.Assert(diskModeenv.DeepEqual(inMemoryModeenv), Equals, true)
   167  
   168  	diskModeenv2, err := diskModeenv.Copy()
   169  	c.Assert(err, IsNil)
   170  	c.Assert(diskModeenv.DeepEqual(diskModeenv2), Equals, true)
   171  	c.Assert(diskModeenv2.DeepEqual(diskModeenv), Equals, true)
   172  	c.Assert(inMemoryModeenv.DeepEqual(diskModeenv2), Equals, true)
   173  	c.Assert(diskModeenv2.DeepEqual(inMemoryModeenv), Equals, true)
   174  
   175  	inMemoryModeenv2, err := inMemoryModeenv.Copy()
   176  	c.Assert(err, IsNil)
   177  	c.Assert(inMemoryModeenv.DeepEqual(inMemoryModeenv2), Equals, true)
   178  	c.Assert(inMemoryModeenv2.DeepEqual(inMemoryModeenv), Equals, true)
   179  	c.Assert(inMemoryModeenv2.DeepEqual(diskModeenv), Equals, true)
   180  	c.Assert(diskModeenv.DeepEqual(inMemoryModeenv2), Equals, true)
   181  }
   182  
   183  func (s *modeenvSuite) TestCopyDiskWriteWorks(c *C) {
   184  	s.makeMockModeenvFile(c, `mode=recovery
   185  recovery_system=20191126
   186  base=core20_123.snap
   187  try_base=core20_124.snap
   188  base_status=try
   189  `)
   190  
   191  	diskModeenv, err := boot.ReadModeenv(s.tmpdir)
   192  	c.Assert(err, IsNil)
   193  	dupDiskModeenv, err := diskModeenv.Copy()
   194  	c.Assert(err, IsNil)
   195  
   196  	// move the original file out of the way
   197  	err = os.Rename(dirs.SnapModeenvFileUnder(s.tmpdir), dirs.SnapModeenvFileUnder(s.tmpdir)+".orig")
   198  	c.Assert(err, IsNil)
   199  	c.Assert(dirs.SnapModeenvFileUnder(s.tmpdir), testutil.FileAbsent)
   200  
   201  	// write the duplicate, it should write to the same original location and it
   202  	// should be the same content
   203  	err = dupDiskModeenv.Write()
   204  	c.Assert(err, IsNil)
   205  	c.Assert(dirs.SnapModeenvFileUnder(s.tmpdir), testutil.FilePresent)
   206  	origBytes, err := ioutil.ReadFile(dirs.SnapModeenvFileUnder(s.tmpdir) + ".orig")
   207  	c.Assert(err, IsNil)
   208  	// the files should be the same
   209  	c.Assert(dirs.SnapModeenvFileUnder(s.tmpdir), testutil.FileEquals, string(origBytes))
   210  }
   211  
   212  func (s *modeenvSuite) TestCopyMemoryWriteFails(c *C) {
   213  	inMemoryModeenv := &boot.Modeenv{
   214  		Mode:           "recovery",
   215  		RecoverySystem: "20191126",
   216  		Base:           "core20_123.snap",
   217  		TryBase:        "core20_124.snap",
   218  		BaseStatus:     "try",
   219  	}
   220  	dupInMemoryModeenv, err := inMemoryModeenv.Copy()
   221  	c.Assert(err, IsNil)
   222  
   223  	// write the duplicate, it should fail
   224  	err = dupInMemoryModeenv.Write()
   225  	c.Assert(err, ErrorMatches, "internal error: must use WriteTo with modeenv not read from disk")
   226  }
   227  
   228  func (s *modeenvSuite) TestDeepEquals(c *C) {
   229  	// start with two identical modeenvs
   230  	modeenv1 := &boot.Modeenv{
   231  		Mode:                   "recovery",
   232  		RecoverySystem:         "20191126",
   233  		CurrentRecoverySystems: []string{"1", "2"},
   234  		GoodRecoverySystems:    []string{"3"},
   235  
   236  		Base:           "core20_123.snap",
   237  		TryBase:        "core20_124.snap",
   238  		BaseStatus:     "try",
   239  		CurrentKernels: []string{"k1", "k2"},
   240  
   241  		Model:          "model",
   242  		BrandID:        "brand",
   243  		Grade:          "secured",
   244  		ModelSignKeyID: "9tydnLa6MTJ-jaQTFUXEwHl1yRx7ZS4K5cyFDhYDcPzhS7uyEkDxdUjg9g08BtNn",
   245  
   246  		BootFlags: []string{"foo", "factory"},
   247  
   248  		CurrentTrustedBootAssets: boot.BootAssetsMap{
   249  			"thing1": {"hash1", "hash2"},
   250  			"thing2": {"hash3"},
   251  		},
   252  
   253  		CurrentKernelCommandLines: boot.BootCommandLines{
   254  			"foo",
   255  			"foo bar",
   256  		},
   257  	}
   258  
   259  	modeenv2 := &boot.Modeenv{
   260  		Mode:                   "recovery",
   261  		RecoverySystem:         "20191126",
   262  		CurrentRecoverySystems: []string{"1", "2"},
   263  		GoodRecoverySystems:    []string{"3"},
   264  
   265  		Base:           "core20_123.snap",
   266  		TryBase:        "core20_124.snap",
   267  		BaseStatus:     "try",
   268  		CurrentKernels: []string{"k1", "k2"},
   269  
   270  		Model:          "model",
   271  		BrandID:        "brand",
   272  		Grade:          "secured",
   273  		ModelSignKeyID: "9tydnLa6MTJ-jaQTFUXEwHl1yRx7ZS4K5cyFDhYDcPzhS7uyEkDxdUjg9g08BtNn",
   274  
   275  		BootFlags: []string{"foo", "factory"},
   276  
   277  		CurrentTrustedBootAssets: boot.BootAssetsMap{
   278  			"thing1": {"hash1", "hash2"},
   279  			"thing2": {"hash3"},
   280  		},
   281  
   282  		CurrentKernelCommandLines: boot.BootCommandLines{
   283  			"foo",
   284  			"foo bar",
   285  		},
   286  	}
   287  
   288  	// same object should be the same
   289  	c.Assert(modeenv1.DeepEqual(modeenv1), Equals, true)
   290  
   291  	// no difference should be the same at the start
   292  	c.Assert(modeenv1.DeepEqual(modeenv2), Equals, true)
   293  	c.Assert(modeenv2.DeepEqual(modeenv1), Equals, true)
   294  
   295  	// invert CurrentKernels
   296  	modeenv2.CurrentKernels = []string{"k2", "k1"}
   297  	c.Assert(modeenv1.DeepEqual(modeenv2), Equals, false)
   298  	c.Assert(modeenv2.DeepEqual(modeenv1), Equals, false)
   299  
   300  	// make CurrentKernels capitalized
   301  	modeenv2.CurrentKernels = []string{"K1", "k2"}
   302  	c.Assert(modeenv1.DeepEqual(modeenv2), Equals, false)
   303  	c.Assert(modeenv2.DeepEqual(modeenv1), Equals, false)
   304  
   305  	// make CurrentKernels disappear
   306  	modeenv2.CurrentKernels = nil
   307  	c.Assert(modeenv1.DeepEqual(modeenv2), Equals, false)
   308  	c.Assert(modeenv2.DeepEqual(modeenv1), Equals, false)
   309  
   310  	// make it identical again
   311  	modeenv2.CurrentKernels = []string{"k1", "k2"}
   312  	c.Assert(modeenv1.DeepEqual(modeenv2), Equals, true)
   313  	// change kernel command lines
   314  	modeenv2.CurrentKernelCommandLines = boot.BootCommandLines{
   315  		// reversed order
   316  		"foo bar",
   317  		"foo",
   318  	}
   319  	c.Assert(modeenv1.DeepEqual(modeenv2), Equals, false)
   320  	// clear kernel command lines list
   321  	modeenv2.CurrentKernelCommandLines = nil
   322  	c.Assert(modeenv1.DeepEqual(modeenv2), Equals, false)
   323  
   324  	// make it identical again
   325  	modeenv2.CurrentKernelCommandLines = boot.BootCommandLines{
   326  		"foo",
   327  		"foo bar",
   328  	}
   329  	c.Assert(modeenv1.DeepEqual(modeenv2), Equals, true)
   330  
   331  	// change the list of current recovery systems
   332  	modeenv2.CurrentRecoverySystems = append(modeenv2.CurrentRecoverySystems, "1234")
   333  	c.Assert(modeenv1.DeepEqual(modeenv2), Equals, false)
   334  	// make it identical again
   335  	modeenv2.CurrentRecoverySystems = []string{"1", "2"}
   336  	c.Assert(modeenv1.DeepEqual(modeenv2), Equals, true)
   337  
   338  	// change the list of good recovery systems
   339  	modeenv2.GoodRecoverySystems = append(modeenv2.GoodRecoverySystems, "999")
   340  	c.Assert(modeenv1.DeepEqual(modeenv2), Equals, false)
   341  	// restore it
   342  	modeenv2.GoodRecoverySystems = modeenv2.GoodRecoverySystems[:len(modeenv2.GoodRecoverySystems)-1]
   343  
   344  	// change the sign key ID
   345  	modeenv2.ModelSignKeyID = "EAD4DbLxK_kn0gzNCXOs3kd6DeMU3f-L6BEsSEuJGBqCORR0gXkdDxMbOm11mRFu"
   346  	c.Assert(modeenv1.DeepEqual(modeenv2), Equals, false)
   347  }
   348  
   349  func (s *modeenvSuite) TestReadModeWithRecoverySystem(c *C) {
   350  	s.makeMockModeenvFile(c, `mode=recovery
   351  recovery_system=20191126
   352  `)
   353  
   354  	modeenv, err := boot.ReadModeenv(s.tmpdir)
   355  	c.Assert(err, IsNil)
   356  	c.Check(modeenv.Mode, Equals, "recovery")
   357  	c.Check(modeenv.RecoverySystem, Equals, "20191126")
   358  }
   359  
   360  func (s *modeenvSuite) TestReadModeenvWithUnknownKeysKeepsWrites(c *C) {
   361  	s.makeMockModeenvFile(c, `first_unknown=thing
   362  mode=recovery
   363  recovery_system=20191126
   364  unknown_key=some unknown value
   365  a_key=other
   366  `)
   367  
   368  	modeenv, err := boot.ReadModeenv(s.tmpdir)
   369  	c.Assert(err, IsNil)
   370  	c.Check(modeenv.Mode, Equals, "recovery")
   371  	c.Check(modeenv.RecoverySystem, Equals, "20191126")
   372  
   373  	c.Assert(modeenv.Write(), IsNil)
   374  
   375  	c.Assert(s.mockModeenvPath, testutil.FileEquals, `mode=recovery
   376  recovery_system=20191126
   377  a_key=other
   378  first_unknown=thing
   379  unknown_key=some unknown value
   380  `)
   381  }
   382  
   383  func (s *modeenvSuite) TestReadModeenvWithUnknownKeysDeepEqualsSameWithoutUnknownKeys(c *C) {
   384  	s.makeMockModeenvFile(c, `first_unknown=thing
   385  mode=recovery
   386  recovery_system=20191126
   387  try_base=core20_124.snap
   388  base_status=try
   389  unknown_key=some unknown value
   390  current_trusted_boot_assets={"grubx64.efi":["hash1","hash2"]}
   391  current_trusted_recovery_boot_assets={"bootx64.efi":["shimhash1","shimhash2"],"grubx64.efi":["recovery-hash1"]}
   392  a_key=other
   393  `)
   394  
   395  	modeenvWithExtraKeys, err := boot.ReadModeenv(s.tmpdir)
   396  	c.Assert(err, IsNil)
   397  	c.Check(modeenvWithExtraKeys.Mode, Equals, "recovery")
   398  	c.Check(modeenvWithExtraKeys.RecoverySystem, Equals, "20191126")
   399  
   400  	// should be the same as one that with just those keys in memory
   401  	c.Assert(modeenvWithExtraKeys.DeepEqual(&boot.Modeenv{
   402  		Mode:           "recovery",
   403  		RecoverySystem: "20191126",
   404  		TryBase:        "core20_124.snap",
   405  		BaseStatus:     boot.TryStatus,
   406  		CurrentTrustedBootAssets: boot.BootAssetsMap{
   407  			"grubx64.efi": []string{"hash1", "hash2"},
   408  		},
   409  		CurrentTrustedRecoveryBootAssets: boot.BootAssetsMap{
   410  			"bootx64.efi": []string{"shimhash1", "shimhash2"},
   411  			"grubx64.efi": []string{"recovery-hash1"},
   412  		},
   413  	}), Equals, true)
   414  }
   415  
   416  func (s *modeenvSuite) TestReadModeWithBase(c *C) {
   417  	s.makeMockModeenvFile(c, `mode=recovery
   418  recovery_system=20191126
   419  base=core20_123.snap
   420  try_base=core20_124.snap
   421  base_status=try
   422  `)
   423  
   424  	modeenv, err := boot.ReadModeenv(s.tmpdir)
   425  	c.Assert(err, IsNil)
   426  	c.Check(modeenv.Mode, Equals, "recovery")
   427  	c.Check(modeenv.RecoverySystem, Equals, "20191126")
   428  	c.Check(modeenv.Base, Equals, "core20_123.snap")
   429  	c.Check(modeenv.TryBase, Equals, "core20_124.snap")
   430  	c.Check(modeenv.BaseStatus, Equals, boot.TryStatus)
   431  }
   432  
   433  func (s *modeenvSuite) TestReadModeWithGrade(c *C) {
   434  	s.makeMockModeenvFile(c, `mode=run
   435  grade=dangerous
   436  `)
   437  	modeenv, err := boot.ReadModeenv(s.tmpdir)
   438  	c.Assert(err, IsNil)
   439  	c.Check(modeenv.Mode, Equals, "run")
   440  	c.Check(modeenv.Grade, Equals, "dangerous")
   441  
   442  	s.makeMockModeenvFile(c, `mode=run
   443  grade=some-random-grade-string
   444  `)
   445  	modeenv, err = boot.ReadModeenv(s.tmpdir)
   446  	c.Assert(err, IsNil)
   447  	c.Check(modeenv.Mode, Equals, "run")
   448  	c.Check(modeenv.Grade, Equals, "some-random-grade-string")
   449  }
   450  
   451  func (s *modeenvSuite) TestReadModeWithModel(c *C) {
   452  	tt := []struct {
   453  		entry        string
   454  		model, brand string
   455  	}{
   456  		{
   457  			entry: "my-brand/my-model",
   458  			brand: "my-brand",
   459  			model: "my-model",
   460  		}, {
   461  			entry: "my-brand/",
   462  		}, {
   463  			entry: "my-model/",
   464  		}, {
   465  			entry: "foobar",
   466  		}, {
   467  			entry: "/",
   468  		}, {
   469  			entry: ",",
   470  		}, {
   471  			entry: "",
   472  		},
   473  	}
   474  
   475  	for _, t := range tt {
   476  		s.makeMockModeenvFile(c, `mode=run
   477  model=`+t.entry+"\n")
   478  		modeenv, err := boot.ReadModeenv(s.tmpdir)
   479  		c.Assert(err, IsNil)
   480  		c.Check(modeenv.Mode, Equals, "run")
   481  		c.Check(modeenv.Model, Equals, t.model)
   482  		c.Check(modeenv.BrandID, Equals, t.brand)
   483  	}
   484  }
   485  
   486  func (s *modeenvSuite) TestReadModeWithCurrentKernels(c *C) {
   487  
   488  	tt := []struct {
   489  		kernelString    string
   490  		expectedKernels []string
   491  	}{
   492  		{
   493  			"pc-kernel_1.snap",
   494  			[]string{"pc-kernel_1.snap"},
   495  		},
   496  		{
   497  			"pc-kernel_1.snap,pc-kernel_2.snap",
   498  			[]string{"pc-kernel_1.snap", "pc-kernel_2.snap"},
   499  		},
   500  		{
   501  			"pc-kernel_1.snap,,,,,pc-kernel_2.snap",
   502  			[]string{"pc-kernel_1.snap", "pc-kernel_2.snap"},
   503  		},
   504  		// we should be robust in parsing the modeenv against garbage
   505  		{
   506  			`pc-kernel_1.snap,this-is-not-a-real-snap$%^&^%$#@#$%^%"$,pc-kernel_2.snap`,
   507  			[]string{"pc-kernel_1.snap", `this-is-not-a-real-snap$%^&^%$#@#$%^%"$`, "pc-kernel_2.snap"},
   508  		},
   509  		{",,,", nil},
   510  		{"", nil},
   511  	}
   512  
   513  	for _, t := range tt {
   514  		s.makeMockModeenvFile(c, `mode=recovery
   515  recovery_system=20191126
   516  current_kernels=`+t.kernelString+"\n")
   517  
   518  		modeenv, err := boot.ReadModeenv(s.tmpdir)
   519  		c.Assert(err, IsNil)
   520  		c.Check(modeenv.Mode, Equals, "recovery")
   521  		c.Check(modeenv.RecoverySystem, Equals, "20191126")
   522  		c.Check(len(modeenv.CurrentKernels), Equals, len(t.expectedKernels))
   523  		if len(t.expectedKernels) != 0 {
   524  			c.Check(modeenv.CurrentKernels, DeepEquals, t.expectedKernels)
   525  		}
   526  	}
   527  }
   528  
   529  func (s *modeenvSuite) TestWriteToNonExisting(c *C) {
   530  	c.Assert(s.mockModeenvPath, testutil.FileAbsent)
   531  
   532  	modeenv := &boot.Modeenv{Mode: "run"}
   533  	err := modeenv.WriteTo(s.tmpdir)
   534  	c.Assert(err, IsNil)
   535  
   536  	c.Assert(s.mockModeenvPath, testutil.FileEquals, "mode=run\n")
   537  }
   538  
   539  func (s *modeenvSuite) TestWriteToExisting(c *C) {
   540  	s.makeMockModeenvFile(c, "mode=run")
   541  
   542  	modeenv, err := boot.ReadModeenv(s.tmpdir)
   543  	c.Assert(err, IsNil)
   544  	modeenv.Mode = "recovery"
   545  	err = modeenv.WriteTo(s.tmpdir)
   546  	c.Assert(err, IsNil)
   547  
   548  	c.Assert(s.mockModeenvPath, testutil.FileEquals, "mode=recovery\n")
   549  }
   550  
   551  func (s *modeenvSuite) TestWriteExisting(c *C) {
   552  	s.makeMockModeenvFile(c, "mode=run")
   553  
   554  	modeenv, err := boot.ReadModeenv(s.tmpdir)
   555  	c.Assert(err, IsNil)
   556  	modeenv.Mode = "recovery"
   557  	err = modeenv.Write()
   558  	c.Assert(err, IsNil)
   559  
   560  	c.Assert(s.mockModeenvPath, testutil.FileEquals, "mode=recovery\n")
   561  }
   562  
   563  func (s *modeenvSuite) TestWriteFreshError(c *C) {
   564  	modeenv := &boot.Modeenv{Mode: "recovery"}
   565  
   566  	err := modeenv.Write()
   567  	c.Assert(err, ErrorMatches, `internal error: must use WriteTo with modeenv not read from disk`)
   568  }
   569  
   570  func (s *modeenvSuite) TestWriteIncompleteModelBrand(c *C) {
   571  	modeenv := &boot.Modeenv{
   572  		Mode:  "run",
   573  		Grade: "dangerous",
   574  	}
   575  
   576  	err := modeenv.WriteTo(s.tmpdir)
   577  	c.Assert(err, ErrorMatches, `internal error: model is unset`)
   578  
   579  	modeenv.Model = "bar"
   580  	err = modeenv.WriteTo(s.tmpdir)
   581  	c.Assert(err, ErrorMatches, `internal error: brand is unset`)
   582  
   583  	modeenv.BrandID = "foo"
   584  	modeenv.TryGrade = "dangerous"
   585  	err = modeenv.WriteTo(s.tmpdir)
   586  	c.Assert(err, ErrorMatches, `internal error: try model is unset`)
   587  
   588  	modeenv.TryModel = "bar"
   589  	err = modeenv.WriteTo(s.tmpdir)
   590  	c.Assert(err, ErrorMatches, `internal error: try brand is unset`)
   591  
   592  	modeenv.TryBrandID = "foo"
   593  	err = modeenv.WriteTo(s.tmpdir)
   594  	c.Assert(err, IsNil)
   595  }
   596  
   597  func (s *modeenvSuite) TestWriteToNonExistingFull(c *C) {
   598  	c.Assert(s.mockModeenvPath, testutil.FileAbsent)
   599  
   600  	modeenv := &boot.Modeenv{
   601  		Mode:                   "run",
   602  		RecoverySystem:         "20191128",
   603  		CurrentRecoverySystems: []string{"20191128", "2020-02-03", "20240101-FOO"},
   604  		// keep this comment to make gofmt 1.9 happy
   605  		Base:           "core20_321.snap",
   606  		TryBase:        "core20_322.snap",
   607  		BaseStatus:     boot.TryStatus,
   608  		CurrentKernels: []string{"pc-kernel_1.snap", "pc-kernel_2.snap"},
   609  	}
   610  	err := modeenv.WriteTo(s.tmpdir)
   611  	c.Assert(err, IsNil)
   612  
   613  	c.Assert(s.mockModeenvPath, testutil.FileEquals, `mode=run
   614  recovery_system=20191128
   615  current_recovery_systems=20191128,2020-02-03,20240101-FOO
   616  base=core20_321.snap
   617  try_base=core20_322.snap
   618  base_status=try
   619  current_kernels=pc-kernel_1.snap,pc-kernel_2.snap
   620  `)
   621  }
   622  
   623  func (s *modeenvSuite) TestReadRecoverySystems(c *C) {
   624  	tt := []struct {
   625  		systemsString   string
   626  		expectedSystems []string
   627  	}{
   628  		{
   629  			"20191126",
   630  			[]string{"20191126"},
   631  		}, {
   632  			"20191128,2020-02-03,20240101-FOO",
   633  			[]string{"20191128", "2020-02-03", "20240101-FOO"},
   634  		},
   635  		{",,,", nil},
   636  		{"", nil},
   637  	}
   638  
   639  	for _, t := range tt {
   640  		c.Logf("tc: %q", t.systemsString)
   641  		s.makeMockModeenvFile(c, fmt.Sprintf(`mode=recovery
   642  recovery_system=20191126
   643  current_recovery_systems=%[1]s
   644  good_recovery_systems=%[1]s
   645  `, t.systemsString))
   646  
   647  		modeenv, err := boot.ReadModeenv(s.tmpdir)
   648  		c.Assert(err, IsNil)
   649  		c.Check(modeenv.Mode, Equals, "recovery")
   650  		c.Check(modeenv.RecoverySystem, Equals, "20191126")
   651  		c.Check(modeenv.CurrentRecoverySystems, DeepEquals, t.expectedSystems)
   652  		c.Check(modeenv.GoodRecoverySystems, DeepEquals, t.expectedSystems)
   653  	}
   654  }
   655  
   656  type fancyDataBothMarshallers struct {
   657  	Foo []string
   658  }
   659  
   660  func (f *fancyDataBothMarshallers) MarshalModeenvValue() (string, error) {
   661  	return strings.Join(f.Foo, "#"), nil
   662  }
   663  
   664  func (f *fancyDataBothMarshallers) UnmarshalModeenvValue(v string) error {
   665  	f.Foo = strings.Split(v, "#")
   666  	return nil
   667  }
   668  
   669  func (f *fancyDataBothMarshallers) MarshalJSON() ([]byte, error) {
   670  	return nil, fmt.Errorf("unexpected call to JSON marshaller")
   671  }
   672  
   673  func (f *fancyDataBothMarshallers) UnmarshalJSON(data []byte) error {
   674  	return fmt.Errorf("unexpected call to JSON unmarshaller")
   675  }
   676  
   677  type fancyDataJSONOnly struct {
   678  	Foo []string
   679  }
   680  
   681  func (f *fancyDataJSONOnly) MarshalJSON() ([]byte, error) {
   682  	return json.Marshal(f.Foo)
   683  }
   684  
   685  func (f *fancyDataJSONOnly) UnmarshalJSON(data []byte) error {
   686  	return json.Unmarshal(data, &f.Foo)
   687  }
   688  
   689  func (s *modeenvSuite) TestFancyMarshalUnmarshal(c *C) {
   690  	var buf bytes.Buffer
   691  
   692  	dboth := fancyDataBothMarshallers{Foo: []string{"1", "two"}}
   693  	err := boot.MarshalModeenvEntryTo(&buf, "fancy", &dboth)
   694  	c.Assert(err, IsNil)
   695  	c.Check(buf.String(), Equals, `fancy=1#two
   696  `)
   697  
   698  	djson := fancyDataJSONOnly{Foo: []string{"1", "two", "with\nnewline"}}
   699  	err = boot.MarshalModeenvEntryTo(&buf, "fancy_json", &djson)
   700  	c.Assert(err, IsNil)
   701  	c.Check(buf.String(), Equals, `fancy=1#two
   702  fancy_json=["1","two","with\nnewline"]
   703  `)
   704  
   705  	cfg := goconfigparser.New()
   706  	cfg.AllowNoSectionHeader = true
   707  	err = cfg.Read(&buf)
   708  	c.Assert(err, IsNil)
   709  
   710  	var dbothRev fancyDataBothMarshallers
   711  	err = boot.UnmarshalModeenvValueFromCfg(cfg, "fancy", &dbothRev)
   712  	c.Assert(err, IsNil)
   713  	c.Check(dbothRev, DeepEquals, dboth)
   714  
   715  	var djsonRev fancyDataJSONOnly
   716  	err = boot.UnmarshalModeenvValueFromCfg(cfg, "fancy_json", &djsonRev)
   717  	c.Assert(err, IsNil)
   718  	c.Check(djsonRev, DeepEquals, djson)
   719  }
   720  
   721  func (s *modeenvSuite) TestFancyUnmarshalJSONEmpty(c *C) {
   722  	var buf bytes.Buffer
   723  
   724  	cfg := goconfigparser.New()
   725  	cfg.AllowNoSectionHeader = true
   726  	err := cfg.Read(&buf)
   727  	c.Assert(err, IsNil)
   728  
   729  	var djsonRev fancyDataJSONOnly
   730  	err = boot.UnmarshalModeenvValueFromCfg(cfg, "fancy_json", &djsonRev)
   731  	c.Assert(err, IsNil)
   732  	c.Check(djsonRev.Foo, IsNil)
   733  }
   734  
   735  func (s *modeenvSuite) TestMarshalCurrentTrustedBootAssets(c *C) {
   736  	c.Assert(s.mockModeenvPath, testutil.FileAbsent)
   737  
   738  	modeenv := &boot.Modeenv{
   739  		Mode:           "run",
   740  		RecoverySystem: "20191128",
   741  		CurrentTrustedBootAssets: boot.BootAssetsMap{
   742  			"grubx64.efi": []string{"hash1", "hash2"},
   743  		},
   744  		CurrentTrustedRecoveryBootAssets: boot.BootAssetsMap{
   745  			"grubx64.efi": []string{"recovery-hash1"},
   746  			"bootx64.efi": []string{"shimhash1", "shimhash2"},
   747  		},
   748  	}
   749  	err := modeenv.WriteTo(s.tmpdir)
   750  	c.Assert(err, IsNil)
   751  
   752  	c.Assert(s.mockModeenvPath, testutil.FileEquals, `mode=run
   753  recovery_system=20191128
   754  current_trusted_boot_assets={"grubx64.efi":["hash1","hash2"]}
   755  current_trusted_recovery_boot_assets={"bootx64.efi":["shimhash1","shimhash2"],"grubx64.efi":["recovery-hash1"]}
   756  `)
   757  
   758  	modeenvRead, err := boot.ReadModeenv(s.tmpdir)
   759  	c.Assert(err, IsNil)
   760  	c.Assert(modeenvRead.CurrentTrustedBootAssets, DeepEquals, boot.BootAssetsMap{
   761  		"grubx64.efi": []string{"hash1", "hash2"},
   762  	})
   763  	c.Assert(modeenvRead.CurrentTrustedRecoveryBootAssets, DeepEquals, boot.BootAssetsMap{
   764  		"grubx64.efi": []string{"recovery-hash1"},
   765  		"bootx64.efi": []string{"shimhash1", "shimhash2"},
   766  	})
   767  }
   768  
   769  func (s *modeenvSuite) TestMarshalKernelCommandLines(c *C) {
   770  	c.Assert(s.mockModeenvPath, testutil.FileAbsent)
   771  
   772  	modeenv := &boot.Modeenv{
   773  		Mode:           "run",
   774  		RecoverySystem: "20191128",
   775  		CurrentKernelCommandLines: boot.BootCommandLines{
   776  			`snapd_recovery_mode=run panic=-1 console=ttyS0,io,9600n8`,
   777  			`snapd_recovery_mode=run candidate panic=-1 console=ttyS0,io,9600n8`,
   778  		},
   779  	}
   780  	err := modeenv.WriteTo(s.tmpdir)
   781  	c.Assert(err, IsNil)
   782  
   783  	c.Assert(s.mockModeenvPath, testutil.FileEquals, `mode=run
   784  recovery_system=20191128
   785  current_kernel_command_lines=["snapd_recovery_mode=run panic=-1 console=ttyS0,io,9600n8","snapd_recovery_mode=run candidate panic=-1 console=ttyS0,io,9600n8"]
   786  `)
   787  
   788  	modeenvRead, err := boot.ReadModeenv(s.tmpdir)
   789  	c.Assert(err, IsNil)
   790  	c.Assert(modeenvRead.CurrentKernelCommandLines, DeepEquals, boot.BootCommandLines{
   791  		`snapd_recovery_mode=run panic=-1 console=ttyS0,io,9600n8`,
   792  		`snapd_recovery_mode=run candidate panic=-1 console=ttyS0,io,9600n8`,
   793  	})
   794  }
   795  
   796  func (s *modeenvSuite) TestModeenvWithModelGradeSignKeyID(c *C) {
   797  	s.makeMockModeenvFile(c, `mode=run
   798  model=canonical/ubuntu-core-20-amd64
   799  grade=dangerous
   800  model_sign_key_id=9tydnLa6MTJ-jaQTFUXEwHl1yRx7ZS4K5cyFDhYDcPzhS7uyEkDxdUjg9g08BtNn
   801  try_model=developer1/testkeys-snapd-secured-core-20-amd64
   802  try_grade=secured
   803  try_model_sign_key_id=EAD4DbLxK_kn0gzNCXOs3kd6DeMU3f-L6BEsSEuJGBqCORR0gXkdDxMbOm11mRFu
   804  `)
   805  
   806  	modeenv, err := boot.ReadModeenv(s.tmpdir)
   807  	c.Assert(err, IsNil)
   808  	c.Check(modeenv.Model, Equals, "ubuntu-core-20-amd64")
   809  	c.Check(modeenv.BrandID, Equals, "canonical")
   810  	c.Check(modeenv.Grade, Equals, "dangerous")
   811  	c.Check(modeenv.ModelSignKeyID, Equals, "9tydnLa6MTJ-jaQTFUXEwHl1yRx7ZS4K5cyFDhYDcPzhS7uyEkDxdUjg9g08BtNn")
   812  	// candidate model
   813  	c.Check(modeenv.TryModel, Equals, "testkeys-snapd-secured-core-20-amd64")
   814  	c.Check(modeenv.TryBrandID, Equals, "developer1")
   815  	c.Check(modeenv.TryGrade, Equals, "secured")
   816  	c.Check(modeenv.TryModelSignKeyID, Equals, "EAD4DbLxK_kn0gzNCXOs3kd6DeMU3f-L6BEsSEuJGBqCORR0gXkdDxMbOm11mRFu")
   817  
   818  	// change some model data now
   819  	modeenv.Model = "testkeys-snapd-signed-core-20-amd64"
   820  	modeenv.BrandID = "developer1"
   821  	modeenv.Grade = "signed"
   822  	modeenv.ModelSignKeyID = "EAD4DbLxK_kn0gzNCXOs3kd6DeMU3f-L6BEsSEuJGBqCORR0gXkdDxMbOm11mRFu"
   823  
   824  	modeenv.TryModel = "bar"
   825  	modeenv.TryBrandID = "foo"
   826  	modeenv.TryGrade = "dangerous"
   827  	modeenv.TryModelSignKeyID = "9tydnLa6MTJ-jaQTFUXEwHl1yRx7ZS4K5cyFDhYDcPzhS7uyEkDxdUjg9g08BtNn"
   828  
   829  	// and write it
   830  	c.Assert(modeenv.Write(), IsNil)
   831  
   832  	c.Assert(s.mockModeenvPath, testutil.FileEquals, `mode=run
   833  model=developer1/testkeys-snapd-signed-core-20-amd64
   834  grade=signed
   835  model_sign_key_id=EAD4DbLxK_kn0gzNCXOs3kd6DeMU3f-L6BEsSEuJGBqCORR0gXkdDxMbOm11mRFu
   836  try_model=foo/bar
   837  try_grade=dangerous
   838  try_model_sign_key_id=9tydnLa6MTJ-jaQTFUXEwHl1yRx7ZS4K5cyFDhYDcPzhS7uyEkDxdUjg9g08BtNn
   839  `)
   840  }
   841  
   842  func (s *modeenvSuite) TestModelForSealing(c *C) {
   843  	s.makeMockModeenvFile(c, `mode=run
   844  model=canonical/ubuntu-core-20-amd64
   845  grade=dangerous
   846  model_sign_key_id=9tydnLa6MTJ-jaQTFUXEwHl1yRx7ZS4K5cyFDhYDcPzhS7uyEkDxdUjg9g08BtNn
   847  try_model=developer1/testkeys-snapd-secured-core-20-amd64
   848  try_grade=secured
   849  try_model_sign_key_id=EAD4DbLxK_kn0gzNCXOs3kd6DeMU3f-L6BEsSEuJGBqCORR0gXkdDxMbOm11mRFu
   850  `)
   851  
   852  	modeenv, err := boot.ReadModeenv(s.tmpdir)
   853  	c.Assert(err, IsNil)
   854  
   855  	modelForSealing := modeenv.ModelForSealing()
   856  	c.Check(modelForSealing.Model(), Equals, "ubuntu-core-20-amd64")
   857  	c.Check(modelForSealing.BrandID(), Equals, "canonical")
   858  	c.Check(modelForSealing.Grade(), Equals, asserts.ModelGrade("dangerous"))
   859  	c.Check(modelForSealing.SignKeyID(), Equals, "9tydnLa6MTJ-jaQTFUXEwHl1yRx7ZS4K5cyFDhYDcPzhS7uyEkDxdUjg9g08BtNn")
   860  	c.Check(modelForSealing.Series(), Equals, "16")
   861  	c.Check(boot.ModelUniqueID(modelForSealing), Equals,
   862  		"canonical/ubuntu-core-20-amd64,dangerous,9tydnLa6MTJ-jaQTFUXEwHl1yRx7ZS4K5cyFDhYDcPzhS7uyEkDxdUjg9g08BtNn")
   863  
   864  	tryModelForSealing := modeenv.TryModelForSealing()
   865  	c.Check(tryModelForSealing.Model(), Equals, "testkeys-snapd-secured-core-20-amd64")
   866  	c.Check(tryModelForSealing.BrandID(), Equals, "developer1")
   867  	c.Check(tryModelForSealing.Grade(), Equals, asserts.ModelGrade("secured"))
   868  	c.Check(tryModelForSealing.SignKeyID(), Equals, "EAD4DbLxK_kn0gzNCXOs3kd6DeMU3f-L6BEsSEuJGBqCORR0gXkdDxMbOm11mRFu")
   869  	c.Check(tryModelForSealing.Series(), Equals, "16")
   870  	c.Check(boot.ModelUniqueID(tryModelForSealing), Equals,
   871  		"developer1/testkeys-snapd-secured-core-20-amd64,secured,EAD4DbLxK_kn0gzNCXOs3kd6DeMU3f-L6BEsSEuJGBqCORR0gXkdDxMbOm11mRFu")
   872  }