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

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 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  package boot_test
    20  
    21  import (
    22  	"fmt"
    23  
    24  	. "gopkg.in/check.v1"
    25  
    26  	"github.com/snapcore/snapd/boot"
    27  	"github.com/snapcore/snapd/boot/boottest"
    28  	"github.com/snapcore/snapd/bootloader"
    29  	"github.com/snapcore/snapd/snap"
    30  )
    31  
    32  // TODO:UC20: move this to bootloadertest package and use from i.e. managers_test.go ?
    33  func runBootloaderLogic(c *C, bl bootloader.Bootloader) (snap.PlaceInfo, error) {
    34  	// switch on which kind of bootloader we have
    35  	ebl, ok := bl.(bootloader.ExtractedRunKernelImageBootloader)
    36  	if ok {
    37  		return extractedRunKernelImageBootloaderLogic(c, ebl)
    38  	}
    39  
    40  	return pureenvBootloaderLogic(c, "kernel_status", bl)
    41  }
    42  
    43  // runBootloaderLogic implements the logic from the gadget snap bootloader,
    44  // namely that we transition kernel_status "try" -> "trying" and "trying" -> ""
    45  // and use try-kernel.efi when kernel_status is "try" and kernel.efi in all
    46  // other situations
    47  func extractedRunKernelImageBootloaderLogic(c *C, ebl bootloader.ExtractedRunKernelImageBootloader) (snap.PlaceInfo, error) {
    48  	m, err := ebl.GetBootVars("kernel_status")
    49  	c.Assert(err, IsNil)
    50  	kernStatus := m["kernel_status"]
    51  
    52  	kern, err := ebl.Kernel()
    53  	c.Assert(err, IsNil)
    54  	c.Assert(kern, Not(IsNil))
    55  
    56  	switch kernStatus {
    57  	case boot.DefaultStatus:
    58  	case boot.TryStatus:
    59  		// move to trying, use the try-kernel
    60  		m["kernel_status"] = boot.TryingStatus
    61  
    62  		// ensure that the try-kernel exists
    63  		tryKern, err := ebl.TryKernel()
    64  		c.Assert(err, IsNil)
    65  		c.Assert(tryKern, Not(IsNil))
    66  		kern = tryKern
    67  
    68  	case boot.TryingStatus:
    69  		// boot failed, move back to default
    70  		m["kernel_status"] = boot.DefaultStatus
    71  	}
    72  
    73  	err = ebl.SetBootVars(m)
    74  	c.Assert(err, IsNil)
    75  
    76  	return kern, nil
    77  }
    78  
    79  func pureenvBootloaderLogic(c *C, modeVar string, bl bootloader.Bootloader) (snap.PlaceInfo, error) {
    80  	m, err := bl.GetBootVars(modeVar, "snap_kernel", "snap_try_kernel")
    81  	c.Assert(err, IsNil)
    82  	var kern snap.PlaceInfo
    83  
    84  	kernStatus := m[modeVar]
    85  
    86  	kern, err = snap.ParsePlaceInfoFromSnapFileName(m["snap_kernel"])
    87  	c.Assert(err, IsNil)
    88  	c.Assert(kern, Not(IsNil))
    89  
    90  	switch kernStatus {
    91  	case boot.DefaultStatus:
    92  		// nothing to do, use normal kernel
    93  
    94  	case boot.TryStatus:
    95  		// move to trying, use the try-kernel
    96  		m[modeVar] = boot.TryingStatus
    97  
    98  		tryKern, err := snap.ParsePlaceInfoFromSnapFileName(m["snap_try_kernel"])
    99  		c.Assert(err, IsNil)
   100  		c.Assert(tryKern, Not(IsNil))
   101  		kern = tryKern
   102  
   103  	case boot.TryingStatus:
   104  		// boot failed, move back to default status
   105  		m[modeVar] = boot.DefaultStatus
   106  
   107  	}
   108  
   109  	err = bl.SetBootVars(m)
   110  	c.Assert(err, IsNil)
   111  
   112  	return kern, nil
   113  }
   114  
   115  // note: this could be implemented just as a function which takes a bootloader
   116  // as an argument and then inspect the type of MockBootloader that was passed
   117  // in, but the gains are little, since we don't need to use this function for
   118  // the non-ExtractedRunKernelImageBootloader implementations, as those
   119  // implementations just have one critical function to run which is just
   120  // SetBootVars
   121  func (s *bootenv20Suite) checkBootStateAfterUnexpectedRebootAndCleanup(
   122  	c *C,
   123  	dev boot.Device,
   124  	bootFunc func(boot.Device) error,
   125  	panicFunc string,
   126  	expectedBootedKernel snap.PlaceInfo,
   127  	expectedModeenvCurrentKernels []snap.PlaceInfo,
   128  	blKernelAfterReboot snap.PlaceInfo,
   129  	comment string,
   130  ) {
   131  	if panicFunc != "" {
   132  		// setup a panic during the given bootloader function
   133  		restoreBootloaderPanic := s.bootloader.SetMockToPanic(panicFunc)
   134  
   135  		// run the boot function that will now panic
   136  		c.Assert(
   137  			func() { bootFunc(dev) },
   138  			PanicMatches,
   139  			fmt.Sprintf("mocked reboot panic in %s", panicFunc),
   140  			Commentf(comment),
   141  		)
   142  
   143  		// don't panic anymore
   144  		restoreBootloaderPanic()
   145  	} else {
   146  		// just run the function directly
   147  		err := bootFunc(dev)
   148  		c.Assert(err, IsNil, Commentf(comment))
   149  	}
   150  
   151  	// do the bootloader kernel failover logic handling
   152  	nextBootingKernel, err := runBootloaderLogic(c, s.bootloader)
   153  	c.Assert(err, IsNil, Commentf(comment))
   154  
   155  	// check that the kernel we booted now is expected
   156  	c.Assert(nextBootingKernel, Equals, expectedBootedKernel, Commentf(comment))
   157  
   158  	// also check that the normal kernel on the bootloader is what we expect
   159  	kern, err := s.bootloader.Kernel()
   160  	c.Assert(err, IsNil, Commentf(comment))
   161  	c.Assert(kern, Equals, blKernelAfterReboot, Commentf(comment))
   162  
   163  	// mark the boot successful like we were rebooted
   164  	err = boot.MarkBootSuccessful(dev)
   165  	c.Assert(err, IsNil, Commentf(comment))
   166  
   167  	// the boot vars should be empty now too
   168  	afterVars, err := s.bootloader.GetBootVars("kernel_status")
   169  	c.Assert(err, IsNil, Commentf(comment))
   170  	c.Assert(afterVars["kernel_status"], DeepEquals, boot.DefaultStatus, Commentf(comment))
   171  
   172  	// the modeenv's setting for CurrentKernels also matches
   173  	m, err := boot.ReadModeenv("")
   174  	c.Assert(err, IsNil, Commentf(comment))
   175  	// it's nicer to pass in just the snap.PlaceInfo's, but to compare we need
   176  	// the string filenames
   177  	currentKernels := make([]string, len(expectedModeenvCurrentKernels))
   178  	for i, sn := range expectedModeenvCurrentKernels {
   179  		currentKernels[i] = sn.Filename()
   180  	}
   181  	c.Assert(m.CurrentKernels, DeepEquals, currentKernels, Commentf(comment))
   182  
   183  	// the final kernel on the bootloader should always match what we booted -
   184  	// after MarkSuccessful runs that is
   185  	afterKernel, err := s.bootloader.Kernel()
   186  	c.Assert(err, IsNil, Commentf(comment))
   187  	c.Assert(afterKernel, DeepEquals, expectedBootedKernel, Commentf(comment))
   188  
   189  	// we should never have a leftover try kernel
   190  	_, err = s.bootloader.TryKernel()
   191  	c.Assert(err, Equals, bootloader.ErrNoTryKernelRef, Commentf(comment))
   192  }
   193  
   194  func (s *bootenv20Suite) TestHappyMarkBootSuccessful20KernelUpgradeUnexpectedReboots(c *C) {
   195  	coreDev := boottest.MockUC20Device("", nil)
   196  	c.Assert(coreDev.HasModeenv(), Equals, true)
   197  
   198  	tt := []struct {
   199  		rebootBeforeFunc  string
   200  		expBootKernel     snap.PlaceInfo
   201  		expModeenvKernels []snap.PlaceInfo
   202  		expBlKernel       snap.PlaceInfo
   203  		comment           string
   204  	}{
   205  		{
   206  			"",                        // don't do any reboots for the happy path
   207  			s.kern2,                   // we should boot the new kernel
   208  			[]snap.PlaceInfo{s.kern2}, // expected modeenv kernel is new one
   209  			s.kern2,                   // after reboot, current kernel on bl is new one
   210  			"happy path",
   211  		},
   212  		{
   213  			"SetBootVars",             // reboot right before SetBootVars
   214  			s.kern1,                   // we should boot the old kernel
   215  			[]snap.PlaceInfo{s.kern1}, // expected modeenv kernel is old one
   216  			s.kern1,                   // after reboot, current kernel on bl is old one
   217  			"reboot before SetBootVars results in old kernel",
   218  		},
   219  		{
   220  			"EnableKernel",            // reboot right before EnableKernel
   221  			s.kern1,                   // we should boot the old kernel
   222  			[]snap.PlaceInfo{s.kern1}, // expected modeenv kernel is old one
   223  			s.kern1,                   // after reboot, current kernel on bl is old one
   224  			"reboot before EnableKernel results in old kernel",
   225  		},
   226  		{
   227  			"DisableTryKernel",        // reboot right before DisableTryKernel
   228  			s.kern2,                   // we should boot the new kernel
   229  			[]snap.PlaceInfo{s.kern2}, // expected modeenv kernel is new one
   230  			s.kern2,                   // after reboot, current kernel on bl is new one
   231  			"reboot before DisableTryKernel results in new kernel",
   232  		},
   233  	}
   234  
   235  	for _, t := range tt {
   236  		// setup the bootloader per test
   237  		restore := setupUC20Bootenv(
   238  			c,
   239  			s.bootloader,
   240  			s.normalTryingKernelState,
   241  		)
   242  
   243  		s.checkBootStateAfterUnexpectedRebootAndCleanup(
   244  			c,
   245  			coreDev,
   246  			boot.MarkBootSuccessful,
   247  			t.rebootBeforeFunc,
   248  			t.expBlKernel,
   249  			t.expModeenvKernels,
   250  			t.expBlKernel,
   251  			t.comment,
   252  		)
   253  
   254  		restore()
   255  	}
   256  }
   257  
   258  func (s *bootenv20Suite) TestHappySetNextBoot20KernelUpgradeUnexpectedReboots(c *C) {
   259  	coreDev := boottest.MockUC20Device("", nil)
   260  	c.Assert(coreDev.HasModeenv(), Equals, true)
   261  
   262  	tt := []struct {
   263  		rebootBeforeFunc  string
   264  		expBootKernel     snap.PlaceInfo
   265  		expModeenvKernels []snap.PlaceInfo
   266  		expBlKernel       snap.PlaceInfo
   267  		comment           string
   268  	}{
   269  		{
   270  			"",                        // don't do any reboots for the happy path
   271  			s.kern2,                   // we should boot the new kernel
   272  			[]snap.PlaceInfo{s.kern2}, // final expected modeenv kernel is new one
   273  			s.kern1,                   // after reboot, current kernel on bl is old one
   274  			"happy path",
   275  		},
   276  		{
   277  			"EnableTryKernel",         // reboot right before EnableTryKernel
   278  			s.kern1,                   // we should boot the old kernel
   279  			[]snap.PlaceInfo{s.kern1}, // final expected modeenv kernel is old one
   280  			s.kern1,                   // after reboot, current kernel on bl is old one
   281  			"reboot before EnableTryKernel results in old kernel",
   282  		},
   283  		{
   284  			"SetBootVars",             // reboot right before SetBootVars
   285  			s.kern1,                   // we should boot the old kernel
   286  			[]snap.PlaceInfo{s.kern1}, // final expected modeenv kernel is old one
   287  			s.kern1,                   // after reboot, current kernel on bl is old one
   288  			"reboot before SetBootVars results in old kernel",
   289  		},
   290  	}
   291  
   292  	for _, t := range tt {
   293  		// setup the bootloader per test
   294  		restore := setupUC20Bootenv(
   295  			c,
   296  			s.bootloader,
   297  			s.normalDefaultState,
   298  		)
   299  
   300  		// get the boot kernel participant from our new kernel snap
   301  		bootKern := boot.Participant(s.kern2, snap.TypeKernel, coreDev)
   302  		// make sure it's not a trivial boot participant
   303  		c.Assert(bootKern.IsTrivial(), Equals, false)
   304  
   305  		setNextFunc := func(boot.Device) error {
   306  			// we don't care about the reboot required logic here
   307  			_, err := bootKern.SetNextBoot()
   308  			return err
   309  		}
   310  
   311  		s.checkBootStateAfterUnexpectedRebootAndCleanup(
   312  			c,
   313  			coreDev,
   314  			setNextFunc,
   315  			t.rebootBeforeFunc,
   316  			t.expBootKernel,
   317  			t.expModeenvKernels,
   318  			t.expBlKernel,
   319  			t.comment,
   320  		)
   321  
   322  		restore()
   323  	}
   324  }