diff options
Diffstat (limited to 'ca/rpki-nanny')
-rwxr-xr-x | ca/rpki-nanny | 258 |
1 files changed, 258 insertions, 0 deletions
diff --git a/ca/rpki-nanny b/ca/rpki-nanny new file mode 100755 index 00000000..a78c10a6 --- /dev/null +++ b/ca/rpki-nanny @@ -0,0 +1,258 @@ +#!/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)) + +# TODO: +# +# * Logging configuration is a mess. Daemons should be handling this +# for themselves, from rpki.conf, and there should be a way to configure +# logging for rpki-nanny itself. +# +# * Perhaps we should re-read the config file so we can turn individual +# daemons on and off? Or is that unnecessary complexity? +# +# * rpki-nanny should probably daemonize itself before forking. + + +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("start_" + name, False): + log_file = os.path.join(args.log_directory, name + ".log") + self.cmd = (os.path.join(rpki.autoconf.libexecdir, name), + "--foreground", + "--log-level", args.log_level) + if args.log_file: + self.cmd += ("--log-file", log_file) + elif args.log_rotating_file_kbytes: + self.cmd += ("--log-rotating-file", log_file, + args.log_rotating_file_kbytes, args.log_backup_count) + elif args.log_rotating_file_hours: + self.cmd += ("--log-timed-rotating-file", log_file, + args.log_rotating_file_hours, args.log_backup_count) + else: + self.cmd += ("--log-syslog", args.log_syslog) + else: + self.cmd = () + + def start_maybe(self): + if self.cmd and self.proc is None and time.time() > self.next_restart: + try: + self.proc = subprocess.Popen(self.cmd) + 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 = "myrpki", 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("--daemonize", default = True, + help = "whether to daemonize") + + # This stuff is a mess. Daemons should control their own logging + # via rpki.conf settings, but we haven't written that yet, and + # this script is meant to be a replacement for rpki-start-servers, + # so leave the mess in place for the moment and clean up later. + + cfg.argparser.add_argument("--log-directory", default = ".", + help = "where to write write log files when not using syslog") + cfg.argparser.add_argument("--log-backup-count", default = "7", type = non_negative_integer, + help = "keep this many old log files when rotating") + cfg.argparser.add_argument("--log-level", default = "warning", + choices = ("debug", "info", "warning", "error", "critical"), + help = "how verbosely to log") + group = cfg.argparser.add_mutually_exclusive_group() + group.add_argument("--log-file", action = "store_true", + help = "log to files, reopening if rotated away") + group.add_argument("--log-rotating-file-kbytes",type = non_negative_integer, + help = "log to files, rotating after this many kbytes") + group.add_argument("--log-rotating-file-hours", type = non_negative_integer, + help = "log to files, rotating after this many hours") + group.add_argument("--log-syslog", default = "daemon", nargs = "?", + choices = sorted(SysLogHandler.facility_names.keys()), + help = "log syslog") + + 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) + + # Log control mess here is continuation of log control mess above: + # all the good names are taken by the pass-through kludge, we'd + # have to reimplement all the common logic to use it ourselves + # too. Just wire to stderr or rotating log file for now, using + # same log file scheme as set in /etc/defaults/ on Debian/Ubuntu + # and the log level wired to DEBUG, fix the whole logging mess later. + + if args.daemonize: + log_handler = lambda: logging.handlers.TimedRotatingFileHandler( + filename = os.path.join(args.log_directory, "rpki-nanny.log"), + interval = 3, + backupCount = 56, + when = "H", + utc = True) + else: + log_handler = logging.StreamHandler + + rpki.log.init(ident = "rpki-nanny", + args = argparse.Namespace(log_level = logging.DEBUG, + log_handler = log_handler)) + if args.daemonize: + rpki.daemonize.daemon(pidfile = args.pidfile) + + 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() + 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() + except: + logger.exception("Unhandled exception in main loop") + for daemon in daemons: + daemon.terminate() + sys.exit(1) |