diff options
| author | Maciej Borzecki <maciej.zenon.borzecki@canonical.com> | 2022-03-21 15:48:58 +0100 |
|---|---|---|
| committer | Michael Vogt <mvo@ubuntu.com> | 2022-03-21 15:49:15 +0100 |
| commit | 8539a46802e7c335b56bc7d38dc04f2d21246984 (patch) | |
| tree | beb8f8f41bb5e09364ad23a4f8d3b7cd5b50afcd | |
| parent | bf9e2409232c1b9416851a9c726755e0d20a2f26 (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.go | 450 | ||||
| -rw-r--r-- | overlord/overlord.go | 2 | ||||
| -rw-r--r-- | overlord/snapstate/backend_test.go | 6 | ||||
| -rw-r--r-- | overlord/snapstate/snapstate.go | 20 | ||||
| -rw-r--r-- | overlord/snapstate/snapstate_update_test.go | 92 | ||||
| -rw-r--r-- | overlord/state/state.go | 2 |
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 } |
