-
Notifications
You must be signed in to change notification settings - Fork 55
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[RFC] shutdown/notify: allow for custom implementations #243
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because external packages need to implement these interfaces (and might want to pass around the type), shouldn't they be exported/capitalised? |
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It seems really unfortunate that these need to be globals when the One approach would be to add Notify and Shutdown to cmd.Personality.ConfigureDaemon = func(opts *daemon.Options) {
opts.Notify = mySpecialNotify
opts.Shutdown = mySpecialShutdown
} I dunno, maybe it's not any better, but at least it would confine the messy globals to one place, the "personality" struct. Or we could put the globals in the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like the idea of confinement but is this really a part of the Personality, I can see this behaviour growing with other options, so a new struct to embody them? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think this is part of Personality, because (i) it does not concern other components apart from the daemon, and (ii) Termus will have its own initialization of the daemon during bootstrap. I'm on board @benhoyt's idea of having those be inside |
||
) | ||
|
||
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" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This has been moved to |
||
|
||
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() | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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,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"}) | ||
|
@@ -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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was moved to |
||
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", "") | ||
|
||
Comment on lines
-929
to
-932
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The folllowing blocks of removals are deliberate, as they add very little value here apart for re-testing the |
||
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() | ||
Comment on lines
-947
to
-949
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See comment above. |
||
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", "") | ||
|
||
Comment on lines
-964
to
-966
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See comment above. |
||
restore := standby.FakeStandbyWait(5 * time.Millisecond) | ||
defer restore() | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not really sure what
WATCHDOG=1
does, but it seems like it's pretty specific to systemd? In that case, could it not be a string in theSend
interface, but just included in the systemd implementation? Or something likeSend(daemon.SendWatchdog)
andSend(daemon.SendReady)
(not the best API, but spitballing here).