aboutsummaryrefslogtreecommitdiff
path: root/rpki/config.py
diff options
context:
space:
mode:
Diffstat (limited to 'rpki/config.py')
-rw-r--r--rpki/config.py266
1 files changed, 243 insertions, 23 deletions
diff --git a/rpki/config.py b/rpki/config.py
index 1aea0132..f1c43938 100644
--- a/rpki/config.py
+++ b/rpki/config.py
@@ -25,6 +25,10 @@ ConfigParser module.
import ConfigParser
import argparse
import logging
+import logging.handlers
+import traceback
+import time
+import sys
import os
import re
@@ -44,6 +48,7 @@ except ImportError:
rpki_conf_envname = "RPKI_CONF"
+
class parser(object):
"""
Extensions to stock Python ConfigParser:
@@ -90,6 +95,7 @@ class parser(object):
self.filename = filename or os.getenv(rpki_conf_envname) or default_filename
self.argparser = argparser
+ self.logging_defaults = None
try:
with open(self.filename, "r") as f:
@@ -132,7 +138,7 @@ class parser(object):
if self.cfg.has_option(section, option):
yield self.cfg.get(section, option)
option += "."
- matches = [o for o in self.cfg.options(section)
+ matches = [o for o in self.cfg.options(section)
if o.startswith(option) and o[len(option):].isdigit()]
matches.sort()
for option in matches:
@@ -201,20 +207,7 @@ class parser(object):
return long(self.get(option, default, section))
- def add_argument(self, *names, **kwargs):
- """
- Combined command line and config file argument. Takes
- arguments mostly like ArgumentParser.add_argument(), but also
- looks in config file for option of the same name.
-
- The "section" and "default" arguments are used for the config file
- lookup; the resulting value is used as the "default" parameter for
- the argument parser.
-
- If a "type" argument is specified, it applies to both the value
- parsed from the config file and the argument parser.
- """
-
+ def _get_argument_default(self, names, kwargs):
section = kwargs.pop("section", None)
default = kwargs.pop("default", None)
@@ -225,15 +218,38 @@ class parser(object):
else:
raise ValueError
- default = self.get(name, default = default, section = section)
+ if self.has_option(option = name, section = section):
+ default = self.get(option = name, section = section, default = default)
if "type" in kwargs:
default = kwargs["type"](default)
+ if "choices" in kwargs and default not in kwargs["choices"]:
+ raise ValueError
+
kwargs["default"] = default
+ return name, default, kwargs
+
+
+ def add_argument(self, *names, **kwargs):
+ """
+ Combined command line and config file argument. Takes
+ arguments mostly like ArgumentParser.add_argument(), but also
+ looks in config file for option of the same name.
+
+ The "section" and "default" arguments are used for the config file
+ lookup; the resulting value is used as the "default" parameter for
+ the argument parser.
+
+ If a "type" argument is specified, it applies to both the value
+ parsed from the config file and the argument parser.
+ """
+
+ name, default, kwargs = self._get_argument_default(names, kwargs)
return self.argparser.add_argument(*names, **kwargs)
+
def add_boolean_argument(self, name, **kwargs):
"""
Combined command line and config file boolean argument. Takes
@@ -241,7 +257,7 @@ class parser(object):
looks in config file for option of the same name.
The "section" and "default" arguments are used for the config file
- lookup; the resulting value is used as the default value for
+ lookup; the resulting value is used as the default value for
the argument parser.
Usage is a bit different from the normal ArgumentParser boolean
@@ -276,6 +292,146 @@ class parser(object):
self.argparser.set_defaults(**{ kwargs["dest"] : default })
+
+ def _add_logging_argument(self, *names, **kwargs):
+ group = kwargs.pop("group", self.argparser)
+ name, default, kwargs = self._get_argument_default(names, kwargs)
+ setattr(self.logging_defaults, name.replace("-", "_"), default)
+ if group is not None:
+ group.add_argument(*names, **kwargs)
+
+
+ def add_logging_arguments(self, section = None):
+ """
+ Set up standard logging-related arguments. This can be called
+ even when we're not going to parse the command line (eg,
+ because we're a WSGI app and therefore don't have a command
+ line), to handle whacking arguments from the config file into
+ the format that the logging setup code expects to see.
+ """
+
+ self.logging_defaults = argparse.Namespace()
+
+ class non_negative_integer(int):
+ def __init__(self, value):
+ if self < 0:
+ raise ValueError
+
+ class positive_integer(int):
+ def __init__(self, value):
+ if self <= 0:
+ raise ValueError
+
+ if self.argparser is None:
+ limit_group = None
+ else:
+ limit_group = self.argparser.add_mutually_exclusive_group()
+
+ self._add_logging_argument(
+ "--log-level",
+ default = "warning",
+ choices = ("debug", "info", "warning", "error", "critical"),
+ help = "how verbosely to log")
+
+ self._add_logging_argument(
+ "--log-destination",
+ default = "stderr",
+ choices = ("syslog", "stdout", "stderr", "file"),
+ help = "logging mechanism to use")
+
+ self._add_logging_argument(
+ "--log-filename",
+ help = "where to log when log destination is \"file\"")
+
+ self._add_logging_argument(
+ "--log-facility",
+ default = "daemon",
+ choices = sorted(logging.handlers.SysLogHandler.facility_names.keys()),
+ help = "syslog facility to use when log destination is \"syslog\"")
+
+ self._add_logging_argument(
+ "--log-count",
+ default = "7",
+ type = positive_integer,
+ help = "how many logs to keep when rotating for log destination \"file\""),
+
+ self._add_logging_argument(
+ "--log-size-limit",
+ group = limit_group,
+ default = 0,
+ type = non_negative_integer,
+ help = "size in kbytes after which to rotate log for destination \"file\"")
+
+ self._add_logging_argument(
+ "--log-time-limit",
+ group = limit_group,
+ default = 0,
+ type = non_negative_integer,
+ help = "hours after which to rotate log for destination \"file\"")
+
+
+ def configure_logging(self, args = None, ident = None):
+ """
+ Configure the logging system, using information from both the
+ config file and the command line; if this particular program
+ doesn't use the command line (eg, a WSGI app), we just use the
+ config file.
+ """
+
+ if self.logging_defaults is None:
+ self.add_logging_arguments()
+
+ if args is None:
+ args = self.logging_defaults
+
+ log_level = getattr(logging, args.log_level.upper())
+
+ if args.log_destination == "stderr":
+ log_handler = logging.StreamHandler(
+ stream = sys.stderr)
+
+ elif args.log_destination == "stdout":
+ log_handler = logging.StreamHandler(
+ stream = sys.stdout)
+
+ elif args.log_destination == "syslog":
+ log_handler = logging.handlers.SysLogHandler(
+ address = ("/dev/log" if os.path.exists("/dev/log")
+ else ("localhost", logging.handlers.SYSLOG_UDP_PORT)),
+ facility = logging.handlers.SysLogHandler.facility_names[args.log_facility])
+
+ elif args.log_destination == "file" and (args.log_size_limit == 0 and
+ args.log_time_limit == 0):
+ log_handler = logging.handlers.WatchedFileHandler(
+ filename = args.log_filename)
+
+ elif args.log_destination == "file" and args.log_time_limit == 0:
+ log_handler = logging.handlers.RotatingFileHandler(
+ filename = args.log_filename,
+ maxBytes = args.log_size_limit * 1024,
+ backupCount = args.log_count)
+
+ elif args.log_destination == "file" and args.log_size_limit == 0:
+ log_handler = logging.handlers.TimedRotatingFileHandler(
+ filename = args.log_filename,
+ interval = args.log_time_limit,
+ backupCount = args.log_count,
+ when = "H",
+ utc = True)
+
+ else:
+ raise ValueError
+
+ if ident is None:
+ ident = os.path.basename(sys.argv[0])
+
+ log_handler.setFormatter(Formatter(ident, log_handler, log_level))
+
+ root_logger = logging.getLogger()
+ root_logger.addHandler(log_handler)
+ root_logger.setLevel(log_level)
+
+
def set_global_flags(self):
"""
Consolidated control for all the little global control flags
@@ -287,7 +443,6 @@ class parser(object):
# pylint: disable=W0621
import rpki.x509
- import rpki.log
import rpki.daemonize
for line in self.multiget("configure_logger"):
@@ -329,11 +484,6 @@ class parser(object):
pass
try:
- rpki.log.enable_tracebacks = self.getboolean("enable_tracebacks")
- except ConfigParser.NoOptionError:
- pass
-
- try:
rpki.daemonize.default_pid_directory = self.get("pid_directory")
except ConfigParser.NoOptionError:
pass
@@ -412,3 +562,73 @@ def argparser(section = None, doc = None, cfg_optional = False):
allow_missing = cfg_optional or args.help)
return cfg
+
+
+class Formatter(object):
+ """
+ Reimplementation (easier than subclassing in this case) of
+ logging.Formatter.
+
+ It turns out that the logging code only cares about this class's
+ .format(record) method, everything else is internal; so long as
+ .format() converts a record into a properly formatted string, the
+ logging code is happy.
+
+ So, rather than mess around with dynamically constructing and
+ deconstructing and tweaking format strings and ten zillion options
+ we don't use, we just provide our own implementation that supports
+ what we do need.
+ """
+
+ converter = time.gmtime
+
+ def __init__(self, ident, handler, level):
+ self.ident = ident
+ self.is_syslog = isinstance(handler, logging.handlers.SysLogHandler)
+ self.debugging = level == logging.DEBUG
+
+ def format(self, record):
+ return "".join(self.coformat(record)).rstrip("\n")
+
+ def coformat(self, record):
+
+ try:
+ if not self.is_syslog:
+ yield time.strftime("%Y-%m-%d %H:%M:%S ", time.gmtime(record.created))
+ except:
+ yield "[$!$Time format failed]"
+
+ try:
+ yield "{}[{:d}]: ".format(self.ident, record.process)
+ except:
+ yield "[$!$ident format failed]"
+
+ try:
+ if isinstance(record.context, (str, unicode)):
+ yield record.context + " "
+ else:
+ yield repr(record.context) + " "
+ except AttributeError:
+ pass
+ except:
+ yield "[$!$context format failed]"
+
+ try:
+ yield record.getMessage()
+ except:
+ yield "[$!$record.getMessage() failed]"
+
+ try:
+ if record.exc_info:
+ if self.is_syslog or not self.debugging:
+ lines = traceback.format_exception_only(
+ record.exc_info[0], record.exc_info[1])
+ lines.insert(0, ": ")
+ else:
+ lines = traceback.format_exception(
+ record.exc_info[0], record.exc_info[1], record.exc_info[2])
+ lines.insert(0, "\n")
+ for line in lines:
+ yield line
+ except:
+ yield "[$!$exception formatting failed]"