aboutsummaryrefslogtreecommitdiff
path: root/ca/rpki-nanny
diff options
context:
space:
mode:
Diffstat (limited to 'ca/rpki-nanny')
-rwxr-xr-xca/rpki-nanny258
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)