diff options
author | Rob Crittenden <rcritten@redhat.com> | 2009-01-22 10:42:41 -0500 |
---|---|---|
committer | Rob Crittenden <rcritten@redhat.com> | 2009-01-22 10:42:41 -0500 |
commit | c2967a675a288e7d31374229fd974d0cb9966f2c (patch) | |
tree | 58be8ca6319f4660d9f18b97a37b9c0c56104d02 /ipalib/config.py | |
parent | 2b8b87b4d6c3b4389a0a7bf48c225035c53e7ad1 (diff) | |
parent | 5d82e3b35a8fb2d4c25f282cddad557a7650197c (diff) | |
download | freeipa.git-c2967a675a288e7d31374229fd974d0cb9966f2c.tar.gz freeipa.git-c2967a675a288e7d31374229fd974d0cb9966f2c.tar.xz freeipa.git-c2967a675a288e7d31374229fd974d0cb9966f2c.zip |
Merge branch 'master' of git://fedorapeople.org/~jderose/freeipa2
Diffstat (limited to 'ipalib/config.py')
-rw-r--r-- | ipalib/config.py | 529 |
1 files changed, 529 insertions, 0 deletions
diff --git a/ipalib/config.py b/ipalib/config.py new file mode 100644 index 00000000..3544331d --- /dev/null +++ b/ipalib/config.py @@ -0,0 +1,529 @@ +# Authors: +# Martin Nagy <mnagy@redhat.com> +# Jason Gerard DeRose <jderose@redhat.com> +# +# Copyright (C) 2008 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; version 2 only +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +""" +Process-wide static configuration and environment. + +The standard run-time instance of the `Env` class is initialized early in the +`ipalib` process and is then locked into a read-only state, after which no +further changes can be made to the environment throughout the remaining life +of the process. + +For the per-request thread-local information, see `ipalib.request`. +""" + +from ConfigParser import RawConfigParser, ParsingError +from types import NoneType +import os +from os import path +import sys + +from base import check_name +from constants import CONFIG_SECTION +from constants import TYPE_ERROR, OVERRIDE_ERROR, SET_ERROR, DEL_ERROR + + +class Env(object): + """ + Store and retrieve environment variables. + + First an foremost, the `Env` class provides a handy container for + environment variables. These variables can be both set *and* retrieved + either as attributes *or* as dictionary items. + + For example, you can set a variable as an attribute: + + >>> env = Env() + >>> env.attr = 'I was set as an attribute.' + >>> env.attr + 'I was set as an attribute.' + >>> env['attr'] # Also retrieve as a dictionary item + 'I was set as an attribute.' + + Or you can set a variable as a dictionary item: + + >>> env['item'] = 'I was set as a dictionary item.' + >>> env['item'] + 'I was set as a dictionary item.' + >>> env.item # Also retrieve as an attribute + 'I was set as a dictionary item.' + + The variable names must be valid lower-case Python identifiers that neither + start nor end with an underscore. If your variable name doesn't meet these + criteria, a ``ValueError`` will be raised when you try to set the variable + (compliments of the `base.check_name()` function). For example: + + >>> env.BadName = 'Wont work as an attribute' + Traceback (most recent call last): + ... + ValueError: name must match '^[a-z][_a-z0-9]*[a-z0-9]$'; got 'BadName' + >>> env['BadName'] = 'Also wont work as a dictionary item' + Traceback (most recent call last): + ... + ValueError: name must match '^[a-z][_a-z0-9]*[a-z0-9]$'; got 'BadName' + + The variable values can be ``str``, ``int``, or ``float`` instances, or the + ``True``, ``False``, or ``None`` constants. When the value provided is an + ``str`` instance, some limited automatic type conversion is performed, which + allows values of specific types to be set easily from configuration files or + command-line options. + + So in addition to their actual values, the ``True``, ``False``, and ``None`` + constants can be specified with an ``str`` equal to what ``repr()`` would + return. For example: + + >>> env.true = True + >>> env.also_true = 'True' # Equal to repr(True) + >>> env.true + True + >>> env.also_true + True + + Note that the automatic type conversion is case sensitive. For example: + + >>> env.not_false = 'false' # Not equal to repr(False)! + >>> env.not_false + 'false' + + If an ``str`` value looks like an integer, it's automatically converted to + the ``int`` type. Likewise, if an ``str`` value looks like a floating-point + number, it's automatically converted to the ``float`` type. For example: + + >>> env.lucky = '7' + >>> env.lucky + 7 + >>> env.three_halves = '1.5' + >>> env.three_halves + 1.5 + + Leading and trailing white-space is automatically stripped from ``str`` + values. For example: + + >>> env.message = ' Hello! ' # Surrounded by double spaces + >>> env.message + 'Hello!' + >>> env.number = ' 42 ' # Still converted to an int + >>> env.number + 42 + >>> env.false = ' False ' # Still equal to repr(False) + >>> env.false + False + + Also, empty ``str`` instances are converted to ``None``. For example: + + >>> env.empty = '' + >>> env.empty is None + True + + `Env` variables are all set-once (first-one-wins). Once a variable has been + set, trying to override it will raise an ``AttributeError``. For example: + + >>> env.date = 'First' + >>> env.date = 'Second' + Traceback (most recent call last): + ... + AttributeError: cannot override Env.date value 'First' with 'Second' + + An `Env` instance can be *locked*, after which no further variables can be + set. Trying to set variables on a locked `Env` instance will also raise + an ``AttributeError``. For example: + + >>> env = Env() + >>> env.okay = 'This will work.' + >>> env.__lock__() + >>> env.nope = 'This wont work!' + Traceback (most recent call last): + ... + AttributeError: locked: cannot set Env.nope to 'This wont work!' + + `Env` instances also provide standard container emulation for membership + testing, counting, and iteration. For example: + + >>> env = Env() + >>> 'key1' in env # Has key1 been set? + False + >>> env.key1 = 'value 1' + >>> 'key1' in env + True + >>> env.key2 = 'value 2' + >>> len(env) # How many variables have been set? + 2 + >>> list(env) # What variables have been set? + ['key1', 'key2'] + + Lastly, in addition to all the handy container functionality, the `Env` + class provides high-level methods for bootstraping a fresh `Env` instance + into one containing all the run-time and configuration information needed + by the built-in freeIPA plugins. + + These are the `Env` bootstraping methods, in the order they must be called: + + 1. `Env._bootstrap()` - initialize the run-time variables and then + merge-in variables specified on the command-line. + + 2. `Env._finalize_core()` - merge-in variables from the configuration + files and then merge-in variables from the internal defaults, after + which at least all the standard variables will be set. After this + method is called, the plugins will be loaded, during which + third-party plugins can merge-in defaults for additional variables + they use (likely using the `Env._merge()` method). + + 3. `Env._finalize()` - one last chance to merge-in variables and then + the instance is locked. After this method is called, no more + environment variables can be set during the remaining life of the + process. + + However, normally none of these three bootstraping methods are called + directly and instead only `plugable.API.bootstrap()` is called, which itself + takes care of correctly calling the `Env` bootstrapping methods. + """ + + __locked = False + + def __init__(self): + object.__setattr__(self, '_Env__d', {}) + object.__setattr__(self, '_Env__done', set()) + + def __lock__(self): + """ + Prevent further changes to environment. + """ + if self.__locked is True: + raise StandardError( + '%s.__lock__() already called' % self.__class__.__name__ + ) + object.__setattr__(self, '_Env__locked', True) + + def __islocked__(self): + """ + Return ``True`` if locked. + """ + return self.__locked + + def __setattr__(self, name, value): + """ + Set the attribute named ``name`` to ``value``. + + This just calls `Env.__setitem__()`. + """ + self[name] = value + + def __setitem__(self, key, value): + """ + Set ``key`` to ``value``. + """ + if self.__locked: + raise AttributeError( + SET_ERROR % (self.__class__.__name__, key, value) + ) + check_name(key) + if key in self.__d: + raise AttributeError(OVERRIDE_ERROR % + (self.__class__.__name__, key, self.__d[key], value) + ) + assert not hasattr(self, key) + if isinstance(value, basestring): + value = str(value.strip()) + m = { + 'True': True, + 'False': False, + 'None': None, + '': None, + } + if value in m: + value = m[value] + elif value.isdigit(): + value = int(value) + else: + try: + value = float(value) + except (TypeError, ValueError): + pass + assert type(value) in (str, int, float, bool, NoneType) + object.__setattr__(self, key, value) + self.__d[key] = value + + def __getitem__(self, key): + """ + Return the value corresponding to ``key``. + """ + return self.__d[key] + + def __delattr__(self, name): + """ + Raise an ``AttributeError`` (deletion is never allowed). + + For example: + + >>> env = Env() + >>> env.name = 'A value' + >>> del env.name + Traceback (most recent call last): + ... + AttributeError: locked: cannot delete Env.name + """ + raise AttributeError( + DEL_ERROR % (self.__class__.__name__, name) + ) + + def __contains__(self, key): + """ + Return True if instance contains ``key``; otherwise return False. + """ + return key in self.__d + + def __len__(self): + """ + Return number of variables currently set. + """ + return len(self.__d) + + def __iter__(self): + """ + Iterate through keys in ascending order. + """ + for key in sorted(self.__d): + yield key + + def _merge(self, **kw): + """ + Merge variables from ``kw`` into the environment. + + Any variables in ``kw`` that have already been set will be ignored + (meaning this method will *not* try to override them, which would raise + an exception). + + This method returns a ``(num_set, num_total)`` tuple containing first + the number of variables that were actually set, and second the total + number of variables that were provided. + + For example: + + >>> env = Env() + >>> env._merge(one=1, two=2) + (2, 2) + >>> env._merge(one=1, three=3) + (1, 2) + >>> env._merge(one=1, two=2, three=3) + (0, 3) + + Also see `Env._merge_from_file()`. + + :param kw: Variables provides as keyword arguments. + """ + i = 0 + for (key, value) in kw.iteritems(): + if key not in self: + self[key] = value + i += 1 + return (i, len(kw)) + + def _merge_from_file(self, config_file): + """ + Merge variables from ``config_file`` into the environment. + + Any variables in ``config_file`` that have already been set will be + ignored (meaning this method will *not* try to override them, which + would raise an exception). + + If ``config_file`` does not exist or is not a regular file, or if there + is an error parsing ``config_file``, ``None`` is returned. + + Otherwise this method returns a ``(num_set, num_total)`` tuple + containing first the number of variables that were actually set, and + second the total number of variables found in ``config_file``. + + This method will raise a ``ValueError`` if ``config_file`` is not an + absolute path. For example: + + >>> env = Env() + >>> env._merge_from_file('my/config.conf') + Traceback (most recent call last): + ... + ValueError: config_file must be an absolute path; got 'my/config.conf' + + Also see `Env._merge()`. + + :param config_file: Absolute path of the configuration file to load. + """ + if path.abspath(config_file) != config_file: + raise ValueError( + 'config_file must be an absolute path; got %r' % config_file + ) + if not path.isfile(config_file): + return + parser = RawConfigParser() + try: + parser.read(config_file) + except ParsingError: + return + if not parser.has_section(CONFIG_SECTION): + parser.add_section(CONFIG_SECTION) + items = parser.items(CONFIG_SECTION) + if len(items) == 0: + return (0, 0) + i = 0 + for (key, value) in items: + if key not in self: + self[key] = value + i += 1 + return (i, len(items)) + + def __doing(self, name): + if name in self.__done: + raise StandardError( + '%s.%s() already called' % (self.__class__.__name__, name) + ) + self.__done.add(name) + + def __do_if_not_done(self, name): + if name not in self.__done: + getattr(self, name)() + + def _isdone(self, name): + return name in self.__done + + def _bootstrap(self, **overrides): + """ + Initialize basic environment. + + This method will perform the following steps: + + 1. Initialize certain run-time variables. These run-time variables + are strictly determined by the external environment the process + is running in; they cannot be specified on the command-line nor + in the configuration files. + + 2. Merge-in the variables in ``overrides`` by calling + `Env._merge()`. The intended use of ``overrides`` is to merge-in + variables specified on the command-line. + + 3. Intelligently fill-in the *in_tree*, *context*, *conf*, and + *conf_default* variables if they haven't been set already. + + Also see `Env._finalize_core()`, the next method in the bootstrap + sequence. + + :param overrides: Variables specified via command-line options. + """ + self.__doing('_bootstrap') + + # Set run-time variables: + self.ipalib = path.dirname(path.abspath(__file__)) + self.site_packages = path.dirname(self.ipalib) + self.script = path.abspath(sys.argv[0]) + self.bin = path.dirname(self.script) + self.home = path.abspath(os.environ['HOME']) + self.dot_ipa = path.join(self.home, '.ipa') + self._merge(**overrides) + if 'in_tree' not in self: + if self.bin == self.site_packages and \ + path.isfile(path.join(self.bin, 'setup.py')): + self.in_tree = True + else: + self.in_tree = False + if 'context' not in self: + self.context = 'default' + if self.in_tree: + base = self.dot_ipa + else: + base = path.join('/', 'etc', 'ipa') + if 'conf' not in self: + self.conf = path.join(base, '%s.conf' % self.context) + if 'conf_default' not in self: + self.conf_default = path.join(base, 'default.conf') + if 'conf_dir' not in self: + self.conf_dir = base + + def _finalize_core(self, **defaults): + """ + Complete initialization of standard IPA environment. + + This method will perform the following steps: + + 1. Call `Env._bootstrap()` if it hasn't already been called. + + 2. Merge-in variables from the configuration file ``self.conf`` + (if it exists) by calling `Env._merge_from_file()`. + + 3. Merge-in variables from the defaults configuration file + ``self.conf_default`` (if it exists) by calling + `Env._merge_from_file()`. + + 4. Intelligently fill-in the *in_server* and *log* variables + if they haven't already been set. + + 5. Merge-in the variables in ``defaults`` by calling `Env._merge()`. + In normal circumstances ``defaults`` will simply be those + specified in `constants.DEFAULT_CONFIG`. + + After this method is called, all the environment variables used by all + the built-in plugins will be available. As such, this method should be + called *before* any plugins are loaded. + + After this method has finished, the `Env` instance is still writable + so that 3rd-party plugins can set variables they may require as the + plugins are registered. + + Also see `Env._finalize()`, the final method in the bootstrap sequence. + + :param defaults: Internal defaults for all built-in variables. + """ + self.__doing('_finalize_core') + self.__do_if_not_done('_bootstrap') + if self.__d.get('mode', None) != 'dummy': + self._merge_from_file(self.conf) + self._merge_from_file(self.conf_default) + if 'in_server' not in self: + self.in_server = (self.context == 'server') + if 'log' not in self: + name = '%s.log' % self.context + if self.in_tree or self.context == 'cli': + self.log = path.join(self.dot_ipa, 'log', name) + else: + self.log = path.join('/', 'var', 'log', 'ipa', name) + self._merge(**defaults) + + def _finalize(self, **lastchance): + """ + Finalize and lock environment. + + This method will perform the following steps: + + 1. Call `Env._finalize_core()` if it hasn't already been called. + + 2. Merge-in the variables in ``lastchance`` by calling + `Env._merge()`. + + 3. Lock this `Env` instance, after which no more environment + variables can be set on this instance. Aside from unit-tests + and example code, normally only one `Env` instance is created, + which means that after this step, no more variables can be set + during the remaining life of the process. + + This method should be called after all plugins have been loaded and + after `plugable.API.finalize()` has been called. + + :param lastchance: Any final variables to merge-in before locking. + """ + self.__doing('_finalize') + self.__do_if_not_done('_finalize_core') + self._merge(**lastchance) + self.__lock__() |