diff --git a/daemon/api_sideload_n_try.go b/daemon/api_sideload_n_try.go index 33e8f493aba..7dc7b0ef5b8 100644 --- a/daemon/api_sideload_n_try.go +++ b/daemon/api_sideload_n_try.go @@ -71,6 +71,7 @@ func sideloadOrTrySnap(c *Command, body io.ReadCloser, boundary string, user *au flags.Unaliased = isTrue(form, "unaliased") flags.IgnoreRunning = isTrue(form, "ignore-running") + systemRestartImmediate := isTrue(form, "system-restart-immediate") // find the file for the "snap" form field var snapBody multipart.File @@ -192,6 +193,9 @@ out: } chg := newChange(st, "install-snap", msg, []*state.TaskSet{tset}, []string{instanceName}) + if systemRestartImmediate { + chg.Set("system-restart-immediate", true) + } chg.Set("api-data", map[string]string{"snap-name": instanceName}) ensureStateSoon(st) diff --git a/daemon/api_sideload_n_try_test.go b/daemon/api_sideload_n_try_test.go index 55092ddaa21..857382f65c9 100644 --- a/daemon/api_sideload_n_try_test.go +++ b/daemon/api_sideload_n_try_test.go @@ -79,8 +79,9 @@ func (s *sideloadSuite) TestSideloadSnapOnNonDevModeDistro(c *check.C) { // try a multipart/form-data upload body := sideLoadBodyWithoutDevMode head := map[string]string{"Content-Type": "multipart/thing; boundary=--hello--"} - chgSummary := s.sideloadCheck(c, body, head, "local", snapstate.Flags{RemoveSnapPath: true}) + chgSummary, systemRestartImmediate := s.sideloadCheck(c, body, head, "local", snapstate.Flags{RemoveSnapPath: true}) c.Check(chgSummary, check.Equals, `Install "local" snap from file "a/b/local.snap"`) + c.Check(systemRestartImmediate, check.Equals, false) } func (s *sideloadSuite) TestSideloadSnapOnDevModeDistro(c *check.C) { @@ -90,7 +91,7 @@ func (s *sideloadSuite) TestSideloadSnapOnDevModeDistro(c *check.C) { restore := sandbox.MockForceDevMode(true) defer restore() flags := snapstate.Flags{RemoveSnapPath: true} - chgSummary := s.sideloadCheck(c, body, head, "local", flags) + chgSummary, _ := s.sideloadCheck(c, body, head, "local", flags) c.Check(chgSummary, check.Equals, `Install "local" snap from file "a/b/local.snap"`) } @@ -109,7 +110,7 @@ func (s *sideloadSuite) TestSideloadSnapDevMode(c *check.C) { // try a multipart/form-data upload flags := snapstate.Flags{RemoveSnapPath: true} flags.DevMode = true - chgSummary := s.sideloadCheck(c, body, head, "local", flags) + chgSummary, _ := s.sideloadCheck(c, body, head, "local", flags) c.Check(chgSummary, check.Equals, `Install "local" snap from file "x"`) } @@ -131,11 +132,11 @@ func (s *sideloadSuite) TestSideloadSnapJailMode(c *check.C) { head := map[string]string{"Content-Type": "multipart/thing; boundary=--hello--"} // try a multipart/form-data upload flags := snapstate.Flags{JailMode: true, RemoveSnapPath: true} - chgSummary := s.sideloadCheck(c, body, head, "local", flags) + chgSummary, _ := s.sideloadCheck(c, body, head, "local", flags) c.Check(chgSummary, check.Equals, `Install "local" snap from file "x"`) } -func (s *sideloadSuite) sideloadCheck(c *check.C, content string, head map[string]string, expectedInstanceName string, expectedFlags snapstate.Flags) string { +func (s *sideloadSuite) sideloadCheck(c *check.C, content string, head map[string]string, expectedInstanceName string, expectedFlags snapstate.Flags) (summary string, systemRestartImmediate bool) { d := s.daemonWithFakeSnapManager(c) soon := 0 @@ -214,7 +215,12 @@ func (s *sideloadSuite) sideloadCheck(c *check.C, content string, head map[strin "snap-name": expectedInstanceName, }) - return chg.Summary() + summary = chg.Summary() + err = chg.Get("system-restart-immediate", &systemRestartImmediate) + if err != nil && err != state.ErrNoState { + c.Error(err) + } + return summary, systemRestartImmediate } func (s *sideloadSuite) TestSideloadSnapJailModeAndDevmode(c *check.C) { @@ -420,7 +426,7 @@ func (s *sideloadSuite) TestSideloadSnapInstanceName(c *check.C) { "local_instance\r\n" + "----hello--\r\n" head := map[string]string{"Content-Type": "multipart/thing; boundary=--hello--"} - chgSummary := s.sideloadCheck(c, body, head, "local_instance", snapstate.Flags{RemoveSnapPath: true}) + chgSummary, _ := s.sideloadCheck(c, body, head, "local_instance", snapstate.Flags{RemoveSnapPath: true}) c.Check(chgSummary, check.Equals, `Install "local_instance" snap from file "a/b/local.snap"`) } @@ -432,7 +438,7 @@ func (s *sideloadSuite) TestSideloadSnapInstanceNameNoKey(c *check.C) { "local\r\n" + "----hello--\r\n" head := map[string]string{"Content-Type": "multipart/thing; boundary=--hello--"} - chgSummary := s.sideloadCheck(c, body, head, "local", snapstate.Flags{RemoveSnapPath: true}) + chgSummary, _ := s.sideloadCheck(c, body, head, "local", snapstate.Flags{RemoveSnapPath: true}) c.Check(chgSummary, check.Equals, `Install "local" snap from file "a/b/local.snap"`) } @@ -475,8 +481,31 @@ func (s *sideloadSuite) TestInstallPathUnaliased(c *check.C) { head := map[string]string{"Content-Type": "multipart/thing; boundary=--hello--"} // try a multipart/form-data upload flags := snapstate.Flags{Unaliased: true, RemoveSnapPath: true, DevMode: true} - chgSummary := s.sideloadCheck(c, body, head, "local", flags) + chgSummary, _ := s.sideloadCheck(c, body, head, "local", flags) + c.Check(chgSummary, check.Equals, `Install "local" snap from file "x"`) +} + +func (s *sideloadSuite) TestInstallPathSystemRestartImmediate(c *check.C) { + body := "" + + "----hello--\r\n" + + "Content-Disposition: form-data; name=\"snap\"; filename=\"x\"\r\n" + + "\r\n" + + "xyzzy\r\n" + + "----hello--\r\n" + + "Content-Disposition: form-data; name=\"devmode\"\r\n" + + "\r\n" + + "true\r\n" + + "----hello--\r\n" + + "Content-Disposition: form-data; name=\"system-restart-immediate\"\r\n" + + "\r\n" + + "true\r\n" + + "----hello--\r\n" + head := map[string]string{"Content-Type": "multipart/thing; boundary=--hello--"} + // try a multipart/form-data upload + flags := snapstate.Flags{RemoveSnapPath: true, DevMode: true} + chgSummary, systemRestartImmediate := s.sideloadCheck(c, body, head, "local", flags) c.Check(chgSummary, check.Equals, `Install "local" snap from file "x"`) + c.Check(systemRestartImmediate, check.Equals, true) } type trySuite struct { diff --git a/daemon/api_snaps.go b/daemon/api_snaps.go index 6ab4a5b4d53..0cda2775093 100644 --- a/daemon/api_snaps.go +++ b/daemon/api_snaps.go @@ -147,6 +147,9 @@ func postSnap(c *Command, r *http.Request, user *auth.UserState) Response { } chg := newChange(state, inst.Action+"-snap", msg, tsets, inst.Snaps) + if inst.SystemRestartImmediate { + chg.Set("system-restart-immediate", true) + } ensureStateSoon(state) @@ -186,15 +189,16 @@ type snapInstruction struct { Action string `json:"action"` Amend bool `json:"amend"` snapRevisionOptions - DevMode bool `json:"devmode"` - JailMode bool `json:"jailmode"` - Classic bool `json:"classic"` - IgnoreValidation bool `json:"ignore-validation"` - IgnoreRunning bool `json:"ignore-running"` - Unaliased bool `json:"unaliased"` - Purge bool `json:"purge,omitempty"` - Snaps []string `json:"snaps"` - Users []string `json:"users"` + DevMode bool `json:"devmode"` + JailMode bool `json:"jailmode"` + Classic bool `json:"classic"` + IgnoreValidation bool `json:"ignore-validation"` + IgnoreRunning bool `json:"ignore-running"` + Unaliased bool `json:"unaliased"` + Purge bool `json:"purge,omitempty"` + SystemRestartImmediate bool `json:"system-restart-immediate"` + Snaps []string `json:"snaps"` + Users []string `json:"users"` // The fields below should not be unmarshalled into. Do not export them. userID int @@ -528,6 +532,10 @@ func snapOpMany(c *Command, r *http.Request, user *auth.UserState) Response { ensureStateSoon(st) } + if inst.SystemRestartImmediate { + chg.Set("system-restart-immediate", true) + } + chg.Set("api-data", map[string]interface{}{"snap-names": res.Affected}) return AsyncResponse(res.Result, chg.ID()) diff --git a/daemon/api_snaps_test.go b/daemon/api_snaps_test.go index 4bf61c5464f..e13504d8116 100644 --- a/daemon/api_snaps_test.go +++ b/daemon/api_snaps_test.go @@ -470,14 +470,21 @@ func (s *snapsSuite) TestPostSnapsNoWeirdses(c *check.C) { } func (s *snapsSuite) TestPostSnapsOp(c *check.C) { - s.testPostSnapsOp(c, "application/json") + systemRestartImmediate := s.testPostSnapsOp(c, "", "application/json") + c.Check(systemRestartImmediate, check.Equals, false) } func (s *snapsSuite) TestPostSnapsOpMoreComplexContentType(c *check.C) { - s.testPostSnapsOp(c, "application/json; charset=utf-8") + systemRestartImmediate := s.testPostSnapsOp(c, "", "application/json; charset=utf-8") + c.Check(systemRestartImmediate, check.Equals, false) } -func (s *snapsSuite) testPostSnapsOp(c *check.C, contentType string) { +func (s *snapsSuite) TestPostSnapsOpSystemRestartImmediate(c *check.C) { + systemRestartImmediate := s.testPostSnapsOp(c, `"system-restart-immediate": true`, "application/json") + c.Check(systemRestartImmediate, check.Equals, true) +} + +func (s *snapsSuite) testPostSnapsOp(c *check.C, extraJSON, contentType string) (systemRestartImmediate bool) { defer daemon.MockAssertstateRefreshSnapDeclarations(func(*state.State, int) error { return nil })() defer daemon.MockSnapstateUpdateMany(func(_ context.Context, s *state.State, names []string, userID int, flags *snapstate.Flags) ([]string, []*state.TaskSet, error) { c.Check(names, check.HasLen, 0) @@ -487,7 +494,10 @@ func (s *snapsSuite) testPostSnapsOp(c *check.C, contentType string) { d := s.daemonWithOverlordMockAndStore(c) - buf := bytes.NewBufferString(`{"action": "refresh"}`) + if extraJSON != "" { + extraJSON = "," + extraJSON + } + buf := bytes.NewBufferString(fmt.Sprintf(`{"action": "refresh"%s}`, extraJSON)) req, err := http.NewRequest("POST", "/v2/snaps", buf) c.Assert(err, check.IsNil) req.Header.Set("Content-Type", contentType) @@ -502,6 +512,11 @@ func (s *snapsSuite) testPostSnapsOp(c *check.C, contentType string) { var apiData map[string]interface{} c.Check(chg.Get("api-data", &apiData), check.IsNil) c.Check(apiData["snap-names"], check.DeepEquals, []interface{}{"fake1", "fake2"}) + err = chg.Get("system-restart-immediate", &systemRestartImmediate) + if err != nil && err != state.ErrNoState { + c.Error(err) + } + return systemRestartImmediate } func (s *snapsSuite) TestPostSnapsOpInvalidCharset(c *check.C) { @@ -1106,14 +1121,32 @@ func (s *snapsSuite) TestPostSnapBadChannel(c *check.C) { } func (s *snapsSuite) TestPostSnap(c *check.C) { - s.testPostSnap(c, false) + checkOpts := func(opts *snapstate.RevisionOptions) { + // no channel in -> no channel out + c.Check(opts.Channel, check.Equals, "") + } + summary, systemRestartImmediate := s.testPostSnap(c, "", checkOpts) + c.Check(summary, check.Equals, `Install "foo" snap`) + c.Check(systemRestartImmediate, check.Equals, false) } func (s *snapsSuite) TestPostSnapWithChannel(c *check.C) { - s.testPostSnap(c, true) + checkOpts := func(opts *snapstate.RevisionOptions) { + // channel in -> channel out + c.Check(opts.Channel, check.Equals, "xyzzy") + } + summary, systemRestartImmediate := s.testPostSnap(c, `"channel": "xyzzy"`, checkOpts) + c.Check(summary, check.Equals, `Install "foo" snap from "xyzzy" channel`) + c.Check(systemRestartImmediate, check.Equals, false) +} + +func (s *snapsSuite) TestPostSnapSystemRestartImmediate(c *check.C) { + checkOpts := func(opts *snapstate.RevisionOptions) {} + _, systemRestartImmediate := s.testPostSnap(c, `"system-restart-immediate": true`, checkOpts) + c.Check(systemRestartImmediate, check.Equals, true) } -func (s *snapsSuite) testPostSnap(c *check.C, withChannel bool) { +func (s *snapsSuite) testPostSnap(c *check.C, extraJSON string, checkOpts func(opts *snapstate.RevisionOptions)) (summary string, systemRestartImmediate bool) { d := s.daemonWithOverlordMock(c) soon := 0 @@ -1126,25 +1159,17 @@ func (s *snapsSuite) testPostSnap(c *check.C, withChannel bool) { checked := false defer daemon.MockSnapstateInstall(func(ctx context.Context, s *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags) (*state.TaskSet, error) { - if withChannel { - // channel in -> channel out - c.Check(opts.Channel, check.Equals, "xyzzy") - } else { - // no channel in -> no channel out - c.Check(opts.Channel, check.Equals, "") - } + checkOpts(opts) checked = true - t := s.NewTask("fake-install-snap", "Doing a fake install") return state.NewTaskSet(t), nil })() var buf *bytes.Buffer - if withChannel { - buf = bytes.NewBufferString(`{"action": "install", "channel": "xyzzy"}`) - } else { - buf = bytes.NewBufferString(`{"action": "install"}`) + if extraJSON != "" { + extraJSON = "," + extraJSON } + buf = bytes.NewBufferString(fmt.Sprintf(`{"action": "install"%s}`, extraJSON)) req, err := http.NewRequest("POST", "/v2/snaps/foo", buf) c.Assert(err, check.IsNil) @@ -1155,11 +1180,7 @@ func (s *snapsSuite) testPostSnap(c *check.C, withChannel bool) { defer st.Unlock() chg := st.Change(rsp.Change) c.Assert(chg, check.NotNil) - if withChannel { - c.Check(chg.Summary(), check.Equals, `Install "foo" snap from "xyzzy" channel`) - } else { - c.Check(chg.Summary(), check.Equals, `Install "foo" snap`) - } + var names []string err = chg.Get("snap-names", &names) c.Assert(err, check.IsNil) @@ -1168,6 +1189,13 @@ func (s *snapsSuite) testPostSnap(c *check.C, withChannel bool) { c.Check(checked, check.Equals, true) c.Check(soon, check.Equals, 1) c.Check(chg.Tasks()[0].Summary(), check.Equals, "Doing a fake install") + + summary = chg.Summary() + err = chg.Get("system-restart-immediate", &systemRestartImmediate) + if err != nil && err != state.ErrNoState { + c.Error(err) + } + return summary, systemRestartImmediate } func (s *snapsSuite) TestPostSnapVerifySnapInstruction(c *check.C) { diff --git a/overlord/devicestate/devicestate_bootconfig_test.go b/overlord/devicestate/devicestate_bootconfig_test.go index 4f5a870d8c9..cc03f527bea 100644 --- a/overlord/devicestate/devicestate_bootconfig_test.go +++ b/overlord/devicestate/devicestate_bootconfig_test.go @@ -105,6 +105,7 @@ func (s *deviceMgrBootconfigSuite) testBootConfigUpdateRun(c *C, updateAttempted tsk := s.state.NewTask("update-managed-boot-config", "update boot config") chg := s.state.NewChange("dummy", "...") chg.AddTask(tsk) + chg.Set("system-restart-immediate", true) s.state.Unlock() s.settle(c) @@ -128,7 +129,7 @@ func (s *deviceMgrBootconfigSuite) testBootConfigUpdateRun(c *C, updateAttempted c.Assert(log, HasLen, 1) c.Check(log[0], Matches, ".* updated boot config assets") // update was applied, thus a restart was requested - c.Check(s.restartRequests, DeepEquals, []state.RestartType{state.RestartSystem}) + c.Check(s.restartRequests, DeepEquals, []state.RestartType{state.RestartSystemNow}) } else { // update was not applied or failed c.Check(s.restartRequests, HasLen, 0) diff --git a/overlord/devicestate/devicestate_gadget_test.go b/overlord/devicestate/devicestate_gadget_test.go index 015f71074d8..18b0f1cce0f 100644 --- a/overlord/devicestate/devicestate_gadget_test.go +++ b/overlord/devicestate/devicestate_gadget_test.go @@ -258,7 +258,7 @@ func (s *deviceMgrGadgetSuite) setupGadgetUpdate(c *C, modelGrade, gadgetYamlCon return chg, tsk } -func (s *deviceMgrGadgetSuite) testUpdateGadgetOnCoreSimple(c *C, grade string, encryption bool, gadgetYamlCont, gadgetYamlContNext string) { +func (s *deviceMgrGadgetSuite) testUpdateGadgetOnCoreSimple(c *C, grade string, encryption, immediate bool, gadgetYamlCont, gadgetYamlContNext string) { var updateCalled bool var passedRollbackDir string @@ -345,8 +345,13 @@ func (s *deviceMgrGadgetSuite) testUpdateGadgetOnCoreSimple(c *C, grade string, } devicestate.SetBootOkRan(s.mgr, true) + expectedRst := state.RestartSystem s.state.Lock() s.state.Set("seeded", true) + if immediate { + expectedRst = state.RestartSystemNow + chg.Set("system-restart-immediate", true) + } s.state.Unlock() s.settle(c) @@ -361,23 +366,32 @@ func (s *deviceMgrGadgetSuite) testUpdateGadgetOnCoreSimple(c *C, grade string, c.Check(rollbackDir, Equals, passedRollbackDir) // should have been removed right after update c.Check(osutil.IsDirectory(rollbackDir), Equals, false) - c.Check(s.restartRequests, DeepEquals, []state.RestartType{state.RestartSystem}) + c.Check(s.restartRequests, DeepEquals, []state.RestartType{expectedRst}) } func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreSimple(c *C) { // unset grade encryption := false - s.testUpdateGadgetOnCoreSimple(c, "", encryption, gadgetYaml, "") + immediate := false + s.testUpdateGadgetOnCoreSimple(c, "", encryption, immediate, gadgetYaml, "") } func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnUC20CoreSimpleWithEncryption(c *C) { encryption := true - s.testUpdateGadgetOnCoreSimple(c, "dangerous", encryption, uc20gadgetYaml, "") + immediate := false + s.testUpdateGadgetOnCoreSimple(c, "dangerous", encryption, immediate, uc20gadgetYaml, "") } func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnUC20CoreSimpleNoEncryption(c *C) { encryption := false - s.testUpdateGadgetOnCoreSimple(c, "dangerous", encryption, uc20gadgetYaml, "") + immediate := false + s.testUpdateGadgetOnCoreSimple(c, "dangerous", encryption, immediate, uc20gadgetYaml, "") +} + +func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnUC20CoreSimpleSystemRestartImmediate(c *C) { + encryption := false + immediate := true + s.testUpdateGadgetOnCoreSimple(c, "dangerous", encryption, immediate, uc20gadgetYaml, "") } func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreNoUpdateNeeded(c *C) { @@ -928,17 +942,19 @@ func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreHybridFirstboot(c *C) { func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreHybridShouldWork(c *C) { encryption := false - s.testUpdateGadgetOnCoreSimple(c, "", encryption, hybridGadgetYaml, "") + immediate := false + s.testUpdateGadgetOnCoreSimple(c, "", encryption, immediate, hybridGadgetYaml, "") } func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreOldIsInvalidNowButShouldWork(c *C) { encryption := false + immediate := false // this is not gadget yaml that we should support, by the UC16/18 // rules it actually has two system-boot role partitions, hybridGadgetYamlBroken := hybridGadgetYaml + ` role: system-boot ` - s.testUpdateGadgetOnCoreSimple(c, "", encryption, hybridGadgetYamlBroken, hybridGadgetYaml) + s.testUpdateGadgetOnCoreSimple(c, "", encryption, immediate, hybridGadgetYamlBroken, hybridGadgetYaml) } func (s *deviceMgrGadgetSuite) makeMinimalKernelAssetsUpdateChange(c *C) (chg *state.Change, tsk *state.Task) { @@ -1428,6 +1444,7 @@ func (s *deviceMgrGadgetSuite) TestGadgetCommandlineUpdateUndo(c *C) { chg := s.state.NewChange("dummy", "...") chg.AddTask(tsk) chg.AddTask(terr) + chg.Set("system-restart-immediate", true) s.state.Unlock() restartCount := 0 @@ -1469,7 +1486,7 @@ func (s *deviceMgrGadgetSuite) TestGadgetCommandlineUpdateUndo(c *C) { c.Check(log[0], Matches, ".* Updated kernel command line") c.Check(log[1], Matches, ".* Reverted kernel command line change") // update was applied and then undone - c.Check(s.restartRequests, DeepEquals, []state.RestartType{state.RestartSystem, state.RestartSystem}) + c.Check(s.restartRequests, DeepEquals, []state.RestartType{state.RestartSystemNow, state.RestartSystemNow}) c.Check(restartCount, Equals, 2) vars, err := s.managedbl.GetBootVars("snapd_extra_cmdline_args") c.Assert(err, IsNil) diff --git a/overlord/devicestate/handlers_bootconfig.go b/overlord/devicestate/handlers_bootconfig.go index c97809d5616..cccbbd2ff52 100644 --- a/overlord/devicestate/handlers_bootconfig.go +++ b/overlord/devicestate/handlers_bootconfig.go @@ -25,6 +25,7 @@ import ( "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/boot" + "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/release" ) @@ -81,7 +82,7 @@ func (m *DeviceManager) doUpdateManagedBootConfig(t *state.Task, _ *tomb.Tomb) e // boot assets were updated, request a restart now so that the // situation does not end up more complicated if more updates of // boot assets were to be applied - st.RequestRestart(state.RestartSystem) + snapstate.RestartSystem(t) } // minimize wasteful redos diff --git a/overlord/devicestate/handlers_gadget.go b/overlord/devicestate/handlers_gadget.go index 684d508fdd6..44090486acc 100644 --- a/overlord/devicestate/handlers_gadget.go +++ b/overlord/devicestate/handlers_gadget.go @@ -209,7 +209,7 @@ func (m *DeviceManager) doUpdateGadgetAssets(t *state.Task, _ *tomb.Tomb) error // TODO: consider having the option to do this early via recovery in // core20, have fallback code as well there - st.RequestRestart(state.RestartSystem) + snapstate.RestartSystem(t) return nil } @@ -284,7 +284,7 @@ func (m *DeviceManager) doUpdateGadgetCommandLine(t *state.Task, _ *tomb.Tomb) e // kernel command line // kernel command line was updated, request a reboot to make it effective - st.RequestRestart(state.RestartSystem) + snapstate.RestartSystem(t) return nil } @@ -321,6 +321,6 @@ func (m *DeviceManager) undoUpdateGadgetCommandLine(t *state.Task, _ *tomb.Tomb) t.SetStatus(state.UndoneStatus) // kernel command line was updated, request a reboot to make it effective - st.RequestRestart(state.RestartSystem) + snapstate.RestartSystem(t) return nil } diff --git a/overlord/ifacestate/ifacestate_test.go b/overlord/ifacestate/ifacestate_test.go index d3db90aa5ec..803020dea22 100644 --- a/overlord/ifacestate/ifacestate_test.go +++ b/overlord/ifacestate/ifacestate_test.go @@ -1865,7 +1865,7 @@ func (s *interfaceManagerSuite) TestStaleConnectionsIgnoredInReloadConnections(c } func (s *interfaceManagerSuite) testStaleAutoConnectionsNotRemovedIfSnapBroken(c *C, brokenSnapName string) { - s.mockIfaces(&ifacetest.TestInterface{InterfaceName: "test"}) + s.mockIfaces(c, &ifacetest.TestInterface{InterfaceName: "test"}) s.state.Lock() defer s.state.Unlock() diff --git a/overlord/snapstate/handlers.go b/overlord/snapstate/handlers.go index 085b7eeafd4..e7a01ab36cb 100644 --- a/overlord/snapstate/handlers.go +++ b/overlord/snapstate/handlers.go @@ -1436,7 +1436,7 @@ func (m *SnapManager) maybeRestart(t *state.Task, info *snap.Info, rebootRequire if rebootRequired { t.Logf("Requested system restart.") - st.RequestRestart(state.RestartSystem) + RestartSystem(t) return } diff --git a/overlord/snapstate/handlers_link_test.go b/overlord/snapstate/handlers_link_test.go index 094761c4a01..5fe2d715dc4 100644 --- a/overlord/snapstate/handlers_link_test.go +++ b/overlord/snapstate/handlers_link_test.go @@ -844,6 +844,46 @@ func (s *linkSnapSuite) TestDoLinkSnapSuccessRebootForCoreBase(c *C) { c.Check(t.Log()[0], Matches, `.*INFO Requested system restart.*`) } +func (s *linkSnapSuite) TestDoLinkSnapSuccessRebootForCoreBaseSystemRestartImmediate(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + r := snapstatetest.MockDeviceModel(ModelWithBase("core18")) + defer r() + + s.fakeBackend.linkSnapMaybeReboot = true + + s.state.Lock() + defer s.state.Unlock() + + // we need to init the boot-id + err := s.state.VerifyReboot("some-boot-id") + c.Assert(err, IsNil) + + si := &snap.SideInfo{ + RealName: "core18", + SnapID: "core18-id", + Revision: snap.R(22), + } + t := s.state.NewTask("link-snap", "test") + t.Set("snap-setup", &snapstate.SnapSetup{ + SideInfo: si, + }) + chg := s.state.NewChange("dummy", "...") + chg.AddTask(t) + chg.Set("system-restart-immediate", true) + + s.state.Unlock() + s.se.Ensure() + s.se.Wait() + s.state.Lock() + + c.Check(t.Status(), Equals, state.DoneStatus) + c.Check(s.stateBackend.restartRequested, DeepEquals, []state.RestartType{state.RestartSystemNow}) + c.Assert(t.Log(), HasLen, 1) + c.Check(t.Log()[0], Matches, `.*INFO Requested system restart.*`) +} + func (s *linkSnapSuite) TestDoLinkSnapSuccessSnapdRestartsOnClassic(c *C) { restore := release.MockOnClassic(true) defer restore() diff --git a/overlord/snapstate/snapstate.go b/overlord/snapstate/snapstate.go index 659b36ada51..23e36e03ca3 100644 --- a/overlord/snapstate/snapstate.go +++ b/overlord/snapstate/snapstate.go @@ -679,6 +679,27 @@ func FinishRestart(task *state.Task, snapsup *SnapSetup) (err error) { return nil } +// RestartSystem requests a system restart. +// It considers how the Change the task belongs to is configured +// (system-restart-immediate) to choose whether request an immediate +// restart or not. +func RestartSystem(task *state.Task) { + chg := task.Change() + var immediate bool + if chg != nil { + // ignore errors intentionally, to follow + // RequestRestart itself which does not + // return errors. If the state is corrupt + // something else will error + chg.Get("system-restart-immediate", &immediate) + } + rst := state.RestartSystem + if immediate { + rst = state.RestartSystemNow + } + task.State().RequestRestart(rst) +} + func contentAttr(attrer interfaces.Attrer) string { var s string err := attrer.Attr("content", &s)