]> git.llucax.com Git - software/pymin.git/commitdiff
Add configuration and command-line option framework.
authorLeandro Lucarella <llucax@gmail.com>
Mon, 16 Jun 2008 23:39:38 +0000 (20:39 -0300)
committerLeandro Lucarella <llucax@gmail.com>
Mon, 16 Jun 2008 23:39:38 +0000 (20:39 -0300)
The new pymin.config module integrates both ConfigParser and optparse
Python modules, adding variables validation throw formencode library and
hooks for plug-ins to add options in run-time.

This closes #3, closes #4 and closes #6.

16 files changed:
config.py [deleted file]
doc/config-examples/pymin.debian.ini [new file with mode: 0644]
doc/config-examples/pymin.devel.ini [new file with mode: 0644]
doc/config-examples/pymin.suse.ini [new file with mode: 0644]
pymin/config.py [new file with mode: 0644]
pymind
services/dhcp/__init__.py
services/dns/__init__.py
services/firewall/__init__.py
services/ip/__init__.py
services/nat/__init__.py
services/ppp/__init__.py
services/proxy/__init__.py
services/qos/__init__.py
services/vpn/__init__.py
services/vrrp/__init__.py

diff --git a/config.py b/config.py
deleted file mode 100644 (file)
index a2e36d7..0000000
--- a/config.py
+++ /dev/null
@@ -1,72 +0,0 @@
-# vim: set et sts=4 sw=4 encoding=utf-8 :
-
-from pymin.dispatcher import Handler
-from os.path import join
-
-base_path = join('var', 'lib', 'pymin')
-pickle_path = join(base_path, 'pickle')
-# FIXME, this should be specific for each service
-config_path = join(base_path, 'config')
-
-class firewall:
-    pickle_dir = join(pickle_path, 'firewall')
-    config_dir = join(config_path, 'firewall')
-
-class nat:
-    pickle_dir = join(pickle_path, 'nat')
-
-class ppp:
-    pickle_dir = join(pickle_path, 'ppp')
-    config_dir = {
-        'pap-secrets':  join(config_path, 'ppp'),
-        'chap-secrets': join(config_path, 'ppp'),
-        'options.X':    join(config_path, 'ppp'),
-        'nameX':        join(config_path, 'ppp', 'peers'),
-    }
-
-class vpn:
-     pickle_dir = join(pickle_path, 'vpn')
-     config_dir = join(config_path, 'vpn')
-
-class ip:
-    pickle_dir = join(pickle_path, 'ip')
-    config_dir = join(config_path, 'ip')
-
-class dns:
-    pickle_dir = join(pickle_path, 'dns')
-    config_dir = {
-        'named.conf': join(config_path, 'dns'),
-        'zoneX.zone': join(config_path, 'dns', 'zones'),
-    }
-
-class dhcp:
-    pickle_dir = join(pickle_path, 'dhcp')
-    config_dir = join(config_path, 'dhcp')
-
-class proxy:
-    pickle_dir = join(pickle_path, 'proxy')
-    config_dir = join(config_path, 'proxy')
-
-class vrrp:
-    pickle_dir = join(pickle_path, 'vrrp')
-    config_dir = join(config_path, 'vrrp')
-    pid_dir    = join(config_path, 'vrrp', 'run')
-
-class vpn:
-    pickle_dir = join(pickle_path, 'vpn')
-    config_dir = join(config_path, 'vpn')
-
-class qos:
-    pickle_dir = join(pickle_path, 'qos')
-    config_dir = join(config_path, 'qos')
-
-bind_addr = \
-(
-    '',   # Bind IP ('' is ANY)
-    9999, # Port
-)
-
-services = 'firewall nat ppp vpn ip dns dhcp proxy vrrp qos'.split()
-
-services_dirs = ['services']
-
diff --git a/doc/config-examples/pymin.debian.ini b/doc/config-examples/pymin.debian.ini
new file mode 100644 (file)
index 0000000..9cb4b44
--- /dev/null
@@ -0,0 +1,66 @@
+
+; all configuration variables in the DEFAULT section are inherited by all
+; other sections, so it's a good place to add common variables and default
+; values
+[DEFAULT]
+; this variables are used by pymin's services, and are reasonable defaults
+pickle-dir = %(pymind-pickle-dir)s/%(__name__)s
+config-dir = %(pymind-config-dir)s/%(__name__)s
+
+; pymind daemon configuration
+[pymind]
+; IP and port where pymind should listen for commands
+bind-addr = 127.0.0.1
+bind-port = 9999
+; services plug-ins to use
+services = dhcp qos firewall nat ppp vpn ip dns proxy vrrp
+
+
+; SERVICES CONFIGURATION
+; ----------------------
+;
+; all following sections are for services configuration, a section per service
+
+[firewall]
+; use the pickle-dir value provided by DEFAULT section
+; there are no config files really, only temporary scripts
+; (that shouldn't exist)
+config-dir = /tmp
+
+[nat]
+; use the pickle-dir value provided by DEFAULT section
+
+[ppp]
+; use the pickle-dir value provided by DEFAULT section
+config-options-dir = /etc/ppp
+config-pap-dir     = /etc/ppp
+config-chap-dir    = /etc/ppp
+config-peers-dir   = /etc/ppp/peers
+
+[ip]
+; use the pickle-dir and config-dir values provided by DEFAULT section
+
+[dns]
+config-named-dir = /etc/bind9
+config-zones-dir = /var/lib/bind9
+
+[dhcp]
+; use the pickle-dir value provided by DEFAULT section
+config-dir = /etc/dhcp3
+
+[proxy]
+; use the pickle-dir value provided by DEFAULT section
+config-dir = /etc/squid
+
+[vrrp]
+; use the pickle-dir and config-dir values provided by DEFAULT section
+pid-dir = /var/run
+
+[vpn]
+; use the pickle-dir value provided by DEFAULT section
+config-dir = /etc/tinc
+
+[qos]
+; use the pickle-dir value provided by DEFAULT section
+
+; vim: set filetype=dosini :
diff --git a/doc/config-examples/pymin.devel.ini b/doc/config-examples/pymin.devel.ini
new file mode 100644 (file)
index 0000000..4f34dd5
--- /dev/null
@@ -0,0 +1,72 @@
+
+; all configuration variables in the DEFAULT section are inherited by all
+; other sections, so it's a good place to add common variables and default
+; values
+[DEFAULT]
+; this are helper variables, not used by pymin (used for interpolation in
+; other variables)
+pymind-data-dir = var/lib/pymin
+pymind-config-dir = %(pymind-data-dir)s/config
+pymind-pickle-dir = %(pymind-data-dir)s/pickle
+; this variables are used by pymin's services, and are reasonable defaults
+pickle-dir = %(pymind-pickle-dir)s/%(__name__)s
+config-dir = %(pymind-config-dir)s/%(__name__)s
+
+; pymind daemon configuration
+[pymind]
+; IP and port where pymind should listen for commands
+bind-addr = 0.0.0.0
+bind-port = 9999
+; services plug-ins to use
+services = dhcp qos firewall nat ppp vpn ip dns proxy vrrp
+; directories where to find those plug-ins
+services-dirs = services
+
+
+
+; SERVICES CONFIGURATION
+; ----------------------
+;
+; all following sections are for services configuration, a section per service
+
+[firewall]
+; use the pickle-dir and config-dir values provided by DEFAULT section
+
+[nat]
+; use the pickle-dir value provided by DEFAULT section
+
+[ppp]
+; use the pickle-dir value provided by DEFAULT section
+; helper variable, not used by pymin
+config-base-dir    = %(config-dir)s/%(__name__)s
+; variables used by pymin
+config-options-dir = %(config-base-dir)s
+config-pap-dir     = %(config-base-dir)s
+config-chap-dir    = %(config-base-dir)s
+config-peers-dir   = %(config-base-dir)s/peers
+
+[ip]
+; use the pickle-dir and config-dir values provided by DEFAULT section
+
+[dns]
+; use the pickle-dir value provided by DEFAULT section
+config-named-dir = %(config-dir)s
+config-zones-dir = %(config-dir)s/zones
+
+[dhcp]
+; use the pickle-dir and config-dir values provided by DEFAULT section
+
+[proxy]
+; use the pickle-dir and config-dir values provided by DEFAULT section
+
+[vrrp]
+; use the pickle-dir and config-dir values provided by DEFAULT section
+pid-dir = /tmp
+
+[vpn]
+; use the pickle-dir and config-dir values provided by DEFAULT section
+
+[qos]
+; use the pickle-dir and config-dir values provided by DEFAULT section
+
+; vim: set filetype=dosini :
diff --git a/doc/config-examples/pymin.suse.ini b/doc/config-examples/pymin.suse.ini
new file mode 100644 (file)
index 0000000..fc39078
--- /dev/null
@@ -0,0 +1,66 @@
+
+; all configuration variables in the DEFAULT section are inherited by all
+; other sections, so it's a good place to add common variables and default
+; values
+[DEFAULT]
+; this variables are used by pymin's services, and are reasonable defaults
+pickle-dir = %(pymind-pickle-dir)s/%(__name__)s
+config-dir = %(pymind-config-dir)s/%(__name__)s
+
+; pymind daemon configuration
+[pymind]
+; IP and port where pymind should listen for commands
+bind-addr = 127.0.0.1
+bind-port = 9999
+; services plug-ins to use
+services = dhcp qos firewall nat ppp vpn ip dns proxy vrrp
+
+
+; SERVICES CONFIGURATION
+; ----------------------
+;
+; all following sections are for services configuration, a section per service
+
+[firewall]
+; use the pickle-dir value provided by DEFAULT section
+; there are no config files really, only temporary scripts
+; (that shouldn't exist)
+config-dir = /tmp
+
+[nat]
+; use the pickle-dir value provided by DEFAULT section
+
+[ppp]
+; use the pickle-dir value provided by DEFAULT section
+config-options-dir = /etc/ppp
+config-pap-dir     = /etc/ppp
+config-chap-dir    = /etc/ppp
+config-peers-dir   = /etc/ppp/peers
+
+[ip]
+; use the pickle-dir and config-dir values provided by DEFAULT section
+
+[dns]
+config-named-dir = /etc
+config-zones-dir = /var/lib/named
+
+[dhcp]
+; use the pickle-dir value provided by DEFAULT section
+config-dir = /etc
+
+[proxy]
+; use the pickle-dir value provided by DEFAULT section
+config-dir = /etc/squid
+
+[vrrp]
+; use the pickle-dir and config-dir values provided by DEFAULT section
+pid-dir = /var/run
+
+[vpn]
+; use the pickle-dir value provided by DEFAULT section
+config-dir = /etc/tinc
+
+[qos]
+; use the pickle-dir value provided by DEFAULT section
+
+; vim: set filetype=dosini :
diff --git a/pymin/config.py b/pymin/config.py
new file mode 100644 (file)
index 0000000..ea9a039
--- /dev/null
@@ -0,0 +1,696 @@
+# vim: set encoding=utf-8 et sw=4 sts=4 :
+
+# TODO
+#
+# * add 'append' mode to ListOption (like ConfigOption).
+#
+# * update/write documentation
+
+import os
+import re
+import sys
+import shlex
+import optparse
+from formencode import Schema, ForEach, Invalid, validators
+from ConfigParser import SafeConfigParser, InterpolationMissingOptionError, \
+                         InterpolationSyntaxError, InterpolationDepthError, \
+                         MissingSectionHeaderError, ParsingError
+import logging ; log = logging.getLogger('pymin.config')
+
+
+__all__ = ('ConfigError', 'MissingSectionHeaderError', 'ParsingError',
+           'Options', 'OptionGroup', 'Option',
+           'VOID', 'load_config', 'config', 'options')
+
+
+# regular expression to check if a name is a valid python identifier
+identifier_re = re.compile(r'^[a-zA-Z_]\w*$')
+
+
+class ConfigError(RuntimeError):
+    """
+    Raised when the problem is due to an user error.
+    """
+    pass
+
+
+class VoidClass:
+    def __repr__(self):
+        return "<void>"
+    def __len__(self):
+        "For boolean expression evaluation"
+        return 0
+VOID = VoidClass()
+
+
+class Options:
+    """
+    Represent a set of options a program can have.
+
+    Both command-line and configuration file options are handled.
+
+    .. attr:: default_group
+
+        The name of the default :class:`OptionGroup`. Options that
+        don't belong to any group are looked in the section with this
+        name in the config file.
+
+    .. attr:: default_group
+
+        Default :class:`OptionGroup` description.
+
+    .. attr:: options
+
+        A list of :class:`Option` or :class:`OptionGroup`
+        instances. Groups are translated to a section when parsing the
+        config file, and to prefixes in the long options when parsing
+        the command-line.
+    """
+
+    def __init__(self, schemacls=None):
+        self.default_group = ''
+        self.default_group_desc = ''
+        self.options = []
+        if schemacls is None:
+            schemacls = Schema
+        self.schemacls = schemacls
+        self.schema = None # filled in process()
+
+    def add(self, options):
+        log.debug('Options.add(%r)', options)
+        if isinstance(options, (list, tuple)):
+            self.options.extend(options)
+        else:
+            self.options.append(options)
+
+    def add_group(self, *args, **kwargs):
+        g = OptionGroup(*args, **kwargs)
+        log.debug('Options.add_group(%r)', g)
+        self.add(g)
+
+    def get_group(self, group_name):
+        for g in self.options:
+            if isinstance(g, OptionGroup) and g.name == group_name:
+                return g
+
+    def init(self, default_group=None, default_group_desc=None,
+             options=None):
+        """
+        Initialize the class. Since the class is instantiated by the
+        config framework, you should use this method to set it's values.
+
+        Arguments are directly mapped to the class attributes, with
+        the particularity of options being appended to existing options
+        (instead of replaced).
+        """
+        log.debug('Options.init(default_group=%r, default_group_desc=%r, '
+                  'options=%r)', default_group, default_group_desc, options)
+        if not identifier_re.match(default_group):
+            raise ValueError("Invalid default group name '%s' (group names "
+                             "must be valid python identifiers" % name)
+        if default_group is not None:
+            self.default_group = default_group
+        if default_group_desc is not None:
+            self.default_group_desc = default_group_desc
+        if options is not None:
+            self.options.extend(options)
+
+    def process(self, parser):
+        """
+        Process the command-line options.
+
+        ``parser`` should be a :class:`optparse.OptionParser` instance.
+        """
+        log.debug('Options.process()')
+        self.schema = self.schemacls()
+        config_opts = []
+        for o in self.options:
+            o.process(parser, self.schema, config_opts, self.default_group)
+        return config_opts
+
+    def values(self, confparser, parseropts, ignore_errors):
+        """
+        Post process the command-line options.
+
+        ``config`` should be an object where options are stored. If
+        ``prefix`` is specified, the prefix will be used to look for
+        the option.
+        """
+        log.debug('Options.values()')
+        # first populate the confparser with the values from the command-line
+        # options to make the variable interpolation works as expected.
+        for o in self.options:
+            o.set_value(confparser, parseropts, self.default_group)
+        # then, get the actual values
+        v = dict()
+        for o in self.options:
+            v[o.name] = o.value(confparser, parseropts, self.default_group,
+                                ignore_errors)
+        log.debug('Options.values() -> %r', v)
+        return v
+
+    @property
+    def defaults(self):
+        log.debug('Options.defaults()')
+        defaults = dict()
+        for o in self.options:
+            defaults.update(o.defaults)
+        log.debug('Options.defaults() -> %r', defaults)
+        return defaults
+
+
+class OptionGroup:
+
+    def __init__(self, name, description=None, options=None):
+        log.debug('OptionGroup(name=%r, description=%r, options=%r)', name,
+                  description, options)
+        if not identifier_re.match(name):
+            raise ValueError("Invalid group name '%s' (group names must be "
+                             "valid python identifiers" % name)
+        self.name = name
+        self.oname = name.replace('_', '-')
+        self.description = description
+        self.options = []
+        if options is not None:
+            self.add(options)
+
+    def add(self, options):
+        log.debug('OptionGroup.add(%r)', options)
+        if not isinstance(options, (list, tuple)):
+            options = [options]
+        for o in options:
+            if isinstance(o, OptionGroup):
+                raise ConfigError("Groups can't be nested (group '%s' found "
+                                  "inside group '%s')" % (o.name, self.name))
+        self.options.extend(options)
+
+    def process(self, parser, schema, config_opts, default_group):
+        """
+        Process the command-line options.
+
+        ``parser`` should be a :class:`optparse.OptionParser` instance.
+        """
+        subschema = schema.__class__()
+        group = optparse.OptionGroup(parser, self.description)
+        for o in self.options:
+            if o.short:
+                raise ValueError("Grouped options can't have short name "
+                                  "(option '%s' in grupo '%s' have this short "
+                                  "names: %s)" % (o.name, self.name,
+                                            ','.join('-' + o for o in o.short)))
+            o.process(group, subschema, config_opts, self)
+        log.debug('Adding group %s to optparse')
+        parser.add_option_group(group)
+        schema.add_field(self.name, subschema)
+
+    def value(self, confparser, parseropts, default_group, ignore_errors):
+        """
+        """
+        v = dict()
+        for o in self.options:
+            v[o.name] = o.value(confparser, parseropts, self, ignore_errors)
+        return v
+
+    def set_value(self, confparser, parseropts, default_group):
+        """
+        """
+        v = dict()
+        for o in self.options:
+            o.set_value(confparser, parseropts, self)
+        return v
+
+    @property
+    def defaults(self):
+        defaults = dict()
+        for o in self.options:
+            defaults.update(o.defaults)
+        defaults = { self.oname: defaults }
+        return defaults
+
+    def __repr__(self):
+        return 'OptionGroup<%s>%r' % (self.name, self.options)
+
+
+class Option:
+    """
+    A program's option.
+
+    .. attr:: name
+
+        The name of the option. The config object will have an attribute
+        with this name. When parsing configuration and command-line
+        options, the ``_`` in this name are replace with ``-``.
+
+    .. attr:: validator
+
+        See :mod:`formencode.validators` for available validators.
+
+    .. attr:: short
+
+        Short aliases (only for command-line options).
+
+    .. attr:: long
+
+        Long aliases.
+
+    .. attr:: default
+
+        Default value.
+
+    .. attr:: help
+
+        Help message for the option.
+
+    .. attr:: metavar
+
+        Name of the option argument used in the help message.
+
+    """
+
+    def __init__(self, name, validator=None, short=(), long=(), default=VOID,
+                 help=VOID, metavar=VOID):
+        log.debug('Option(name=%r, validator=%r, short=%r, long=%r, '
+                  'default=%r, help=%r, metavar=%r)', name, validator, short,
+                  long, default, help, metavar)
+        if not identifier_re.match(name):
+            raise ValueError("Invalid option name '%s' (option names must be "
+                             "valid python identifiers" % name)
+        self.name = name
+        self.oname = name.replace('_', '-')
+        self.validator = validator
+        if isinstance(short, basestring):
+            short = [short]
+        self.short = list(short)
+        if isinstance(long, basestring):
+            long = [long]
+        self.long = [self.oname] + list(long)
+        self.default = default
+        self.help = help
+        self.metavar = metavar
+
+    def process(self, parser, schema, config_opts, group):
+        """
+        Process the command-line options.
+
+        ``parser`` should be a :class:`optparse.OptionParser` instance. If
+        ``prefix`` is specified, all long command-line options are
+        prefixed with that. For example: ``some_option`` will become
+        ``--prefix-some-option``.
+        """
+        if isinstance(group, basestring):
+            group = None
+        parser.add_option(type='string', dest=self.dest(group), default=VOID,
+                          metavar=self.metavar, help=self.help,
+                          *self.optparser_args(group))
+        log.debug('Option<%s>.process() -> add_option(%s, type=%r, dest=%r, '
+                  'default=%r, metavar=%r, help=%r)', self.name,
+                  ', '.join(repr(i) for i in self.optparser_args(group)),
+                  'string', self.dest(group), VOID, self.metavar, self.help)
+        schema.add_field(self.name, self.validator)
+
+    def value(self, confparser, parseropts, group, ignore_errors):
+        """
+        Post process the command-line options.
+
+        ``config`` should be an object where options are stored. If
+        ``prefix`` is specified, the prefix will be used to look for
+        the option.
+        """
+        section = group
+        if isinstance(group, OptionGroup):
+            section = group.oname
+        if (confparser.has_section(section)
+                    and confparser.has_option(section, self.oname)):
+            return confparser.get(section, self.oname)
+        if self.default is VOID:
+            if ignore_errors:
+                return None
+            raise ConfigError('mandatory option "%s" not present' % self.name)
+        return self.default
+
+    def set_value(self, confparser, parseropts, group):
+        val = getattr(parseropts, self.dest(group))
+        if val is not VOID:
+            section = group
+            if isinstance(group, OptionGroup):
+                section = group.oname
+            if not confparser.has_section(section):
+                confparser.add_section(section)
+            confparser.set(section, self.oname, val)
+
+    def optparser_args(self, group=None):
+        args = []
+        args.extend('-' + s for s in self.short)
+        prefix = ''
+        if group:
+            prefix = group.oname + '-'
+        args.extend('--' + prefix + l for l in self.long)
+        return args
+
+    def dest(self, group=None):
+        prefix = ''
+        if group and not isinstance(group, basestring):
+            prefix = group.name + '.'
+        return prefix + self.name
+
+    @property
+    def defaults(self):
+        default = {}
+        if self.default is not VOID:
+            default[self.oname] = str(self.default)
+        return default
+
+    def __repr__(self):
+        return 'Option<%s>' % (self.name)
+
+
+class ListOption(Option):
+
+    def process(self, parser, schema, config_opts, group):
+        """
+        """
+        if isinstance(group, basestring):
+            group = None
+        parser.add_option(type='string', dest=self.dest(group), default=[],
+                          metavar=self.metavar, help=self.help,
+                          action='append', *self.optparser_args(group))
+        log.debug('Option<%s>.process() -> add_option(%s, type=%r, dest=%r, '
+                  'action=%r, default=%r, metavar=%r, help=%r)', self.name,
+                  ', '.join(repr(i) for i in self.optparser_args(group)),
+                  'string', self.dest(group), 'append', [], self.metavar,
+                  self.help)
+        schema.add_field(self.name, ForEach(self.validator, if_empty=[]))
+
+    def value(self, confparser, parseropts, group, ignore_errors):
+        """
+        """
+        value = Option.value(self, confparser, parseropts, group, ignore_errors)
+        if not isinstance(value, (list, tuple)):
+            value = shlex.split(value)
+        return value
+
+    def set_value(self, confparser, parseropts, group):
+        val = getattr(parseropts, self.dest(group))
+        if val:
+            val = ' '.join([repr(i) for i in val])
+            setattr(parseropts, self.dest(group), val)
+            Option.set_value(self, confparser, parseropts, group)
+
+    def __repr__(self):
+        return 'ListOption<%s>' % (self.name)
+
+
+class ConfigOption(ListOption):
+
+    def __init__(self, name, short=(), long=(), default=VOID, help=VOID,
+                 metavar=VOID, override=False):
+        ListOption.__init__(self, name, validators.String, short, long, default,
+                            help, metavar)
+        self.override = override
+
+    def process(self, parser, schema, config_opts, group):
+        ListOption.process(self, parser, schema, config_opts, group)
+        config_opts.append(self)
+
+    def value(self, confparser, parseropts, group, ignore_errors):
+        pass
+
+    def set_value(self, confparser, parseropts, group):
+        pass
+
+
+class Config:
+    """
+    Dummy object that stores all the configuration data.
+    """
+
+    def __repr__(self):
+        return 'Config(%r)' % self.__dict__
+
+
+config = None
+
+args = []
+
+options = Options()
+
+
+class LazyOptionParser(optparse.OptionParser):
+
+    ignore_errors = False
+
+    exit_status = 1
+
+    def exit(self, status=0, msg=None):
+        if self.ignore_errors:
+            return
+        optparse.OptionParser.exit(self, status, msg)
+
+    def error(self, msg):
+        if self.ignore_errors:
+            return
+        self.print_usage(sys.stderr)
+        self.exit(self.exit_status, "%s: error: %s\n"
+                  % (self.get_prog_name(), msg))
+
+    def print_help(self, file=None):
+        if self.ignore_errors:
+            return
+        optparse.OptionParser.print_help(self, file)
+
+    def _process_short_opts(self, rargs, values):
+        try:
+            return optparse.OptionParser._process_short_opts(self, rargs,
+                                                             values)
+        except Exception, e:
+            if not self.ignore_errors:
+                raise
+
+    def _process_long_opt(self, rargs, values):
+        try:
+            return optparse.OptionParser._process_long_opt(self, rargs, values)
+        except Exception, e:
+            if not self.ignore_errors:
+                raise
+
+    def _process_short_opts(self, rargs, values):
+        try:
+            return optparse.OptionParser._process_short_opts(self, rargs, values)
+        except Exception, e:
+            if not self.ignore_errors:
+                raise
+
+
+def load_options(version=None, description=None, ignore_errors=False):
+    # load command-line options
+    optparser = LazyOptionParser(version=version, description=description)
+    optparser.ignore_errors = ignore_errors
+    config_opts = options.process(optparser)
+    (opts, args) = optparser.parse_args()
+    log.debug('load_options() -> %r %r', opts, args)
+    # help the GC
+    optparser.destroy()
+    return (opts, config_opts, args)
+
+
+def make_config(values):
+    log.debug('make_config()')
+    config = Config()
+    for (name, value) in values.items():
+        if isinstance(value, dict):
+            log.debug('make_config() -> processing group %s: %r', name, value)
+            setattr(config, name, make_config(value))
+        else:
+            log.debug('make_config() -> processing value %s: %r', name, value)
+            setattr(config, name, value)
+    log.debug('make_config() -> config = %r', config)
+    return config
+
+
+def load_conf(config_file_paths, version, description, defaults, ignore_errors):
+    log.debug('load_conf(%r, version=%r, description=%r, ignore_errors=%r)',
+              config_file_paths, version, description, ignore_errors)
+    global options
+
+    # load command-line options
+    (opts, config_opts, args) = load_options(version, description,
+                                             ignore_errors=ignore_errors)
+
+    # process config file options to see what config files to load
+    for opt in config_opts:
+        files = getattr(opts, opt.name)
+        log.debug('load_conf() -> processing configuration file option '
+                  '"%s": %r', opt.name, files)
+        if not files:
+            log.debug('load_conf() -> option not set! looking for the next')
+            continue
+        if opt.override:
+            log.debug('load_conf() -> overriding default config files')
+            config_file_paths = files
+        else:
+            log.debug('load_conf() -> appending to default config files')
+            config_file_paths.extend(files)
+
+    confparser = SafeConfigParser(defaults)
+    readed = confparser.read(config_file_paths)
+    log.debug('load_conf() -> readed config files: %r', readed)
+
+    try:
+        log.debug('load_conf() -> sections: %r', confparser.sections())
+        log.debug('load_conf() -> readed values from config files: %r',
+                  [confparser.items(s) for s in confparser.sections()])
+        values = options.values(confparser, opts, ignore_errors)
+    except InterpolationMissingOptionError, e:
+        raise ConfigError('bad value substitution for option "%s" in '
+                              'section [%s] (references an unexistent option '
+                              '"%s")' % (e.option, e.section, e.reference))
+    except InterpolationDepthError, e:
+        raise ConfigError('value interpolation too deeply recursive in '
+                          'option "%s", section [%s]' % (e.option, e.section))
+    except InterpolationSyntaxError, e:
+        raise ConfigError('bad syntax for interpolation variable in option '
+                          '"%s", section [%s]' % (e.option, e.section))
+
+    values = options.schema.to_python(values)
+    log.debug('load_conf() -> validated values: %r', values)
+
+    config = make_config(values)
+
+    # TODO options.check_orphans(confparser, config)
+
+    return (config, args)
+
+
+def load_config(config_file_paths, version=None, description=None,
+               add_plugin_opts=None, defaults=None):
+    "callback signature: add_plugin_opts(config, args)"
+    log.debug('load_config(%r, version=%r, description=%r, add_plugin_opts=%r)',
+              config_file_paths, version, description, add_plugin_opts)
+    global args
+    global config
+
+    (config, args) = load_conf(config_file_paths, version, description,
+                               defaults, add_plugin_opts is not None)
+    while add_plugin_opts:
+        log.debug('load_config() -> calling %r', add_plugin_opts)
+        add_plugin_opts = add_plugin_opts(config, args)
+        log.debug('load_config() -> got config=%r / args=%r', config, args)
+        (config, args) = load_conf(config_file_paths, version, description,
+                                   defaults, add_plugin_opts is not None)
+
+    return (config, args)
+
+
+if __name__ == '__main__':
+
+    import os
+    import tempfile
+    from formencode import validators as V
+
+    logging.basicConfig(
+        level   = logging.DEBUG,
+        format  = '%(levelname)-8s %(message)s',
+    )
+
+    def print_config(config, prefix=''):
+        for attr in dir(config):
+            if not attr.startswith('__'):
+                val = getattr(config, attr)
+                if isinstance(val, Config):
+                    print prefix, 'Group %s:' % attr
+                    print_config(val, '\t')
+                else:
+                    print prefix, attr, '=', val
+
+    options.init('pymind', 'Default group description', [
+        Option('dry_run', V.StringBool, 'd', default=True,
+                help="pretend, don't execute commands"),
+        Option('bind_addr', V.CIDR, 'a', default='127.0.0.1', metavar='ADDR',
+                help='IP address to bind to'),
+        Option('bind_port', V.Int, 'p', default=9999, metavar='PORT',
+                help="port to bind to"),
+        Option('mandatory', V.Int, 'm', metavar='N',
+                help="mandatory parameter"),
+        ConfigOption('config_file', 'c', metavar="FILE",
+                help="configuration file"),
+        ConfigOption('override_config_file', 'o', metavar="FILE",
+                help="configuration file", override=True),
+        ListOption('plugins', V.String, 's', metavar="SERV", default=[],
+                help="plugins pymin should use"),
+        OptionGroup('test_grp', 'A test group', [
+            Option('test', V.StringBool, default=False,
+                    help="test group option"),
+            Option('oneof', V.OneOf(['tcp', 'udp']), default='tcp',
+                    help="test option for OneOf validator"),
+            ListOption('list', V.StringBool, default=[True, True, False],
+                    help="test ListOption"),
+        ]),
+    ])
+
+    def add_more_plugin_opts(config, args):
+        print 'add_more_plugin_opts'
+        print '---------------'
+        print config, args
+        print '---------------'
+        print
+        g = options.get_group('plugin')
+        g.add(ListOption('other', V.Int, default=[1,2], metavar='OTHER',
+                         help="a list of numbers"))
+
+    def add_plugin_opts(config, args):
+        print 'add_plugin_opts'
+        print '---------------'
+        print config, args
+        print '---------------'
+        print
+        if 'plugin' in config.plugins:
+            options.add(
+                OptionGroup('plugin', 'some plug-in options', [
+                    Option('active', V.StringBool, default=True, metavar='ACT',
+                            help="if ACT is true, the plug-in is active"),
+                ])
+            )
+            return add_more_plugin_opts
+
+
+    f = tempfile.NamedTemporaryFile()
+    f.write("""
+[pymind]
+dry-run:         yes
+bind-addr:       0.0.0.0
+bind-port:       2000
+log-config-file: /etc/pymin/log.conf
+
+[test-grp]
+test = yes
+""")
+    f.flush()
+    f.seek(0)
+    print '-'*78
+    print f.read()
+    print '-'*78
+
+    try:
+        (c, a) = load_config([f.name], '%prog 0.1', 'this is a program test',
+                             add_plugin_opts)
+        print "Config:"
+        print_config(c)
+        print "Args:", a
+        print
+        print "Globals:"
+        print "Config:"
+        print_config(config)
+        print "Args:", args
+    except ConfigError, e:
+        print e
+    except MissingSectionHeaderError, e:
+        print "%s:%s: missing section header near: %s" \
+                % (e.filename, e.lineno, e.line)
+    except ParsingError, e:
+        for (lineno, line) in e.errors:
+            print "%s:%s: invalid syntax near: %s" % (e.filename, lineno, line)
+        print e.errors
+    except Invalid, e:
+        print e.error_dict
+    f.close()
+
diff --git a/pymind b/pymind
index 22fb0923a838b082617a6cef265475214a429a37..29d296bd2f5ec06fd862c31457e2eb2014aa5603 100755 (executable)
--- a/pymind
+++ b/pymind
@@ -1,7 +1,6 @@
 #!/usr/bin/env python
 # vim: set encoding=utf-8 et sw=4 sts=4 :
 
-import sys
 import logging ; log = logging.getLogger('pymind')
 # First of all, we need to setup the logging framework
 # FIXME: this should go in a configuration file
@@ -11,31 +10,65 @@ logging.basicConfig(
         datefmt = '%a, %d %b %Y %H:%M:%S',
 )
 
-from pymin.pymindaemon import PyminDaemon
+import os
+import sys
+from formencode import Invalid, validators as V
+
+from pymin.config import OptionGroup, Option, ConfigOption, ListOption
+from pymin.config import load_config, options
+from pymin.config import ConfigError, MissingSectionHeaderError, ParsingError
 from pymin.dispatcher import Handler
+from pymin.pymindaemon import PyminDaemon
 from pymin.service import load_service, LoadError
-import config
 
-# exit status
-EXIT_NO_SERVICE = 1
+# exit status (1 is reserved for command-line errors)
+EXIT_CONFIG_ERROR = 2
+EXIT_NO_SERVICE   = 3
 
-class Root(Handler):
-    pass
+# default locations where to look for configuration files
+# all found files will be processed, overriding the previous configuration
+# files values.
+config_file_paths = [
+    '/etc/pymin.ini',
+    '/etc/pymin/pymin.ini',
+    os.path.expanduser('~/.pymin.ini'),
+    os.path.expanduser('~/.pymin/pymin.ini'),
+]
+
+# default locations where to look for service plug-ins
+# search stops when a service plug-in is found
+services_paths = [
+    os.path.expanduser('~/.pymin/services'),
+    '/usr/share/pymin/services',
+]
+
+# default configuration variables
+# these are useful variables to help the user writing the configuration file
+config_defaults = {
+    'pymind-data-dir':   '/var/lib/pymin',
+    'pymind-pickle-dir': '/var/lib/pymin/pickle',
+    'pymind-config-dir': '/var/lib/pymin/config',
+}
+
+# Validator to check if is a valid Python identifier
+PythonIdentifier = V.Regex(r'^[a-zA-Z_]\w*$')
+
+options.init('pymind', 'Pymin daemon global options', [
+    Option('bind_addr', V.CIDR, 'a', default='127.0.0.1', metavar='ADDR',
+           help='Bind to IP ADDR'),
+    Option('bind_port', V.Int(min=1, max=65535), 'p', default=9999,
+           metavar='PORT', help="Bind to port PORT"),
+    ListOption('services', PythonIdentifier, 's', default=[],
+               metavar='SERVICE', help="manage service SERVICE"),
+    ListOption('services_dirs', V.String, 'd', default=[],
+               metavar='DIR', help="search for services in DIR"),
+    ConfigOption('config_file', 'c', metavar='FILE',
+                 help="load the configuration file FILE after the default "
+                      "configuration files"),
+    ConfigOption('replace_config_file', 'C', override=True, metavar='FILE',
+                 help="don't load the default configuration files, just FILE"),
+])
 
-def build_root(config):
-    # TODO check services dependencies
-    services = dict()
-    for service in config.services:
-        try:
-            s = load_service(service, config.services_dirs)
-        except LoadError, e:
-            log.error("Can't find service called '%s'\n", service)
-            sys.exit(EXIT_NO_SERVICE)
-        services[service] = s
-    root = Root()
-    for name, service in services.items():
-        setattr(root, name, service.get_service(config))
-    return root
 
 # FIXME
 try:
@@ -45,5 +78,61 @@ try:
 except (IOError, OSError), e:
     log.warning("Can't set ip_forward: %s", e)
 
-PyminDaemon(build_root(config), config.bind_addr).run()
+
+def get_config(paths, version, desc, add_options, defaults):
+    global config_file_paths
+    try:
+        (config, args) = load_config(paths, version, desc, add_options, defaults)
+    except ConfigError, e:
+        log.error(str(e))
+        sys.exit(EXIT_CONFIG_ERROR)
+    except MissingSectionHeaderError, e:
+        log.error("%s:%s: missing section header near: %s", e.filename, e.lineno,
+                  e.line)
+        sys.exit(EXIT_CONFIG_ERROR)
+    except ParsingError, e:
+        for (lineno, line) in e.errors:
+            log.error("%s:%s: invalid syntax near: %s", e.filename, lineno, line)
+        log.error(str(e.errors))
+        sys.exit(EXIT_CONFIG_ERROR)
+    except Invalid, e:
+        log.error(str(e.unpack_errors()))
+        sys.exit(EXIT_CONFIG_ERROR)
+    except LoadError, e:
+        log.error("service '%s' not found (see option --services-dir)",
+                  e.service_name)
+        sys.exit(EXIT_NO_SERVICE)
+    return (config, args)
+
+
+class Services:
+    def __init__(self):
+        self.services = dict()
+    def add_config_options(self, config, args):
+        for service in config.services:
+            s = load_service(service, config.services_dirs)
+            s.setup_service(options, config)
+            self.services[service] = s
+
+def build_root(config, args, services):
+    from pymin.dispatcher import Handler
+    class Root(Handler):
+        pass
+    # TODO check services dependencies
+    root = Root()
+    for name, service in services.items():
+        setattr(root, name, service.get_service(config))
+    return root
+
+
+def main():
+    services = Services()
+    (config, args) = get_config(config_file_paths, '%prog 0.1',
+                                'Router services administration daemon',
+                                services.add_config_options, config_defaults)
+    root_handler = build_root(config, args, services.services)
+    PyminDaemon(root_handler, (config.bind_addr, config.bind_port)).run()
+
+if __name__ == '__main__':
+    main()
 
index d493b1c53ba373d08d71934e9a6867d31df820dd..8f9581f239e7020c2b7778f9be4e5128209fab03 100644 (file)
@@ -1,7 +1,17 @@
 # vim: set encoding=utf-8 et sw=4 sts=4 :
 
+from formencode import validators as V
+from pymin.config import Option
 from handler import DhcpHandler
 
+def setup_service(options, config):
+    options.add_group('dhcp', 'DHCP service', [
+        Option('pickle_dir', V.String, metavar='DIR',
+               help='store persistent data in DIR directory'),
+        Option('config_dir', V.String, metavar='DIR',
+               help='write config file in DIR directory'),
+    ])
+
 def get_service(config):
     return DhcpHandler(config.dhcp.pickle_dir, config.dhcp.config_dir)
 
index 1b5e4e59396b05bc11b7770000f1afd1dbf7e197..882cdd11b6c550e7b9d3e6741e7a86228ce8f6ed 100644 (file)
@@ -1,7 +1,22 @@
 # vim: set encoding=utf-8 et sw=4 sts=4 :
 
+from formencode import validators as V
+from pymin.config import Option
 from handler import DnsHandler
 
+def setup_service(options, config):
+    options.add_group('dns', 'DNS service', [
+        Option('pickle_dir', V.String, metavar='DIR',
+               help='store persistent data in DIR directory'),
+        Option('config_named_dir', V.String, metavar='DIR',
+               help='write named config files in DIR directory'),
+        Option('config_zones_dir', V.String, metavar='DIR',
+               help='write zone config files in DIR directory'),
+    ])
+
 def get_service(config):
-    return DnsHandler(config.dns.pickle_dir, config.dns.config_dir)
+    return DnsHandler(config.dns.pickle_dir, {
+                'named.conf': config.dns.config_named_dir,
+                'zoneX.zone': config.dns.config_zones_dir,
+    })
 
index 2c8e091124646f381685ea936e78e73841463dd8..6c1cbe30aaf2793b1a83e0a1cfc00c919cdcdcd4 100644 (file)
@@ -1,7 +1,17 @@
 # vim: set encoding=utf-8 et sw=4 sts=4 :
 
+from formencode import validators as V
+from pymin.config import Option
 from handler import FirewallHandler
 
+def setup_service(options, config):
+    options.add_group('firewall', 'Firewall service', [
+        Option('pickle_dir', V.String, metavar='DIR',
+               help='store persistent data in DIR directory'),
+        Option('config_dir', V.String, metavar='DIR',
+               help='write config file in DIR directory'),
+    ])
+
 def get_service(config):
     return FirewallHandler(config.firewall.pickle_dir, config.firewall.config_dir)
 
index 8840e224c747af768bb6c15b44686a6552de47a3..8ce72f78b60d305d2322eacc89fe4a02b0c3dae4 100644 (file)
@@ -1,7 +1,17 @@
 # vim: set encoding=utf-8 et sw=4 sts=4 :
 
+from formencode import validators as V
+from pymin.config import Option
 from handler import IpHandler
 
+def setup_service(options, config):
+    options.add_group('ip', 'IP network interfaces', [
+        Option('pickle_dir', V.String, metavar='DIR',
+               help='store persistent data in DIR directory'),
+        Option('config_dir', V.String, metavar='DIR',
+               help='write config files in DIR directory'),
+    ])
+
 def get_service(config):
     return IpHandler(config.ip.pickle_dir, config.ip.config_dir)
 
index 0deab9a7d1bb493875ea0c87a079acaa82bf7528..8a3804746bccf67591f86ffc59f46ca5d9b60c7b 100644 (file)
@@ -1,7 +1,15 @@
 # vim: set encoding=utf-8 et sw=4 sts=4 :
 
+from formencode import validators as V
+from pymin.config import Option
 from handler import NatHandler
 
+def setup_service(options, config):
+    options.add_group('nat', 'Network Address Translation service', [
+        Option('pickle_dir', V.String, metavar='DIR',
+               help='store persistent data in DIR directory'),
+    ])
+
 def get_service(config):
     return NatHandler(config.nat.pickle_dir)
 
index cc53450753a5578b8b4d71cf525a08ccf30ddcd3..7681d2895696aaa7509408cdec43aeaf9bc1fd4e 100644 (file)
@@ -1,7 +1,28 @@
 # vim: set encoding=utf-8 et sw=4 sts=4 :
 
+from formencode import validators as V
+from pymin.config import Option
 from handler import PppHandler
 
+def setup_service(options, config):
+    options.add_group('ppp', 'PPP network interfaces', [
+        Option('pickle_dir', V.String, metavar='DIR',
+               help='store persistent data in DIR directory'),
+        Option('config_options_dir', V.String, metavar='DIR',
+               help='write options config files in DIR directory'),
+        Option('config_pap_dir', V.String, metavar='DIR',
+               help='write pap-secrets config file in DIR directory'),
+        Option('config_chap_dir', V.String, metavar='DIR',
+               help='write chap-secrets config file in DIR directory'),
+        Option('config_peers_dir', V.String, metavar='DIR',
+               help='write peer config files in DIR directory'),
+    ])
+
 def get_service(config):
-    return PppHandler(config.ppp.pickle_dir, config.ppp.config_dir)
+    return PppHandler(config.ppp.pickle_dir, {
+                'options.X': config.ppp.config_options_dir,
+                'pap-secrets': config.ppp.config_pap_dir,
+                'chap-secrets': config.ppp.config_chap_dir,
+                'nameX': config.ppp.config_peers_dir,
+    })
 
index ab2e84cce2f959f6d78c29154c55d99bddd94a57..33e18d6ef11fd4c8e972a531a7a1e1ca81c98706 100644 (file)
@@ -1,7 +1,17 @@
 # vim: set encoding=utf-8 et sw=4 sts=4 :
 
+from formencode import validators as V
+from pymin.config import Option
 from handler import ProxyHandler
 
+def setup_service(options, config):
+    options.add_group('proxy', 'Proxy service', [
+        Option('pickle_dir', V.String, metavar='DIR',
+               help='store persistent data in DIR directory'),
+        Option('config_dir', V.String, metavar='DIR',
+               help='write config files in DIR directory'),
+    ])
+
 def get_service(config):
     return ProxyHandler(config.proxy.pickle_dir, config.proxy.config_dir)
 
index 78e8134b45159adf5c78512fb4da3f7cfda2a34a..83aacea12eea9f199012204e4c6525246f93cd4e 100644 (file)
@@ -1,7 +1,17 @@
 # vim: set encoding=utf-8 et sw=4 sts=4 :
 
+from formencode import validators as V
+from pymin.config import Option
 from handler import QoSHandler
 
+def setup_service(options, config):
+    options.add_group('qos', 'Quality of Service', [
+        Option('pickle_dir', V.String, metavar='DIR',
+               help='store persistent data in DIR directory'),
+        Option('config_dir', V.String, metavar='DIR',
+               help='write config files in DIR directory'),
+    ])
+
 def get_service(config):
     return QoSHandler(config.qos.pickle_dir, config.qos.config_dir)
 
index 0e2747a681d2ebde997127fa45522697aeb7b2b0..5071c7ef738dae2e47686dc890541921341fe6df 100644 (file)
@@ -1,7 +1,17 @@
 # vim: set encoding=utf-8 et sw=4 sts=4 :
 
+from formencode import validators as V
+from pymin.config import Option
 from handler import VpnHandler
 
+def setup_service(options, config):
+    options.add_group('vpn', 'Virtual Private Networking service', [
+        Option('pickle_dir', V.String, metavar='DIR',
+               help='store persistent data in DIR directory'),
+        Option('config_dir', V.String, metavar='DIR',
+               help='write config file in DIR directory'),
+    ])
+
 def get_service(config):
     return VpnHandler(config.vpn.pickle_dir, config.vpn.config_dir)
 
index 15cf0612a6021c06432a78cc9bddbfbb054a42fe..0d71b88f3fa7b66315bf20df2ebb4f085086f08e 100644 (file)
@@ -1,7 +1,20 @@
 # vim: set encoding=utf-8 et sw=4 sts=4 :
 
+from formencode import validators as V
+from pymin.config import Option
 from handler import VrrpHandler
 
+def setup_service(options, config):
+    options.add_group('vrrp', 'Virtual Router Redundancy service', [
+        Option('pickle_dir', V.String, metavar='DIR',
+               help='store persistent data in DIR directory'),
+        Option('config_dir', V.String, metavar='DIR',
+               help='write config file in DIR directory'),
+        Option('pid_dir', V.String, metavar='DIR',
+               help='write PID file in DIR directory'),
+    ])
+
 def get_service(config):
-    return VrrpHandler(config.vrrp.pickle_dir, config.vrrp.config_dir)
+    return VrrpHandler(config.vrrp.pickle_dir, config.vrrp.config_dir,
+                       config.vrrp.pid_dir)