diff options
Diffstat (limited to 'ca/rpki-nanny')
-rwxr-xr-x | ca/rpki-nanny | 217 |
1 files changed, 217 insertions, 0 deletions
diff --git a/ca/rpki-nanny b/ca/rpki-nanny new file mode 100755 index 00000000..914c8584 --- /dev/null +++ b/ca/rpki-nanny @@ -0,0 +1,217 @@ +#!/usr/bin/env python + +# $Id$ +# +# Copyright (C) 2014 Dragon Research Labs ("DRL") +# Portions copyright (C) 2009--2013 Internet Systems Consortium ("ISC") +# Portions copyright (C) 2007--2008 American Registry for Internet Numbers ("ARIN") +# +# Permission to use, copy, modify, and distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notices and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND DRL, ISC, AND ARIN DISCLAIM ALL +# WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL DRL, +# ISC, OR ARIN BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR +# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION +# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +""" +Start servers, using config file to figure out which servers the user +wants started. +""" + +import os +import pwd +import sys +import time +import signal +import logging +import argparse +import subprocess + +import rpki.log +import rpki.config +import rpki.autoconf +import rpki.daemonize + +from logging.handlers import SysLogHandler + +logger = logging.getLogger(__name__) + +signames = dict((getattr(signal, sig), sig) + for sig in dir(signal) + if sig.startswith("SIG") + and sig.isalnum() + and sig.isupper() + and isinstance(getattr(signal, sig), int)) + + +class Daemon(object): + """ + Representation and control of one daemon under our care. + """ + + def __init__(self, name): + self.name = name + self.proc = None + self.next_restart = 0 + if cfg.getboolean(option = "start_" + name, section = "myrpki", default = False): + self.cmd = (os.path.join(rpki.autoconf.libexecdir, name), "--foreground") + else: + self.cmd = () + + def start_maybe(self, output): + if self.cmd and self.proc is None and time.time() > self.next_restart: + try: + self.proc = subprocess.Popen(self.cmd, stdout = output, stderr = output) + self.next_restart = int(time.time() + args.restart_delay) + logger.debug("Started %s[%s]", self.name, self.proc.pid) + except: + logger.exception("Trouble starting %s", self.name) + + def terminate(self): + if self.proc is not None: + try: + logger.debug("Terminating daemon %s[%s]", self.name, self.proc.pid) + self.proc.terminate() + except: + logger.exception("Trouble terminating %s[%s]", self.name, self.proc.pid) + + def delay(self): + return max(0, int(self.next_restart - time.time())) if self.cmd and self.proc is None else 0 + + def reap(self): + if self.proc is not None and self.proc.poll() is not None: + code = self.proc.wait() + if code < 0: + logger.warn("%s[%s] exited on signal %s", + self.name, self.proc.pid, signames.get(-code, "???")) + else: + logger.warn("%s[%s] exited with status %s", + self.name, self.proc.pid, code) + self.proc = None + + +class Signals(object): + """ + + Convert POSIX signals into something we can use in a loop at main + program level. Assumes that we use signal.pause() to block, so + simply receiving the signal is enough to wake us up. + + Calling the constructed Signals object with one or more signal + numbers returns True if any of those signals have been received, + and clears the internal flag for the first such signal. + """ + + def __init__(self, *sigs): + self._active = set() + for sig in sigs: + signal.signal(sig, self._handler) + + def _handler(self, sig, frame): + self._active.add(sig) + #logger.debug("Received %s", signames.get(sig, "???")) + + def __call__(self, *sigs): + for sig in sigs: + try: + self._active.remove(sig) + return True + except KeyError: + pass + return False + + +def non_negative_integer(s): + if int(s) < 0: + raise ValueError + return s + +def positive_integer(s): + if int(s) <= 0: + raise ValueError + return s + + +if __name__ == "__main__": + + os.environ.update(TZ = "UTC") + time.tzset() + + cfg = rpki.config.argparser(section = "rpki-nanny", doc = __doc__) + + cfg.add_argument("--restart-delay", type = positive_integer, default = 60, + help = "how long to wait before restarting a crashed daemon") + cfg.add_argument("--pidfile", + default = os.path.join(rpki.daemonize.default_pid_directory, "rpki-nanny.pid"), + help = "override default location of pid file") + cfg.add_boolean_argument("--foreground", default = False, + help = "whether to stay in foreground rather than daemonizing") + cfg.add_boolean_argument("--capture-stdout-stderr", default = True, + help = "whether to capture output incorrectly sent to stdout/stderr") + cfg.add_logging_arguments() + + args = cfg.argparser.parse_args() + + # Drop privs before daemonizing or opening log file + pw = pwd.getpwnam(rpki.autoconf.RPKI_USER) + os.setgid(pw.pw_gid) + os.setuid(pw.pw_uid) + + cfg.configure_logging(ident = "rpki-nanny", args = args) + + if not args.foreground: + rpki.daemonize.daemon(pidfile = args.pidfile) + + if args.capture_stdout_stderr: + try: + logger_pipe = os.pipe() + logger_pid = os.fork() + if logger_pid == 0: + os.close(logger_pipe[1]) + with os.fdopen(logger_pipe[0]) as f: + for line in f: + logger.warn("Captured: %s", line.rstrip()) + # Should never get here, but just in case + logger.error("[Unexpected EOF in stdout/stderr capture logger]") + sys.exit(1) + else: + os.close(logger_pipe[0]) + except: + logger.exception("Trouble setting up stdout/stderr capture process") + sys.exit(1) + + daemon_output = logger_pipe[1] if args.capture_stdout_stderr else None + + signals = Signals(signal.SIGALRM, signal.SIGCHLD, signal.SIGTERM, signal.SIGINT) + daemons = [Daemon(name) for name in ("irdbd", "rpkid", "pubd", "rootd")] + exiting = False + + try: + while not exiting or not all(daemon.proc is None for daemon in daemons): + if not exiting and signals(signal.SIGTERM, signal.SIGINT): + logger.info("Received exit signal") + exiting = True + for daemon in daemons: + daemon.terminate() + if not exiting: + for daemon in daemons: + daemon.start_maybe(daemon_output) + alarms = tuple(daemon.delay() for daemon in daemons) + signal.alarm(min(a for a in alarms if a > 0) + 1 if any(alarms) else 0) + if not signals(signal.SIGCHLD, signal.SIGALRM): + signal.pause() + for daemon in daemons: + daemon.reap() + if args.capture_stdout_stderr: + os.kill(logger_pid, signal.SIGTERM) + except: + logger.exception("Unhandled exception in main loop") + for daemon in daemons: + daemon.terminate() + sys.exit(1) |