diff options
-rwxr-xr-x | ipa | 5 | ||||
-rw-r--r-- | ipa_server/mod_python_xmlrpc.py | 3 | ||||
-rw-r--r-- | ipalib/cli.py | 351 | ||||
-rw-r--r-- | ipalib/config.py | 221 | ||||
-rw-r--r-- | ipalib/constants.py | 106 | ||||
-rw-r--r-- | ipalib/frontend.py | 2 | ||||
-rw-r--r-- | ipalib/load_plugins.py | 82 | ||||
-rw-r--r-- | ipalib/plugable.py | 58 | ||||
-rw-r--r-- | ipalib/plugins/b_xmlrpc.py | 23 | ||||
-rw-r--r-- | ipalib/util.py | 86 | ||||
-rwxr-xr-x | lite-webui.py | 5 | ||||
-rwxr-xr-x | lite-xmlrpc.py | 80 | ||||
-rwxr-xr-x | make-test | 1 | ||||
-rw-r--r-- | tests/test_ipalib/test_cli.py | 208 | ||||
-rw-r--r-- | tests/test_ipalib/test_config.py | 455 | ||||
-rw-r--r-- | tests/test_ipalib/test_crud.py | 2 | ||||
-rw-r--r-- | tests/test_ipalib/test_frontend.py | 10 | ||||
-rw-r--r-- | tests/test_ipalib/test_plugable.py | 214 | ||||
-rw-r--r-- | tests/util.py | 63 |
19 files changed, 1567 insertions, 408 deletions
@@ -28,8 +28,9 @@ The CLI functionality is implemented in ipalib/cli.py import sys from ipalib import api from ipalib.cli import CLI -import ipalib.load_plugins if __name__ == '__main__': - cli = CLI(api) + cli = CLI(api, + (s.decode('utf-8') for s in sys.argv[1:]) + ) sys.exit(cli.run()) diff --git a/ipa_server/mod_python_xmlrpc.py b/ipa_server/mod_python_xmlrpc.py index 814b1f65..450f4a51 100644 --- a/ipa_server/mod_python_xmlrpc.py +++ b/ipa_server/mod_python_xmlrpc.py @@ -46,11 +46,12 @@ from ipalib import config from ipa_server import conn from ipa_server.servercore import context from ipa_server.servercore import ipautil -import ipalib.load_plugins from ipalib.util import xmlrpc_unmarshal import string +api.load_plugins() + # Global list of available functions gfunctions = {} diff --git a/ipalib/cli.py b/ipalib/cli.py index a802f8ef..021e01ad 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -85,6 +85,8 @@ class help(frontend.Application): self.application.build_parser(cmd).print_help() + + class console(frontend.Application): 'Start the IPA interactive Python console.' @@ -95,6 +97,21 @@ class console(frontend.Application): ) +class env(frontend.Application): + """ + Show environment variables. + """ + + def run(self): + return tuple( + (key, self.api.env[key]) for key in self.api.env + ) + + def output_for_cli(self, ret): + for (key, value) in ret: + print '%s = %r' % (key, value) + + class show_api(text_ui): 'Show attributes on dynamic API object' @@ -181,6 +198,7 @@ cli_application_commands = ( console, show_api, plugins, + env, ) @@ -204,17 +222,160 @@ class KWCollector(object): class CLI(object): + """ + All logic for dispatching over command line interface. + """ + __d = None __mcl = None - def __init__(self, api): - self.__api = api - self.__all_interactive = False - self.__not_interactive = False + def __init__(self, api, argv): + self.api = api + self.argv = tuple(argv) + self.__done = set() + + def run(self, init_only=False): + """ + Parse ``argv`` and potentially run a command. + + This method requires several initialization steps to be completed + first, all of which all automatically called with a single call to + `CLI.finalize()`. The initialization steps are broken into separate + methods simply to make it easy to write unit tests. + + The initialization involves these steps: + + 1. `CLI.parse_globals` parses the global options, which get stored + in ``CLI.options``, and stores the remaining args in + ``CLI.cmd_argv``. + + 2. `CLI.bootstrap` initializes the environment information in + ``CLI.api.env``. + + 3. `CLI.load_plugins` registers all plugins, including the + CLI-specific plugins. + + 4. `CLI.finalize` instantiates all plugins and performs the + remaining initialization needed to use the `plugable.API` + instance. + """ + self.__doing('run') + self.finalize() + if self.api.env.mode == 'unit_test': + return + if len(self.cmd_argv) < 1: + self.print_commands() + print 'Usage: ipa [global-options] COMMAND' + sys.exit(2) + key = self.cmd_argv[0] + if key not in self: + self.print_commands() + print 'ipa: ERROR: unknown command %r' % key + sys.exit(2) + return self.run_cmd(self[key]) + + def finalize(self): + """ + Fully initialize ``CLI.api`` `plugable.API` instance. + + This method first calls `CLI.load_plugins` to perform some dependant + initialization steps, after which `plugable.API.finalize` is called. - def __get_api(self): - return self.__api - api = property(__get_api) + Finally, the CLI-specific commands are passed a reference to this + `CLI` instance by calling `frontend.Application.set_application`. + """ + self.__doing('finalize') + self.load_plugins() + self.api.finalize() + for a in self.api.Application(): + a.set_application(self) + assert self.__d is None + self.__d = dict( + (c.name.replace('_', '-'), c) for c in self.api.Command() + ) + + def load_plugins(self): + """ + Load all standard plugins plus the CLI-specific plugins. + + This method first calls `CLI.bootstrap` to preform some dependant + initialization steps, after which `plugable.API.load_plugins` is + called. + + Finally, all the CLI-specific plugins are registered. + """ + self.__doing('load_plugins') + self.bootstrap() + self.api.load_plugins() + for klass in cli_application_commands: + self.api.register(klass) + + def bootstrap(self): + """ + Initialize the ``CLI.api.env`` environment variables. + + This method first calls `CLI.parse_globals` to perform some dependant + initialization steps. Then, using environment variables that may have + been passed in the global options, the ``overrides`` are constructed + and `plugable.API.bootstrap` is called. + """ + self.__doing('bootstrap') + self.parse_globals() + self.api.env.verbose = self.options.verbose + if self.options.config_file: + self.api.env.conf = self.options.config_file + overrides = {} + if self.options.environment: + for a in self.options.environment.split(','): + a = a.split('=', 1) + if len(a) < 2: + parser.error('badly specified environment string,'\ + 'use var1=val1[,var2=val2]..') + overrides[a[0].strip()] = a[1].strip() + overrides['context'] = 'cli' + self.api.bootstrap(**overrides) + + def parse_globals(self): + """ + Parse out the global options. + + This method parses the global options out of the ``CLI.argv`` instance + attribute, after which two new instance attributes are available: + + 1. ``CLI.options`` - an ``optparse.Values`` instance containing + the global options. + + 2. ``CLI.cmd_argv`` - a tuple containing the remainder of + ``CLI.argv`` after the global options have been consumed. + """ + self.__doing('parse_globals') + parser = optparse.OptionParser() + parser.disable_interspersed_args() + parser.add_option('-a', dest='prompt_all', action='store_true', + help='Prompt for all missing options interactively') + parser.add_option('-n', dest='interactive', action='store_false', + help='Don\'t prompt for any options interactively') + parser.add_option('-c', dest='config_file', + help='Specify different configuration file') + parser.add_option('-e', dest='environment', + help='Specify or override environment variables') + parser.add_option('-v', dest='verbose', action='store_true', + help='Verbose output') + parser.set_defaults( + prompt_all=False, + interactive=True, + verbose=False, + ) + (options, args) = parser.parse_args(list(self.argv)) + self.options = options + self.cmd_argv = tuple(args) + + def __doing(self, name): + if name in self.__done: + raise StandardError( + '%s.%s() already called' % (self.__class__.__name__, name) + ) + self.__done.add(name) def print_commands(self): std = set(self.api.Command) - set(self.api.Application) @@ -234,66 +395,38 @@ class CLI(object): cmd.doc, ) - def __contains__(self, key): - assert self.__d is not None, 'you must call finalize() first' - return key in self.__d - - def __getitem__(self, key): - assert self.__d is not None, 'you must call finalize() first' - return self.__d[key] - - def finalize(self): - api = self.api - for klass in cli_application_commands: - api.register(klass) - api.finalize() - for a in api.Application(): - a.set_application(self) - self.build_map() - - def build_map(self): - assert self.__d is None - self.__d = dict( - (c.name.replace('_', '-'), c) for c in self.api.Command() - ) - - def run(self): - self.finalize() - set_default_env(self.api.env) - args = self.parse_globals() - if len(args) < 1: - self.print_commands() - print 'Usage: ipa [global-options] COMMAND' - sys.exit(2) - key = args[0] - if key not in self: - self.print_commands() - print 'ipa: ERROR: unknown command %r' % key - sys.exit(2) - return self.run_cmd( - self[key], - list(s.decode('utf-8') for s in args[1:]) - ) - - def run_cmd(self, cmd, argv): - kw = self.parse(cmd, argv) + def run_cmd(self, cmd): + kw = self.parse(cmd) + # If options.interactive, interactively validate params: + if self.options.interactive: + try: + kw = self.prompt_interactively(cmd, kw) + except KeyboardInterrupt: + return 0 + # Now run the command try: - self.run_interactive(cmd, kw) - except KeyboardInterrupt: + ret = cmd(**kw) + if callable(cmd.output_for_cli): + cmd.output_for_cli(ret) return 0 - except errors.RuleError, e: + except StandardError, e: print e return 2 - return 0 - def run_interactive(self, cmd, kw): + def prompt_interactively(self, cmd, kw): + """ + Interactively prompt for missing or invalid values. + + By default this method will only prompt for *required* Param that + have a missing or invalid value. However, if + ``CLI.options.prompt_all`` is True, this method will prompt for any + params that have a missing or required values, even if the param is + optional. + """ for param in cmd.params(): if param.name not in kw: - if not param.required: - if not self.__all_interactive: - continue - elif self.__not_interactive: - exit_error('Not enough arguments given') + if not (param.required or self.options.prompt_all): + continue default = param.get_default(**kw) if default is None: prompt = '%s: ' % param.cli_name @@ -311,29 +444,34 @@ class CLI(object): break except errors.ValidationError, e: error = e.error - if self.api.env.server_context: - try: - import krbV - import ldap - from ipa_server import conn - from ipa_server.servercore import context - krbccache = krbV.default_context().default_ccache().name - context.conn = conn.IPAConn(self.api.env.ldaphost, self.api.env.ldapport, krbccache) - except ImportError: - print >> sys.stderr, "There was a problem importing a Python module: %s" % sys.exc_value - return 2 - except ldap.LDAPError, e: - print >> sys.stderr, "There was a problem connecting to the LDAP server: %s" % e[0].get('desc') - return 2 - ret = cmd(**kw) - if callable(cmd.output_for_cli): - return cmd.output_for_cli(ret) - else: - return 0 + return kw - def parse(self, cmd, argv): +# FIXME: This should be done as the plugins are loaded +# if self.api.env.server_context: +# try: +# import krbV +# import ldap +# from ipa_server import conn +# from ipa_server.servercore import context +# krbccache = krbV.default_context().default_ccache().name +# context.conn = conn.IPAConn(self.api.env.ldaphost, self.api.env.ldapport, krbccache) +# except ImportError: +# print >> sys.stderr, "There was a problem importing a Python module: %s" % sys.exc_value +# return 2 +# except ldap.LDAPError, e: +# print >> sys.stderr, "There was a problem connecting to the LDAP server: %s" % e[0].get('desc') +# return 2 +# ret = cmd(**kw) +# if callable(cmd.output_for_cli): +# return cmd.output_for_cli(ret) +# else: +# return 0 + + def parse(self, cmd): parser = self.build_parser(cmd) - (kwc, args) = parser.parse_args(argv, KWCollector()) + (kwc, args) = parser.parse_args( + list(self.cmd_argv[1:]), KWCollector() + ) kw = kwc.__todict__() try: arg_kw = cmd.args_to_kw(*args) @@ -360,43 +498,6 @@ class CLI(object): parser.add_option(o) return parser - def parse_globals(self, argv=sys.argv[1:]): - parser = optparse.OptionParser() - parser.disable_interspersed_args() - parser.add_option('-a', dest='interactive', action='store_true', - help='Prompt for all missing options interactively') - parser.add_option('-n', dest='interactive', action='store_false', - help='Don\'t prompt for any options interactively') - parser.add_option('-c', dest='config_file', - help='Specify different configuration file') - parser.add_option('-e', dest='environment', - help='Specify or override environment variables') - parser.add_option('-v', dest='verbose', action='store_true', - help='Verbose output') - (options, args) = parser.parse_args(argv) - - if options.interactive == True: - self.__all_interactive = True - elif options.interactive == False: - self.__not_interactive = True - if options.verbose != None: - self.api.env.verbose = True - if options.environment: - env_dict = {} - for a in options.environment.split(','): - a = a.split('=', 1) - if len(a) < 2: - parser.error('badly specified environment string,'\ - 'use var1=val1[,var2=val2]..') - env_dict[a[0].strip()] = a[1].strip() - self.api.env.update(env_dict, True) - if options.config_file: - self.api.env.update(read_config(options.config_file), True) - else: - self.api.env.update(read_config(), True) - - return args - def get_usage(self, cmd): return ' '.join(self.get_usage_iter(cmd)) @@ -421,3 +522,17 @@ class CLI(object): self.__mcl = max(len(k) for k in self.__d) return self.__mcl mcl = property(__get_mcl) + + def isdone(self, name): + """ + Return True in method named ``name`` has already been called. + """ + return name in self.__done + + def __contains__(self, key): + assert self.__d is not None, 'you must call finalize() first' + return key in self.__d + + def __getitem__(self, key): + assert self.__d is not None, 'you must call finalize() first' + return self.__d[key] diff --git a/ipalib/config.py b/ipalib/config.py index 75e009dc..02a3fadd 100644 --- a/ipalib/config.py +++ b/ipalib/config.py @@ -25,11 +25,13 @@ It will also take care of settings that can be discovered by different methods, such as DNS. """ -from ConfigParser import SafeConfigParser, ParsingError +from ConfigParser import SafeConfigParser, ParsingError, RawConfigParser import types import os - +from os import path +import sys from errors import check_isinstance, raise_TypeError +import constants DEFAULT_CONF='/etc/ipa/ipa.conf' @@ -126,6 +128,221 @@ class Environment(object): return default +class Env(object): + """ + A mapping object used to store the environment variables. + """ + + __locked = False + + def __init__(self): + object.__setattr__(self, '_Env__d', {}) + object.__setattr__(self, '_Env__done', set()) + 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') + + 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 initialize only enough environment information to + determine whether ipa is running in-tree, what the context is, + and the location of the configuration file. + """ + self.__doing('_bootstrap') + for (key, value) in overrides.iteritems(): + self[key] = value + 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') + + def _finalize_core(self, **defaults): + """ + Complete initialization of standard IPA environment. + + After this method is called, the all environment variables + used by all the built-in plugins will be available. + + This method should be called before loading any plugins. It will + automatically call `Env._bootstrap()` if it has not yet been called. + + After this method has finished, the `Env` instance is still writable + so that third + """ + self.__doing('_finalize_core') + self.__do_if_not_done('_bootstrap') + self._merge_config(self.conf) + if self.conf_default != self.conf: + self._merge_config(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) + for (key, value) in defaults.iteritems(): + if key not in self: + self[key] = value + + def _finalize(self, **lastchance): + """ + Finalize and lock environment. + + This method should be called after all plugins have bean loaded and + after `plugable.API.finalize()` has been called. + """ + self.__doing('_finalize') + self.__do_if_not_done('_finalize_core') + for (key, value) in lastchance.iteritems(): + if key not in self: + self[key] = value + self.__lock__() + + def _merge_config(self, conf_file): + """ + Merge values from ``conf_file`` into this `Env`. + """ + section = constants.CONFIG_SECTION + if not path.isfile(conf_file): + return + parser = RawConfigParser() + try: + parser.read(conf_file) + except ParsingError: + return + if not parser.has_section(section): + parser.add_section(section) + items = parser.items(section) + if len(items) == 0: + return + i = 0 + for (key, value) in items: + if key not in self: + self[key] = value + i += 1 + return (i, len(items)) + + 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 self.__locked + + def __getattr__(self, name): + """ + Return the attribute named ``name``. + """ + if name in self.__d: + return self[name] + raise AttributeError('%s.%s' % + (self.__class__.__name__, name) + ) + + def __setattr__(self, name, value): + """ + Set the attribute named ``name`` to ``value``. + """ + self[name] = value + + def __delattr__(self, name): + """ + Raise AttributeError (deletion is not allowed). + """ + raise AttributeError('cannot del %s.%s' % + (self.__class__.__name__, name) + ) + + def __getitem__(self, key): + """ + Return the value corresponding to ``key``. + """ + if key not in self.__d: + raise KeyError(key) + value = self.__d[key] + if callable(value): + return value() + return value + + def __setitem__(self, key, value): + """ + Set ``key`` to ``value``. + """ + # FIXME: the key should be checked with check_name() + if self.__locked: + raise AttributeError('locked: cannot set %s.%s to %r' % + (self.__class__.__name__, key, value) + ) + if key in self.__d or hasattr(self, key): + raise AttributeError('cannot overwrite %s.%s with %r' % + (self.__class__.__name__, key, value) + ) + if not callable(value): + if isinstance(value, basestring): + value = str(value.strip()) + if value.lower() == 'true': + value = True + elif value.lower() == 'false': + value = False + elif value.isdigit(): + value = int(value) + assert type(value) in (str, int, bool) + object.__setattr__(self, key, value) + self.__d[key] = value + + def __contains__(self, key): + """ + Return True if instance contains ``key``; otherwise return False. + """ + return key in self.__d + + def __iter__(self): # Fix + """ + Iterate through keys in ascending order. + """ + for key in sorted(self.__d): + yield key + + def set_default_env(env): """ Set default values for ``env``. diff --git a/ipalib/constants.py b/ipalib/constants.py new file mode 100644 index 00000000..f4a440c6 --- /dev/null +++ b/ipalib/constants.py @@ -0,0 +1,106 @@ +# 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 + +""" +All constants centralized in one file. +""" + +# The section to read in the config files, i.e. [global] +CONFIG_SECTION = 'global' + + +# The default configuration for api.env +# This is a tuple instead of a dict so that it is immutable. +# To create a dict with this config, just "d = dict(DEFAULT_CONFIG)". +DEFAULT_CONFIG = ( + # Domain, realm, basedn: + ('domain', 'example.com'), + ('realm', 'EXAMPLE.COM'), + ('basedn', 'dc=example,dc=com'), + + # LDAP containers: + ('container_accounts', 'cn=accounts'), + ('container_user', 'cn=users,cn=accounts'), + ('container_group', 'cn=groups,cn=accounts'), + ('container_service', 'cn=services,cn=accounts'), + ('container_host', 'cn=computers,cn=accounts'), + + # Ports, hosts, and URIs: + ('lite_xmlrpc_port', 8888), + ('lite_webui_port', 9999), + ('xmlrpc_uri', 'http://localhost:8888'), + ('ldap_uri', 'ldap://localhost:389'), + ('ldap_host', 'localhost'), + ('ldap_port', 389), + + # Debugging: + ('verbose', False), + ('debug', False), + ('mode', 'production'), + + # ******************************************************** + # The remaining keys are never set from the values here! + # ******************************************************** + # + # Env.__init__() or Env._bootstrap() or Env._finalize_core() + # will have filled in all the keys below by the time DEFAULT_CONFIG + # is merged in, so the values below are never actually used. They are + # listed both to provide a big picture and also so DEFAULT_CONFIG contains + # the keys that should be present after Env._finalize_core() is called. + # + # The values are all None so if for some reason any of these keys were + # set from the values here, an exception will be raised. + + # Set in Env.__init__(): + ('ipalib', None), # The directory containing ipalib/__init__.py + ('site_packages', None), # The directory contaning ipalib + ('script', None), # sys.argv[0] + ('bin', None), # The directory containing script + ('home', None), # The home directory of user underwhich process is running + ('dot_ipa', None), # ~/.ipa directory + + # Set in Env._bootstrap(): + ('in_tree', None), # Whether or not running in-tree (bool) + ('context', None), # Name of context, default is 'default' + ('conf', None), # Path to config file + ('conf_default', None), # Path to common default config file + + # Set in Env._finalize_core(): + ('in_server', None), # Whether or not running in-server (bool) + ('log', None), # Path to log file + +) + + +LOGGING_CONSOLE_FORMAT = ': '.join([ + '%(name)s', + '%(levelname)s', + '%(message)s', +]) + + +# Tab-delimited format designed to be easily opened in a spreadsheet: +LOGGING_FILE_FORMAT = ' '.join([ + '%(created)f', + '%(levelname)s', + '%(message)r', # Using %r for repr() so message is a single line + '%(pathname)s', + '%(lineno)d', +]) diff --git a/ipalib/frontend.py b/ipalib/frontend.py index d918dd83..62a503cc 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -668,7 +668,7 @@ class Command(plugable.Plugin): on the nearest IPA server and the actual work this command performs is executed remotely. """ - if self.api.env.server_context: + if self.api.env.in_server: target = self.execute else: target = self.forward diff --git a/ipalib/load_plugins.py b/ipalib/load_plugins.py deleted file mode 100644 index 4e02f5ba..00000000 --- a/ipalib/load_plugins.py +++ /dev/null @@ -1,82 +0,0 @@ -# Authors: -# 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 - -""" -Importing this module causes the plugins to be loaded. - -This is not in __init__.py so that importing ipalib or its other sub-modules -does not cause unnecessary side effects. - -Eventually this will also load the out-of tree plugins, but for now it just -loads the internal plugins. -""" - -import os -from os import path -import imp -import inspect - - -def find_modules_in_dir(src_dir): - """ - Iterate through module names found in ``src_dir``. - """ - if not (path.abspath(src_dir) == src_dir and path.isdir(src_dir)): - return - if path.islink(src_dir): - return - suffix = '.py' - for name in sorted(os.listdir(src_dir)): - if not name.endswith(suffix): - continue - py_file = path.join(src_dir, name) - if path.islink(py_file) or not path.isfile(py_file): - continue - module = name[:-len(suffix)] - if module == '__init__': - continue - yield module - - -def load_plugins_in_dir(src_dir): - """ - Import each Python module found in ``src_dir``. - """ - for module in find_modules_in_dir(src_dir): - imp.load_module(module, *imp.find_module(module, [src_dir])) - - -def import_plugins(name): - """ - Load all plugins found in standard 'plugins' sub-package. - """ - try: - plugins = __import__(name + '.plugins').plugins - except ImportError: - return - src_dir = path.dirname(path.abspath(plugins.__file__)) - for name in find_modules_in_dir(src_dir): - full_name = '%s.%s' % (plugins.__name__, name) - __import__(full_name) - - -for name in ['ipalib', 'ipa_server', 'ipa_not_a_package']: - import_plugins(name) - -load_plugins_in_dir(path.expanduser('~/.freeipa')) diff --git a/ipalib/plugable.py b/ipalib/plugable.py index fd87586d..b0ba32b7 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -29,7 +29,9 @@ import re import inspect import errors from errors import check_type, check_isinstance -from config import Environment +from config import Environment, Env +import constants +import util class ReadOnly(object): @@ -707,19 +709,67 @@ class API(DictProxy): """ Dynamic API object through which `Plugin` instances are accessed. """ - __finalized = False def __init__(self, *allowed): self.__d = dict() + self.__done = set() self.register = Registrar(*allowed) - self.env = Environment() + self.env = Env() super(API, self).__init__(self.__d) + 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 environment variables needed by built-in plugins. + """ + self.__doing('bootstrap') + self.env._bootstrap(**overrides) + self.env._finalize_core(**dict(constants.DEFAULT_CONFIG)) + if self.env.mode == 'unit_test': + return + logger = util.configure_logging( + self.env.log, + self.env.verbose, + ) + object.__setattr__(self, 'logger', logger) + + def load_plugins(self): + """ + Load plugins from all standard locations. + + `API.bootstrap` will automatically be called if it hasn't been + already. + """ + self.__doing('load_plugins') + self.__do_if_not_done('bootstrap') + if self.env.mode == 'unit_test': + return + util.import_plugins_subpackage('ipalib') + if self.env.in_server: + util.import_plugins_subpackage('ipa_server') + def finalize(self): """ Finalize the registration, instantiate the plugins. + + `API.bootstrap` will automatically be called if it hasn't been + already. """ - assert not self.__finalized, 'finalize() can only be called once' + self.__doing('finalize') + self.__do_if_not_done('bootstrap') class PluginInstance(object): """ diff --git a/ipalib/plugins/b_xmlrpc.py b/ipalib/plugins/b_xmlrpc.py index 572a7511..2c98fb8a 100644 --- a/ipalib/plugins/b_xmlrpc.py +++ b/ipalib/plugins/b_xmlrpc.py @@ -36,23 +36,26 @@ from ipalib import errors class xmlrpc(Backend): """ - Kerberos backend plugin. + XML-RPC client backend plugin. """ - def get_client(self, verbose=False): - # FIXME: The server uri should come from self.api.env.server_uri - if api.env.get('kerberos'): - server = api.env.server.next() - if verbose: print "Connecting to %s" % server - return xmlrpclib.ServerProxy('https://%s/ipa/xml' % server, transport=KerbTransport(), verbose=verbose) - else: - return xmlrpclib.ServerProxy('http://localhost:8888', verbose=verbose) + def get_client(self): + """ + Return an xmlrpclib.ServerProxy instance (the client). + """ + uri = self.api.env.xmlrpc_uri + if uri.startswith('https://'): + return xmlrpclib.ServerProxy(uri, + transport=KerbTransport(), + verbose=self.api.env.verbose, + ) + return xmlrpclib.ServerProxy(uri, verbose=self.api.env.verbose) def forward_call(self, name, *args, **kw): """ Forward a call over XML-RPC to an IPA server. """ - client = self.get_client(verbose=api.env.get('verbose', False)) + client = self.get_client() command = getattr(client, name) params = xmlrpc_marshal(*args, **kw) try: diff --git a/ipalib/util.py b/ipalib/util.py index 184c6d7c..d577524b 100644 --- a/ipalib/util.py +++ b/ipalib/util.py @@ -20,7 +20,14 @@ """ Various utility functions. """ + +import logging +import os +from os import path +import imp import krbV +from constants import LOGGING_CONSOLE_FORMAT, LOGGING_FILE_FORMAT + def xmlrpc_marshal(*args, **kw): """ @@ -41,6 +48,7 @@ def xmlrpc_unmarshal(*params): kw = {} return (params[1:], kw) + def get_current_principal(): try: return krbV.default_context().default_ccache().principal().name @@ -48,3 +56,81 @@ def get_current_principal(): #TODO: do a kinit print "Unable to get kerberos principal" return None + + +# FIXME: This function has no unit test +def find_modules_in_dir(src_dir): + """ + Iterate through module names found in ``src_dir``. + """ + if not (path.abspath(src_dir) == src_dir and path.isdir(src_dir)): + return + if path.islink(src_dir): + return + suffix = '.py' + for name in sorted(os.listdir(src_dir)): + if not name.endswith(suffix): + continue + py_file = path.join(src_dir, name) + if path.islink(py_file) or not path.isfile(py_file): + continue + module = name[:-len(suffix)] + if module == '__init__': + continue + yield module + + +# FIXME: This function has no unit test +def load_plugins_in_dir(src_dir): + """ + Import each Python module found in ``src_dir``. + """ + for module in find_modules_in_dir(src_dir): + imp.load_module(module, *imp.find_module(module, [src_dir])) + + +# FIXME: This function has no unit test +def import_plugins_subpackage(name): + """ + Import everythig in ``plugins`` sub-package of package named ``name``. + """ + try: + plugins = __import__(name + '.plugins').plugins + except ImportError: + return + src_dir = path.dirname(path.abspath(plugins.__file__)) + for name in find_modules_in_dir(src_dir): + full_name = '%s.%s' % (plugins.__name__, name) + __import__(full_name) + + +def configure_logging(log_file, verbose): + """ + Configure standard logging. + """ + # Set logging level: + level = logging.INFO + if verbose: + level -= 10 + + log = logging.getLogger('ipa') + log.setLevel(level) + + # Configure console handler + console = logging.StreamHandler() + console.setFormatter(logging.Formatter(LOGGING_CONSOLE_FORMAT)) + log.addHandler(console) + + # Configure file handler + log_dir = path.dirname(log_file) + if not path.isdir(log_dir): + try: + os.makedirs(log_dir) + except OSError: + log.warn('Could not create log_dir %r', log_dir) + return log + file_handler = logging.FileHandler(log_file) + file_handler.setFormatter(logging.Formatter(LOGGING_FILE_FORMAT)) + log.addHandler(file_handler) + + return log diff --git a/lite-webui.py b/lite-webui.py index ccef77ed..e75e5d9d 100755 --- a/lite-webui.py +++ b/lite-webui.py @@ -27,9 +27,8 @@ from cherrypy import expose, config, quickstart from ipa_webui.templates import form, main from ipa_webui import controller from ipalib import api -from ipalib import load_plugins - +api.load_plugins() api.finalize() @@ -42,5 +41,5 @@ class root(object): setattr(self, cmd.name, ctr) -if __name__ == '__main__' +if __name__ == '__main__': quickstart(root()) diff --git a/lite-xmlrpc.py b/lite-xmlrpc.py index 7e9c69a8..3483ceb5 100755 --- a/lite-xmlrpc.py +++ b/lite-xmlrpc.py @@ -34,14 +34,10 @@ from ipalib import api from ipalib import config from ipa_server import conn from ipa_server.servercore import context -import ipalib.load_plugins from ipalib.util import xmlrpc_unmarshal import traceback import krbV - -PORT=8888 - class StoppableXMLRPCServer(SimpleXMLRPCServer.SimpleXMLRPCServer): """Override of TIME_WAIT""" allow_reuse_address = True @@ -65,31 +61,25 @@ class LoggingSimpleXMLRPCRequestHandler(SimpleXMLRPCServer.SimpleXMLRPCRequestHa return (args, kw) def _dispatch(self, method, params): - """Dispatches the XML-RPC method. + """ + Dispatches the XML-RPC method. Methods beginning with an '_' are considered private and will not be called. """ - + if method not in funcs: + logger.error('no such method %r', method) + raise Exception('method "%s" is not supported' % method) + func = funcs[method] krbccache = krbV.default_context().default_ccache().name - - func = None - try: - try: - # check to see if a matching function has been registered - func = funcs[method] - except KeyError: - raise Exception('method "%s" is not supported' % method) - (args, kw) = xmlrpc_unmarshal(*params) - # FIXME: don't hardcode host and port - context.conn = conn.IPAConn(api.env.ldaphost, api.env.ldapport, krbccache) - logger.info("calling %s" % method) - return func(*args, **kw) - finally: - # Clean up any per-request data and connections -# for k in context.__dict__.keys(): -# del context.__dict__[k] - pass + context.conn = conn.IPAConn( + api.env.ldap_host, + api.env.ldap_port, + krbccache, + ) + logger.info('calling %s', method) + (args, kw) = xmlrpc_unmarshal(*params) + return func(*args, **kw) def _marshaled_dispatch(self, data, dispatch_method = None): try: @@ -117,7 +107,7 @@ class LoggingSimpleXMLRPCRequestHandler(SimpleXMLRPCServer.SimpleXMLRPCRequestHa def do_POST(self): clientIP, port = self.client_address - # Log client IP and Port + # Log client IP and Port logger.info('Client IP: %s - Port: %s' % (clientIP, port)) try: # get arguments @@ -127,14 +117,14 @@ class LoggingSimpleXMLRPCRequestHandler(SimpleXMLRPCServer.SimpleXMLRPCRequestHa params, method = xmlrpclib.loads(data) # Log client request - logger.info('Client request: \n%s\n' % data) + logger.info('Client request: \n%s\n' % data) response = self._marshaled_dispatch( data, getattr(self, '_dispatch', None)) - # Log server response + # Log server response logger.info('Server response: \n%s\n' % response) - except Exception, e: + except Exception, e: # This should only happen if the module is buggy # internal error, report as HTTP server error print e @@ -154,37 +144,29 @@ class LoggingSimpleXMLRPCRequestHandler(SimpleXMLRPCServer.SimpleXMLRPCRequestHa if __name__ == '__main__': - # Set up our logger - logger = logging.getLogger('xmlrpcserver') - hdlr = logging.FileHandler('xmlrpcserver.log') - formatter = logging.Formatter("%(asctime)s %(levelname)s %(message)s") - hdlr.setFormatter(formatter) - logger.addHandler(hdlr) - logger.setLevel(logging.INFO) + api.bootstrap(context='server') + api.load_plugins() + api.finalize() + logger = api.logger # Set up the server - XMLRPCServer = StoppableXMLRPCServer(("",PORT), LoggingSimpleXMLRPCRequestHandler) - + XMLRPCServer = StoppableXMLRPCServer( + ('', api.env.lite_xmlrpc_port), + LoggingSimpleXMLRPCRequestHandler + ) XMLRPCServer.register_introspection_functions() - api.finalize() - - # Initialize our environment - config.set_default_env(api.env) - env_dict = config.read_config() - env_dict['server_context'] = True - api.env.update(env_dict) - # Get and register all the methods + for cmd in api.Command: - logger.info("registering %s" % cmd) + logger.debug('registering %s', cmd) XMLRPCServer.register_function(api.Command[cmd], cmd) - funcs = XMLRPCServer.funcs - print "Listening on port %d" % PORT + logger.info('Logging to file %r', api.env.log) + logger.info('Listening on port %d', api.env.lite_xmlrpc_port) try: XMLRPCServer.serve_forever() except KeyboardInterrupt: XMLRPCServer.server_close() - print "Server shutdown." + logger.info('Server shutdown.') @@ -3,6 +3,7 @@ # Script to run nosetests under multiple versions of Python versions="python2.4 python2.5 python2.6" +versions="python2.5 python2.6" for name in $versions do diff --git a/tests/test_ipalib/test_cli.py b/tests/test_ipalib/test_cli.py index 50bfb932..7b3239d7 100644 --- a/tests/test_ipalib/test_cli.py +++ b/tests/test_ipalib/test_cli.py @@ -22,7 +22,8 @@ Test the `ipalib.cli` module. """ from tests.util import raises, getitem, no_set, no_del, read_only, ClassChecker -from ipalib import cli, plugable +from tests.util import TempHome +from ipalib import cli, plugable, frontend, backend def test_to_cli(): @@ -75,66 +76,185 @@ class DummyAPI(object): pass +config_cli = """ +[global] + +from_cli_conf = set in cli.conf +""" + +config_default = """ +[global] + +from_default_conf = set in default.conf + +# Make sure cli.conf is loaded first: +from_cli_conf = overridden in default.conf +""" + + + + class test_CLI(ClassChecker): """ Test the `ipalib.cli.CLI` class. """ _cls = cli.CLI - def test_class(self): + def new(self, argv=tuple()): + home = TempHome() + api = plugable.API( + frontend.Command, + frontend.Object, + frontend.Method, + frontend.Property, + frontend.Application, + backend.Backend, + ) + api.env.mode = 'unit_test' + api.env.in_tree = True + o = self.cls(api, argv) + assert o.api is api + return (o, api, home) + + def check_cascade(self, *names): + (o, api, home) = self.new() + method = getattr(o, names[0]) + for name in names: + assert o.isdone(name) is False + method() + for name in names: + assert o.isdone(name) is True + e = raises(StandardError, method) + assert str(e) == 'CLI.%s() already called' % names[0] + + def test_init(self): + """ + Test the `ipalib.cli.CLI.__init__` method. + """ + argv = ['-v', 'user-add', '--first=Jonh', '--last=Doe'] + (o, api, home) = self.new(argv) + assert o.api is api + assert o.argv == tuple(argv) + + def test_run(self): """ - Test the `ipalib.cli.CLI` class. + Test the `ipalib.cli.CLI.run` method. """ - assert type(self.cls.api) is property + self.check_cascade( + 'run', + 'finalize', + 'load_plugins', + 'bootstrap', + 'parse_globals' + ) - def test_api(self): + def test_finalize(self): """ - Test the `ipalib.cli.CLI.api` property. + Test the `ipalib.cli.CLI.finalize` method. """ - api = 'the plugable.API instance' - o = self.cls(api) - assert read_only(o, 'api') is api + self.check_cascade( + 'finalize', + 'load_plugins', + 'bootstrap', + 'parse_globals' + ) - def dont_parse(self): + (o, api, home) = self.new() + assert api.isdone('finalize') is False + assert 'Command' not in api + o.finalize() + assert api.isdone('finalize') is True + assert list(api.Command) == \ + sorted(k.__name__ for k in cli.cli_application_commands) + + def test_load_plugins(self): """ - Test the `ipalib.cli.CLI.parse` method. + Test the `ipalib.cli.CLI.load_plugins` method. """ - o = self.cls(None) - args = ['hello', 'naughty', 'nurse'] - kw = dict( - first_name='Naughty', - last_name='Nurse', + self.check_cascade( + 'load_plugins', + 'bootstrap', + 'parse_globals' ) - opts = ['--%s=%s' % (k.replace('_', '-'), v) for (k, v) in kw.items()] - assert o.parse(args + []) == (args, {}) - assert o.parse(opts + []) == ([], kw) - assert o.parse(args + opts) == (args, kw) - assert o.parse(opts + args) == (args, kw) + (o, api, home) = self.new() + assert api.isdone('load_plugins') is False + o.load_plugins() + assert api.isdone('load_plugins') is True - def test_mcl(self): + def test_bootstrap(self): """ - Test the `ipalib.cli.CLI.mcl` property . + Test the `ipalib.cli.CLI.bootstrap` method. """ - cnt = 100 - api = DummyAPI(cnt) - len(api.Command) == cnt - o = self.cls(api) - assert o.mcl is None - o.build_map() - assert o.mcl == 6 # len('cmd_99') - - def test_dict(self): + self.check_cascade( + 'bootstrap', + 'parse_globals' + ) + # Test with empty argv + (o, api, home) = self.new() + keys = tuple(api.env) + assert api.isdone('bootstrap') is False + o.bootstrap() + assert api.isdone('bootstrap') is True + e = raises(StandardError, o.bootstrap) + assert str(e) == 'CLI.bootstrap() already called' + assert api.env.verbose is False + assert api.env.context == 'cli' + keys = tuple(api.env) + added = ( + 'my_key', + 'whatever', + 'from_default_conf', + 'from_cli_conf' + ) + for key in added: + assert key not in api.env + assert key not in keys + + # Test with a populated argv + argv = ['-e', 'my_key=my_val,whatever=Hello'] + (o, api, home) = self.new(argv) + home.write(config_default, '.ipa', 'default.conf') + home.write(config_cli, '.ipa', 'cli.conf') + o.bootstrap() + assert api.env.my_key == 'my_val' + assert api.env.whatever == 'Hello' + assert api.env.from_default_conf == 'set in default.conf' + assert api.env.from_cli_conf == 'set in cli.conf' + assert list(api.env) == sorted(keys + added) + + def test_parse_globals(self): """ - Test container emulation of `ipalib.cli.CLI` class. + Test the `ipalib.cli.CLI.parse_globals` method. """ - cnt = 25 - api = DummyAPI(cnt) - assert len(api.Command) == cnt - o = self.cls(api) - o.build_map() - for cmd in api.Command(): - key = cli.to_cli(cmd.name) - assert key in o - assert o[key] is cmd - assert cmd.name not in o - raises(KeyError, getitem, o, cmd.name) + # Test with empty argv + (o, api, home) = self.new() + assert not hasattr(o, 'options') + assert not hasattr(o, 'cmd_argv') + assert o.isdone('parse_globals') is False + o.parse_globals() + assert o.isdone('parse_globals') is True + assert o.options.interactive is True + assert o.options.verbose is False + assert o.options.config_file is None + assert o.options.environment is None + assert o.cmd_argv == tuple() + e = raises(StandardError, o.parse_globals) + assert str(e) == 'CLI.parse_globals() already called' + + # Test with a populated argv + argv = ('-a', '-n', '-v', '-c', '/my/config.conf', '-e', 'my_key=my_val') + cmd_argv = ('user-add', '--first', 'John', '--last', 'Doe') + (o, api, home) = self.new(argv + cmd_argv) + assert not hasattr(o, 'options') + assert not hasattr(o, 'cmd_argv') + assert o.isdone('parse_globals') is False + o.parse_globals() + assert o.isdone('parse_globals') is True + assert o.options.prompt_all is True + assert o.options.interactive is False + assert o.options.verbose is True + assert o.options.config_file == '/my/config.conf' + assert o.options.environment == 'my_key=my_val' + assert o.cmd_argv == cmd_argv + e = raises(StandardError, o.parse_globals) + assert str(e) == 'CLI.parse_globals() already called' diff --git a/tests/test_ipalib/test_config.py b/tests/test_ipalib/test_config.py index ed982a73..ddfbb708 100644 --- a/tests/test_ipalib/test_config.py +++ b/tests/test_ipalib/test_config.py @@ -22,10 +22,13 @@ Test the `ipalib.config` module. """ import types - -from tests.util import raises, setitem, delitem -#from tests.util import getitem, setitem, delitem -from ipalib import config +import os +from os import path +import sys +from tests.util import raises, setitem, delitem, ClassChecker +from tests.util import getitem, setitem, delitem +from tests.util import TempDir, TempHome +from ipalib import config, constants def test_Environment(): @@ -112,6 +115,450 @@ def test_Environment(): assert env.a != 1000 +# Random base64-encoded data to simulate a misbehaving config file. +config_bad = """ +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 +""" + +# A config file that tries to override some standard vars: +config_override = """ +[global] + +key0 = var0 +home = /home/sweet/home +key1 = var1 +site_packages = planet +key2 = var2 +key3 = var3 +""" + +# A config file that tests the automatic type conversion +config_good = """ +[global] + +yes = tRUE +no = fALse +number = 42 +""" + +# A default config file to make sure it does not overwrite the explicit one +config_default = """ +[global] + +yes = Hello +not_in_other = foo_bar +""" + + +class test_Env(ClassChecker): + """ + Test the `ipalib.config.Env` class. + """ + + _cls = config.Env + + def new(self): + """ + Set os.environ['HOME'] to a tempdir. + + Returns tuple with new Env instance and the TempHome instance. + """ + home = TempHome() + return (self.cls(), home) + + def test_init(self): + """ + Test the `ipalib.config.Env.__init__` method. + """ + (o, home) = self.new() + ipalib = path.dirname(path.abspath(config.__file__)) + assert o.ipalib == ipalib + assert o.site_packages == path.dirname(ipalib) + assert o.script == path.abspath(sys.argv[0]) + assert o.bin == path.dirname(path.abspath(sys.argv[0])) + assert o.home == home.path + assert o.dot_ipa == home.join('.ipa') + + def bootstrap(self, **overrides): + (o, home) = self.new() + assert o._isdone('_bootstrap') is False + o._bootstrap(**overrides) + assert o._isdone('_bootstrap') is True + e = raises(StandardError, o._bootstrap) + assert str(e) == 'Env._bootstrap() already called' + return (o, home) + + def test_bootstrap(self): + """ + Test the `ipalib.config.Env._bootstrap` method. + """ + # Test defaults created by _bootstrap(): + (o, home) = self.new() + assert 'in_tree' not in o + assert 'context' not in o + assert 'conf' not in o + o._bootstrap() + assert o.in_tree is False + assert o.context == 'default' + assert o.conf == '/etc/ipa/default.conf' + assert o.conf_default == o.conf + + # Test overriding values created by _bootstrap() + (o, home) = self.bootstrap(in_tree='true', context='server') + assert o.in_tree is True + assert o.context == 'server' + assert o.conf == home.join('.ipa', 'server.conf') + (o, home) = self.bootstrap(conf='/my/wacky/whatever.conf') + assert o.in_tree is False + assert o.context == 'default' + assert o.conf == '/my/wacky/whatever.conf' + assert o.conf_default == '/etc/ipa/default.conf' + (o, home) = self.bootstrap(conf_default='/my/wacky/default.conf') + assert o.in_tree is False + assert o.context == 'default' + assert o.conf == '/etc/ipa/default.conf' + assert o.conf_default == '/my/wacky/default.conf' + + # Test various overrides and types conversion + kw = dict( + yes=True, + no=False, + num=42, + msg='Hello, world!', + ) + override = dict( + (k, u' %s ' % v) for (k, v) in kw.items() + ) + (o, home) = self.new() + for key in kw: + assert key not in o + o._bootstrap(**override) + for (key, value) in kw.items(): + assert getattr(o, key) == value + assert o[key] == value + + def finalize_core(self, **defaults): + (o, home) = self.new() + assert o._isdone('_finalize_core') is False + o._finalize_core(**defaults) + assert o._isdone('_finalize_core') is True + e = raises(StandardError, o._finalize_core) + assert str(e) == 'Env._finalize_core() already called' + return (o, home) + + def test_finalize_core(self): + """ + Test the `ipalib.config.Env._finalize_core` method. + """ + # Check that calls cascade up the chain: + (o, home) = self.new() + assert o._isdone('_bootstrap') is False + assert o._isdone('_finalize_core') is False + assert o._isdone('_finalize') is False + o._finalize_core() + assert o._isdone('_bootstrap') is True + assert o._isdone('_finalize_core') is True + assert o._isdone('_finalize') is False + + # Check that it can't be called twice: + e = raises(StandardError, o._finalize_core) + assert str(e) == 'Env._finalize_core() already called' + + # Check that _bootstrap() did its job: + (o, home) = self.bootstrap() + assert 'in_tree' in o + assert 'conf' in o + assert 'context' in o + + # Check that keys _finalize_core() will set are not set yet: + assert 'log' not in o + assert 'in_server' not in o + + # Check that _finalize_core() did its job: + o._finalize_core() + assert 'in_server' in o + assert 'log' in o + assert o.in_tree is False + assert o.context == 'default' + assert o.in_server is False + assert o.log == '/var/log/ipa/default.log' + + # Check log is in ~/.ipa/log when context='cli' + (o, home) = self.bootstrap(context='cli') + o._finalize_core() + assert o.in_tree is False + assert o.log == home.join('.ipa', 'log', 'cli.log') + + # Check **defaults can't set in_server nor log: + (o, home) = self.bootstrap(in_server='tRUE') + o._finalize_core(in_server=False) + assert o.in_server is True + (o, home) = self.bootstrap(log='/some/silly/log') + o._finalize_core(log='/a/different/log') + assert o.log == '/some/silly/log' + + # Test loading config file, plus test some in-tree stuff + (o, home) = self.bootstrap(in_tree=True, context='server') + for key in ('yes', 'no', 'number'): + assert key not in o + home.write(config_good, '.ipa', 'server.conf') + home.write(config_default, '.ipa', 'default.conf') + o._finalize_core() + assert o.in_tree is True + assert o.context == 'server' + assert o.in_server is True + assert o.log == home.join('.ipa', 'log', 'server.log') + assert o.yes is True + assert o.no is False + assert o.number == 42 + assert o.not_in_other == 'foo_bar' + + # Test using DEFAULT_CONFIG: + defaults = dict(constants.DEFAULT_CONFIG) + (o, home) = self.finalize_core(**defaults) + assert list(o) == sorted(defaults) + for (key, value) in defaults.items(): + if value is None: + continue + assert o[key] is value + + def test_finalize(self): + """ + Test the `ipalib.config.Env._finalize` method. + """ + # Check that calls cascade up the chain: + (o, home) = self.new() + assert o._isdone('_bootstrap') is False + assert o._isdone('_finalize_core') is False + assert o._isdone('_finalize') is False + o._finalize() + assert o._isdone('_bootstrap') is True + assert o._isdone('_finalize_core') is True + assert o._isdone('_finalize') is True + + # Check that it can't be called twice: + e = raises(StandardError, o._finalize) + assert str(e) == 'Env._finalize() already called' + + # Check that _finalize() calls __lock__() + (o, home) = self.new() + assert o.__islocked__() is False + o._finalize() + assert o.__islocked__() is True + e = raises(StandardError, o.__lock__) + assert str(e) == 'Env.__lock__() already called' + + # Check that **lastchance works + (o, home) = self.finalize_core() + key = 'just_one_more_key' + value = 'with one more value' + lastchance = {key: value} + assert key not in o + assert o._isdone('_finalize') is False + o._finalize(**lastchance) + assert key in o + assert o[key] is value + + def test_merge_config(self): + """ + Test the `ipalib.config.Env._merge_config` method. + """ + tmp = TempDir() + assert callable(tmp.join) + + # Test a config file that doesn't exist + no_exist = tmp.join('no_exist.conf') + assert not path.exists(no_exist) + o = self.cls() + keys = tuple(o) + orig = dict((k, o[k]) for k in o) + assert o._merge_config(no_exist) is None + assert tuple(o) == keys + + # Test an empty config file + empty = tmp.touch('empty.conf') + assert path.isfile(empty) + assert o._merge_config(empty) is None + assert tuple(o) == keys + + # Test a mal-formed config file: + bad = tmp.join('bad.conf') + open(bad, 'w').write(config_bad) + assert path.isfile(bad) + assert o._merge_config(bad) is None + assert tuple(o) == keys + + # Test a valid config file that tries to override + override = tmp.join('override.conf') + open(override, 'w').write(config_override) + assert path.isfile(override) + assert o._merge_config(override) == (4, 6) + for (k, v) in orig.items(): + assert o[k] is v + assert list(o) == sorted(keys + ('key0', 'key1', 'key2', 'key3')) + for i in xrange(4): + assert o['key%d' % i] == ('var%d' % i) + keys = tuple(o) + + # Test a valid config file with type conversion + good = tmp.join('good.conf') + open(good, 'w').write(config_good) + assert path.isfile(good) + assert o._merge_config(good) == (3, 3) + assert list(o) == sorted(keys + ('yes', 'no', 'number')) + assert o.yes is True + assert o.no is False + assert o.number == 42 + + def test_lock(self): + """ + Test the `ipalib.config.Env.__lock__` method. + """ + o = self.cls() + assert o._Env__locked is False + o.__lock__() + assert o._Env__locked is True + e = raises(StandardError, o.__lock__) + assert str(e) == 'Env.__lock__() already called' + + def test_getattr(self): + """ + Test the `ipalib.config.Env.__getattr__` method. + + Also tests the `ipalib.config.Env.__getitem__` method. + """ + o = self.cls() + value = 'some value' + o.key = value + assert o.key is value + assert o['key'] is value + o.call = lambda: 'whatever' + assert o.call == 'whatever' + assert o['call'] == 'whatever' + for name in ('one', 'two'): + e = raises(AttributeError, getattr, o, name) + assert str(e) == 'Env.%s' % name + e = raises(KeyError, getitem, o, name) + assert str(e) == repr(name) + + def test_setattr(self): + """ + Test the `ipalib.config.Env.__setattr__` method. + + Also tests the `ipalib.config.Env.__setitem__` method. + """ + items = [ + ('one', 1), + ('two', lambda: 2), + ('three', 3), + ('four', lambda: 4), + ] + for setvar in (setattr, setitem): + o = self.cls() + for (i, (name, value)) in enumerate(items): + setvar(o, name, value) + assert getattr(o, name) == i + 1 + assert o[name] == i + 1 + if callable(value): + assert name not in dir(o) + else: + assert name in dir(o) + e = raises(AttributeError, setvar, o, name, 42) + assert str(e) == 'cannot overwrite Env.%s with 42' % name + o = self.cls() + o.__lock__() + for (name, value) in items: + e = raises(AttributeError, setvar, o, name, value) + assert str(e) == \ + 'locked: cannot set Env.%s to %r' % (name, value) + o = self.cls() + setvar(o, 'yes', ' true ') + assert o.yes is True + setvar(o, 'no', ' false ') + assert o.no is False + setvar(o, 'msg', u' Hello, world! ') + assert o.msg == 'Hello, world!' + assert type(o.msg) is str + setvar(o, 'num', ' 42 ') + assert o.num == 42 + + def test_delattr(self): + """ + Test the `ipalib.config.Env.__delattr__` method. + + This also tests that ``__delitem__`` is not implemented. + """ + o = self.cls() + o.one = 1 + assert o.one == 1 + for key in ('one', 'two'): + e = raises(AttributeError, delattr, o, key) + assert str(e) == 'cannot del Env.%s' % key + e = raises(AttributeError, delitem, o, key) + assert str(e) == '__delitem__' + + def test_contains(self): + """ + Test the `ipalib.config.Env.__contains__` method. + """ + o = self.cls() + items = [ + ('one', 1), + ('two', lambda: 2), + ('three', 3), + ('four', lambda: 4), + ] + for (key, value) in items: + assert key not in o + o[key] = value + assert key in o + + def test_iter(self): + """ + Test the `ipalib.config.Env.__iter__` method. + """ + o = self.cls() + default_keys = tuple(o) + keys = ('one', 'two', 'three', 'four', 'five') + for key in keys: + o[key] = 'the value' + assert list(o) == sorted(keys + default_keys) + + def test_set_default_env(): """ Test the `ipalib.config.set_default_env` function. diff --git a/tests/test_ipalib/test_crud.py b/tests/test_ipalib/test_crud.py index 794921aa..421eaca8 100644 --- a/tests/test_ipalib/test_crud.py +++ b/tests/test_ipalib/test_crud.py @@ -40,7 +40,7 @@ class CrudChecker(ClassChecker): frontend.Method, frontend.Property, ) - config.set_default_env(api.env) + #config.set_default_env(api.env) class user(frontend.Object): takes_params = ( 'givenname', diff --git a/tests/test_ipalib/test_frontend.py b/tests/test_ipalib/test_frontend.py index 966b5e93..5a678b5b 100644 --- a/tests/test_ipalib/test_frontend.py +++ b/tests/test_ipalib/test_frontend.py @@ -764,7 +764,8 @@ class test_Command(ClassChecker): # Test in server context: api = plugable.API(self.cls) - api.env.update(dict(server_context=True)) + #api.env.update(dict(server_context=True)) + api.env.in_server = True api.finalize() o = my_cmd() o.set_api(api) @@ -774,7 +775,8 @@ class test_Command(ClassChecker): # Test in non-server context api = plugable.API(self.cls) - api.env.update(dict(server_context=False)) + #api.env.update(dict(server_context=False)) + api.env.in_server = False api.finalize() o = my_cmd() o.set_api(api) @@ -907,7 +909,7 @@ class test_Object(ClassChecker): frontend.Method, frontend.Property, ) - config.set_default_env(api.env) + #config.set_default_env(api.env) api.finalize() # Test with no primary keys: @@ -964,7 +966,7 @@ class test_Object(ClassChecker): frontend.Property, backend.Backend, ) - config.set_default_env(api.env) + #config.set_default_env(api.env) class ldap(backend.Backend): whatever = 'It worked!' api.register(ldap) diff --git a/tests/test_ipalib/test_plugable.py b/tests/test_ipalib/test_plugable.py index 61011797..c7b8abbd 100644 --- a/tests/test_ipalib/test_plugable.py +++ b/tests/test_ipalib/test_plugable.py @@ -23,7 +23,7 @@ Test the `ipalib.plugable` module. from tests.util import raises, no_set, no_del, read_only from tests.util import getitem, setitem, delitem -from tests.util import ClassChecker +from tests.util import ClassChecker, TempHome from ipalib import plugable, errors @@ -764,99 +764,147 @@ def test_Registrar(): assert issubclass(klass, base) -def test_API(): +class test_API(ClassChecker): """ Test the `ipalib.plugable.API` class. """ - assert issubclass(plugable.API, plugable.ReadOnly) - # Setup the test bases, create the API: - class base0(plugable.Plugin): - __public__ = frozenset(( - 'method', - )) + _cls = plugable.API - def method(self, n): - return n + def new(self, *bases): + home = TempHome() + api = self.cls(*bases) + api.env.mode = 'unit_test' + api.env.in_tree = True + return (api, home) - class base1(plugable.Plugin): - __public__ = frozenset(( - 'method', - )) + def test_API(self): + """ + Test the `ipalib.plugable.API` class. + """ + assert issubclass(plugable.API, plugable.ReadOnly) - def method(self, n): - return n + 1 + # Setup the test bases, create the API: + class base0(plugable.Plugin): + __public__ = frozenset(( + 'method', + )) - api = plugable.API(base0, base1) - r = api.register - assert isinstance(r, plugable.Registrar) - assert read_only(api, 'register') is r + def method(self, n): + return n - class base0_plugin0(base0): - pass - r(base0_plugin0) + class base1(plugable.Plugin): + __public__ = frozenset(( + 'method', + )) - class base0_plugin1(base0): - pass - r(base0_plugin1) + def method(self, n): + return n + 1 - class base0_plugin2(base0): - pass - r(base0_plugin2) + api = plugable.API(base0, base1) + r = api.register + assert isinstance(r, plugable.Registrar) + assert read_only(api, 'register') is r - class base1_plugin0(base1): - pass - r(base1_plugin0) + class base0_plugin0(base0): + pass + r(base0_plugin0) - class base1_plugin1(base1): - pass - r(base1_plugin1) + class base0_plugin1(base0): + pass + r(base0_plugin1) - class base1_plugin2(base1): - pass - r(base1_plugin2) - - # Test API instance: - api.finalize() - - def get_base(b): - return 'base%d' % b - - def get_plugin(b, p): - return 'base%d_plugin%d' % (b, p) - - for b in xrange(2): - base_name = get_base(b) - ns = getattr(api, base_name) - assert isinstance(ns, plugable.NameSpace) - assert read_only(api, base_name) is ns - assert len(ns) == 3 - for p in xrange(3): - plugin_name = get_plugin(b, p) - proxy = ns[plugin_name] - assert isinstance(proxy, plugable.PluginProxy) - assert proxy.name == plugin_name - assert read_only(ns, plugin_name) is proxy - assert read_only(proxy, 'method')(7) == 7 + b - - # Test that calling finilize again raises AssertionError: - raises(AssertionError, api.finalize) - - # Test with base class that doesn't request a proxy - class NoProxy(plugable.Plugin): - __proxy__ = False - api = plugable.API(NoProxy) - class plugin0(NoProxy): - pass - api.register(plugin0) - class plugin1(NoProxy): - pass - api.register(plugin1) - api.finalize() - names = ['plugin0', 'plugin1'] - assert list(api.NoProxy) == names - for name in names: - plugin = api.NoProxy[name] - assert getattr(api.NoProxy, name) is plugin - assert isinstance(plugin, plugable.Plugin) - assert not isinstance(plugin, plugable.PluginProxy) + class base0_plugin2(base0): + pass + r(base0_plugin2) + + class base1_plugin0(base1): + pass + r(base1_plugin0) + + class base1_plugin1(base1): + pass + r(base1_plugin1) + + class base1_plugin2(base1): + pass + r(base1_plugin2) + + # Test API instance: + assert api.isdone('bootstrap') is False + assert api.isdone('finalize') is False + api.finalize() + assert api.isdone('bootstrap') is True + assert api.isdone('finalize') is True + + def get_base(b): + return 'base%d' % b + + def get_plugin(b, p): + return 'base%d_plugin%d' % (b, p) + + for b in xrange(2): + base_name = get_base(b) + ns = getattr(api, base_name) + assert isinstance(ns, plugable.NameSpace) + assert read_only(api, base_name) is ns + assert len(ns) == 3 + for p in xrange(3): + plugin_name = get_plugin(b, p) + proxy = ns[plugin_name] + assert isinstance(proxy, plugable.PluginProxy) + assert proxy.name == plugin_name + assert read_only(ns, plugin_name) is proxy + assert read_only(proxy, 'method')(7) == 7 + b + + # Test that calling finilize again raises AssertionError: + e = raises(StandardError, api.finalize) + assert str(e) == 'API.finalize() already called', str(e) + + # Test with base class that doesn't request a proxy + class NoProxy(plugable.Plugin): + __proxy__ = False + api = plugable.API(NoProxy) + class plugin0(NoProxy): + pass + api.register(plugin0) + class plugin1(NoProxy): + pass + api.register(plugin1) + api.finalize() + names = ['plugin0', 'plugin1'] + assert list(api.NoProxy) == names + for name in names: + plugin = api.NoProxy[name] + assert getattr(api.NoProxy, name) is plugin + assert isinstance(plugin, plugable.Plugin) + assert not isinstance(plugin, plugable.PluginProxy) + + def test_bootstrap(self): + """ + Test the `ipalib.plugable.API.bootstrap` method. + """ + (o, home) = self.new() + assert o.env._isdone('_bootstrap') is False + assert o.env._isdone('_finalize_core') is False + assert o.isdone('bootstrap') is False + o.bootstrap(my_test_override='Hello, world!') + assert o.isdone('bootstrap') is True + assert o.env._isdone('_bootstrap') is True + assert o.env._isdone('_finalize_core') is True + assert o.env.my_test_override == 'Hello, world!' + e = raises(StandardError, o.bootstrap) + assert str(e) == 'API.bootstrap() already called' + + def test_load_plugins(self): + """ + Test the `ipalib.plugable.API.load_plugins` method. + """ + (o, home) = self.new() + assert o.isdone('bootstrap') is False + assert o.isdone('load_plugins') is False + o.load_plugins() + assert o.isdone('bootstrap') is True + assert o.isdone('load_plugins') is True + e = raises(StandardError, o.load_plugins) + assert str(e) == 'API.load_plugins() already called' diff --git a/tests/util.py b/tests/util.py index 5656515c..cc761ce7 100644 --- a/tests/util.py +++ b/tests/util.py @@ -22,8 +22,71 @@ Common utility functions and classes for unit tests. """ import inspect +import os +from os import path +import tempfile +import shutil from ipalib import errors + +class TempDir(object): + def __init__(self): + self.__path = tempfile.mkdtemp(prefix='ipa.tests.') + assert self.path == self.__path + + def __get_path(self): + assert path.abspath(self.__path) == self.__path + assert self.__path.startswith('/tmp/ipa.tests.') + assert path.isdir(self.__path) and not path.islink(self.__path) + return self.__path + path = property(__get_path) + + def rmtree(self): + if self.__path is not None: + shutil.rmtree(self.path) + self.__path = None + + def makedirs(self, *parts): + d = self.join(*parts) + if not path.exists(d): + os.makedirs(d) + assert path.isdir(d) and not path.islink(d) + return d + + def touch(self, *parts): + d = self.makedirs(*parts[:-1]) + f = path.join(d, parts[-1]) + assert not path.exists(f) + open(f, 'w').close() + assert path.isfile(f) and not path.islink(f) + return f + + def write(self, content, *parts): + d = self.makedirs(*parts[:-1]) + f = path.join(d, parts[-1]) + assert not path.exists(f) + open(f, 'w').write(content) + assert path.isfile(f) and not path.islink(f) + return f + + def join(self, *parts): + return path.join(self.path, *parts) + + def __del__(self): + self.rmtree() + + +class TempHome(TempDir): + def __init__(self): + super(TempHome, self).__init__() + self.__home = os.environ['HOME'] + os.environ['HOME'] = self.path + + def rmtree(self): + os.environ['HOME'] = self.__home + super(TempHome, self).rmtree() + + class ExceptionNotRaised(Exception): """ Exception raised when an *expected* exception is *not* raised during a |