summaryrefslogtreecommitdiff
diff options
authorMaciej Borzecki <maciej.zenon.borzecki@canonical.com>2022-03-21 15:48:58 +0100
committerMichael Vogt <mvo@ubuntu.com>2022-03-21 15:49:15 +0100
commit8539a46802e7c335b56bc7d38dc04f2d21246984 (patch)
treebeb8f8f41bb5e09364ad23a4f8d3b7cd5b50afcd
parentbf9e2409232c1b9416851a9c726755e0d20a2f26 (diff)
o/snapstate: avoid setting up single reboot when update includes base, kernel and gadget
* o/snapstate: avoid setting up single reboot when update includes base, kernel and gadget Otherwise there is a circular dependency between base, kernel and gadget, where the kernel waits for gadget (to handle gadget assets update), gadget waits for the base, and the base waits for some of the kernel tasks. Signed-off-by: Maciej Borzecki <maciej.zenon.borzecki@canonical.com> * o/snapstate: procure circular dependency and verify abort untangles the state Signed-off-by: Maciej Borzecki <maciej.zenon.borzecki@canonical.com> * overlord/state: add helper for aborting unready lanes A helper for aborting all lanes that aren't ready in a given change. An unready lane is one that carries tasks which have not reached a final status yet. Signed-off-by: Maciej Borzecki <maciej.zenon.borzecki@canonical.com> * overlord/state: drop unused lanes field Signed-off-by: Maciej Borzecki <maciej.zenon.borzecki@canonical.com> * overlord: wait for up to 3 days before automatically aborting a change Signed-off-by: Maciej Borzecki <maciej.zenon.borzecki@canonical.com> * overlord/state: use AbortUnreadyLanes when pruning Signed-off-by: Maciej Borzecki <maciej.zenon.borzecki@canonical.com> * overlord: managers test to verify self healing via abort-unready-lanes in prune Signed-off-by: Maciej Borzecki <maciej.zenon.borzecki@canonical.com> * overlord: leave a comment about the scenario being tested, test tweaks Signed-off-by: Maciej Borzecki <maciej.zenon.borzecki@canonical.com>
-rw-r--r--overlord/managers_test.go450
-rw-r--r--overlord/overlord.go2
-rw-r--r--overlord/snapstate/backend_test.go6
-rw-r--r--overlord/snapstate/snapstate.go20
-rw-r--r--overlord/snapstate/snapstate_update_test.go92
-rw-r--r--overlord/state/state.go2
6 files changed, 569 insertions, 3 deletions
diff --git a/overlord/managers_test.go b/overlord/managers_test.go
index 3e44ff96c7..df49068bd4 100644
--- a/overlord/managers_test.go
+++ b/overlord/managers_test.go
@@ -84,6 +84,7 @@ import (
"github.com/snapcore/snapd/snap/snapfile"
"github.com/snapcore/snapd/snap/snaptest"
"github.com/snapcore/snapd/store"
+ "github.com/snapcore/snapd/strutil"
"github.com/snapcore/snapd/systemd"
"github.com/snapcore/snapd/systemd/systemdtest"
"github.com/snapcore/snapd/testutil"
@@ -9623,6 +9624,455 @@ func (s *mgrsSuite) TestUpdateKernelBaseSingleRebootKernelUndo(c *C) {
}
}
+func (s *mgrsSuite) testUpdateKernelBaseSingleRebootWithGadgetSetup(c *C, snapYamlGadget string) (*boottest.RunBootenv20, *state.Change) {
+ bloader := boottest.MockUC20RunBootenv(bootloadertest.Mock("mock", c.MkDir()))
+ bootloader.Force(bloader)
+ s.AddCleanup(func() { bootloader.Force(nil) })
+
+ // a revision which is assumed to be installed
+ kernel, err := snap.ParsePlaceInfoFromSnapFileName("pc-kernel_1.snap")
+ c.Assert(err, IsNil)
+ restore := bloader.SetEnabledKernel(kernel)
+ s.AddCleanup(restore)
+
+ restore = release.MockOnClassic(false)
+ s.AddCleanup(restore)
+
+ mockServer := s.mockStore(c)
+ s.AddCleanup(func() { mockServer.Close() })
+
+ st := s.o.State()
+ st.Lock()
+ defer st.Unlock()
+
+ model := s.brands.Model("can0nical", "my-model", uc20ModelDefaults)
+ // setup model assertion
+ devicestatetest.SetDevice(st, &auth.DeviceState{
+ Brand: "can0nical",
+ Model: "my-model",
+ Serial: "serialserialserial",
+ })
+ err = assertstate.Add(st, model)
+ c.Assert(err, IsNil)
+
+ // mock the modeenv file
+ m := &boot.Modeenv{
+ Mode: "run",
+ Base: "core20_1.snap",
+ CurrentKernels: []string{"pc-kernel_1.snap"},
+ CurrentRecoverySystems: []string{"1234"},
+ GoodRecoverySystems: []string{"1234"},
+
+ Model: model.Model(),
+ BrandID: model.BrandID(),
+ Grade: string(model.Grade()),
+ ModelSignKeyID: model.SignKeyID(),
+ }
+ err = m.WriteTo("")
+ c.Assert(err, IsNil)
+ c.Assert(s.o.DeviceManager().ReloadModeenv(), IsNil)
+
+ pcKernelYaml := "name: pc-kernel\nversion: 1.0\ntype: kernel"
+ baseYaml := "name: core20\nversion: 1.0\ntype: base"
+ siKernel := &snap.SideInfo{RealName: "pc-kernel", SnapID: fakeSnapID("pc-kernel"), Revision: snap.R(1)}
+ snaptest.MockSnap(c, pcKernelYaml, siKernel)
+ siBase := &snap.SideInfo{RealName: "core20", SnapID: fakeSnapID("core20"), Revision: snap.R(1)}
+ snaptest.MockSnap(c, baseYaml, siBase)
+ siSnapd := &snap.SideInfo{RealName: "snapd", Revision: snap.R(1), SnapID: fakeSnapID("snapd")}
+ snaptest.MockSnap(c, "name: snapd\ntype: snapd\nversion: 123", siSnapd)
+ siGadget := &snap.SideInfo{RealName: "pc", Revision: snap.R(1), SnapID: fakeSnapID("pc")}
+ snaptest.MockSnapWithFiles(c, snapYamlGadget, siGadget, [][]string{
+ {"meta/gadget.yaml", pcGadgetYaml},
+ })
+
+ // test setup adds core, get rid of it
+ snapstate.Set(st, "core", nil)
+ snapstate.Set(st, "pc-kernel", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{siKernel},
+ Current: snap.R(1),
+ SnapType: "kernel",
+ })
+ snapstate.Set(st, "core20", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{siBase},
+ Current: snap.R(1),
+ SnapType: "base",
+ })
+ snapstate.Set(st, "snapd", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{siSnapd},
+ Current: snap.R(1),
+ SnapType: "snapd",
+ })
+ snapstate.Set(st, "pc", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{siGadget},
+ Current: snap.R(1),
+ SnapType: "gadget",
+ })
+
+ p, _ := s.makeStoreTestSnap(c, pcKernelYaml, "2")
+ s.serveSnap(p, "2")
+ p, _ = s.makeStoreTestSnap(c, baseYaml, "2")
+ s.serveSnap(p, "2")
+ p, _ = s.makeStoreTestSnap(c, "name: snapd\ntype: snapd\nversion: 123", "2")
+ s.serveSnap(p, "2")
+ p, _ = s.makeStoreTestSnapWithFiles(c, snapYamlGadget, "2", [][]string{
+ {"meta/gadget.yaml", pcGadgetYaml},
+ })
+ s.serveSnap(p, "2")
+
+ affected, tss, err := snapstate.UpdateMany(context.Background(), st, []string{"pc-kernel", "core20", "pc", "snapd"}, 0, nil)
+ c.Assert(err, IsNil)
+ c.Assert(affected, DeepEquals, []string{"core20", "pc", "pc-kernel", "snapd"})
+ chg := st.NewChange("update-many", "...")
+ for _, ts := range tss {
+ // skip the taskset of UpdateMany that does the
+ // check-rerefresh, see tsWithoutReRefresh for details
+ if ts.Tasks()[0].Kind() == "check-rerefresh" {
+ c.Logf("skipping rerefresh")
+ continue
+ }
+ chg.AddAll(ts)
+ }
+ return bloader, chg
+}
+
+func (s *mgrsSuite) TestUpdateKernelBaseSingleRebootWithGadgetWithExplicitBase(c *C) {
+ // verify a scenario when the update contains snapd, kernel, base and
+ // the gadget, in which case we revert to having at least 2 reboots due
+ // to the kernel depending on the gadget and the gadget depending on the
+ // base
+
+ const pcGadget = `
+name: pc
+version: 1.0
+type: gadget
+base: core20
+`
+ bloader, chg := s.testUpdateKernelBaseSingleRebootWithGadgetSetup(c, pcGadget)
+
+ st := s.o.State()
+ st.Lock()
+ defer st.Unlock()
+
+ st.Unlock()
+ err := s.o.Settle(settleTimeout)
+ st.Lock()
+ c.Assert(err, IsNil, Commentf(s.logbuf.String()))
+ c.Logf(s.logbuf.String())
+
+ // snapd is updated first (as it's a prerequisite for the base)
+ ok, rst := restart.Pending(st)
+ c.Assert(ok, Equals, true)
+ c.Assert(rst, Equals, restart.RestartDaemon)
+ restart.MockPending(st, restart.RestartUnset)
+
+ autoConnectStatus := func(inDoing string, done []string) {
+ autoConnectCount := 0
+ for _, tsk := range chg.Tasks() {
+ if tsk.Kind() == "auto-connect" {
+ autoConnectCount++
+ expectedStatus := state.DoStatus
+ snapsup, err := snapstate.TaskSnapSetup(tsk)
+ c.Assert(err, IsNil)
+ if snapsup.InstanceName() == inDoing {
+ expectedStatus = state.DoingStatus
+ } else if strutil.ListContains(done, snapsup.InstanceName()) {
+ expectedStatus = state.DoneStatus
+ }
+ c.Check(tsk.Status(), Equals, expectedStatus,
+ Commentf("%q has status other than %s", tsk.Summary(), expectedStatus))
+ }
+ }
+ // one for snapd, one for kernel, one for gadget, one for base
+ c.Check(autoConnectCount, Equals, 4)
+ }
+ autoConnectStatus("snapd", nil)
+
+ st.Unlock()
+ err = s.o.Settle(settleTimeout)
+ st.Lock()
+ c.Assert(err, IsNil, Commentf(s.logbuf.String()))
+ c.Logf(s.logbuf.String())
+
+ ok, rst = restart.Pending(st)
+ c.Assert(ok, Equals, true)
+ c.Assert(rst, Equals, restart.RestartSystem)
+
+ autoConnectStatus("core20", []string{"snapd"})
+
+ // we are trying out a new base
+ restart.MockPending(st, restart.RestartUnset)
+ _, err = bloader.TryKernel()
+ c.Assert(err, Equals, bootloader.ErrNoTryKernelRef)
+ m, err := boot.ReadModeenv("")
+ c.Assert(err, IsNil)
+ c.Check(m.BaseStatus, Equals, boot.TryStatus)
+ c.Check(m.TryBase, Equals, "core20_2.snap")
+
+ // pretend it boots
+ m.BaseStatus = boot.TryingStatus
+ c.Assert(m.Write(), IsNil)
+ s.o.DeviceManager().ResetToPostBootState()
+ st.Unlock()
+ err = s.o.DeviceManager().Ensure()
+ st.Lock()
+ c.Assert(err, IsNil)
+
+ st.Unlock()
+ err = s.o.Settle(settleTimeout)
+ st.Lock()
+ c.Assert(err, IsNil, Commentf(s.logbuf.String()))
+ c.Logf(s.logbuf.String())
+ dumpTasks(c, "after run", chg.Tasks())
+
+ autoConnectStatus("pc-kernel", []string{"core20", "pc", "snapd"})
+
+ // try snaps are set
+ currentTryKernel, err := bloader.TryKernel()
+ c.Assert(err, IsNil)
+ c.Assert(currentTryKernel.Filename(), Equals, "pc-kernel_2.snap")
+ m, err = boot.ReadModeenv("")
+ c.Assert(err, IsNil)
+ c.Check(m.BaseStatus, Equals, "")
+
+ // simulate successful restart happened
+ restart.MockPending(st, restart.RestartUnset)
+ err = bloader.SetTryingDuringReboot([]snap.Type{snap.TypeKernel})
+ c.Assert(err, IsNil)
+ m.BaseStatus = boot.TryingStatus
+ c.Assert(m.Write(), IsNil)
+ s.o.DeviceManager().ResetToPostBootState()
+ st.Unlock()
+ err = s.o.DeviceManager().Ensure()
+ st.Lock()
+ c.Assert(err, IsNil)
+
+ // go on
+ st.Unlock()
+ err = s.o.Settle(settleTimeout)
+ st.Lock()
+ c.Assert(err, IsNil)
+
+ c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("change failed with: %v", chg.Err()))
+}
+
+func (s *mgrsSuite) TestUpdateKernelBaseSingleRebootWithGadgetWithExplicitBaseBuggy(c *C) {
+ // verify a buggy scenario when the update contains snapd, kernel, base
+ // and the gadget, in which case the buggy behavior will cause a cyclic
+ // dependency between kernel, gadget and base, which then gets fixed by
+ // calling AbortUnreadyLanes()
+
+ // enable buggy behavior
+ restore := snapstate.MockEnforceSingleRebootForBaseKernelGadget(true)
+ defer restore()
+ const pcGadget = `
+name: pc
+version: 1.0
+type: gadget
+base: core20
+`
+ _, chg := s.testUpdateKernelBaseSingleRebootWithGadgetSetup(c, pcGadget)
+
+ st := s.o.State()
+ st.Lock()
+ defer st.Unlock()
+
+ var snapst snapstate.SnapState
+ err := snapstate.Get(st, "snapd", &snapst)
+ c.Assert(err, IsNil)
+ c.Assert(snapst.Current, Equals, snap.R(1))
+
+ st.Unlock()
+ err = s.o.Settle(settleTimeout)
+ st.Lock()
+ c.Assert(err, IsNil, Commentf(s.logbuf.String()))
+ c.Logf(s.logbuf.String())
+ dumpTasks(c, "after run", chg.Tasks())
+
+ // first comes the snapd restart
+ ok, rst := restart.Pending(st)
+ c.Assert(ok, Equals, true)
+ c.Assert(rst, Equals, restart.RestartDaemon)
+ restart.MockPending(st, restart.RestartUnset)
+
+ st.Unlock()
+ err = s.o.Settle(settleTimeout)
+ st.Lock()
+ c.Assert(err, IsNil, Commentf(s.logbuf.String()))
+ c.Logf(s.logbuf.String())
+ dumpTasks(c, "after run", chg.Tasks())
+
+ // final steps will are postponed until we are in the restarted snapd
+ ok, rst = restart.Pending(st)
+ c.Assert(ok, Equals, false)
+ c.Assert(rst, Equals, restart.RestartUnset)
+
+ // settle has exited as there are no more tasks that can be run due to
+ // the circular dependency, we expect all tasks to be in either Do or
+ // Done states
+ for _, tsk := range chg.Tasks() {
+ if tsk.Status() != state.DoneStatus && tsk.Status() != state.DoStatus {
+ c.Errorf("unexpected status %s of task %s %s", tsk.Status(), tsk.ID(), tsk.Summary())
+ c.FailNow()
+ }
+ }
+
+ chg.AbortUnreadyLanes()
+
+ st.Unlock()
+ err = s.o.Settle(settleTimeout)
+ st.Lock()
+ c.Assert(err, IsNil, Commentf(s.logbuf.String()))
+ c.Logf(s.logbuf.String())
+ dumpTasks(c, "after abort", chg.Tasks())
+ c.Assert(chg.IsReady(), Equals, true)
+ c.Assert(chg.Status(), Equals, state.UndoneStatus)
+ // snapd should have been hept
+ for _, tsk := range chg.Tasks() {
+ if tsk.Kind() == "link-snap" {
+ snapsup, err := snapstate.TaskSnapSetup(tsk)
+ c.Assert(err, IsNil)
+ if snapsup.InstanceName() == "snapd" {
+ c.Assert(tsk.Status(), Equals, state.DoneStatus)
+ }
+ }
+ }
+
+ // snapd update is kept
+ err = snapstate.Get(st, "snapd", &snapst)
+ c.Assert(err, IsNil)
+ c.Assert(snapst.Current, Equals, snap.R(2))
+
+ for _, name := range []string{"pc-kernel", "pc", "core20"} {
+ err = snapstate.Get(st, name, &snapst)
+ c.Assert(err, IsNil)
+ // the current is the old revision
+ c.Assert(snapst.Current, Equals, snap.R(1))
+ }
+}
+
+func (s *mgrsSuite) TestUpdateKernelBaseSingleRebootWithGadgetWithBuggySelfHeal(c *C) {
+ // pretend it's a buggy snapd version that generates the change, then
+ // snapd gets updated as part of the auto-refresh, during which we
+ // restart to the new snapd which uses a new prune interval that
+ // effectively aborts unready lanes and thus the buggy change completes,
+ // while the new version of snaps remains
+
+ restore := snapstate.MockEnforceSingleRebootForBaseKernelGadget(true)
+ defer restore()
+ const pcGadget = `
+name: pc
+version: 1.0
+type: gadget
+base: core20
+`
+ _, chg := s.testUpdateKernelBaseSingleRebootWithGadgetSetup(c, pcGadget)
+
+ st := s.o.State()
+ st.Lock()
+ defer st.Unlock()
+
+ var snapst snapstate.SnapState
+ err := snapstate.Get(st, "snapd", &snapst)
+ c.Assert(err, IsNil)
+ c.Assert(snapst.Current, Equals, snap.R(1))
+
+ st.Unlock()
+ err = s.o.Settle(settleTimeout)
+ st.Lock()
+ c.Assert(err, IsNil, Commentf(s.logbuf.String()))
+ c.Logf(s.logbuf.String())
+ dumpTasks(c, "after run", chg.Tasks())
+
+ // first comes the snapd restart
+ ok, rst := restart.Pending(st)
+ c.Assert(ok, Equals, true)
+ c.Assert(rst, Equals, restart.RestartDaemon)
+ restart.MockPending(st, restart.RestartUnset)
+
+ st.Unlock()
+ err = s.o.Settle(settleTimeout)
+ st.Lock()
+ c.Assert(err, IsNil, Commentf(s.logbuf.String()))
+ c.Logf(s.logbuf.String())
+ dumpTasks(c, "after run", chg.Tasks())
+
+ // final steps will are postponed until we are in the restarted snapd
+ ok, rst = restart.Pending(st)
+ c.Assert(ok, Equals, false)
+ c.Assert(rst, Equals, restart.RestartUnset)
+
+ // settle has exited as there are no more tasks that can be run due to
+ // the circular dependency, we expect all tasks to be in either Do or
+ // Done states
+ for _, tsk := range chg.Tasks() {
+ if tsk.Status() != state.DoneStatus && tsk.Status() != state.DoStatus {
+ c.Errorf("unexpected status %s of task %s %s", tsk.Status(), tsk.ID(), tsk.Summary())
+ c.FailNow()
+ }
+ }
+
+ // start settle and wait for prune to kick in
+ restoreIntv := overlord.MockPruneInterval(200*time.Millisecond, 1000*time.Millisecond, 1000*time.Millisecond)
+ defer restoreIntv()
+
+ st.Unlock()
+ s.o.Loop()
+
+ checkTicker := time.NewTicker(time.Second)
+ timeout := time.After(settleTimeout)
+waitLoop:
+ for {
+ select {
+ case <-checkTicker.C:
+ st.Lock()
+ rdy := chg.IsReady()
+ st.Unlock()
+ if rdy {
+ break waitLoop
+ }
+ case <-timeout:
+ c.Errorf("timeout waiting for prune to complete")
+ c.FailNow()
+ }
+ }
+
+ err = s.o.Stop()
+ c.Assert(err, IsNil)
+
+ st.Lock()
+
+ c.Assert(chg.IsReady(), Equals, true)
+
+ dumpTasks(c, "after prune", chg.Tasks())
+
+ // snapd should have been hept
+ for _, tsk := range chg.Tasks() {
+ if tsk.Kind() == "link-snap" {
+ snapsup, err := snapstate.TaskSnapSetup(tsk)
+ c.Assert(err, IsNil)
+ if snapsup.InstanceName() == "snapd" {
+ c.Assert(tsk.Status(), Equals, state.DoneStatus)
+ }
+ }
+ }
+
+ // snapd update is kept
+ err = snapstate.Get(st, "snapd", &snapst)
+ c.Assert(err, IsNil)
+ c.Assert(snapst.Current, Equals, snap.R(2))
+
+ for _, name := range []string{"pc-kernel", "pc", "core20"} {
+ err = snapstate.Get(st, name, &snapst)
+ c.Assert(err, IsNil)
+ // the current is the old revision
+ c.Assert(snapst.Current, Equals, snap.R(1))
+ }
+}
+
type gadgetUpdatesSuite struct {
baseMgrsSuite
diff --git a/overlord/overlord.go b/overlord/overlord.go
index c87d632792..593b637675 100644
--- a/overlord/overlord.go
+++ b/overlord/overlord.go
@@ -60,7 +60,7 @@ var (
ensureInterval = 5 * time.Minute
pruneInterval = 10 * time.Minute
pruneWait = 24 * time.Hour * 1
- abortWait = 24 * time.Hour * 7
+ abortWait = 24 * time.Hour * 3
pruneMaxChanges = 500
diff --git a/overlord/snapstate/backend_test.go b/overlord/snapstate/backend_test.go
index f9511b906d..fa13e5104a 100644
--- a/overlord/snapstate/backend_test.go
+++ b/overlord/snapstate/backend_test.go
@@ -334,6 +334,7 @@ func (f *fakeStore) lookupRefresh(cand refreshCand) (*snap.Info, error) {
typ := snap.TypeApp
epoch := snap.E("1*")
+ base := ""
switch cand.snapID {
case "":
@@ -380,6 +381,10 @@ func (f *fakeStore) lookupRefresh(cand refreshCand) (*snap.Info, error) {
case "kernel-id":
name = "kernel"
typ = snap.TypeKernel
+ case "gadget-core18-id":
+ name = "gadget"
+ typ = snap.TypeGadget
+ base = "core18"
case "brand-kernel-id":
name = "brand-kernel"
typ = snap.TypeKernel
@@ -430,6 +435,7 @@ func (f *fakeStore) lookupRefresh(cand refreshCand) (*snap.Info, error) {
Confinement: confinement,
Architectures: []string{"all"},
Epoch: epoch,
+ Base: base,
}
if name == "outdated-consumer" {
diff --git a/overlord/snapstate/snapstate.go b/overlord/snapstate/snapstate.go
index 929924db03..a3879bdfea 100644
--- a/overlord/snapstate/snapstate.go
+++ b/overlord/snapstate/snapstate.go
@@ -1601,9 +1601,13 @@ func doUpdate(ctx context.Context, st *state.State, names []string, updates []mi
kernelTs.WaitAll(gadgetTs)
}
- if deviceCtx.Model().Base() != "" {
+ if deviceCtx.Model().Base() != "" && (gadgetTs == nil || enforcedSingleRebootForGadgetKernelBase) {
// reordering of kernel and base tasks is supported only on
// UC18+ devices
+ // this can only be done safely when the gadget is not a part of
+ // the same update, otherwise there will be a circular
+ // dependency, where gadget waits for base, kernel waits for
+ // gadget, but base waits for some of the kernel tasks
if err := rearrangeBaseKernelForSingleReboot(kernelTs, bootBaseTs); err != nil {
return nil, nil, err
}
@@ -3553,3 +3557,17 @@ func MockEnforcedValidationSets(f func(st *state.State) (*snapasserts.Validation
EnforcedValidationSets = old
}
}
+
+// only useful for procuring a buggy behavior in the tests
+var enforcedSingleRebootForGadgetKernelBase = false
+
+func MockEnforceSingleRebootForBaseKernelGadget(val bool) (restore func()) {
+ osutil.MustBeTestBinary("mocking can be done only in tests")
+
+ old := enforcedSingleRebootForGadgetKernelBase
+ enforcedSingleRebootForGadgetKernelBase = val
+ return func() {
+ enforcedSingleRebootForGadgetKernelBase = old
+ }
+
+}
diff --git a/overlord/snapstate/snapstate_update_test.go b/overlord/snapstate/snapstate_update_test.go
index d6ecc404b6..8be1ad6f6a 100644
--- a/overlord/snapstate/snapstate_update_test.go
+++ b/overlord/snapstate/snapstate_update_test.go
@@ -7518,6 +7518,98 @@ func (s *snapmgrTestSuite) TestUpdateBaseKernelSingleRebootUnsupportedWithCoreHa
}
}
+func (s *snapmgrTestSuite) TestUpdateBaseKernelSingleRebootUnsupportedWithGadget(c *C) {
+ restore := release.MockOnClassic(false)
+ defer restore()
+ restore = snapstate.MockRevisionDate(nil)
+ defer restore()
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ var restartRequested []restart.RestartType
+ restart.Init(s.state, "boot-id-0", snapstatetest.MockRestartHandler(func(t restart.RestartType) {
+ restartRequested = append(restartRequested, t)
+ }))
+
+ restore = snapstatetest.MockDeviceModel(MakeModel(map[string]interface{}{
+ "kernel": "kernel",
+ "base": "core18",
+ }))
+ defer restore()
+
+ siKernel := snap.SideInfo{
+ RealName: "kernel",
+ Revision: snap.R(7),
+ SnapID: "kernel-id",
+ }
+ siBase := snap.SideInfo{
+ RealName: "core18",
+ Revision: snap.R(7),
+ SnapID: "core18-snap-id",
+ }
+ siGadget := snap.SideInfo{
+ RealName: "gadget",
+ Revision: snap.R(7),
+ SnapID: "gadget-core18-id",
+ }
+ for _, si := range []*snap.SideInfo{&siKernel, &siBase, &siGadget} {
+ snaptest.MockSnap(c, fmt.Sprintf(`name: %s`, si.RealName), si)
+ typ := "kernel"
+ if si.RealName == "core18" {
+ typ = "base"
+ } else if si.RealName == "gadget" {
+ typ = "gadget"
+ }
+ snapstate.Set(s.state, si.RealName, &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{si},
+ Current: si.Revision,
+ TrackingChannel: "latest/stable",
+ SnapType: typ,
+ })
+ }
+
+ chg := s.state.NewChange("refresh", "refresh kernel and base")
+ affected, tss, err := snapstate.UpdateMany(context.Background(), s.state,
+ []string{"kernel", "core18", "gadget"}, s.user.ID, &snapstate.Flags{})
+ c.Assert(err, IsNil)
+ c.Assert(affected, DeepEquals, []string{"core18", "gadget", "kernel"})
+ var kernelTsk, baseTsk, gadgetTsk *state.Task
+ for _, ts := range tss {
+ chg.AddAll(ts)
+ for _, tsk := range ts.Tasks() {
+ switch tsk.Kind() {
+ // setup-profiles should appear right before link-snap,
+ // while set-auto-aliase appears right after
+ // auto-connect
+ case "link-snap":
+ snapsup, err := snapstate.TaskSnapSetup(tsk)
+ c.Assert(err, IsNil)
+ switch snapsup.InstanceName() {
+ case "kernel":
+ kernelTsk = tsk
+ case "gadget":
+ gadgetTsk = tsk
+ case "core18":
+ baseTsk = tsk
+ }
+ var dummy bool
+ // the flag isn't set for any of link-snap tasks
+ c.Assert(tsk.Get("cannot-reboot", &dummy), Equals, state.ErrNoState)
+ }
+ }
+ }
+
+ c.Assert(kernelTsk, NotNil)
+ c.Assert(baseTsk, NotNil)
+ c.Assert(gadgetTsk, NotNil)
+
+ c.Assert(kernelTsk.WaitTasks(), testutil.Contains, gadgetTsk)
+ c.Assert(kernelTsk.WaitTasks(), Not(testutil.Contains), baseTsk)
+ c.Assert(baseTsk.WaitTasks(), Not(testutil.Contains), kernelTsk)
+}
+
func (s *snapmgrTestSuite) TestUpdateBaseKernelSingleRebootUndone(c *C) {
restore := release.MockOnClassic(false)
defer restore()
diff --git a/overlord/state/state.go b/overlord/state/state.go
index 282bdcf696..6bb9b60c47 100644
--- a/overlord/state/state.go
+++ b/overlord/state/state.go
@@ -405,7 +405,7 @@ func (s *State) Prune(startOfOperation time.Time, pruneWait, abortWait time.Dura
chg.Abort()
delete(s.changes, chg.ID())
} else if spawnTime.Before(abortLimit) {
- chg.Abort()
+ chg.AbortUnreadyLanes()
}
continue
}