Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions boot/assets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3075,6 +3075,28 @@ func (s *assetsSuite) TestUpdateBootEntryOnUpdate(c *C) {
c.Check(foundOther, Equals, 0)
}

func (s *assetsSuite) TestReconfigureRecoveryBootConfigCallsBootloaderHook(c *C) {
coreDev := boottest.MockUC20Device("", nil)
bloader := bootloadertest.Mock("runtime-config", c.MkDir())
s.forceBootloader(bloader)

updated, err := boot.ReconfigureRecoveryBootConfig(coreDev)
c.Assert(err, IsNil)
c.Check(updated, Equals, true)
c.Check(bloader.ReconfigureRecoveryBootConfigCalls, Equals, 1)
}

func (s *assetsSuite) TestReconfigureRecoveryBootConfigNoopOutsideRunMode(c *C) {
coreDevInstallMode := boottest.MockUC20Device("install", nil)
bloader := bootloadertest.Mock("runtime-config", c.MkDir())
s.forceBootloader(bloader)

updated, err := boot.ReconfigureRecoveryBootConfig(coreDevInstallMode)
c.Assert(err, IsNil)
c.Check(updated, Equals, false)
c.Check(bloader.ReconfigureRecoveryBootConfigCalls, Equals, 0)
}

func (s *assetsSuite) TestUpdateBootEntryOnInstall(c *C) {
tab := bootloadertest.Mock("trusted", "").WithTrustedAssetsAndEfi()

Expand Down
25 changes: 25 additions & 0 deletions boot/boot.go
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,31 @@ func SetRecoveryBootSystemAndMode(dev snap.Device, systemLabel, mode string) err
return bl.SetBootVars(m)
}

// ReconfigureRecoveryBootConfig rebuilds recovery boot configuration files for
// bootloaders that support it. Returns true when the recovery bootloader
// provides the capability and the reconfiguration was attempted.
func ReconfigureRecoveryBootConfig(dev snap.Device) (updated bool, err error) {
if !dev.HasModeenv() || !dev.RunMode() {
return false, nil
}

opts := &bootloader.Options{
Role: bootloader.RoleRecovery,
}
bl, err := bootloader.Find(InitramfsUbuntuSeedDir, opts)
if err != nil {
return false, err
}
rcb, ok := bl.(bootloader.RecoveryBootConfigBootloader)
if !ok {
return false, nil
}
if err := rcb.Reconfigure(); err != nil {
return false, err
}
return true, nil
}

// UpdateManagedBootConfigs updates managed boot config assets if
// those are present for the ubuntu-boot bootloader. To do this it
// needs information from the model, the gadget we are updating to,
Expand Down
2 changes: 2 additions & 0 deletions boot/boot_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4380,6 +4380,7 @@ func (s *bootenv20Suite) TestCoreParticipant20SetNextSameGadgetSnap(c *C) {

// we didn't call SetBootVars on the bootloader (unneeded for gadget)
c.Assert(s.bootloader.SetBootVarsCalls, Equals, 0)
c.Assert(s.bootloader.ReconfigureRecoveryBootConfigCalls, Equals, 0)
}

func (s *bootenv20Suite) TestCoreParticipant20SetNextNewGadgetSnap(c *C) {
Expand Down Expand Up @@ -4415,6 +4416,7 @@ func (s *bootenv20Suite) TestCoreParticipant20SetNextNewGadgetSnap(c *C) {

// we didn't call SetBootVars on the bootloader (unneeded for gadget)
c.Assert(s.bootloader.SetBootVarsCalls, Equals, 0)
c.Assert(s.bootloader.ReconfigureRecoveryBootConfigCalls, Equals, 1)
}

func (s *bootenv20Suite) TestCoreParticipant20UndoKernelSnapInstallSame(c *C) {
Expand Down
11 changes: 9 additions & 2 deletions boot/bootstate20.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func newBootState20(typ snap.Type, dev snap.Device) bootState {
dev: dev,
}
case snap.TypeGadget:
return &bootState20Gadget{}
return &bootState20Gadget{dev: dev}
default:
panic(fmt.Sprintf("cannot make a bootState20 for snap type %q", typ))
}
Expand Down Expand Up @@ -471,7 +471,7 @@ func (ks20 *bootState20Kernel) selectAndCommitSnapInitramfsMount(modeenv *Modeen
// snaps on UC20+. It is used for both setNext() and markSuccessful(),
// with both of those methods returning bootStateUpdate20 to be used
// with bootStateUpdate.
type bootState20Gadget struct{}
type bootState20Gadget struct{ dev snap.Device }

func (bs20 *bootState20Gadget) revisions() (curSnap, trySnap snap.PlaceInfo, tryingStatus string, err error) {
return nil, nil, "", fmt.Errorf("internal error, revisions not implemented for gadget")
Expand All @@ -483,6 +483,13 @@ func (bs20 *bootState20Gadget) setNext(next snap.PlaceInfo, bootCtx NextBootCont
return RebootInfo{RebootRequired: false}, nil, err
}

if u20.modeenv.Gadget != next.Filename() {
u20.postModeenv(func() error {
_, err := ReconfigureRecoveryBootConfig(bs20.dev)
return err
})
}

u20.writeModeenv.Gadget = next.Filename()

return RebootInfo{RebootRequired: false}, u20, err
Expand Down
10 changes: 10 additions & 0 deletions bootloader/bootloader.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,16 @@ type RecoveryAwareBootloader interface {
GetRecoverySystemEnv(recoverySystemDir string, key string) (string, error)
}

// RecoveryBootConfigBootloader can explicitly regenerate recovery boot
// configuration files from the currently persisted bootloader state.
type RecoveryBootConfigBootloader interface {
Bootloader

// Reconfigure rebuilds the bootloader recovery
// configuration using the current boot variables and on-disk state.
Reconfigure() error
}

type ExtractedRecoveryKernelImageBootloader interface {
Bootloader
ExtractRecoveryKernelAssets(recoverySystemDir string, s snap.PlaceInfo, snapf snap.Container) error
Expand Down
13 changes: 13 additions & 0 deletions bootloader/bootloadertest/bootloadertest.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ type MockBootloader struct {
InstallBootConfigCalled []string
InstallBootConfigErr error

ReconfigureRecoveryBootConfigCalls int
ReconfigureRecoveryBootConfigErr error
ReconfigureRecoveryBootConfigFunc func() error

enabledKernel snap.PlaceInfo
enabledTryKernel snap.PlaceInfo

Expand All @@ -68,6 +72,7 @@ var _ bootloader.NotScriptableBootloader = (*MockNotScriptableBootloader)(nil)
var _ bootloader.NotScriptableBootloader = (*MockExtractedRecoveryKernelNotScriptableBootloader)(nil)
var _ bootloader.ExtractedRecoveryKernelImageBootloader = (*MockExtractedRecoveryKernelNotScriptableBootloader)(nil)
var _ bootloader.RebootBootloader = (*MockRebootBootloader)(nil)
var _ bootloader.RecoveryBootConfigBootloader = (*MockBootloader)(nil)

func Mock(name, bootdir string) *MockBootloader {
return &MockBootloader{
Expand Down Expand Up @@ -162,6 +167,14 @@ func (b *MockBootloader) InstallBootConfig(gadgetDir string, opts *bootloader.Op
return b.InstallBootConfigErr
}

func (b *MockBootloader) Reconfigure() error {
b.ReconfigureRecoveryBootConfigCalls++
if b.ReconfigureRecoveryBootConfigFunc != nil {
return b.ReconfigureRecoveryBootConfigFunc()
}
return b.ReconfigureRecoveryBootConfigErr
}

// SetMockToPanic allows setting any method in the Bootloader interface or derived
// interface to panic instead of returning. This allows one to test what would
// happen if the system was rebooted during execution of a particular function.
Expand Down
12 changes: 12 additions & 0 deletions bootloader/piboot.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ var (
_ ExtractedRecoveryKernelImageBootloader = (*piboot)(nil)
_ NotScriptableBootloader = (*piboot)(nil)
_ RebootBootloader = (*piboot)(nil)
_ RecoveryBootConfigBootloader = (*piboot)(nil)
)

const (
Expand Down Expand Up @@ -235,6 +236,17 @@ func (p *piboot) loadAndApplyConfig(env *ubootenv.Env) error {
return p.applyConfig(env, cfgFile, prefix, cfgDir, dstDir)
}

func (p *piboot) Reconfigure() error {
env, err := ubootenv.OpenWithFlags(p.envFile(), ubootenv.OpenBestEffort)
if err != nil {
return err
}
// Reconfigure only regenerates the config selected by the persisted boot
// state. Leaving try mode and removing tryboot.txt is handled when
// SetBootVars clears snap_try_kernel.
return p.loadAndApplyConfig(env)
}

// Writes os_prefix in RPi config.txt or tryboot.txt
func (p *piboot) writeRPiCfgWithOsPrefix(prefix, inFile, outFile string) error {
buf, err := os.ReadFile(inFile)
Expand Down
26 changes: 26 additions & 0 deletions bootloader/piboot_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,32 @@ func (s *pibootTestSuite) TestOnlyOneOsPrefix(c *C) {
}
}

func (s *pibootTestSuite) TestReconfigureRecoveryBootConfig(c *C) {
opts := bootloader.Options{PrepareImageTime: false,
Role: bootloader.RoleRecovery}
r := bootloader.MockPibootFiles(c, s.rootdir, &opts)
defer r()
p := bootloader.NewPiboot(s.rootdir, &opts)
c.Assert(p, NotNil)
rcb, ok := p.(bootloader.RecoveryBootConfigBootloader)
c.Assert(ok, Equals, true)

err := p.SetBootVars(map[string]string{
"snap_kernel": "pi-kernel_1",
"snapd_recovery_mode": "run",
"kernel_status": boot.DefaultStatus,
})
c.Assert(err, IsNil)

configFile := filepath.Join(s.rootdir, "config.txt")
err = os.WriteFile(configFile, []byte("gpu_mem=64\nos_prefix=\ndtoverlay=disable-bt\n"), 0644)
c.Assert(err, IsNil)

err = rcb.Reconfigure()
c.Assert(err, IsNil)
c.Check(configFile, testutil.FileEquals, "gpu_mem=64\nos_prefix=/piboot/ubuntu/pi-kernel_1/\ndtoverlay=disable-bt\n")
}

func (s *pibootTestSuite) TestGetRebootArguments(c *C) {
opts := bootloader.Options{PrepareImageTime: false,
Role: bootloader.RoleRunMode, NoSlashBoot: true}
Expand Down
55 changes: 55 additions & 0 deletions overlord/devicestate/devicestate_gadget_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -808,6 +808,61 @@ volumes:
c.Check(updaterForStructureCalls, Equals, 1)
}

func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCorePibootPreservesExistingOsPrefix(c *C) {
var updateCalled int
currentConfig := filepath.Join(boot.InitramfsUbuntuSeedDir, "config.txt")
s.bootloader.ReconfigureRecoveryBootConfigFunc = func() error {
buf, err := os.ReadFile(currentConfig)
if err != nil {
return err
}
updated := strings.Replace(string(buf), "os_prefix=\n", "os_prefix=/piboot/ubuntu/pi-kernel_1/\n", 1)
if updated == string(buf) {
return fmt.Errorf("cannot update %s: blank os_prefix not found", currentConfig)
}
return os.WriteFile(currentConfig, []byte(updated), 0644)
}
defer func() {
s.bootloader.ReconfigureRecoveryBootConfigFunc = nil
s.bootloader.ReconfigureRecoveryBootConfigCalls = 0
}()

restore := devicestate.MockGadgetUpdate(func(model gadget.Model, current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, observer gadget.ContentUpdateObserver) error {
updateCalled++
c.Assert(os.WriteFile(currentConfig, []byte("gpu_mem=64\nos_prefix=\ndtoverlay=disable-bt\n"), 0644), IsNil)
return nil
})
defer restore()

chg, t := s.setupGadgetUpdate(c, "dangerous", strings.Replace(uc20gadgetYaml, "bootloader: grub", "bootloader: piboot", 1), "", false)

c.Assert(os.MkdirAll(boot.InitramfsUbuntuSeedDir, 0755), IsNil)
err := os.WriteFile(filepath.Join(boot.InitramfsUbuntuSeedDir, "config.txt"), []byte("gpu_mem=16\nos_prefix=/piboot/ubuntu/pi-kernel_1/\n"), 0644)
c.Assert(err, IsNil)
s.mockModeenvForMode(c, "run")
restore = devicestate.SetBootOkRanForCurrentBootID(s.mgr, true)
defer restore()

s.state.Lock()
s.state.Set("seeded", true)
s.state.Unlock()

s.settle(c)

s.state.Lock()
defer s.state.Unlock()

// simulate restart and settle again
s.mockRestartAndSettle(c, s.state, chg)

c.Assert(chg.IsReady(), Equals, true)
c.Check(chg.Err(), IsNil)
c.Check(t.Status(), Equals, state.DoneStatus)
c.Check(updateCalled, Equals, 1)
c.Check(s.bootloader.ReconfigureRecoveryBootConfigCalls, Equals, 1)
c.Check(filepath.Join(boot.InitramfsUbuntuSeedDir, "config.txt"), testutil.FileEquals, "gpu_mem=64\nos_prefix=/piboot/ubuntu/pi-kernel_1/\ndtoverlay=disable-bt\n")
}

func (s *deviceMgrGadgetSuite) TestCurrentAndUpdateInfo(c *C) {
siCurrent := &snap.SideInfo{
RealName: "foo-gadget",
Expand Down
10 changes: 10 additions & 0 deletions overlord/devicestate/handlers_gadget.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,13 +200,23 @@ func (m *DeviceManager) doUpdateGadgetAssets(t *state.Task, _ *tomb.Tomb) error
updateObserver = observeTrustedBootAssets
defer observeTrustedBootAssets.Done()
}

// do not release the state lock, the update observer may
// attempt to modify modeenv inside, which implicitly is
// guarded by the state lock; on top of that we do not expect
// the update to be moving large amounts of data
if err := gadgetUpdate(model, *currentData, *updateData, snapRollbackDir, updatePolicy, updateObserver); err != nil {
return err
}

// After a gadget update, some recovery bootloaders may need to
// regenerate their config from the persisted boot variables.
if updated, err := boot.ReconfigureRecoveryBootConfig(groundDeviceCtx); err != nil {
return fmt.Errorf("cannot reconfigure recovery boot config: %v", err)
} else if updated {
t.Logf("reconfigured recovery boot config")
}

if updateObserver == nil {
return nil
}
Expand Down
Loading