Skip to content

Commit

Permalink
shutdown/notify: allow custom implementations
Browse files Browse the repository at this point in the history
The current shutdown and notify logic in pebble is designed with the
assumption that it will run under systemd, or at least that some
systemd components, such as shutdown, is installed.

Allow custom flavours of pebble to change the default assumptions.

- The daemon now defines an notification and shutdown interface, and
  configures the default implementation to use systemd.

- The shutdown logic (of which we currently only support reboot) is
  moved to the systemd package, as its based on systemd.

- Two globals daemon.Notify and daemon.Shutdown now provides a way
  to override the defaults.
  • Loading branch information
flotter committed Jun 21, 2023
1 parent 43a5daf commit 93859bb
Show file tree
Hide file tree
Showing 7 changed files with 195 additions and 91 deletions.
3 changes: 1 addition & 2 deletions internals/cli/cmd_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}
Expand Down
61 changes: 35 additions & 26 deletions internals/daemon/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import (
"net"
"net/http"
"os"
"os/exec"
"os/signal"
"runtime"
"strings"
Expand All @@ -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"
Expand All @@ -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.
Expand Down Expand Up @@ -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() {
Expand All @@ -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 {
Expand Down Expand Up @@ -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.
Expand All @@ -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)
}

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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()
}
Expand Down
83 changes: 32 additions & 51 deletions internals/daemon/daemon_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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)
Expand All @@ -700,17 +717,17 @@ 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()

err = d.Stop(nil)

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"})
Expand All @@ -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)
Expand Down Expand Up @@ -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()

Expand All @@ -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 {
Expand All @@ -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()

Expand Down
18 changes: 12 additions & 6 deletions internals/systemd/sdnotify.go → internals/systemd/notify.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 = &notifySystemd{}
)

// 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")
}

Expand All @@ -56,6 +62,6 @@ func SdNotify(notifyState string) error {
}
defer conn.Close()

_, err = conn.Write([]byte(notifyState))
_, err = conn.Write([]byte(state))
return err
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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)
}
}

Expand All @@ -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")
}
Expand Down
Loading

0 comments on commit 93859bb

Please sign in to comment.