diff --git a/internals/cli/cmd_run.go b/internals/cli/cmd_run.go index 2c1f2083..794be96e 100644 --- a/internals/cli/cmd_run.go +++ b/internals/cli/cmd_run.go @@ -28,7 +28,6 @@ import ( "github.com/canonical/pebble/cmd" "github.com/canonical/pebble/internals/daemon" "github.com/canonical/pebble/internals/logger" - "github.com/canonical/pebble/internals/systemd" ) var shortRunHelp = "Run the pebble environment" @@ -114,7 +113,7 @@ func runWatchdog(d *daemon.Daemon) (*time.Ticker, error) { case <-wt.C: // TODO: poke the API here and only report WATCHDOG=1 if it // replies with valid data. - systemd.SdNotify("WATCHDOG=1") + daemon.Notify.Send("WATCHDOG=1") case <-d.Dying(): return } diff --git a/internals/daemon/daemon.go b/internals/daemon/daemon.go index b7405ee0..92542771 100644 --- a/internals/daemon/daemon.go +++ b/internals/daemon/daemon.go @@ -23,7 +23,6 @@ import ( "net" "net/http" "os" - "os/exec" "os/signal" "runtime" "strings" @@ -36,7 +35,6 @@ import ( "github.com/gorilla/mux" "github.com/canonical/pebble/internals/logger" - "github.com/canonical/pebble/internals/osutil" "github.com/canonical/pebble/internals/osutil/sys" "github.com/canonical/pebble/internals/overlord" "github.com/canonical/pebble/internals/overlord/checkstate" @@ -47,13 +45,40 @@ import ( "github.com/canonical/pebble/internals/systemd" ) +// notifySupervisor interface defines the required API for +// sending notifications to a supervisor daemon. +type notifySupervisor interface { + Available() bool + Send(state string) error +} + +// shutdown defines the required API for reboot, poweroff +// and halt control of the underlying system +type shutdown interface { + Reboot(delay time.Duration, msg string) error +} + var ( ErrRestartSocket = fmt.Errorf("daemon stop requested to wait for socket activation") - systemdSdNotify = systemd.SdNotify - sysGetuid = sys.Getuid + sysGetuid = sys.Getuid + + // The following functions should be modified depending on the + // underlying system in use. For example, some systems may + // not have systemd, and will use this system manager directly. + + // Notify provides a way to send a notification to a supervisor + // daemon, if one exists. The default implementation assumes + // systemd and should be replaced if not available. + Notify notifySupervisor = systemd.Notifier + + // Shutdown provides the system specific functionality to + // halt, poweroff or reboot. + Shutdown shutdown = systemd.Shutdown ) +var shutdownMsg = "reboot scheduled to update the system" + // Options holds the daemon setup required for the initialization of a new daemon. type Options struct { // Dir is the pebble directory where all setup is found. Defaults to /var/lib/pebble/default. @@ -441,7 +466,7 @@ func (ct *connTracker) trackConn(conn net.Conn, state http.ConnState) { } func (d *Daemon) CanStandby() bool { - return systemd.SocketAvailable() + return Notify.Available() } func (d *Daemon) initStandbyHandling() { @@ -457,7 +482,7 @@ func (d *Daemon) Start() { // we need to schedule and wait for a system restart d.tomb.Kill(nil) // avoid systemd killing us again while we wait - systemdSdNotify("READY=1") + Notify.Send("READY=1") return } if d.overlord == nil { @@ -504,7 +529,7 @@ func (d *Daemon) Start() { } // notify systemd that we are ready - systemdSdNotify("READY=1") + Notify.Send("READY=1") } // HandleRestart implements overlord.RestartBehavior. @@ -515,7 +540,7 @@ func (d *Daemon) HandleRestart(t restart.RestartType) { case restart.RestartSystem: // try to schedule a fallback slow reboot already here // in case we get stuck shutting down - if err := reboot(rebootWaitTimeout); err != nil { + if err := Shutdown.Reboot(rebootWaitTimeout, shutdownMsg); err != nil { logger.Noticef("%s", err) } @@ -593,7 +618,7 @@ func (d *Daemon) Stop(sigCh chan<- os.Signal) error { if !restartSystem { // tell systemd that we are stopping - systemdSdNotify("STOPPING=1") + Notify.Send("STOPPING=1") } if restartSocket { @@ -700,7 +725,7 @@ func (d *Daemon) doReboot(sigCh chan<- os.Signal, waitTimeout time.Duration) err } // ask for shutdown and wait for it to happen. // if we exit, pebble will be restarted by systemd - if err := reboot(rebootDelay); err != nil { + if err := Shutdown.Reboot(rebootDelay, shutdownMsg); err != nil { return err } // wait for reboot to happen @@ -717,22 +742,6 @@ func (d *Daemon) doReboot(sigCh chan<- os.Signal, waitTimeout time.Duration) err return fmt.Errorf("expected reboot did not happen") } -var shutdownMsg = "reboot scheduled to update the system" - -func rebootImpl(rebootDelay time.Duration) error { - if rebootDelay < 0 { - rebootDelay = 0 - } - mins := int64(rebootDelay / time.Minute) - cmd := exec.Command("shutdown", "-r", fmt.Sprintf("+%d", mins), shutdownMsg) - if out, err := cmd.CombinedOutput(); err != nil { - return osutil.OutputErr(out, err) - } - return nil -} - -var reboot = rebootImpl - func (d *Daemon) Dying() <-chan struct{} { return d.tomb.Dying() } diff --git a/internals/daemon/daemon_test.go b/internals/daemon/daemon_test.go index c10d04d9..a717e1d8 100644 --- a/internals/daemon/daemon_test.go +++ b/internals/daemon/daemon_test.go @@ -56,23 +56,44 @@ type daemonSuite struct { authorized bool err error notified []string + delays []time.Duration restoreBackends func() } var _ = check.Suite(&daemonSuite{}) +type fakeNotify struct { + d *daemonSuite +} + +func (f fakeNotify) Available() bool { + return true +} + +func (f fakeNotify) Send(state string) error { + f.d.notified = append(f.d.notified, state) + return nil +} + +type fakeShutdown struct { + d *daemonSuite +} + +func (f fakeShutdown) Reboot(delay time.Duration, msg string) error { + f.d.delays = append(f.d.delays, delay) + return nil +} + func (s *daemonSuite) SetUpTest(c *check.C) { s.pebbleDir = c.MkDir() s.statePath = filepath.Join(s.pebbleDir, ".pebble.state") - systemdSdNotify = func(notif string) error { - s.notified = append(s.notified, notif) - return nil - } + Notify = &fakeNotify{d: s} } func (s *daemonSuite) TearDownTest(c *check.C) { - systemdSdNotify = systemd.SdNotify + Notify = systemd.Notifier s.notified = nil + s.delays = nil s.authorized = false s.err = nil } @@ -665,18 +686,14 @@ func (s *daemonSuite) TestRestartSystemWiring(c *check.C) { oldRebootNoticeWait := rebootNoticeWait oldRebootWaitTimeout := rebootWaitTimeout defer func() { - reboot = rebootImpl + Shutdown = systemd.Shutdown rebootNoticeWait = oldRebootNoticeWait rebootWaitTimeout = oldRebootWaitTimeout }() rebootWaitTimeout = 100 * time.Millisecond rebootNoticeWait = 150 * time.Millisecond - var delays []time.Duration - reboot = func(d time.Duration) error { - delays = append(delays, d) - return nil - } + Shutdown = &fakeShutdown{d: s} st.Lock() restart.Request(st, restart.RestartSystem) @@ -700,8 +717,8 @@ func (s *daemonSuite) TestRestartSystemWiring(c *check.C) { c.Check(rs, check.Equals, true) - c.Check(delays, check.HasLen, 1) - c.Check(delays[0], check.DeepEquals, rebootWaitTimeout) + c.Check(s.delays, check.HasLen, 1) + c.Check(s.delays[0], check.DeepEquals, rebootWaitTimeout) now := time.Now() @@ -709,8 +726,8 @@ func (s *daemonSuite) TestRestartSystemWiring(c *check.C) { c.Check(err, check.ErrorMatches, "expected reboot did not happen") - c.Check(delays, check.HasLen, 2) - c.Check(delays[1], check.DeepEquals, 1*time.Minute) + c.Check(s.delays, check.HasLen, 2) + c.Check(s.delays[1], check.DeepEquals, 1*time.Minute) // we are not stopping, we wait for the reboot instead c.Check(s.notified, check.DeepEquals, []string{"READY=1"}) @@ -724,32 +741,6 @@ func (s *daemonSuite) TestRestartSystemWiring(c *check.C) { c.Check(rebootAt.After(approxAt) || rebootAt.Equal(approxAt), check.Equals, true) } -func (s *daemonSuite) TestRebootHelper(c *check.C) { - cmd := testutil.FakeCommand(c, "shutdown", "", true) - defer cmd.Restore() - - tests := []struct { - delay time.Duration - delayArg string - }{ - {-1, "+0"}, - {0, "+0"}, - {time.Minute, "+1"}, - {10 * time.Minute, "+10"}, - {30 * time.Second, "+0"}, - } - - for _, t := range tests { - err := reboot(t.delay) - c.Assert(err, check.IsNil) - c.Check(cmd.Calls(), check.DeepEquals, [][]string{ - {"shutdown", "-r", t.delayArg, "reboot scheduled to update the system"}, - }) - - cmd.ForgetCalls() - } -} - func makeDaemonListeners(c *check.C, d *Daemon) { generalL, err := net.Listen("tcp", "127.0.0.1:0") c.Assert(err, check.IsNil) @@ -926,10 +917,6 @@ func (s *daemonSuite) TestRestartExpectedRebootGiveUp(c *check.C) { } func (s *daemonSuite) TestRestartIntoSocketModeNoNewChanges(c *check.C) { - notifySocket := filepath.Join(c.MkDir(), "notify.socket") - os.Setenv("NOTIFY_SOCKET", notifySocket) - defer os.Setenv("NOTIFY_SOCKET", "") - restore := standby.FakeStandbyWait(5 * time.Millisecond) defer restore() @@ -944,9 +931,6 @@ func (s *daemonSuite) TestRestartIntoSocketModeNoNewChanges(c *check.C) { time.Sleep(5 * time.Millisecond) } - c.Assert(d.standbyOpinions.CanStandby(), check.Equals, false) - f, _ := os.Create(notifySocket) - f.Close() c.Assert(d.standbyOpinions.CanStandby(), check.Equals, true) select { @@ -961,9 +945,6 @@ func (s *daemonSuite) TestRestartIntoSocketModeNoNewChanges(c *check.C) { } func (s *daemonSuite) TestRestartIntoSocketModePendingChanges(c *check.C) { - os.Setenv("NOTIFY_SOCKET", c.MkDir()) - defer os.Setenv("NOTIFY_SOCKET", "") - restore := standby.FakeStandbyWait(5 * time.Millisecond) defer restore() diff --git a/internals/systemd/sdnotify.go b/internals/systemd/notify.go similarity index 80% rename from internals/systemd/sdnotify.go rename to internals/systemd/notify.go index 5f1279cf..3ce67abf 100644 --- a/internals/systemd/sdnotify.go +++ b/internals/systemd/notify.go @@ -23,18 +23,24 @@ import ( "github.com/canonical/pebble/internals/osutil" ) -var osGetenv = os.Getenv +type notifySystemd struct{} -func SocketAvailable() bool { +var ( + osGetenv = os.Getenv + Notifier = ¬ifySystemd{} +) + +// Available determines if the systemd unit supports notifications. +func (n notifySystemd) Available() bool { notifySocket := osGetenv("NOTIFY_SOCKET") return notifySocket != "" && osutil.CanStat(notifySocket) } -// SdNotify sends the given state string notification to systemd. +// Send the given state string notification to systemd. // // inspired by libsystemd/sd-daemon/sd-daemon.c from the systemd source -func SdNotify(notifyState string) error { - if notifyState == "" { +func (n notifySystemd) Send(state string) error { + if state == "" { return fmt.Errorf("cannot use empty notify state") } @@ -56,6 +62,6 @@ func SdNotify(notifyState string) error { } defer conn.Close() - _, err = conn.Write([]byte(notifyState)) + _, err = conn.Write([]byte(state)) return err } diff --git a/internals/systemd/sdnotify_test.go b/internals/systemd/notify_test.go similarity index 86% rename from internals/systemd/sdnotify_test.go rename to internals/systemd/notify_test.go index ad19dabe..2926803e 100644 --- a/internals/systemd/sdnotify_test.go +++ b/internals/systemd/notify_test.go @@ -44,16 +44,16 @@ func (sd *sdNotifyTestSuite) TearDownTest(c *C) { func (sd *sdNotifyTestSuite) TestSocketAvailable(c *C) { socketPath := filepath.Join(c.MkDir(), "notify.socket") - c.Assert(systemd.SocketAvailable(), Equals, false) + c.Assert(systemd.Notifier.Available(), Equals, false) sd.env["NOTIFY_SOCKET"] = socketPath - c.Assert(systemd.SocketAvailable(), Equals, false) + c.Assert(systemd.Notifier.Available(), Equals, false) f, _ := os.Create(socketPath) f.Close() - c.Assert(systemd.SocketAvailable(), Equals, true) + c.Assert(systemd.Notifier.Available(), Equals, true) } func (sd *sdNotifyTestSuite) TestSdNotifyMissingNotifyState(c *C) { - c.Check(systemd.SdNotify(""), ErrorMatches, "cannot use empty notify state") + c.Check(systemd.Notifier.Send(""), ErrorMatches, "cannot use empty notify state") } func (sd *sdNotifyTestSuite) TestSdNotifyWrongNotifySocket(c *C) { @@ -65,7 +65,7 @@ func (sd *sdNotifyTestSuite) TestSdNotifyWrongNotifySocket(c *C) { {"xxx", `cannot use \$NOTIFY_SOCKET value: "xxx"`}, } { sd.env["NOTIFY_SOCKET"] = t.env - c.Check(systemd.SdNotify("something"), ErrorMatches, t.errStr) + c.Check(systemd.Notifier.Send("something"), ErrorMatches, t.errStr) } } @@ -91,7 +91,7 @@ func (sd *sdNotifyTestSuite) TestSdNotifyIntegration(c *C) { ch <- string(buf[:n]) }() - err = systemd.SdNotify("something") + err = systemd.Notifier.Send("something") c.Assert(err, IsNil) c.Check(<-ch, Equals, "something") } diff --git a/internals/systemd/shutdown.go b/internals/systemd/shutdown.go new file mode 100644 index 00000000..5552775e --- /dev/null +++ b/internals/systemd/shutdown.go @@ -0,0 +1,41 @@ +// Copyright (c) 2014-2020 Canonical Ltd +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 3 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package systemd + +import ( + "fmt" + "os/exec" + "time" + + "github.com/canonical/pebble/internals/osutil" +) + +type shutdown struct{} + +var Shutdown = &shutdown{} + +// Reboot the system after a specified duration of time, optionally +// displaying a wall message. +func (s shutdown) Reboot(delay time.Duration, msg string) error { + if delay < 0 { + delay = 0 + } + mins := int64(delay / time.Minute) + cmd := exec.Command("shutdown", "-r", fmt.Sprintf("+%d", mins), msg) + if out, err := cmd.CombinedOutput(); err != nil { + return osutil.OutputErr(out, err) + } + return nil +} diff --git a/internals/systemd/shutdown_test.go b/internals/systemd/shutdown_test.go new file mode 100644 index 00000000..11dd77ce --- /dev/null +++ b/internals/systemd/shutdown_test.go @@ -0,0 +1,68 @@ +// Copyright (c) 2014-2020 Canonical Ltd +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License version 3 as +// published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package systemd_test + +import ( + "time" + + . "gopkg.in/check.v1" + + "github.com/canonical/pebble/internals/reaper" + "github.com/canonical/pebble/internals/systemd" + "github.com/canonical/pebble/internals/testutil" +) + +type shutdownTestSuite struct{} + +var _ = Suite(&shutdownTestSuite{}) + +func (s *shutdownTestSuite) SetUpTest(c *C) { + // Needed for testutil.exec + reaper.Start() +} + +func (s *shutdownTestSuite) TearDownTest(c *C) { + reaper.Stop() +} + +// TestReboot checks that command construction match the +// expectation of the systemd shutdown command. +func (s *shutdownTestSuite) TestReboot(c *C) { + cmd := testutil.FakeCommand(c, "shutdown", "", true) + defer cmd.Restore() + + tests := []struct { + delay time.Duration + delayArg string + msg string + }{ + {0, "+0", ""}, + {0, "+0", "some msg"}, + {-1, "+0", "some msg"}, + {time.Minute, "+1", "some msg"}, + {10 * time.Minute, "+10", "some msg"}, + {30 * time.Second, "+0", "some msg"}, + } + + for _, t := range tests { + err := systemd.Shutdown.Reboot(t.delay, t.msg) + c.Assert(err, IsNil) + c.Check(cmd.Calls(), DeepEquals, [][]string{ + {"shutdown", "-r", t.delayArg, t.msg}, + }) + + cmd.ForgetCalls() + } +} diff --git a/internals/testutil/exec.go b/internals/testutil/exec.go index 113f7ceb..154127ca 100644 --- a/internals/testutil/exec.go +++ b/internals/testutil/exec.go @@ -92,9 +92,10 @@ type FakeCmd struct { // faking commands that need "\n" in their args (like zenity) // we use the following convention: // - generate \0 to separate args -// - generate \0\0 to separate commands +// - generate \0\f\n\r magic sequence to separate commands var scriptTpl = `#!/bin/bash printf "%%s" "$(basename "$0")" >> %[1]q + printf '\0' >> %[1]q for arg in "$@"; do @@ -102,7 +103,7 @@ for arg in "$@"; do printf '\0' >> %[1]q done -printf '\0' >> %[1]q +printf '\f\n\r' >> %[1]q %s ` @@ -176,12 +177,11 @@ func (cmd *FakeCmd) Calls() [][]string { if err != nil { panic(err) } - logContent := strings.TrimSuffix(string(raw), "\000") + logContent := strings.TrimSuffix(string(raw), "\000\f\n\r") allCalls := [][]string{} - calls := strings.Split(logContent, "\000\000") + calls := strings.Split(logContent, "\000\f\n\r") for _, call := range calls { - call = strings.TrimSuffix(call, "\000") allCalls = append(allCalls, strings.Split(call, "\000")) } return allCalls