summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRob Crittenden <rcritten@redhat.com>2009-01-22 10:42:41 -0500
committerRob Crittenden <rcritten@redhat.com>2009-01-22 10:42:41 -0500
commitc2967a675a288e7d31374229fd974d0cb9966f2c (patch)
tree58be8ca6319f4660d9f18b97a37b9c0c56104d02
parent2b8b87b4d6c3b4389a0a7bf48c225035c53e7ad1 (diff)
parent5d82e3b35a8fb2d4c25f282cddad557a7650197c (diff)
downloadfreeipa-c2967a675a288e7d31374229fd974d0cb9966f2c.tar.gz
freeipa-c2967a675a288e7d31374229fd974d0cb9966f2c.tar.xz
freeipa-c2967a675a288e7d31374229fd974d0cb9966f2c.zip
Merge branch 'master' of git://fedorapeople.org/~jderose/freeipa2
-rw-r--r--.bzrignore2
-rw-r--r--.gitignore4
-rw-r--r--MANIFEST.in4
-rw-r--r--TODO97
-rwxr-xr-xipa41
-rw-r--r--ipalib/__init__.py915
-rwxr-xr-xipalib/aci.py245
-rw-r--r--ipalib/backend.py45
-rw-r--r--ipalib/base.py486
-rw-r--r--ipalib/cli.py854
-rw-r--r--ipalib/config.py529
-rw-r--r--ipalib/constants.py153
-rw-r--r--ipalib/crud.py149
-rw-r--r--ipalib/errors.py465
-rw-r--r--ipalib/errors2.py580
-rw-r--r--ipalib/frontend.py696
-rw-r--r--ipalib/ipauuid.py556
-rw-r--r--ipalib/parameters.py990
-rw-r--r--ipalib/plugable.py718
-rw-r--r--ipalib/plugins/__init__.py25
-rw-r--r--ipalib/plugins/b_kerberos.py34
-rw-r--r--ipalib/plugins/b_xmlrpc.py102
-rw-r--r--ipalib/plugins/f_automount.py563
-rw-r--r--ipalib/plugins/f_delegation.py65
-rw-r--r--ipalib/plugins/f_group.py384
-rw-r--r--ipalib/plugins/f_host.py286
-rw-r--r--ipalib/plugins/f_hostgroup.py354
-rw-r--r--ipalib/plugins/f_misc.py89
-rw-r--r--ipalib/plugins/f_netgroup.py461
-rw-r--r--ipalib/plugins/f_passwd.py70
-rw-r--r--ipalib/plugins/f_pwpolicy.py122
-rw-r--r--ipalib/plugins/f_ra.py117
-rw-r--r--ipalib/plugins/f_service.py204
-rw-r--r--ipalib/plugins/f_user.py367
-rw-r--r--ipalib/request.py71
-rw-r--r--ipalib/rpc.py207
-rw-r--r--ipalib/util.py151
-rw-r--r--ipaserver/__init__.py22
-rw-r--r--ipaserver/conn.py69
-rw-r--r--ipaserver/context.py32
-rw-r--r--ipaserver/ipaldap.py553
-rw-r--r--ipaserver/ipautil.py201
-rw-r--r--ipaserver/mod_python_xmlrpc.py367
-rw-r--r--ipaserver/plugins/__init__.py24
-rw-r--r--ipaserver/plugins/b_ldap.py334
-rw-r--r--ipaserver/plugins/b_ra.py407
-rw-r--r--ipaserver/rpcserver.py67
-rw-r--r--ipaserver/servercore.py467
-rwxr-xr-xipaserver/test_client28
-rw-r--r--ipaserver/updates/automount.update54
-rw-r--r--ipaserver/updates/groupofhosts.update5
-rw-r--r--ipaserver/updates/host.update25
-rw-r--r--ipawebui/__init__.py24
-rw-r--r--ipawebui/controller.py71
-rw-r--r--ipawebui/mod_python_webui.py22
-rw-r--r--ipawebui/static/mootools-core.js3946
-rw-r--r--ipawebui/templates/__init__.py21
-rw-r--r--ipawebui/templates/form.kid16
-rw-r--r--ipawebui/templates/main.kid14
-rwxr-xr-xlite-webui.py45
-rwxr-xr-xlite-xmlrpc.py175
-rwxr-xr-xmake-doc29
-rwxr-xr-xmake-test32
-rwxr-xr-xsetup.py46
-rw-r--r--tests/__init__.py22
-rw-r--r--tests/data.py38
-rw-r--r--tests/test_ipalib/__init__.py22
-rw-r--r--tests/test_ipalib/test_backend.py55
-rw-r--r--tests/test_ipalib/test_base.py352
-rw-r--r--tests/test_ipalib/test_cli.py277
-rw-r--r--tests/test_ipalib/test_config.py608
-rw-r--r--tests/test_ipalib/test_crud.py237
-rw-r--r--tests/test_ipalib/test_error2.py371
-rw-r--r--tests/test_ipalib/test_errors.py289
-rw-r--r--tests/test_ipalib/test_frontend.py771
-rw-r--r--tests/test_ipalib/test_parameters.py994
-rw-r--r--tests/test_ipalib/test_plugable.py756
-rw-r--r--tests/test_ipalib/test_request.py161
-rw-r--r--tests/test_ipalib/test_rpc.py249
-rw-r--r--tests/test_ipalib/test_util.py61
-rw-r--r--tests/test_ipaserver/__init__.py22
-rw-r--r--tests/test_ipaserver/test_rpcserver.py79
-rw-r--r--tests/test_ipawebui/__init__.py21
-rw-r--r--tests/test_ipawebui/test_controllers.py70
-rw-r--r--tests/test_util.py148
-rw-r--r--tests/test_xmlrpc/__init__.py22
-rw-r--r--tests/test_xmlrpc/test_automount_plugin.py243
-rw-r--r--tests/test_xmlrpc/test_group_plugin.py178
-rw-r--r--tests/test_xmlrpc/test_host_plugin.py128
-rw-r--r--tests/test_xmlrpc/test_hostgroup_plugin.py149
-rw-r--r--tests/test_xmlrpc/test_netgroup_plugin.py320
-rw-r--r--tests/test_xmlrpc/test_service_plugin.py93
-rw-r--r--tests/test_xmlrpc/test_user_plugin.py151
-rw-r--r--tests/test_xmlrpc/xmlrpc_test.py49
-rw-r--r--tests/util.py391
95 files changed, 25624 insertions, 0 deletions
diff --git a/.bzrignore b/.bzrignore
new file mode 100644
index 000000000..3d1eb8741
--- /dev/null
+++ b/.bzrignore
@@ -0,0 +1,2 @@
+.git
+freeipa2-dev-doc
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 000000000..7ed6c41dd
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+*.pyc
+.bzr
+freeipa2-dev-doc
+build
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 000000000..f7a626160
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,4 @@
+include LICENSE TODO lite-webui.py lite-xmlrpc.py
+graft tests/
+graft ipawebui/static/
+include ipawebui/templates/*.kid
diff --git a/TODO b/TODO
new file mode 100644
index 000000000..56c8c00ae
--- /dev/null
+++ b/TODO
@@ -0,0 +1,97 @@
+API chages before January 2009 simi-freeze:
+
+ * Merge Param and Type together so that rather than taking the type as a
+ kwarg, you simply use the Type. For example, instead of:
+ >>> Param('number', type=Int())
+ You would do this:
+ >>> Int('number')
+ The types will correspond to Python 3.0 text/binary disambiguaiton, so we
+ will have Bytes, Str, Int, Float, and Bool.
+
+ * Rename crud Method base classes to standard CRUDS name: Add=>Create,
+ Get=>Retrieve, Mod=>Update, Del=>Delete, Find=>Search.
+
+ * Add a Command.backend convenience attribute that checks if the class
+ uses_backend attribute is sets the Command.backend attribute like this:
+ self.backend = self.Backend[self.uses_backend]
+
+ * Finish methods on Plugin base class for calling external commands via
+ subprocess.
+
+ * Probably renamed ipa_server package to ipaserver.
+
+ * Add special logging methods to Plugin baseclass for authorization events
+ (escalation, de-escalation, and denial).
+
+ * Implement gettext service.
+
+ * Add ability to register pre-op, post-op plugins per command.
+
+ * Add ability to have certain args/options only active on either client-side
+ or server-side, and also the same for things like default_from callbacks.
+
+ * Add ability to have a post-processing step that only gets called
+ client-side. It should have a signature like output_for_cli() minus the
+ textui argument. Need to decide whether we allow this method to modify
+ the return value.
+
+ * Make Plugin base class parse class docstring into overview and
+ full-description strings (similar to Bazaar).
+
+ * Removed depreciated code in config.py.
+
+ * Remove __getattr__() from Env (and probably elsewhere) as in Python 2.4 and
+ 2.5 hasattr() will catch KeyboardInterrupt and SystemExit exceptions (BTW,
+ this has been fixed in Python 2.6).
+
+ * Remove support for dynamic environment values from Env... Jason feels this
+ the Env class should be simple and static. Other mechanisms should be used
+ for retrieving per-request dynamic environment variables.
+
+
+CRUD base classes:
+
+ * The Retrieve method should add in the common Flag('all') option for
+ retrieving all attributes.
+
+ * We probably need some LDAP centric crud method base classes, like
+ LDAPCreate, etc. Or other options it to have an LDAPObject base class and
+ have the crud Method plugins rely more on their corresponding Object plugin.
+
+ * Update the Retrieve, Update, Delete, and Search classes so that the utilize
+ the new Param.query kwarg (to turn off validation) when cloning params.
+
+
+Existing plugins:
+
+ * Many existing plugins that are doing crud-type operations aren't using the
+ Object + Method way of defining their parameters, and are therefore defining
+ the exact same parameter several times in a module. This should be fixed
+ one way or another... if there are deficiencies in the crud base classes,
+ they need to be improved.
+
+
+Command Line interface:
+
+ * Finish textui plugin
+
+ * Make possible Enum values self-documenting
+
+ * All "comma-separated list of..." parameters should really be changed to
+ multivalue and have a flag that tells the CLI whether a multivalue should
+ be parsed as comma-separated.
+
+
+Improve ease of plugin writting
+ - make "from ipalib import *" import everything a plugin writter will need
+ - Finish ipa_types, add Str and Float Types
+
+Packaging
+ - Use setuptools instead of plain distutils
+ - Make setup.py generate dev-docs and run unit tests
+ - Package for rpm (.spec file)
+ - Package for apt (debian/ dir)
+
+Migration
+ - Add the IPAService objectclass to existing principals
+ - Move existng host/ principals from cn=services to cn=computers?
diff --git a/ipa b/ipa
new file mode 100755
index 000000000..cab12b6ba
--- /dev/null
+++ b/ipa
@@ -0,0 +1,41 @@
+#!/usr/bin/env python
+
+# 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
+
+"""
+Command Line Interface for IPA administration.
+
+The CLI functionality is implemented in ipalib/cli.py
+"""
+
+import sys
+from ipalib import api
+from ipalib.cli import CLI
+
+if __name__ == '__main__':
+ # If we can't explicitly determin the encoding, we assume UTF-8:
+ if sys.stdin.encoding is None:
+ encoding = 'UTF-8'
+ else:
+ encoding = sys.stdin.encoding
+ cli = CLI(api,
+ (s.decode(encoding) for s in sys.argv[1:])
+ )
+ sys.exit(cli.run())
diff --git a/ipalib/__init__.py b/ipalib/__init__.py
new file mode 100644
index 000000000..29344e182
--- /dev/null
+++ b/ipalib/__init__.py
@@ -0,0 +1,915 @@
+# 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
+
+
+'''
+Package containing the core library.
+
+=============================
+ Tutorial for Plugin Authors
+=============================
+
+This tutorial will introduce you to writing plugins for freeIPA v2. It does
+not cover every detail, but it provides enough to get you started and is
+heavily cross-referenced with further documentation that (hopefully) fills
+in the missing details.
+
+In addition to this tutorial, the many built-in plugins in `ipalib.plugins`
+and `ipaserver.plugins` provide real-life examples of how to write good
+plugins.
+
+
+----------------------------
+How this tutorial is written
+----------------------------
+
+The code examples in this tutorial are presented as if entered into a Python
+interactive interpreter session. As such, when you create a real plugin in
+a source file, a few details will be different (in addition to the fact that
+you will never include the ``>>>`` nor ``...`` that the interpreter places at
+the beginning of each line of code).
+
+The tutorial examples all have this pattern:
+
+ ::
+
+ >>> from ipalib import Command, create_api
+ >>> api = create_api()
+ >>> class my_command(Command):
+ ... pass
+ ...
+ >>> api.register(my_command)
+ >>> api.finalize()
+
+In the tutorial we call `create_api()` to create an *example* instance
+of `plugable.API` to work with. But a real plugin will simply use
+``ipalib.api``, the standard run-time instance of `plugable.API`.
+
+A real plugin will have this pattern:
+
+ ::
+
+ from ipalib import Command, api
+
+ class my_command(Command):
+ pass
+ api.register(my_command)
+
+As seen above, also note that in a real plugin you will *not* call
+`plugable.API.finalize()`. When in doubt, look at some of the built-in
+plugins for guidance, like those in `ipalib.plugins`.
+
+If you don't know what the Python *interactive interpreter* is, or are
+confused about what this *Python* is in the first place, then you probably
+should start with the Python tutorial:
+
+ http://docs.python.org/tutorial/index.html
+
+
+------------------------------------
+First steps: A simple command plugin
+------------------------------------
+
+Our first example will create the most basic command plugin possible. This
+command will be seen in the list of command plugins, but it wont be capable
+of actually doing anything yet.
+
+A command plugin simultaneously adds a new command that can be called through
+the command-line ``ipa`` script *and* adds a new XML-RPC method... the two are
+one in the same, simply invoked in different ways.
+
+A freeIPA plugin is a Python class, and when you create a plugin, you register
+this class itself (instead of an instance of the class). To be a command
+plugin, your plugin must subclass from `frontend.Command` (or from a subclass
+thereof). Here is our first example:
+
+>>> from ipalib import Command, create_api
+>>> api = create_api()
+>>> class my_command(Command): # Step 1, define class
+... """My example plugin."""
+...
+>>> api.register(my_command) # Step 2, register class
+
+Notice that we are registering the ``my_command`` class itself, not an
+instance of ``my_command``.
+
+Until `plugable.API.finalize()` is called, your plugin class has not been
+instantiated nor does the ``Command`` namespace yet exist. For example:
+
+>>> hasattr(api, 'Command')
+False
+>>> api.finalize() # plugable.API.finalize()
+>>> hasattr(api.Command, 'my_command')
+True
+>>> api.Command.my_command.doc
+'My example plugin.'
+
+Notice that your plugin instance is accessed through an attribute named
+``my_command``, the same name as your plugin class name.
+
+
+------------------------------
+Make your command do something
+------------------------------
+
+This simplest way to make your example command plugin do something is to
+implement a ``run()`` method, like this:
+
+>>> class my_command(Command):
+... """My example plugin with run()."""
+...
+... def run(self):
+... return 'My run() method was called!'
+...
+>>> api = create_api()
+>>> api.register(my_command)
+>>> api.finalize()
+>>> api.Command.my_command() # Call your command
+'My run() method was called!'
+
+When `frontend.Command.__call__()` is called, it first validates any arguments
+and options your command plugin takes (if any) and then calls its ``run()``
+method.
+
+
+------------------------
+Forwarding vs. execution
+------------------------
+
+However, unlike the example above, a typical command plugin will implement an
+``execute()`` method instead of a ``run()`` method. Your command plugin can
+be loaded in two distinct contexts:
+
+ 1. In a *client* context - Your command plugin is only used to validate
+ any arguments and options it takes, and then ``self.forward()`` is
+ called, which forwards the call over XML-RPC to an IPA server where
+ the actual work is done.
+
+ 2. In a *server* context - Your same command plugin validates any
+ arguments and options it takes, and then ``self.execute()`` is called,
+ which you should implement to perform whatever work your plugin does.
+
+The base `frontend.Command.run()` method simply dispatches the call to
+``self.execute()`` if ``self.env.in_server`` is True, or otherwise
+dispatches the call to ``self.forward()``.
+
+For example, say you have a command plugin like this:
+
+>>> class my_command(Command):
+... """Forwarding vs. execution."""
+...
+... def forward(self):
+... return 'in_server=%r; forward() was called.' % self.env.in_server
+...
+... def execute(self):
+... return 'in_server=%r; execute() was called.' % self.env.in_server
+...
+
+If ``my_command`` is loaded in a *client* context, ``forward()`` will be
+called:
+
+>>> api = create_api()
+>>> api.env.in_server = False # run() will dispatch to forward()
+>>> api.register(my_command)
+>>> api.finalize()
+>>> api.Command.my_command() # Call your command plugin
+'in_server=False; forward() was called.'
+
+On the other hand, if ``my_command`` is loaded in a *server* context,
+``execute()`` will be called:
+
+>>> api = create_api()
+>>> api.env.in_server = True # run() will dispatch to execute()
+>>> api.register(my_command)
+>>> api.finalize()
+>>> api.Command.my_command() # Call your command plugin
+'in_server=True; execute() was called.'
+
+Normally there should be no reason to override `frontend.Command.forward()`,
+but, as above, it can be done for demonstration purposes. In contrast, there
+*is* a reason you might want to override `frontend.Command.run()`: if it only
+makes sense to execute your command locally, if it should never be forwarded
+to the server. In this case, you should implement your *do-stuff* in the
+``run()`` method instead of in the ``execute()`` method.
+
+For example, the ``ipa`` command line script has a ``help`` command
+(`ipalib.cli.help`) that is specific to the command-line-interface and should
+never be forwarded to the server.
+
+
+---------------
+Backend plugins
+---------------
+
+There are two types of plugins:
+
+ 1. *Frontend plugins* - These are loaded in both the *client* and *server*
+ contexts. These need to be installed with any application built atop
+ the `ipalib` library. The built-in frontend plugins can be found in
+ `ipalib.plugins`. The ``my_command`` example above is a frontend
+ plugin.
+
+ 2. *Backend plugins* - These are only loaded in a *server* context and
+ only need to be installed on the IPA server. The built-in backend
+ plugins can be found in `ipaserver.plugins`.
+
+Backend plugins should provide a set of methods that standardize how IPA
+interacts with some external system or library. For example, all interaction
+with LDAP is done through the ``ldap`` backend plugin defined in
+`ipaserver.plugins.b_ldap`. As a good rule of thumb, anytime you need to
+import some package that is not part of the Python standard library, you
+should probably interact with that package via a corresponding backend
+plugin you implement.
+
+Backend plugins are much more free-form than command plugins. Aside from a
+few reserved attribute names, you can define arbitrary public methods on your
+backend plugin (in contrast, frontend plugins get wrapped in a
+`plugable.PluginProxy`, which allow access to only specific attributes on the
+frontend plugin).
+
+Here is a simple example:
+
+>>> from ipalib import Backend
+>>> class my_backend(Backend):
+... """My example backend plugin."""
+...
+... def do_stuff(self):
+... """Part of your API."""
+... return 'Stuff got done.'
+...
+>>> api = create_api()
+>>> api.register(my_backend)
+>>> api.finalize()
+>>> api.Backend.my_backend.do_stuff()
+'Stuff got done.'
+
+
+-------------------------------
+How your command should do work
+-------------------------------
+
+We now return to our ``my_command`` plugin example.
+
+Plugins are separated into frontend and backend plugins so that there are not
+unnecessary dependencies required by an application that only uses `ipalib` and
+its built-in frontend plugins (and then forwards over XML-RPC for execution).
+
+But how do we avoid introducing additional dependencies? For example, the
+``user_add`` command needs to talk to LDAP to add the user, yet we want to
+somehow load the ``user_add`` plugin on client machines without requiring the
+``python-ldap`` package (Python bindings to openldap) to be installed. To
+answer that, we consult our golden rule:
+
+ **The golden rule:** A command plugin should implement its ``execute()``
+ method strictly via calls to methods on one or more backend plugins.
+
+So the module containing the ``user_add`` command does not itself import the
+Python LDAP bindings, only the module containing the ``ldap`` backend plugin
+does that, and the backend plugins are only installed on the server. The
+``user_add.execute()`` method, which is only called when in a server context,
+is implemented as a series of calls to methods on the ``ldap`` backend plugin.
+
+When `plugable.Plugin.set_api()` is called, each plugin stores a reference to
+the `plugable.API` instance it has been loaded into. So your plugin can
+access the ``my_backend`` plugin as ``self.api.Backend.my_backend``.
+
+Additionally, convenience attributes are set for each namespace, so your
+plugin can also access the ``my_backend`` plugin as simply
+``self.Backend.my_backend``.
+
+This next example will tie everything together. First we create our backend
+plugin:
+
+>>> api = create_api()
+>>> api.env.in_server = True # We want to execute, not forward
+>>> class my_backend(Backend):
+... """My example backend plugin."""
+...
+... def do_stuff(self):
+... """my_command.execute() calls this."""
+... return 'my_backend.do_stuff() indeed did do stuff!'
+...
+>>> api.register(my_backend)
+
+Second, we have our frontend plugin, the command:
+
+>>> class my_command(Command):
+... """My example command plugin."""
+...
+... def execute(self):
+... """Implemented against Backend.my_backend"""
+... return self.Backend.my_backend.do_stuff()
+...
+>>> api.register(my_command)
+
+Lastly, we call ``api.finalize()`` and see what happens when we call
+``my_command()``:
+
+>>> api.finalize()
+>>> api.Command.my_command()
+'my_backend.do_stuff() indeed did do stuff!'
+
+When not in a server context, ``my_command.execute()`` never gets called, so
+it never tries to access the non-existent backend plugin at
+``self.Backend.my_backend.`` To emphasize this point, here is one last
+example:
+
+>>> api = create_api()
+>>> api.env.in_server = False # We want to forward, not execute
+>>> class my_command(Command):
+... """My example command plugin."""
+...
+... def execute(self):
+... """Same as above."""
+... return self.Backend.my_backend.do_stuff()
+...
+... def forward(self):
+... return 'Just my_command.forward() getting called here.'
+...
+>>> api.register(my_command)
+>>> api.finalize()
+
+Notice that the ``my_backend`` plugin has certainly not be registered:
+
+>>> hasattr(api.Backend, 'my_backend')
+False
+
+And yet we can call ``my_command()``:
+
+>>> api.Command.my_command()
+'Just my_command.forward() getting called here.'
+
+
+----------------------------------------
+Calling other commands from your command
+----------------------------------------
+
+It can be useful to have your ``execute()`` method call other command plugins.
+Among other things, this allows for meta-commands that conveniently call
+several other commands in a single operation. For example:
+
+>>> api = create_api()
+>>> api.env.in_server = True # We want to execute, not forward
+>>> class meta_command(Command):
+... """My meta-command plugin."""
+...
+... def execute(self):
+... """Calls command_1(), command_2()"""
+... return '%s; %s.' % (
+... self.Command.command_1(),
+... self.Command.command_2()
+... )
+>>> class command_1(Command):
+... def execute(self):
+... return 'command_1.execute() called'
+...
+>>> class command_2(Command):
+... def execute(self):
+... return 'command_2.execute() called'
+...
+>>> api.register(meta_command)
+>>> api.register(command_1)
+>>> api.register(command_2)
+>>> api.finalize()
+>>> api.Command.meta_command()
+'command_1.execute() called; command_2.execute() called.'
+
+Because this is quite useful, we are going to revise our golden rule somewhat:
+
+ **The revised golden rule:** A command plugin should implement its
+ ``execute()`` method strictly via what it can access through ``self.api``,
+ most likely via the backend plugins in ``self.api.Backend`` (which can also
+ be conveniently accessed as ``self.Backend``).
+
+
+-----------------------------------------------
+Defining arguments and options for your command
+-----------------------------------------------
+
+You can define a command that will accept specific arguments and options.
+For example:
+
+>>> from ipalib import Str
+>>> class nudge(Command):
+... """Takes one argument, one option"""
+...
+... takes_args = ['programmer']
+...
+... takes_options = [Str('stuff', default=u'documentation')]
+...
+... def execute(self, programmer, **kw):
+... return '%s, go write more %s!' % (programmer, kw['stuff'])
+...
+>>> api = create_api()
+>>> api.env.in_server = True
+>>> api.register(nudge)
+>>> api.finalize()
+>>> api.Command.nudge(u'Jason')
+u'Jason, go write more documentation!'
+>>> api.Command.nudge(u'Jason', stuff=u'unit tests')
+u'Jason, go write more unit tests!'
+
+The ``args`` and ``options`` attributes are `plugable.NameSpace` instances
+containing a command's arguments and options, respectively, as you can see:
+
+>>> list(api.Command.nudge.args) # Iterates through argument names
+['programmer']
+>>> api.Command.nudge.args.programmer
+Str('programmer')
+>>> list(api.Command.nudge.options) # Iterates through option names
+['stuff']
+>>> api.Command.nudge.options.stuff
+Str('stuff', default=u'documentation')
+>>> api.Command.nudge.options.stuff.default
+u'documentation'
+
+The arguments and options must not contain colliding names. They are both
+merged together into the ``params`` attribute, another `plugable.NameSpace`
+instance, as you can see:
+
+>>> api.Command.nudge.params
+NameSpace(<2 members>, sort=False)
+>>> list(api.Command.nudge.params) # Iterates through the param names
+['programmer', 'stuff']
+
+When calling a command, its positional arguments can also be provided as
+keyword arguments, and in any order. For example:
+
+>>> api.Command.nudge(stuff=u'lines of code', programmer=u'Jason')
+u'Jason, go write more lines of code!'
+
+When a command plugin is called, the values supplied for its parameters are
+put through a sophisticated processing pipeline that includes steps for
+normalization, type conversion, validation, and dynamically constructing
+the defaults for missing values. The details wont be covered here; however,
+here is a quick teaser:
+
+>>> from ipalib import Int
+>>> class create_player(Command):
+... takes_options = [
+... 'first',
+... 'last',
+... Str('nick',
+... normalizer=lambda value: value.lower(),
+... default_from=lambda first, last: first[0] + last,
+... ),
+... Int('points', default=0),
+... ]
+...
+>>> cp = create_player()
+>>> cp.finalize()
+>>> cp.convert(points=u' 1000 ')
+{'points': 1000}
+>>> cp.normalize(nick=u'NickName')
+{'nick': u'nickname'}
+>>> cp.get_default(first=u'Jason', last=u'DeRose')
+{'nick': u'jderose', 'points': 0}
+
+For the full details on the parameter system, see the
+`frontend.parse_param_spec()` function, and the `frontend.Param` and
+`frontend.Command` classes.
+
+
+---------------------------------------
+Allowed return values from your command
+---------------------------------------
+
+The return values from your command can be rendered by different user
+interfaces (CLI, web-UI); furthermore, a call to your command can be
+transparently forwarded over the network (XML-RPC, JSON). As such, the return
+values from your command must be usable by the least common denominator.
+
+Your command should return only simple data types and simple data structures,
+the kinds that can be represented in an XML-RPC request or in the JSON format.
+The return values from your command's ``execute()`` method can include only
+the following:
+
+ Simple scalar values:
+ These can be ``str``, ``unicode``, ``int``, and ``float`` instances,
+ plus the ``True``, ``False``, and ``None`` constants.
+
+ Simple compound values:
+ These can be ``dict``, ``list``, and ``tuple`` instances. These
+ compound values must contain only the simple scalar values above or
+ other simple compound values. These compound values can also be empty.
+ For our purposes here, the ``list`` and ``tuple`` types are equivalent
+ and can be used interchangeably.
+
+Also note that your ``execute()`` method should not contain any ``print``
+statements or otherwise cause any output on ``sys.stdout``. Your command can
+(and should) produce log messages by using ``self.log`` (see below).
+
+To learn more about XML-RPC (XML Remote Procedure Call), see:
+
+ http://docs.python.org/library/xmlrpclib.html
+
+ http://en.wikipedia.org/wiki/XML-RPC
+
+To learn more about JSON (Java Script Object Notation), see:
+
+ http://docs.python.org/library/json.html
+
+ http://www.json.org/
+
+
+---------------------------------------
+How your command should print to stdout
+---------------------------------------
+
+As noted above, your command should not print anything while in its
+``execute()`` method. So how does your command format its output when
+called from the ``ipa`` script?
+
+After the `cli.CLI.run_cmd()` method calls your command, it will call your
+command's ``output_for_cli()`` method (if you have implemented one).
+
+If you implement an ``output_for_cli()`` method, it must have the following
+signature:
+
+ ::
+
+ output_for_cli(textui, result, *args, **options)
+
+ textui
+ An object implementing methods for outputting to the console.
+ Currently the `ipalib.cli.textui` plugin is passed, which your method
+ can also access as ``self.Backend.textui``. However, in case this
+ changes in the future, your method should use the instance passed to
+ it in this first argument.
+
+ result
+ This is the return value from calling your command plugin. Depending
+ upon how your command is implemented, this is probably the return
+ value from your ``execute()`` method.
+
+ args
+ The arguments your command was called with. If your command takes no
+ arguments, you can omit this. You can also explicitly list your
+ arguments rather than using the generic ``*args`` form.
+
+ options
+ The options your command was called with. If your command takes no
+ options, you can omit this. If your command takes any options, you
+ must use the ``**options`` form as they will be provided strictly as
+ keyword arguments.
+
+For example, say we setup a command like this:
+
+>>> class show_items(Command):
+...
+... takes_args = ['key?']
+...
+... takes_options = [Flag('reverse')]
+...
+... def execute(self, key, **options):
+... items = dict(
+... fruit='apple',
+... pet='dog',
+... city='Berlin',
+... )
+... if key in items:
+... return items[key]
+... return [
+... (k, items[k]) for k in sorted(items, reverse=options['reverse'])
+... ]
+...
+... def output_for_cli(self, textui, result, key, **options):
+... if key is not None:
+... textui.print_plain('%s = %r' % (key, result))
+... else:
+... textui.print_name(self.name)
+... textui.print_keyval(result)
+... format = '%d items'
+... if options['reverse']:
+... format += ' (in reverse order)'
+... textui.print_count(result, format)
+...
+>>> api = create_api()
+>>> api.env.in_server = True # We want to execute, not forward.
+>>> api.register(show_items)
+>>> api.finalize()
+
+Normally when you invoke the ``ipa`` script, `cli.CLI.load_plugins()` will
+register the `cli.textui` backend plugin, but for the sake of our example,
+we will just create an instance here:
+
+>>> from ipalib import cli
+>>> textui = cli.textui() # We'll pass this to output_for_cli()
+
+Now for what we are concerned with in this example, calling your command
+through the ``ipa`` script basically will do the following:
+
+>>> result = api.Command.show_items()
+>>> api.Command.show_items.output_for_cli(textui, result, None, reverse=False)
+-----------
+show-items:
+-----------
+ city = 'Berlin'
+ fruit = 'apple'
+ pet = 'dog'
+-------
+3 items
+-------
+
+Similarly, calling it with ``reverse=True`` would result in the following:
+
+>>> result = api.Command.show_items(reverse=True)
+>>> api.Command.show_items.output_for_cli(textui, result, None, reverse=True)
+-----------
+show-items:
+-----------
+ pet = 'dog'
+ fruit = 'apple'
+ city = 'Berlin'
+--------------------------
+3 items (in reverse order)
+--------------------------
+
+Lastly, providing a ``key`` would result in the following:
+
+>>> result = api.Command.show_items(u'city')
+>>> api.Command.show_items.output_for_cli(textui, result, 'city', reverse=False)
+city = 'Berlin'
+
+See the `ipalib.cli.textui` plugin for a description of its methods.
+
+
+------------------------
+Logging from your plugin
+------------------------
+
+After `plugable.Plugin.set_api()` is called, your plugin will have a
+``self.log`` attribute. Plugins should only log through this attribute.
+For example:
+
+>>> class paint_house(Command):
+...
+... takes_args = ['color']
+...
+... def execute(self, color):
+... """Uses self.log.error()"""
+... if color not in ('red', 'blue', 'green'):
+... self.log.error("I don't have %s paint!", color) # Log error
+... return
+... return 'I painted the house %s.' % color
+...
+
+Some basic knowledge of the Python ``logging`` module might be helpful. See:
+
+ http://docs.python.org/library/logging.html
+
+The important thing to remember is that your plugin should not configure
+logging itself, but should instead simply use the ``self.log`` logger.
+
+Also see the `plugable.API.bootstrap()` method for details on how the logging
+is configured.
+
+
+---------------------
+Environment variables
+---------------------
+
+Plugins access configuration variables and run-time information through
+``self.api.env`` (or for convenience, ``self.env`` is equivalent). This
+attribute is a refences to the `ipalib.config.Env` instance created in
+`plugable.API.__init__()`.
+
+After `API.bootstrap()` has been called, the `Env` instance will be populated
+with all the environment information used by the built-in plugins.
+This will be called before any plugins are registered, so plugin authors can
+assume these variables will all exist by the time the module containing their
+plugin (or plugins) is imported. For example:
+
+>>> api = create_api()
+>>> len(api.env)
+1
+>>> api.bootstrap(in_server=True) # We want to execute, not forward
+>>> len(api.env)
+35
+
+`Env._bootstrap()`, which is called by `API.bootstrap()`, will create several
+run-time variables that connot be overriden in configuration files or through
+command-line options. Here is an overview of this run-time information:
+
+============= ============================= =======================
+Key Example value Description
+============= ============================= =======================
+bin '/usr/bin' Dir. containing script
+dot_ipa '/home/jderose/.ipa' User config directory
+home os.environ['HOME'] User home dir.
+ipalib '.../site-packages/ipalib' Dir. of ipalib package
+mode 'unit_test' The mode ipalib is in
+script sys.argv[0] Path of script
+site_packages '.../python2.5/site-packages' Dir. containing ipalib/
+============= ============================= =======================
+
+If your plugin requires new environment variables *and* will be included in
+the freeIPA built-in plugins, you should add the defaults for your variables
+in `ipalib.constants.DEFAULT_CONFIG`. Also, you should consider whether your
+new environment variables should have any auto-magic logic to determine their
+values if they haven't already been set by the time `config.Env._bootstrap()`,
+`config.Env._finalize_core()`, or `config.Env._finalize()` is called.
+
+On the other hand, if your plugin requires new environment variables and will
+be installed in a 3rd-party package, your plugin should set these variables
+in the module it is defined in.
+
+`config.Env` values work on a first-one-wins basis... after a value has been
+set, it can not be overridden with a new value. As any variables can be set
+using the command-line ``-e`` global option or set in a configuration file,
+your module must check whether a variable has already been set before
+setting its default value. For example:
+
+>>> if 'message_of_the_day' not in api.env:
+... api.env.message_of_the_day = 'Hello, world!'
+...
+
+Your plugin can access any environment variables via ``self.env``.
+For example:
+
+>>> class motd(Command):
+... """Print message of the day."""
+...
+... def execute(self):
+... return self.env.message_of_the_day
+...
+>>> api.register(motd)
+>>> api.finalize()
+>>> api.Command.motd()
+'Hello, world!'
+
+Also see the `plugable.API.bootstrap_with_global_options()` method.
+
+
+---------------------------------------------
+Indispensable ipa script commands and options
+---------------------------------------------
+
+The ``console`` command will launch a custom interactive Python interpreter
+session. The global environment will have an ``api`` variable, which is the
+standard `plugable.API` instance found at ``ipalib.api``. All plugins will
+have been loaded (well, except the backend plugins if ``in_server`` is False)
+and ``api`` will be fully initialized. To launch the console from within the
+top-level directory in the the source tree, just run ``ipa console`` from a
+terminal, like this:
+
+ ::
+
+ $ ./ipa console
+
+By default, ``in_server`` is False. If you want to start the console in a
+server context (so that all the backend plugins are loaded), you can use the
+``-e`` option to set the ``in_server`` environment variable, like this:
+
+ ::
+
+ $ ./ipa -e in_server=True console
+
+You can specify multiple environment variables by including the ``-e`` option
+multiple times, like this:
+
+ ::
+
+ $ ./ipa -e in_server=True -e mode=dummy console
+
+The space after the ``-e`` is optional. This is equivalent to the above command:
+
+ ::
+
+ $ ./ipa -ein_server=True -emode=dummy console
+
+The ``env`` command will print out the full environment in key=value pairs,
+like this:
+
+ ::
+
+ $ ./ipa env
+
+If you use the ``--server`` option, it will forward the call to the server
+over XML-RPC and print out what the environment is on the server, like this:
+
+ ::
+
+ $ ./ipa env --server
+
+The ``plugins`` command will show details of all the plugin that are loaded,
+like this:
+
+ ::
+
+ $ ./ipa plugins
+
+
+-----------------------------------
+Learning more about freeIPA plugins
+-----------------------------------
+
+To learn more about writing freeIPA plugins, you should:
+
+ 1. Look at some of the built-in plugins, like the frontend plugins in
+ `ipalib.plugins.f_user` and the backend plugins in
+ `ipaserver.plugins.b_ldap`.
+
+ 2. Learn about the base classes for frontend plugins in `ipalib.frontend`.
+
+ 3. Learn about the core plugin framework in `ipalib.plugable`.
+
+Furthermore, the freeIPA plugin architecture was inspired by the Bazaar plugin
+architecture. Although the two are different enough that learning how to
+write plugins for Bazaar will not particularly help you write plugins for
+freeIPA, some might be interested in the documentation on writing plugins for
+Bazaar, available here:
+
+ http://bazaar-vcs.org/WritingPlugins
+
+If nothing else, we just want to give credit where credit is deserved!
+However, freeIPA does not use any *code* from Bazaar... it merely borrows a
+little inspiration.
+
+
+--------------------------
+A note on docstring markup
+--------------------------
+
+Lastly, a quick note on markup: All the Python docstrings in freeIPA v2
+(including this tutorial) use the *reStructuredText* markup language. For
+information on reStructuredText, see:
+
+ http://docutils.sourceforge.net/rst.html
+
+For information on using reStructuredText markup with epydoc, see:
+
+ http://epydoc.sourceforge.net/manual-othermarkup.html
+
+
+--------------------------------------------------
+Next steps: get involved with freeIPA development!
+--------------------------------------------------
+
+The freeIPA team is always interested in feedback and contribution from the
+community. To get involved with freeIPA, see the *Contribute* page on
+freeIPA.org:
+
+ http://freeipa.org/page/Contribute
+
+'''
+
+import plugable
+from backend import Backend, Context
+from frontend import Command, LocalOrRemote, Application
+from frontend import Object, Method, Property
+from parameters import DefaultFrom, Bool, Flag, Int, Float, Bytes, Str, Password
+from parameters import BytesEnum, StrEnum
+
+try:
+ import uuid
+except ImportError:
+ import ipauuid as uuid
+
+def create_api(mode='dummy'):
+ """
+ Return standard `plugable.API` instance.
+
+ This standard instance allows plugins that subclass from the following
+ base classes:
+
+ - `frontend.Command`
+
+ - `frontend.Object`
+
+ - `frontend.Method`
+
+ - `frontend.Property`
+
+ - `frontend.Application`
+
+ - `backend.Backend`
+
+ - `backend.Context`
+ """
+ api = plugable.API(
+ Command, Object, Method, Property, Application,
+ Backend, Context,
+ )
+ if mode is not None:
+ api.env.mode = mode
+ return api
+
+
+api = create_api(mode=None)
diff --git a/ipalib/aci.py b/ipalib/aci.py
new file mode 100755
index 000000000..9dde767c0
--- /dev/null
+++ b/ipalib/aci.py
@@ -0,0 +1,245 @@
+# Authors:
+# Rob Crittenden <rcritten@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
+
+import shlex
+import re
+import ldap
+
+# The Python re module doesn't do nested parenthesis
+
+# Break the ACI into 3 pieces: target, name, permissions/bind_rules
+ACIPat = re.compile(r'\s*(\(.*\)+)\s*\(version\s+3.0\s*;\s*acl\s+\"(.*)\"\s*;\s*(.*);\)')
+
+# Break the permissions/bind_rules out
+PermPat = re.compile(r'(\w+)\s*\((.*)\)\s+(.*)')
+
+
+class ACI:
+ """
+ Holds the basic data for an ACI entry, as stored in the cn=accounts
+ entry in LDAP. Has methods to parse an ACI string and export to an
+ ACI String.
+ """
+
+ # Don't allow arbitrary attributes to be set in our __setattr__ implementation.
+ _objectattrs = ["name", "orig_acistr", "target", "action", "permissions",
+ "bindrule"]
+
+ __actions = ["allow", "deny"]
+
+ __permissions = ["read", "write", "add", "delete", "search", "compare",
+ "selfwrite", "proxy", "all"]
+
+ def __init__(self,acistr=None):
+ self.name = None
+ self.orig_acistr = acistr
+ self.target = {}
+ self.action = "allow"
+ self.permissions = ["write"]
+ self.bindrule = None
+ if acistr is not None:
+ self._parse_acistr(acistr)
+
+ def __getitem__(self,key):
+ """Fake getting attributes by key for sorting"""
+ if key == 0:
+ return self.name
+ if key == 1:
+ return self.source_group
+ if key == 2:
+ return self.dest_group
+ raise TypeError("Unknown key value %s" % key)
+
+ def __repr__(self):
+ """An alias for export_to_string()"""
+ return self.export_to_string()
+
+ def __getattr__(self, name):
+ """
+ Backward compatibility for the old ACI class.
+
+ The following extra attributes are available:
+
+ - source_group
+ - dest_group
+ - attrs
+ """
+ if name == 'source_group':
+ group = ''
+ dn = self.bindrule.split('=',1)
+ if dn[0] == "groupdn":
+ group = self._remove_quotes(dn[1])
+ if group.startswith("ldap:///"):
+ group = group[8:]
+ return group
+ if name == 'dest_group':
+ group = self.target.get('targetfilter', '')
+ if group:
+ g = group.split('=',1)[1]
+ if g.endswith(')'):
+ g = g[:-1]
+ return g
+ return ''
+ if name == 'attrs':
+ return self.target.get('targetattr', None)
+ raise AttributeError, "object has no attribute '%s'" % name
+
+ def __setattr__(self, name, value):
+ """
+ Backward compatibility for the old ACI class.
+
+ The following extra attributes are available:
+ - source_group
+ - dest_group
+ - attrs
+ """
+ if name == 'source_group':
+ self.__dict__['bindrule'] = 'groupdn="ldap:///%s"' % value
+ elif name == 'dest_group':
+ if value.startswith('('):
+ self.__dict__['target']['targetfilter'] = 'memberOf=%s' % value
+ else:
+ self.__dict__['target']['targetfilter'] = '(memberOf=%s)' % value
+ elif name == 'attrs':
+ self.__dict__['target']['targetattr'] = value
+ elif name in self._objectattrs:
+ self.__dict__[name] = value
+ else:
+ raise AttributeError, "object has no attribute '%s'" % name
+
+ def export_to_string(self):
+ """Output a Directory Server-compatible ACI string"""
+ self.validate()
+ aci = ""
+ for t in self.target:
+ if isinstance(self.target[t], list):
+ target = ""
+ for l in self.target[t]:
+ target = target + l + " || "
+ target = target[:-4]
+ aci = aci + "(%s=\"%s\")" % (t, target)
+ else:
+ aci = aci + "(%s=\"%s\")" % (t, self.target[t])
+ aci = aci + "(version 3.0;acl \"%s\";%s (%s) %s" % (self.name, self.action, ",".join(self.permissions), self.bindrule) + ";)"
+ return aci
+
+ def _remove_quotes(self, s):
+ # Remove leading and trailing quotes
+ if s.startswith('"'):
+ s = s[1:]
+ if s.endswith('"'):
+ s = s[:-1]
+ return s
+
+ def _parse_target(self, aci):
+ lexer = shlex.shlex(aci)
+ lexer.wordchars = lexer.wordchars + "."
+
+ l = []
+
+ var = False
+ for token in lexer:
+ # We should have the form (a = b)(a = b)...
+ if token == "(":
+ var = lexer.next().strip()
+ operator = lexer.next()
+ if operator != "=" and operator != "!=":
+ raise SyntaxError('No operator in target, got %s' % operator)
+ val = lexer.next().strip()
+ val = self._remove_quotes(val)
+ end = lexer.next()
+ if end != ")":
+ raise SyntaxError('No end parenthesis in target, got %s' % end)
+
+ if var == 'targetattr':
+ # Make a string of the form attr || attr || ... into a list
+ t = re.split('[\W]+', val)
+ self.target[var] = t
+ else:
+ self.target[var] = val
+
+ def _parse_acistr(self, acistr):
+ acimatch = ACIPat.match(acistr)
+ if not acimatch or len(acimatch.groups()) < 3:
+ raise SyntaxError, "malformed ACI"
+ self._parse_target(acimatch.group(1))
+ self.name = acimatch.group(2)
+ bindperms = PermPat.match(acimatch.group(3))
+ if not bindperms or len(bindperms.groups()) < 3:
+ raise SyntaxError, "malformed ACI"
+ self.action = bindperms.group(1)
+ self.permissions = bindperms.group(2).split(',')
+ self.bindrule = bindperms.group(3)
+
+ def validate(self):
+ """Do some basic verification that this will produce a
+ valid LDAP ACI.
+
+ returns True if valid
+ """
+ if not isinstance(self.permissions, list):
+ raise SyntaxError, "permissions must be a list"
+ for p in self.permissions:
+ if not p.lower() in self.__permissions:
+ raise SyntaxError, "invalid permission: '%s'" % p
+ if not self.name:
+ raise SyntaxError, "name must be set"
+ if not isinstance(self.name, basestring):
+ raise SyntaxError, "name must be a string"
+ if not isinstance(self.target, dict) or len(self.target) == 0:
+ raise SyntaxError, "target must be a non-empty dictionary"
+ return True
+
+def extract_group_cns(aci_list, client):
+ """Extracts all the cn's from a list of aci's and returns them as a hash
+ from group_dn to group_cn.
+
+ It first tries to cheat by looking at the first rdn for the
+ group dn. If that's not cn for some reason, it looks up the group."""
+ group_dn_to_cn = {}
+ for aci in aci_list:
+ for dn in (aci.source_group, aci.dest_group):
+ if not group_dn_to_cn.has_key(dn):
+ rdn_list = ldap.explode_dn(dn, 0)
+ first_rdn = rdn_list[0]
+ (type,value) = first_rdn.split('=')
+ if type == "cn":
+ group_dn_to_cn[dn] = value
+ else:
+ try:
+ group = client.get_entry_by_dn(dn, ['cn'])
+ group_dn_to_cn[dn] = group.getValue('cn')
+ except ipaerror.IPAError, e:
+ group_dn_to_cn[dn] = 'unknown'
+
+ return group_dn_to_cn
+
+if __name__ == '__main__':
+ # Pass in an ACI as a string
+ a = ACI('(targetattr="title")(targetfilter="(memberOf=cn=bar,cn=groups,cn=accounts ,dc=example,dc=com)")(version 3.0;acl "foobar";allow (write) groupdn="ldap:///cn=foo,cn=groups,cn=accounts,dc=example,dc=com";)')
+ print a
+
+ # Create an ACI in pieces
+ a = ACI()
+ a.name ="foobar"
+ a.source_group="cn=foo,cn=groups,dc=example,dc=org"
+ a.dest_group="cn=bar,cn=groups,dc=example,dc=org"
+ a.attrs = ['title']
+ a.permissions = ['read','write','add']
+ print a
diff --git a/ipalib/backend.py b/ipalib/backend.py
new file mode 100644
index 000000000..b1e15f337
--- /dev/null
+++ b/ipalib/backend.py
@@ -0,0 +1,45 @@
+# 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
+
+"""
+Base classes for all backed-end plugins.
+"""
+
+import plugable
+
+
+class Backend(plugable.Plugin):
+ """
+ Base class for all backend plugins.
+ """
+
+ __proxy__ = False # Backend plugins are not wrapped in a PluginProxy
+
+
+class Context(plugable.Plugin):
+ """
+ Base class for plugable context components.
+ """
+
+ __proxy__ = False # Backend plugins are not wrapped in a PluginProxy
+
+ def get_value(self):
+ raise NotImplementedError(
+ '%s.get_value()' % self.__class__.__name__
+ )
diff --git a/ipalib/base.py b/ipalib/base.py
new file mode 100644
index 000000000..bff8f1951
--- /dev/null
+++ b/ipalib/base.py
@@ -0,0 +1,486 @@
+# 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
+
+"""
+Foundational classes and functions.
+"""
+
+import re
+from constants import NAME_REGEX, NAME_ERROR
+from constants import TYPE_ERROR, SET_ERROR, DEL_ERROR, OVERRIDE_ERROR
+
+
+class ReadOnly(object):
+ """
+ Base class for classes that can be locked into a read-only state.
+
+ Be forewarned that Python does not offer true read-only attributes for
+ user-defined classes. Do *not* rely upon the read-only-ness of this
+ class for security purposes!
+
+ The point of this class is not to make it impossible to set or to delete
+ attributes after an instance is locked, but to make it impossible to do so
+ *accidentally*. Rather than constantly reminding our programmers of things
+ like, for example, "Don't set any attributes on this ``FooBar`` instance
+ because doing so wont be thread-safe", this class offers a real way to
+ enforce read-only attribute usage.
+
+ For example, before a `ReadOnly` instance is locked, you can set and delete
+ its attributes as normal:
+
+ >>> class Person(ReadOnly):
+ ... pass
+ ...
+ >>> p = Person()
+ >>> p.name = 'John Doe'
+ >>> p.phone = '123-456-7890'
+ >>> del p.phone
+
+ But after an instance is locked, you cannot set its attributes:
+
+ >>> p.__islocked__() # Is this instance locked?
+ False
+ >>> p.__lock__() # This will lock the instance
+ >>> p.__islocked__()
+ True
+ >>> p.department = 'Engineering'
+ Traceback (most recent call last):
+ ...
+ AttributeError: locked: cannot set Person.department to 'Engineering'
+
+ Nor can you deleted its attributes:
+
+ >>> del p.name
+ Traceback (most recent call last):
+ ...
+ AttributeError: locked: cannot delete Person.name
+
+ However, as noted at the start, there are still obscure ways in which
+ attributes can be set or deleted on a locked `ReadOnly` instance. For
+ example:
+
+ >>> object.__setattr__(p, 'department', 'Engineering')
+ >>> p.department
+ 'Engineering'
+ >>> object.__delattr__(p, 'name')
+ >>> hasattr(p, 'name')
+ False
+
+ But again, the point is that a programmer would never employ the above
+ techniques *accidentally*.
+
+ Lastly, this example aside, you should use the `lock()` function rather
+ than the `ReadOnly.__lock__()` method. And likewise, you should
+ use the `islocked()` function rather than the `ReadOnly.__islocked__()`
+ method. For example:
+
+ >>> readonly = ReadOnly()
+ >>> islocked(readonly)
+ False
+ >>> lock(readonly) is readonly # lock() returns the instance
+ True
+ >>> islocked(readonly)
+ True
+ """
+
+ __locked = False
+
+ def __lock__(self):
+ """
+ Put this instance into a read-only state.
+
+ After the instance has been locked, attempting to set or delete an
+ attribute will raise an AttributeError.
+ """
+ assert self.__locked is False, '__lock__() can only be called once'
+ self.__locked = True
+
+ def __islocked__(self):
+ """
+ Return True if instance is locked, otherwise False.
+ """
+ return self.__locked
+
+ def __setattr__(self, name, value):
+ """
+ If unlocked, set attribute named ``name`` to ``value``.
+
+ If this instance is locked, an AttributeError will be raised.
+
+ :param name: Name of attribute to set.
+ :param value: Value to assign to attribute.
+ """
+ if self.__locked:
+ raise AttributeError(
+ SET_ERROR % (self.__class__.__name__, name, value)
+ )
+ return object.__setattr__(self, name, value)
+
+ def __delattr__(self, name):
+ """
+ If unlocked, delete attribute named ``name``.
+
+ If this instance is locked, an AttributeError will be raised.
+
+ :param name: Name of attribute to delete.
+ """
+ if self.__locked:
+ raise AttributeError(
+ DEL_ERROR % (self.__class__.__name__, name)
+ )
+ return object.__delattr__(self, name)
+
+
+def lock(instance):
+ """
+ Lock an instance of the `ReadOnly` class or similar.
+
+ This function can be used to lock instances of any class that implements
+ the same locking API as the `ReadOnly` class. For example, this function
+ can lock instances of the `config.Env` class.
+
+ So that this function can be easily used within an assignment, ``instance``
+ is returned after it is locked. For example:
+
+ >>> readonly = ReadOnly()
+ >>> readonly is lock(readonly)
+ True
+ >>> readonly.attr = 'This wont work'
+ Traceback (most recent call last):
+ ...
+ AttributeError: locked: cannot set ReadOnly.attr to 'This wont work'
+
+ Also see the `islocked()` function.
+
+ :param instance: The instance of `ReadOnly` (or similar) to lock.
+ """
+ assert instance.__islocked__() is False, 'already locked: %r' % instance
+ instance.__lock__()
+ assert instance.__islocked__() is True, 'failed to lock: %r' % instance
+ return instance
+
+
+def islocked(instance):
+ """
+ Return ``True`` if ``instance`` is locked.
+
+ This function can be used on an instance of the `ReadOnly` class or an
+ instance of any other class implemented the same locking API.
+
+ For example:
+
+ >>> readonly = ReadOnly()
+ >>> islocked(readonly)
+ False
+ >>> readonly.__lock__()
+ >>> islocked(readonly)
+ True
+
+ Also see the `lock()` function.
+
+ :param instance: The instance of `ReadOnly` (or similar) to interrogate.
+ """
+ assert (
+ hasattr(instance, '__lock__') and callable(instance.__lock__)
+ ), 'no __lock__() method: %r' % instance
+ return instance.__islocked__()
+
+
+def check_name(name):
+ """
+ Verify that ``name`` is suitable for a `NameSpace` member name.
+
+ In short, ``name`` must be a valid lower-case Python identifier that
+ neither starts nor ends with an underscore. Otherwise an exception is
+ raised.
+
+ This function will raise a ``ValueError`` if ``name`` does not match the
+ `constants.NAME_REGEX` regular expression. For example:
+
+ >>> check_name('MyName')
+ Traceback (most recent call last):
+ ...
+ ValueError: name must match '^[a-z][_a-z0-9]*[a-z0-9]$'; got 'MyName'
+
+ Also, this function will raise a ``TypeError`` if ``name`` is not an
+ ``str`` instance. For example:
+
+ >>> check_name(u'my_name')
+ Traceback (most recent call last):
+ ...
+ TypeError: name: need a <type 'str'>; got u'my_name' (a <type 'unicode'>)
+
+ So that `check_name()` can be easily used within an assignment, ``name``
+ is returned unchanged if it passes the check. For example:
+
+ >>> n = check_name('my_name')
+ >>> n
+ 'my_name'
+
+ :param name: Identifier to test.
+ """
+ if type(name) is not str:
+ raise TypeError(
+ TYPE_ERROR % ('name', str, name, type(name))
+ )
+ if re.match(NAME_REGEX, name) is None:
+ raise ValueError(
+ NAME_ERROR % (NAME_REGEX, name)
+ )
+ return name
+
+
+class NameSpace(ReadOnly):
+ """
+ A read-only name-space with handy container behaviours.
+
+ A `NameSpace` instance is an ordered, immutable mapping object whose values
+ can also be accessed as attributes. A `NameSpace` instance is constructed
+ from an iterable providing its *members*, which are simply arbitrary objects
+ with a ``name`` attribute whose value:
+
+ 1. Is unique among the members
+
+ 2. Passes the `check_name()` function
+
+ Beyond that, no restrictions are placed on the members: they can be
+ classes or instances, and of any type.
+
+ The members can be accessed as attributes on the `NameSpace` instance or
+ through a dictionary interface. For example, say we create a `NameSpace`
+ instance from a list containing a single member, like this:
+
+ >>> class my_member(object):
+ ... name = 'my_name'
+ ...
+ >>> namespace = NameSpace([my_member])
+ >>> namespace
+ NameSpace(<1 member>, sort=True)
+
+ We can then access ``my_member`` both as an attribute and as a dictionary
+ item:
+
+ >>> my_member is namespace.my_name # As an attribute
+ True
+ >>> my_member is namespace['my_name'] # As dictionary item
+ True
+
+ For a more detailed example, say we create a `NameSpace` instance from a
+ generator like this:
+
+ >>> class Member(object):
+ ... def __init__(self, i):
+ ... self.i = i
+ ... self.name = 'member%d' % i
+ ... def __repr__(self):
+ ... return 'Member(%d)' % self.i
+ ...
+ >>> ns = NameSpace(Member(i) for i in xrange(3))
+ >>> ns
+ NameSpace(<3 members>, sort=True)
+
+ As above, the members can be accessed as attributes and as dictionary items:
+
+ >>> ns.member0 is ns['member0']
+ True
+ >>> ns.member1 is ns['member1']
+ True
+ >>> ns.member2 is ns['member2']
+ True
+
+ Members can also be accessed by index and by slice. For example:
+
+ >>> ns[0]
+ Member(0)
+ >>> ns[-1]
+ Member(2)
+ >>> ns[1:]
+ (Member(1), Member(2))
+
+ (Note that slicing a `NameSpace` returns a ``tuple``.)
+
+ `NameSpace` instances provide standard container emulation for membership
+ testing, counting, and iteration. For example:
+
+ >>> 'member3' in ns # Is there a member named 'member3'?
+ False
+ >>> 'member2' in ns # But there is a member named 'member2'
+ True
+ >>> len(ns) # The number of members
+ 3
+ >>> list(ns) # Iterate through the member names
+ ['member0', 'member1', 'member2']
+
+ Although not a standard container feature, the `NameSpace.__call__()` method
+ provides a convenient (and efficient) way to iterate through the *members*
+ (as opposed to the member names). Think of it like an ordered version of
+ the ``dict.itervalues()`` method. For example:
+
+ >>> list(ns[name] for name in ns) # One way to do it
+ [Member(0), Member(1), Member(2)]
+ >>> list(ns()) # A more efficient, simpler way to do it
+ [Member(0), Member(1), Member(2)]
+
+ Another convenience method is `NameSpace.__todict__()`, which will return
+ a copy of the ``dict`` mapping the member names to the members.
+ For example:
+
+ >>> ns.__todict__()
+ {'member1': Member(1), 'member0': Member(0), 'member2': Member(2)}
+
+ As `NameSpace.__init__()` locks the instance, `NameSpace` instances are
+ read-only from the get-go. An ``AttributeError`` is raised if you try to
+ set *any* attribute on a `NameSpace` instance. For example:
+
+ >>> ns.member3 = Member(3) # Lets add that missing 'member3'
+ Traceback (most recent call last):
+ ...
+ AttributeError: locked: cannot set NameSpace.member3 to Member(3)
+
+ (For information on the locking protocol, see the `ReadOnly` class, of which
+ `NameSpace` is a subclass.)
+
+ By default the members will be sorted alphabetically by the member name.
+ For example:
+
+ >>> sorted_ns = NameSpace([Member(7), Member(3), Member(5)])
+ >>> sorted_ns
+ NameSpace(<3 members>, sort=True)
+ >>> list(sorted_ns)
+ ['member3', 'member5', 'member7']
+ >>> sorted_ns[0]
+ Member(3)
+
+ But if the instance is created with the ``sort=False`` keyword argument, the
+ original order of the members is preserved. For example:
+
+ >>> unsorted_ns = NameSpace([Member(7), Member(3), Member(5)], sort=False)
+ >>> unsorted_ns
+ NameSpace(<3 members>, sort=False)
+ >>> list(unsorted_ns)
+ ['member7', 'member3', 'member5']
+ >>> unsorted_ns[0]
+ Member(7)
+
+ The `NameSpace` class is used in many places throughout freeIPA. For a few
+ examples, see the `plugable.API` and the `frontend.Command` classes.
+ """
+
+ def __init__(self, members, sort=True):
+ """
+ :param members: An iterable providing the members.
+ :param sort: Whether to sort the members by member name.
+ """
+ if type(sort) is not bool:
+ raise TypeError(
+ TYPE_ERROR % ('sort', bool, sort, type(sort))
+ )
+ self.__sort = sort
+ if sort:
+ self.__members = tuple(
+ sorted(members, key=lambda m: m.name)
+ )
+ else:
+ self.__members = tuple(members)
+ self.__names = tuple(m.name for m in self.__members)
+ self.__map = dict()
+ for member in self.__members:
+ name = check_name(member.name)
+ if name in self.__map:
+ raise AttributeError(OVERRIDE_ERROR %
+ (self.__class__.__name__, name, self.__map[name], member)
+ )
+ assert not hasattr(self, name), 'Ouch! Has attribute %r' % name
+ self.__map[name] = member
+ setattr(self, name, member)
+ lock(self)
+
+ def __len__(self):
+ """
+ Return the number of members.
+ """
+ return len(self.__members)
+
+ def __iter__(self):
+ """
+ Iterate through the member names.
+
+ If this instance was created with ``sort=False``, the names will be in
+ the same order as the members were passed to the constructor; otherwise
+ the names will be in alphabetical order (which is the default).
+
+ This method is like an ordered version of ``dict.iterkeys()``.
+ """
+ for name in self.__names:
+ yield name
+
+ def __call__(self):
+ """
+ Iterate through the members.
+
+ If this instance was created with ``sort=False``, the members will be
+ in the same order as they were passed to the constructor; otherwise the
+ members will be in alphabetical order by name (which is the default).
+
+ This method is like an ordered version of ``dict.itervalues()``.
+ """
+ for member in self.__members:
+ yield member
+
+ def __contains__(self, name):
+ """
+ Return ``True`` if namespace has a member named ``name``.
+ """
+ return name in self.__map
+
+ def __getitem__(self, key):
+ """
+ Return a member by name or index, or return a slice of members.
+
+ :param key: The name or index of a member, or a slice object.
+ """
+ if type(key) is str:
+ return self.__map[key]
+ if type(key) in (int, slice):
+ return self.__members[key]
+ raise TypeError(
+ TYPE_ERROR % ('key', (str, int, slice), key, type(key))
+ )
+
+ def __repr__(self):
+ """
+ Return a pseudo-valid expression that could create this instance.
+ """
+ cnt = len(self)
+ if cnt == 1:
+ m = 'member'
+ else:
+ m = 'members'
+ return '%s(<%d %s>, sort=%r)' % (
+ self.__class__.__name__,
+ cnt,
+ m,
+ self.__sort,
+ )
+
+ def __todict__(self):
+ """
+ Return a copy of the private dict mapping member name to member.
+ """
+ return dict(self.__map)
diff --git a/ipalib/cli.py b/ipalib/cli.py
new file mode 100644
index 000000000..fb2fd95f4
--- /dev/null
+++ b/ipalib/cli.py
@@ -0,0 +1,854 @@
+# 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
+
+"""
+Functionality for Command Line Interface.
+"""
+
+import re
+import textwrap
+import sys
+import getpass
+import code
+import optparse
+import socket
+import fcntl
+import termios
+import struct
+
+import frontend
+import backend
+import errors
+import plugable
+import util
+from constants import CLI_TAB
+from parameters import Password
+
+
+def to_cli(name):
+ """
+ Takes a Python identifier and transforms it into form suitable for the
+ Command Line Interface.
+ """
+ assert isinstance(name, str)
+ return name.replace('_', '-')
+
+
+def from_cli(cli_name):
+ """
+ Takes a string from the Command Line Interface and transforms it into a
+ Python identifier.
+ """
+ return str(cli_name).replace('-', '_')
+
+
+class textui(backend.Backend):
+ """
+ Backend plugin to nicely format output to stdout.
+ """
+
+ def get_tty_width(self):
+ """
+ Return the width (in characters) of output tty.
+
+ If stdout is not a tty, this method will return ``None``.
+ """
+ # /usr/include/asm/termios.h says that struct winsize has four
+ # unsigned shorts, hence the HHHH
+ if sys.stdout.isatty():
+ try:
+ winsize = fcntl.ioctl(sys.stdout, termios.TIOCGWINSZ,
+ struct.pack('HHHH', 0, 0, 0, 0))
+ return struct.unpack('HHHH', winsize)[1]
+ except IOError:
+ return None
+
+ def max_col_width(self, rows, col=None):
+ """
+ Return the max width (in characters) of a specified column.
+
+ For example:
+
+ >>> ui = textui()
+ >>> rows = [
+ ... ('a', 'package'),
+ ... ('an', 'egg'),
+ ... ]
+ >>> ui.max_col_width(rows, col=0) # len('an')
+ 2
+ >>> ui.max_col_width(rows, col=1) # len('package')
+ 7
+ >>> ui.max_col_width(['a', 'cherry', 'py']) # len('cherry')
+ 6
+ """
+ if type(rows) not in (list, tuple):
+ raise TypeError(
+ 'rows: need %r or %r; got %r' % (list, tuple, rows)
+ )
+ if len(rows) == 0:
+ return 0
+ if col is None:
+ return max(len(row) for row in rows)
+ return max(len(row[col]) for row in rows)
+
+ def __get_encoding(self, stream):
+ assert stream in (sys.stdin, sys.stdout)
+ if stream.encoding is None:
+ if stream.isatty():
+ return sys.getdefaultencoding()
+ return 'UTF-8'
+ return stream.encoding
+
+ def decode(self, str_buffer):
+ """
+ Decode text from stdin.
+ """
+ assert type(str_buffer) is str
+ encoding = self.__get_encoding(sys.stdin)
+ return str_buffer.decode(encoding)
+
+ def encode(self, unicode_text):
+ """
+ Encode text for output to stdout.
+ """
+ assert type(unicode_text) is unicode
+ encoding = self.__get_encoding(sys.stdout)
+ return unicode_text.encode(encoding)
+
+ def choose_number(self, n, singular, plural=None):
+ if n == 1 or plural is None:
+ return singular % n
+ return plural % n
+
+ def print_plain(self, string):
+ """
+ Print exactly like ``print`` statement would.
+ """
+ print string
+
+ def print_line(self, text, width=None):
+ """
+ Force printing on a single line, using ellipsis if needed.
+
+ For example:
+
+ >>> ui = textui()
+ >>> ui.print_line('This line can fit!', width=18)
+ This line can fit!
+ >>> ui.print_line('This line wont quite fit!', width=18)
+ This line wont ...
+
+ The above example aside, you normally should not specify the
+ ``width``. When you don't, it is automatically determined by calling
+ `textui.get_tty_width()`.
+ """
+ if width is None:
+ width = self.get_tty_width()
+ if width is not None and width < len(text):
+ text = text[:width - 3] + '...'
+ print text
+
+ def print_paragraph(self, text, width=None):
+ """
+ Print a paragraph, automatically word-wrapping to tty width.
+
+ For example:
+
+ >>> text = '''
+ ... Python is a dynamic object-oriented programming language that can
+ ... be used for many kinds of software development.
+ ... '''
+ >>> ui = textui()
+ >>> ui.print_paragraph(text, width=45)
+ Python is a dynamic object-oriented
+ programming language that can be used for
+ many kinds of software development.
+
+ The above example aside, you normally should not specify the
+ ``width``. When you don't, it is automatically determined by calling
+ `textui.get_tty_width()`.
+
+ The word-wrapping is done using the Python ``textwrap`` module. See:
+
+ http://docs.python.org/library/textwrap.html
+ """
+ if width is None:
+ width = self.get_tty_width()
+ for line in textwrap.wrap(text.strip(), width):
+ print line
+
+ def print_indented(self, text, indent=1):
+ """
+ Print at specified indentation level.
+
+ For example:
+
+ >>> ui = textui()
+ >>> ui.print_indented('One indentation level.')
+ One indentation level.
+ >>> ui.print_indented('Two indentation levels.', indent=2)
+ Two indentation levels.
+ >>> ui.print_indented('No indentation.', indent=0)
+ No indentation.
+ """
+ print (CLI_TAB * indent + text)
+
+ def print_keyval(self, rows, indent=1):
+ """
+ Print (key = value) pairs, one pair per line.
+
+ For example:
+
+ >>> items = [
+ ... ('in_server', True),
+ ... ('mode', 'production'),
+ ... ]
+ >>> ui = textui()
+ >>> ui.print_keyval(items)
+ in_server = True
+ mode = 'production'
+ >>> ui.print_keyval(items, indent=0)
+ in_server = True
+ mode = 'production'
+
+ Also see `textui.print_indented`.
+ """
+ for (key, value) in rows:
+ self.print_indented('%s = %r' % (key, value), indent)
+
+ def print_entry(self, entry, indent=1):
+ """
+ Print an ldap entry dict.
+
+ For example:
+
+ >>> entry = dict(sn='Last', givenname='First', uid='flast')
+ >>> ui = textui()
+ >>> ui.print_entry(entry)
+ givenname: 'First'
+ sn: 'Last'
+ uid: 'flast'
+ """
+ assert type(entry) is dict
+ for key in sorted(entry):
+ value = entry[key]
+ if type(value) in (list, tuple):
+ value = ', '.join(repr(v) for v in value)
+ else:
+ value = repr(value)
+ self.print_indented('%s: %s' % (key, value), indent)
+
+ def print_dashed(self, string, above=True, below=True, indent=0, dash='-'):
+ """
+ Print a string with a dashed line above and/or below.
+
+ For example:
+
+ >>> ui = textui()
+ >>> ui.print_dashed('Dashed above and below.')
+ -----------------------
+ Dashed above and below.
+ -----------------------
+ >>> ui.print_dashed('Only dashed below.', above=False)
+ Only dashed below.
+ ------------------
+ >>> ui.print_dashed('Only dashed above.', below=False)
+ ------------------
+ Only dashed above.
+ """
+ assert isinstance(dash, basestring)
+ assert len(dash) == 1
+ dashes = dash * len(string)
+ if above:
+ self.print_indented(dashes, indent)
+ self.print_indented(string, indent)
+ if below:
+ self.print_indented(dashes, indent)
+
+ def print_h1(self, text):
+ """
+ Print a primary header at indentation level 0.
+
+ For example:
+
+ >>> ui = textui()
+ >>> ui.print_h1('A primary header')
+ ================
+ A primary header
+ ================
+ """
+ self.print_dashed(text, indent=0, dash='=')
+
+ def print_h2(self, text):
+ """
+ Print a secondary header at indentation level 1.
+
+ For example:
+
+ >>> ui = textui()
+ >>> ui.print_h2('A secondary header')
+ ------------------
+ A secondary header
+ ------------------
+ """
+ self.print_dashed(text, indent=1, dash='-')
+
+ def print_name(self, name):
+ """
+ Print a command name.
+
+ The typical use for this is to mark the start of output from a
+ command. For example, a hypothetical ``show_status`` command would
+ output something like this:
+
+ >>> ui = textui()
+ >>> ui.print_name('show_status')
+ ------------
+ show-status:
+ ------------
+ """
+ self.print_dashed('%s:' % to_cli(name))
+
+ def print_count(self, count, singular, plural=None):
+ """
+ Print a summary count.
+
+ The typical use for this is to print the number of items returned
+ by a command, especially when this return count can vary. This
+ preferably should be used as a summary and should be the final text
+ a command outputs.
+
+ For example:
+
+ >>> ui = textui()
+ >>> ui.print_count(1, '%d goose', '%d geese')
+ -------
+ 1 goose
+ -------
+ >>> ui.print_count(['Don', 'Sue'], 'Found %d user', 'Found %d users')
+ -------------
+ Found 2 users
+ -------------
+
+ If ``count`` is not an integer, it must be a list or tuple, and then
+ ``len(count)`` is used as the count.
+ """
+ if type(count) is not int:
+ assert type(count) in (list, tuple)
+ count = len(count)
+ self.print_dashed(
+ self.choose_number(count, singular, plural)
+ )
+
+ def prompt(self, label, default=None, get_values=None):
+ """
+ Prompt user for input.
+ """
+ # TODO: Add tab completion using readline
+ if default is None:
+ prompt = u'%s: ' % label
+ else:
+ prompt = u'%s [%s]: ' % (label, default)
+ return self.decode(
+ raw_input(self.encode(prompt))
+ )
+
+ def prompt_password(self, label):
+ """
+ Prompt user for a password.
+ """
+ try:
+ while True:
+ pw1 = getpass.getpass('%s: ' % label)
+ pw2 = getpass.getpass('Enter again to verify: ')
+ if pw1 == pw2:
+ return self.decode(pw1)
+ print ' ** Passwords do not match. Please enter again. **'
+ except KeyboardInterrupt:
+ print ''
+ print ' ** Cancelled. **'
+
+
+class help(frontend.Application):
+ '''Display help on a command.'''
+
+ takes_args = ['command?']
+
+ def run(self, command):
+ textui = self.Backend.textui
+ if command is None:
+ self.print_commands()
+ return
+ key = str(command)
+ if key not in self.application:
+ raise errors.UnknownHelpError(key)
+ cmd = self.application[key]
+ print 'Purpose: %s' % cmd.doc
+ self.application.build_parser(cmd).print_help()
+
+ def print_commands(self):
+ std = set(self.Command) - set(self.Application)
+ print '\nStandard IPA commands:'
+ for key in sorted(std):
+ cmd = self.api.Command[key]
+ self.print_cmd(cmd)
+ print '\nSpecial CLI commands:'
+ for cmd in self.api.Application():
+ self.print_cmd(cmd)
+ print '\nUse the --help option to see all the global options'
+ print ''
+
+ def print_cmd(self, cmd):
+ print ' %s %s' % (
+ to_cli(cmd.name).ljust(self.application.mcl),
+ cmd.doc,
+ )
+
+
+class console(frontend.Application):
+ """Start the IPA interactive Python console."""
+
+ def run(self):
+ code.interact(
+ '(Custom IPA interactive Python console)',
+ local=dict(api=self.api)
+ )
+
+
+class show_api(frontend.Application):
+ 'Show attributes on dynamic API object'
+
+ takes_args = ('namespaces*',)
+
+ def run(self, namespaces):
+ if namespaces is None:
+ names = tuple(self.api)
+ else:
+ for name in namespaces:
+ if name not in self.api:
+ raise errors.NoSuchNamespaceError(name)
+ names = namespaces
+ lines = self.__traverse(names)
+ ml = max(len(l[1]) for l in lines)
+ self.Backend.textui.print_name('run')
+ first = True
+ for line in lines:
+ if line[0] == 0 and not first:
+ print ''
+ if first:
+ first = False
+ print '%s%s %r' % (
+ ' ' * line[0],
+ line[1].ljust(ml),
+ line[2],
+ )
+ if len(lines) == 1:
+ s = '1 attribute shown.'
+ else:
+ s = '%d attributes show.' % len(lines)
+ self.Backend.textui.print_dashed(s)
+
+
+ def __traverse(self, names):
+ lines = []
+ for name in names:
+ namespace = self.api[name]
+ self.__traverse_namespace('%s' % name, namespace, lines)
+ return lines
+
+ def __traverse_namespace(self, name, namespace, lines, tab=0):
+ lines.append((tab, name, namespace))
+ for member_name in namespace:
+ member = namespace[member_name]
+ lines.append((tab + 1, member_name, member))
+ if not hasattr(member, '__iter__'):
+ continue
+ for n in member:
+ attr = member[n]
+ if isinstance(attr, plugable.NameSpace) and len(attr) > 0:
+ self.__traverse_namespace(n, attr, lines, tab + 2)
+
+
+cli_application_commands = (
+ help,
+ console,
+ show_api,
+)
+
+
+class KWCollector(object):
+ def __init__(self):
+ object.__setattr__(self, '_KWCollector__d', {})
+
+ def __setattr__(self, name, value):
+ if name in self.__d:
+ v = self.__d[name]
+ if type(v) is tuple:
+ value = v + (value,)
+ else:
+ value = (v, value)
+ self.__d[name] = value
+ object.__setattr__(self, name, value)
+
+ def __todict__(self):
+ return dict(self.__d)
+
+
+class CLI(object):
+ """
+ All logic for dispatching over command line interface.
+ """
+
+ __d = None
+ __mcl = None
+
+ def __init__(self, api, argv):
+ self.api = api
+ self.argv = tuple(argv)
+ self.__done = set()
+
+ def run(self):
+ """
+ Call `CLI.run_real` in a try/except.
+ """
+ self.bootstrap()
+ try:
+ self.run_real()
+ except KeyboardInterrupt:
+ print ''
+ self.api.log.info('operation aborted')
+ sys.exit()
+ except errors.IPAError, e:
+ self.api.log.error(unicode(e))
+ sys.exit(e.faultCode)
+
+ def run_real(self):
+ """
+ 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_real')
+ self.finalize()
+ if self.api.env.mode == 'unit_test':
+ return
+ if len(self.cmd_argv) < 1:
+ self.api.Command.help()
+ return
+ key = self.cmd_argv[0]
+ if key not in self:
+ raise errors.UnknownCommandError(key)
+ self.run_cmd(self[key])
+
+ # FIXME: Stuff that might need special handling still:
+# # Now run the command
+# try:
+# ret = cmd(**kw)
+# if callable(cmd.output_for_cli):
+# (args, options) = cmd.params_2_args_options(kw)
+# cmd.output_for_cli(self.api.Backend.textui, ret, *args, **options)
+# return 0
+# except socket.error, e:
+# print e[1]
+# return 1
+# except errors.GenericError, err:
+# code = getattr(err,'faultCode',None)
+# faultString = getattr(err,'faultString',None)
+# if not code:
+# raise err
+# if code < errors.IPA_ERROR_BASE:
+# print "%s: %s" % (code, faultString)
+# else:
+# print "%s: %s" % (code, getattr(err,'__doc__',''))
+# return 1
+# except StandardError, e:
+# print e
+# return 2
+
+ 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.
+
+ 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()
+ )
+ self.textui = self.api.Backend.textui
+
+ 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')
+ if 'bootstrap' not in self.__done:
+ self.bootstrap()
+ self.api.load_plugins()
+ for klass in cli_application_commands:
+ self.api.register(klass)
+ self.api.register(textui)
+
+ 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.bootstrap_with_global_options(self.options, context='cli')
+
+ 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.
+
+ The common global options are added using the
+ `util.add_global_options` function.
+ """
+ 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.set_defaults(
+ prompt_all=False,
+ interactive=True,
+ )
+ util.add_global_options(parser)
+ (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 run_cmd(self, cmd):
+ kw = self.parse(cmd)
+ if self.options.interactive:
+ self.prompt_interactively(cmd, kw)
+ self.prompt_for_passwords(cmd, kw)
+ self.set_defaults(cmd, kw)
+ result = cmd(**kw)
+ if callable(cmd.output_for_cli):
+ for param in cmd.params():
+ if isinstance(param, Password):
+ try:
+ del kw[param.name]
+ except KeyError:
+ pass
+ (args, options) = cmd.params_2_args_options(kw)
+ cmd.output_for_cli(self.api.Backend.textui, result, *args, **options)
+
+ def set_defaults(self, cmd, kw):
+ for param in cmd.params():
+ if not kw.get(param.name):
+ value = param.get_default(**kw)
+ if value:
+ kw[param.name] = value
+
+ def prompt_for_passwords(self, cmd, kw):
+ for param in cmd.params():
+ if 'password' not in param.flags:
+ continue
+ if kw.get(param.name, False) is True or param.name in cmd.args:
+ kw[param.name] = self.textui.prompt_password(
+ param.cli_name
+ )
+ else:
+ kw.pop(param.name, None)
+ return 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 'password' in param.flags:
+ continue
+ elif param.name not in kw:
+ if not (param.required or self.options.prompt_all):
+ continue
+ default = param.get_default(**kw)
+ error = None
+ while True:
+ if error is not None:
+ print '>>> %s: %s' % (param.cli_name, error)
+ raw = self.textui.prompt(param.cli_name, default)
+ try:
+ value = param(raw, **kw)
+ if value is not None:
+ kw[param.name] = value
+ break
+ except errors.ValidationError, e:
+ error = e.error
+ return kw
+
+# FIXME: This should be done as the plugins are loaded
+# if self.api.env.server_context:
+# try:
+# import krbV
+# import ldap
+# from ipaserver import conn
+# from ipaserver.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(
+ list(self.cmd_argv[1:]), KWCollector()
+ )
+ kw = kwc.__todict__()
+ arg_kw = cmd.args_to_kw(*args)
+ assert set(arg_kw).intersection(kw) == set()
+ kw.update(arg_kw)
+ return kw
+
+ def build_parser(self, cmd):
+ parser = optparse.OptionParser(
+ usage=self.get_usage(cmd),
+ )
+ for option in cmd.options():
+ kw = dict(
+ dest=option.name,
+ help=option.doc,
+ )
+ if 'password' in option.flags:
+ kw['action'] = 'store_true'
+ elif option.type is bool:
+ if option.default is True:
+ kw['action'] = 'store_false'
+ else:
+ kw['action'] = 'store_true'
+ else:
+ kw['metavar'] = metavar=option.__class__.__name__.upper()
+ o = optparse.make_option('--%s' % to_cli(option.cli_name), **kw)
+ parser.add_option(o)
+ return parser
+
+ def get_usage(self, cmd):
+ return ' '.join(self.get_usage_iter(cmd))
+
+ def get_usage_iter(self, cmd):
+ yield 'Usage: %%prog [global-options] %s' % to_cli(cmd.name)
+ for arg in cmd.args():
+ if 'password' in arg.flags:
+ continue
+ name = to_cli(arg.cli_name).upper()
+ if arg.multivalue:
+ name = '%s...' % name
+ if arg.required:
+ yield name
+ else:
+ yield '[%s]' % name
+
+ def __get_mcl(self):
+ """
+ Returns the Max Command Length.
+ """
+ if self.__mcl is None:
+ if self.__d is None:
+ return None
+ 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
new file mode 100644
index 000000000..3544331df
--- /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__()
diff --git a/ipalib/constants.py b/ipalib/constants.py
new file mode 100644
index 000000000..5687c53e6
--- /dev/null
+++ b/ipalib/constants.py
@@ -0,0 +1,153 @@
+# 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 centralised in one file.
+"""
+
+# The parameter system treats all these values as None:
+NULLS = (None, '', u'', tuple(), [])
+
+# regular expression NameSpace member names must match:
+NAME_REGEX = r'^[a-z][_a-z0-9]*[a-z0-9]$'
+
+# Format for ValueError raised when name does not match above regex:
+NAME_ERROR = 'name must match %r; got %r'
+
+# Standard format for TypeError message:
+TYPE_ERROR = '%s: need a %r; got %r (a %r)'
+
+# Stardard format for TypeError message when a callable is expected:
+CALLABLE_ERROR = '%s: need a callable; got %r (which is a %r)'
+
+# Standard format for StandardError message when overriding an attribute:
+OVERRIDE_ERROR = 'cannot override %s.%s value %r with %r'
+
+# Standard format for AttributeError message when a read-only attribute is
+# already locked:
+SET_ERROR = 'locked: cannot set %s.%s to %r'
+DEL_ERROR = 'locked: cannot delete %s.%s'
+
+# Used for a tab (or indentation level) when formatting for CLI:
+CLI_TAB = ' ' # Two spaces
+
+# The section to read in the config files, i.e. [global]
+CONFIG_SECTION = 'global'
+
+
+# Log format for console output
+LOG_FORMAT_STDERR = ': '.join([
+ '%(name)s',
+ '%(levelname)s',
+ '%(message)s',
+])
+
+
+# Log format for console output when env.dubug is True:
+LOG_FORMAT_STDERR_DEBUG = ' '.join([
+ '%(levelname)s',
+ '%(message)r',
+ '%(lineno)d',
+ '%(filename)s',
+])
+
+
+# Tab-delimited log format for file (easy to opened in a spreadsheet):
+LOG_FORMAT_FILE = '\t'.join([
+ '%(asctime)s',
+ '%(levelname)s',
+ '%(message)r', # Using %r for repr() so message is a single line
+ '%(lineno)d',
+ '%(pathname)s',
+])
+
+
+# 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'),
+ ('container_hostgroup', 'cn=hostgroups,cn=accounts'),
+ ('container_automount', 'cn=automount'),
+
+ # 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'),
+
+ # Logging:
+ ('log_format_stderr', LOG_FORMAT_STDERR),
+ ('log_format_stderr_debug', LOG_FORMAT_STDERR_DEBUG),
+ ('log_format_file', LOG_FORMAT_FILE),
+
+ # ********************************************************
+ # 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
+ # at least all the keys that should be present after Env._finalize_core()
+ # is called.
+ #
+ # Each environment variable below is sent to ``object``, which just happens
+ # to be an invalid value for an environment variable, 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', object), # The directory containing ipalib/__init__.py
+ ('site_packages', object), # The directory contaning ipalib
+ ('script', object), # sys.argv[0]
+ ('bin', object), # The directory containing script
+ ('home', object), # The home directory of user underwhich process is running
+ ('dot_ipa', object), # ~/.ipa directory
+
+ # Set in Env._bootstrap():
+ ('in_tree', object), # Whether or not running in-tree (bool)
+ ('context', object), # Name of context, default is 'default'
+ ('conf', object), # Path to config file
+ ('conf_default', object), # Path to common default config file
+ ('conf_dir', object), # Directory containing config files
+
+ # Set in Env._finalize_core():
+ ('in_server', object), # Whether or not running in-server (bool)
+ ('log', object), # Path to log file
+
+)
diff --git a/ipalib/crud.py b/ipalib/crud.py
new file mode 100644
index 000000000..345fc2700
--- /dev/null
+++ b/ipalib/crud.py
@@ -0,0 +1,149 @@
+# 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
+
+"""
+Base classes for standard CRUD operations.
+"""
+
+import backend, frontend, errors
+
+
+class Add(frontend.Method):
+ def get_args(self):
+ yield self.obj.primary_key
+ for arg in self.takes_args:
+ yield arg
+
+ def get_options(self):
+ for param in self.obj.params_minus_pk():
+ yield param
+ for option in self.takes_options:
+ yield option
+
+
+class Get(frontend.Method):
+ def get_args(self):
+ yield self.obj.primary_key
+
+
+class Del(frontend.Method):
+ def get_args(self):
+ yield self.obj.primary_key
+
+ def get_options(self):
+ for option in self.takes_options:
+ yield option
+
+
+class Mod(frontend.Method):
+ def get_args(self):
+ yield self.obj.primary_key
+
+ def get_options(self):
+ for param in self.obj.params_minus_pk():
+ yield param.clone(required=False, query=True)
+ for option in self.takes_options:
+ yield option
+
+
+class Find(frontend.Method):
+ def get_args(self):
+ yield self.obj.primary_key
+
+ def get_options(self):
+ for param in self.obj.params_minus_pk():
+ yield param.clone(required=False, query=True)
+ for option in self.takes_options:
+ yield option
+
+
+class CrudBackend(backend.Backend):
+ """
+ Base class defining generic CRUD backend API.
+ """
+
+ def create(self, **kw):
+ """
+ Create a new entry.
+
+ This method should take key word arguments representing the
+ attributes the created entry will have.
+
+ If this methods constructs the primary_key internally, it should raise
+ an exception if the primary_key was passed. Likewise, if this method
+ requires the primary_key to be passed in from the caller, it should
+ raise an exception if the primary key was *not* passed.
+
+ This method should return a dict of the exact entry as it was created
+ in the backing store, including any automatically created attributes.
+ """
+ raise NotImplementedError('%s.create()' % self.name)
+
+ def retrieve(self, primary_key, attributes):
+ """
+ Retrieve an existing entry.
+
+ This method should take a two arguments: the primary_key of the
+ entry in question and a list of the attributes to be retrieved.
+ If the list of attributes is None then all non-operational
+ attributes will be returned.
+
+ If such an entry exists, this method should return a dict
+ representing that entry. If no such entry exists, this method
+ should return None.
+ """
+ raise NotImplementedError('%s.retrieve()' % self.name)
+
+ def update(self, primary_key, **kw):
+ """
+ Update an existing entry.
+
+ This method should take one required argument, the primary_key of the
+ entry to modify, plus optional keyword arguments for each of the
+ attributes being updated.
+
+ This method should return a dict representing the entry as it now
+ exists in the backing store. If no such entry exists, this method
+ should return None.
+ """
+ raise NotImplementedError('%s.update()' % self.name)
+
+ def delete(self, primary_key):
+ """
+ Delete an existing entry.
+
+ This method should take one required argument, the primary_key of the
+ entry to delete.
+ """
+ raise NotImplementedError('%s.delete()' % self.name)
+
+ def search(self, **kw):
+ """
+ Return entries matching specific criteria.
+
+ This method should take keyword arguments representing the search
+ criteria. If a key is the name of an entry attribute, the value
+ should be treated as a filter on that attribute. The meaning of
+ keys outside this namespace is left to the implementation.
+
+ This method should return and iterable containing the matched
+ entries, where each entry is a dict. If no entries are matched,
+ this method should return an empty iterable.
+ """
+ raise NotImplementedError('%s.search()' % self.name)
diff --git a/ipalib/errors.py b/ipalib/errors.py
new file mode 100644
index 000000000..beb6342d9
--- /dev/null
+++ b/ipalib/errors.py
@@ -0,0 +1,465 @@
+# Authors:
+# Jason Gerard DeRose <jderose@redhat.com>
+#
+# Copyright (C) 2008 Red Hat
+# see file 'COPYING' for use and warranty inmsgion
+#
+# 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 custom errors raised by `ipalib` package.
+
+Also includes a few utility functions for raising exceptions.
+"""
+
+IPA_ERROR_BASE = 1000
+
+TYPE_FORMAT = '%s: need a %r; got %r'
+
+def raise_TypeError(value, type_, name):
+ """
+ Raises a TypeError with a nicely formatted message and helpful attributes.
+
+ The TypeError raised will have three custom attributes:
+
+ ``value`` - The value (of incorrect type) passed as argument.
+
+ ``type`` - The type expected for the argument.
+
+ ``name`` - The name (identifier) of the argument in question.
+
+ There is no edict that all TypeError should be raised with raise_TypeError,
+ but when it fits, use it... it makes the unit tests faster to write and
+ the debugging easier to read.
+
+ Here is an example:
+
+ >>> raise_TypeError(u'Hello, world!', str, 'message')
+ Traceback (most recent call last):
+ File "<stdin>", line 1, in <module>
+ File "ipalib/errors.py", line 65, in raise_TypeError
+ raise e
+ TypeError: message: need a <type 'str'>; got u'Hello, world!'
+
+ :param value: The value (of incorrect type) passed as argument.
+ :param type_: The type expected for the argument.
+ :param name: The name (identifier) of the argument in question.
+ """
+
+ assert type(type_) is type, TYPE_FORMAT % ('type_', type, type_)
+ assert type(value) is not type_, 'value: %r is a %r' % (value, type_)
+ assert type(name) is str, TYPE_FORMAT % ('name', str, name)
+ e = TypeError(TYPE_FORMAT % (name, type_, value))
+ setattr(e, 'value', value)
+ setattr(e, 'type', type_)
+ setattr(e, 'name', name)
+ raise e
+
+
+def check_type(value, type_, name, allow_none=False):
+ assert type(name) is str, TYPE_FORMAT % ('name', str, name)
+ assert type(type_) is type, TYPE_FORMAT % ('type_', type, type_)
+ assert type(allow_none) is bool, TYPE_FORMAT % ('allow_none', bool, allow_none)
+ if value is None and allow_none:
+ return
+ if type(value) is not type_:
+ raise_TypeError(value, type_, name)
+ return value
+
+
+def check_isinstance(value, type_, name, allow_none=False):
+ assert type(type_) is type, TYPE_FORMAT % ('type_', type, type_)
+ assert type(name) is str, TYPE_FORMAT % ('name', str, name)
+ assert type(allow_none) is bool, TYPE_FORMAT % ('allow_none', bool, allow_none)
+ if value is None and allow_none:
+ return
+ if not isinstance(value, type_):
+ raise_TypeError(value, type_, name)
+ return value
+
+
+class IPAError(StandardError):
+ """
+ Base class for all custom IPA errors.
+
+ Use this base class for your custom IPA errors unless there is a
+ specific reason to subclass from AttributeError, KeyError, etc.
+ """
+
+ format = None
+ faultCode = 1
+
+ def __init__(self, *args):
+ self.args = args
+
+ def __str__(self):
+ """
+ Returns the string representation of this exception.
+ """
+ return self.format % self.args
+
+
+class InvocationError(IPAError):
+ pass
+
+
+class UnknownCommandError(InvocationError):
+ format = 'unknown command "%s"'
+
+class NoSuchNamespaceError(InvocationError):
+ format = 'api has no such namespace: %s'
+
+def _(text):
+ return text
+
+
+class SubprocessError(StandardError):
+ def __init__(self, returncode, argv):
+ self.returncode = returncode
+ self.argv = argv
+ StandardError.__init__(self,
+ 'return code %d from %r' % (returncode, argv)
+ )
+
+class HandledError(StandardError):
+ """
+ Base class for errors that can be raised across a remote procedure call.
+ """
+
+ code = 1
+
+ def __init__(self, message=None, **kw):
+ self.kw = kw
+ if message is None:
+ message = self.format % kw
+ StandardError.__init__(self, message)
+
+
+class UnknownError(HandledError):
+ """
+ Raised when the true error is not a handled error.
+ """
+
+ format = _('An unknown internal error has occurred')
+
+
+class CommandError(HandledError):
+ """
+ Raised when an unknown command is called client-side.
+ """
+ format = _('Unknown command %(name)r')
+
+
+class RemoteCommandError(HandledError):
+ format = 'Server at %(uri)r has no command %(name)r'
+
+
+class UnknownHelpError(InvocationError):
+ format = 'no command nor topic "%s"'
+
+
+class ArgumentError(IPAError):
+ """
+ Raised when a command is called with wrong number of arguments.
+ """
+
+ format = '%s %s'
+
+ def __init__(self, command, error):
+ self.command = command
+ self.error = error
+ IPAError.__init__(self, command.name, error)
+
+
+class ValidationError(IPAError):
+ """
+ Base class for all types of validation errors.
+ """
+
+ format = 'invalid %r value %r: %s'
+
+ def __init__(self, name, value, error, index=None):
+ """
+ :param name: The name of the value that failed validation.
+ :param value: The value that failed validation.
+ :param error: The error message describing the failure.
+ :param index: If multivalue, index of value in multivalue tuple
+ """
+ assert type(name) is str
+ assert index is None or (type(index) is int and index >= 0)
+ self.name = name
+ self.value = value
+ self.error = error
+ self.index = index
+ IPAError.__init__(self, name, value, error)
+
+
+class ConversionError(ValidationError):
+ """
+ Raised when a value cannot be converted to the correct type.
+ """
+
+ def __init__(self, name, value, type_, index=None):
+ self.type = type_
+ ValidationError.__init__(self, name, value, type_.conversion_error,
+ index=index,
+ )
+
+
+class RuleError(ValidationError):
+ """
+ Raised when a value fails a validation rule.
+ """
+ def __init__(self, name, value, error, rule, index=None):
+ assert callable(rule)
+ self.rule = rule
+ ValidationError.__init__(self, name, value, error, index=index)
+
+
+class RequirementError(ValidationError):
+ """
+ Raised when a required option was not provided.
+ """
+ def __init__(self, name):
+ ValidationError.__init__(self, name, None, 'Required')
+
+
+class RegistrationError(IPAError):
+ """
+ Base class for errors that occur during plugin registration.
+ """
+
+
+class SubclassError(RegistrationError):
+ """
+ Raised when registering a plugin that is not a subclass of one of the
+ allowed bases.
+ """
+ msg = 'plugin %r not subclass of any base in %r'
+
+ def __init__(self, cls, allowed):
+ self.cls = cls
+ self.allowed = allowed
+
+ def __str__(self):
+ return self.msg % (self.cls, self.allowed)
+
+
+class DuplicateError(RegistrationError):
+ """
+ Raised when registering a plugin whose exact class has already been
+ registered.
+ """
+ msg = '%r at %d was already registered'
+
+ def __init__(self, cls):
+ self.cls = cls
+
+ def __str__(self):
+ return self.msg % (self.cls, id(self.cls))
+
+
+class OverrideError(RegistrationError):
+ """
+ Raised when override=False yet registering a plugin that overrides an
+ existing plugin in the same namespace.
+ """
+ msg = 'unexpected override of %s.%s with %r (use override=True if intended)'
+
+ def __init__(self, base, cls):
+ self.base = base
+ self.cls = cls
+
+ def __str__(self):
+ return self.msg % (self.base.__name__, self.cls.__name__, self.cls)
+
+
+class MissingOverrideError(RegistrationError):
+ """
+ Raised when override=True yet no preexisting plugin with the same name
+ and base has been registered.
+ """
+ msg = '%s.%s has not been registered, cannot override with %r'
+
+ def __init__(self, base, cls):
+ self.base = base
+ self.cls = cls
+
+ def __str__(self):
+ return self.msg % (self.base.__name__, self.cls.__name__, self.cls)
+
+class GenericError(IPAError):
+ """Base class for our custom exceptions"""
+ faultCode = 1000
+ fromFault = False
+ def __str__(self):
+ try:
+ return str(self.args[0]['args'][0])
+ except:
+ try:
+ return str(self.args[0])
+ except:
+ return str(self.__dict__)
+
+class DatabaseError(GenericError):
+ """A database error has occurred"""
+ faultCode = 1001
+
+class MidairCollision(GenericError):
+ """Change collided with another change"""
+ faultCode = 1002
+
+class NotFound(GenericError):
+ """Entry not found"""
+ faultCode = 1003
+
+class DuplicateEntry(GenericError):
+ """This entry already exists"""
+ faultCode = 1004
+
+class MissingDN(GenericError):
+ """The distinguished name (DN) is missing"""
+ faultCode = 1005
+
+class EmptyModlist(GenericError):
+ """No modifications to be performed"""
+ faultCode = 1006
+
+class InputError(GenericError):
+ """Error on input"""
+ faultCode = 1007
+
+class SameGroupError(InputError):
+ """You can't add a group to itself"""
+ faultCode = 1008
+
+class NotGroupMember(InputError):
+ """This entry is not a member of the group"""
+ faultCode = 1009
+
+class AdminsImmutable(InputError):
+ """The admins group cannot be renamed"""
+ faultCode = 1010
+
+class UsernameTooLong(InputError):
+ """The requested username is too long"""
+ faultCode = 1011
+
+class PrincipalError(GenericError):
+ """There is a problem with the kerberos principal"""
+ faultCode = 1012
+
+class MalformedServicePrincipal(PrincipalError):
+ """The requested service principal is not of the form: service/fully-qualified host name"""
+ faultCode = 1013
+
+class RealmMismatch(PrincipalError):
+ """The realm for the principal does not match the realm for this IPA server"""
+ faultCode = 1014
+
+class PrincipalRequired(PrincipalError):
+ """You cannot remove IPA server service principals"""
+ faultCode = 1015
+
+class InactivationError(GenericError):
+ """This entry cannot be inactivated"""
+ faultCode = 1016
+
+class AlreadyActiveError(InactivationError):
+ """This entry is already locked"""
+ faultCode = 1017
+
+class AlreadyInactiveError(InactivationError):
+ """This entry is already unlocked"""
+ faultCode = 1018
+
+class HasNSAccountLock(InactivationError):
+ """This entry appears to have the nsAccountLock attribute in it so the Class of Service activation/inactivation will not work. You will need to remove the attribute nsAccountLock for this to work."""
+ faultCode = 1019
+
+class ConnectionError(GenericError):
+ """Connection to database failed"""
+ faultCode = 1020
+
+class NoCCacheError(GenericError):
+ """No Kerberos credentials cache is available. Connection cannot be made"""
+ faultCode = 1021
+
+class GSSAPIError(GenericError):
+ """GSSAPI Authorization error"""
+ faultCode = 1022
+
+class ServerUnwilling(GenericError):
+ """Account inactivated. Server is unwilling to perform"""
+ faultCode = 1023
+
+class ConfigurationError(GenericError):
+ """A configuration error occurred"""
+ faultCode = 1024
+
+class DefaultGroup(ConfigurationError):
+ """You cannot remove the default users group"""
+ faultCode = 1025
+
+class HostService(ConfigurationError):
+ """You must enroll a host in order to create a host service"""
+ faultCode = 1026
+
+class InsufficientAccess(GenericError):
+ """You do not have permission to perform this task"""
+ faultCode = 1027
+
+class InvalidUserPrincipal(GenericError):
+ """Invalid user principal"""
+ faultCode = 1028
+
+class FunctionDeprecated(GenericError):
+ """Raised by a deprecated function"""
+ faultCode = 2000
+
+def convertFault(fault):
+ """Convert a fault to the corresponding Exception type, if possible"""
+ code = getattr(fault,'faultCode',None)
+ if code is None:
+ return fault
+ for v in globals().values():
+ if type(v) == type(Exception) and issubclass(v,GenericError) and \
+ code == getattr(v,'faultCode',None):
+ ret = v(fault.faultString)
+ ret.fromFault = True
+ return ret
+ #otherwise...
+ return fault
+
+def listFaults():
+ """Return a list of faults
+
+ Returns a list of dictionaries whose keys are:
+ faultCode: the numeric code used in fault conversion
+ name: the name of the exception
+ desc: the description of the exception (docstring)
+ """
+ ret = []
+ for n,v in globals().items():
+ if type(v) == type(Exception) and issubclass(v,GenericError):
+ code = getattr(v,'faultCode',None)
+ if code is None:
+ continue
+ info = {}
+ info['faultCode'] = code
+ info['name'] = n
+ info['desc'] = getattr(v,'__doc__',None)
+ ret.append(info)
+ ret.sort(lambda a,b: cmp(a['faultCode'],b['faultCode']))
+ return ret
diff --git a/ipalib/errors2.py b/ipalib/errors2.py
new file mode 100644
index 000000000..7e2eea058
--- /dev/null
+++ b/ipalib/errors2.py
@@ -0,0 +1,580 @@
+# Authors:
+# Jason Gerard DeRose <jderose@redhat.com>
+#
+# Copyright (C) 2008 Red Hat
+# see file 'COPYING' for use and warranty inmsgion
+#
+# 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
+
+"""
+Custom exception classes (some which are RPC transparent).
+
+`PrivateError` and its subclasses are custom IPA excetions that will *never* be
+forwarded in a Remote Procedure Call (RPC) response.
+
+On the other hand, `PublicError` and its subclasses can be forwarded in an RPC
+response. These public errors each carry a unique integer error code as well as
+a gettext translated error message (translated at the time the exception is
+raised). The purpose of the public errors is to relay information about
+*expected* user errors, service availability errors, and so on. They should
+*never* be used for *unexpected* programmatic or run-time errors.
+
+For security reasons it is *extremely* important that arbitrary exceptions *not*
+be forwarded in an RPC response. Unexpected exceptions can easily contain
+compromising information in their error messages. Any time the server catches
+any exception that isn't a `PublicError` subclass, it should raise an
+`InternalError`, which itself always has the same, static error message (and
+therefore cannot be populated with information about the true exception).
+
+The public errors are arranging into five main blocks of error code ranges:
+
+ ============= ========================================
+ Error codes Exceptions
+ ============= ========================================
+ 1000 - 1999 `AuthenticationError` and its subclasses
+ 2000 - 2999 `AuthorizationError` and its subclasses
+ 3000 - 3999 `InvocationError` and its subclasses
+ 4000 - 4999 `ExecutionError` and its subclasses
+ 5000 - 5999 `GenericError` and its subclasses
+ ============= ========================================
+
+Within these five blocks some sub-ranges are already allocated for certain types
+of error messages, while others are reserved for future use. Here are the
+current block assignments:
+
+ - **900-5999** `PublicError` and its subclasses
+
+ - **901 - 907** Assigned to special top-level public errors
+
+ - **908 - 999** *Reserved for future use*
+
+ - **1000 - 1999** `AuthenticationError` and its subclasses
+
+ - **1001 - 1099** Open for general authentication errors
+
+ - **1100 - 1199** `KerberosError` and its subclasses
+
+ - **1200 - 1999** *Reserved for future use*
+
+ - **2000 - 2999** `AuthorizationError` and its subclasses
+
+ - **2001 - 2099** Open for general authorization errors
+
+ - **2100 - 2199** `ACIError` and its subclasses
+
+ - **2200 - 2999** *Reserved for future use*
+
+ - **3000 - 3999** `InvocationError` and its subclasses
+
+ - **3001 - 3099** Open for general invocation errors
+
+ - **3100 - 3199** *Reserved for future use*
+
+ - **4000 - 4999** `ExecutionError` and its subclasses
+
+ - **4001 - 4099** Open for general execution errors
+
+ - **4100 - 4199** `LDAPError` and its subclasses
+
+ - **4300 - 4999** *Reserved for future use*
+
+ - **5000 - 5999** `GenericError` and its subclasses
+
+ - **5001 - 5099** Open for generic errors
+
+ - **5100 - 5999** *Reserved for future use*
+"""
+
+from inspect import isclass
+from request import ugettext, ungettext
+from constants import TYPE_ERROR
+
+
+class PrivateError(StandardError):
+ """
+ Base class for exceptions that are *never* forwarded in an RPC response.
+ """
+
+ format = ''
+
+ def __init__(self, **kw):
+ self.message = self.format % kw
+ for (key, value) in kw.iteritems():
+ assert not hasattr(self, key), 'conflicting kwarg %s.%s = %r' % (
+ self.__class__.__name__, key, value,
+ )
+ setattr(self, key, value)
+ StandardError.__init__(self, self.message)
+
+
+class SubprocessError(PrivateError):
+ """
+ Raised when ``subprocess.call()`` returns a non-zero exit status.
+
+ This custom exception is needed because Python 2.4 doesn't have the
+ ``subprocess.CalledProcessError`` exception (which was added in Python 2.5).
+
+ For example:
+
+ >>> raise SubprocessError(returncode=2, argv=('ls', '-lh', '/no-foo/'))
+ Traceback (most recent call last):
+ ...
+ SubprocessError: return code 2 from ('ls', '-lh', '/no-foo/')
+
+ The exit code of the sub-process is available via the ``returncode``
+ instance attribute. For example:
+
+ >>> e = SubprocessError(returncode=1, argv=('/bin/false',))
+ >>> e.returncode
+ 1
+ >>> e.argv # argv is also available
+ ('/bin/false',)
+ """
+
+ format = 'return code %(returncode)d from %(argv)r'
+
+
+class PluginSubclassError(PrivateError):
+ """
+ Raised when a plugin doesn't subclass from an allowed base.
+
+ For example:
+
+ >>> raise PluginSubclassError(plugin='bad', bases=('base1', 'base2'))
+ Traceback (most recent call last):
+ ...
+ PluginSubclassError: 'bad' not subclass of any base in ('base1', 'base2')
+
+ """
+
+ format = '%(plugin)r not subclass of any base in %(bases)r'
+
+
+class PluginDuplicateError(PrivateError):
+ """
+ Raised when the same plugin class is registered more than once.
+
+ For example:
+
+ >>> raise PluginDuplicateError(plugin='my_plugin')
+ Traceback (most recent call last):
+ ...
+ PluginDuplicateError: 'my_plugin' was already registered
+ """
+
+ format = '%(plugin)r was already registered'
+
+
+class PluginOverrideError(PrivateError):
+ """
+ Raised when a plugin overrides another without using ``override=True``.
+
+ For example:
+
+ >>> raise PluginOverrideError(base='Command', name='env', plugin='my_env')
+ Traceback (most recent call last):
+ ...
+ PluginOverrideError: unexpected override of Command.env with 'my_env'
+ """
+
+ format = 'unexpected override of %(base)s.%(name)s with %(plugin)r'
+
+
+class PluginMissingOverrideError(PrivateError):
+ """
+ Raised when a plugin overrides another that has not been registered.
+
+ For example:
+
+ >>> raise PluginMissingOverrideError(base='Command', name='env', plugin='my_env')
+ Traceback (most recent call last):
+ ...
+ PluginMissingOverrideError: Command.env not registered, cannot override with 'my_env'
+ """
+
+ format = '%(base)s.%(name)s not registered, cannot override with %(plugin)r'
+
+
+
+##############################################################################
+# Public errors:
+
+__messages = []
+
+def _(message):
+ __messages.append(message)
+ return message
+
+
+class PublicError(StandardError):
+ """
+ **900** Base class for exceptions that can be forwarded in an RPC response.
+ """
+
+ errno = 900
+ format = None
+
+ def __init__(self, format=None, message=None, **kw):
+ name = self.__class__.__name__
+ if self.format is not None and format is not None:
+ raise ValueError(
+ 'non-generic %r needs format=None; got format=%r' % (
+ name, format)
+ )
+ if message is None:
+ if self.format is None:
+ if format is None:
+ raise ValueError(
+ '%s.format is None yet format=None, message=None' % name
+ )
+ self.format = format
+ self.forwarded = False
+ self.message = self.format % kw
+ self.strerror = ugettext(self.format) % kw
+ else:
+ if type(message) is not unicode:
+ raise TypeError(
+ TYPE_ERROR % ('message', unicode, message, type(message))
+ )
+ self.forwarded = True
+ self.message = message
+ self.strerror = message
+ for (key, value) in kw.iteritems():
+ assert not hasattr(self, key), 'conflicting kwarg %s.%s = %r' % (
+ name, key, value,
+ )
+ setattr(self, key, value)
+ StandardError.__init__(self, self.message)
+
+
+class VersionError(PublicError):
+ """
+ **901** Raised when client and server versions are incompatible.
+
+ For example:
+
+ >>> raise VersionError(cver='2.0', sver='2.1', server='https://localhost')
+ Traceback (most recent call last):
+ ...
+ VersionError: 2.0 client incompatible with 2.1 server at 'https://localhost'
+
+ """
+
+ errno = 901
+ format = _('%(cver)s client incompatible with %(sver)s server at %(server)r')
+
+
+class UnknownError(PublicError):
+ """
+ **902** Raised when client does not know error it caught from server.
+
+ For example:
+
+ >>> raise UnknownError(code=57, server='localhost', error=u'a new error')
+ ...
+ Traceback (most recent call last):
+ ...
+ UnknownError: unknown error 57 from localhost: a new error
+
+ """
+
+ errno = 902
+ format = _('unknown error %(code)d from %(server)s: %(error)s')
+
+
+class InternalError(PublicError):
+ """
+ **903** Raised to conceal a non-public exception.
+
+ For example:
+
+ >>> raise InternalError()
+ Traceback (most recent call last):
+ ...
+ InternalError: an internal error has occured
+ """
+
+ errno = 903
+ format = _('an internal error has occured')
+
+ def __init__(self, message=None):
+ """
+ Security issue: ignore any information given to constructor.
+ """
+ PublicError.__init__(self)
+
+
+class ServerInternalError(PublicError):
+ """
+ **904** Raised when client catches an `InternalError` from server.
+
+ For example:
+
+ >>> raise ServerInternalError(server='https://localhost')
+ Traceback (most recent call last):
+ ...
+ ServerInternalError: an internal error has occured on server at 'https://localhost'
+ """
+
+ errno = 904
+ format = _('an internal error has occured on server at %(server)r')
+
+
+class CommandError(PublicError):
+ """
+ **905** Raised when an unknown command is called.
+
+ For example:
+
+ >>> raise CommandError(name='foobar')
+ Traceback (most recent call last):
+ ...
+ CommandError: unknown command 'foobar'
+ """
+
+ errno = 905
+ format = _('unknown command %(name)r')
+
+
+class ServerCommandError(PublicError):
+ """
+ **906** Raised when client catches a `CommandError` from server.
+
+ For example:
+
+ >>> e = CommandError(name='foobar')
+ >>> raise ServerCommandError(error=e.message, server='https://localhost')
+ Traceback (most recent call last):
+ ...
+ ServerCommandError: error on server 'https://localhost': unknown command 'foobar'
+ """
+
+ errno = 906
+ format = _('error on server %(server)r: %(error)s')
+
+
+class NetworkError(PublicError):
+ """
+ **907** Raised when a network connection cannot be created.
+
+ For example:
+
+ >>> raise NetworkError(uri='ldap://localhost:389')
+ Traceback (most recent call last):
+ ...
+ NetworkError: cannot connect to 'ldap://localhost:389'
+ """
+
+ errno = 907
+ format = _('cannot connect to %(uri)r')
+
+
+class ServerNetworkError(PublicError):
+ """
+ **908** Raised when client catches a `NetworkError` from server.
+
+ For example:
+
+ >>> e = NetworkError(uri='ldap://localhost:389')
+ >>> raise ServerNetworkError(error=e.message, server='https://localhost')
+ Traceback (most recent call last):
+ ...
+ ServerNetworkError: error on server 'https://localhost': cannot connect to 'ldap://localhost:389'
+ """
+
+ errno = 908
+ format = _('error on server %(server)r: %(error)s')
+
+
+
+##############################################################################
+# 1000 - 1999: Authentication errors
+class AuthenticationError(PublicError):
+ """
+ **1000** Base class for authentication errors (*1000 - 1999*).
+ """
+
+ errno = 1000
+
+
+class KerberosError(AuthenticationError):
+ """
+ **1100** Base class for Kerberos authentication errors (*1100 - 1199*).
+ """
+
+ errno = 1100
+
+
+
+##############################################################################
+# 2000 - 2999: Authorization errors
+class AuthorizationError(PublicError):
+ """
+ **2000** Base class for authorization errors (*2000 - 2999*).
+ """
+
+ errno = 2000
+
+
+class ACIError(AuthorizationError):
+ """
+ **2100** Base class for ACI authorization errors (*2100 - 2199*).
+ """
+
+ errno = 2100
+
+
+
+##############################################################################
+# 3000 - 3999: Invocation errors
+
+class InvocationError(PublicError):
+ """
+ **3000** Base class for command invocation errors (*3000 - 3999*).
+ """
+
+ errno = 3000
+
+
+class EncodingError(InvocationError):
+ """
+ **3001** Raised when received text is incorrectly encoded.
+ """
+
+ errno = 3001
+
+
+class BinaryEncodingError(InvocationError):
+ """
+ **3002** Raised when received binary data is incorrectly encoded.
+ """
+
+ errno = 3002
+
+
+class ArgumentError(InvocationError):
+ """
+ **3003** Raised when a command is called with wrong number of arguments.
+ """
+
+ errno = 3003
+
+
+class OptionError(InvocationError):
+ """
+ **3004** Raised when a command is called with unknown options.
+ """
+
+ errno = 3004
+
+
+class RequirementError(InvocationError):
+ """
+ **3005** Raised when a required parameter is not provided.
+
+ For example:
+
+ >>> raise RequirementError(name='givenname')
+ Traceback (most recent call last):
+ ...
+ RequirementError: 'givenname' is required
+ """
+
+ errno = 3005
+ format = _('%(name)r is required')
+
+
+class ConversionError(InvocationError):
+ """
+ **3006** Raised when parameter value can't be converted to correct type.
+
+ For example:
+
+ >>> raise ConversionError(name='age', error='must be an integer')
+ Traceback (most recent call last):
+ ...
+ ConversionError: invalid 'age': must be an integer
+ """
+
+ errno = 3006
+ format = _('invalid %(name)r: %(error)s')
+
+
+class ValidationError(InvocationError):
+ """
+ **3007** Raised when a parameter value fails a validation rule.
+
+ For example:
+
+ >>> raise ValidationError(name='sn', error='can be at most 128 characters')
+ Traceback (most recent call last):
+ ...
+ ValidationError: invalid 'sn': can be at most 128 characters
+ """
+
+ errno = 3007
+ format = _('invalid %(name)r: %(error)s')
+
+
+
+##############################################################################
+# 4000 - 4999: Execution errors
+
+class ExecutionError(PublicError):
+ """
+ **4000** Base class for execution errors (*4000 - 4999*).
+ """
+
+ errno = 4000
+
+
+class LDAPError(ExecutionError):
+ """
+ **4100** Base class for LDAP execution errors (*4100 - 4199*).
+ """
+
+ errno = 4100
+
+
+
+##############################################################################
+# 5000 - 5999: Generic errors
+
+class GenericError(PublicError):
+ """
+ **5000** Base class for errors that don't fit elsewhere (*5000 - 5999*).
+ """
+
+ errno = 5000
+
+
+
+def __errors_iter():
+ """
+ Iterate through all the `PublicError` subclasses.
+ """
+ for (key, value) in globals().items():
+ if key.startswith('_') or not isclass(value):
+ continue
+ if issubclass(value, PublicError):
+ yield value
+
+public_errors = tuple(
+ sorted(__errors_iter(), key=lambda E: E.errno)
+)
+
+if __name__ == '__main__':
+ for klass in public_errors:
+ print '%d\t%s' % (klass.code, klass.__name__)
+ print '(%d public errors)' % len(public_errors)
diff --git a/ipalib/frontend.py b/ipalib/frontend.py
new file mode 100644
index 000000000..b30205fe8
--- /dev/null
+++ b/ipalib/frontend.py
@@ -0,0 +1,696 @@
+# 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
+
+"""
+Base classes for all front-end plugins.
+"""
+
+import re
+import inspect
+import plugable
+from plugable import lock, check_name
+import errors
+from errors import check_type, check_isinstance, raise_TypeError
+from parameters import create_param, Param, Str, Flag
+from util import make_repr
+
+
+RULE_FLAG = 'validation_rule'
+
+def rule(obj):
+ assert not hasattr(obj, RULE_FLAG)
+ setattr(obj, RULE_FLAG, True)
+ return obj
+
+def is_rule(obj):
+ return callable(obj) and getattr(obj, RULE_FLAG, False) is True
+
+
+class Command(plugable.Plugin):
+ """
+ A public IPA atomic operation.
+
+ All plugins that subclass from `Command` will be automatically available
+ as a CLI command and as an XML-RPC method.
+
+ Plugins that subclass from Command are registered in the ``api.Command``
+ namespace. For example:
+
+ >>> from ipalib import create_api
+ >>> api = create_api()
+ >>> class my_command(Command):
+ ... pass
+ ...
+ >>> api.register(my_command)
+ >>> api.finalize()
+ >>> list(api.Command)
+ ['my_command']
+ >>> api.Command.my_command # doctest:+ELLIPSIS
+ PluginProxy(Command, ...my_command())
+ """
+
+ __public__ = frozenset((
+ 'get_default',
+ 'convert',
+ 'normalize',
+ 'validate',
+ 'execute',
+ '__call__',
+ 'args',
+ 'options',
+ 'params',
+ 'args_to_kw',
+ 'params_2_args_options',
+ 'output_for_cli',
+ ))
+ takes_options = tuple()
+ takes_args = tuple()
+ args = None
+ options = None
+ params = None
+ output_for_cli = None
+
+ def __call__(self, *args, **kw):
+ """
+ Perform validation and then execute the command.
+
+ If not in a server context, the call will be forwarded over
+ XML-RPC and the executed an the nearest IPA server.
+ """
+ self.debug(make_repr(self.name, *args, **kw))
+ if len(args) > 0:
+ arg_kw = self.args_to_kw(*args)
+ assert set(arg_kw).intersection(kw) == set()
+ kw.update(arg_kw)
+ kw = self.normalize(**kw)
+ kw = self.convert(**kw)
+ kw.update(self.get_default(**kw))
+ self.validate(**kw)
+ (args, options) = self.params_2_args_options(kw)
+ result = self.run(*args, **options)
+ self.debug('%s result: %r', self.name, result)
+ return result
+
+ def args_to_kw(self, *values):
+ """
+ Map positional into keyword arguments.
+ """
+ if self.max_args is not None and len(values) > self.max_args:
+ if self.max_args == 0:
+ raise errors.ArgumentError(self, 'takes no arguments')
+ if self.max_args == 1:
+ raise errors.ArgumentError(self, 'takes at most 1 argument')
+ raise errors.ArgumentError(self,
+ 'takes at most %d arguments' % len(self.args)
+ )
+ return dict(self.__args_to_kw_iter(values))
+
+ def __args_to_kw_iter(self, values):
+ """
+ Generator used by `Command.args_to_kw` method.
+ """
+ multivalue = False
+ for (i, arg) in enumerate(self.args()):
+ assert not multivalue
+ if len(values) > i:
+ if arg.multivalue:
+ multivalue = True
+ if len(values) == i + 1 and type(values[i]) in (list, tuple):
+ yield (arg.name, values[i])
+ else:
+ yield (arg.name, values[i:])
+ else:
+ yield (arg.name, values[i])
+ else:
+ break
+
+ def args_options_2_params(self, args, options):
+ pass
+
+ def params_2_args_options(self, params):
+ """
+ Split params into (args, kw).
+ """
+ args = tuple(params.get(name, None) for name in self.args)
+ options = dict(
+ (name, params.get(name, None)) for name in self.options
+ )
+ return (args, options)
+
+ def normalize(self, **kw):
+ """
+ Return a dictionary of normalized values.
+
+ For example:
+
+ >>> class my_command(Command):
+ ... takes_options = (
+ ... Param('first', normalizer=lambda value: value.lower()),
+ ... Param('last'),
+ ... )
+ ...
+ >>> c = my_command()
+ >>> c.finalize()
+ >>> c.normalize(first=u'JOHN', last=u'DOE')
+ {'last': u'DOE', 'first': u'john'}
+ """
+ return dict(
+ (k, self.params[k].normalize(v)) for (k, v) in kw.iteritems()
+ )
+
+ def convert(self, **kw):
+ """
+ Return a dictionary of values converted to correct type.
+
+ >>> from ipalib import Int
+ >>> class my_command(Command):
+ ... takes_args = (
+ ... Int('one'),
+ ... 'two',
+ ... )
+ ...
+ >>> c = my_command()
+ >>> c.finalize()
+ >>> c.convert(one=1, two=2)
+ {'two': u'2', 'one': 1}
+ """
+ return dict(
+ (k, self.params[k].convert(v)) for (k, v) in kw.iteritems()
+ )
+
+ def get_default(self, **kw):
+ """
+ Return a dictionary of defaults for all missing required values.
+
+ For example:
+
+ >>> from ipalib import Str
+ >>> class my_command(Command):
+ ... takes_args = [Str('color', default=u'Red')]
+ ...
+ >>> c = my_command()
+ >>> c.finalize()
+ >>> c.get_default()
+ {'color': u'Red'}
+ >>> c.get_default(color=u'Yellow')
+ {}
+ """
+ return dict(self.__get_default_iter(kw))
+
+ def __get_default_iter(self, kw):
+ """
+ Generator method used by `Command.get_default`.
+ """
+ for param in self.params():
+ if param.name in kw:
+ continue
+ if param.required or param.autofill:
+ default = param.get_default(**kw)
+ if default is not None:
+ yield (param.name, default)
+
+ def validate(self, **kw):
+ """
+ Validate all values.
+
+ If any value fails the validation, `ipalib.errors.ValidationError`
+ (or a subclass thereof) will be raised.
+ """
+ for param in self.params():
+ value = kw.get(param.name, None)
+ if value is not None:
+ param.validate(value)
+ elif param.required:
+ raise errors.RequirementError(param.name)
+
+ def run(self, *args, **kw):
+ """
+ Dispatch to `Command.execute` or `Command.forward`.
+
+ If running in a server context, `Command.execute` is called and the
+ actually work this command performs is executed locally.
+
+ If running in a non-server context, `Command.forward` is called,
+ which forwards this call over XML-RPC to the exact same command
+ on the nearest IPA server and the actual work this command
+ performs is executed remotely.
+ """
+ if self.api.env.in_server:
+ target = self.execute
+ else:
+ target = self.forward
+ object.__setattr__(self, 'run', target)
+ return target(*args, **kw)
+
+ def execute(self, *args, **kw):
+ """
+ Perform the actual work this command does.
+
+ This method should be implemented only against functionality
+ in self.api.Backend. For example, a hypothetical
+ user_add.execute() might be implemented like this:
+
+ >>> class user_add(Command):
+ ... def execute(self, **kw):
+ ... return self.api.Backend.ldap.add(**kw)
+ ...
+ """
+ raise NotImplementedError('%s.execute()' % self.name)
+
+ def forward(self, *args, **kw):
+ """
+ Forward call over XML-RPC to this same command on server.
+ """
+ return self.Backend.xmlrpc.forward_call(self.name, *args, **kw)
+
+ def finalize(self):
+ """
+ Finalize plugin initialization.
+
+ This method creates the ``args``, ``options``, and ``params``
+ namespaces. This is not done in `Command.__init__` because
+ subclasses (like `crud.Add`) might need to access other plugins
+ loaded in self.api to determine what their custom `Command.get_args`
+ and `Command.get_options` methods should yield.
+ """
+ self.args = plugable.NameSpace(self.__create_args(), sort=False)
+ if len(self.args) == 0 or not self.args[-1].multivalue:
+ self.max_args = len(self.args)
+ else:
+ self.max_args = None
+ self.options = plugable.NameSpace(
+ (create_param(spec) for spec in self.get_options()),
+ sort=False
+ )
+ def get_key(p):
+ if p.required:
+ if p.default_from is None:
+ return 0
+ return 1
+ return 2
+ self.params = plugable.NameSpace(
+ sorted(tuple(self.args()) + tuple(self.options()), key=get_key),
+ sort=False
+ )
+ super(Command, self).finalize()
+
+ def get_args(self):
+ """
+ Return iterable with arguments for Command.args namespace.
+
+ Subclasses can override this to customize how the arguments
+ are determined. For an example of why this can be useful,
+ see `ipalib.crud.Mod`.
+ """
+ return self.takes_args
+
+ def get_options(self):
+ """
+ Return iterable with options for Command.options namespace.
+
+ Subclasses can override this to customize how the options
+ are determined. For an example of why this can be useful,
+ see `ipalib.crud.Mod`.
+ """
+ return self.takes_options
+
+ def __create_args(self):
+ """
+ Generator used to create args namespace.
+ """
+ optional = False
+ multivalue = False
+ for arg in self.get_args():
+ arg = create_param(arg)
+ if optional and arg.required:
+ raise ValueError(
+ '%s: required argument after optional' % arg.name
+ )
+ if multivalue:
+ raise ValueError(
+ '%s: only final argument can be multivalue' % arg.name
+ )
+ if not arg.required:
+ optional = True
+ if arg.multivalue:
+ multivalue = True
+ yield arg
+
+
+class LocalOrRemote(Command):
+ """
+ A command that is explicitly executed locally or remotely.
+
+ This is for commands that makes sense to execute either locally or
+ remotely to return a perhaps different result. The best example of
+ this is the `ipalib.plugins.f_misc.env` plugin which returns the
+ key/value pairs describing the configuration state: it can be
+ """
+
+ takes_options = (
+ Flag('server?',
+ doc='Forward to server instead of running locally',
+ ),
+ )
+
+ def run(self, *args, **options):
+ """
+ Dispatch to forward() or execute() based on ``server`` option.
+
+ When running in a client context, this command is executed remotely if
+ ``options['server']`` is true; otherwise it is executed locally.
+
+ When running in a server context, this command is always executed
+ locally and the value of ``options['server']`` is ignored.
+ """
+ if options['server'] and not self.env.in_server:
+ return self.forward(*args, **options)
+ return self.execute(*args, **options)
+
+
+class Object(plugable.Plugin):
+ __public__ = frozenset((
+ 'backend',
+ 'methods',
+ 'properties',
+ 'params',
+ 'primary_key',
+ 'params_minus_pk',
+ 'get_dn',
+ ))
+ backend = None
+ methods = None
+ properties = None
+ params = None
+ primary_key = None
+ params_minus_pk = None
+
+ # Can override in subclasses:
+ backend_name = None
+ takes_params = tuple()
+
+ def set_api(self, api):
+ super(Object, self).set_api(api)
+ self.methods = plugable.NameSpace(
+ self.__get_attrs('Method'), sort=False
+ )
+ self.properties = plugable.NameSpace(
+ self.__get_attrs('Property'), sort=False
+ )
+ self.params = plugable.NameSpace(
+ self.__get_params(), sort=False
+ )
+ pkeys = filter(lambda p: p.primary_key, self.params())
+ if len(pkeys) > 1:
+ raise ValueError(
+ '%s (Object) has multiple primary keys: %s' % (
+ self.name,
+ ', '.join(p.name for p in pkeys),
+ )
+ )
+ if len(pkeys) == 1:
+ self.primary_key = pkeys[0]
+ self.params_minus_pk = plugable.NameSpace(
+ filter(lambda p: not p.primary_key, self.params()), sort=False
+ )
+
+ if 'Backend' in self.api and self.backend_name in self.api.Backend:
+ self.backend = self.api.Backend[self.backend_name]
+
+ def get_dn(self, primary_key):
+ """
+ Construct an LDAP DN from a primary_key.
+ """
+ raise NotImplementedError('%s.get_dn()' % self.name)
+
+ def __get_attrs(self, name):
+ if name not in self.api:
+ return
+ namespace = self.api[name]
+ assert type(namespace) is plugable.NameSpace
+ for proxy in namespace(): # Equivalent to dict.itervalues()
+ if proxy.obj_name == self.name:
+ yield proxy.__clone__('attr_name')
+
+ def __get_params(self):
+ props = self.properties.__todict__()
+ for spec in self.takes_params:
+ if type(spec) is str:
+ key = spec.rstrip('?*+')
+ else:
+ assert isinstance(spec, Param)
+ key = spec.name
+ if key in props:
+ yield props.pop(key).param
+ else:
+ yield create_param(spec)
+ def get_key(p):
+ if p.param.required:
+ if p.param.default_from is None:
+ return 0
+ return 1
+ return 2
+ for prop in sorted(props.itervalues(), key=get_key):
+ yield prop.param
+
+
+class Attribute(plugable.Plugin):
+ """
+ Base class implementing the attribute-to-object association.
+
+ `Attribute` plugins are associated with an `Object` plugin to group
+ a common set of commands that operate on a common set of parameters.
+
+ The association between attribute and object is done using a simple
+ naming convention: the first part of the plugin class name (up to the
+ first underscore) is the object name, and rest is the attribute name,
+ as this table shows:
+
+ =============== =========== ==============
+ Class name Object name Attribute name
+ =============== =========== ==============
+ noun_verb noun verb
+ user_add user add
+ user_first_name user first_name
+ =============== =========== ==============
+
+ For example:
+
+ >>> class user_add(Attribute):
+ ... pass
+ ...
+ >>> instance = user_add()
+ >>> instance.obj_name
+ 'user'
+ >>> instance.attr_name
+ 'add'
+
+ In practice the `Attribute` class is not used directly, but rather is
+ only the base class for the `Method` and `Property` classes. Also see
+ the `Object` class.
+ """
+ __public__ = frozenset((
+ 'obj',
+ 'obj_name',
+ ))
+ __obj = None
+
+ def __init__(self):
+ m = re.match(
+ '^([a-z][a-z0-9]+)_([a-z][a-z0-9]+)$',
+ self.__class__.__name__
+ )
+ assert m
+ self.__obj_name = m.group(1)
+ self.__attr_name = m.group(2)
+ super(Attribute, self).__init__()
+
+ def __get_obj_name(self):
+ return self.__obj_name
+ obj_name = property(__get_obj_name)
+
+ def __get_attr_name(self):
+ return self.__attr_name
+ attr_name = property(__get_attr_name)
+
+ def __get_obj(self):
+ """
+ Returns the obj instance this attribute is associated with, or None
+ if no association has been set.
+ """
+ return self.__obj
+ obj = property(__get_obj)
+
+ def set_api(self, api):
+ self.__obj = api.Object[self.obj_name]
+ super(Attribute, self).set_api(api)
+
+
+class Method(Attribute, Command):
+ """
+ A command with an associated object.
+
+ A `Method` plugin must have a corresponding `Object` plugin. The
+ association between object and method is done through a simple naming
+ convention: the first part of the method name (up to the first under
+ score) is the object name, as the examples in this table show:
+
+ ============= =========== ==============
+ Method name Object name Attribute name
+ ============= =========== ==============
+ user_add user add
+ noun_verb noun verb
+ door_open_now door open_now
+ ============= =========== ==============
+
+ There are three different places a method can be accessed. For example,
+ say you created a `Method` plugin and its corresponding `Object` plugin
+ like this:
+
+ >>> from ipalib import create_api
+ >>> api = create_api()
+ >>> class user_add(Method):
+ ... def run(self):
+ ... return 'Added the user!'
+ ...
+ >>> class user(Object):
+ ... pass
+ ...
+ >>> api.register(user_add)
+ >>> api.register(user)
+ >>> api.finalize()
+
+ First, the ``user_add`` plugin can be accessed through the ``api.Method``
+ namespace:
+
+ >>> list(api.Method)
+ ['user_add']
+ >>> api.Method.user_add() # Will call user_add.run()
+ 'Added the user!'
+
+ Second, because `Method` is a subclass of `Command`, the ``user_add``
+ plugin can also be accessed through the ``api.Command`` namespace:
+
+ >>> list(api.Command)
+ ['user_add']
+ >>> api.Command.user_add() # Will call user_add.run()
+ 'Added the user!'
+
+ And third, ``user_add`` can be accessed as an attribute on the ``user``
+ `Object`:
+
+ >>> list(api.Object)
+ ['user']
+ >>> list(api.Object.user.methods)
+ ['add']
+ >>> api.Object.user.methods.add() # Will call user_add.run()
+ 'Added the user!'
+
+ The `Attribute` base class implements the naming convention for the
+ attribute-to-object association. Also see the `Object` and the
+ `Property` classes.
+ """
+ __public__ = Attribute.__public__.union(Command.__public__)
+
+ def __init__(self):
+ super(Method, self).__init__()
+
+
+class Property(Attribute):
+ __public__ = frozenset((
+ 'rules',
+ 'param',
+ 'type',
+ )).union(Attribute.__public__)
+
+ klass = Str
+ default = None
+ default_from = None
+ normalizer = None
+
+ def __init__(self):
+ super(Property, self).__init__()
+ self.rules = tuple(
+ sorted(self.__rules_iter(), key=lambda f: getattr(f, '__name__'))
+ )
+ self.kwargs = tuple(
+ sorted(self.__kw_iter(), key=lambda keyvalue: keyvalue[0])
+ )
+ kw = dict(self.kwargs)
+ self.param = self.klass(self.attr_name, *self.rules, **kw)
+
+ def __kw_iter(self):
+ for (key, kind, default) in self.klass.kwargs:
+ if getattr(self, key, None) is not None:
+ yield (key, getattr(self, key))
+
+ def __rules_iter(self):
+ """
+ Iterates through the attributes in this instance to retrieve the
+ methods implementing validation rules.
+ """
+ for name in dir(self.__class__):
+ if name.startswith('_'):
+ continue
+ base_attr = getattr(self.__class__, name)
+ if is_rule(base_attr):
+ attr = getattr(self, name)
+ if is_rule(attr):
+ yield attr
+
+
+class Application(Command):
+ """
+ Base class for commands register by an external application.
+
+ Special commands that only apply to a particular application built atop
+ `ipalib` should subclass from ``Application``.
+
+ Because ``Application`` subclasses from `Command`, plugins that subclass
+ from ``Application`` with be available in both the ``api.Command`` and
+ ``api.Application`` namespaces.
+ """
+
+ __public__ = frozenset((
+ 'application',
+ 'set_application'
+ )).union(Command.__public__)
+ __application = None
+
+ def __get_application(self):
+ """
+ Returns external ``application`` object.
+ """
+ return self.__application
+ application = property(__get_application)
+
+ def set_application(self, application):
+ """
+ Sets the external application object to ``application``.
+ """
+ if self.__application is not None:
+ raise AttributeError(
+ '%s.application can only be set once' % self.name
+ )
+ if application is None:
+ raise TypeError(
+ '%s.application cannot be None' % self.name
+ )
+ object.__setattr__(self, '_Application__application', application)
+ assert self.application is application
diff --git a/ipalib/ipauuid.py b/ipalib/ipauuid.py
new file mode 100644
index 000000000..9923dc7a9
--- /dev/null
+++ b/ipalib/ipauuid.py
@@ -0,0 +1,556 @@
+# This is a backport of the Python2.5 uuid module.
+
+r"""UUID objects (universally unique identifiers) according to RFC 4122.
+
+This module provides immutable UUID objects (class UUID) and the functions
+uuid1(), uuid3(), uuid4(), uuid5() for generating version 1, 3, 4, and 5
+UUIDs as specified in RFC 4122.
+
+If all you want is a unique ID, you should probably call uuid1() or uuid4().
+Note that uuid1() may compromise privacy since it creates a UUID containing
+the computer's network address. uuid4() creates a random UUID.
+
+Typical usage:
+
+ **Important:** So that the freeIPA Python 2.4 ``uuid`` backport can be
+ automatically loaded when needed, import the ``uuid`` module like this:
+
+ >>> from ipalib import uuid
+
+ Make a UUID based on the host ID and current time:
+
+ >>> uuid.uuid1() #doctest: +ELLIPSIS
+ UUID('...')
+
+ Make a UUID using an MD5 hash of a namespace UUID and a name:
+
+ >>> uuid.uuid3(uuid.NAMESPACE_DNS, 'python.org')
+ UUID('6fa459ea-ee8a-3ca4-894e-db77e160355e')
+
+ Make a random UUID:
+
+ >>> uuid.uuid4() #doctest: +ELLIPSIS
+ UUID('...')
+
+ Make a UUID using a SHA-1 hash of a namespace UUID and a name:
+
+ >>> uuid.uuid5(uuid.NAMESPACE_DNS, 'python.org')
+ UUID('886313e1-3b8a-5372-9b90-0c9aee199e5d')
+
+ Make a UUID from a string of hex digits (braces and hyphens ignored):
+
+ >>> x = uuid.UUID('{00010203-0405-0607-0809-0a0b0c0d0e0f}')
+ >>> x
+ UUID('00010203-0405-0607-0809-0a0b0c0d0e0f')
+
+ Convert a UUID to a string of hex digits in standard form:
+
+ >>> str(x)
+ '00010203-0405-0607-0809-0a0b0c0d0e0f'
+
+ Get the raw 16 bytes of the UUID:
+
+ >>> x.bytes
+ '\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f'
+
+ Make a UUID from a 16-byte string:
+
+ >>> uuid.UUID(bytes=x.bytes)
+ UUID('00010203-0405-0607-0809-0a0b0c0d0e0f')
+"""
+
+__author__ = 'Ka-Ping Yee <ping@zesty.ca>'
+
+RESERVED_NCS, RFC_4122, RESERVED_MICROSOFT, RESERVED_FUTURE = [
+ 'reserved for NCS compatibility', 'specified in RFC 4122',
+ 'reserved for Microsoft compatibility', 'reserved for future definition']
+
+class UUID(object):
+ """Instances of the UUID class represent UUIDs as specified in RFC 4122.
+ UUID objects are immutable, hashable, and usable as dictionary keys.
+ Converting a UUID to a string with str() yields something in the form
+ '12345678-1234-1234-1234-123456789abc'. The UUID constructor accepts
+ five possible forms: a similar string of hexadecimal digits, or a tuple
+ of six integer fields (with 32-bit, 16-bit, 16-bit, 8-bit, 8-bit, and
+ 48-bit values respectively) as an argument named 'fields', or a string
+ of 16 bytes (with all the integer fields in big-endian order) as an
+ argument named 'bytes', or a string of 16 bytes (with the first three
+ fields in little-endian order) as an argument named 'bytes_le', or a
+ single 128-bit integer as an argument named 'int'.
+
+ UUIDs have these read-only attributes:
+
+ bytes the UUID as a 16-byte string (containing the six
+ integer fields in big-endian byte order)
+
+ bytes_le the UUID as a 16-byte string (with time_low, time_mid,
+ and time_hi_version in little-endian byte order)
+
+ fields a tuple of the six integer fields of the UUID,
+ which are also available as six individual attributes
+ and two derived attributes:
+
+ time_low the first 32 bits of the UUID
+ time_mid the next 16 bits of the UUID
+ time_hi_version the next 16 bits of the UUID
+ clock_seq_hi_variant the next 8 bits of the UUID
+ clock_seq_low the next 8 bits of the UUID
+ node the last 48 bits of the UUID
+
+ time the 60-bit timestamp
+ clock_seq the 14-bit sequence number
+
+ hex the UUID as a 32-character hexadecimal string
+
+ int the UUID as a 128-bit integer
+
+ urn the UUID as a URN as specified in RFC 4122
+
+ variant the UUID variant (one of the constants RESERVED_NCS,
+ RFC_4122, RESERVED_MICROSOFT, or RESERVED_FUTURE)
+
+ version the UUID version number (1 through 5, meaningful only
+ when the variant is RFC_4122)
+ """
+
+ def __init__(self, hex=None, bytes=None, bytes_le=None, fields=None,
+ int=None, version=None):
+ r"""Create a UUID from either a string of 32 hexadecimal digits,
+ a string of 16 bytes as the 'bytes' argument, a string of 16 bytes
+ in little-endian order as the 'bytes_le' argument, a tuple of six
+ integers (32-bit time_low, 16-bit time_mid, 16-bit time_hi_version,
+ 8-bit clock_seq_hi_variant, 8-bit clock_seq_low, 48-bit node) as
+ the 'fields' argument, or a single 128-bit integer as the 'int'
+ argument. When a string of hex digits is given, curly braces,
+ hyphens, and a URN prefix are all optional. For example, these
+ expressions all yield the same UUID:
+
+ UUID('{12345678-1234-5678-1234-567812345678}')
+ UUID('12345678123456781234567812345678')
+ UUID('urn:uuid:12345678-1234-5678-1234-567812345678')
+ UUID(bytes='\x12\x34\x56\x78'*4)
+ UUID(bytes_le='\x78\x56\x34\x12\x34\x12\x78\x56' +
+ '\x12\x34\x56\x78\x12\x34\x56\x78')
+ UUID(fields=(0x12345678, 0x1234, 0x5678, 0x12, 0x34, 0x567812345678))
+ UUID(int=0x12345678123456781234567812345678)
+
+ Exactly one of 'hex', 'bytes', 'bytes_le', 'fields', or 'int' must
+ be given. The 'version' argument is optional; if given, the resulting
+ UUID will have its variant and version set according to RFC 4122,
+ overriding the given 'hex', 'bytes', 'bytes_le', 'fields', or 'int'.
+ """
+
+ if [hex, bytes, bytes_le, fields, int].count(None) != 4:
+ raise TypeError('need one of hex, bytes, bytes_le, fields, or int')
+ if hex is not None:
+ hex = hex.replace('urn:', '').replace('uuid:', '')
+ hex = hex.strip('{}').replace('-', '')
+ if len(hex) != 32:
+ raise ValueError('badly formed hexadecimal UUID string')
+ int = long(hex, 16)
+ if bytes_le is not None:
+ if len(bytes_le) != 16:
+ raise ValueError('bytes_le is not a 16-char string')
+ bytes = (bytes_le[3] + bytes_le[2] + bytes_le[1] + bytes_le[0] +
+ bytes_le[5] + bytes_le[4] + bytes_le[7] + bytes_le[6] +
+ bytes_le[8:])
+ if bytes is not None:
+ if len(bytes) != 16:
+ raise ValueError('bytes is not a 16-char string')
+ int = long(('%02x'*16) % tuple(map(ord, bytes)), 16)
+ if fields is not None:
+ if len(fields) != 6:
+ raise ValueError('fields is not a 6-tuple')
+ (time_low, time_mid, time_hi_version,
+ clock_seq_hi_variant, clock_seq_low, node) = fields
+ if not 0 <= time_low < 1<<32L:
+ raise ValueError('field 1 out of range (need a 32-bit value)')
+ if not 0 <= time_mid < 1<<16L:
+ raise ValueError('field 2 out of range (need a 16-bit value)')
+ if not 0 <= time_hi_version < 1<<16L:
+ raise ValueError('field 3 out of range (need a 16-bit value)')
+ if not 0 <= clock_seq_hi_variant < 1<<8L:
+ raise ValueError('field 4 out of range (need an 8-bit value)')
+ if not 0 <= clock_seq_low < 1<<8L:
+ raise ValueError('field 5 out of range (need an 8-bit value)')
+ if not 0 <= node < 1<<48L:
+ raise ValueError('field 6 out of range (need a 48-bit value)')
+ clock_seq = (clock_seq_hi_variant << 8L) | clock_seq_low
+ int = ((time_low << 96L) | (time_mid << 80L) |
+ (time_hi_version << 64L) | (clock_seq << 48L) | node)
+ if int is not None:
+ if not 0 <= int < 1<<128L:
+ raise ValueError('int is out of range (need a 128-bit value)')
+ if version is not None:
+ if not 1 <= version <= 5:
+ raise ValueError('illegal version number')
+ # Set the variant to RFC 4122.
+ int &= ~(0xc000 << 48L)
+ int |= 0x8000 << 48L
+ # Set the version number.
+ int &= ~(0xf000 << 64L)
+ int |= version << 76L
+ self.__dict__['int'] = int
+
+ def __cmp__(self, other):
+ if isinstance(other, UUID):
+ return cmp(self.int, other.int)
+ return NotImplemented
+
+ def __hash__(self):
+ return hash(self.int)
+
+ def __int__(self):
+ return self.int
+
+ def __repr__(self):
+ return 'UUID(%r)' % str(self)
+
+ def __setattr__(self, name, value):
+ raise TypeError('UUID objects are immutable')
+
+ def __str__(self):
+ hex = '%032x' % self.int
+ return '%s-%s-%s-%s-%s' % (
+ hex[:8], hex[8:12], hex[12:16], hex[16:20], hex[20:])
+
+ def get_bytes(self):
+ bytes = ''
+ for shift in range(0, 128, 8):
+ bytes = chr((self.int >> shift) & 0xff) + bytes
+ return bytes
+
+ bytes = property(get_bytes)
+
+ def get_bytes_le(self):
+ bytes = self.bytes
+ return (bytes[3] + bytes[2] + bytes[1] + bytes[0] +
+ bytes[5] + bytes[4] + bytes[7] + bytes[6] + bytes[8:])
+
+ bytes_le = property(get_bytes_le)
+
+ def get_fields(self):
+ return (self.time_low, self.time_mid, self.time_hi_version,
+ self.clock_seq_hi_variant, self.clock_seq_low, self.node)
+
+ fields = property(get_fields)
+
+ def get_time_low(self):
+ return self.int >> 96L
+
+ time_low = property(get_time_low)
+
+ def get_time_mid(self):
+ return (self.int >> 80L) & 0xffff
+
+ time_mid = property(get_time_mid)
+
+ def get_time_hi_version(self):
+ return (self.int >> 64L) & 0xffff
+
+ time_hi_version = property(get_time_hi_version)
+
+ def get_clock_seq_hi_variant(self):
+ return (self.int >> 56L) & 0xff
+
+ clock_seq_hi_variant = property(get_clock_seq_hi_variant)
+
+ def get_clock_seq_low(self):
+ return (self.int >> 48L) & 0xff
+
+ clock_seq_low = property(get_clock_seq_low)
+
+ def get_time(self):
+ return (((self.time_hi_version & 0x0fffL) << 48L) |
+ (self.time_mid << 32L) | self.time_low)
+
+ time = property(get_time)
+
+ def get_clock_seq(self):
+ return (((self.clock_seq_hi_variant & 0x3fL) << 8L) |
+ self.clock_seq_low)
+
+ clock_seq = property(get_clock_seq)
+
+ def get_node(self):
+ return self.int & 0xffffffffffff
+
+ node = property(get_node)
+
+ def get_hex(self):
+ return '%032x' % self.int
+
+ hex = property(get_hex)
+
+ def get_urn(self):
+ return 'urn:uuid:' + str(self)
+
+ urn = property(get_urn)
+
+ def get_variant(self):
+ if not self.int & (0x8000 << 48L):
+ return RESERVED_NCS
+ elif not self.int & (0x4000 << 48L):
+ return RFC_4122
+ elif not self.int & (0x2000 << 48L):
+ return RESERVED_MICROSOFT
+ else:
+ return RESERVED_FUTURE
+
+ variant = property(get_variant)
+
+ def get_version(self):
+ # The version bits are only meaningful for RFC 4122 UUIDs.
+ if self.variant == RFC_4122:
+ return int((self.int >> 76L) & 0xf)
+
+ version = property(get_version)
+
+def _find_mac(command, args, hw_identifiers, get_index):
+ import os
+ for dir in ['', '/sbin/', '/usr/sbin']:
+ executable = os.path.join(dir, command)
+ if not os.path.exists(executable):
+ continue
+
+ try:
+ # LC_ALL to get English output, 2>/dev/null to
+ # prevent output on stderr
+ cmd = 'LC_ALL=C %s %s 2>/dev/null' % (executable, args)
+ pipe = os.popen(cmd)
+ except IOError:
+ continue
+
+ for line in pipe:
+ words = line.lower().split()
+ for i in range(len(words)):
+ if words[i] in hw_identifiers:
+ return int(words[get_index(i)].replace(':', ''), 16)
+ return None
+
+def _ifconfig_getnode():
+ """Get the hardware address on Unix by running ifconfig."""
+
+ # This works on Linux ('' or '-a'), Tru64 ('-av'), but not all Unixes.
+ for args in ('', '-a', '-av'):
+ mac = _find_mac('ifconfig', args, ['hwaddr', 'ether'], lambda i: i+1)
+ if mac:
+ return mac
+
+ import socket
+ ip_addr = socket.gethostbyname(socket.gethostname())
+
+ # Try getting the MAC addr from arp based on our IP address (Solaris).
+ mac = _find_mac('arp', '-an', [ip_addr], lambda i: -1)
+ if mac:
+ return mac
+
+ # This might work on HP-UX.
+ mac = _find_mac('lanscan', '-ai', ['lan0'], lambda i: 0)
+ if mac:
+ return mac
+
+ return None
+
+def _ipconfig_getnode():
+ """Get the hardware address on Windows by running ipconfig.exe."""
+ import os, re
+ dirs = ['', r'c:\windows\system32', r'c:\winnt\system32']
+ try:
+ import ctypes
+ buffer = ctypes.create_string_buffer(300)
+ ctypes.windll.kernel32.GetSystemDirectoryA(buffer, 300)
+ dirs.insert(0, buffer.value.decode('mbcs'))
+ except:
+ pass
+ for dir in dirs:
+ try:
+ pipe = os.popen(os.path.join(dir, 'ipconfig') + ' /all')
+ except IOError:
+ continue
+ for line in pipe:
+ value = line.split(':')[-1].strip().lower()
+ if re.match('([0-9a-f][0-9a-f]-){5}[0-9a-f][0-9a-f]', value):
+ return int(value.replace('-', ''), 16)
+
+def _netbios_getnode():
+ """Get the hardware address on Windows using NetBIOS calls.
+ See http://support.microsoft.com/kb/118623 for details."""
+ import win32wnet, netbios
+ ncb = netbios.NCB()
+ ncb.Command = netbios.NCBENUM
+ ncb.Buffer = adapters = netbios.LANA_ENUM()
+ adapters._pack()
+ if win32wnet.Netbios(ncb) != 0:
+ return
+ adapters._unpack()
+ for i in range(adapters.length):
+ ncb.Reset()
+ ncb.Command = netbios.NCBRESET
+ ncb.Lana_num = ord(adapters.lana[i])
+ if win32wnet.Netbios(ncb) != 0:
+ continue
+ ncb.Reset()
+ ncb.Command = netbios.NCBASTAT
+ ncb.Lana_num = ord(adapters.lana[i])
+ ncb.Callname = '*'.ljust(16)
+ ncb.Buffer = status = netbios.ADAPTER_STATUS()
+ if win32wnet.Netbios(ncb) != 0:
+ continue
+ status._unpack()
+ bytes = map(ord, status.adapter_address)
+ return ((bytes[0]<<40L) + (bytes[1]<<32L) + (bytes[2]<<24L) +
+ (bytes[3]<<16L) + (bytes[4]<<8L) + bytes[5])
+
+# Thanks to Thomas Heller for ctypes and for his help with its use here.
+
+# If ctypes is available, use it to find system routines for UUID generation.
+_uuid_generate_random = _uuid_generate_time = _UuidCreate = None
+try:
+ import ctypes, ctypes.util
+ _buffer = ctypes.create_string_buffer(16)
+
+ # The uuid_generate_* routines are provided by libuuid on at least
+ # Linux and FreeBSD, and provided by libc on Mac OS X.
+ for libname in ['uuid', 'c']:
+ try:
+ lib = ctypes.CDLL(ctypes.util.find_library(libname))
+ except:
+ continue
+ if hasattr(lib, 'uuid_generate_random'):
+ _uuid_generate_random = lib.uuid_generate_random
+ if hasattr(lib, 'uuid_generate_time'):
+ _uuid_generate_time = lib.uuid_generate_time
+
+ # On Windows prior to 2000, UuidCreate gives a UUID containing the
+ # hardware address. On Windows 2000 and later, UuidCreate makes a
+ # random UUID and UuidCreateSequential gives a UUID containing the
+ # hardware address. These routines are provided by the RPC runtime.
+ # NOTE: at least on Tim's WinXP Pro SP2 desktop box, while the last
+ # 6 bytes returned by UuidCreateSequential are fixed, they don't appear
+ # to bear any relationship to the MAC address of any network device
+ # on the box.
+ try:
+ lib = ctypes.windll.rpcrt4
+ except:
+ lib = None
+ _UuidCreate = getattr(lib, 'UuidCreateSequential',
+ getattr(lib, 'UuidCreate', None))
+except:
+ pass
+
+def _unixdll_getnode():
+ """Get the hardware address on Unix using ctypes."""
+ _uuid_generate_time(_buffer)
+ return UUID(bytes=_buffer.raw).node
+
+def _windll_getnode():
+ """Get the hardware address on Windows using ctypes."""
+ if _UuidCreate(_buffer) == 0:
+ return UUID(bytes=_buffer.raw).node
+
+def _random_getnode():
+ """Get a random node ID, with eighth bit set as suggested by RFC 4122."""
+ import random
+ return random.randrange(0, 1<<48L) | 0x010000000000L
+
+_node = None
+
+def getnode():
+ """Get the hardware address as a 48-bit positive integer.
+
+ The first time this runs, it may launch a separate program, which could
+ be quite slow. If all attempts to obtain the hardware address fail, we
+ choose a random 48-bit number with its eighth bit set to 1 as recommended
+ in RFC 4122.
+ """
+
+ global _node
+ if _node is not None:
+ return _node
+
+ import sys
+ if sys.platform == 'win32':
+ getters = [_windll_getnode, _netbios_getnode, _ipconfig_getnode]
+ else:
+ getters = [_unixdll_getnode, _ifconfig_getnode]
+
+ for getter in getters + [_random_getnode]:
+ try:
+ _node = getter()
+ except:
+ continue
+ if _node is not None:
+ return _node
+
+_last_timestamp = None
+
+def uuid1(node=None, clock_seq=None):
+ """Generate a UUID from a host ID, sequence number, and the current time.
+ If 'node' is not given, getnode() is used to obtain the hardware
+ address. If 'clock_seq' is given, it is used as the sequence number;
+ otherwise a random 14-bit sequence number is chosen."""
+
+ # When the system provides a version-1 UUID generator, use it (but don't
+ # use UuidCreate here because its UUIDs don't conform to RFC 4122).
+ if _uuid_generate_time and node is clock_seq is None:
+ _uuid_generate_time(_buffer)
+ return UUID(bytes=_buffer.raw)
+
+ global _last_timestamp
+ import time
+ nanoseconds = int(time.time() * 1e9)
+ # 0x01b21dd213814000 is the number of 100-ns intervals between the
+ # UUID epoch 1582-10-15 00:00:00 and the Unix epoch 1970-01-01 00:00:00.
+ timestamp = int(nanoseconds/100) + 0x01b21dd213814000L
+ if timestamp <= _last_timestamp:
+ timestamp = _last_timestamp + 1
+ _last_timestamp = timestamp
+ if clock_seq is None:
+ import random
+ clock_seq = random.randrange(1<<14L) # instead of stable storage
+ time_low = timestamp & 0xffffffffL
+ time_mid = (timestamp >> 32L) & 0xffffL
+ time_hi_version = (timestamp >> 48L) & 0x0fffL
+ clock_seq_low = clock_seq & 0xffL
+ clock_seq_hi_variant = (clock_seq >> 8L) & 0x3fL
+ if node is None:
+ node = getnode()
+ return UUID(fields=(time_low, time_mid, time_hi_version,
+ clock_seq_hi_variant, clock_seq_low, node), version=1)
+
+def uuid3(namespace, name):
+ """Generate a UUID from the MD5 hash of a namespace UUID and a name."""
+ import md5
+ hash = md5.md5(namespace.bytes + name).digest()
+ return UUID(bytes=hash[:16], version=3)
+
+def uuid4():
+ """Generate a random UUID."""
+
+ # When the system provides a version-4 UUID generator, use it.
+ if _uuid_generate_random:
+ _uuid_generate_random(_buffer)
+ return UUID(bytes=_buffer.raw)
+
+ # Otherwise, get randomness from urandom or the 'random' module.
+ try:
+ import os
+ return UUID(bytes=os.urandom(16), version=4)
+ except:
+ import random
+ bytes = [chr(random.randrange(256)) for i in range(16)]
+ return UUID(bytes=bytes, version=4)
+
+def uuid5(namespace, name):
+ """Generate a UUID from the SHA-1 hash of a namespace UUID and a name."""
+ import sha
+ hash = sha.sha(namespace.bytes + name).digest()
+ return UUID(bytes=hash[:16], version=5)
+
+# The following standard UUIDs are for use with uuid3() or uuid5().
+
+NAMESPACE_DNS = UUID('6ba7b810-9dad-11d1-80b4-00c04fd430c8')
+NAMESPACE_URL = UUID('6ba7b811-9dad-11d1-80b4-00c04fd430c8')
+NAMESPACE_OID = UUID('6ba7b812-9dad-11d1-80b4-00c04fd430c8')
+NAMESPACE_X500 = UUID('6ba7b814-9dad-11d1-80b4-00c04fd430c8')
diff --git a/ipalib/parameters.py b/ipalib/parameters.py
new file mode 100644
index 000000000..76d88347c
--- /dev/null
+++ b/ipalib/parameters.py
@@ -0,0 +1,990 @@
+# 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
+
+"""
+Parameter system for command plugins.
+
+TODO:
+
+ * Change rule call signature to rule(_, value, **kw) so that rules can also
+ validate relative to other parameter values (e.g., login name as it relates
+ to first name and last name)
+
+ * Add the _rule_pattern() methods to `Bytes` and `Str`
+
+ * Add maxvalue, minvalue kwargs and rules to `Int` and `Float`
+"""
+
+from types import NoneType
+from util import make_repr
+from request import ugettext
+from plugable import ReadOnly, lock, check_name
+from errors2 import ConversionError, RequirementError, ValidationError
+from constants import NULLS, TYPE_ERROR, CALLABLE_ERROR
+
+class DefaultFrom(ReadOnly):
+ """
+ Derive a default value from other supplied values.
+
+ For example, say you wanted to create a default for the user's login from
+ the user's first and last names. It could be implemented like this:
+
+ >>> login = DefaultFrom(lambda first, last: first[0] + last)
+ >>> login(first='John', last='Doe')
+ 'JDoe'
+
+ If you do not explicitly provide keys when you create a `DefaultFrom`
+ instance, the keys are implicitly derived from your callback by
+ inspecting ``callback.func_code.co_varnames``. The keys are available
+ through the ``DefaultFrom.keys`` instance attribute, like this:
+
+ >>> login.keys
+ ('first', 'last')
+
+ The callback is available through the ``DefaultFrom.callback`` instance
+ attribute, like this:
+
+ >>> login.callback # doctest:+ELLIPSIS
+ <function <lambda> at 0x...>
+ >>> login.callback.func_code.co_varnames # The keys
+ ('first', 'last')
+
+ The keys can be explicitly provided as optional positional arguments after
+ the callback. For example, this is equivalent to the ``login`` instance
+ above:
+
+ >>> login2 = DefaultFrom(lambda a, b: a[0] + b, 'first', 'last')
+ >>> login2.keys
+ ('first', 'last')
+ >>> login2.callback.func_code.co_varnames # Not the keys
+ ('a', 'b')
+ >>> login2(first='John', last='Doe')
+ 'JDoe'
+
+ If any keys are missing when calling your `DefaultFrom` instance, your
+ callback is not called and ``None`` is returned. For example:
+
+ >>> login(first='John', lastname='Doe') is None
+ True
+ >>> login() is None
+ True
+
+ Any additional keys are simply ignored, like this:
+
+ >>> login(last='Doe', first='John', middle='Whatever')
+ 'JDoe'
+
+ As above, because `DefaultFrom.__call__` takes only pure keyword
+ arguments, they can be supplied in any order.
+
+ Of course, the callback need not be a ``lambda`` expression. This third
+ example is equivalent to both the ``login`` and ``login2`` instances
+ above:
+
+ >>> def get_login(first, last):
+ ... return first[0] + last
+ ...
+ >>> login3 = DefaultFrom(get_login)
+ >>> login3.keys
+ ('first', 'last')
+ >>> login3.callback.func_code.co_varnames
+ ('first', 'last')
+ >>> login3(first='John', last='Doe')
+ 'JDoe'
+ """
+
+ def __init__(self, callback, *keys):
+ """
+ :param callback: The callable to call when all keys are present.
+ :param keys: Optional keys used for source values.
+ """
+ if not callable(callback):
+ raise TypeError(
+ CALLABLE_ERROR % ('callback', callback, type(callback))
+ )
+ self.callback = callback
+ if len(keys) == 0:
+ fc = callback.func_code
+ self.keys = fc.co_varnames[:fc.co_argcount]
+ else:
+ self.keys = keys
+ for key in self.keys:
+ if type(key) is not str:
+ raise TypeError(
+ TYPE_ERROR % ('keys', str, key, type(key))
+ )
+ lock(self)
+
+ def __call__(self, **kw):
+ """
+ Call the callback if all keys are present.
+
+ If all keys are present, the callback is called and its return value is
+ returned. If any keys are missing, ``None`` is returned.
+
+ :param kw: The keyword arguments.
+ """
+ vals = tuple(kw.get(k, None) for k in self.keys)
+ if None in vals:
+ return
+ try:
+ return self.callback(*vals)
+ except StandardError:
+ pass
+
+
+def parse_param_spec(spec):
+ """
+ Parse shorthand ``spec`` into to ``(name, kw)``.
+
+ The ``spec`` string determines the parameter name, whether the parameter is
+ required, and whether the parameter is multivalue according the following
+ syntax:
+
+ ====== ===== ======== ==========
+ Spec Name Required Multivalue
+ ====== ===== ======== ==========
+ 'var' 'var' True False
+ 'var?' 'var' False False
+ 'var*' 'var' False True
+ 'var+' 'var' True True
+ ====== ===== ======== ==========
+
+ For example,
+
+ >>> parse_param_spec('login')
+ ('login', {'required': True, 'multivalue': False})
+ >>> parse_param_spec('gecos?')
+ ('gecos', {'required': False, 'multivalue': False})
+ >>> parse_param_spec('telephone_numbers*')
+ ('telephone_numbers', {'required': False, 'multivalue': True})
+ >>> parse_param_spec('group+')
+ ('group', {'required': True, 'multivalue': True})
+
+ :param spec: A spec string.
+ """
+ if type(spec) is not str:
+ raise TypeError(
+ TYPE_ERROR % ('spec', str, spec, type(spec))
+ )
+ if len(spec) < 2:
+ raise ValueError(
+ 'spec must be at least 2 characters; got %r' % spec
+ )
+ _map = {
+ '?': dict(required=False, multivalue=False),
+ '*': dict(required=False, multivalue=True),
+ '+': dict(required=True, multivalue=True),
+ }
+ end = spec[-1]
+ if end in _map:
+ return (spec[:-1], _map[end])
+ return (spec, dict(required=True, multivalue=False))
+
+
+__messages = set()
+
+def _(message):
+ __messages.add(message)
+ return message
+
+
+class Param(ReadOnly):
+ """
+ Base class for all parameters.
+ """
+
+ # This is a dummy type so that most of the functionality of Param can be
+ # unit tested directly without always creating a subclass; however, a real
+ # (direct) subclass must *always* override this class attribute:
+ type = NoneType # Ouch, this wont be very useful in the real world!
+
+ # Subclasses should override this with something more specific:
+ type_error = _('incorrect type')
+
+ kwargs = (
+ ('cli_name', str, None),
+ ('label', callable, None),
+ ('doc', str, ''),
+ ('required', bool, True),
+ ('multivalue', bool, False),
+ ('primary_key', bool, False),
+ ('normalizer', callable, None),
+ ('default_from', DefaultFrom, None),
+ ('create_default', callable, None),
+ ('autofill', bool, False),
+ ('query', bool, False),
+ ('flags', frozenset, frozenset()),
+
+ # The 'default' kwarg gets appended in Param.__init__():
+ # ('default', self.type, None),
+ )
+
+ def __init__(self, name, *rules, **kw):
+ # We keep these values to use in __repr__():
+ self.param_spec = name
+ self.__kw = dict(kw)
+
+ # Merge in kw from parse_param_spec():
+ if not ('required' in kw or 'multivalue' in kw):
+ (name, kw_from_spec) = parse_param_spec(name)
+ kw.update(kw_from_spec)
+ self.name = check_name(name)
+ self.nice = '%s(%r)' % (self.__class__.__name__, self.param_spec)
+
+ # Add 'default' to self.kwargs and makes sure no unknown kw were given:
+ assert type(self.type) is type
+ self.kwargs += (('default', self.type, None),)
+ if not set(t[0] for t in self.kwargs).issuperset(self.__kw):
+ extra = set(kw) - set(t[0] for t in self.kwargs)
+ raise TypeError(
+ '%s: takes no such kwargs: %s' % (self.nice,
+ ', '.join(repr(k) for k in sorted(extra))
+ )
+ )
+
+ # Merge in default for 'cli_name' if not given:
+ if kw.get('cli_name', None) is None:
+ kw['cli_name'] = self.name
+
+ # Wrap 'default_from' in a DefaultFrom if not already:
+ df = kw.get('default_from', None)
+ if callable(df) and not isinstance(df, DefaultFrom):
+ kw['default_from'] = DefaultFrom(df)
+
+ # We keep this copy with merged values also to use when cloning:
+ self.__clonekw = kw
+
+ # Perform type validation on kw, add in class rules:
+ class_rules = []
+ for (key, kind, default) in self.kwargs:
+ value = kw.get(key, default)
+ if value is not None:
+ if kind is frozenset:
+ if type(value) in (list, tuple):
+ value = frozenset(value)
+ elif type(value) is str:
+ value = frozenset([value])
+ if (
+ type(kind) is type and type(value) is not kind
+ or
+ type(kind) is tuple and not isinstance(value, kind)
+ ):
+ raise TypeError(
+ TYPE_ERROR % (key, kind, value, type(value))
+ )
+ elif kind is callable and not callable(value):
+ raise TypeError(
+ CALLABLE_ERROR % (key, value, type(value))
+ )
+ if hasattr(self, key):
+ raise ValueError('kwarg %r conflicts with attribute on %s' % (
+ key, self.__class__.__name__)
+ )
+ setattr(self, key, value)
+ rule_name = '_rule_%s' % key
+ if value is not None and hasattr(self, rule_name):
+ class_rules.append(getattr(self, rule_name))
+ check_name(self.cli_name)
+
+ # Check that only default_from or create_default was provided:
+ assert not hasattr(self, '_get_default'), self.nice
+ if callable(self.default_from):
+ if callable(self.create_default):
+ raise ValueError(
+ '%s: cannot have both %r and %r' % (
+ self.nice, 'default_from', 'create_default')
+ )
+ self._get_default = self.default_from
+ elif callable(self.create_default):
+ self._get_default = self.create_default
+ else:
+ self._get_default = None
+
+ # Check that all the rules are callable
+ self.class_rules = tuple(class_rules)
+ self.rules = rules
+ self.all_rules = self.class_rules + self.rules
+ for rule in self.all_rules:
+ if not callable(rule):
+ raise TypeError(
+ '%s: rules must be callable; got %r' % (self.nice, rule)
+ )
+
+ # And we're done.
+ lock(self)
+
+ def __repr__(self):
+ """
+ Return an expresion that could construct this `Param` instance.
+ """
+ return make_repr(
+ self.__class__.__name__,
+ self.param_spec,
+ **self.__kw
+ )
+
+ def __call__(self, value, **kw):
+ """
+ One stop shopping.
+ """
+ if value in NULLS:
+ value = self.get_default(**kw)
+ else:
+ value = self.convert(self.normalize(value))
+ self.validate(value)
+ return value
+
+ def clone(self, **overrides):
+ """
+ Return a new `Param` instance similar to this one.
+ """
+ kw = dict(self.__clonekw)
+ kw.update(overrides)
+ return self.__class__(self.name, **kw)
+
+ def get_label(self):
+ """
+ Return translated label using `request.ugettext`.
+ """
+ if self.label is None:
+ return self.cli_name.decode('UTF-8')
+ return self.label(ugettext)
+
+ def normalize(self, value):
+ """
+ Normalize ``value`` using normalizer callback.
+
+ For example:
+
+ >>> param = Param('telephone',
+ ... normalizer=lambda value: value.replace('.', '-')
+ ... )
+ >>> param.normalize(u'800.123.4567')
+ u'800-123-4567'
+
+ If this `Param` instance was created with a normalizer callback and
+ ``value`` is a unicode instance, the normalizer callback is called and
+ *its* return value is returned.
+
+ On the other hand, if this `Param` instance was *not* created with a
+ normalizer callback, if ``value`` is *not* a unicode instance, or if an
+ exception is caught when calling the normalizer callback, ``value`` is
+ returned unchanged.
+
+ :param value: A proposed value for this parameter.
+ """
+ if self.normalizer is None:
+ return value
+ if self.multivalue:
+ if type(value) in (tuple, list):
+ return tuple(
+ self._normalize_scalar(v) for v in value
+ )
+ return (self._normalize_scalar(value),) # Return a tuple
+ return self._normalize_scalar(value)
+
+ def _normalize_scalar(self, value):
+ """
+ Normalize a scalar value.
+
+ This method is called once for each value in a multivalue.
+ """
+ if type(value) is not unicode:
+ return value
+ try:
+ return self.normalizer(value)
+ except StandardError:
+ return value
+
+ def convert(self, value):
+ """
+ Convert ``value`` to the Python type required by this parameter.
+
+ For example:
+
+ >>> scalar = Str('my_scalar')
+ >>> scalar.type
+ <type 'unicode'>
+ >>> scalar.convert(43.2)
+ u'43.2'
+
+ (Note that `Str` is a subclass of `Param`.)
+
+ All values in `constants.NULLS` will be converted to ``None``. For
+ example:
+
+ >>> scalar.convert(u'') is None # An empty string
+ True
+ >>> scalar.convert([]) is None # An empty list
+ True
+
+ Likewise, values in `constants.NULLS` will be filtered out of a
+ multivalue parameter. For example:
+
+ >>> multi = Str('my_multi', multivalue=True)
+ >>> multi.convert([1.5, '', 17, None, u'Hello'])
+ (u'1.5', u'17', u'Hello')
+ >>> multi.convert([None, u'']) is None # Filters to an empty list
+ True
+
+ Lastly, multivalue parameters will always return a ``tuple`` (assuming
+ they don't return ``None`` as in the last example above). For example:
+
+ >>> multi.convert(42) # Called with a scalar value
+ (u'42',)
+ >>> multi.convert([0, 1]) # Called with a list value
+ (u'0', u'1')
+
+ Note that how values are converted (and from what types they will be
+ converted) completely depends upon how a subclass implements its
+ `Param._convert_scalar()` method. For example, see
+ `Str._convert_scalar()`.
+
+ :param value: A proposed value for this parameter.
+ """
+ if value in NULLS:
+ return
+ if self.multivalue:
+ if type(value) not in (tuple, list):
+ value = (value,)
+ values = tuple(
+ self._convert_scalar(v, i) for (i, v) in filter(
+ lambda iv: iv[1] not in NULLS, enumerate(value)
+ )
+ )
+ if len(values) == 0:
+ return
+ return values
+ return self._convert_scalar(value)
+
+ def _convert_scalar(self, value, index=None):
+ """
+ Convert a single scalar value.
+ """
+ if type(value) is self.type:
+ return value
+ raise ConversionError(name=self.name, index=index,
+ error=ugettext(self.type_error),
+ )
+
+ def validate(self, value):
+ """
+ Check validity of ``value``.
+
+ :param value: A proposed value for this parameter.
+ """
+ if value is None:
+ if self.required:
+ raise RequirementError(name=self.name)
+ return
+ if self.query:
+ return
+ if self.multivalue:
+ if type(value) is not tuple:
+ raise TypeError(
+ TYPE_ERROR % ('value', tuple, value, type(value))
+ )
+ if len(value) < 1:
+ raise ValueError('value: empty tuple must be converted to None')
+ for (i, v) in enumerate(value):
+ self._validate_scalar(v, i)
+ else:
+ self._validate_scalar(value)
+
+ def _validate_scalar(self, value, index=None):
+ if type(value) is not self.type:
+ if index is None:
+ name = 'value'
+ else:
+ name = 'value[%d]' % index
+ raise TypeError(
+ TYPE_ERROR % (name, self.type, value, type(value))
+ )
+ if index is not None and type(index) is not int:
+ raise TypeError(
+ TYPE_ERROR % ('index', int, index, type(index))
+ )
+ for rule in self.all_rules:
+ error = rule(ugettext, value)
+ if error is not None:
+ raise ValidationError(
+ name=self.name,
+ value=value,
+ index=index,
+ error=error,
+ rule=rule,
+ )
+
+ def get_default(self, **kw):
+ """
+ Return the static default or construct and return a dynamic default.
+
+ (In these examples, we will use the `Str` and `Bytes` classes, which
+ both subclass from `Param`.)
+
+ The *default* static default is ``None``. For example:
+
+ >>> s = Str('my_str')
+ >>> s.default is None
+ True
+ >>> s.get_default() is None
+ True
+
+ However, you can provide your own static default via the ``default``
+ keyword argument when you create your `Param` instance. For example:
+
+ >>> s = Str('my_str', default=u'My Static Default')
+ >>> s.default
+ u'My Static Default'
+ >>> s.get_default()
+ u'My Static Default'
+
+ If you need to generate a dynamic default from other supplied parameter
+ values, provide a callback via the ``default_from`` keyword argument.
+ This callback will be automatically wrapped in a `DefaultFrom` instance
+ if it isn't one already (see the `DefaultFrom` class for all the gory
+ details). For example:
+
+ >>> login = Str('login', default=u'my-static-login-default',
+ ... default_from=lambda first, last: (first[0] + last).lower(),
+ ... )
+ >>> isinstance(login.default_from, DefaultFrom)
+ True
+ >>> login.default_from.keys
+ ('first', 'last')
+
+ Then when all the keys needed by the `DefaultFrom` instance are present,
+ the dynamic default is constructed and returned. For example:
+
+ >>> kw = dict(last=u'Doe', first=u'John')
+ >>> login.get_default(**kw)
+ u'jdoe'
+
+ Or if any keys are missing, your *static* default is returned.
+ For example:
+
+ >>> kw = dict(first=u'John', department=u'Engineering')
+ >>> login.get_default(**kw)
+ u'my-static-login-default'
+
+ The second, less common way to construct a dynamic default is to provide
+ a callback via the ``create_default`` keyword argument. Unlike a
+ ``default_from`` callback, your ``create_default`` callback will not get
+ wrapped in any dispatcher. Instead, it will be called directly, which
+ means your callback must accept arbitrary keyword arguments, although
+ whether your callback utilises these values is up to your
+ implementation. For example:
+
+ >>> def make_csr(**kw):
+ ... print ' make_csr(%r)' % (kw,) # Note output below
+ ... return 'Certificate Signing Request'
+ ...
+ >>> csr = Bytes('csr', create_default=make_csr)
+
+ Your ``create_default`` callback will be called with whatever keyword
+ arguments are passed to `Param.get_default()`. For example:
+
+ >>> kw = dict(arbitrary='Keyword', arguments='Here')
+ >>> csr.get_default(**kw)
+ make_csr({'arguments': 'Here', 'arbitrary': 'Keyword'})
+ 'Certificate Signing Request'
+
+ And your ``create_default`` callback is called even if
+ `Param.get_default()` is called with *zero* keyword arguments.
+ For example:
+
+ >>> csr.get_default()
+ make_csr({})
+ 'Certificate Signing Request'
+
+ The ``create_default`` callback will most likely be used as a
+ pre-execute hook to perform some special client-side operation. For
+ example, the ``csr`` parameter above might make a call to
+ ``/usr/bin/openssl``. However, often a ``create_default`` callback
+ could also be implemented as a ``default_from`` callback. When this is
+ the case, a ``default_from`` callback should be used as they are more
+ structured and therefore less error-prone.
+
+ The ``default_from`` and ``create_default`` keyword arguments are
+ mutually exclusive. If you provide both, a ``ValueError`` will be
+ raised. For example:
+
+ >>> homedir = Str('home',
+ ... default_from=lambda login: '/home/%s' % login,
+ ... create_default=lambda **kw: '/lets/use/this',
+ ... )
+ Traceback (most recent call last):
+ ...
+ ValueError: Str('home'): cannot have both 'default_from' and 'create_default'
+ """
+ if self._get_default is not None:
+ default = self._get_default(**kw)
+ if default is not None:
+ try:
+ return self.convert(self.normalize(default))
+ except StandardError:
+ pass
+ return self.default
+
+
+class Bool(Param):
+ """
+ A parameter for boolean values (stored in the ``bool`` type).
+ """
+
+ type = bool
+ type_error = _('must be True or False')
+
+
+class Flag(Bool):
+ """
+ A boolean parameter that always gets filled in with a default value.
+
+ This `Bool` subclass forces ``autofill=True`` in `Flag.__init__()`. If no
+ default is provided, it also fills in a default value of ``False``.
+ Lastly, unlike the `Bool` class, the default must be either ``True`` or
+ ``False`` and cannot be ``None``.
+
+ For example:
+
+ >>> flag = Flag('my_flag')
+ >>> (flag.autofill, flag.default)
+ (True, False)
+
+ To have a default value of ``True``, create your `Flag` intance with
+ ``default=True``. For example:
+
+ >>> flag = Flag('my_flag', default=True)
+ >>> (flag.autofill, flag.default)
+ (True, True)
+
+ Also note that creating a `Flag` instance with ``autofill=False`` will have
+ no effect. For example:
+
+ >>> flag = Flag('my_flag', autofill=False)
+ >>> flag.autofill
+ True
+ """
+
+ def __init__(self, name, *rules, **kw):
+ kw['autofill'] = True
+ if 'default' not in kw:
+ kw['default'] = False
+ if type(kw['default']) is not bool:
+ default = kw['default']
+ raise TypeError(
+ TYPE_ERROR % ('default', bool, default, type(default))
+ )
+ super(Flag, self).__init__(name, *rules, **kw)
+
+
+class Number(Param):
+ """
+ Base class for the `Int` and `Float` parameters.
+ """
+
+ def _convert_scalar(self, value, index=None):
+ """
+ Convert a single scalar value.
+ """
+ if type(value) is self.type:
+ return value
+ if type(value) in (unicode, int, float):
+ try:
+ return self.type(value)
+ except ValueError:
+ pass
+ raise ConversionError(name=self.name, index=index,
+ error=ugettext(self.type_error),
+ )
+
+
+class Int(Number):
+ """
+ A parameter for integer values (stored in the ``int`` type).
+ """
+
+ type = int
+ type_error = _('must be an integer')
+
+
+class Float(Number):
+ """
+ A parameter for floating-point values (stored in the ``float`` type).
+ """
+
+ type = float
+ type_error = _('must be a decimal number')
+
+
+class Data(Param):
+ """
+ Base class for the `Bytes` and `Str` parameters.
+
+ Previously `Str` was as subclass of `Bytes`. Now the common functionality
+ has been split into this base class so that ``isinstance(foo, Bytes)`` wont
+ be ``True`` when ``foo`` is actually an `Str` instance (which is confusing).
+ """
+
+ kwargs = Param.kwargs + (
+ ('minlength', int, None),
+ ('maxlength', int, None),
+ ('length', int, None),
+ )
+
+ def __init__(self, name, *rules, **kw):
+ super(Data, self).__init__(name, *rules, **kw)
+
+ if not (
+ self.length is None or
+ (self.minlength is None and self.maxlength is None)
+ ):
+ raise ValueError(
+ '%s: cannot mix length with minlength or maxlength' % self.nice
+ )
+
+ if self.minlength is not None and self.minlength < 1:
+ raise ValueError(
+ '%s: minlength must be >= 1; got %r' % (self.nice, self.minlength)
+ )
+
+ if self.maxlength is not None and self.maxlength < 1:
+ raise ValueError(
+ '%s: maxlength must be >= 1; got %r' % (self.nice, self.maxlength)
+ )
+
+ if None not in (self.minlength, self.maxlength):
+ if self.minlength > self.maxlength:
+ raise ValueError(
+ '%s: minlength > maxlength (minlength=%r, maxlength=%r)' % (
+ self.nice, self.minlength, self.maxlength)
+ )
+ elif self.minlength == self.maxlength:
+ raise ValueError(
+ '%s: minlength == maxlength; use length=%d instead' % (
+ self.nice, self.minlength)
+ )
+
+
+class Bytes(Data):
+ """
+ A parameter for binary data (stored in the ``str`` type).
+
+ This class is named *Bytes* instead of *Str* so it's aligned with the
+ Python v3 ``(str, unicode) => (bytes, str)`` clean-up. See:
+
+ http://docs.python.org/3.0/whatsnew/3.0.html
+ """
+
+ type = str
+ type_error = _('must be binary data')
+
+ kwargs = Data.kwargs + (
+ ('pattern', str, None),
+ )
+
+ def _rule_minlength(self, _, value):
+ """
+ Check minlength constraint.
+ """
+ assert type(value) is str
+ if len(value) < self.minlength:
+ return _('must be at least %(minlength)d bytes') % dict(
+ minlength=self.minlength,
+ )
+
+ def _rule_maxlength(self, _, value):
+ """
+ Check maxlength constraint.
+ """
+ assert type(value) is str
+ if len(value) > self.maxlength:
+ return _('can be at most %(maxlength)d bytes') % dict(
+ maxlength=self.maxlength,
+ )
+
+ def _rule_length(self, _, value):
+ """
+ Check length constraint.
+ """
+ assert type(value) is str
+ if len(value) != self.length:
+ return _('must be exactly %(length)d bytes') % dict(
+ length=self.length,
+ )
+
+
+class Str(Data):
+ """
+ A parameter for Unicode text (stored in the ``unicode`` type).
+
+ This class is named *Str* instead of *Unicode* so it's aligned with the
+ Python v3 ``(str, unicode) => (bytes, str)`` clean-up. See:
+
+ http://docs.python.org/3.0/whatsnew/3.0.html
+ """
+
+ type = unicode
+ type_error = _('must be Unicode text')
+
+ kwargs = Data.kwargs + (
+ ('pattern', unicode, None),
+ )
+
+ def _convert_scalar(self, value, index=None):
+ """
+ Convert a single scalar value.
+ """
+ if type(value) is self.type:
+ return value
+ if type(value) in (int, float):
+ return self.type(value)
+ raise ConversionError(name=self.name, index=index,
+ error=ugettext(self.type_error),
+ )
+
+ def _rule_minlength(self, _, value):
+ """
+ Check minlength constraint.
+ """
+ assert type(value) is unicode
+ if len(value) < self.minlength:
+ return _('must be at least %(minlength)d characters') % dict(
+ minlength=self.minlength,
+ )
+
+ def _rule_maxlength(self, _, value):
+ """
+ Check maxlength constraint.
+ """
+ assert type(value) is unicode
+ if len(value) > self.maxlength:
+ return _('can be at most %(maxlength)d characters') % dict(
+ maxlength=self.maxlength,
+ )
+
+ def _rule_length(self, _, value):
+ """
+ Check length constraint.
+ """
+ assert type(value) is unicode
+ if len(value) != self.length:
+ return _('must be exactly %(length)d characters') % dict(
+ length=self.length,
+ )
+
+
+class Password(Str):
+ """
+ A parameter for passwords (stored in the ``unicode`` type).
+ """
+
+
+class Enum(Param):
+ """
+ Base class for parameters with enumerable values.
+ """
+
+ kwargs = Param.kwargs + (
+ ('values', tuple, tuple()),
+ )
+
+ def __init__(self, name, *rules, **kw):
+ super(Enum, self).__init__(name, *rules, **kw)
+ for (i, v) in enumerate(self.values):
+ if type(v) is not self.type:
+ n = '%s values[%d]' % (self.nice, i)
+ raise TypeError(
+ TYPE_ERROR % (n, self.type, v, type(v))
+ )
+
+ def _rule_values(self, _, value, **kw):
+ if value not in self.values:
+ return _('must be one of %(values)r') % dict(
+ values=self.values,
+ )
+
+
+class BytesEnum(Enum):
+ """
+ Enumerable for binary data (stored in the ``str`` type).
+ """
+
+ type = unicode
+
+
+class StrEnum(Enum):
+ """
+ Enumerable for Unicode text (stored in the ``unicode`` type).
+
+ For example:
+
+ >>> enum = StrEnum('my_enum', values=(u'One', u'Two', u'Three'))
+ >>> enum.validate(u'Two') is None
+ True
+ >>> enum.validate(u'Four')
+ Traceback (most recent call last):
+ ...
+ ValidationError: invalid 'my_enum': must be one of (u'One', u'Two', u'Three')
+ """
+
+ type = unicode
+
+
+def create_param(spec):
+ """
+ Create an `Str` instance from the shorthand ``spec``.
+
+ This function allows you to create `Str` parameters (the most common) from
+ a convenient shorthand that defines the parameter name, whether it is
+ required, and whether it is multivalue. (For the definition of the
+ shorthand syntax, see the `parse_param_spec()` function.)
+
+ If ``spec`` is an ``str`` instance, it will be used to create a new `Str`
+ parameter, which will be returned. For example:
+
+ >>> s = create_param('hometown?')
+ >>> s
+ Str('hometown?')
+ >>> (s.name, s.required, s.multivalue)
+ ('hometown', False, False)
+
+ On the other hand, if ``spec`` is already a `Param` instance, it is
+ returned unchanged. For example:
+
+ >>> b = Bytes('cert')
+ >>> create_param(b) is b
+ True
+
+ As a plugin author, you will not call this function directly (which would
+ be no more convenient than simply creating the `Str` instance). Instead,
+ `frontend.Command` will call it for you when it evaluates the
+ ``takes_args`` and ``takes_options`` attributes, and `frontend.Object`
+ will call it for you when it evaluates the ``takes_params`` attribute.
+
+ :param spec: A spec string or a `Param` instance.
+ """
+ if isinstance(spec, Param):
+ return spec
+ if type(spec) is not str:
+ raise TypeError(
+ TYPE_ERROR % ('spec', (str, Param), spec, type(spec))
+ )
+ return Str(spec)
diff --git a/ipalib/plugable.py b/ipalib/plugable.py
new file mode 100644
index 000000000..b52db9008
--- /dev/null
+++ b/ipalib/plugable.py
@@ -0,0 +1,718 @@
+# 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
+
+"""
+Plugin framework.
+
+The classes in this module make heavy use of Python container emulation. If
+you are unfamiliar with this Python feature, see
+http://docs.python.org/ref/sequence-types.html
+"""
+
+import re
+import sys
+import inspect
+import threading
+import logging
+import os
+from os import path
+import subprocess
+import errors2
+from config import Env
+import util
+from base import ReadOnly, NameSpace, lock, islocked, check_name
+from constants import DEFAULT_CONFIG
+
+
+class SetProxy(ReadOnly):
+ """
+ A read-only container with set/sequence behaviour.
+
+ This container acts as a proxy to an actual set-like object (a set,
+ frozenset, or dict) that is passed to the constructor. To the extent
+ possible in Python, this underlying set-like object cannot be modified
+ through the SetProxy... which just means you wont do it accidentally.
+ """
+ def __init__(self, s):
+ """
+ :param s: The target set-like object (a set, frozenset, or dict)
+ """
+ allowed = (set, frozenset, dict)
+ if type(s) not in allowed:
+ raise TypeError('%r not in %r' % (type(s), allowed))
+ self.__s = s
+ lock(self)
+
+ def __len__(self):
+ """
+ Return the number of items in this container.
+ """
+ return len(self.__s)
+
+ def __iter__(self):
+ """
+ Iterate (in ascending order) through keys.
+ """
+ for key in sorted(self.__s):
+ yield key
+
+ def __contains__(self, key):
+ """
+ Return True if this container contains ``key``.
+
+ :param key: The key to test for membership.
+ """
+ return key in self.__s
+
+
+class DictProxy(SetProxy):
+ """
+ A read-only container with mapping behaviour.
+
+ This container acts as a proxy to an actual mapping object (a dict) that
+ is passed to the constructor. To the extent possible in Python, this
+ underlying mapping object cannot be modified through the DictProxy...
+ which just means you wont do it accidentally.
+
+ Also see `SetProxy`.
+ """
+ def __init__(self, d):
+ """
+ :param d: The target mapping object (a dict)
+ """
+ if type(d) is not dict:
+ raise TypeError('%r is not %r' % (type(d), dict))
+ self.__d = d
+ super(DictProxy, self).__init__(d)
+
+ def __getitem__(self, key):
+ """
+ Return the value corresponding to ``key``.
+
+ :param key: The key of the value you wish to retrieve.
+ """
+ return self.__d[key]
+
+ def __call__(self):
+ """
+ Iterate (in ascending order by key) through values.
+ """
+ for key in self:
+ yield self.__d[key]
+
+
+class MagicDict(DictProxy):
+ """
+ A mapping container whose values can be accessed as attributes.
+
+ For example:
+
+ >>> magic = MagicDict({'the_key': 'the value'})
+ >>> magic['the_key']
+ 'the value'
+ >>> magic.the_key
+ 'the value'
+
+ This container acts as a proxy to an actual mapping object (a dict) that
+ is passed to the constructor. To the extent possible in Python, this
+ underlying mapping object cannot be modified through the MagicDict...
+ which just means you wont do it accidentally.
+
+ Also see `DictProxy` and `SetProxy`.
+ """
+
+ def __getattr__(self, name):
+ """
+ Return the value corresponding to ``name``.
+
+ :param name: The name of the attribute you wish to retrieve.
+ """
+ try:
+ return self[name]
+ except KeyError:
+ raise AttributeError('no magic attribute %r' % name)
+
+
+class Plugin(ReadOnly):
+ """
+ Base class for all plugins.
+ """
+ __public__ = frozenset()
+ __proxy__ = True
+ __api = None
+
+ def __init__(self):
+ cls = self.__class__
+ self.name = cls.__name__
+ self.module = cls.__module__
+ self.fullname = '%s.%s' % (self.module, self.name)
+ self.doc = inspect.getdoc(cls)
+ if self.doc is None:
+ self.summary = '<%s>' % self.fullname
+ else:
+ self.summary = self.doc.split('\n\n', 1)[0]
+ log = logging.getLogger(self.fullname)
+ for name in ('debug', 'info', 'warning', 'error', 'critical', 'exception'):
+ if hasattr(self, name):
+ raise StandardError(
+ '%s.%s attribute (%r) conflicts with Plugin logger' % (
+ self.name, name, getattr(self, name))
+ )
+ setattr(self, name, getattr(log, name))
+
+ def __get_api(self):
+ """
+ Return `API` instance passed to `finalize()`.
+
+ If `finalize()` has not yet been called, None is returned.
+ """
+ return self.__api
+ api = property(__get_api)
+
+ @classmethod
+ def implements(cls, arg):
+ """
+ Return True if this class implements ``arg``.
+
+ There are three different ways this method can be called:
+
+ With a <type 'str'> argument, e.g.:
+
+ >>> class base(Plugin):
+ ... __public__ = frozenset(['attr1', 'attr2'])
+ ...
+ >>> base.implements('attr1')
+ True
+ >>> base.implements('attr2')
+ True
+ >>> base.implements('attr3')
+ False
+
+ With a <type 'frozenset'> argument, e.g.:
+
+ With any object that has a `__public__` attribute that is
+ <type 'frozenset'>, e.g.:
+
+ Unlike ProxyTarget.implemented_by(), this returns an abstract answer
+ because only the __public__ frozenset is checked... a ProxyTarget
+ need not itself have attributes for all names in __public__
+ (subclasses might provide them).
+ """
+ assert type(cls.__public__) is frozenset
+ if isinstance(arg, str):
+ return arg in cls.__public__
+ if type(getattr(arg, '__public__', None)) is frozenset:
+ return cls.__public__.issuperset(arg.__public__)
+ if type(arg) is frozenset:
+ return cls.__public__.issuperset(arg)
+ raise TypeError(
+ "must be str, frozenset, or have frozenset '__public__' attribute"
+ )
+
+ @classmethod
+ def implemented_by(cls, arg):
+ """
+ Return True if ``arg`` implements public interface of this class.
+
+ This classmethod returns True if:
+
+ 1. ``arg`` is an instance of or subclass of this class, and
+
+ 2. ``arg`` (or ``arg.__class__`` if instance) has an attribute for
+ each name in this class's ``__public__`` frozenset.
+
+ Otherwise, returns False.
+
+ Unlike `Plugin.implements`, this returns a concrete answer because
+ the attributes of the subclass are checked.
+
+ :param arg: An instance of or subclass of this class.
+ """
+ if inspect.isclass(arg):
+ subclass = arg
+ else:
+ subclass = arg.__class__
+ assert issubclass(subclass, cls), 'must be subclass of %r' % cls
+ for name in cls.__public__:
+ if not hasattr(subclass, name):
+ return False
+ return True
+
+ def finalize(self):
+ """
+ """
+ lock(self)
+
+ def set_api(self, api):
+ """
+ Set reference to `API` instance.
+ """
+ assert self.__api is None, 'set_api() can only be called once'
+ assert api is not None, 'set_api() argument cannot be None'
+ self.__api = api
+ if not isinstance(api, API):
+ return
+ for name in api:
+ assert not hasattr(self, name)
+ setattr(self, name, api[name])
+ # FIXME: the 'log' attribute is depreciated. See Plugin.__init__()
+ for name in ('env', 'context', 'log'):
+ if hasattr(api, name):
+ assert not hasattr(self, name)
+ setattr(self, name, getattr(api, name))
+
+ def call(self, executable, *args):
+ """
+ Call ``executable`` with ``args`` using subprocess.call().
+
+ If the call exits with a non-zero exit status,
+ `ipalib.errors2.SubprocessError` is raised, from which you can retrieve
+ the exit code by checking the SubprocessError.returncode attribute.
+
+ This method does *not* return what ``executable`` sent to stdout... for
+ that, use `Plugin.callread()`.
+ """
+ argv = (executable,) + args
+ self.debug('Calling %r', argv)
+ code = subprocess.call(argv)
+ if code != 0:
+ raise errors2.SubprocessError(returncode=code, argv=argv)
+
+ def __repr__(self):
+ """
+ Return 'module_name.class_name()' representation.
+
+ This representation could be used to instantiate this Plugin
+ instance given the appropriate environment.
+ """
+ return '%s.%s()' % (
+ self.__class__.__module__,
+ self.__class__.__name__
+ )
+
+
+class PluginProxy(SetProxy):
+ """
+ Allow access to only certain attributes on a `Plugin`.
+
+ Think of a proxy as an agreement that "I will have at most these
+ attributes". This is different from (although similar to) an interface,
+ which can be thought of as an agreement that "I will have at least these
+ attributes".
+ """
+
+ __slots__ = (
+ '__base',
+ '__target',
+ '__name_attr',
+ '__public__',
+ 'name',
+ 'doc',
+ )
+
+ def __init__(self, base, target, name_attr='name'):
+ """
+ :param base: A subclass of `Plugin`.
+ :param target: An instance ``base`` or a subclass of ``base``.
+ :param name_attr: The name of the attribute on ``target`` from which
+ to derive ``self.name``.
+ """
+ if not inspect.isclass(base):
+ raise TypeError(
+ '`base` must be a class, got %r' % base
+ )
+ if not isinstance(target, base):
+ raise ValueError(
+ '`target` must be an instance of `base`, got %r' % target
+ )
+ self.__base = base
+ self.__target = target
+ self.__name_attr = name_attr
+ self.__public__ = base.__public__
+ self.name = getattr(target, name_attr)
+ self.doc = target.doc
+ assert type(self.__public__) is frozenset
+ super(PluginProxy, self).__init__(self.__public__)
+
+ def implements(self, arg):
+ """
+ Return True if plugin being proxied implements ``arg``.
+
+ This method simply calls the corresponding `Plugin.implements`
+ classmethod.
+
+ Unlike `Plugin.implements`, this is not a classmethod as a
+ `PluginProxy` can only implement anything as an instance.
+ """
+ return self.__base.implements(arg)
+
+ def __clone__(self, name_attr):
+ """
+ Return a `PluginProxy` instance similar to this one.
+
+ The new `PluginProxy` returned will be identical to this one except
+ the proxy name might be derived from a different attribute on the
+ target `Plugin`. The same base and target will be used.
+ """
+ return self.__class__(self.__base, self.__target, name_attr)
+
+ def __getitem__(self, key):
+ """
+ Return attribute named ``key`` on target `Plugin`.
+
+ If this proxy allows access to an attribute named ``key``, that
+ attribute will be returned. If access is not allowed,
+ KeyError will be raised.
+ """
+ if key in self.__public__:
+ return getattr(self.__target, key)
+ raise KeyError('no public attribute %s.%s' % (self.name, key))
+
+ def __getattr__(self, name):
+ """
+ Return attribute named ``name`` on target `Plugin`.
+
+ If this proxy allows access to an attribute named ``name``, that
+ attribute will be returned. If access is not allowed,
+ AttributeError will be raised.
+ """
+ if name in self.__public__:
+ return getattr(self.__target, name)
+ raise AttributeError('no public attribute %s.%s' % (self.name, name))
+
+ def __call__(self, *args, **kw):
+ """
+ Call target `Plugin` and return its return value.
+
+ If `__call__` is not an attribute this proxy allows access to,
+ KeyError is raised.
+ """
+ return self['__call__'](*args, **kw)
+
+ def __repr__(self):
+ """
+ Return a Python expression that could create this instance.
+ """
+ return '%s(%s, %r)' % (
+ self.__class__.__name__,
+ self.__base.__name__,
+ self.__target,
+ )
+
+
+class Registrar(DictProxy):
+ """
+ Collects plugin classes as they are registered.
+
+ The Registrar does not instantiate plugins... it only implements the
+ override logic and stores the plugins in a namespace per allowed base
+ class.
+
+ The plugins are instantiated when `API.finalize()` is called.
+ """
+ def __init__(self, *allowed):
+ """
+ :param allowed: Base classes from which plugins accepted by this
+ Registrar must subclass.
+ """
+ self.__allowed = dict((base, {}) for base in allowed)
+ self.__registered = set()
+ super(Registrar, self).__init__(
+ dict(self.__base_iter())
+ )
+
+ def __base_iter(self):
+ for (base, sub_d) in self.__allowed.iteritems():
+ assert inspect.isclass(base)
+ name = base.__name__
+ assert not hasattr(self, name)
+ setattr(self, name, MagicDict(sub_d))
+ yield (name, base)
+
+ def __findbases(self, klass):
+ """
+ Iterates through allowed bases that ``klass`` is a subclass of.
+
+ Raises `errors2.PluginSubclassError` if ``klass`` is not a subclass of
+ any allowed base.
+
+ :param klass: The plugin class to find bases for.
+ """
+ assert inspect.isclass(klass)
+ found = False
+ for (base, sub_d) in self.__allowed.iteritems():
+ if issubclass(klass, base):
+ found = True
+ yield (base, sub_d)
+ if not found:
+ raise errors2.PluginSubclassError(
+ plugin=klass, bases=self.__allowed.keys()
+ )
+
+ def __call__(self, klass, override=False):
+ """
+ Register the plugin ``klass``.
+
+ :param klass: A subclass of `Plugin` to attempt to register.
+ :param override: If true, override an already registered plugin.
+ """
+ if not inspect.isclass(klass):
+ raise TypeError('plugin must be a class; got %r' % klass)
+
+ # Raise DuplicateError if this exact class was already registered:
+ if klass in self.__registered:
+ raise errors2.PluginDuplicateError(plugin=klass)
+
+ # Find the base class or raise SubclassError:
+ for (base, sub_d) in self.__findbases(klass):
+ # Check override:
+ if klass.__name__ in sub_d:
+ if not override:
+ # Must use override=True to override:
+ raise errors2.PluginOverrideError(
+ base=base.__name__,
+ name=klass.__name__,
+ plugin=klass,
+ )
+ else:
+ if override:
+ # There was nothing already registered to override:
+ raise errors2.PluginMissingOverrideError(
+ base=base.__name__,
+ name=klass.__name__,
+ plugin=klass,
+ )
+
+ # The plugin is okay, add to sub_d:
+ sub_d[klass.__name__] = klass
+
+ # The plugin is okay, add to __registered:
+ self.__registered.add(klass)
+
+
+class LazyContext(object):
+ """
+ On-demand creation of thread-local context attributes.
+ """
+
+ def __init__(self, api):
+ self.__api = api
+ self.__context = threading.local()
+
+ def __getattr__(self, name):
+ if name not in self.__context.__dict__:
+ if name not in self.__api.Context:
+ raise AttributeError('no Context plugin for %r' % name)
+ value = self.__api.Context[name].get_value()
+ self.__context.__dict__[name] = value
+ return self.__context.__dict__[name]
+
+ def __getitem__(self, key):
+ return self.__getattr__(key)
+
+
+
+class API(DictProxy):
+ """
+ Dynamic API object through which `Plugin` instances are accessed.
+ """
+
+ def __init__(self, *allowed):
+ self.__d = dict()
+ self.__done = set()
+ self.register = Registrar(*allowed)
+ self.env = Env()
+ self.context = LazyContext(self)
+ 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 and logging.
+ """
+ self.__doing('bootstrap')
+ self.env._bootstrap(**overrides)
+ self.env._finalize_core(**dict(DEFAULT_CONFIG))
+ log = logging.getLogger('ipa')
+ object.__setattr__(self, 'log', log)
+ if self.env.debug:
+ log.setLevel(logging.DEBUG)
+ else:
+ log.setLevel(logging.INFO)
+
+ # Add stderr handler:
+ stderr = logging.StreamHandler()
+ format = self.env.log_format_stderr
+ if self.env.debug:
+ format = self.env.log_format_stderr_debug
+ stderr.setLevel(logging.DEBUG)
+ elif self.env.verbose:
+ stderr.setLevel(logging.INFO)
+ else:
+ stderr.setLevel(logging.WARNING)
+ stderr.setFormatter(util.LogFormatter(format))
+ log.addHandler(stderr)
+
+ # Add file handler:
+ if self.env.mode in ('dummy', 'unit_test'):
+ return # But not if in unit-test mode
+ log_dir = path.dirname(self.env.log)
+ if not path.isdir(log_dir):
+ try:
+ os.makedirs(log_dir)
+ except OSError:
+ log.warn('Could not create log_dir %r', log_dir)
+ return
+ handler = logging.FileHandler(self.env.log)
+ handler.setFormatter(util.LogFormatter(self.env.log_format_file))
+ if self.env.debug:
+ handler.setLevel(logging.DEBUG)
+ else:
+ handler.setLevel(logging.INFO)
+ log.addHandler(handler)
+
+ def bootstrap_with_global_options(self, options=None, context=None):
+ if options is None:
+ parser = util.add_global_options()
+ (options, args) = parser.parse_args(
+ list(s.decode('utf-8') for s in sys.argv[1:])
+ )
+ overrides = {}
+ if options.env is not None:
+ assert type(options.env) is list
+ for item in options.env:
+ try:
+ (key, value) = item.split('=', 1)
+ except ValueError:
+ # FIXME: this should raise an IPA exception with an
+ # error code.
+ # --Jason, 2008-10-31
+ pass
+ overrides[str(key.strip())] = value.strip()
+ for key in ('conf', 'debug', 'verbose'):
+ value = getattr(options, key, None)
+ if value is not None:
+ overrides[key] = value
+ if context is not None:
+ overrides['context'] = context
+ self.bootstrap(**overrides)
+
+ 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 in ('dummy', 'unit_test'):
+ return
+ util.import_plugins_subpackage('ipalib')
+ if self.env.in_server:
+ util.import_plugins_subpackage('ipaserver')
+
+ def finalize(self):
+ """
+ Finalize the registration, instantiate the plugins.
+
+ `API.bootstrap` will automatically be called if it hasn't been
+ already.
+ """
+ self.__doing('finalize')
+ self.__do_if_not_done('load_plugins')
+
+ class PluginInstance(object):
+ """
+ Represents a plugin instance.
+ """
+
+ i = 0
+
+ def __init__(self, klass):
+ self.created = self.next()
+ self.klass = klass
+ self.instance = klass()
+ self.bases = []
+
+ @classmethod
+ def next(cls):
+ cls.i += 1
+ return cls.i
+
+ class PluginInfo(ReadOnly):
+ def __init__(self, p):
+ assert isinstance(p, PluginInstance)
+ self.created = p.created
+ self.name = p.klass.__name__
+ self.module = str(p.klass.__module__)
+ self.plugin = '%s.%s' % (self.module, self.name)
+ self.bases = tuple(b.__name__ for b in p.bases)
+ lock(self)
+
+ plugins = {}
+ def plugin_iter(base, subclasses):
+ for klass in subclasses:
+ assert issubclass(klass, base)
+ if klass not in plugins:
+ plugins[klass] = PluginInstance(klass)
+ p = plugins[klass]
+ assert base not in p.bases
+ p.bases.append(base)
+ if base.__proxy__:
+ yield PluginProxy(base, p.instance)
+ else:
+ yield p.instance
+
+ for name in self.register:
+ base = self.register[name]
+ magic = getattr(self.register, name)
+ namespace = NameSpace(
+ plugin_iter(base, (magic[k] for k in magic))
+ )
+ assert not (
+ name in self.__d or hasattr(self, name)
+ )
+ self.__d[name] = namespace
+ object.__setattr__(self, name, namespace)
+
+ for p in plugins.itervalues():
+ p.instance.set_api(self)
+ assert p.instance.api is self
+
+ for p in plugins.itervalues():
+ p.instance.finalize()
+ object.__setattr__(self, '_API__finalized', True)
+ tuple(PluginInfo(p) for p in plugins.itervalues())
+ object.__setattr__(self, 'plugins',
+ tuple(PluginInfo(p) for p in plugins.itervalues())
+ )
diff --git a/ipalib/plugins/__init__.py b/ipalib/plugins/__init__.py
new file mode 100644
index 000000000..544429ef3
--- /dev/null
+++ b/ipalib/plugins/__init__.py
@@ -0,0 +1,25 @@
+# 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
+
+"""
+Sub-package containing all core plugins.
+
+By convention, modules with frontend plugins are named f_*.py and modules
+with backend plugins are named b_*.py.
+"""
diff --git a/ipalib/plugins/b_kerberos.py b/ipalib/plugins/b_kerberos.py
new file mode 100644
index 000000000..cc8204976
--- /dev/null
+++ b/ipalib/plugins/b_kerberos.py
@@ -0,0 +1,34 @@
+# 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
+
+"""
+Backend plugin for Kerberos.
+
+This wraps the python-kerberos and python-krbV bindings.
+"""
+
+from ipalib import api
+from ipalib.backend import Backend
+
+class krb(Backend):
+ """
+ Kerberos backend plugin.
+ """
+
+api.register(krb)
diff --git a/ipalib/plugins/b_xmlrpc.py b/ipalib/plugins/b_xmlrpc.py
new file mode 100644
index 000000000..14f2a9bed
--- /dev/null
+++ b/ipalib/plugins/b_xmlrpc.py
@@ -0,0 +1,102 @@
+# Authors:
+# Jason Gerard DeRose <jderose@redhat.com>
+# Rob Crittenden <rcritten@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
+
+"""
+Backend plugin for XML-RPC client.
+
+This provides a lightwieght XML-RPC client using Python standard library
+``xmlrpclib`` module.
+"""
+
+import xmlrpclib
+import socket
+import httplib
+import kerberos
+from ipalib.backend import Backend
+from ipalib.util import xmlrpc_marshal
+from ipalib import api
+from ipalib import errors
+
+class xmlrpc(Backend):
+ """
+ XML-RPC client backend plugin.
+ """
+
+ def get_client(self):
+ """
+ Return an xmlrpclib.ServerProxy instance (the client).
+ """
+ # FIXME: Rob, is there any reason we can't use allow_none=True here?
+ # Are there any reasonably common XML-RPC client implementations
+ # that don't support the <nil/> extension?
+ # See: http://docs.python.org/library/xmlrpclib.html
+ uri = self.env.xmlrpc_uri
+ if uri.startswith('https://'):
+ return xmlrpclib.ServerProxy(uri,
+ transport=KerbTransport(),
+ )
+ return xmlrpclib.ServerProxy(uri)
+
+ def forward_call(self, name, *args, **kw):
+ """
+ Forward a call over XML-RPC to an IPA server.
+ """
+ self.info('Forwarding %r call to %r' % (name, self.env.xmlrpc_uri))
+ client = self.get_client()
+ command = getattr(client, name)
+ params = xmlrpc_marshal(*args, **kw)
+ try:
+ return command(*params)
+ except socket.error, e:
+ raise
+ except xmlrpclib.Fault, e:
+ err = errors.convertFault(e)
+ raise err
+ return
+
+api.register(xmlrpc)
+
+class KerbTransport(xmlrpclib.SafeTransport):
+ """Handles Kerberos Negotiation authentication to an XML-RPC server."""
+
+ def get_host_info(self, host):
+
+ host, extra_headers, x509 = xmlrpclib.Transport.get_host_info(self, host)
+
+ # Set the remote host principal
+ h = host
+ hostinfo = h.split(':')
+ service = "HTTP@" + hostinfo[0]
+
+ try:
+ rc, vc = kerberos.authGSSClientInit(service);
+ except kerberos.GSSError, e:
+ raise kerberos.GSSError(e)
+
+ try:
+ kerberos.authGSSClientStep(vc, "");
+ except kerberos.GSSError, e:
+ raise kerberos.GSSError(e)
+
+ extra_headers = [
+ ("Authorization", "negotiate %s" % kerberos.authGSSClientResponse(vc) )
+ ]
+
+ return host, extra_headers, x509
diff --git a/ipalib/plugins/f_automount.py b/ipalib/plugins/f_automount.py
new file mode 100644
index 000000000..2365ce221
--- /dev/null
+++ b/ipalib/plugins/f_automount.py
@@ -0,0 +1,563 @@
+# Authors:
+# Rob Crittenden <rcritten@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
+
+"""
+Frontend plugins for automount.
+
+RFC 2707bis http://www.padl.com/~lukeh/rfc2307bis.txt
+"""
+
+from ldap import explode_dn
+from ipalib import crud, errors
+from ipalib import api, Str, Flag, Object, Command
+
+map_attributes = ['automountMapName', 'description', ]
+key_attributes = ['description', 'automountKey', 'automountInformation']
+
+def display_entry(textui, entry):
+ # FIXME: for now delete dn here. In the future pass in the kw to
+ # output_for_cli()
+ attr = sorted(entry.keys())
+
+ for a in attr:
+ if a != 'dn':
+ textui.print_plain("%s: %s" % (a, entry[a]))
+
+def make_automount_dn(mapname):
+ """
+ Construct automount dn from map name.
+ """
+ # FIXME, should this be in b_ldap?
+ # Experimenting to see what a plugin looks like for a 3rd party who can't
+ # modify the backend.
+ import ldap
+ return 'automountmapname=%s,%s,%s' % (
+ ldap.dn.escape_dn_chars(mapname),
+ api.env.container_automount,
+ api.env.basedn,
+ )
+
+class automount(Object):
+ """
+ Automount object.
+ """
+ takes_params = (
+ Str('automountmapname',
+ cli_name='mapname',
+ primary_key=True,
+ doc='A group of related automount objects',
+ ),
+ )
+api.register(automount)
+
+
+class automount_addmap(crud.Add):
+ 'Add a new automount map.'
+
+ takes_options = (
+ Str('description?',
+ doc='A description of the automount map'),
+ )
+
+ def execute(self, mapname, **kw):
+ """
+ Execute the automount-addmap operation.
+
+ Returns the entry as it will be created in LDAP.
+
+ :param mapname: The map name being added.
+ :param kw: Keyword arguments for the other LDAP attributes.
+ """
+ assert 'automountmapname' not in kw
+ assert 'dn' not in kw
+ ldap = self.api.Backend.ldap
+ kw['automountmapname'] = mapname
+ kw['dn'] = make_automount_dn(mapname)
+
+ kw['objectClass'] = ['automountMap']
+
+ return ldap.create(**kw)
+
+ def output_for_cli(self, textui, result, map, **options):
+ """
+ Output result of this command to command line interface.
+ """
+ textui.print_plain("Automount map %s added" % map)
+
+api.register(automount_addmap)
+
+
+class automount_addkey(crud.Add):
+ 'Add a new automount key.'
+ takes_options = (
+ Str('automountkey',
+ cli_name='key',
+ doc='An entry in an automount map'),
+ Str('automountinformation',
+ cli_name='info',
+ doc='Mount information for this key'),
+ Str('description?',
+ doc='A description of the mount'),
+ )
+
+ def execute(self, mapname, **kw):
+ """
+ Execute the automount-addkey operation.
+
+ Returns the entry as it will be created in LDAP.
+
+ :param mapname: The map name being added to.
+ :param kw: Keyword arguments for the other LDAP attributes.
+ """
+ assert 'automountmapname' not in kw
+ assert 'dn' not in kw
+ ldap = self.api.Backend.ldap
+ # use find_entry_dn instead of make_automap_dn so we can confirm that
+ # the map exists
+ map_dn = ldap.find_entry_dn("automountmapname", mapname, "automountmap", api.env.container_automount)
+ kw['dn'] = "automountkey=%s,%s" % (kw['automountkey'], map_dn)
+
+ kw['objectClass'] = ['automount']
+
+ return ldap.create(**kw)
+
+ def output_for_cli(self, textui, result, *args, **options):
+ """
+ Output result of this command to command line interface.
+ """
+ textui.print_plain("Automount key added")
+
+api.register(automount_addkey)
+
+
+class automount_delmap(crud.Del):
+ 'Delete an automount map.'
+ def execute(self, mapname, **kw):
+ """Delete an automount map. This will also remove all of the keys
+ associated with this map.
+
+ mapname is the automount map to remove
+
+ :param mapname: The map to be removed
+ :param kw: Not used.
+ """
+ ldap = self.api.Backend.ldap
+ dn = ldap.find_entry_dn("automountmapname", mapname, "automountmap", api.env.container_automount)
+ keys = api.Command['automount_getkeys'](mapname)
+ if keys:
+ for k in keys:
+ ldap.delete(k.get('dn'))
+ return ldap.delete(dn)
+ def output_for_cli(self, textui, result, *args, **options):
+ """
+ Output result of this command to command line interface.
+ """
+ print "Automount map and associated keys deleted"
+
+api.register(automount_delmap)
+
+
+class automount_delkey(crud.Del):
+ 'Delete an automount key.'
+ takes_options = (
+ Str('automountkey',
+ cli_name='key',
+ doc='The automount key to remove'),
+ )
+ def execute(self, mapname, **kw):
+ """Delete an automount key.
+
+ key is the automount key to remove
+
+ :param mapname: The automount map containing the key to be removed
+ :param kw: "key" the key to be removed
+ """
+ ldap = self.api.Backend.ldap
+ dn = ldap.find_entry_dn("automountmapname", mapname, "automountmap", api.env.container_automount)
+ keys = api.Command['automount_getkeys'](mapname)
+ keydn = None
+ keyname = kw.get('automountkey').lower()
+ if keys:
+ for k in keys:
+ if k.get('automountkey').lower() == keyname:
+ keydn = k.get('dn')
+ break
+ if not keydn:
+ raise errors.NotFound
+ return ldap.delete(keydn)
+ def output_for_cli(self, textui, result, *args, **options):
+ """
+ Output result of this command to command line interface.
+ """
+ print "Automount key deleted"
+
+api.register(automount_delkey)
+
+class automount_modmap(crud.Mod):
+ 'Edit an existing automount map.'
+ takes_options = (
+ Str('description?',
+ doc='A description of the automount map'),
+ )
+ def execute(self, mapname, **kw):
+ """
+ Execute the automount-modmap operation.
+
+ The dn should not be passed as a keyword argument as it is constructed
+ by this method.
+
+ Returns the entry
+
+ :param mapname: The map name to update.
+ :param kw: Keyword arguments for the other LDAP attributes.
+ """
+ assert 'automountmapname' not in kw
+ assert 'dn' not in kw
+ ldap = self.api.Backend.ldap
+ dn = ldap.find_entry_dn("automountmapname", mapname, "automountmap", api.env.container_automount)
+ return ldap.update(dn, **kw)
+
+ def output_for_cli(self, textui, result, *args, **options):
+ """
+ Output result of this command to command line interface.
+ """
+ print "Automount map updated"
+
+api.register(automount_modmap)
+
+
+class automount_modkey(crud.Mod):
+ 'Edit an existing automount key.'
+ takes_options = (
+ Str('automountkey',
+ cli_name='key',
+ doc='An entry in an automount map'),
+ Str('automountinformation?',
+ cli_name='info',
+ doc='Mount information for this key'),
+ Str('description?',
+ doc='A description of the automount map'),
+ )
+ def execute(self, mapname, **kw):
+ """
+ Execute the automount-modkey operation.
+
+ Returns the entry
+
+ :param mapname: The map name to update.
+ :param kw: Keyword arguments for the other LDAP attributes.
+ """
+ assert 'automountmapname' not in kw
+ assert 'dn' not in kw
+ keyname = kw.get('automountkey').lower()
+ del kw['automountkey']
+ ldap = self.api.Backend.ldap
+ dn = ldap.find_entry_dn("automountmapname", mapname, "automountmap", api.env.container_automount)
+ keys = api.Command['automount_getkeys'](mapname)
+ keydn = None
+ if keys:
+ for k in keys:
+ if k.get('automountkey').lower() == keyname:
+ keydn = k.get('dn')
+ break
+ if not keydn:
+ raise errors.NotFound
+ return ldap.update(keydn, **kw)
+
+ def output_for_cli(self, textui, result, *args, **options):
+ """
+ Output result of this command to command line interface.
+ """
+ print "Automount key updated"
+
+api.register(automount_modkey)
+
+
+class automount_findmap(crud.Find):
+ 'Search automount maps.'
+ takes_options = (
+ Flag('all', doc='Retrieve all attributes'),
+ )
+ def execute(self, term, **kw):
+ ldap = self.api.Backend.ldap
+
+ search_fields = map_attributes
+
+ for s in search_fields:
+ kw[s] = term
+
+ kw['objectclass'] = 'automountMap'
+ kw['base'] = api.env.container_automount
+ if kw.get('all', False):
+ kw['attributes'] = ['*']
+ else:
+ kw['attributes'] = map_attributes
+ return ldap.search(**kw)
+
+ def output_for_cli(self, textui, result, *args, **options):
+ counter = result[0]
+ entries = result[1:]
+ if counter == 0:
+ textui.print_plain("No entries found")
+ return
+ elif counter == -1:
+ textui.print_plain("These results are truncated.")
+ textui.print_plain("Please refine your search and try again.")
+
+ for e in entries:
+ display_entry(textui, e)
+ textui.print_plain("")
+
+api.register(automount_findmap)
+
+
+class automount_findkey(crud.Find):
+ 'Search automount keys.'
+ takes_options = (
+ Flag('all?', doc='Retrieve all attributes'),
+ )
+ def get_args(self):
+ return (Str('automountkey',
+ cli_name='key',
+ doc='An entry in an automount map'),)
+ def execute(self, term, **kw):
+ ldap = self.api.Backend.ldap
+
+ search_fields = key_attributes
+
+ for s in search_fields:
+ kw[s] = term
+
+ kw['objectclass'] = 'automount'
+ kw['base'] = api.env.container_automount
+ if kw.get('all', False):
+ kw['attributes'] = ['*']
+ else:
+ kw['attributes'] = key_attributes
+ return ldap.search(**kw)
+ def output_for_cli(self, textui, result, *args, **options):
+ counter = result[0]
+ entries = result[1:]
+ if counter == 0:
+ textui.print_plain("No entries found")
+ return
+ elif counter == -1:
+ textui.print_plain("These results are truncated.")
+ textui.print_plain("Please refine your search and try again.")
+
+ for e in entries:
+ display_entry(textui, e)
+ textui.print_plain("")
+
+api.register(automount_findkey)
+
+
+class automount_showmap(crud.Get):
+ 'Examine an existing automount map.'
+ takes_options = (
+ Flag('all?', doc='Retrieve all attributes'),
+ )
+ def execute(self, mapname, **kw):
+ """
+ Execute the automount-showmap operation.
+
+ Returns the entry
+
+ :param mapname: The automount map to retrieve
+ :param kw: "all" set to True = return all attributes
+ """
+ ldap = self.api.Backend.ldap
+ dn = ldap.find_entry_dn("automountmapname", mapname, "automountmap", api.env.container_automount)
+ # FIXME: should kw contain the list of attributes to display?
+ if kw.get('all', False):
+ return ldap.retrieve(dn)
+ else:
+ return ldap.retrieve(dn, map_attributes)
+ def output_for_cli(self, textui, result, *args, **options):
+ if result:
+ display_entry(textui, result)
+
+api.register(automount_showmap)
+
+
+class automount_showkey(crud.Get):
+ 'Examine an existing automount key.'
+ takes_options = (
+ Str('automountkey',
+ cli_name='key',
+ doc='The automount key to display'),
+ Flag('all?', doc='Retrieve all attributes'),
+ )
+ def execute(self, mapname, **kw):
+ """
+ Execute the automount-showkey operation.
+
+ Returns the entry
+
+ :param mapname: The mapname to examine
+ :param kw: "automountkey" the key to retrieve
+ :param kw: "all" set to True = return all attributes
+ """
+ ldap = self.api.Backend.ldap
+ dn = ldap.find_entry_dn("automountmapname", mapname, "automountmap", api.env.container_automount)
+ keys = api.Command['automount_getkeys'](mapname)
+ keyname = kw.get('automountkey').lower()
+ keydn = None
+ if keys:
+ for k in keys:
+ if k.get('automountkey').lower() == keyname:
+ keydn = k.get('dn')
+ break
+ if not keydn:
+ raise errors.NotFound
+ # FIXME: should kw contain the list of attributes to display?
+ if kw.get('all', False):
+ return ldap.retrieve(keydn)
+ else:
+ return ldap.retrieve(keydn, key_attributes)
+ def output_for_cli(self, textui, result, *args, **options):
+ # The automount map name associated with this key is available only
+ # in the dn. Add it as an attribute to display instead.
+ if result and not result.get('automountmapname'):
+ elements = explode_dn(result.get('dn').lower())
+ for e in elements:
+ (attr, value) = e.split('=',1)
+ if attr == 'automountmapname':
+ result['automountmapname'] = value
+ display_entry(textui, result)
+
+api.register(automount_showkey)
+
+
+class automount_getkeys(Command):
+ 'Retrieve all keys for an automount map.'
+ takes_args = (
+ Str('automountmapname',
+ cli_name='mapname',
+ primary_key=True,
+ doc='A group of related automount objects',
+ ),
+ )
+ def execute(self, mapname, **kw):
+ """
+ Execute the automount-getkeys operation.
+
+ Return a list of all automount keys for this mapname
+
+ :param mapname: Retrieve all keys for this mapname
+ """
+ ldap = self.api.Backend.ldap
+ dn = ldap.find_entry_dn("automountmapname", mapname, "automountmap", api.env.container_automount)
+ try:
+ keys = ldap.get_one_entry(dn, 'objectclass=*', ['automountkey'])
+ except errors.NotFound:
+ keys = []
+
+ return keys
+ def output_for_cli(self, textui, result, *args, **options):
+ for k in result:
+ textui.print_plain('%s' % k.get('automountkey'))
+
+api.register(automount_getkeys)
+
+
+class automount_getmaps(Command):
+ 'Retrieve all automount maps'
+ takes_args = (
+ Str('automountmapname?',
+ cli_name='mapname',
+ primary_key=True,
+ doc='A group of related automount objects',
+ ),
+ )
+ def execute(self, mapname, **kw):
+ """
+ Execute the automount-getmaps operation.
+
+ Return a list of all automount maps.
+ """
+
+ ldap = self.api.Backend.ldap
+ base = api.env.container_automount + "," + api.env.basedn
+
+ if not mapname:
+ mapname = "auto.master"
+ search_base = "automountmapname=%s,%s" % (mapname, base)
+ maps = ldap.get_one_entry(search_base, "objectClass=*", ["*"])
+
+ return maps
+ def output_for_cli(self, textui, result, *args, **options):
+ for k in result:
+ textui.print_plain('%s: %s' % (k.get('automountinformation'), k.get('automountkey')))
+
+api.register(automount_getmaps)
+
+class automount_addindirectmap(crud.Add):
+ """
+ Add a new automap indirect mount point.
+ """
+
+ takes_options = (
+ Str('parentmap?',
+ cli_name='parentmap',
+ default=u'auto.master',
+ doc='The parent map to connect this to.',
+ ),
+ Str('automountkey',
+ cli_name='key',
+ doc='An entry in an automount map',
+ ),
+ Str('description?',
+ doc='A description of the automount map',
+ ),
+ )
+
+ def execute(self, mapname, **kw):
+ """
+ Execute the automount-addindirectmap operation.
+
+ Returns the key entry as it will be created in LDAP.
+
+ This function creates 2 LDAP entries. It creates an
+ automountmapname entry and an automountkey entry.
+
+ :param mapname: The map name being added.
+ :param kw['parentmap'] is the top-level map to add this to.
+ defaulting to auto.master
+ :param kw['automountkey'] is the mount point
+ :param kw['description'] is a textual description of this map
+ """
+ mapkw = {}
+ if kw.get('description'):
+ mapkw['description'] = kw.get('description')
+ newmap = api.Command['automount_addmap'](mapname, **mapkw)
+
+ keykw = {'automountkey': kw['automountkey'], 'automountinformation': mapname}
+ if kw.get('description'):
+ keykw['description'] = kw.get('description')
+ newkey = api.Command['automount_addkey'](kw['parentmap'], **keykw)
+
+ return newkey
+ def output_for_cli(self, textui, result, map, **options):
+ """
+ Output result of this command to command line interface.
+ """
+ textui.print_plain("Indirect automount map %s added" % map)
+
+api.register(automount_addindirectmap)
diff --git a/ipalib/plugins/f_delegation.py b/ipalib/plugins/f_delegation.py
new file mode 100644
index 000000000..fbf8cfbff
--- /dev/null
+++ b/ipalib/plugins/f_delegation.py
@@ -0,0 +1,65 @@
+# Authors:
+# Rob Crittenden <rcritten@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
+
+"""
+Frontend plugins for delegations.
+"""
+
+from ipalib import frontend
+from ipalib import crud
+from ipalib.frontend import Param
+from ipalib import api
+from ipalib import errors
+
+class delegation(frontend.Object):
+ """
+ Delegation object.
+ """
+ takes_params = (
+ 'attributes',
+ 'source',
+ 'target',
+ Param('name', primary_key=True)
+ )
+api.register(delegation)
+
+
+class delegation_add(crud.Add):
+ 'Add a new delegation.'
+api.register(delegation_add)
+
+
+class delegation_del(crud.Del):
+ 'Delete an existing delegation.'
+api.register(delegation_del)
+
+
+class delegation_mod(crud.Mod):
+ 'Edit an existing delegation.'
+api.register(delegation_mod)
+
+
+class delegation_find(crud.Find):
+ 'Search for a delegation.'
+api.register(delegation_find)
+
+
+class delegation_show(crud.Get):
+ 'Examine an existing delegation.'
+api.register(delegation_show)
diff --git a/ipalib/plugins/f_group.py b/ipalib/plugins/f_group.py
new file mode 100644
index 000000000..740b32f8c
--- /dev/null
+++ b/ipalib/plugins/f_group.py
@@ -0,0 +1,384 @@
+# 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
+
+"""
+Frontend plugins for group (Identity).
+"""
+
+from ipalib import api, crud, errors
+from ipalib import Object, Command # Plugin base classes
+from ipalib import Str, Int # Parameter types
+
+
+def get_members(members):
+ """
+ Return a list of members.
+
+ It is possible that the value passed in is None.
+ """
+ if members:
+ members = members.split(',')
+ else:
+ members = []
+
+ return members
+
+class group(Object):
+ """
+ Group object.
+ """
+ takes_params = (
+ Str('description',
+ doc='A description of this group',
+ ),
+ Int('gidnumber?',
+ cli_name='gid',
+ doc='The gid to use for this group. If not included one is automatically set.',
+ ),
+ Str('cn',
+ cli_name='name',
+ primary_key=True,
+ normalizer=lambda value: value.lower(),
+ ),
+ )
+api.register(group)
+
+
+class group_add(crud.Add):
+ 'Add a new group.'
+
+ def execute(self, cn, **kw):
+ """
+ Execute the group-add operation.
+
+ The dn should not be passed as a keyword argument as it is constructed
+ by this method.
+
+ Returns the entry as it will be created in LDAP.
+
+ No need to explicitly set gidNumber. The dna_plugin will do this
+ for us if the value isn't provided by the caller.
+
+ :param cn: The name of the group being added.
+ :param kw: Keyword arguments for the other LDAP attributes.
+ """
+ assert 'cn' not in kw
+ assert 'dn' not in kw
+ ldap = self.api.Backend.ldap
+ kw['cn'] = cn
+ kw['dn'] = ldap.make_group_dn(cn)
+
+ # Get our configuration
+ config = ldap.get_ipa_config()
+
+ # some required objectclasses
+ kw['objectClass'] = config.get('ipagroupobjectclasses')
+
+ return ldap.create(**kw)
+
+ def output_for_cli(self, textui, result, *args, **options):
+ """
+ Output result of this command to command line interface.
+ """
+ textui.print_name(self.name)
+ textui.print_entry(result)
+ textui.print_dashed('Added group "%s"' % result['cn'])
+
+api.register(group_add)
+
+
+class group_del(crud.Del):
+ 'Delete an existing group.'
+ def execute(self, cn, **kw):
+ """
+ Delete a group
+
+ The memberOf plugin handles removing the group from any other
+ groups.
+
+ :param cn: The name of the group being removed
+ :param kw: Unused
+ """
+ # We have 2 special groups, don't allow them to be removed
+# if "admins" == cn.lower() or "editors" == cn.lower():
+# raise ipaerror.gen_exception(ipaerror.CONFIG_REQUIRED_GROUPS)
+
+ ldap = self.api.Backend.ldap
+ dn = ldap.find_entry_dn("cn", cn, "posixGroup")
+ self.log.info("IPA: group-del '%s'" % dn)
+
+ # Don't allow the default user group to be removed
+ config=ldap.get_ipa_config()
+ default_group = ldap.find_entry_dn("cn", config.get('ipadefaultprimarygroup'), "posixGroup")
+ if dn == default_group:
+ raise errors.DefaultGroup
+
+ return ldap.delete(dn)
+
+ def output_for_cli(self, textui, result, cn):
+ """
+ Output result of this command to command line interface.
+ """
+ textui.print_plain("Deleted group %s" % cn)
+
+api.register(group_del)
+
+
+class group_mod(crud.Mod):
+ 'Edit an existing group.'
+ def execute(self, cn, **kw):
+ """
+ Execute the group-mod operation.
+
+ The dn should not be passed as a keyword argument as it is constructed
+ by this method.
+
+ Returns the entry
+
+ :param cn: The name of the group to update.
+ :param kw: Keyword arguments for the other LDAP attributes.
+ """
+ assert 'cn' not in kw
+ assert 'dn' not in kw
+ ldap = self.api.Backend.ldap
+ dn = ldap.find_entry_dn("cn", cn, "posixGroup")
+ return ldap.update(dn, **kw)
+
+ def output_for_cli(self, textui, result, cn, **options):
+ """
+ Output result of this command to command line interface.
+ """
+ if result:
+ textui.print_plain("Group updated")
+
+api.register(group_mod)
+
+
+class group_find(crud.Find):
+ 'Search the groups.'
+ def execute(self, term, **kw):
+ ldap = self.api.Backend.ldap
+
+ # Pull the list of searchable attributes out of the configuration.
+ config = ldap.get_ipa_config()
+ search_fields_conf_str = config.get('ipagroupsearchfields')
+ search_fields = search_fields_conf_str.split(",")
+
+ search_kw = {}
+ for s in search_fields:
+ search_kw[s] = term
+
+ object_type = ldap.get_object_type("cn")
+ if object_type and not kw.get('objectclass'):
+ search_kw['objectclass'] = object_type
+ return ldap.search(**search_kw)
+
+ def output_for_cli(self, textui, result, uid, **options):
+ counter = result[0]
+ groups = result[1:]
+ if counter == 0 or len(groups) == 0:
+ textui.print_plain("No entries found")
+ return
+ if len(groups) == 1:
+ textui.print_entry(groups[0])
+ return
+ textui.print_name(self.name)
+
+ for g in groups:
+ textui.print_entry(g)
+ textui.print_plain('')
+ if counter == -1:
+ textui.print_plain("These results are truncated.")
+ textui.print_plain("Please refine your search and try again.")
+ textui.print_count(groups, '%d groups matched')
+
+api.register(group_find)
+
+
+class group_show(crud.Get):
+ 'Examine an existing group.'
+ def execute(self, cn, **kw):
+ """
+ Execute the group-show operation.
+
+ The dn should not be passed as a keyword argument as it is constructed
+ by this method.
+
+ Returns the entry
+
+ :param cn: The group name to retrieve.
+ :param kw: Not used.
+ """
+ ldap = self.api.Backend.ldap
+ dn = ldap.find_entry_dn("cn", cn, "posixGroup")
+ # FIXME: should kw contain the list of attributes to display?
+ return ldap.retrieve(dn)
+
+ def output_for_cli(self, textui, result, *args, **options):
+ counter = result[0]
+ groups = result[1:]
+ if counter == 0 or len(groups) == 0:
+ textui.print_plain("No entries found")
+ return
+ if len(groups) == 1:
+ textui.print_entry(groups[0])
+ return
+ textui.print_name(self.name)
+ for u in groups:
+ textui.print_plain('%(givenname)s %(sn)s:' % u)
+ textui.print_entry(u)
+ textui.print_plain('')
+ if counter == -1:
+ textui.print_plain('These results are truncated.')
+ textui.print_plain('Please refine your search and try again.')
+ textui.print_count(groups, '%d groups matched')
+
+api.register(group_show)
+
+
+class group_add_member(Command):
+ 'Add a member to a group.'
+ takes_args = (
+ Str('group', primary_key=True),
+ )
+ takes_options = (
+ Str('users?', doc='comma-separated list of users to add'),
+ Str('groups?', doc='comma-separated list of groups to add'),
+ )
+ def execute(self, cn, **kw):
+ """
+ Execute the group-add-member operation.
+
+ Returns the updated group entry
+
+ :param cn: The group name to add new members to.
+ :param kw: groups is a comma-separated list of groups to add
+ :parem kw: users is a comma-separated list of users to add
+ """
+ ldap = self.api.Backend.ldap
+ dn = ldap.find_entry_dn("cn", cn)
+ add_failed = []
+ to_add = []
+ completed = 0
+
+ members = get_members(kw.get('groups', ''))
+ for m in members:
+ if not m: continue
+ try:
+ member_dn = ldap.find_entry_dn("cn", m)
+ to_add.append(member_dn)
+ except errors.NotFound:
+ add_failed.append(m)
+ continue
+
+ members = get_members(kw.get('users', ''))
+ for m in members:
+ if not m: continue
+ try:
+ member_dn = ldap.find_entry_dn("uid", m)
+ to_add.append(member_dn)
+ except errors.NotFound:
+ add_failed.append(m)
+ continue
+
+ for member_dn in to_add:
+ try:
+ ldap.add_member_to_group(member_dn, dn)
+ completed+=1
+ except:
+ add_failed.append(member_dn)
+
+ return add_failed
+
+ def output_for_cli(self, textui, result, *args, **options):
+ """
+ Output result of this command to command line interface.
+ """
+ if result:
+ print "These entries failed to add to the group:"
+ for a in add_failed:
+ print "\t'%s'" % a
+
+
+api.register(group_add_member)
+
+
+class group_remove_member(Command):
+ 'Remove a member from a group.'
+ takes_args = (
+ Str('group', primary_key=True),
+ )
+ takes_options = (
+ Str('users?', doc='comma-separated list of users to remove'),
+ Str('groups?', doc='comma-separated list of groups to remove'),
+ )
+ def execute(self, cn, **kw):
+ """
+ Execute the group-remove-member operation.
+
+ Returns the members that could not be added
+
+ :param cn: The group name to add new members to.
+ :param kw: groups is a comma-separated list of groups to remove
+ :parem kw: users is a comma-separated list of users to remove
+ """
+ ldap = self.api.Backend.ldap
+ dn = ldap.find_entry_dn("cn", cn)
+ to_remove = []
+ remove_failed = []
+ completed = 0
+
+ members = get_members(kw.get('groups', ''))
+ for m in members:
+ if not m: continue
+ try:
+ member_dn = ldap.find_entry_dn("cn", m)
+ to_remove.append(member_dn)
+ except errors.NotFound:
+ remove_failed.append(m)
+ continue
+
+ members = get_members(kw.get('users', ''))
+ for m in members:
+ try:
+ member_dn = ldap.find_entry_dn("uid", m,)
+ to_remove.append(member_dn)
+ except errors.NotFound:
+ remove_failed.append(m)
+ continue
+
+ for member_dn in to_remove:
+ try:
+ ldap.remove_member_from_group(member_dn, dn)
+ completed+=1
+ except:
+ remove_failed.append(member_dn)
+
+ return remove_failed
+
+ def output_for_cli(self, textui, result, *args, **options):
+ """
+ Output result of this command to command line interface.
+ """
+ if result:
+ print "These entries failed to be removed from the group:"
+ for a in remove_failed:
+ print "\t'%s'" % a
+
+api.register(group_remove_member)
diff --git a/ipalib/plugins/f_host.py b/ipalib/plugins/f_host.py
new file mode 100644
index 000000000..ea819a77d
--- /dev/null
+++ b/ipalib/plugins/f_host.py
@@ -0,0 +1,286 @@
+# Authors:
+# Rob Crittenden <rcritten@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
+
+"""
+Frontend plugins for host/machine Identity.
+"""
+
+from ipalib import api, crud, errors, util
+from ipalib import Object # Plugin base class
+from ipalib import Str, Flag # Parameter types
+
+
+def get_host(hostname):
+ """
+ Try to get the hostname as fully-qualified first, then fall back to
+ just a host name search.
+ """
+ ldap = api.Backend.ldap
+
+ # Strip off trailing dot
+ if hostname.endswith('.'):
+ hostname = hostname[:-1]
+ try:
+ dn = ldap.find_entry_dn("cn", hostname, "ipaHost")
+ except errors.NotFound:
+ dn = ldap.find_entry_dn("serverhostname", hostname, "ipaHost")
+ return dn
+
+def validate_host(ugettext, cn):
+ """
+ Require at least one dot in the hostname (to support localhost.localdomain)
+ """
+ dots = len(cn.split('.'))
+ if dots < 2:
+ return 'Fully-qualified hostname required'
+ return None
+
+default_attributes = ['cn','description','localityname','nshostlocation','nshardwareplatform','nsosversion']
+
+class host(Object):
+ """
+ Host object.
+ """
+ takes_params = (
+ Str('cn', validate_host,
+ cli_name='hostname',
+ primary_key=True,
+ normalizer=lambda value: value.lower(),
+ ),
+ Str('description?',
+ doc='Description of the host',
+ ),
+ Str('localityname?',
+ cli_name='locality',
+ doc='Locality of this host (Baltimore, MD)',
+ ),
+ Str('nshostlocation?',
+ cli_name='location',
+ doc='Location of this host (e.g. Lab 2)',
+ ),
+ Str('nshardwareplatform?',
+ cli_name='platform',
+ doc='Hardware platform of this host (e.g. Lenovo T61)',
+ ),
+ Str('nsosversion?',
+ cli_name='os',
+ doc='Operating System and version on this host (e.g. Fedora 9)',
+ ),
+ Str('userpassword?',
+ cli_name='password',
+ doc='Set a password to be used in bulk enrollment',
+ ),
+ )
+api.register(host)
+
+
+class host_add(crud.Add):
+ 'Add a new host.'
+ def execute(self, hostname, **kw):
+ """
+ Execute the host-add operation.
+
+ The dn should not be passed as a keyword argument as it is constructed
+ by this method.
+
+ If password is set then this is considered a 'bulk' host so we
+ do not create a kerberos service principal.
+
+ Returns the entry as it will be created in LDAP.
+
+ :param hostname: The name of the host being added.
+ :param kw: Keyword arguments for the other LDAP attributes.
+ """
+ assert 'cn' not in kw
+ assert 'dn' not in kw
+ assert 'krbprincipalname' not in kw
+ ldap = self.api.Backend.ldap
+
+ kw['cn'] = hostname
+ kw['serverhostname'] = hostname.split('.',1)[0]
+ kw['dn'] = ldap.make_host_dn(hostname)
+
+ # FIXME: do a DNS lookup to ensure host exists
+
+ current = util.get_current_principal()
+ if not current:
+ raise errors.NotFound('Unable to determine current user')
+ kw['enrolledby'] = ldap.find_entry_dn("krbPrincipalName", current, "posixAccount")
+
+ # Get our configuration
+ config = ldap.get_ipa_config()
+
+ # some required objectclasses
+ # FIXME: add this attribute to cn=ipaconfig
+ #kw['objectclass'] = config.get('ipahostobjectclasses')
+ kw['objectclass'] = ['nsHost', 'ipaHost', 'pkiUser']
+
+ # Ensure the list of objectclasses is lower-case
+ kw['objectclass'] = map(lambda z: z.lower(), kw.get('objectclass'))
+
+ if not kw.get('userpassword', False):
+ kw['krbprincipalname'] = "host/%s@%s" % (hostname, self.api.env.realm)
+
+ if 'krbprincipalaux' not in kw.get('objectclass'):
+ kw['objectclass'].append('krbprincipalaux')
+ else:
+ if 'krbprincipalaux' in kw.get('objectclass'):
+ kw['objectclass'].remove('krbprincipalaux')
+
+ return ldap.create(**kw)
+ def output_for_cli(self, textui, result, *args, **options):
+ """
+ Output result of this command to command line interface.
+ """
+ textui.print_plain("Host added")
+
+api.register(host_add)
+
+
+class host_del(crud.Del):
+ 'Delete an existing host.'
+ def execute(self, hostname, **kw):
+ """Delete a host.
+
+ hostname is the name of the host to delete
+
+ :param hostname: The name of the host being removed.
+ :param kw: Not used.
+ """
+ ldap = self.api.Backend.ldap
+ dn = get_host(hostname)
+ return ldap.delete(dn)
+ def output_for_cli(self, textui, result, *args, **options):
+ """
+ Output result of this command to command line interface.
+ """
+ textui.print_plain("Host deleted")
+
+api.register(host_del)
+
+
+class host_mod(crud.Mod):
+ 'Edit an existing host.'
+ def execute(self, hostname, **kw):
+ """
+ Execute the host-mod operation.
+
+ The dn should not be passed as a keyword argument as it is constructed
+ by this method.
+
+ Returns the entry
+
+ :param hostname: The name of the host to retrieve.
+ :param kw: Keyword arguments for the other LDAP attributes.
+ """
+ assert 'cn' not in kw
+ assert 'dn' not in kw
+ ldap = self.api.Backend.ldap
+ dn = get_host(hostname)
+ return ldap.update(dn, **kw)
+
+ def output_for_cli(self, textui, result, *args, **options):
+ """
+ Output result of this command to command line interface.
+ """
+ textui.print_plain("Host updated")
+
+api.register(host_mod)
+
+
+class host_find(crud.Find):
+ 'Search the hosts.'
+
+ takes_options = (
+ Flag('all', doc='Retrieve all attributes'),
+ )
+
+ # FIXME: This should no longer be needed with the Param.query kwarg.
+# def get_args(self):
+# """
+# Override Find.get_args() so we can exclude the validation rules
+# """
+# yield self.obj.primary_key.__clone__(rules=tuple())
+
+ def execute(self, term, **kw):
+ ldap = self.api.Backend.ldap
+
+ # Pull the list of searchable attributes out of the configuration.
+ #config = ldap.get_ipa_config()
+ # FIXME: add this attribute to cn=ipaconfig
+ #search_fields_conf_str = config.get('ipahostsearchfields')
+ #search_fields = search_fields_conf_str.split(",")
+ search_fields = ['cn','serverhostname','description','localityname','nshostlocation','nshardwareplatform','nsosversion']
+
+ search_kw = {}
+ for s in search_fields:
+ search_kw[s] = term
+
+ # Can't use ldap.get_object_type() since cn is also used for group dns
+ search_kw['objectclass'] = "ipaHost"
+ if kw.get('all', False):
+ search_kw['attributes'] = ['*']
+ else:
+ search_kw['attributes'] = default_attributes
+ return ldap.search(**search_kw)
+ def output_for_cli(self, textui, result, *args, **options):
+ counter = result[0]
+ hosts = result[1:]
+ if counter == 0:
+ textui.print_plain("No entries found")
+ return
+
+ for h in hosts:
+ textui.print_entry(h)
+ if counter == -1:
+ textui.print_plain("These results are truncated.")
+ textui.print_plain("Please refine your search and try again.")
+api.register(host_find)
+
+
+class host_show(crud.Get):
+ 'Examine an existing host.'
+ takes_options = (
+ Flag('all', doc='Display all host attributes'),
+ )
+ def execute(self, hostname, **kw):
+ """
+ Execute the host-show operation.
+
+ The dn should not be passed as a keyword argument as it is constructed
+ by this method.
+
+ Returns the entry
+
+ :param hostname: The login name of the host to retrieve.
+ :param kw: "all" set to True = return all attributes
+ """
+ ldap = self.api.Backend.ldap
+ dn = get_host(hostname)
+ # FIXME: should kw contain the list of attributes to display?
+ if kw.get('all', False):
+ return ldap.retrieve(dn)
+ else:
+ value = ldap.retrieve(dn, default_attributes)
+ del value['dn']
+ return value
+ def output_for_cli(self, textui, result, *args, **options):
+ textui.print_entry(result)
+
+api.register(host_show)
diff --git a/ipalib/plugins/f_hostgroup.py b/ipalib/plugins/f_hostgroup.py
new file mode 100644
index 000000000..706712c9a
--- /dev/null
+++ b/ipalib/plugins/f_hostgroup.py
@@ -0,0 +1,354 @@
+# Authors:
+# Rob Crittenden <rcritten@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
+
+"""
+Frontend plugins for groups of hosts
+"""
+
+from ipalib import api, crud, errors
+from ipalib import Object, Command # Plugin base classes
+from ipalib import Str # Parameter types
+
+
+hostgroup_filter = "groupofnames)(!(objectclass=posixGroup)"
+
+def get_members(members):
+ """
+ Return a list of members.
+
+ It is possible that the value passed in is None.
+ """
+ if members:
+ members = members.split(',')
+ else:
+ members = []
+
+ return members
+
+class hostgroup(Object):
+ """
+ Host Group object.
+ """
+ takes_params = (
+ Str('description',
+ doc='A description of this group',
+ ),
+ Str('cn',
+ cli_name='name',
+ primary_key=True,
+ normalizer=lambda value: value.lower(),
+ )
+ )
+api.register(hostgroup)
+
+
+class hostgroup_add(crud.Add):
+ 'Add a new group of hosts.'
+
+ def execute(self, cn, **kw):
+ """
+ Execute the hostgroup-add operation.
+
+ The dn should not be passed as a keyword argument as it is constructed
+ by this method.
+
+ Returns the entry as it will be created in LDAP.
+
+ No need to explicitly set gidNumber. The dna_plugin will do this
+ for us if the value isn't provided by the caller.
+
+ :param cn: The name of the host group being added.
+ :param kw: Keyword arguments for the other LDAP attributes.
+ """
+ assert 'cn' not in kw
+ assert 'dn' not in kw
+ ldap = self.api.Backend.ldap
+ kw['cn'] = cn
+ kw['dn'] = ldap.make_hostgroup_dn(cn)
+
+ # Get our configuration
+ #config = ldap.get_ipa_config()
+
+ # some required objectclasses
+ # FIXME: get this out of config
+ kw['objectClass'] = ['groupofnames']
+
+ return ldap.create(**kw)
+
+ def output_for_cli(self, textui, result, *args, **options):
+ """
+ Output result of this command to command line interface.
+ """
+ textui.print_plain("Group added")
+
+api.register(hostgroup_add)
+
+
+class hostgroup_del(crud.Del):
+ 'Delete an existing group of hosts.'
+ def execute(self, cn, **kw):
+ """
+ Delete a group of hosts
+
+ The memberOf plugin handles removing the group from any other
+ groups.
+
+ :param cn: The name of the group being removed
+ :param kw: Unused
+ """
+ ldap = self.api.Backend.ldap
+ dn = ldap.find_entry_dn("cn", cn, hostgroup_filter)
+
+ return ldap.delete(dn)
+
+ def output_for_cli(self, textui, result, *args, **options):
+ """
+ Output result of this command to command line interface.
+ """
+ textui.print_plain("Group deleted")
+
+api.register(hostgroup_del)
+
+
+class hostgroup_mod(crud.Mod):
+ 'Edit an existing group of hosts.'
+ def execute(self, cn, **kw):
+ """
+ Execute the hostgroup-mod operation.
+
+ The dn should not be passed as a keyword argument as it is constructed
+ by this method.
+
+ Returns the entry
+
+ :param cn: The name of the group to update.
+ :param kw: Keyword arguments for the other LDAP attributes.
+ """
+ assert 'cn' not in kw
+ assert 'dn' not in kw
+ ldap = self.api.Backend.ldap
+ dn = ldap.find_entry_dn("cn", cn, hostgroup_filter)
+ return ldap.update(dn, **kw)
+
+ def output_for_cli(self, textui, result, *args, **options):
+ """
+ Output result of this command to command line interface.
+ """
+ texui.print_plain("Group updated")
+
+api.register(hostgroup_mod)
+
+
+class hostgroup_find(crud.Find):
+ 'Search the groups of hosts.'
+ def execute(self, term, **kw):
+ ldap = self.api.Backend.ldap
+
+ # Pull the list of searchable attributes out of the configuration.
+ config = ldap.get_ipa_config()
+
+ # FIXME: for now use same search fields as user groups
+ search_fields_conf_str = config.get('ipagroupsearchfields')
+ search_fields = search_fields_conf_str.split(",")
+
+ search_kw = {}
+ for s in search_fields:
+ search_kw[s] = term
+
+ search_kw['objectclass'] = hostgroup_filter
+ return ldap.search(**search_kw)
+
+ def output_for_cli(self, textui, result, *args, **options):
+ counter = result[0]
+ groups = result[1:]
+ if counter == 0:
+ textui.print_plain("No entries found")
+ return
+
+ for g in groups:
+ textui.print_entry(g)
+
+ if counter == -1:
+ textui.print_plain("These results are truncated.")
+ textui.print_plain("Please refine your search and try again.")
+
+api.register(hostgroup_find)
+
+
+class hostgroup_show(crud.Get):
+ 'Examine an existing group of hosts.'
+ def execute(self, cn, **kw):
+ """
+ Execute the hostgroup-show operation.
+
+ The dn should not be passed as a keyword argument as it is constructed
+ by this method.
+
+ Returns the entry
+
+ :param cn: The group name to retrieve.
+ :param kw: Not used.
+ """
+ ldap = self.api.Backend.ldap
+ # FIXME: this works for now but the plan is to add a new objectclass
+ # type.
+ dn = ldap.find_entry_dn("cn", cn, hostgroup_filter)
+ # FIXME: should kw contain the list of attributes to display?
+ return ldap.retrieve(dn)
+
+ def output_for_cli(self, textui, result, *args, **options):
+ textui.print_entry(result)
+
+api.register(hostgroup_show)
+
+
+class hostgroup_add_member(Command):
+ 'Add a member to a group.'
+ takes_args = (
+ Str('group', primary_key=True),
+ )
+ takes_options = (
+ Str('groups?', doc='comma-separated list of host groups to add'),
+ Str('hosts?', doc='comma-separated list of hosts to add'),
+ )
+ def execute(self, cn, **kw):
+ """
+ Execute the hostgroup-add-member operation.
+
+ Returns the updated group entry
+
+ :param cn: The group name to add new members to.
+ :param kw: groups is a comma-separated list of host groups to add
+ :param kw: hosts is a comma-separated list of hosts to add
+ """
+ ldap = self.api.Backend.ldap
+ dn = ldap.find_entry_dn("cn", cn, hostgroup_filter)
+ add_failed = []
+ to_add = []
+ completed = 0
+
+ members = get_members(kw.get('groups', ''))
+ for m in members:
+ if not m: continue
+ try:
+ member_dn = ldap.find_entry_dn("cn", m, hostgroup_filter)
+ to_add.append(member_dn)
+ except errors.NotFound:
+ add_failed.append(m)
+ continue
+
+ members = get_members(kw.get('hosts', ''))
+ for m in members:
+ if not m: continue
+ try:
+ member_dn = ldap.find_entry_dn("cn", m, "ipaHost")
+ to_add.append(member_dn)
+ except errors.NotFound:
+ add_failed.append(m)
+ continue
+
+ for member_dn in to_add:
+ try:
+ ldap.add_member_to_group(member_dn, dn)
+ completed+=1
+ except:
+ add_failed.append(member_dn)
+
+ return add_failed
+
+ def output_for_cli(self, textui, result, *args, **options):
+ """
+ Output result of this command to command line interface.
+ """
+ if result:
+ textui.print_plain("These entries failed to add to the group:")
+ for a in result:
+ print "\t'%s'" % a
+ else:
+ textui.print_plain("Group membership updated.")
+
+api.register(hostgroup_add_member)
+
+
+class hostgroup_remove_member(Command):
+ 'Remove a member from a group.'
+ takes_args = (
+ Str('group', primary_key=True),
+ )
+ takes_options = (
+ Str('hosts?', doc='comma-separated list of hosts to add'),
+ Str('groups?', doc='comma-separated list of groups to remove'),
+ )
+ def execute(self, cn, **kw):
+ """
+ Execute the group-remove-member operation.
+
+ Returns the members that could not be added
+
+ :param cn: The group name to add new members to.
+ :param kw: groups is a comma-separated list of groups to remove
+ :param kw: hosts is a comma-separated list of hosts to add
+ """
+ ldap = self.api.Backend.ldap
+ dn = ldap.find_entry_dn("cn", cn, hostgroup_filter)
+ to_remove = []
+ remove_failed = []
+ completed = 0
+
+ members = get_members(kw.get('groups', ''))
+ for m in members:
+ if not m: continue
+ try:
+ member_dn = ldap.find_entry_dn("cn", m, hostgroup_filter)
+ to_remove.append(member_dn)
+ except errors.NotFound:
+ remove_failed.append(m)
+ continue
+
+ members = get_members(kw.get('hosts', ''))
+ for m in members:
+ if not m: continue
+ try:
+ member_dn = ldap.find_entry_dn("cn", m, "ipaHost")
+ to_remove.append(member_dn)
+ except errors.NotFound:
+ remove_failed.append(m)
+ continue
+
+ for member_dn in to_remove:
+ try:
+ ldap.remove_member_from_group(member_dn, dn)
+ completed+=1
+ except:
+ remove_failed.append(member_dn)
+
+ return remove_failed
+
+ def output_for_cli(self, textui, result, *args, **options):
+ """
+ Output result of this command to command line interface.
+ """
+ if result:
+ textui.print_plain("These entries failed to be removed from the group:")
+ for a in result:
+ print "\t'%s'" % a
+ else:
+ textui.print_plain("Group membership updated.")
+
+api.register(hostgroup_remove_member)
diff --git a/ipalib/plugins/f_misc.py b/ipalib/plugins/f_misc.py
new file mode 100644
index 000000000..a2f0fa4e4
--- /dev/null
+++ b/ipalib/plugins/f_misc.py
@@ -0,0 +1,89 @@
+# 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
+
+"""
+Misc frontend plugins.
+"""
+
+import re
+from ipalib import api, LocalOrRemote
+
+
+
+# FIXME: We should not let env return anything in_server
+# when mode == 'production'. This would allow an attacker to see the
+# configuration of the server, potentially revealing compromising
+# information. However, it's damn handy for testing/debugging.
+class env(LocalOrRemote):
+ """Show environment variables"""
+
+ takes_args = ('variables*',)
+
+ def __find_keys(self, variables):
+ keys = set()
+ for query in variables:
+ if '*' in query:
+ pat = re.compile(query.replace('*', '.*') + '$')
+ for key in self.env:
+ if pat.match(key):
+ keys.add(key)
+ elif query in self.env:
+ keys.add(query)
+ return sorted(keys)
+
+ def execute(self, variables, **options):
+ if variables is None:
+ keys = self.env
+ else:
+ keys = self.__find_keys(variables)
+ return tuple(
+ (key, self.env[key]) for key in keys
+ )
+
+ def output_for_cli(self, textui, result, variables, **options):
+ if len(result) == 0:
+ return
+ if len(result) == 1:
+ textui.print_keyval(result)
+ return
+ textui.print_name(self.name)
+ textui.print_keyval(result)
+ textui.print_count(result, '%d variables')
+
+api.register(env)
+
+
+class plugins(LocalOrRemote):
+ """Show all loaded plugins"""
+
+ def execute(self, **options):
+ plugins = sorted(self.api.plugins, key=lambda o: o.plugin)
+ return tuple(
+ (p.plugin, p.bases) for p in plugins
+ )
+
+ def output_for_cli(self, textui, result, **options):
+ textui.print_name(self.name)
+ for (plugin, bases) in result:
+ textui.print_indented(
+ '%s: %s' % (plugin, ', '.join(bases))
+ )
+ textui.print_count(result, '%d plugin loaded', '%s plugins loaded')
+
+api.register(plugins)
diff --git a/ipalib/plugins/f_netgroup.py b/ipalib/plugins/f_netgroup.py
new file mode 100644
index 000000000..6ee55b0db
--- /dev/null
+++ b/ipalib/plugins/f_netgroup.py
@@ -0,0 +1,461 @@
+# Authors:
+# Rob Crittenden <rcritten@redhat.com>
+#
+# Copyright (C) 2009 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
+
+"""
+Frontend plugin for netgroups.
+"""
+
+from ipalib import api, crud, errors
+from ipalib import Object, Command # Plugin base classes
+from ipalib import Str # Parameter types
+from ipalib import uuid
+
+netgroup_base = "cn=ng, cn=alt"
+netgroup_filter = "ipaNISNetgroup"
+hostgroup_filter = "groupofnames)(!(objectclass=posixGroup)"
+
+def get_members(members):
+ """
+ Return a list of members.
+
+ It is possible that the value passed in is None.
+ """
+ if members:
+ members = members.split(',')
+ else:
+ members = []
+
+ return members
+
+def find_members(ldap, failed, members, attribute, filter=None):
+ """
+ Return 2 lists: one a list of DNs found, one a list of errors
+ """
+ found = []
+ for m in members:
+ if not m: continue
+ try:
+ member_dn = ldap.find_entry_dn(attribute, m, filter)
+ found.append(member_dn)
+ except errors.NotFound:
+ failed.append(m)
+ continue
+
+ return found, failed
+
+def add_members(ldap, completed, members, dn, memberattr):
+ add_failed = []
+ for member_dn in members:
+ try:
+ ldap.add_member_to_group(member_dn, dn, memberattr)
+ completed+=1
+ except:
+ add_failed.append(member_dn)
+
+ return completed, add_failed
+
+def add_external(ldap, completed, members, cn):
+ failed = []
+ netgroup = api.Command['netgroup_show'](cn)
+ external = netgroup.get('externalhost', [])
+ if not isinstance(external, list):
+ external = [external]
+ external_len = len(external)
+ for m in members:
+ if not m in external:
+ external.append(m)
+ completed+=1
+ else:
+ failed.append(m)
+ if len(external) > external_len:
+ kw = {'externalhost': external}
+ ldap.update(netgroup['dn'], **kw)
+
+ return completed, failed
+
+def remove_members(ldap, completed, members, dn, memberattr):
+ remove_failed = []
+ for member_dn in members:
+ try:
+ ldap.remove_member_from_group(member_dn, dn, memberattr)
+ completed+=1
+ except:
+ remove_failed.append(member_dn)
+
+ return completed, remove_failed
+
+def remove_external(ldap, completed, members, cn):
+ failed = []
+ netgroup = api.Command['netgroup_show'](cn)
+ external = netgroup.get('externalhost', [])
+ if not isinstance(external, list):
+ external = [external]
+ external_len = len(external)
+ for m in members:
+ try:
+ external.remove(m)
+ completed+=1
+ except ValueError:
+ failed.append(m)
+ if len(external) < external_len:
+ kw = {'externalhost': external}
+ ldap.update(netgroup['dn'], **kw)
+
+ return completed, failed
+
+class netgroup(Object):
+ """
+ netgroups object.
+ """
+ takes_params = (
+ Str('cn',
+ cli_name='name',
+ primary_key=True
+ ),
+ Str('description',
+ doc='Description',
+ ),
+ Str('nisdomainname?',
+ cli_name='domainname',
+ doc='Domain name',
+ ),
+ )
+api.register(netgroup)
+
+
+class netgroup_add(crud.Add):
+ 'Add a new netgroup.'
+
+ def execute(self, cn, **kw):
+ """
+ Execute the netgroup-add operation.
+
+ The dn should not be passed as a keyword argument as it is constructed
+ by this method.
+
+ Returns the entry as it will be created in LDAP.
+
+ :param cn: The name of the netgroup
+ :param kw: Keyword arguments for the other LDAP attributes.
+ """
+ self.log.info("IPA: netgroup-add '%s'" % cn)
+ assert 'cn' not in kw
+ assert 'dn' not in kw
+ ldap = self.api.Backend.ldap
+ kw['cn'] = cn
+# kw['dn'] = ldap.make_netgroup_dn()
+ kw['ipauniqueid'] = str(uuid.uuid1())
+ kw['dn'] = "ipauniqueid=%s,%s,%s" % (kw['ipauniqueid'], netgroup_base, api.env.basedn)
+
+ if not kw.get('nisdomainname', False):
+ kw['nisdomainname'] = api.env.domain
+
+ # some required objectclasses
+ kw['objectClass'] = ['top', 'ipaAssociation', 'ipaNISNetgroup']
+
+ return ldap.create(**kw)
+
+ def output_for_cli(self, textui, result, *args, **options):
+ """
+ Output result of this command to command line interface.
+ """
+ textui.print_name(self.name)
+ textui.print_entry(result)
+ textui.print_dashed('Added netgroup "%s"' % result.get('cn'))
+
+api.register(netgroup_add)
+
+
+class netgroup_del(crud.Del):
+ 'Delete an existing netgroup.'
+
+ def execute(self, cn, **kw):
+ """Delete a netgroup.
+
+ cn is the cn of the netgroup to delete
+
+ The memberOf plugin handles removing the netgroup from any other
+ groups.
+
+ :param cn: The name of the netgroup being removed.
+ :param kw: Not used.
+ """
+ self.log.info("IPA: netgroup-del '%s'" % cn)
+
+ ldap = self.api.Backend.ldap
+ dn = ldap.find_entry_dn("cn", cn, netgroup_filter, netgroup_base)
+ return ldap.delete(dn)
+
+ def output_for_cli(self, textui, result, cn):
+ """
+ Output result of this command to command line interface.
+ """
+ textui.print_plain('Deleted net group "%s"' % cn)
+
+api.register(netgroup_del)
+
+
+class netgroup_mod(crud.Mod):
+ 'Edit an existing netgroup.'
+ def execute(self, cn, **kw):
+ """
+ Execute the netgroup-mod operation.
+
+ The dn should not be passed as a keyword argument as it is constructed
+ by this method.
+
+ Returns the entry
+
+ :param cn: The name of the netgroup to retrieve.
+ :param kw: Keyword arguments for the other LDAP attributes.
+ """
+ self.log.info("IPA: netgroup-mod '%s'" % cn)
+ assert 'cn' not in kw
+ assert 'dn' not in kw
+ ldap = self.api.Backend.ldap
+ dn = ldap.find_entry_dn("cn", cn, netgroup_filter, netgroup_base)
+ return ldap.update(dn, **kw)
+
+ def output_for_cli(self, textui, result, cn, **options):
+ """
+ Output result of this command to command line interface.
+ """
+ textui.print_name(self.name)
+ textui.print_entry(result)
+ textui.print_dashed('Updated netgroup "%s"' % result['cn'])
+
+api.register(netgroup_mod)
+
+
+class netgroup_find(crud.Find):
+ 'Search the netgroups.'
+ def execute(self, term, **kw):
+ ldap = self.api.Backend.ldap
+
+ search_fields = ['ipauniqueid','description','nisdomainname','cn']
+
+ search_kw = {}
+ for s in search_fields:
+ search_kw[s] = term
+
+ search_kw['objectclass'] = netgroup_filter
+ search_kw['base'] = netgroup_base
+ return ldap.search(**search_kw)
+
+ def output_for_cli(self, textui, result, *args, **options):
+ counter = result[0]
+ groups = result[1:]
+ if counter == 0 or len(groups) == 0:
+ textui.print_plain("No entries found")
+ return
+ if len(groups) == 1:
+ textui.print_entry(groups[0])
+ return
+ textui.print_name(self.name)
+ for g in groups:
+ textui.print_entry(g)
+ textui.print_plain('')
+ if counter == -1:
+ textui.print_plain('These results are truncated.')
+ textui.print_plain('Please refine your search and try again.')
+ textui.print_count(groups, '%d netgroups matched')
+
+api.register(netgroup_find)
+
+
+class netgroup_show(crud.Get):
+ 'Examine an existing netgroup.'
+ def execute(self, cn, **kw):
+ """
+ Execute the netgroup-show operation.
+
+ The dn should not be passed as a keyword argument as it is constructed
+ by this method.
+
+ Returns the entry
+
+ :param cn: The name of the netgroup to retrieve.
+ :param kw: Unused
+ """
+ ldap = self.api.Backend.ldap
+ dn = ldap.find_entry_dn("cn", cn, netgroup_filter, netgroup_base)
+ return ldap.retrieve(dn)
+
+ def output_for_cli(self, textui, result, *args, **options):
+ textui.print_entry(result)
+
+api.register(netgroup_show)
+
+class netgroup_add_member(Command):
+ 'Add a member to a group.'
+ takes_args = (
+ Str('cn',
+ cli_name='name',
+ primary_key=True
+ ),
+ )
+ takes_options = (
+ Str('hosts?', doc='comma-separated list of hosts to add'),
+ Str('hostgroups?', doc='comma-separated list of host groups to add'),
+ Str('users?', doc='comma-separated list of users to add'),
+ Str('groups?', doc='comma-separated list of groups to add'),
+ )
+
+ def execute(self, cn, **kw):
+ """
+ Execute the netgroup-add-member operation.
+
+ Returns the updated group entry
+
+ :param cn: The netgroup name to add new members to.
+ :param kw: hosts is a comma-separated list of hosts to add
+ :param kw: hostgroups is a comma-separated list of host groups to add
+ :param kw: users is a comma-separated list of users to add
+ :param kw: groups is a comma-separated list of host to add
+ """
+ ldap = self.api.Backend.ldap
+ dn = ldap.find_entry_dn("cn", cn, netgroup_filter, netgroup_base)
+ add_failed = []
+ to_add = []
+ completed = 0
+
+ # Hosts
+ members = get_members(kw.get('hosts', ''))
+ (to_add, add_failed) = find_members(ldap, add_failed, members, "cn", "ipaHost")
+
+ # If a host is not found we'll consider it an externalHost. It will
+ # be up to the user to handle typos
+ if add_failed:
+ (completed, failed) = add_external(ldap, completed, add_failed, cn)
+ add_failed = failed
+
+ (completed, failed) = add_members(ldap, completed, to_add, dn, 'memberhost')
+ add_failed+=failed
+
+ # Host groups
+ members = get_members(kw.get('hostgroups', ''))
+ (to_add, add_failed) = find_members(ldap, add_failed, members, "cn", hostgroup_filter)
+ (completed, failed) = add_members(ldap, completed, to_add, dn, 'memberhost')
+ add_failed+=failed
+
+ # User
+ members = get_members(kw.get('users', ''))
+ (to_add, add_failed) = find_members(ldap, add_failed, members, "uid")
+ (completed, failed) = add_members(ldap, completed, to_add, dn, 'memberuser')
+ add_failed+=failed
+
+ # Groups
+ members = get_members(kw.get('groups', ''))
+ (to_add, add_failed) = find_members(ldap, add_failed, members, "cn", "posixGroup")
+ (completed, failed) = add_members(ldap, completed, to_add, dn, 'memberuser')
+ add_failed+=failed
+
+ return add_failed
+
+ def output_for_cli(self, textui, result, *args, **options):
+ """
+ Output result of this command to command line interface.
+ """
+ if result:
+ textui.print_plain("These entries failed to add to the group:")
+ for a in result:
+ print "\t'%s'" % a
+ else:
+ textui.print_plain("netgroup membership updated.")
+
+api.register(netgroup_add_member)
+
+
+class netgroup_remove_member(Command):
+ 'Remove a member from a group.'
+ takes_args = (
+ Str('cn',
+ cli_name='name',
+ primary_key=True
+ ),
+ )
+ takes_options = (
+ Str('hosts?', doc='comma-separated list of hosts to remove'),
+ Str('hostgroups?', doc='comma-separated list of groups to remove'),
+ Str('users?', doc='comma-separated list of users to remove'),
+ Str('groups?', doc='comma-separated list of groups to remove'),
+ )
+ def execute(self, cn, **kw):
+ """
+ Execute the group-remove-member operation.
+
+ Returns the members that could not be added
+
+ :param cn: The group name to add new members to.
+ :param kw: hosts is a comma-separated list of hosts to remove
+ :param kw: hostgroups is a comma-separated list of host groups to remove
+ :param kw: users is a comma-separated list of users to remove
+ :param kw: groups is a comma-separated list of host to remove
+ """
+ ldap = self.api.Backend.ldap
+ dn = ldap.find_entry_dn("cn", cn, netgroup_filter, netgroup_base)
+ remove_failed = []
+ to_remove = []
+ completed = 0
+
+ # Hosts
+ members = get_members(kw.get('hosts', ''))
+ (to_remove, remove_failed) = find_members(ldap, remove_failed, members, "cn", "ipaHost")
+
+ # If a host is not found we'll consider it an externalHost. It will
+ # be up to the user to handle typos
+ if remove_failed:
+ (completed, failed) = remove_external(ldap, completed, remove_failed, cn)
+ remove_failed = failed
+
+ (completed, failed) = remove_members(ldap, completed, to_remove, dn, 'memberhost')
+ remove_failed+=failed
+
+ # Host groups
+ members = get_members(kw.get('hostgroups', ''))
+ (to_remove, remove_failed) = find_members(ldap, remove_failed, members, "cn", hostgroup_filter)
+ (completed, failed) = remove_members(ldap, completed, to_remove, dn, 'memberhost')
+ remove_failed+=failed
+
+ # User
+ members = get_members(kw.get('users', ''))
+ (to_remove, remove_failed) = find_members(ldap, remove_failed, members, "uid")
+ (completed, failed) = remove_members(ldap, completed, to_remove, dn, 'memberuser')
+ remove_failed+=failed
+
+ # Groups
+ members = get_members(kw.get('groups', ''))
+ (to_remove, remove_failed) = find_members(ldap, remove_failed, members, "cn", "posixGroup")
+ (completed, failed) = remove_members(ldap, completed, to_remove, dn, 'memberuser')
+ remove_failed+=failed
+
+ return remove_failed
+
+ def output_for_cli(self, textui, result, *args, **options):
+ """
+ Output result of this command to command line interface.
+ """
+ if result:
+ textui.print_plain("These entries failed to be removed from the group:")
+ for a in result:
+ print "\t'%s'" % a
+ else:
+ textui.print_plain("netgroup membership updated.")
+
+api.register(netgroup_remove_member)
diff --git a/ipalib/plugins/f_passwd.py b/ipalib/plugins/f_passwd.py
new file mode 100644
index 000000000..ea78c4c15
--- /dev/null
+++ b/ipalib/plugins/f_passwd.py
@@ -0,0 +1,70 @@
+# Authors:
+# Rob Crittenden <rcritten@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
+
+"""
+Frontend plugins for password changes.
+"""
+
+from ipalib import api, errors, util
+from ipalib import Command # Plugin base classes
+from ipalib import Str, Password # Parameter types
+
+
+class passwd(Command):
+ 'Edit existing password policy.'
+
+ takes_args = (
+ Str('principal',
+ cli_name='user',
+ primary_key=True,
+ default_from=util.get_current_principal,
+ ),
+ Password('password'),
+ )
+
+ def execute(self, principal, password):
+ """
+ Execute the passwd operation.
+
+ The dn should not be passed as a keyword argument as it is constructed
+ by this method.
+
+ Returns the entry
+
+ :param param uid: The login name of the user being updated.
+ :param kw: Not used.
+ """
+ if principal.find('@') > 0:
+ u = principal.split('@')
+ if len(u) > 2:
+ raise errors.InvalidUserPrincipal, principal
+ else:
+ principal = principal+"@"+self.api.env.realm
+ dn = self.Backend.ldap.find_entry_dn(
+ "krbprincipalname",
+ principal,
+ "posixAccount"
+ )
+ return self.Backend.ldap.modify_password(dn, newpass=password)
+
+ def output_for_cli(self, textui, result, principal, password):
+ assert password is None
+ textui.print_plain('Changed password for "%s"' % principal)
+
+api.register(passwd)
diff --git a/ipalib/plugins/f_pwpolicy.py b/ipalib/plugins/f_pwpolicy.py
new file mode 100644
index 000000000..d914ce72a
--- /dev/null
+++ b/ipalib/plugins/f_pwpolicy.py
@@ -0,0 +1,122 @@
+# Authors:
+# Rob Crittenden <rcritten@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
+
+"""
+Frontend plugins for password policy.
+"""
+
+from ipalib import api
+from ipalib import Command # Plugin base classes
+from ipalib import Int # Parameter types
+
+
+class pwpolicy_mod(Command):
+ 'Edit existing password policy.'
+ takes_options = (
+ Int('krbmaxpwdlife?',
+ cli_name='maxlife',
+ doc='Max. Password Lifetime (days)'
+ ),
+ Int('krbminpwdlife?',
+ cli_name='minlife',
+ doc='Min. Password Lifetime (hours)'
+ ),
+ Int('krbpwdhistorylength?',
+ cli_name='history',
+ doc='Password History Size'
+ ),
+ Int('krbpwdmindiffchars?',
+ cli_name='minclasses',
+ doc='Min. Number of Character Classes'
+ ),
+ Int('krbpwdminlength?',
+ cli_name='minlength',
+ doc='Min. Length of Password'
+ ),
+ )
+ def execute(self, *args, **kw):
+ """
+ Execute the pwpolicy-mod operation.
+
+ The dn should not be passed as a keyword argument as it is constructed
+ by this method.
+
+ Returns the entry
+
+ :param args: This function takes no positional arguments
+ :param kw: Keyword arguments for the other LDAP attributes.
+ """
+ assert 'dn' not in kw
+ ldap = self.api.Backend.ldap
+ dn = ldap.find_entry_dn("cn", "accounts", "krbPwdPolicy")
+
+ # The LDAP routines want strings, not ints, so convert a few
+ # things. Otherwise it sees a string -> int conversion as a change.
+ for k in kw.iterkeys():
+ if k.startswith("krb", 0, 3):
+ kw[k] = str(kw[k])
+
+ # Convert hours and days to seconds
+ if kw.get('krbmaxpwdlife'):
+ kw['krbmaxpwdlife'] = str(int(kw.get('krbmaxpwdlife')) * 86400)
+ if kw.get('krbminpwdlife'):
+ kw['krbminpwdlife'] = str(int(kw.get('krbminpwdlife')) * 3600)
+
+ return ldap.update(dn, **kw)
+
+ def output_for_cli(self, textui, result, *args, **options):
+ textui.print_plain("Policy modified")
+
+api.register(pwpolicy_mod)
+
+
+class pwpolicy_show(Command):
+ 'Retrieve current password policy'
+ def execute(self, *args, **kw):
+ """
+ Execute the pwpolicy-show operation.
+
+ The dn should not be passed as a keyword argument as it is constructed
+ by this method.
+
+ Returns the entry
+
+ :param args: Not used.
+ :param kw: Not used.
+ """
+ ldap = self.api.Backend.ldap
+ dn = ldap.find_entry_dn("cn", "accounts", "krbPwdPolicy")
+
+ policy = ldap.retrieve(dn)
+
+ # convert some values for display purposes
+ policy['krbmaxpwdlife'] = str(int(policy.get('krbmaxpwdlife')) / 86400)
+ policy['krbminpwdlife'] = str(int(policy.get('krbminpwdlife')) / 3600)
+
+ return policy
+
+ def output_for_cli(self, textui, result, *args, **options):
+ textui.print_plain("Password Policy")
+ textui.print_plain("Min. Password Lifetime (hours): %s" % result.get('krbminpwdlife'))
+ textui.print_plain("Max. Password Lifetime (days): %s" % result.get('krbmaxpwdlife'))
+ textui.print_plain("Min. Number of Character Classes: %s" % result.get('krbpwdmindiffchars'))
+ textui.print_plain("Min. Length of Password: %s" % result.get('krbpwdminlength'))
+ textui.print_plain("Password History Size: %s" % result.get('krbpwdhistorylength'))
+
+api.register(pwpolicy_show)
diff --git a/ipalib/plugins/f_ra.py b/ipalib/plugins/f_ra.py
new file mode 100644
index 000000000..7ac84e65f
--- /dev/null
+++ b/ipalib/plugins/f_ra.py
@@ -0,0 +1,117 @@
+# Authors:
+# Andrew Wnuk <awnuk@redhat.com>
+# Jason Gerard DeRose <jderose@redhat.com>
+#
+# Copyright (C) 2009 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
+
+"""
+Frontend plugins for IPA-RA PKI operations.
+"""
+
+from ipalib import api, Command, Str, Int
+
+
+class request_certificate(Command):
+ """ Submit a certificate request. """
+
+ takes_args = ['csr']
+
+ takes_options = [Str('request_type?', default=u'pkcs10')]
+
+ def execute(self, csr, **options):
+ return self.Backend.ra.request_certificate(csr, **options)
+
+ def output_for_cli(self, textui, result, *args, **options):
+ if isinstance(result, dict) and len(result) > 0:
+ textui.print_entry(result, 0)
+ else:
+ textui.print_plain('Failed to submit a certificate request.')
+
+api.register(request_certificate)
+
+
+class get_certificate(Command):
+ """ Retrieve an existing certificate. """
+
+ takes_args = ['serial_number']
+
+ def execute(self, serial_number, **options):
+ return self.Backend.ra.get_certificate(serial_number)
+
+ def output_for_cli(self, textui, result, *args, **options):
+ if isinstance(result, dict) and len(result) > 0:
+ textui.print_entry(result, 0)
+ else:
+ textui.print_plain('Failed to obtain a certificate.')
+
+api.register(get_certificate)
+
+
+class check_request_status(Command):
+ """ Check a request status. """
+
+ takes_args = ['request_id']
+
+
+ def execute(self, request_id, **options):
+ return self.Backend.ra.check_request_status(request_id)
+
+ def output_for_cli(self, textui, result, *args, **options):
+ if isinstance(result, dict) and len(result) > 0:
+ textui.print_entry(result, 0)
+ else:
+ textui.print_plain('Failed to retrieve a request status.')
+
+api.register(check_request_status)
+
+
+class revoke_certificate(Command):
+ """ Revoke a certificate. """
+
+ takes_args = ['serial_number']
+
+ # FIXME: The default is 0. Is this really an Int param?
+ takes_options = [Int('revocation_reason?', default=0)]
+
+
+ def execute(self, serial_number, **options):
+ return self.Backend.ra.revoke_certificate(serial_number, **options)
+
+ def output_for_cli(self, textui, result, *args, **options):
+ if isinstance(result, dict) and len(result) > 0:
+ textui.print_entry(result, 0)
+ else:
+ textui.print_plain('Failed to revoke a certificate.')
+
+api.register(revoke_certificate)
+
+
+class take_certificate_off_hold(Command):
+ """ Take a revoked certificate off hold. """
+
+ takes_args = ['serial_number']
+
+ def execute(self, serial_number, **options):
+ return self.Backend.ra.take_certificate_off_hold(serial_number)
+
+ def output_for_cli(self, textui, result, *args, **options):
+ if isinstance(result, dict) and len(result) > 0:
+ textui.print_entry(result, 0)
+ else:
+ textui.print_plain('Failed to take a revoked certificate off hold.')
+
+api.register(take_certificate_off_hold)
diff --git a/ipalib/plugins/f_service.py b/ipalib/plugins/f_service.py
new file mode 100644
index 000000000..06d6a5d08
--- /dev/null
+++ b/ipalib/plugins/f_service.py
@@ -0,0 +1,204 @@
+# Authors:
+# Jason Gerard DeRose <jderose@redhat.com>
+# Rob Crittenden <rcritten@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
+
+"""
+Frontend plugins for service (Identity).
+"""
+
+from ipalib import api, crud, errors
+from ipalib import Object # Plugin base classes
+from ipalib import Str, Flag # Parameter types
+
+
+class service(Object):
+ """
+ Service object.
+ """
+ takes_params = (
+ Str('principal', primary_key=True),
+ )
+api.register(service)
+
+
+class service_add(crud.Add):
+ """
+ Add a new service.
+ """
+
+ takes_options = (
+ Flag('force',
+ doc='Force a service principal name',
+ ),
+ )
+ def execute(self, principal, **kw):
+ """
+ Execute the service-add operation.
+
+ The dn should not be passed as a keyword argument as it is constructed
+ by this method.
+
+ Returns the entry as it will be created in LDAP.
+
+ :param principal: The service to be added in the form: service/hostname
+ :param kw: Keyword arguments for the other LDAP attributes.
+ """
+ assert 'krbprincipalname' not in kw
+ ldap = self.api.Backend.ldap
+
+ force = kw.get('force', False)
+ try:
+ del kw['force']
+ except:
+ pass
+
+ # Break down the principal into its component parts, which may or
+ # may not include the realm.
+ sp = principal.split('/')
+ if len(sp) != 2:
+ raise errors.MalformedServicePrincipal
+ service = sp[0]
+
+ if service.lower() == "host":
+ raise errors.HostService
+
+ sr = sp[1].split('@')
+ if len(sr) == 1:
+ hostname = sr[0].lower()
+ realm = self.api.env.realm
+ elif len(sr) == 2:
+ hostname = sr[0].lower()
+ realm = sr[1]
+ else:
+ raise MalformedServicePrincipal
+
+ """
+ FIXME once DNS client is done
+ if not force:
+ fqdn = hostname + "."
+ rs = dnsclient.query(fqdn, dnsclient.DNS_C_IN, dnsclient.DNS_T_A)
+ if len(rs) == 0:
+ self.log.debug("IPA: DNS A record lookup failed for '%s'" % hostname)
+ raise ipaerror.gen_exception(ipaerror.INPUT_NOT_DNS_A_RECORD)
+ else:
+ self.log.debug("IPA: found %d records for '%s'" % (len(rs), hostname))
+ """
+
+ # At some point we'll support multiple realms
+ if (realm != self.api.env.realm):
+ raise errors.RealmMismatch
+
+ # Put the principal back together again
+ princ_name = service + "/" + hostname + "@" + realm
+
+ dn = ldap.make_service_dn(princ_name)
+
+ kw['dn'] = dn
+ kw['objectClass'] = ['krbPrincipal', 'krbPrincipalAux', 'krbTicketPolicyAux']
+
+ return ldap.create(**kw)
+
+ def output_to_cli(self, ret):
+ if ret:
+ print "Service added"
+
+api.register(service_add)
+
+
+class service_del(crud.Del):
+ 'Delete an existing service.'
+ def execute(self, principal, **kw):
+ """
+ Delete a service principal.
+
+ principal is the krbprincipalname of the entry to delete.
+
+ This should be called with much care.
+
+ :param principal: The service to be added in the form: service/hostname
+ :param kw: not used
+ """
+ ldap = self.api.Backend.ldap
+ dn = ldap.find_entry_dn("krbprincipalname", principal)
+ return ldap.delete(dn)
+
+ def output_to_cli(self, ret):
+ if ret:
+ print "Service removed"
+
+api.register(service_del)
+
+# There is no service-mod. The principal itself contains nothing that
+# is user-changeable
+
+class service_find(crud.Find):
+ 'Search the existing services.'
+ def execute(self, principal, **kw):
+ ldap = self.api.Backend.ldap
+
+ search_kw = {}
+ search_kw['filter'] = "&(objectclass=krbPrincipalAux)(!(objectClass=posixAccount))(!(|(krbprincipalname=kadmin/*)(krbprincipalname=K/M@*)(krbprincipalname=krbtgt/*)))"
+ search_kw['krbprincipalname'] = principal
+
+ object_type = ldap.get_object_type("krbprincipalname")
+ if object_type and not kw.get('objectclass'):
+ search_kw['objectclass'] = object_type
+
+ return ldap.search(**search_kw)
+
+ def output_for_cli(self, textui, result, *args, **options):
+ counter = result[0]
+ services = result[1:]
+ if counter == 0:
+ textui.print_plain("No entries found")
+ return
+
+ for s in services:
+ textui.print_entry(s)
+
+ if counter == -1:
+ textui.print_plain("These results are truncated.")
+ textui.print_plain("Please refine your search and try again.")
+ textui.print_count(services, '%d services matched')
+
+api.register(service_find)
+
+
+class service_show(crud.Get):
+ 'Examine an existing service.'
+ def execute(self, principal, **kw):
+ """
+ Execute the service-show operation.
+
+ The dn should not be passed as a keyword argument as it is constructed
+ by this method.
+
+ Returns the entry
+
+ :param principal: The service principal to retrieve
+ :param kw: Not used.
+ """
+ ldap = self.api.Backend.ldap
+ dn = ldap.find_entry_dn("krbprincipalname", principal)
+ # FIXME: should kw contain the list of attributes to display?
+ return ldap.retrieve(dn)
+ def output_for_cli(self, textui, result, *args, **options):
+ textui.print_entry(result)
+
+api.register(service_show)
diff --git a/ipalib/plugins/f_user.py b/ipalib/plugins/f_user.py
new file mode 100644
index 000000000..506ad14d0
--- /dev/null
+++ b/ipalib/plugins/f_user.py
@@ -0,0 +1,367 @@
+# 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
+
+"""
+Frontend plugins for user (Identity).
+"""
+
+from ipalib import api, crud, errors
+from ipalib import Object, Command # Plugin base classes
+from ipalib import Str, Password, Flag, Int # Parameter types
+
+
+def display_user(user):
+ # FIXME: for now delete dn here. In the future pass in the kw to
+ # output_for_cli()
+ attr = sorted(user.keys())
+ # Always have sn following givenname
+ try:
+ l = attr.index('givenname')
+ attr.remove('sn')
+ attr.insert(l+1, 'sn')
+ except ValueError:
+ pass
+
+ for a in attr:
+ if a != 'dn':
+ print "%s: %s" % (a, user[a])
+
+default_attributes = ['uid','givenname','sn','homeDirectory','loginshell']
+
+
+class user(Object):
+ """
+ User object.
+ """
+
+ takes_params = (
+ Str('givenname',
+ cli_name='first',
+ doc="User's first name",
+ ),
+ Str('sn',
+ cli_name='last',
+ doc="User's last name",
+ ),
+ Str('uid',
+ cli_name='user',
+ primary_key=True,
+ default_from=lambda givenname, sn: givenname[0] + sn,
+ normalizer=lambda value: value.lower(),
+ ),
+ Str('gecos?',
+ doc='GECOS field',
+ default_from=lambda uid: uid,
+ ),
+ Str('homedirectory?',
+ cli_name='home',
+ doc="User's home directory",
+ default_from=lambda uid: '/home/%s' % uid,
+ ),
+ Str('loginshell?',
+ cli_name='shell',
+ default=u'/bin/sh',
+ doc="User's Login shell",
+ ),
+ Str('krbprincipalname?',
+ cli_name='principal',
+ doc="User's Kerberos Principal name",
+ default_from=lambda uid: '%s@%s' % (uid, api.env.realm),
+ ),
+ Str('mailaddress?',
+ cli_name='email',
+ doc="User's e-mail address",
+ ),
+ Password('userpassword?',
+ cli_name='password',
+ doc="Set user's password",
+ ),
+ Str('groups?',
+ doc='Add account to one or more groups (comma-separated)',
+ ),
+ Int('uidnumber?',
+ cli_name='uid',
+ doc='The uid to use for this user. If not included one is automatically set.',
+ ),
+ )
+
+api.register(user)
+
+
+class user_add(crud.Add):
+ 'Add a new user.'
+
+ def execute(self, uid, **kw):
+ """
+ Execute the user-add operation.
+
+ The dn should not be passed as a keyword argument as it is constructed
+ by this method.
+
+ Returns the entry as it will be created in LDAP.
+
+ :param uid: The login name of the user being added.
+ :param kw: Keyword arguments for the other LDAP attributes.
+ """
+ assert 'uid' not in kw
+ assert 'dn' not in kw
+ ldap = self.api.Backend.ldap
+ kw['uid'] = uid
+ kw['dn'] = ldap.make_user_dn(uid)
+
+ # FIXME: enforce this elsewhere
+# if servercore.uid_too_long(kw['uid']):
+# raise errors.UsernameTooLong
+
+ # Get our configuration
+ config = ldap.get_ipa_config()
+
+ # Let us add in some missing attributes
+ if kw.get('homedirectory') is None:
+ kw['homedirectory'] = '%s/%s' % (config.get('ipahomesrootdir'), kw.get('uid'))
+ kw['homedirectory'] = kw['homedirectory'].replace('//', '/')
+ kw['homedirectory'] = kw['homedirectory'].rstrip('/')
+ if kw.get('loginshell') is None:
+ kw['loginshell'] = config.get('ipadefaultloginshell')
+ if kw.get('gecos') is None:
+ kw['gecos'] = kw['uid']
+
+ # If uidnumber is blank the the FDS dna_plugin will automatically
+ # assign the next value. So we don't have to do anything with it.
+
+ if not kw.get('gidnumber'):
+ try:
+ group_dn = ldap.find_entry_dn("cn", config.get('ipadefaultprimarygroup'))
+ default_group = ldap.retrieve(group_dn, ['dn','gidNumber'])
+ if default_group:
+ kw['gidnumber'] = default_group.get('gidnumber')
+ except errors.NotFound:
+ # Fake an LDAP error so we can return something useful to the kw
+ raise errors.NotFound, "The default group for new kws, '%s', cannot be found." % config.get('ipadefaultprimarygroup')
+ except Exception, e:
+ # catch everything else
+ raise e
+
+ if kw.get('krbprincipalname') is None:
+ kw['krbprincipalname'] = "%s@%s" % (kw.get('uid'), self.api.env.realm)
+
+ # FIXME. This is a hack so we can request separate First and Last
+ # name in the GUI.
+ if kw.get('cn') is None:
+ kw['cn'] = "%s %s" % (kw.get('givenname'),
+ kw.get('sn'))
+
+ # some required objectclasses
+ kw['objectClass'] = config.get('ipauserobjectclasses')
+
+ return ldap.create(**kw)
+
+ def output_for_cli(self, textui, result, *args, **options):
+ """
+ Output result of this command to command line interface.
+ """
+ textui.print_name(self.name)
+ textui.print_entry(result)
+ textui.print_dashed('Added user "%s"' % result['uid'])
+
+api.register(user_add)
+
+
+class user_del(crud.Del):
+ 'Delete an existing user.'
+
+ def execute(self, uid, **kw):
+ """Delete a user. Not to be confused with inactivate_user. This
+ makes the entry go away completely.
+
+ uid is the uid of the user to delete
+
+ The memberOf plugin handles removing the user from any other
+ groups.
+
+ :param uid: The login name of the user being added.
+ :param kw: Not used.
+ """
+ if uid == "admin":
+ # FIXME: do we still want a "special" user?
+ raise SyntaxError("admin required")
+# raise ipaerror.gen_exception(ipaerror.INPUT_ADMIN_REQUIRED)
+ self.log.info("IPA: user-del '%s'" % uid)
+
+ ldap = self.api.Backend.ldap
+ dn = ldap.find_entry_dn("uid", uid)
+ return ldap.delete(dn)
+
+ def output_for_cli(self, textui, result, uid):
+ """
+ Output result of this command to command line interface.
+ """
+ textui.print_plain('Deleted user "%s"' % uid)
+
+api.register(user_del)
+
+
+class user_mod(crud.Mod):
+ 'Edit an existing user.'
+ def execute(self, uid, **kw):
+ """
+ Execute the user-mod operation.
+
+ The dn should not be passed as a keyword argument as it is constructed
+ by this method.
+
+ Returns the entry
+
+ :param uid: The login name of the user to retrieve.
+ :param kw: Keyword arguments for the other LDAP attributes.
+ """
+ assert 'uid' not in kw
+ assert 'dn' not in kw
+ ldap = self.api.Backend.ldap
+ dn = ldap.find_entry_dn("uid", uid)
+ return ldap.update(dn, **kw)
+
+ def output_for_cli(self, textui, result, uid, **options):
+ """
+ Output result of this command to command line interface.
+ """
+ textui.print_name(self.name)
+ textui.print_entry(result)
+ textui.print_dashed('Updated user "%s"' % result['uid'])
+
+api.register(user_mod)
+
+
+class user_find(crud.Find):
+ 'Search the users.'
+ takes_options = (
+ Flag('all', doc='Retrieve all user attributes'),
+ )
+ def execute(self, term, **kw):
+ ldap = self.api.Backend.ldap
+
+ # Pull the list of searchable attributes out of the configuration.
+ config = ldap.get_ipa_config()
+ search_fields_conf_str = config.get('ipausersearchfields')
+ search_fields = search_fields_conf_str.split(",")
+
+ search_kw = {}
+ for s in search_fields:
+ search_kw[s] = term
+
+ object_type = ldap.get_object_type("uid")
+ if object_type and not kw.get('objectclass'):
+ search_kw['objectclass'] = object_type
+ if kw.get('all', False):
+ search_kw['attributes'] = ['*']
+ else:
+ search_kw['attributes'] = default_attributes
+ return ldap.search(**search_kw)
+
+ def output_for_cli(self, textui, result, uid, **options):
+ counter = result[0]
+ users = result[1:]
+ if counter == 0 or len(users) == 0:
+ textui.print_plain("No entries found")
+ return
+ if len(users) == 1:
+ textui.print_entry(users[0])
+ return
+ textui.print_name(self.name)
+ for u in users:
+ gn = u.get('givenname', '')
+ sn= u.get('sn', '')
+ textui.print_plain('%s %s:' % (gn, sn))
+ textui.print_entry(u)
+ textui.print_plain('')
+ if counter == -1:
+ textui.print_plain('These results are truncated.')
+ textui.print_plain('Please refine your search and try again.')
+ textui.print_count(users, '%d users matched')
+
+api.register(user_find)
+
+
+class user_show(crud.Get):
+ 'Examine an existing user.'
+ takes_options = (
+ Flag('all', doc='Retrieve all user attributes'),
+ )
+ def execute(self, uid, **kw):
+ """
+ Execute the user-show operation.
+
+ The dn should not be passed as a keyword argument as it is constructed
+ by this method.
+
+ Returns the entry
+
+ :param uid: The login name of the user to retrieve.
+ :param kw: "all" set to True = return all attributes
+ """
+ ldap = self.api.Backend.ldap
+ dn = ldap.find_entry_dn("uid", uid)
+ # FIXME: should kw contain the list of attributes to display?
+ if kw.get('all', False):
+ return ldap.retrieve(dn)
+ else:
+ return ldap.retrieve(dn, default_attributes)
+
+ def output_for_cli(self, textui, result, uid, **options):
+ if result:
+ display_user(result)
+
+api.register(user_show)
+
+class user_lock(Command):
+ 'Lock a user account.'
+
+ takes_args = (
+ Str('uid', primary_key=True),
+ )
+
+ def execute(self, uid, **kw):
+ ldap = self.api.Backend.ldap
+ dn = ldap.find_entry_dn("uid", uid)
+ return ldap.mark_entry_inactive(dn)
+
+ def output_for_cli(self, textui, result, uid):
+ if result:
+ textui.print_plain('Locked user "%s"' % uid)
+
+api.register(user_lock)
+
+
+class user_unlock(Command):
+ 'Unlock a user account.'
+
+ takes_args = (
+ Str('uid', primary_key=True),
+ )
+
+ def execute(self, uid, **kw):
+ ldap = self.api.Backend.ldap
+ dn = ldap.find_entry_dn("uid", uid)
+ return ldap.mark_entry_active(dn)
+
+ def output_for_cli(self, textui, result, uid):
+ if result:
+ textui.print_plain('Unlocked user "%s"' % uid)
+
+api.register(user_unlock)
diff --git a/ipalib/request.py b/ipalib/request.py
new file mode 100644
index 000000000..6ad7ad35f
--- /dev/null
+++ b/ipalib/request.py
@@ -0,0 +1,71 @@
+# Authors:
+# Rob Crittenden <rcritten@redhat.com>
+# Jason Gerard DeRose <jderose@redhat.com>
+#
+# Copyright (C) 2008 Red Hat
+# see file 'COPYING' for use and warranty contextrmation
+#
+# 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
+
+"""
+Per-request thread-local data.
+"""
+
+import threading
+import locale
+import gettext
+from constants import OVERRIDE_ERROR
+
+
+# Thread-local storage of most per-request information
+context = threading.local()
+
+
+def ugettext(message):
+ if hasattr(context, 'ugettext'):
+ return context.ugettext(message)
+ return message.decode('UTF-8')
+
+
+def ungettext(singular, plural, n):
+ if hasattr(context, 'ungettext'):
+ return context.ungettext(singular, plural, n)
+ if n == 1:
+ return singular.decode('UTF-8')
+ return plural.decode('UTF-8')
+
+
+def set_languages(*languages):
+ if hasattr(context, 'languages'):
+ raise StandardError(OVERRIDE_ERROR %
+ ('context', 'languages', context.languages, languages)
+ )
+ if len(languages) == 0:
+ languages = locale.getdefaultlocale()[:1]
+ context.languages = languages
+ assert type(context.languages) is tuple
+
+
+def create_translation(domain, localedir, *languages):
+ if hasattr(context, 'ugettext') or hasattr(context, 'ungettext'):
+ raise StandardError(
+ 'create_translation() already called in thread %r' %
+ threading.currentThread().getName()
+ )
+ set_languages(*languages)
+ translation = gettext.translation(domain,
+ localedir=localedir, languages=context.languages, fallback=True
+ )
+ context.ugettext = translation.ugettext
+ context.ungettext = translation.ungettext
diff --git a/ipalib/rpc.py b/ipalib/rpc.py
new file mode 100644
index 000000000..e7823ef95
--- /dev/null
+++ b/ipalib/rpc.py
@@ -0,0 +1,207 @@
+# 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
+
+"""
+RPC client and shared RPC client/server functionality.
+
+This module adds some additional functionality on top of the ``xmlrpclib``
+module in the Python standard library. For documentation on the
+``xmlrpclib`` module, see:
+
+ http://docs.python.org/library/xmlrpclib.html
+
+Also see the `ipaserver.rpcserver` module.
+"""
+
+from types import NoneType
+import threading
+from xmlrpclib import Binary, Fault, dumps, loads
+from ipalib.backend import Backend
+from ipalib.errors2 import public_errors, PublicError, UnknownError
+from ipalib.request import context
+
+
+def xml_wrap(value):
+ """
+ Wrap all ``str`` in ``xmlrpclib.Binary``.
+
+ Because ``xmlrpclib.dumps()`` will itself convert all ``unicode`` instances
+ into UTF-8 encoded ``str`` instances, we don't do it here.
+
+ So in total, when encoding data for an XML-RPC packet, the following
+ transformations occur:
+
+ * All ``str`` instances are treated as binary data and are wrapped in
+ an ``xmlrpclib.Binary()`` instance.
+
+ * Only ``unicode`` instances are treated as character data. They get
+ converted to UTF-8 encoded ``str`` instances (although as mentioned,
+ not by this function).
+
+ Also see `xml_unwrap()`.
+
+ :param value: The simple scalar or simple compound value to wrap.
+ """
+ if type(value) in (list, tuple):
+ return tuple(xml_wrap(v) for v in value)
+ if type(value) is dict:
+ return dict(
+ (k, xml_wrap(v)) for (k, v) in value.iteritems()
+ )
+ if type(value) is str:
+ return Binary(value)
+ assert type(value) in (unicode, int, float, bool, NoneType)
+ return value
+
+
+def xml_unwrap(value, encoding='UTF-8'):
+ """
+ Unwrap all ``xmlrpc.Binary``, decode all ``str`` into ``unicode``.
+
+ When decoding data from an XML-RPC packet, the following transformations
+ occur:
+
+ * The binary payloads of all ``xmlrpclib.Binary`` instances are
+ returned as ``str`` instances.
+
+ * All ``str`` instances are treated as UTF-8 encoded Unicode strings.
+ They are decoded and the resulting ``unicode`` instance is returned.
+
+ Also see `xml_wrap()`.
+
+ :param value: The value to unwrap.
+ :param encoding: The Unicode encoding to use (defaults to ``'UTF-8'``).
+ """
+ if type(value) in (list, tuple):
+ return tuple(xml_unwrap(v, encoding) for v in value)
+ if type(value) is dict:
+ return dict(
+ (k, xml_unwrap(v, encoding)) for (k, v) in value.iteritems()
+ )
+ if type(value) is str:
+ return value.decode(encoding)
+ if isinstance(value, Binary):
+ assert type(value.data) is str
+ return value.data
+ assert type(value) in (unicode, int, float, bool, NoneType)
+ return value
+
+
+def xml_dumps(params, methodname=None, methodresponse=False, encoding='UTF-8'):
+ """
+ Encode an XML-RPC data packet, transparently wraping ``params``.
+
+ This function will wrap ``params`` using `xml_wrap()` and will
+ then encode the XML-RPC data packet using ``xmlrpclib.dumps()`` (from the
+ Python standard library).
+
+ For documentation on the ``xmlrpclib.dumps()`` function, see:
+
+ http://docs.python.org/library/xmlrpclib.html#convenience-functions
+
+ Also see `xml_loads()`.
+
+ :param params: A ``tuple`` or an ``xmlrpclib.Fault`` instance.
+ :param methodname: The name of the method to call if this is a request.
+ :param methodresponse: Set this to ``True`` if this is a response.
+ :param encoding: The Unicode encoding to use (defaults to ``'UTF-8'``).
+ """
+ if type(params) is tuple:
+ params = xml_wrap(params)
+ else:
+ assert isinstance(params, Fault)
+ return dumps(params,
+ methodname=methodname,
+ methodresponse=methodresponse,
+ encoding=encoding,
+ allow_none=True,
+ )
+
+
+def xml_loads(data):
+ """
+ Decode the XML-RPC packet in ``data``, transparently unwrapping its params.
+
+ This function will decode the XML-RPC packet in ``data`` using
+ ``xmlrpclib.loads()`` (from the Python standard library). If ``data``
+ contains a fault, ``xmlrpclib.loads()`` will itself raise an
+ ``xmlrpclib.Fault`` exception.
+
+ Assuming an exception is not raised, this function will then unwrap the
+ params in ``data`` using `xml_unwrap()`. Finally, a
+ ``(params, methodname)`` tuple is returned containing the unwrapped params
+ and the name of the method being called. If the packet contains no method
+ name, ``methodname`` will be ``None``.
+
+ For documentation on the ``xmlrpclib.loads()`` function, see:
+
+ http://docs.python.org/library/xmlrpclib.html#convenience-functions
+
+ Also see `xml_dumps()`.
+
+ :param data: The XML-RPC packet to decode.
+ """
+ (params, method) = loads(data)
+ return (xml_unwrap(params), method)
+
+
+class xmlclient(Backend):
+ """
+ Forwarding backend for XML-RPC client.
+ """
+
+ def __init__(self):
+ super(xmlclient, self).__init__()
+ self.__errors = dict((e.errno, e) for e in public_errors)
+
+ def forward(self, name, *args, **kw):
+ """
+ Forward call to command named ``name`` over XML-RPC.
+
+ This method will encode and forward an XML-RPC request, and will then
+ decode and return the corresponding XML-RPC response.
+
+ :param command: The name of the command being forwarded.
+ :param args: Positional arguments to pass to remote command.
+ :param kw: Keyword arguments to pass to remote command.
+ """
+ if name not in self.Command:
+ raise ValueError(
+ '%s.forward(): %r not in api.Command' % (self.name, name)
+ )
+ if not hasattr(context, 'xmlconn'):
+ raise StandardError(
+ '%s.forward(%r): need context.xmlconn in thread %r' % (
+ self.name, name, threading.currentThread().getName()
+ )
+ )
+ command = getattr(context.xmlconn, name)
+ params = args + (kw,)
+ try:
+ response = command(xml_wrap(params))
+ return xml_unwrap(response)
+ except Fault, e:
+ if e.faultCode in self.__errors:
+ error = self.__errors[e.faultCode]
+ raise error(message=e.faultString)
+ raise UnknownError(
+ code=e.faultCode,
+ error=e.faultString,
+ server=self.env.xmlrpc_uri,
+ )
diff --git a/ipalib/util.py b/ipalib/util.py
new file mode 100644
index 000000000..4a58d7fbc
--- /dev/null
+++ b/ipalib/util.py
@@ -0,0 +1,151 @@
+# 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
+
+"""
+Various utility functions.
+"""
+
+import os
+from os import path
+import imp
+import optparse
+import logging
+import time
+from types import NoneType
+from xmlrpclib import Binary
+import krbV
+
+
+
+def xmlrpc_marshal(*args, **kw):
+ """
+ Marshal (args, kw) into ((kw,) + args).
+ """
+ kw = dict(
+ filter(lambda item: item[1] is not None, kw.iteritems())
+ )
+ args = tuple(
+ filter(lambda value: value is not None, args)
+ )
+ return ((kw,) + args)
+
+
+def xmlrpc_unmarshal(*params):
+ """
+ Unmarshal (params) into (args, kw).
+ """
+ if len(params) > 0:
+ kw = params[0]
+ if type(kw) is not dict:
+ raise TypeError('first xmlrpc argument must be dict')
+ else:
+ kw = {}
+ return (params[1:], kw)
+
+
+def get_current_principal():
+ try:
+ return krbV.default_context().default_ccache().principal().name
+ except krbV.Krb5Error:
+ #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 add_global_options(parser=None):
+ """
+ Add global options to an optparse.OptionParser instance.
+ """
+ if parser is None:
+ parser = optparse.OptionParser()
+ parser.add_option('-e', dest='env', metavar='KEY=VAL', action='append',
+ help='Set environment variable KEY to VAL',
+ )
+ parser.add_option('-c', dest='conf', metavar='FILE',
+ help='Load configuration from FILE',
+ )
+ parser.add_option('-d', '--debug', action='store_true',
+ help='Produce full debuging output',
+ )
+ parser.add_option('-v', '--verbose', action='store_true',
+ help='Produce more verbose output',
+ )
+ return parser
+
+
+class LogFormatter(logging.Formatter):
+ """
+ Log formatter that uses UTC for all timestamps.
+ """
+ converter = time.gmtime
+
+
+def make_repr(name, *args, **kw):
+ """
+ Construct a standard representation of a class instance.
+ """
+ args = [repr(a) for a in args]
+ kw = ['%s=%r' % (k, kw[k]) for k in sorted(kw)]
+ return '%s(%s)' % (name, ', '.join(args + kw))
diff --git a/ipaserver/__init__.py b/ipaserver/__init__.py
new file mode 100644
index 000000000..b0be96bd2
--- /dev/null
+++ b/ipaserver/__init__.py
@@ -0,0 +1,22 @@
+# 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
+
+"""
+Package containing server backend.
+"""
diff --git a/ipaserver/conn.py b/ipaserver/conn.py
new file mode 100644
index 000000000..fb00ad998
--- /dev/null
+++ b/ipaserver/conn.py
@@ -0,0 +1,69 @@
+# Authors: Rob Crittenden <rcritten@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
+#
+
+import krbV
+import ldap
+import ldap.dn
+import ipaldap
+
+class IPAConn:
+ def __init__(self, host, port, krbccache, debug=None):
+ self._conn = None
+
+ # Save the arguments
+ self._host = host
+ self._port = port
+ self._krbccache = krbccache
+ self._debug = debug
+
+ self._ctx = krbV.default_context()
+
+ ccache = krbV.CCache(name=krbccache, context=self._ctx)
+ cprinc = ccache.principal()
+
+ self._conn = ipaldap.IPAdmin(host,port,None,None,None,debug)
+
+ # This will bind the connection
+ try:
+ self._conn.set_krbccache(krbccache, cprinc.name)
+ except ldap.UNWILLING_TO_PERFORM, e:
+ raise e
+ except Exception, e:
+ raise e
+
+ def __del__(self):
+ # take no chances on unreleased connections
+ self.releaseConn()
+
+ def getConn(self):
+ return self._conn
+
+ def releaseConn(self):
+ if self._conn is None:
+ return
+
+ self._conn.unbind_s()
+ self._conn = None
+
+ return
+
+if __name__ == "__main__":
+ ipaconn = IPAConn("localhost", 389, "FILE:/tmp/krb5cc_500")
+ x = ipaconn.getConn().getEntry("dc=example,dc=com", ldap.SCOPE_SUBTREE, "uid=admin", ["cn"])
+ print "%s" % x
diff --git a/ipaserver/context.py b/ipaserver/context.py
new file mode 100644
index 000000000..15dd7d908
--- /dev/null
+++ b/ipaserver/context.py
@@ -0,0 +1,32 @@
+# Authors: Rob Crittenden <rcritten@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
+#
+
+# This should only be imported once. Importing again will cause the
+# a new instance to be created in the same thread
+
+# To use:
+# from ipaserver.context import context
+# context.foo = "bar"
+
+# FIXME: This module is depreciated and code should switch to using
+# ipalib.request instead
+
+import threading
+
+context = threading.local()
diff --git a/ipaserver/ipaldap.py b/ipaserver/ipaldap.py
new file mode 100644
index 000000000..4a2e4e31c
--- /dev/null
+++ b/ipaserver/ipaldap.py
@@ -0,0 +1,553 @@
+# Authors: Rich Megginson <richm@redhat.com>
+# Rob Crittenden <rcritten@redhat.com
+#
+# Copyright (C) 2007 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
+#
+
+import sys
+import os
+import os.path
+import socket
+import ldif
+import re
+import string
+import ldap
+import cStringIO
+import struct
+import ldap.sasl
+from ldap.controls import LDAPControl,DecodeControlTuples,EncodeControlTuples
+from ldap.ldapobject import SimpleLDAPObject
+from ipaserver import ipautil
+from ipalib import errors
+
+# Global variable to define SASL auth
+sasl_auth = ldap.sasl.sasl({},'GSSAPI')
+
+class Entry:
+ """
+ This class represents an LDAP Entry object. An LDAP entry consists of
+ a DN and a list of attributes. Each attribute consists of a name and
+ a list of values. In python-ldap, entries are returned as a list of
+ 2-tuples. Instance variables:
+
+ * dn - string - the string DN of the entry
+ * data - CIDict - case insensitive dict of the attributes and values
+ """
+ def __init__(self,entrydata):
+ """data is the raw data returned from the python-ldap result method, which is
+ a search result entry or a reference or None.
+ If creating a new empty entry, data is the string DN."""
+ if entrydata:
+ if isinstance(entrydata,tuple):
+ self.dn = entrydata[0]
+ self.data = ipautil.CIDict(entrydata[1])
+ elif isinstance(entrydata,str) or isinstance(entrydata,unicode):
+ self.dn = entrydata
+ self.data = ipautil.CIDict()
+ else:
+ self.dn = ''
+ self.data = ipautil.CIDict()
+
+ def __nonzero__(self):
+ """This allows us to do tests like if entry: returns false if there is no data,
+ true otherwise"""
+ return self.data != None and len(self.data) > 0
+
+ def hasAttr(self,name):
+ """Return True if this entry has an attribute named name, False otherwise"""
+ return self.data and self.data.has_key(name)
+
+ def __getattr__(self,name):
+ """If name is the name of an LDAP attribute, return the first value for that
+ attribute - equivalent to getValue - this allows the use of
+ entry.cn
+ instead of
+ entry.getValue('cn')
+ This also allows us to return None if an attribute is not found rather than
+ throwing an exception"""
+ return self.getValue(name)
+
+ def getValues(self,name):
+ """Get the list (array) of values for the attribute named name"""
+ return self.data.get(name)
+
+ def getValue(self,name):
+ """Get the first value for the attribute named name"""
+ return self.data.get(name,[None])[0]
+
+ def setValue(self, name, *value):
+ """
+ Set a value on this entry.
+
+ The value passed in may be a single value, several values, or a
+ single sequence. For example:
+
+ * ent.setValue('name', 'value')
+ * ent.setValue('name', 'value1', 'value2', ..., 'valueN')
+ * ent.setValue('name', ['value1', 'value2', ..., 'valueN'])
+ * ent.setValue('name', ('value1', 'value2', ..., 'valueN'))
+
+ Since value is a tuple, we may have to extract a list or tuple from
+ that tuple as in the last two examples above.
+ """
+ if isinstance(value[0],list) or isinstance(value[0],tuple):
+ self.data[name] = value[0]
+ else:
+ self.data[name] = value
+
+ setValues = setValue
+
+ def delAttr(self, name):
+ """
+ Entirely remove an attribute of this entry.
+ """
+ if self.hasAttr(name):
+ del self.data[name]
+
+ def toTupleList(self):
+ """Convert the attrs and values to a list of 2-tuples. The first element
+ of the tuple is the attribute name. The second element is either a
+ single value or a list of values."""
+ r = []
+ for i in self.data.iteritems():
+ n = ipautil.utf8_encode_values(i[1])
+ r.append((i[0], n))
+ return r
+
+ def toDict(self):
+ """Convert the attrs and values to a dict. The dict is keyed on the
+ attribute name. The value is either single value or a list of values."""
+ result = ipautil.CIDict(self.data)
+ for i in result.keys():
+ result[i] = ipautil.utf8_encode_values(result[i])
+ result['dn'] = self.dn
+ return result
+
+ def __str__(self):
+ """Convert the Entry to its LDIF representation"""
+ return self.__repr__()
+
+ # the ldif class base64 encodes some attrs which I would rather see in
+ # raw form - to encode specific attrs as base64, add them to the list below
+ ldif.safe_string_re = re.compile('^$')
+ base64_attrs = ['nsstate', 'krbprincipalkey', 'krbExtraData']
+
+ def __repr__(self):
+ """Convert the Entry to its LDIF representation"""
+ sio = cStringIO.StringIO()
+ # what's all this then? the unparse method will currently only accept
+ # a list or a dict, not a class derived from them. self.data is a
+ # cidict, so unparse barfs on it. I've filed a bug against python-ldap,
+ # but in the meantime, we have to convert to a plain old dict for
+ # printing
+ # I also don't want to see wrapping, so set the line width really high
+ # (1000)
+ newdata = {}
+ newdata.update(self.data)
+ ldif.LDIFWriter(sio,Entry.base64_attrs,1000).unparse(self.dn,newdata)
+ return sio.getvalue()
+
+def wrapper(f,name):
+ """This is the method that wraps all of the methods of the superclass.
+ This seems to need to be an unbound method, that's why it's outside
+ of IPAdmin. Perhaps there is some way to do this with the new
+ classmethod or staticmethod of 2.4. Basically, we replace every call
+ to a method in SimpleLDAPObject (the superclass of IPAdmin) with a
+ call to inner. The f argument to wrapper is the bound method of
+ IPAdmin (which is inherited from the superclass). Bound means that it
+ will implicitly be called with the self argument, it is not in the
+ args list. name is the name of the method to call. If name is a
+ method that returns entry objects (e.g. result), we wrap the data
+ returned by an Entry class. If name is a method that takes an entry
+ argument, we extract the raw data from the entry object to pass in.
+ """
+ def inner(*args, **kargs):
+ if name == 'result':
+ objtype, data = f(*args, **kargs)
+ # data is either a 2-tuple or a list of 2-tuples
+ # print data
+ if data:
+ if isinstance(data,tuple):
+ return objtype, Entry(data)
+ elif isinstance(data,list):
+ return objtype, [Entry(x) for x in data]
+ else:
+ raise TypeError, "unknown data type %s returned by result" % type(data)
+ else:
+ return objtype, data
+ elif name.startswith('add'):
+ # the first arg is self
+ # the second and third arg are the dn and the data to send
+ # We need to convert the Entry into the format used by
+ # python-ldap
+ ent = args[0]
+ if isinstance(ent,Entry):
+ return f(ent.dn, ent.toTupleList(), *args[2:])
+ else:
+ return f(*args, **kargs)
+ else:
+ return f(*args, **kargs)
+ return inner
+
+class IPAdmin(SimpleLDAPObject):
+
+ def __localinit(self):
+ """If a CA certificate is provided then it is assumed that we are
+ doing SSL client authentication with proxy auth.
+
+ If a CA certificate is not present then it is assumed that we are
+ using a forwarded kerberos ticket for SASL auth. SASL provides
+ its own encryption.
+ """
+ if self.cacert is not None:
+ SimpleLDAPObject.__init__(self,'ldaps://%s:%d' % (self.host,self.port))
+ else:
+ SimpleLDAPObject.__init__(self,'ldap://%s:%d' % (self.host,self.port))
+
+ def __init__(self,host,port=389,cacert=None,bindcert=None,bindkey=None,proxydn=None,debug=None):
+ """We just set our instance variables and wrap the methods - the real
+ work is done in __localinit. This is separated out this way so
+ that we can call it from places other than instance creation
+ e.g. when we just need to reconnect
+ """
+ if debug and debug.lower() == "on":
+ ldap.set_option(ldap.OPT_DEBUG_LEVEL,255)
+ if cacert is not None:
+ ldap.set_option(ldap.OPT_X_TLS_CACERTFILE,cacert)
+ if bindcert is not None:
+ ldap.set_option(ldap.OPT_X_TLS_CERTFILE,bindcert)
+ if bindkey is not None:
+ ldap.set_option(ldap.OPT_X_TLS_KEYFILE,bindkey)
+
+ self.__wrapmethods()
+ self.port = port
+ self.host = host
+ self.cacert = cacert
+ self.bindcert = bindcert
+ self.bindkey = bindkey
+ self.proxydn = proxydn
+ self.suffixes = {}
+ self.__localinit()
+
+ def __str__(self):
+ return self.host + ":" + str(self.port)
+
+ def __get_server_controls(self):
+ """Create the proxy user server control. The control has the form
+ 0x04 = Octet String
+ 4|0x80 sets the length of the string length field at 4 bytes
+ the struct() gets us the length in bytes of string self.proxydn
+ self.proxydn is the proxy dn to send"""
+
+ if self.proxydn is not None:
+ proxydn = chr(0x04) + chr(4|0x80) + struct.pack('l', socket.htonl(len(self.proxydn))) + self.proxydn;
+
+ # Create the proxy control
+ sctrl=[]
+ sctrl.append(LDAPControl('2.16.840.1.113730.3.4.18',True,proxydn))
+ else:
+ sctrl=None
+
+ return sctrl
+
+ def toLDAPURL(self):
+ return "ldap://%s:%d/" % (self.host,self.port)
+
+ def set_proxydn(self, proxydn):
+ self.proxydn = proxydn
+
+ def set_krbccache(self, krbccache, principal):
+ if krbccache is not None:
+ os.environ["KRB5CCNAME"] = krbccache
+ self.sasl_interactive_bind_s("", sasl_auth)
+ self.principal = principal
+ self.proxydn = None
+
+ def do_simple_bind(self, binddn="cn=directory manager", bindpw=""):
+ self.binddn = binddn
+ self.bindpwd = bindpw
+ self.simple_bind_s(binddn, bindpw)
+
+ def getEntry(self,*args):
+ """This wraps the search function. It is common to just get one entry"""
+
+ sctrl = self.__get_server_controls()
+
+ if sctrl is not None:
+ self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl)
+
+ try:
+ res = self.search(*args)
+ objtype, obj = self.result(res)
+ except ldap.NO_SUCH_OBJECT, e:
+ raise errors.NotFound, notfound(args)
+ except ldap.LDAPError, e:
+ raise errors.DatabaseError, e
+
+ if not obj:
+ raise errors.NotFound, notfound(args)
+
+ elif isinstance(obj,Entry):
+ return obj
+ else: # assume list/tuple
+ return obj[0]
+
+ def getList(self,*args):
+ """This wraps the search function to find multiple entries."""
+
+ sctrl = self.__get_server_controls()
+ if sctrl is not None:
+ self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl)
+
+ try:
+ res = self.search(*args)
+ objtype, obj = self.result(res)
+ except (ldap.ADMINLIMIT_EXCEEDED, ldap.SIZELIMIT_EXCEEDED), e:
+ # Too many results returned by search
+ raise e
+ except ldap.LDAPError, e:
+ raise e
+
+ if not obj:
+ raise errors.NotFound, notfound(args)
+
+ entries = []
+ for s in obj:
+ entries.append(s)
+
+ return entries
+
+ def getListAsync(self,*args):
+ """This version performs an asynchronous search, to allow
+ results even if we hit a limit.
+
+ It returns a list: counter followed by the results.
+ If the results are truncated, counter will be set to -1.
+ """
+
+ sctrl = self.__get_server_controls()
+ if sctrl is not None:
+ self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl)
+
+ entries = []
+ partial = 0
+
+ try:
+ msgid = self.search_ext(*args)
+ objtype, result_list = self.result(msgid, 0)
+ while result_list:
+ for result in result_list:
+ entries.append(result)
+ objtype, result_list = self.result(msgid, 0)
+ except (ldap.ADMINLIMIT_EXCEEDED, ldap.SIZELIMIT_EXCEEDED,
+ ldap.TIMELIMIT_EXCEEDED), e:
+ partial = 1
+ except ldap.LDAPError, e:
+ raise e
+
+ if not entries:
+ raise errors.NotFound, notfound(args)
+
+ if partial == 1:
+ counter = -1
+ else:
+ counter = len(entries)
+
+ return [counter] + entries
+
+ def addEntry(self,*args):
+ """This wraps the add function. It assumes that the entry is already
+ populated with all of the desired objectclasses and attributes"""
+
+ sctrl = self.__get_server_controls()
+
+ try:
+ if sctrl is not None:
+ self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl)
+ self.add_s(*args)
+ except ldap.ALREADY_EXISTS, e:
+ raise errors.DuplicateEntry, "Entry already exists"
+ except ldap.LDAPError, e:
+ raise errors.DatabaseError, e
+ return True
+
+ def updateRDN(self, dn, newrdn):
+ """Wrap the modrdn function."""
+
+ sctrl = self.__get_server_controls()
+
+ if dn == newrdn:
+ # no need to report an error
+ return True
+
+ try:
+ if sctrl is not None:
+ self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl)
+ self.modrdn_s(dn, newrdn, delold=1)
+ except ldap.LDAPError, e:
+ raise errors.DatabaseError, e
+ return True
+
+ def updateEntry(self,dn,oldentry,newentry):
+ """This wraps the mod function. It assumes that the entry is already
+ populated with all of the desired objectclasses and attributes"""
+
+ sctrl = self.__get_server_controls()
+
+ modlist = self.generateModList(oldentry, newentry)
+
+ if len(modlist) == 0:
+ raise errors.EmptyModlist
+
+ try:
+ if sctrl is not None:
+ self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl)
+ self.modify_s(dn, modlist)
+ # this is raised when a 'delete' attribute isn't found.
+ # it indicates the previous attribute was removed by another
+ # update, making the oldentry stale.
+ except ldap.NO_SUCH_ATTRIBUTE:
+ raise errors.MidairCollision
+ except ldap.LDAPError, e:
+ raise errors.DatabaseError, e
+ return True
+
+ def generateModList(self, old_entry, new_entry):
+ """A mod list generator that computes more precise modification lists
+ than the python-ldap version. This version purposely generates no
+ REPLACE operations, to deal with multi-user updates more properly."""
+ modlist = []
+
+ old_entry = ipautil.CIDict(old_entry)
+ new_entry = ipautil.CIDict(new_entry)
+
+ keys = set(map(string.lower, old_entry.keys()))
+ keys.update(map(string.lower, new_entry.keys()))
+
+ for key in keys:
+ new_values = new_entry.get(key, [])
+ if not(isinstance(new_values,list) or isinstance(new_values,tuple)):
+ new_values = [new_values]
+ new_values = filter(lambda value:value!=None, new_values)
+ new_values = set(new_values)
+
+ old_values = old_entry.get(key, [])
+ if not(isinstance(old_values,list) or isinstance(old_values,tuple)):
+ old_values = [old_values]
+ old_values = filter(lambda value:value!=None, old_values)
+ old_values = set(old_values)
+
+ adds = list(new_values.difference(old_values))
+ removes = list(old_values.difference(new_values))
+
+ if len(removes) > 0:
+ modlist.append((ldap.MOD_DELETE, key, removes))
+ if len(adds) > 0:
+ modlist.append((ldap.MOD_ADD, key, adds))
+
+ return modlist
+
+ def inactivateEntry(self,dn,has_key):
+ """Rather than deleting entries we mark them as inactive.
+ has_key defines whether the entry already has nsAccountlock
+ set so we can determine which type of mod operation to run."""
+
+ sctrl = self.__get_server_controls()
+ modlist=[]
+
+ if has_key:
+ operation = ldap.MOD_REPLACE
+ else:
+ operation = ldap.MOD_ADD
+
+ modlist.append((operation, "nsAccountlock", "true"))
+
+ try:
+ if sctrl is not None:
+ self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl)
+ self.modify_s(dn, modlist)
+ except ldap.LDAPError, e:
+ raise errors.DatabaseError, e
+ return True
+
+ def deleteEntry(self,*args):
+ """This wraps the delete function. Use with caution."""
+
+ sctrl = self.__get_server_controls()
+
+ try:
+ if sctrl is not None:
+ self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl)
+ self.delete_s(*args)
+ except ldap.INSUFFICIENT_ACCESS, e:
+ raise errors.InsufficientAccess, e
+ except ldap.LDAPError, e:
+ raise errors.DatabaseError, e
+ return True
+
+ def modifyPassword(self,dn,oldpass,newpass):
+ """Set the user password using RFC 3062, LDAP Password Modify Extended
+ Operation. This ends up calling the IPA password slapi plugin
+ handler so the Kerberos password gets set properly.
+
+ oldpass is not mandatory
+ """
+
+ sctrl = self.__get_server_controls()
+
+ try:
+ if sctrl is not None:
+ self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl)
+ self.passwd_s(dn, oldpass, newpass)
+ except ldap.LDAPError, e:
+ raise e
+ return True
+
+ def __wrapmethods(self):
+ """This wraps all methods of SimpleLDAPObject, so that we can intercept
+ the methods that deal with entries. Instead of using a raw list of tuples
+ of lists of hashes of arrays as the entry object, we want to wrap entries
+ in an Entry class that provides some useful methods"""
+ for name in dir(self.__class__.__bases__[0]):
+ attr = getattr(self, name)
+ if callable(attr):
+ setattr(self, name, wrapper(attr, name))
+
+ def normalizeDN(dn):
+ # not great, but will do until we use a newer version of python-ldap
+ # that has DN utilities
+ ary = ldap.explode_dn(dn.lower())
+ return ",".join(ary)
+ normalizeDN = staticmethod(normalizeDN)
+
+def notfound(args):
+ """Return a string suitable for displaying as an error when a
+ search returns no results.
+
+ This just returns whatever is after the equals sign"""
+ if len(args) > 2:
+ searchfilter = args[2]
+ try:
+ # Python re doesn't do paren counting so the string could
+ # have a trailing paren "foo)"
+ target = re.match(r'\(.*=(.*)\)', searchfilter).group(1)
+ target = target.replace(")","")
+ except:
+ target = searchfilter
+ return "%s not found" % str(target)
+ else:
+ return args[0]
diff --git a/ipaserver/ipautil.py b/ipaserver/ipautil.py
new file mode 100644
index 000000000..6422fe5a6
--- /dev/null
+++ b/ipaserver/ipautil.py
@@ -0,0 +1,201 @@
+# Authors: Simo Sorce <ssorce@redhat.com>
+#
+# Copyright (C) 2007 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
+#
+
+import string
+import xmlrpclib
+import re
+
+def realm_to_suffix(realm_name):
+ s = realm_name.split(".")
+ terms = ["dc=" + x.lower() for x in s]
+ return ",".join(terms)
+
+class CIDict(dict):
+ """
+ Case-insensitive but case-respecting dictionary.
+
+ This code is derived from python-ldap's cidict.py module,
+ written by stroeder: http://python-ldap.sourceforge.net/
+
+ This version extends 'dict' so it works properly with TurboGears.
+ If you extend UserDict, isinstance(foo, dict) returns false.
+ """
+
+ def __init__(self,default=None):
+ super(CIDict, self).__init__()
+ self._keys = {}
+ self.update(default or {})
+
+ def __getitem__(self,key):
+ return super(CIDict,self).__getitem__(string.lower(key))
+
+ def __setitem__(self,key,value):
+ lower_key = string.lower(key)
+ self._keys[lower_key] = key
+ return super(CIDict,self).__setitem__(string.lower(key),value)
+
+ def __delitem__(self,key):
+ lower_key = string.lower(key)
+ del self._keys[lower_key]
+ return super(CIDict,self).__delitem__(string.lower(key))
+
+ def update(self,dict):
+ for key in dict.keys():
+ self[key] = dict[key]
+
+ def has_key(self,key):
+ return super(CIDict, self).has_key(string.lower(key))
+
+ def get(self,key,failobj=None):
+ try:
+ return self[key]
+ except KeyError:
+ return failobj
+
+ def keys(self):
+ return self._keys.values()
+
+ def items(self):
+ result = []
+ for k in self._keys.values():
+ result.append((k,self[k]))
+ return result
+
+ def copy(self):
+ copy = {}
+ for k in self._keys.values():
+ copy[k] = self[k]
+ return copy
+
+ def iteritems(self):
+ return self.copy().iteritems()
+
+ def iterkeys(self):
+ return self.copy().iterkeys()
+
+ def setdefault(self,key,value=None):
+ try:
+ return self[key]
+ except KeyError:
+ self[key] = value
+ return value
+
+ def pop(self, key, *args):
+ try:
+ value = self[key]
+ del self[key]
+ return value
+ except KeyError:
+ if len(args) == 1:
+ return args[0]
+ raise
+
+ def popitem(self):
+ (lower_key,value) = super(CIDict,self).popitem()
+ key = self._keys[lower_key]
+ del self._keys[lower_key]
+
+ return (key,value)
+
+
+#
+# The safe_string_re regexp and needs_base64 function are extracted from the
+# python-ldap ldif module, which was
+# written by Michael Stroeder <michael@stroeder.com>
+# http://python-ldap.sourceforge.net
+#
+# It was extracted because ipaldap.py is naughtily reaching into the ldif
+# module and squashing this regexp.
+#
+SAFE_STRING_PATTERN = '(^(\000|\n|\r| |:|<)|[\000\n\r\200-\377]+|[ ]+$)'
+safe_string_re = re.compile(SAFE_STRING_PATTERN)
+
+def needs_base64(s):
+ """
+ returns 1 if s has to be base-64 encoded because of special chars
+ """
+ return not safe_string_re.search(s) is None
+
+
+def wrap_binary_data(data):
+ """Converts all binary data strings into Binary objects for transport
+ back over xmlrpc."""
+ if isinstance(data, str):
+ if needs_base64(data):
+ return xmlrpclib.Binary(data)
+ else:
+ return data
+ elif isinstance(data, list) or isinstance(data,tuple):
+ retval = []
+ for value in data:
+ retval.append(wrap_binary_data(value))
+ return retval
+ elif isinstance(data, dict):
+ retval = {}
+ for (k,v) in data.iteritems():
+ retval[k] = wrap_binary_data(v)
+ return retval
+ else:
+ return data
+
+
+def unwrap_binary_data(data):
+ """Converts all Binary objects back into strings."""
+ if isinstance(data, xmlrpclib.Binary):
+ # The data is decoded by the xmlproxy, but is stored
+ # in a binary object for us.
+ return str(data)
+ elif isinstance(data, str):
+ return data
+ elif isinstance(data, list) or isinstance(data,tuple):
+ retval = []
+ for value in data:
+ retval.append(unwrap_binary_data(value))
+ return retval
+ elif isinstance(data, dict):
+ retval = {}
+ for (k,v) in data.iteritems():
+ retval[k] = unwrap_binary_data(v)
+ return retval
+ else:
+ return data
+
+def get_gsserror(e):
+ """A GSSError exception looks differently in python 2.4 than it does
+ in python 2.5, deal with it."""
+
+ try:
+ primary = e[0]
+ secondary = e[1]
+ except:
+ primary = e[0][0]
+ secondary = e[0][1]
+
+ return (primary[0], secondary[0])
+
+def utf8_encode_value(value):
+ if isinstance(value,unicode):
+ return value.encode('utf-8')
+ return value
+
+def utf8_encode_values(values):
+ if isinstance(values,list) or isinstance(values,tuple):
+ return map(utf8_encode_value, values)
+ else:
+ return utf8_encode_value(values)
diff --git a/ipaserver/mod_python_xmlrpc.py b/ipaserver/mod_python_xmlrpc.py
new file mode 100644
index 000000000..9a2960f93
--- /dev/null
+++ b/ipaserver/mod_python_xmlrpc.py
@@ -0,0 +1,367 @@
+# mod_python script
+
+# ipaxmlrpc - an XMLRPC interface for ipa.
+# Copyright (c) 2007 Red Hat
+#
+# IPA is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation;
+# version 2.1 of the License.
+#
+# This software 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this software; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+#
+# Based on kojixmlrpc - an XMLRPC interface for koji by
+# Mike McLean <mikem@redhat.com>
+#
+# Authors:
+# Rob Crittenden <rcritten@redhat.com>
+
+"""
+Production XML-RPC server using mod_python.
+"""
+
+import sys
+
+
+import time
+import traceback
+import pprint
+from xmlrpclib import Marshaller,loads,dumps,Fault
+try:
+ from mod_python import apache
+except ImportError:
+ pass
+import logging
+
+import ldap
+from ipalib import api
+from ipalib import config
+from ipaserver import conn
+from ipaserver.servercore import context
+from ipaserver.servercore import ipautil
+from ipalib.util import xmlrpc_unmarshal
+
+import string
+
+api.load_plugins()
+
+# Global list of available functions
+gfunctions = {}
+
+def register_function(function, name = None):
+ if name is None:
+ name = function.__name__
+ gfunctions[name] = function
+
+class ModXMLRPCRequestHandler(object):
+ """Simple XML-RPC handler for mod_python environment"""
+
+ def __init__(self):
+ global gfunctions
+
+ self.funcs = gfunctions
+ self.traceback = False
+ #introspection functions
+ self.register_function(self.ping, name="ping")
+ self.register_function(self.list_api, name="_listapi")
+ self.register_function(self.system_listMethods, name="system.listMethods")
+ self.register_function(self.system_methodSignature, name="system.methodSignature")
+ self.register_function(self.system_methodHelp, name="system.methodHelp")
+ self.register_function(self.multiCall)
+
+ def register_function(self, function, name = None):
+ if name is None:
+ name = function.__name__
+ self.funcs[name] = function
+
+ def register_module(self, instance, prefix=None):
+ """Register all the public functions in an instance with prefix prepended
+
+ For example
+ h.register_module(exports,"pub.sys")
+ will register the methods of exports with names like
+ pub.sys.method1
+ pub.sys.method2
+ ...etc
+ """
+ for name in dir(instance):
+ if name.startswith('_'):
+ continue
+ function = getattr(instance, name)
+ if not callable(function):
+ continue
+ if prefix is not None:
+ name = "%s.%s" %(prefix,name)
+ self.register_function(function, name=name)
+
+ def register_instance(self,instance):
+ self.register_module(instance)
+
+ def _marshaled_dispatch(self, data, req):
+ """Dispatches an XML-RPC method from marshalled (XML) data."""
+
+ params, method = loads(data)
+ pythonopts = req.get_options()
+
+ # Populate the Apache environment variables
+ req.add_common_vars()
+
+ context.opts['remoteuser'] = req.user
+
+ if req.subprocess_env.get("KRB5CCNAME") is not None:
+ krbccache = req.subprocess_env.get("KRB5CCNAME")
+ else:
+ response = dumps(Fault(5, "Did not receive Kerberos credentials."))
+ return response
+
+ debuglevel = logging.INFO
+ if pythonopts.get("IPADebug"):
+ context.opts['ipadebug'] = pythonopts.get("IPADebug").lower()
+
+ if context.opts['ipadebug'] == "on":
+ debuglevel = logging.DEBUG
+
+ if not context.opts.get('ipadebug'):
+ context.opts['ipadebug'] = "off"
+
+ logging.basicConfig(level=debuglevel,
+ format='[%(asctime)s] [%(levelname)s] %(message)s',
+ datefmt='%a %b %d %H:%M:%S %Y',
+ stream=sys.stderr)
+
+ logging.info("Interpreter: %s" % req.interpreter)
+
+
+# if opts['ipadebug'] == "on":
+# for o in opts:
+# logging.debug("IPA: setting option %s: %s" % (o, opts[o]))
+# for e in req.subprocess_env:
+# logging.debug("IPA: environment %s: %s" % (e, req.subprocess_env[e]))
+
+ context.conn = conn.IPAConn(api.env.ldaphost, api.env.ldapport, krbccache, context.opts.get('ipadebug'))
+
+ start = time.time()
+ # generate response
+ try:
+ response = self._dispatch(method, params)
+ # wrap response in a singleton tuple
+ response = (response,)
+ response = dumps(response, methodresponse=1, allow_none=1)
+ except Fault, e:
+ response = dumps(Fault(e.faultCode, e.faultString))
+ except:
+ self.traceback = True
+ # report exception back to server
+ e_class, e = sys.exc_info()[:2]
+ faultCode = getattr(e_class,'faultCode',1)
+ tb_str = ''.join(traceback.format_exception(*sys.exc_info()))
+ faultString = tb_str
+ response = dumps(Fault(faultCode, faultString))
+
+ return response
+
+ def _dispatch(self,method,params):
+ func = self.funcs.get(method,None)
+ if func is None:
+ raise Fault(1, "Invalid method: %s" % method)
+
+ params = list(ipautil.unwrap_binary_data(params))
+ (args, kw) = xmlrpc_unmarshal(*params)
+
+ ret = func(*args, **kw)
+
+ return ipautil.wrap_binary_data(ret)
+
+ def multiCall(self, calls):
+ """Execute a multicall. Execute each method call in the calls list, collecting results and errors, and return those as a list."""
+ results = []
+ for call in calls:
+ try:
+ result = self._dispatch(call['methodName'], call['params'])
+ except Fault, fault:
+ results.append({'faultCode': fault.faultCode, 'faultString': fault.faultString})
+ except:
+ # transform unknown exceptions into XML-RPC Faults
+ # don't create a reference to full traceback since this creates
+ # a circular reference.
+ exc_type, exc_value = sys.exc_info()[:2]
+ faultCode = getattr(exc_type, 'faultCode', 1)
+ faultString = ', '.join(exc_value.args)
+ trace = traceback.format_exception(*sys.exc_info())
+ # traceback is not part of the multicall spec, but we include it for debugging purposes
+ results.append({'faultCode': faultCode, 'faultString': faultString, 'traceback': trace})
+ else:
+ results.append([result])
+
+ return results
+
+ def list_api(self):
+ funcs = []
+ for name,func in self.funcs.items():
+ #the keys in self.funcs determine the name of the method as seen over xmlrpc
+ #func.__name__ might differ (e.g. for dotted method names)
+ args = self._getFuncArgs(func)
+ doc = None
+ try:
+ doc = func.doc
+ except AttributeError:
+ doc = func.__doc__
+ funcs.append({'name': name,
+ 'doc': doc,
+ 'args': args})
+ return funcs
+
+ def ping(self):
+ """Simple test to see if the XML-RPC is up and active."""
+ return "pong"
+
+ def _getFuncArgs(self, func):
+ try:
+ # Plugins have this
+ args = list(func.args)
+ args.append("kw")
+ except:
+ # non-plugin functions such as the introspective ones
+ args = []
+ for x in range(0, func.func_code.co_argcount):
+ if x == 0 and func.func_code.co_varnames[x] == "self":
+ continue
+ # opts is a name we tack on internally. Don't publish it.
+ if func.func_code.co_varnames[x] == "opts":
+ continue
+ if func.func_defaults and func.func_code.co_argcount - x <= len(func.func_defaults):
+ args.append((func.func_code.co_varnames[x], func.func_defaults[x - func.func_code.co_argcount + len(func.func_defaults)]))
+ else:
+ args.append(func.func_code.co_varnames[x])
+ return args
+
+ def system_listMethods(self):
+ """List all available XML-RPC methods"""
+ return self.funcs.keys()
+
+ def system_methodSignature(self, method):
+ """signatures are not supported"""
+ #it is not possible to autogenerate this data
+ return 'signatures not supported'
+
+ def system_methodHelp(self, method):
+ """Return help on a specific method"""
+ func = self.funcs.get(method)
+ if func is None:
+ return ""
+ arglist = []
+ for arg in self._getFuncArgs(func):
+ if isinstance(arg,str):
+ arglist.append(arg)
+ else:
+ arglist.append('%s=%s' % (arg[0], arg[1]))
+ ret = '%s(%s)' % (method, ", ".join(arglist))
+ doc = None
+ try:
+ doc = func.doc
+ except AttributeError:
+ doc = func.__doc__
+ if doc:
+ ret += "\ndescription: %s" % func.__doc__
+ return ret
+
+ def handle_request(self,req):
+ """Handle a single XML-RPC request"""
+
+ # XMLRPC uses POST only. Reject anything else
+ if req.method != 'POST':
+ req.allow_methods(['POST'],1)
+ raise apache.SERVER_RETURN, apache.HTTP_METHOD_NOT_ALLOWED
+
+ # The LDAP connection pool is not thread-safe. Avoid problems and
+ # force the forked model for now.
+ if apache.mpm_query(apache.AP_MPMQ_IS_THREADED):
+ response = dumps(Fault(3, "Apache must use the forked model"))
+ else:
+ response = self._marshaled_dispatch(req.read(), req)
+
+ req.content_type = "text/xml"
+ req.set_content_length(len(response))
+ req.write(response)
+
+
+#
+# mod_python handler
+#
+
+def handler(req, profiling=False):
+ h = ModXMLRPCRequestHandler()
+
+ if profiling:
+ import profile, pstats, StringIO, tempfile
+ global _profiling_req
+ _profiling_req = req
+ temp = tempfile.NamedTemporaryFile()
+ profile.run("import ipxmlrpc; ipaxmlrpc.handler(ipaxmlrpc._profiling_req, False)", temp.name)
+ stats = pstats.Stats(temp.name)
+ strstream = StringIO.StringIO()
+ sys.stdout = strstream
+ stats.sort_stats("time")
+ stats.print_stats()
+ req.write("<pre>" + strstream.getvalue() + "</pre>")
+ _profiling_req = None
+ else:
+ context.opts = req.get_options()
+ context.reqs = req
+ try:
+ h.handle_request(req)
+ finally:
+ # Clean up any per-request data and connections
+ for k in context.__dict__.keys():
+ del context.__dict__[k]
+
+ return apache.OK
+
+def setup_logger(level):
+ """Make a global logging object."""
+ l = logging.getLogger()
+ l.setLevel(level)
+ h = logging.StreamHandler()
+ f = logging.Formatter("[%(asctime)s] [%(levelname)s] %(message)s")
+ h.setFormatter(f)
+ l.addHandler(h)
+
+ return
+
+def load_modules():
+ """Load all plugins and register the XML-RPC functions we provide.
+
+ Called by mod_python PythonImport
+
+ PythonImport /path/to/ipaxmlrpc.py::load_modules main_interpreter
+ ...
+ PythonInterpreter main_interpreter
+ PythonHandler ipaxmlrpc
+ """
+
+ # setup up the logger with a DEBUG level. It may get reset to INFO
+ # once we start processing requests. We don't have access to the
+ # Apache configuration yet.
+ setup_logger(logging.DEBUG)
+
+ 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:
+ logging.debug("registering XML-RPC call %s" % cmd)
+ register_function(api.Command[cmd], cmd)
+
+ return
diff --git a/ipaserver/plugins/__init__.py b/ipaserver/plugins/__init__.py
new file mode 100644
index 000000000..5737dcb79
--- /dev/null
+++ b/ipaserver/plugins/__init__.py
@@ -0,0 +1,24 @@
+# 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
+
+"""
+Sub-package containing all server plugins.
+
+By convention, modules with frontend plugins are named f_*.py and modules
+with backend plugins are named b_*.py.
+"""
diff --git a/ipaserver/plugins/b_ldap.py b/ipaserver/plugins/b_ldap.py
new file mode 100644
index 000000000..9e06ce51b
--- /dev/null
+++ b/ipaserver/plugins/b_ldap.py
@@ -0,0 +1,334 @@
+# Authors:
+# Rob Crittenden <rcritten@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
+
+"""
+Backend plugin for LDAP.
+
+This wraps the python-ldap bindings.
+"""
+
+import ldap as _ldap
+from ipalib import api, Context
+from ipalib import errors
+from ipalib.crud import CrudBackend
+from ipaserver import servercore
+from ipaserver import ipaldap
+
+
+class conn(Context):
+ """
+ Thread-local LDAP connection.
+ """
+
+ def get_value(self):
+ return 'it worked'
+
+api.register(conn)
+
+
+class ldap(CrudBackend):
+ """
+ LDAP backend plugin.
+ """
+
+ def __init__(self):
+ self.dn = _ldap.dn
+ super(ldap, self).__init__()
+
+ def make_user_dn(self, uid):
+ """
+ Construct user dn from uid.
+ """
+ return 'uid=%s,%s,%s' % (
+ self.dn.escape_dn_chars(uid),
+ self.api.env.container_user,
+ self.api.env.basedn,
+ )
+
+ def make_group_dn(self, cn):
+ """
+ Construct group dn from cn.
+ """
+ return 'cn=%s,%s,%s' % (
+ self.dn.escape_dn_chars(cn),
+ self.api.env.container_group,
+ self.api.env.basedn,
+ )
+
+ def make_hostgroup_dn(self, cn):
+ """
+ Construct group of hosts dn from cn.
+ """
+ return 'cn=%s,%s,%s' % (
+ self.dn.escape_dn_chars(cn),
+ self.api.env.container_hostgroup,
+ self.api.env.basedn,
+ )
+
+ def make_service_dn(self, principal):
+ """
+ Construct service principal dn from principal name
+ """
+ return 'krbprincipalname=%s,%s,%s' % (
+ self.dn.escape_dn_chars(principal),
+ self.api.env.container_service,
+ self.api.env.basedn,
+ )
+
+ def make_host_dn(self, hostname):
+ """
+ Construct host dn from hostname
+ """
+ return 'cn=%s,%s,%s' % (
+ self.dn.escape_dn_chars(hostname),
+ self.api.env.container_host,
+ self.api.env.basedn,
+ )
+
+ def get_object_type(self, attribute):
+ """
+ Based on attribute, make an educated guess as to the type of
+ object we're looking for.
+ """
+ attribute = attribute.lower()
+ object_type = None
+ if attribute == "uid": # User
+ object_type = "posixAccount"
+ elif attribute == "cn": # Group
+ object_type = "posixGroup"
+ elif attribute == "krbprincipalname": # Service
+ object_type = "krbPrincipal"
+
+ return object_type
+
+ def find_entry_dn(self, key_attribute, primary_key, object_type=None, base=None):
+ """
+ Find an existing entry's dn from an attribute
+ """
+ key_attribute = key_attribute.lower()
+ if not object_type:
+ object_type = self.get_object_type(key_attribute)
+ if not object_type:
+ return None
+
+ search_filter = "(&(objectclass=%s)(%s=%s))" % (
+ object_type,
+ key_attribute,
+ self.dn.escape_dn_chars(primary_key)
+ )
+
+ if not base:
+ base = self.api.env.container_accounts
+
+ search_base = "%s, %s" % (base, self.api.env.basedn)
+
+ entry = servercore.get_sub_entry(search_base, search_filter, ['dn', 'objectclass'])
+
+ return entry.get('dn')
+
+ def get_base_entry(self, searchbase, searchfilter, attrs):
+ return servercore.get_base_entry(searchbase, searchfilter, attrs)
+
+ def get_sub_entry(self, searchbase, searchfilter, attrs):
+ return servercore.get_sub_entry(searchbase, searchfilter, attrs)
+
+ def get_one_entry(self, searchbase, searchfilter, attrs):
+ return servercore.get_one_entry(searchbase, searchfilter, attrs)
+
+ def get_ipa_config(self):
+ """Return a dictionary of the IPA configuration"""
+ return servercore.get_ipa_config()
+
+ def mark_entry_active(self, dn):
+ return servercore.mark_entry_active(dn)
+
+ def mark_entry_inactive(self, dn):
+ return servercore.mark_entry_inactive(dn)
+
+ def _generate_search_filters(self, **kw):
+ """Generates a search filter based on a list of words and a list
+ of fields to search against.
+
+ Returns a tuple of two filters: (exact_match, partial_match)
+ """
+
+ # construct search pattern for a single word
+ # (|(f1=word)(f2=word)...)
+ exact_pattern = "(|"
+ for field in kw.keys():
+ exact_pattern += "(%s=%s)" % (field, kw[field])
+ exact_pattern += ")"
+
+ sub_pattern = "(|"
+ for field in kw.keys():
+ sub_pattern += "(%s=*%s*)" % (field, kw[field])
+ sub_pattern += ")"
+
+ # construct the giant match for all words
+ exact_match_filter = "(&" + exact_pattern + ")"
+ partial_match_filter = "(|" + sub_pattern + ")"
+
+ return (exact_match_filter, partial_match_filter)
+
+ def modify_password(self, dn, **kw):
+ return servercore.modify_password(dn, kw.get('oldpass'), kw.get('newpass'))
+
+ def add_member_to_group(self, memberdn, groupdn, memberattr='member'):
+ """
+ Add a new member to a group.
+
+ :param memberdn: the DN of the member to add
+ :param groupdn: the DN of the group to add a member to
+ """
+ return servercore.add_member_to_group(memberdn, groupdn, memberattr)
+
+ def remove_member_from_group(self, memberdn, groupdn, memberattr='member'):
+ """
+ Remove a new member from a group.
+
+ :param memberdn: the DN of the member to remove
+ :param groupdn: the DN of the group to remove a member from
+ """
+ return servercore.remove_member_from_group(memberdn, groupdn, memberattr)
+
+ # The CRUD operations
+
+ def strip_none(self, kw):
+ """
+ Remove any None values present in the LDAP attribute dict.
+ """
+ for (key, value) in kw.iteritems():
+ if value is None:
+ continue
+ if type(value) in (list, tuple):
+ value = filter(
+ lambda v: type(v) in (str, unicode, bool, int, float),
+ value
+ )
+ if len(value) > 0:
+ yield (key, value)
+ else:
+ assert type(value) in (str, unicode, bool, int, float)
+ yield (key, value)
+ yield (key, value)
+
+ def create(self, **kw):
+ if servercore.entry_exists(kw['dn']):
+ raise errors.DuplicateEntry("entry already exists")
+ kw = dict(self.strip_none(kw))
+
+
+ entry = ipaldap.Entry(kw['dn'])
+
+ # dn isn't allowed to be in the entry itself
+ del kw['dn']
+
+ # Fill in our new entry
+ for k in kw:
+ entry.setValues(k, kw[k])
+
+ servercore.add_entry(entry)
+ return self.retrieve(entry.dn)
+
+ def retrieve(self, dn, attributes=None):
+ return servercore.get_entry_by_dn(dn, attributes)
+
+ def update(self, dn, **kw):
+ result = self.retrieve(dn, ["*"])
+ start_keys = kw.keys()
+
+ entry = ipaldap.Entry((dn, servercore.convert_scalar_values(result)))
+ kw = dict(self.strip_none(kw))
+ for k in kw:
+ entry.setValues(k, kw[k])
+
+ remove_keys = list(set(start_keys) - set(kw.keys()))
+ for k in remove_keys:
+ entry.delAttr(k)
+
+ servercore.update_entry(entry.toDict(), remove_keys)
+
+ return self.retrieve(dn)
+
+ def delete(self, dn):
+ return servercore.delete_entry(dn)
+
+ def search(self, **kw):
+ objectclass = kw.get('objectclass')
+ sfilter = kw.get('filter')
+ attributes = kw.get('attributes')
+ base = kw.get('base')
+ if attributes:
+ del kw['attributes']
+ else:
+ attributes = ['*']
+ if objectclass:
+ del kw['objectclass']
+ if base:
+ del kw['base']
+ if sfilter:
+ del kw['filter']
+ (exact_match_filter, partial_match_filter) = self._generate_search_filters(**kw)
+ if objectclass:
+ exact_match_filter = "(&(objectClass=%s)%s)" % (objectclass, exact_match_filter)
+ partial_match_filter = "(&(objectClass=%s)%s)" % (objectclass, partial_match_filter)
+ if sfilter:
+ exact_match_filter = "(%s%s)" % (sfilter, exact_match_filter)
+ partial_match_filter = "(%s%s)" % (sfilter, partial_match_filter)
+
+ if not base:
+ base = self.api.env.container_accounts
+
+ search_base = "%s, %s" % (base, self.api.env.basedn)
+ try:
+ exact_results = servercore.search(search_base,
+ exact_match_filter, attributes)
+ except errors.NotFound:
+ exact_results = [0]
+
+ try:
+ partial_results = servercore.search(search_base,
+ partial_match_filter, attributes)
+ except errors.NotFound:
+ partial_results = [0]
+
+ exact_counter = exact_results[0]
+ partial_counter = partial_results[0]
+
+ exact_results = exact_results[1:]
+ partial_results = partial_results[1:]
+
+ # Remove exact matches from the partial_match list
+ exact_dns = set(map(lambda e: e.get('dn'), exact_results))
+ partial_results = filter(lambda e: e.get('dn') not in exact_dns,
+ partial_results)
+
+ if (exact_counter == -1) or (partial_counter == -1):
+ counter = -1
+ else:
+ counter = len(exact_results) + len(partial_results)
+
+ results = [counter]
+ for r in exact_results + partial_results:
+ results.append(r)
+
+ return results
+
+api.register(ldap)
diff --git a/ipaserver/plugins/b_ra.py b/ipaserver/plugins/b_ra.py
new file mode 100644
index 000000000..e6a9b63f4
--- /dev/null
+++ b/ipaserver/plugins/b_ra.py
@@ -0,0 +1,407 @@
+# Authors:
+# Andrew Wnuk <awnuk@redhat.com>
+# Jason Gerard DeRose <jderose@redhat.com>
+#
+# Copyright (C) 2009 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
+
+"""
+Backend plugin for IPA-RA.
+
+IPA-RA provides an access to CA to issue, retrieve, and revoke certificates.
+IPA-RA plugin provides CA interface via the following methods:
+ check_request_status to check certificate request status
+ get_certificate to retrieve an existing certificate
+ request_certificate to request certificate
+ revoke_certificate to revoke certificate
+ take_certificate_off_hold to take certificate off hold
+"""
+
+import os, stat, subprocess
+import array
+import errno
+import binascii
+import httplib, urllib
+from socket import gethostname
+
+from ipalib import api, Backend
+from ipalib import errors
+from ipaserver import servercore
+from ipaserver import ipaldap
+
+
+class ra(Backend):
+
+
+ def __init__(self):
+ self.sec_dir = api.env.dot_ipa + os.sep + 'alias'
+ self.pwd_file = self.sec_dir + os.sep + '.pwd'
+ self.noise_file = self.sec_dir + os.sep + '.noise'
+
+ self.ca_host = None
+ self.ca_port = None
+ self.ca_ssl_port = None
+
+ self.__get_ca_location()
+
+ self.ipa_key_size = "2048"
+ self.ipa_certificate_nickname = "ipaCert"
+ self.ca_certificate_nickname = "caCert"
+
+ if not os.path.isdir(self.sec_dir):
+ os.mkdir(self.sec_dir)
+ self.__create_pwd_file()
+ self.__create_nss_db()
+ self.__import_ca_chain()
+ self.__request_ipa_certificate(self.__generate_ipa_request())
+ super(ra, self).__init__()
+
+
+ def check_request_status(self, request_id=None):
+ """
+ Check certificate request status
+ :param request_id: request ID
+ """
+ self.log.debug("IPA-RA: check_request_status")
+ return_values = {}
+ if request_id is not None:
+ params = urllib.urlencode({'requestId': request_id, 'xmlOutput': 'true'})
+ headers = {"Content-type": "application/x-www-form-urlencoded"}
+ conn = httplib.HTTPConnection(self.ca_host, self.ca_port)
+ conn.request("POST", "/ca/ee/ca/checkRequest", params, headers)
+ response = conn.getresponse()
+ api.log.debug("IPA-RA: response.status: %d response.reason: %s" % (response.status, response.reason))
+ data = response.read()
+ conn.close()
+ self.log.debug(data)
+ if data is not None:
+ request_status = self.__find_substring(data, 'header.status = "', '"')
+ if request_status is not None:
+ return_values["status"] = "0"
+ return_values["request_status"] = request_status
+ self.log.debug("IPA-RA: request_status: '%s'" % request_status)
+ serial_number = self.__find_substring(data, 'record.serialNumber="', '"')
+ if serial_number is not None:
+ return_values["serial_number"] = "0x"+serial_number
+ request_id = self.__find_substring(data, 'header.requestId = "', '"')
+ if request_id is not None:
+ return_values["request_id"] = request_id
+ error = self.__find_substring(data, 'fixed.unexpectedError = "', '"')
+ if error is not None:
+ return_values["error"] = error
+ if return_values.has_key("status") is False:
+ return_values["status"] = "2"
+ else:
+ return_values["status"] = "1"
+ return return_values
+
+
+ def get_certificate(self, serial_number=None):
+ """
+ Retrieve an existing certificate
+ :param serial_number: certificate serial number
+ """
+ self.log.debug("IPA-RA: get_certificate")
+ issued_certificate = None
+ return_values = {}
+ if serial_number is not None:
+ request_info = ("serialNumber=%s" % serial_number)
+ self.log.debug("request_info: '%s'" % request_info)
+ returncode, stdout, stderr = self.__run_sslget(["-e", request_info, "-r", "/ca/agent/ca/displayBySerial", self.ca_host+":"+str(self.ca_ssl_port)])
+ self.log.debug("IPA-RA: returncode: %d" % returncode)
+ if (returncode == 0):
+ issued_certificate = self.__find_substring(stdout, 'header.certChainBase64 = "', '"')
+ if issued_certificate is not None:
+ return_values["status"] = "0"
+ issued_certificate = issued_certificate.replace("\\r", "")
+ issued_certificate = issued_certificate.replace("\\n", "")
+ self.log.debug("IPA-RA: issued_certificate: '%s'" % issued_certificate)
+ return_values["certificate"] = issued_certificate
+ else:
+ return_values["status"] = "1"
+ revocation_reason = self.__find_substring(stdout, 'header.revocationReason = ', ';')
+ if revocation_reason is not None:
+ return_values["revocation_reason"] = revocation_reason
+ else:
+ return_values["status"] = str(-returncode)
+ else:
+ return_values["status"] = "1"
+ return return_values
+
+
+ def request_certificate(self, certificate_request=None, request_type="pkcs10"):
+ """
+ Submit certificate request
+ :param certificate_request: certificate request
+ :param request_type: request type
+ """
+ self.log.debug("IPA-RA: request_certificate")
+ certificate = None
+ return_values = {}
+ if request_type is None:
+ request_type="pkcs10"
+ if certificate_request is not None:
+ request = urllib.quote(certificate_request)
+ request_info = "profileId=caRAserverCert&cert_request_type="+request_type+"&cert_request="+request+"&xmlOutput=true"
+ returncode, stdout, stderr = self.__run_sslget(["-e", request_info, "-r", "/ca/ee/ca/profileSubmit", self.ca_host+":"+str(self.ca_ssl_port)])
+ self.log.debug("IPA-RA: returncode: %d" % returncode)
+ if (returncode == 0):
+ status = self.__find_substring(stdout, "<Status>", "</Status>")
+ if status is not None:
+ self.log.debug ("status=%s" % status)
+ return_values["status"] = status
+ request_id = self.__find_substring(stdout, "<Id>", "</Id>")
+ if request_id is not None:
+ self.log.debug ("request_id=%s" % request_id)
+ return_values["request_id"] = request_id
+ serial_number = self.__find_substring(stdout, "<serialno>", "</serialno>")
+ if serial_number is not None:
+ self.log.debug ("serial_number=%s" % serial_number)
+ return_values["serial_number"] = ("0x%s" % serial_number)
+ subject = self.__find_substring(stdout, "<SubjectDN>", "</SubjectDN>")
+ if subject is not None:
+ self.log.debug ("subject=%s" % subject)
+ return_values["subject"] = subject
+ certificate = self.__find_substring(stdout, "<b64>", "</b64>")
+ if certificate is not None:
+ self.log.debug ("certificate=%s" % certificate)
+ return_values["certificate"] = certificate
+ if return_values.has_key("status") is False:
+ return_values["status"] = "2"
+ else:
+ return_values["status"] = str(-returncode)
+ else:
+ return_values["status"] = "1"
+ return return_values
+
+
+ def revoke_certificate(self, serial_number=None, revocation_reason=0):
+ """
+ Revoke a certificate
+ :param serial_number: certificate serial number
+ :param revocation_reason: revocation reason
+ revocationr reasons: 0 - unspecified
+ 1 - key compromise
+ 2 - ca compromise
+ 3 - affiliation changed
+ 4 - superseded
+ 5 - cessation of operation
+ 6 - certificate hold
+ 7 - value 7 is not used
+ 8 - remove from CRL
+ 9 - privilege withdrawn
+ 10 - aa compromise
+ see RFC 5280 for more details
+ """
+ return_values = {}
+ self.log.debug("IPA-RA: revoke_certificate")
+ if revocation_reason is None:
+ revocation_reason = 0
+ if serial_number is not None:
+ if isinstance(serial_number, int):
+ serial_number = str(serial_number)
+ if isinstance(revocation_reason, int):
+ revocation_reason = str(revocation_reason)
+ request_info = "op=revoke&revocationReason="+revocation_reason+"&revokeAll=(certRecordId%3D"+serial_number+")&totalRecordCount=1"
+ returncode, stdout, stderr = self.__run_sslget(["-e", request_info, "-r", "/ca/agent/ca/doRevoke", self.ca_host+":"+str(self.ca_ssl_port)])
+ api.log.debug("IPA-RA: returncode: %d" % returncode)
+ if (returncode == 0):
+ return_values["status"] = "0"
+ if (stdout.find('revoked = "yes"') > -1):
+ return_values["revoked"] = True
+ else:
+ return_values["revoked"] = False
+ else:
+ return_values["status"] = str(-returncode)
+ else:
+ return_values["status"] = "1"
+ return return_values
+
+
+ def take_certificate_off_hold(self, serial_number=None):
+ """
+ Take revoked certificate off hold
+ :param serial_number: certificate serial number
+ """
+ return_values = {}
+ self.log.debug("IPA-RA: revoke_certificate")
+ if serial_number is not None:
+ if isinstance(serial_number, int):
+ serial_number = str(serial_number)
+ request_info = "serialNumber="+serial_number
+ returncode, stdout, stderr = self.__run_sslget(["-e", request_info, "-r", "/ca/agent/ca/doUnrevoke", self.ca_host+":"+str(self.ca_ssl_port)])
+ api.log.debug("IPA-RA: returncode: %d" % returncode)
+ if (returncode == 0):
+ if (stdout.find('unrevoked = "yes"') > -1):
+ return_values["taken_off_hold"] = True
+ else:
+ return_values["taken_off_hold"] = False
+ else:
+ return_values["status"] = str(-returncode)
+ else:
+ return_values["status"] = "1"
+ return return_values
+
+
+ def __find_substring(self, str, str1, str2):
+ sub_str = None
+ k0 = len(str)
+ k1 = str.find(str1)
+ k2 = len(str1)
+ if (k0 > 0 and k1 > -1 and k2 > 0 and k0 > k1 + k2):
+ sub_str = str[k1+k2:]
+ k3 = len(sub_str)
+ k4 = sub_str.find(str2)
+ if (k3 > 0 and k4 > -1 and k3 > k4):
+ sub_str = sub_str[:k4]
+ return sub_str
+
+
+ def __get_ca_location(self):
+ if 'ca_host' in api.env:
+ api.log.debug("ca_host configuration found")
+ if api.env.ca_host is not None:
+ self.ca_host = api.env.ca_host
+ else:
+ api.log.debug("ca_host configuration not found")
+ # if CA is not hosted with IPA on the same system and there is no configuration support for 'api.env.ca_host', then set ca_host below
+ # self.ca_host = "example.com"
+ if self.ca_host is None:
+ self.ca_host = gethostname()
+ api.log.debug("ca_host: %s" % self.ca_host)
+
+ if 'ca_ssl_port' in api.env:
+ api.log.debug("ca_ssl_port configuration found")
+ if api.env.ca_ssl_port is not None:
+ self.ca_ssl_port = api.env.ca_ssl_port
+ else:
+ api.log.debug("ca_ssl_port configuration not found")
+ if self.ca_ssl_port is None:
+ self.ca_ssl_port = 9443
+ api.log.debug("ca_ssl_port: %d" % self.ca_ssl_port)
+
+ if 'ca_port' in api.env:
+ api.log.debug("ca_port configuration found")
+ if api.env.ca_port is not None:
+ self.ca_port = api.env.ca_port
+ else:
+ api.log.debug("ca_port configuration not found")
+ if self.ca_port is None:
+ self.ca_port = 9080
+ api.log.debug("ca_port: %d" % self.ca_port)
+
+
+ def __generate_ipa_request(self):
+ certificate_request = None
+ if not os.path.isfile(self.noise_file):
+ self.__create_noise_file()
+ returncode, stdout, stderr = self.__run_certutil(["-R", "-k", "rsa", "-g", self.ipa_key_size, "-s", "CN=IPA-Subsystem-Certificate,OU=pki-ipa,O=UsersysRedhat-Domain", "-z", self.noise_file, "-a"])
+ if os.path.isfile(self.noise_file):
+ os.unlink(self.noise_file)
+ if (returncode == 0):
+ api.log.info("IPA-RA: IPA certificate request generated")
+ certificate_request = self.__find_substring(stdout, "-----BEGIN NEW CERTIFICATE REQUEST-----", "-----END NEW CERTIFICATE REQUEST-----")
+ if certificate_request is not None:
+ api.log.debug("certificate_request=%s" % certificate_request)
+ else:
+ api.log.warn("IPA-RA: Error parsing certificate request." % returncode)
+ else:
+ api.log.warn("IPA-RA: Error (%d) generating IPA certificate request." % returncode)
+ return certificate_request
+
+ def __request_ipa_certificate(self, certificate_request=None):
+ ipa_certificate = None
+ if certificate_request is not None:
+ params = urllib.urlencode({'profileId': 'caServerCert', 'cert_request_type': 'pkcs10', 'requestor_name': 'freeIPA', 'cert_request': self.__generate_ipa_request(), 'xmlOutput': 'true'})
+ headers = {"Content-type": "application/x-www-form-urlencoded"}
+ conn = httplib.HTTPConnection(self.ca_host+":"+self.ca_port)
+ conn.request("POST", "/ca/ee/ca/profileSubmit", params, headers)
+ response = conn.getresponse()
+ api.log.debug("IPA-RA: response.status: %d response.reason: '%s'" % (response.status, response.reason))
+ data = response.read()
+ conn.close()
+ api.log.info("IPA-RA: IPA certificate request submitted to CA: %s" % data)
+ return ipa_certificate
+
+ def __get_ca_chain(self):
+ headers = {"Content-type": "application/x-www-form-urlencoded"}
+ conn = httplib.HTTPConnection(self.ca_host+":"+self.ca_port)
+ conn.request("POST", "/ca/ee/ca/getCertChain", None, headers)
+ response = conn.getresponse()
+ api.log.debug("IPA-RA: response.status: %d response.reason: '%s'" % (response.status, response.reason))
+ data = response.read()
+ conn.close()
+ certificate_chain = self.__find_substring(data, "<ChainBase64>", "</ChainBase64>")
+ if certificate_chain is not None:
+ api.log.info(("IPA-RA: CA chain obtained from CA: %s" % certificate_chain))
+ else:
+ api.log.warn("IPA-RA: Error parsing certificate chain.")
+ return certificate_chain
+
+ def __import_ca_chain(self):
+ returncode, stdout, stderr = self.__run_certutil(["-A", "-t", "CT,C,C", "-n", self.ca_certificate_nickname, "-a"], self.__get_ca_chain())
+ if (returncode == 0):
+ api.log.info("IPA-RA: CA chain imported to IPA's NSS DB")
+ else:
+ api.log.warn("IPA-RA: Error (%d) importing CA chain to IPA's NSS DB." % returncode)
+
+ def __create_noise_file(self):
+ noise = array.array('B', os.urandom(128))
+ f = open(self.noise_file, "wb")
+ noise.tofile(f)
+ f.close()
+
+ def __create_pwd_file(self):
+ hex_str = binascii.hexlify(os.urandom(10))
+ print "urandom: %s" % hex_str
+ f = os.open(self.pwd_file, os.O_CREAT | os.O_RDWR)
+ os.write(f, hex_str)
+ os.close(f)
+
+ def __create_nss_db(self):
+ returncode, stdout, stderr = self.__run_certutil(["-N"])
+ if (returncode == 0):
+ api.log.info("IPA-RA: NSS DB created")
+ else:
+ api.log.warn("IPA-RA: Error (%d) creating NSS DB." % returncode)
+
+ """
+ sslget and certutil utilities are used only till Python-NSS completion.
+ """
+ def __run_sslget(self, args, stdin=None):
+ new_args = ["/usr/bin/sslget", "-d", self.sec_dir, "-w", self.pwd_file, "-n", self.ipa_certificate_nickname]
+ new_args = new_args + args
+ return self.__run(new_args, stdin)
+
+ def __run_certutil(self, args, stdin=None):
+ new_args = ["/usr/bin/certutil", "-d", self.sec_dir, "-f", self.pwd_file]
+ new_args = new_args + args
+ return self.__run(new_args, stdin)
+
+ def __run(self, args, stdin=None):
+ if stdin:
+ p = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True)
+ stdout,stderr = p.communicate(stdin)
+ else:
+ p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True)
+ stdout,stderr = p.communicate()
+
+ api.log.debug("IPA-RA: returncode: %d args: '%s'" % (p.returncode, ' '.join(args)))
+ # api.log.debug("IPA-RA: stdout: '%s'" % stdout)
+ # api.log.debug("IPA-RA: stderr: '%s'" % stderr)
+ return (p.returncode, stdout, stderr)
+
+api.register(ra)
diff --git a/ipaserver/rpcserver.py b/ipaserver/rpcserver.py
new file mode 100644
index 000000000..225173675
--- /dev/null
+++ b/ipaserver/rpcserver.py
@@ -0,0 +1,67 @@
+# 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
+
+"""
+RPC server.
+
+Also see the `ipalib.rpc` module.
+"""
+
+from xmlrpclib import Fault
+from ipalib import Backend
+from ipalib.errors2 import PublicError, InternalError, CommandError
+from ipalib.rpc import xml_dumps, xml_loads
+
+
+def params_2_args_options(params):
+ assert type(params) is tuple
+ if len(params) == 0:
+ return (tuple(), dict())
+ if type(params[-1]) is dict:
+ return (params[:-1], params[-1])
+ return (params, dict())
+
+
+class xmlserver(Backend):
+ """
+ Execution backend for XML-RPC server.
+ """
+
+ def dispatch(self, method, params):
+ self.debug('Received RPC call to %r', method)
+ if method not in self.Command:
+ raise CommandError(name=method)
+ (args, options) = params_2_args_options(params)
+ result = self.Command[method](*args, **options)
+ return (result,) # Must wrap XML-RPC response in a tuple singleton
+
+ def execute(self, data, ccache=None, client_version=None,
+ client_ip=None, languages=None):
+ """
+ Execute the XML-RPC request in contained in ``data``.
+ """
+ try:
+ (params, method) = xml_loads(data)
+ response = self.dispatch(method, params)
+ except Exception, e:
+ if not isinstance(e, PublicError):
+ e = InternalError()
+ assert isinstance(e, PublicError)
+ response = Fault(e.errno, e.strerror)
+ return dumps(response)
diff --git a/ipaserver/servercore.py b/ipaserver/servercore.py
new file mode 100644
index 000000000..362013401
--- /dev/null
+++ b/ipaserver/servercore.py
@@ -0,0 +1,467 @@
+# Authors: Rob Crittenden <rcritten@redhat.com>
+#
+# Copyright (C) 2007 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
+#
+
+import ldap
+import string
+import re
+from ipaserver.context import context
+from ipaserver import ipaldap
+import ipautil
+from ipalib import errors
+from ipalib import api
+
+def convert_entry(ent):
+ entry = dict(ent.data)
+ entry['dn'] = ent.dn
+ # For now convert single entry lists to a string for the ui.
+ # TODO: we need to deal with multi-values better
+ for key,value in entry.iteritems():
+ if isinstance(value,list) or isinstance(value,tuple):
+ if len(value) == 0:
+ entry[key] = ''
+ elif len(value) == 1:
+ entry[key] = value[0]
+ return entry
+
+def convert_scalar_values(orig_dict):
+ """LDAP update dicts expect all values to be a list (except for dn).
+ This method converts single entries to a list."""
+ new_dict={}
+ for (k,v) in orig_dict.iteritems():
+ if not isinstance(v, list) and k != 'dn':
+ v = [v]
+ new_dict[k] = v
+
+ return new_dict
+
+def generate_match_filters(search_fields, criteria_words):
+ """Generates a search filter based on a list of words and a list
+ of fields to search against.
+
+ Returns a tuple of two filters: (exact_match, partial_match)"""
+
+ # construct search pattern for a single word
+ # (|(f1=word)(f2=word)...)
+ search_pattern = "(|"
+ for field in search_fields:
+ search_pattern += "(" + field + "=%(match)s)"
+ search_pattern += ")"
+ gen_search_pattern = lambda word: search_pattern % {'match':word}
+
+ # construct the giant match for all words
+ exact_match_filter = "(&"
+ partial_match_filter = "(|"
+ for word in criteria_words:
+ exact_match_filter += gen_search_pattern(word)
+ partial_match_filter += gen_search_pattern("*%s*" % word)
+ exact_match_filter += ")"
+ partial_match_filter += ")"
+
+ return (exact_match_filter, partial_match_filter)
+
+# TODO: rethink the get_entry vs get_list API calls.
+# they currently restrict the data coming back without
+# restricting scope. For now adding a get_base/sub_entry()
+# calls, but the API isn't great.
+def get_entry (base, scope, searchfilter, sattrs=None):
+ """Get a specific entry (with a parametized scope).
+ Return as a dict of values.
+ Multi-valued fields are represented as lists.
+ """
+ ent=""
+
+ ent = context.conn.getConn().getEntry(base, scope, searchfilter, sattrs)
+
+ return convert_entry(ent)
+
+def get_base_entry (base, searchfilter, sattrs=None):
+ """Get a specific entry (with a scope of BASE).
+ Return as a dict of values.
+ Multi-valued fields are represented as lists.
+ """
+ return get_entry(base, ldap.SCOPE_BASE, searchfilter, sattrs)
+
+def get_sub_entry (base, searchfilter, sattrs=None):
+ """Get a specific entry (with a scope of SUB).
+ Return as a dict of values.
+ Multi-valued fields are represented as lists.
+ """
+ return get_entry(base, ldap.SCOPE_SUBTREE, searchfilter, sattrs)
+
+def get_one_entry (base, searchfilter, sattrs=None):
+ """Get the children of an entry (with a scope of ONE).
+ Return as a list of dict of values.
+ Multi-valued fields are represented as lists.
+ """
+ return get_list(base, searchfilter, sattrs, ldap.SCOPE_ONELEVEL)
+
+def get_list (base, searchfilter, sattrs=None, scope=ldap.SCOPE_SUBTREE):
+ """Gets a list of entries. Each is converted to a dict of values.
+ Multi-valued fields are represented as lists.
+ """
+ entries = []
+
+ entries = context.conn.getConn().getList(base, scope, searchfilter, sattrs)
+
+ return map(convert_entry, entries)
+
+def has_nsaccountlock(dn):
+ """Check to see if an entry has the nsaccountlock attribute.
+ This attribute is provided by the Class of Service plugin so
+ doing a search isn't enough. It is provided by the two
+ entries cn=inactivated and cn=activated. So if the entry has
+ the attribute and isn't in either cn=activated or cn=inactivated
+ then the attribute must be in the entry itself.
+
+ Returns True or False
+ """
+ # First get the entry. If it doesn't have nsaccountlock at all we
+ # can exit early.
+ entry = get_entry_by_dn(dn, ['dn', 'nsaccountlock', 'memberof'])
+ if not entry.get('nsaccountlock'):
+ return False
+
+ # Now look to see if they are in activated or inactivated
+ # entry is a member
+ memberof = entry.get('memberof')
+ if isinstance(memberof, basestring):
+ memberof = [memberof]
+ for m in memberof:
+ inactivated = m.find("cn=inactivated")
+ activated = m.find("cn=activated")
+ # if they are in either group that means that the nsaccountlock
+ # value comes from there, otherwise it must be in this entry.
+ if inactivated >= 0 or activated >= 0:
+ return False
+
+ return True
+
+# General searches
+
+def get_entry_by_dn (dn, sattrs=None):
+ """Get a specific entry. Return as a dict of values.
+ Multi-valued fields are represented as lists.
+ """
+ searchfilter = "(objectClass=*)"
+ api.log.info("IPA: get_entry_by_dn '%s'" % dn)
+ return get_base_entry(dn, searchfilter, sattrs)
+
+def get_entry_by_cn (cn, sattrs):
+ """Get a specific entry by cn. Return as a dict of values.
+ Multi-valued fields are represented as lists.
+ """
+ api.log.info("IPA: get_entry_by_cn '%s'" % cn)
+# cn = self.__safe_filter(cn)
+ searchfilter = "(cn=%s)" % cn
+ return get_sub_entry("cn=accounts," + api.env.basedn, searchfilter, sattrs)
+
+def get_user_by_uid(uid, sattrs):
+ """Get a specific user's entry."""
+ # FIXME: should accept a container to look in
+# uid = self.__safe_filter(uid)
+ searchfilter = "(&(uid=%s)(objectclass=posixAccount))" % uid
+
+ return get_sub_entry("cn=accounts," + api.env.basedn, searchfilter, sattrs)
+
+# User support
+
+def entry_exists(dn):
+ """Return True if the entry exists, False otherwise."""
+ try:
+ get_base_entry(dn, "objectclass=*", ['dn','objectclass'])
+ return True
+ except errors.NotFound:
+ return False
+
+def get_user_by_uid (uid, sattrs):
+ """Get a specific user's entry. Return as a dict of values.
+ Multi-valued fields are represented as lists.
+ """
+
+ if not isinstance(uid,basestring) or len(uid) == 0:
+ raise SyntaxError("uid is not a string")
+# raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ if sattrs is not None and not isinstance(sattrs,list):
+ raise SyntaxError("sattrs is not a list")
+# raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER)
+ api.log.info("IPA: get_user_by_uid '%s'" % uid)
+# uid = self.__safe_filter(uid)
+ searchfilter = "(uid=" + uid + ")"
+ return get_sub_entry("cn=accounts," + api.env.basedn, searchfilter, sattrs)
+
+def uid_too_long(uid):
+ """Verify that the new uid is within the limits we set. This is a
+ very narrow test.
+
+ Returns True if it is longer than allowed
+ False otherwise
+ """
+ if not isinstance(uid,basestring) or len(uid) == 0:
+ # It is bad, but not too long
+ return False
+ api.log.debug("IPA: __uid_too_long(%s)" % uid)
+ try:
+ config = get_ipa_config()
+ maxlen = int(config.get('ipamaxusernamelength', 0))
+ if maxlen > 0 and len(uid) > maxlen:
+ return True
+ except Exception, e:
+ api.log.debug("There was a problem " + str(e))
+ pass
+
+ return False
+
+def update_entry (entry, remove_keys=[]):
+ """Update an LDAP entry
+
+ entry is a dict
+ remove_keys is a list of attributes to remove from this entry
+
+ This refreshes the record from LDAP in order to obtain the list of
+ attributes that has changed. It only retrieves the attributes that
+ are in the update so attributes aren't inadvertantly lost.
+ """
+ assert type(remove_keys) is list
+ attrs = entry.keys()
+ o = get_base_entry(entry['dn'], "objectclass=*", attrs + remove_keys)
+ oldentry = convert_scalar_values(o)
+ newentry = convert_scalar_values(entry)
+
+ # Should be able to get this from either the old or new entry
+ # but just in case someone has decided to try changing it, use the
+ # original
+ try:
+ moddn = oldentry['dn']
+ except KeyError, e:
+ # FIXME: return a missing DN error message
+ raise e
+
+ return context.conn.getConn().updateEntry(moddn, oldentry, newentry)
+
+def add_entry(entry):
+ """Add a new entry"""
+ return context.conn.getConn().addEntry(entry)
+
+def delete_entry(dn):
+ """Remove an entry"""
+ return context.conn.getConn().deleteEntry(dn)
+
+# FIXME, get time and search limit from cn=ipaconfig
+def search(base, filter, attributes, timelimit=1, sizelimit=3000):
+ """Perform an LDAP query"""
+ try:
+ timelimit = float(timelimit)
+ results = context.conn.getConn().getListAsync(base, ldap.SCOPE_SUBTREE,
+ filter, attributes, 0, None, None, timelimit, sizelimit)
+ except ldap.NO_SUCH_OBJECT:
+ raise errors.NotFound
+
+ counter = results[0]
+ entries = [counter]
+ for r in results[1:]:
+ entries.append(convert_entry(r))
+
+ return entries
+
+def uniq_list(x):
+ """Return a unique list, preserving order and ignoring case"""
+ myset = {}
+ return [myset.setdefault(e.lower(),e) for e in x if e.lower() not in myset]
+
+def get_schema():
+ """Retrieves the current LDAP schema from the LDAP server."""
+
+ schema_entry = get_base_entry("", "objectclass=*", ['dn','subschemasubentry'])
+ schema_cn = schema_entry.get('subschemasubentry')
+ schema = get_base_entry(schema_cn, "objectclass=*", ['*'])
+
+ return schema
+
+def get_objectclasses():
+ """Returns a list of available objectclasses that the LDAP
+ server supports. This parses out the syntax, attributes, etc
+ and JUST returns a lower-case list of the names."""
+
+ schema = get_schema()
+
+ objectclasses = schema.get('objectclasses')
+
+ # Convert this list into something more readable
+ result = []
+ for i in range(len(objectclasses)):
+ oc = objectclasses[i].lower().split(" ")
+ result.append(oc[3].replace("'",""))
+
+ return result
+
+def get_ipa_config():
+ """Retrieve the IPA configuration"""
+ searchfilter = "cn=ipaconfig"
+ try:
+ config = get_sub_entry("cn=etc," + api.env.basedn, searchfilter)
+ except ldap.NO_SUCH_OBJECT, e:
+ # FIXME
+ raise errors.NotFound
+
+ return config
+
+def modify_password(dn, oldpass, newpass):
+ return context.conn.getConn().modifyPassword(dn, oldpass, newpass)
+
+def mark_entry_active (dn):
+ """Mark an entry as active in LDAP."""
+
+ # This can be tricky. The entry itself can be marked inactive
+ # by being in the inactivated group. It can also be inactivated by
+ # being the member of an inactive group.
+ #
+ # First we try to remove the entry from the inactivated group. Then
+ # if it is still inactive we have to add it to the activated group
+ # which will override the group membership.
+
+ res = ""
+ # First, check the entry status
+ entry = get_entry_by_dn(dn, ['dn', 'nsAccountlock'])
+
+ if entry.get('nsaccountlock', 'false').lower() == "false":
+ api.log.debug("IPA: already active")
+ raise errors.AlreadyActiveError
+
+ if has_nsaccountlock(dn):
+ api.log.debug("IPA: appears to have the nsaccountlock attribute")
+ raise errors.HasNSAccountLock
+
+ group = get_entry_by_cn("inactivated", None)
+ try:
+ remove_member_from_group(entry.get('dn'), group.get('dn'))
+ except errors.NotGroupMember:
+ # Perhaps the user is there as a result of group membership
+ pass
+
+ # Now they aren't a member of inactivated directly, what is the status
+ # now?
+ entry = get_entry_by_dn(dn, ['dn', 'nsAccountlock'])
+
+ if entry.get('nsaccountlock', 'false').lower() == "false":
+ # great, we're done
+ api.log.debug("IPA: removing from inactivated did it.")
+ return True
+
+ # So still inactive, add them to activated
+ group = get_entry_by_cn("activated", None)
+ res = add_member_to_group(dn, group.get('dn'))
+ api.log.debug("IPA: added to activated.")
+
+ return res
+
+def mark_entry_inactive (dn):
+ """Mark an entry as inactive in LDAP."""
+
+ entry = get_entry_by_dn(dn, ['dn', 'nsAccountlock', 'memberOf'])
+
+ if entry.get('nsaccountlock', 'false').lower() == "true":
+ api.log.debug("IPA: already marked as inactive")
+ raise errors.AlreadyInactiveError
+
+ if has_nsaccountlock(dn):
+ api.log.debug("IPA: appears to have the nsaccountlock attribute")
+ raise errors.HasNSAccountLock
+
+ # First see if they are in the activated group as this will override
+ # the our inactivation.
+ group = get_entry_by_cn("activated", None)
+ try:
+ remove_member_from_group(dn, group.get('dn'))
+ except errors.NotGroupMember:
+ # this is fine, they may not be explicitly in this group
+ pass
+
+ # Now add them to inactivated
+ group = get_entry_by_cn("inactivated", None)
+ res = add_member_to_group(dn, group.get('dn'))
+
+ return res
+
+def add_member_to_group(member_dn, group_dn, memberattr='member'):
+ """
+ Add a member to an existing group.
+ """
+ api.log.info("IPA: add_member_to_group '%s' to '%s'" % (member_dn, group_dn))
+ if member_dn.lower() == group_dn.lower():
+ # You can't add a group to itself
+ raise errors.SameGroupError
+
+ group = get_entry_by_dn(group_dn, None)
+ if group is None:
+ raise errors.NotFound
+
+ # check to make sure member_dn exists
+ member_entry = get_base_entry(member_dn, "(objectClass=*)", ['dn','objectclass'])
+ if not member_entry:
+ raise errors.NotFound
+
+ # Add the new member to the group member attribute
+ members = group.get(memberattr, [])
+ if isinstance(members, basestring):
+ members = [members]
+ members.append(member_dn)
+ group[memberattr] = members
+
+ try:
+ return update_entry(group)
+ except errors.EmptyModlist:
+ raise
+
+def remove_member_from_group(member_dn, group_dn, memberattr='member'):
+ """Remove a member_dn from an existing group."""
+
+ group = get_entry_by_dn(group_dn, None)
+ if group is None:
+ raise errors.NotFound
+ """
+ if group.get('cn') == "admins":
+ member = get_entry_by_dn(member_dn, ['dn','uid'])
+ if member.get('uid') == "admin":
+ raise ipaerror.gen_exception(ipaerror.INPUT_ADMIN_REQUIRED_IN_ADMINS)
+ """
+ api.log.info("IPA: remove_member_from_group '%s' from '%s'" % (member_dn, group_dn))
+
+ members = group.get(memberattr, False)
+ if not members:
+ raise errors.NotGroupMember
+
+ if isinstance(members,basestring):
+ members = [members]
+ for i in range(len(members)):
+ members[i] = ipaldap.IPAdmin.normalizeDN(members[i])
+ try:
+ members.remove(member_dn)
+ except ValueError:
+ # member is not in the group
+ # FIXME: raise more specific error?
+ raise errors.NotGroupMember
+ except Exception, e:
+ raise e
+
+ group[memberattr] = members
+
+ try:
+ return update_entry(group)
+ except errors.EmptyModlist:
+ raise
diff --git a/ipaserver/test_client b/ipaserver/test_client
new file mode 100755
index 000000000..3b4794d95
--- /dev/null
+++ b/ipaserver/test_client
@@ -0,0 +1,28 @@
+#!/usr/bin/python
+
+import xmlrpclib
+
+def user_find(uid):
+ try:
+ args=uid
+ result = server.user_find(args)
+ print "returned %s" % result
+ except xmlrpclib.Fault, e:
+ print e.faultString
+
+# main
+server = xmlrpclib.ServerProxy("http://localhost:8888/")
+
+#print server.system.listMethods()
+#print server.system.methodHelp("user_add")
+
+try:
+ args="jsmith1"
+ kw = {'givenname':'Joe', 'sn':'Smith'}
+ result = server.user_add(kw, args)
+ print "returned %s" % result
+except xmlrpclib.Fault, e:
+ print e.faultString
+
+#user_find("admin")
+#user_find("notfound")
diff --git a/ipaserver/updates/automount.update b/ipaserver/updates/automount.update
new file mode 100644
index 000000000..13d9a6df0
--- /dev/null
+++ b/ipaserver/updates/automount.update
@@ -0,0 +1,54 @@
+#
+# An automount schema based on RFC 2307-bis.
+#
+# This schema defines new automount and automountMap objectclasses to represent
+# the automount maps and their entries.
+#
+dn: cn=schema
+add:attributeTypes:
+ ( 1.3.6.1.1.1.1.31 NAME 'automountMapName'
+ DESC 'automount Map Name'
+ EQUALITY caseExactIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE
+ X-ORIGIN 'RFC 2307bis' )
+add:attributeTypes:
+ ( 1.3.6.1.1.1.1.32 NAME 'automountKey'
+ DESC 'Automount Key value'
+ EQUALITY caseExactIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE
+ X-ORIGIN 'RFC 2307bis' )
+add:attributeTypes:
+ ( 1.3.6.1.1.1.1.33 NAME 'automountInformation'
+ DESC 'Automount information'
+ EQUALITY caseExactIA5Match
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE
+ X-ORIGIN 'RFC 2307bis' )
+add:objectClasses:
+ ( 1.3.6.1.1.1.2.16 NAME 'automountMap'
+ DESC 'Automount Map information' SUP top
+ STRUCTURAL MUST automountMapName MAY description
+ X-ORIGIN 'RFC 2307bis' )
+add:objectClasses:
+ ( 1.3.6.1.1.1.2.17 NAME 'automount'
+ DESC 'Automount information' SUP top STRUCTURAL
+ MUST ( automountKey $ automountInformation ) MAY description
+ X-ORIGIN 'RFC 2307bis' )
+
+# Add the default automount entries
+
+dn: cn=automount,$SUFFIX
+add:objectClass: nsContainer
+add:cn: automount
+
+dn: automountmapname=auto.master,cn=automount,$SUFFIX
+add:objectClass: automountMap
+add:automountMapName: auto.master
+
+dn: automountkey=/-,automountmapname=auto.master,cn=automount,$SUFFIX
+add:objectClass: automount
+add:automountKey: '/-'
+add:automountInformation: auto.direct
+
+dn: automountmapname=auto.direct,cn=automount,$SUFFIX
+add:objectClass: automountMap
+add:automountMapName: auto.direct
diff --git a/ipaserver/updates/groupofhosts.update b/ipaserver/updates/groupofhosts.update
new file mode 100644
index 000000000..fb39c5e25
--- /dev/null
+++ b/ipaserver/updates/groupofhosts.update
@@ -0,0 +1,5 @@
+dn: cn=hostgroups,cn=accounts,$SUFFIX
+add:objectClass: top
+add:objectClass: nsContainer
+add:cn: hostgroups
+
diff --git a/ipaserver/updates/host.update b/ipaserver/updates/host.update
new file mode 100644
index 000000000..f5ecda5ac
--- /dev/null
+++ b/ipaserver/updates/host.update
@@ -0,0 +1,25 @@
+#
+# Schema for IPA Hosts
+#
+dn: cn=schema
+add: attributeTypes:
+ ( 2.16.840.1.113730.3.8.3.10 NAME 'ipaClientVersion'
+ DESC 'Text string describing client version of the IPA software installed'
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
+ X-ORIGIN 'IPA v2' )
+
+add: attributeTypes:
+ ( 2.16.840.1.113730.3.8.3.11 NAME 'enrolledBy'
+ DESC 'DN of administrator who performed manual enrollment of the host'
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.12
+ X-ORIGIN 'IPA v2' )
+add: objectClasses:
+ ( 2.16.840.1.113730.3.8.4.2 NAME 'ipaHost'
+ AUXILIARY
+ MAY ( userPassword $ ipaClientVersion $ enrolledBy)
+ X-ORIGIN 'IPA v2' )
+add: objectClasses:
+ ( 2.5.6.21 NAME 'pkiUser'
+ SUP top AUXILIARY
+ MAY ( userCertificate )
+ X-ORIGIN 'RFC 2587' )
diff --git a/ipawebui/__init__.py b/ipawebui/__init__.py
new file mode 100644
index 000000000..408481a27
--- /dev/null
+++ b/ipawebui/__init__.py
@@ -0,0 +1,24 @@
+# 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
+
+"""
+Package containing web-based UI components.
+"""
+
+import kid
+kid.enable_import()
diff --git a/ipawebui/controller.py b/ipawebui/controller.py
new file mode 100644
index 000000000..a2a270cbd
--- /dev/null
+++ b/ipawebui/controller.py
@@ -0,0 +1,71 @@
+# 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
+
+"""
+Controller classes.
+"""
+
+import simplejson
+from ipalib.plugable import ReadOnly, lock
+
+
+class Controller(ReadOnly):
+ exposed = True
+
+ def __init__(self, template=None):
+ self.template = template
+ lock(self)
+
+ def output_xhtml(self, **kw):
+ return self.template.serialize(
+ output='xhtml-strict',
+ format='pretty',
+ **kw
+ )
+
+ def output_json(self, **kw):
+ return simplejson.dumps(kw, sort_keys=True, indent=4)
+
+ def __call__(self, **kw):
+ json = bool(kw.pop('_format', None) == 'json')
+ result = self.run(**kw)
+ assert type(result) is dict
+ if json or self.template is None:
+ return self.output_json(**result)
+ return self.output_xhtml(**result)
+
+ def run(self, **kw):
+ return {}
+
+
+class Command(Controller):
+ def __init__(self, command, template=None):
+ self.command = command
+ super(Command, self).__init__(template)
+
+ def run(self, **kw):
+ return dict(command=self.command)
+
+
+class Index(Controller):
+ def __init__(self, api, template=None):
+ self.api = api
+ super(Index, self).__init__(template)
+
+ def run(self):
+ return dict(api=self.api)
diff --git a/ipawebui/mod_python_webui.py b/ipawebui/mod_python_webui.py
new file mode 100644
index 000000000..6a889fce6
--- /dev/null
+++ b/ipawebui/mod_python_webui.py
@@ -0,0 +1,22 @@
+# 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
+
+"""
+Production Web UI using mod_python.
+"""
diff --git a/ipawebui/static/mootools-core.js b/ipawebui/static/mootools-core.js
new file mode 100644
index 000000000..7e1e482bf
--- /dev/null
+++ b/ipawebui/static/mootools-core.js
@@ -0,0 +1,3946 @@
+/*
+Script: Core.js
+ MooTools - My Object Oriented JavaScript Tools.
+
+License:
+ MIT-style license.
+
+Copyright:
+ Copyright (c) 2006-2008 [Valerio Proietti](http://mad4milk.net/).
+
+Code & Documentation:
+ [The MooTools production team](http://mootools.net/developers/).
+
+Inspiration:
+ - Class implementation inspired by [Base.js](http://dean.edwards.name/weblog/2006/03/base/) Copyright (c) 2006 Dean Edwards, [GNU Lesser General Public License](http://opensource.org/licenses/lgpl-license.php)
+ - Some functionality inspired by [Prototype.js](http://prototypejs.org) Copyright (c) 2005-2007 Sam Stephenson, [MIT License](http://opensource.org/licenses/mit-license.php)
+*/
+
+var MooTools = {
+ 'version': '1.2.1',
+ 'build': '0d4845aab3d9a4fdee2f0d4a6dd59210e4b697cf'
+};
+
+var Native = function(options){
+ options = options || {};
+ var name = options.name;
+ var legacy = options.legacy;
+ var protect = options.protect;
+ var methods = options.implement;
+ var generics = options.generics;
+ var initialize = options.initialize;
+ var afterImplement = options.afterImplement || function(){};
+ var object = initialize || legacy;
+ generics = generics !== false;
+
+ object.constructor = Native;
+ object.$family = {name: 'native'};
+ if (legacy && initialize) object.prototype = legacy.prototype;
+ object.prototype.constructor = object;
+
+ if (name){
+ var family = name.toLowerCase();
+ object.prototype.$family = {name: family};
+ Native.typize(object, family);
+ }
+
+ var add = function(obj, name, method, force){
+ if (!protect || force || !obj.prototype[name]) obj.prototype[name] = method;
+ if (generics) Native.genericize(obj, name, protect);
+ afterImplement.call(obj, name, method);
+ return obj;
+ };
+
+ object.alias = function(a1, a2, a3){
+ if (typeof a1 == 'string'){
+ if ((a1 = this.prototype[a1])) return add(this, a2, a1, a3);
+ }
+ for (var a in a1) this.alias(a, a1[a], a2);
+ return this;
+ };
+
+ object.implement = function(a1, a2, a3){
+ if (typeof a1 == 'string') return add(this, a1, a2, a3);
+ for (var p in a1) add(this, p, a1[p], a2);
+ return this;
+ };
+
+ if (methods) object.implement(methods);
+
+ return object;
+};
+
+Native.genericize = function(object, property, check){
+ if ((!check || !object[property]) && typeof object.prototype[property] == 'function') object[property] = function(){
+ var args = Array.prototype.slice.call(arguments);
+ return object.prototype[property].apply(args.shift(), args);
+ };
+};
+
+Native.implement = function(objects, properties){
+ for (var i = 0, l = objects.length; i < l; i++) objects[i].implement(properties);
+};
+
+Native.typize = function(object, family){
+ if (!object.type) object.type = function(item){
+ return ($type(item) === family);
+ };
+};
+
+(function(){
+ var natives = {'Array': Array, 'Date': Date, 'Function': Function, 'Number': Number, 'RegExp': RegExp, 'String': String};
+ for (var n in natives) new Native({name: n, initialize: natives[n], protect: true});
+
+ var types = {'boolean': Boolean, 'native': Native, 'object': Object};
+ for (var t in types) Native.typize(types[t], t);
+
+ var generics = {
+ 'Array': ["concat", "indexOf", "join", "lastIndexOf", "pop", "push", "reverse", "shift", "slice", "sort", "splice", "toString", "unshift", "valueOf"],
+ 'String': ["charAt", "charCodeAt", "concat", "indexOf", "lastIndexOf", "match", "replace", "search", "slice", "split", "substr", "substring", "toLowerCase", "toUpperCase", "valueOf"]
+ };
+ for (var g in generics){
+ for (var i = generics[g].length; i--;) Native.genericize(window[g], generics[g][i], true);
+ };
+})();
+
+var Hash = new Native({
+
+ name: 'Hash',
+
+ initialize: function(object){
+ if ($type(object) == 'hash') object = $unlink(object.getClean());
+ for (var key in object) this[key] = object[key];
+ return this;
+ }
+
+});
+
+Hash.implement({
+
+ forEach: function(fn, bind){
+ for (var key in this){
+ if (this.hasOwnProperty(key)) fn.call(bind, this[key], key, this);
+ }
+ },
+
+ getClean: function(){
+ var clean = {};
+ for (var key in this){
+ if (this.hasOwnProperty(key)) clean[key] = this[key];
+ }
+ return clean;
+ },
+
+ getLength: function(){
+ var length = 0;
+ for (var key in this){
+ if (this.hasOwnProperty(key)) length++;
+ }
+ return length;
+ }
+
+});
+
+Hash.alias('forEach', 'each');
+
+Array.implement({
+
+ forEach: function(fn, bind){
+ for (var i = 0, l = this.length; i < l; i++) fn.call(bind, this[i], i, this);
+ }
+
+});
+
+Array.alias('forEach', 'each');
+
+function $A(iterable){
+ if (iterable.item){
+ var array = [];
+ for (var i = 0, l = iterable.length; i < l; i++) array[i] = iterable[i];
+ return array;
+ }
+ return Array.prototype.slice.call(iterable);
+};
+
+function $arguments(i){
+ return function(){
+ return arguments[i];
+ };
+};
+
+function $chk(obj){
+ return !!(obj || obj === 0);
+};
+
+function $clear(timer){
+ clearTimeout(timer);
+ clearInterval(timer);
+ return null;
+};
+
+function $defined(obj){
+ return (obj != undefined);
+};
+
+function $each(iterable, fn, bind){
+ var type = $type(iterable);
+ ((type == 'arguments' || type == 'collection' || type == 'array') ? Array : Hash).each(iterable, fn, bind);
+};
+
+function $empty(){};
+
+function $extend(original, extended){
+ for (var key in (extended || {})) original[key] = extended[key];
+ return original;
+};
+
+function $H(object){
+ return new Hash(object);
+};
+
+function $lambda(value){
+ return (typeof value == 'function') ? value : function(){
+ return value;
+ };
+};
+
+function $merge(){
+ var mix = {};
+ for (var i = 0, l = arguments.length; i < l; i++){
+ var object = arguments[i];
+ if ($type(object) != 'object') continue;
+ for (var key in object){
+ var op = object[key], mp = mix[key];
+ mix[key] = (mp && $type(op) == 'object' && $type(mp) == 'object') ? $merge(mp, op) : $unlink(op);
+ }
+ }
+ return mix;
+};
+
+function $pick(){
+ for (var i = 0, l = arguments.length; i < l; i++){
+ if (arguments[i] != undefined) return arguments[i];
+ }
+ return null;
+};
+
+function $random(min, max){
+ return Math.floor(Math.random() * (max - min + 1) + min);
+};
+
+function $splat(obj){
+ var type = $type(obj);
+ return (type) ? ((type != 'array' && type != 'arguments') ? [obj] : obj) : [];
+};
+
+var $time = Date.now || function(){
+ return +new Date;
+};
+
+function $try(){
+ for (var i = 0, l = arguments.length; i < l; i++){
+ try {
+ return arguments[i]();
+ } catch(e){}
+ }
+ return null;
+};
+
+function $type(obj){
+ if (obj == undefined) return false;
+ if (obj.$family) return (obj.$family.name == 'number' && !isFinite(obj)) ? false : obj.$family.name;
+ if (obj.nodeName){
+ switch (obj.nodeType){
+ case 1: return 'element';
+ case 3: return (/\S/).test(obj.nodeValue) ? 'textnode' : 'whitespace';
+ }
+ } else if (typeof obj.length == 'number'){
+ if (obj.callee) return 'arguments';
+ else if (obj.item) return 'collection';
+ }
+ return typeof obj;
+};
+
+function $unlink(object){
+ var unlinked;
+ switch ($type(object)){
+ case 'object':
+ unlinked = {};
+ for (var p in object) unlinked[p] = $unlink(object[p]);
+ break;
+ case 'hash':
+ unlinked = new Hash(object);
+ break;
+ case 'array':
+ unlinked = [];
+ for (var i = 0, l = object.length; i < l; i++) unlinked[i] = $unlink(object[i]);
+ break;
+ default: return object;
+ }
+ return unlinked;
+};
+
+
+/*
+Script: Browser.js
+ The Browser Core. Contains Browser initialization, Window and Document, and the Browser Hash.
+
+License:
+ MIT-style license.
+*/
+
+var Browser = $merge({
+
+ Engine: {name: 'unknown', version: 0},
+
+ Platform: {name: (window.orientation != undefined) ? 'ipod' : (navigator.platform.match(/mac|win|linux/i) || ['other'])[0].toLowerCase()},
+
+ Features: {xpath: !!(document.evaluate), air: !!(window.runtime), query: !!(document.querySelector)},
+
+ Plugins: {},
+
+ Engines: {
+
+ presto: function(){
+ return (!window.opera) ? false : ((arguments.callee.caller) ? 960 : ((document.getElementsByClassName) ? 950 : 925));
+ },
+
+ trident: function(){
+ return (!window.ActiveXObject) ? false : ((window.XMLHttpRequest) ? 5 : 4);
+ },
+
+ webkit: function(){
+ return (navigator.taintEnabled) ? false : ((Browser.Features.xpath) ? ((Browser.Features.query) ? 525 : 420) : 419);
+ },
+
+ gecko: function(){
+ return (document.getBoxObjectFor == undefined) ? false : ((document.getElementsByClassName) ? 19 : 18);
+ }
+
+ }
+
+}, Browser || {});
+
+Browser.Platform[Browser.Platform.name] = true;
+
+Browser.detect = function(){
+
+ for (var engine in this.Engines){
+ var version = this.Engines[engine]();
+ if (version){
+ this.Engine = {name: engine, version: version};
+ this.Engine[engine] = this.Engine[engine + version] = true;
+ break;
+ }
+ }
+
+ return {name: engine, version: version};
+
+};
+
+Browser.detect();
+
+Browser.Request = function(){
+ return $try(function(){
+ return new XMLHttpRequest();
+ }, function(){
+ return new ActiveXObject('MSXML2.XMLHTTP');
+ });
+};
+
+Browser.Features.xhr = !!(Browser.Request());
+
+Browser.Plugins.Flash = (function(){
+ var version = ($try(function(){
+ return navigator.plugins['Shockwave Flash'].description;
+ }, function(){
+ return new ActiveXObject('ShockwaveFlash.ShockwaveFlash').GetVariable('$version');
+ }) || '0 r0').match(/\d+/g);
+ return {version: parseInt(version[0] || 0 + '.' + version[1] || 0), build: parseInt(version[2] || 0)};
+})();
+
+function $exec(text){
+ if (!text) return text;
+ if (window.execScript){
+ window.execScript(text);
+ } else {
+ var script = document.createElement('script');
+ script.setAttribute('type', 'text/javascript');
+ script[(Browser.Engine.webkit && Browser.Engine.version < 420) ? 'innerText' : 'text'] = text;
+ document.head.appendChild(script);
+ document.head.removeChild(script);
+ }
+ return text;
+};
+
+Native.UID = 1;
+
+var $uid = (Browser.Engine.trident) ? function(item){
+ return (item.uid || (item.uid = [Native.UID++]))[0];
+} : function(item){
+ return item.uid || (item.uid = Native.UID++);
+};
+
+var Window = new Native({
+
+ name: 'Window',
+
+ legacy: (Browser.Engine.trident) ? null: window.Window,
+
+ initialize: function(win){
+ $uid(win);
+ if (!win.Element){
+ win.Element = $empty;
+ if (Browser.Engine.webkit) win.document.createElement("iframe"); //fixes safari 2
+ win.Element.prototype = (Browser.Engine.webkit) ? window["[[DOMElement.prototype]]"] : {};
+ }
+ win.document.window = win;
+ return $extend(win, Window.Prototype);
+ },
+
+ afterImplement: function(property, value){
+ window[property] = Window.Prototype[property] = value;
+ }
+
+});
+
+Window.Prototype = {$family: {name: 'window'}};
+
+new Window(window);
+
+var Document = new Native({
+
+ name: 'Document',
+
+ legacy: (Browser.Engine.trident) ? null: window.Document,
+
+ initialize: function(doc){
+ $uid(doc);
+ doc.head = doc.getElementsByTagName('head')[0];
+ doc.html = doc.getElementsByTagName('html')[0];
+ if (Browser.Engine.trident && Browser.Engine.version <= 4) $try(function(){
+ doc.execCommand("BackgroundImageCache", false, true);
+ });
+ if (Browser.Engine.trident) doc.window.attachEvent('onunload', function() {
+ doc.window.detachEvent('onunload', arguments.callee);
+ doc.head = doc.html = doc.window = null;
+ });
+ return $extend(doc, Document.Prototype);
+ },
+
+ afterImplement: function(property, value){
+ document[property] = Document.Prototype[property] = value;
+ }
+
+});
+
+Document.Prototype = {$family: {name: 'document'}};
+
+new Document(document);
+
+
+/*
+Script: Array.js
+ Contains Array Prototypes like each, contains, and erase.
+
+License:
+ MIT-style license.
+*/
+
+Array.implement({
+
+ every: function(fn, bind){
+ for (var i = 0, l = this.length; i < l; i++){
+ if (!fn.call(bind, this[i], i, this)) return false;
+ }
+ return true;
+ },
+
+ filter: function(fn, bind){
+ var results = [];
+ for (var i = 0, l = this.length; i < l; i++){
+ if (fn.call(bind, this[i], i, this)) results.push(this[i]);
+ }
+ return results;
+ },
+
+ clean: function() {
+ return this.filter($defined);
+ },
+
+ indexOf: function(item, from){
+ var len = this.length;
+ for (var i = (from < 0) ? Math.max(0, len + from) : from || 0; i < len; i++){
+ if (this[i] === item) return i;
+ }
+ return -1;
+ },
+
+ map: function(fn, bind){
+ var results = [];
+ for (var i = 0, l = this.length; i < l; i++) results[i] = fn.call(bind, this[i], i, this);
+ return results;
+ },
+
+ some: function(fn, bind){
+ for (var i = 0, l = this.length; i < l; i++){
+ if (fn.call(bind, this[i], i, this)) return true;
+ }
+ return false;
+ },
+
+ associate: function(keys){
+ var obj = {}, length = Math.min(this.length, keys.length);
+ for (var i = 0; i < length; i++) obj[keys[i]] = this[i];
+ return obj;
+ },
+
+ link: function(object){
+ var result = {};
+ for (var i = 0, l = this.length; i < l; i++){
+ for (var key in object){
+ if (object[key](this[i])){
+ result[key] = this[i];
+ delete object[key];
+ break;
+ }
+ }
+ }
+ return result;
+ },
+
+ contains: function(item, from){
+ return this.indexOf(item, from) != -1;
+ },
+
+ extend: function(array){
+ for (var i = 0, j = array.length; i < j; i++) this.push(array[i]);
+ return this;
+ },
+
+ getLast: function(){
+ return (this.length) ? this[this.length - 1] : null;
+ },
+
+ getRandom: function(){
+ return (this.length) ? this[$random(0, this.length - 1)] : null;
+ },
+
+ include: function(item){
+ if (!this.contains(item)) this.push(item);
+ return this;
+ },
+
+ combine: function(array){
+ for (var i = 0, l = array.length; i < l; i++) this.include(array[i]);
+ return this;
+ },
+
+ erase: function(item){
+ for (var i = this.length; i--; i){
+ if (this[i] === item) this.splice(i, 1);
+ }
+ return this;
+ },
+
+ empty: function(){
+ this.length = 0;
+ return this;
+ },
+
+ flatten: function(){
+ var array = [];
+ for (var i = 0, l = this.length; i < l; i++){
+ var type = $type(this[i]);
+ if (!type) continue;
+ array = array.concat((type == 'array' || type == 'collection' || type == 'arguments') ? Array.flatten(this[i]) : this[i]);
+ }
+ return array;
+ },
+
+ hexToRgb: function(array){
+ if (this.length != 3) return null;
+ var rgb = this.map(function(value){
+ if (value.length == 1) value += value;
+ return value.toInt(16);
+ });
+ return (array) ? rgb : 'rgb(' + rgb + ')';
+ },
+
+ rgbToHex: function(array){
+ if (this.length < 3) return null;
+ if (this.length == 4 && this[3] == 0 && !array) return 'transparent';
+ var hex = [];
+ for (var i = 0; i < 3; i++){
+ var bit = (this[i] - 0).toString(16);
+ hex.push((bit.length == 1) ? '0' + bit : bit);
+ }
+ return (array) ? hex : '#' + hex.join('');
+ }
+
+});
+
+
+/*
+Script: Function.js
+ Contains Function Prototypes like create, bind, pass, and delay.
+
+License:
+ MIT-style license.
+*/
+
+Function.implement({
+
+ extend: function(properties){
+ for (var property in properties) this[property] = properties[property];
+ return this;
+ },
+
+ create: function(options){
+ var self = this;
+ options = options || {};
+ return function(event){
+ var args = options.arguments;
+ args = (args != undefined) ? $splat(args) : Array.slice(arguments, (options.event) ? 1 : 0);
+ if (options.event) args = [event || window.event].extend(args);
+ var returns = function(){
+ return self.apply(options.bind || null, args);
+ };
+ if (options.delay) return setTimeout(returns, options.delay);
+ if (options.periodical) return setInterval(returns, options.periodical);
+ if (options.attempt) return $try(returns);
+ return returns();
+ };
+ },
+
+ run: function(args, bind){
+ return this.apply(bind, $splat(args));
+ },
+
+ pass: function(args, bind){
+ return this.create({bind: bind, arguments: args});
+ },
+
+ bind: function(bind, args){
+ return this.create({bind: bind, arguments: args});
+ },
+
+ bindWithEvent: function(bind, args){
+ return this.create({bind: bind, arguments: args, event: true});
+ },
+
+ attempt: function(args, bind){
+ return this.create({bind: bind, arguments: args, attempt: true})();
+ },
+
+ delay: function(delay, bind, args){
+ return this.create({bind: bind, arguments: args, delay: delay})();
+ },
+
+ periodical: function(periodical, bind, args){
+ return this.create({bind: bind, arguments: args, periodical: periodical})();
+ }
+
+});
+
+
+/*
+Script: Number.js
+ Contains Number Prototypes like limit, round, times, and ceil.
+
+License:
+ MIT-style license.
+*/
+
+Number.implement({
+
+ limit: function(min, max){
+ return Math.min(max, Math.max(min, this));
+ },
+
+ round: function(precision){
+ precision = Math.pow(10, precision || 0);
+ return Math.round(this * precision) / precision;
+ },
+
+ times: function(fn, bind){
+ for (var i = 0; i < this; i++) fn.call(bind, i, this);
+ },
+
+ toFloat: function(){
+ return parseFloat(this);
+ },
+
+ toInt: function(base){
+ return parseInt(this, base || 10);
+ }
+
+});
+
+Number.alias('times', 'each');
+
+(function(math){
+ var methods = {};
+ math.each(function(name){
+ if (!Number[name]) methods[name] = function(){
+ return Math[name].apply(null, [this].concat($A(arguments)));
+ };
+ });
+ Number.implement(methods);
+})(['abs', 'acos', 'asin', 'atan', 'atan2', 'ceil', 'cos', 'exp', 'floor', 'log', 'max', 'min', 'pow', 'sin', 'sqrt', 'tan']);
+
+
+/*
+Script: String.js
+ Contains String Prototypes like camelCase, capitalize, test, and toInt.
+
+License:
+ MIT-style license.
+*/
+
+String.implement({
+
+ test: function(regex, params){
+ return ((typeof regex == 'string') ? new RegExp(regex, params) : regex).test(this);
+ },
+
+ contains: function(string, separator){
+ return (separator) ? (separator + this + separator).indexOf(separator + string + separator) > -1 : this.indexOf(string) > -1;
+ },
+
+ trim: function(){
+ return this.replace(/^\s+|\s+$/g, '');
+ },
+
+ clean: function(){
+ return this.replace(/\s+/g, ' ').trim();
+ },
+
+ camelCase: function(){
+ return this.replace(/-\D/g, function(match){
+ return match.charAt(1).toUpperCase();
+ });
+ },
+
+ hyphenate: function(){
+ return this.replace(/[A-Z]/g, function(match){
+ return ('-' + match.charAt(0).toLowerCase());
+ });
+ },
+
+ capitalize: function(){
+ return this.replace(/\b[a-z]/g, function(match){
+ return match.toUpperCase();
+ });
+ },
+
+ escapeRegExp: function(){
+ return this.replace(/([-.*+?^${}()|[\]\/\\])/g, '\\$1');
+ },
+
+ toInt: function(base){
+ return parseInt(this, base || 10);
+ },
+
+ toFloat: function(){
+ return parseFloat(this);
+ },
+
+ hexToRgb: function(array){
+ var hex = this.match(/^#?(\w{1,2})(\w{1,2})(\w{1,2})$/);
+ return (hex) ? hex.slice(1).hexToRgb(array) : null;
+ },
+
+ rgbToHex: function(array){
+ var rgb = this.match(/\d{1,3}/g);
+ return (rgb) ? rgb.rgbToHex(array) : null;
+ },
+
+ stripScripts: function(option){
+ var scripts = '';
+ var text = this.replace(/<script[^>]*>([\s\S]*?)<\/script>/gi, function(){
+ scripts += arguments[1] + '\n';
+ return '';
+ });
+ if (option === true) $exec(scripts);
+ else if ($type(option) == 'function') option(scripts, text);
+ return text;
+ },
+
+ substitute: function(object, regexp){
+ return this.replace(regexp || (/\\?\{([^{}]+)\}/g), function(match, name){
+ if (match.charAt(0) == '\\') return match.slice(1);
+ return (object[name] != undefined) ? object[name] : '';
+ });
+ }
+
+});
+
+
+/*
+Script: Hash.js
+ Contains Hash Prototypes. Provides a means for overcoming the JavaScript practical impossibility of extending native Objects.
+
+License:
+ MIT-style license.
+*/
+
+Hash.implement({
+
+ has: Object.prototype.hasOwnProperty,
+
+ keyOf: function(value){
+ for (var key in this){
+ if (this.hasOwnProperty(key) && this[key] === value) return key;
+ }
+ return null;
+ },
+
+ hasValue: function(value){
+ return (Hash.keyOf(this, value) !== null);
+ },
+
+ extend: function(properties){
+ Hash.each(properties, function(value, key){
+ Hash.set(this, key, value);
+ }, this);
+ return this;
+ },
+
+ combine: function(properties){
+ Hash.each(properties, function(value, key){
+ Hash.include(this, key, value);
+ }, this);
+ return this;
+ },
+
+ erase: function(key){
+ if (this.hasOwnProperty(key)) delete this[key];
+ return this;
+ },
+
+ get: function(key){
+ return (this.hasOwnProperty(key)) ? this[key] : null;
+ },
+
+ set: function(key, value){
+ if (!this[key] || this.hasOwnProperty(key)) this[key] = value;
+ return this;
+ },
+
+ empty: function(){
+ Hash.each(this, function(value, key){
+ delete this[key];
+ }, this);
+ return this;
+ },
+
+ include: function(key, value){
+ var k = this[key];
+ if (k == undefined) this[key] = value;
+ return this;
+ },
+
+ map: function(fn, bind){
+ var results = new Hash;
+ Hash.each(this, function(value, key){
+ results.set(key, fn.call(bind, value, key, this));
+ }, this);
+ return results;
+ },
+
+ filter: function(fn, bind){
+ var results = new Hash;
+ Hash.each(this, function(value, key){
+ if (fn.call(bind, value, key, this)) results.set(key, value);
+ }, this);
+ return results;
+ },
+
+ every: function(fn, bind){
+ for (var key in this){
+ if (this.hasOwnProperty(key) && !fn.call(bind, this[key], key)) return false;
+ }
+ return true;
+ },
+
+ some: function(fn, bind){
+ for (var key in this){
+ if (this.hasOwnProperty(key) && fn.call(bind, this[key], key)) return true;
+ }
+ return false;
+ },
+
+ getKeys: function(){
+ var keys = [];
+ Hash.each(this, function(value, key){
+ keys.push(key);
+ });
+ return keys;
+ },
+
+ getValues: function(){
+ var values = [];
+ Hash.each(this, function(value){
+ values.push(value);
+ });
+ return values;
+ },
+
+ toQueryString: function(base){
+ var queryString = [];
+ Hash.each(this, function(value, key){
+ if (base) key = base + '[' + key + ']';
+ var result;
+ switch ($type(value)){
+ case 'object': result = Hash.toQueryString(value, key); break;
+ case 'array':
+ var qs = {};
+ value.each(function(val, i){
+ qs[i] = val;
+ });
+ result = Hash.toQueryString(qs, key);
+ break;
+ default: result = key + '=' + encodeURIComponent(value);
+ }
+ if (value != undefined) queryString.push(result);
+ });
+
+ return queryString.join('&');
+ }
+
+});
+
+Hash.alias({keyOf: 'indexOf', hasValue: 'contains'});
+
+
+/*
+Script: Event.js
+ Contains the Event Native, to make the event object completely crossbrowser.
+
+License:
+ MIT-style license.
+*/
+
+var Event = new Native({
+
+ name: 'Event',
+
+ initialize: function(event, win){
+ win = win || window;
+ var doc = win.document;
+ event = event || win.event;
+ if (event.$extended) return event;
+ this.$extended = true;
+ var type = event.type;
+ var target = event.target || event.srcElement;
+ while (target && target.nodeType == 3) target = target.parentNode;
+
+ if (type.test(/key/)){
+ var code = event.which || event.keyCode;
+ var key = Event.Keys.keyOf(code);
+ if (type == 'keydown'){
+ var fKey = code - 111;
+ if (fKey > 0 && fKey < 13) key = 'f' + fKey;
+ }
+ key = key || String.fromCharCode(code).toLowerCase();
+ } else if (type.match(/(click|mouse|menu)/i)){
+ doc = (!doc.compatMode || doc.compatMode == 'CSS1Compat') ? doc.html : doc.body;
+ var page = {
+ x: event.pageX || event.clientX + doc.scrollLeft,
+ y: event.pageY || event.clientY + doc.scrollTop
+ };
+ var client = {
+ x: (event.pageX) ? event.pageX - win.pageXOffset : event.clientX,
+ y: (event.pageY) ? event.pageY - win.pageYOffset : event.clientY
+ };
+ if (type.match(/DOMMouseScroll|mousewheel/)){
+ var wheel = (event.wheelDelta) ? event.wheelDelta / 120 : -(event.detail || 0) / 3;
+ }
+ var rightClick = (event.which == 3) || (event.button == 2);
+ var related = null;
+ if (type.match(/over|out/)){
+ switch (type){
+ case 'mouseover': related = event.relatedTarget || event.fromElement; break;
+ case 'mouseout': related = event.relatedTarget || event.toElement;
+ }
+ if (!(function(){
+ while (related && related.nodeType == 3) related = related.parentNode;
+ return true;
+ }).create({attempt: Browser.Engine.gecko})()) related = false;
+ }
+ }
+
+ return $extend(this, {
+ event: event,
+ type: type,
+
+ page: page,
+ client: client,
+ rightClick: rightClick,
+
+ wheel: wheel,
+
+ relatedTarget: related,
+ target: target,
+
+ code: code,
+ key: key,
+
+ shift: event.shiftKey,
+ control: event.ctrlKey,
+ alt: event.altKey,
+ meta: event.metaKey
+ });
+ }
+
+});
+
+Event.Keys = new Hash({
+ 'enter': 13,
+ 'up': 38,
+ 'down': 40,
+ 'left': 37,
+ 'right': 39,
+ 'esc': 27,
+ 'space': 32,
+ 'backspace': 8,
+ 'tab': 9,
+ 'delete': 46
+});
+
+Event.implement({
+
+ stop: function(){
+ return this.stopPropagation().preventDefault();
+ },
+
+ stopPropagation: function(){
+ if (this.event.stopPropagation) this.event.stopPropagation();
+ else this.event.cancelBubble = true;
+ return this;
+ },
+
+ preventDefault: function(){
+ if (this.event.preventDefault) this.event.preventDefault();
+ else this.event.returnValue = false;
+ return this;
+ }
+
+});
+
+
+/*
+Script: Class.js
+ Contains the Class Function for easily creating, extending, and implementing reusable Classes.
+
+License:
+ MIT-style license.
+*/
+
+var Class = new Native({
+
+ name: 'Class',
+
+ initialize: function(properties){
+ properties = properties || {};
+ var klass = function(){
+ for (var key in this){
+ if ($type(this[key]) != 'function') this[key] = $unlink(this[key]);
+ }
+ this.constructor = klass;
+ if (Class.prototyping) return this;
+ var instance = (this.initialize) ? this.initialize.apply(this, arguments) : this;
+ if (this.options && this.options.initialize) this.options.initialize.call(this);
+ return instance;
+ };
+
+ for (var mutator in Class.Mutators){
+ if (!properties[mutator]) continue;
+ properties = Class.Mutators[mutator](properties, properties[mutator]);
+ delete properties[mutator];
+ }
+
+ $extend(klass, this);
+ klass.constructor = Class;
+ klass.prototype = properties;
+ return klass;
+ }
+
+});
+
+Class.Mutators = {
+
+ Extends: function(self, klass){
+ Class.prototyping = klass.prototype;
+ var subclass = new klass;
+ delete subclass.parent;
+ subclass = Class.inherit(subclass, self);
+ delete Class.prototyping;
+ return subclass;
+ },
+
+ Implements: function(self, klasses){
+ $splat(klasses).each(function(klass){
+ Class.prototying = klass;
+ $extend(self, ($type(klass) == 'class') ? new klass : klass);
+ delete Class.prototyping;
+ });
+ return self;
+ }
+
+};
+
+Class.extend({
+
+ inherit: function(object, properties){
+ var caller = arguments.callee.caller;
+ for (var key in properties){
+ var override = properties[key];
+ var previous = object[key];
+ var type = $type(override);
+ if (previous && type == 'function'){
+ if (override != previous){
+ if (caller){
+ override.__parent = previous;
+ object[key] = override;
+ } else {
+ Class.override(object, key, override);
+ }
+ }
+ } else if(type == 'object'){
+ object[key] = $merge(previous, override);
+ } else {
+ object[key] = override;
+ }
+ }
+
+ if (caller) object.parent = function(){
+ return arguments.callee.caller.__parent.apply(this, arguments);
+ };
+
+ return object;
+ },
+
+ override: function(object, name, method){
+ var parent = Class.prototyping;
+ if (parent && object[name] != parent[name]) parent = null;
+ var override = function(){
+ var previous = this.parent;
+ this.parent = parent ? parent[name] : object[name];
+ var value = method.apply(this, arguments);
+ this.parent = previous;
+ return value;
+ };
+ object[name] = override;
+ }
+
+});
+
+Class.implement({
+
+ implement: function(){
+ var proto = this.prototype;
+ $each(arguments, function(properties){
+ Class.inherit(proto, properties);
+ });
+ return this;
+ }
+
+});
+
+
+/*
+Script: Class.Extras.js
+ Contains Utility Classes that can be implemented into your own Classes to ease the execution of many common tasks.
+
+License:
+ MIT-style license.
+*/
+
+var Chain = new Class({
+
+ $chain: [],
+
+ chain: function(){
+ this.$chain.extend(Array.flatten(arguments));
+ return this;
+ },
+
+ callChain: function(){
+ return (this.$chain.length) ? this.$chain.shift().apply(this, arguments) : false;
+ },
+
+ clearChain: function(){
+ this.$chain.empty();
+ return this;
+ }
+
+});
+
+var Events = new Class({
+
+ $events: {},
+
+ addEvent: function(type, fn, internal){
+ type = Events.removeOn(type);
+ if (fn != $empty){
+ this.$events[type] = this.$events[type] || [];
+ this.$events[type].include(fn);
+ if (internal) fn.internal = true;
+ }
+ return this;
+ },
+
+ addEvents: function(events){
+ for (var type in events) this.addEvent(type, events[type]);
+ return this;
+ },
+
+ fireEvent: function(type, args, delay){
+ type = Events.removeOn(type);
+ if (!this.$events || !this.$events[type]) return this;
+ this.$events[type].each(function(fn){
+ fn.create({'bind': this, 'delay': delay, 'arguments': args})();
+ }, this);
+ return this;
+ },
+
+ removeEvent: function(type, fn){
+ type = Events.removeOn(type);
+ if (!this.$events[type]) return this;
+ if (!fn.internal) this.$events[type].erase(fn);
+ return this;
+ },
+
+ removeEvents: function(events){
+ if ($type(events) == 'object'){
+ for (var type in events) this.removeEvent(type, events[type]);
+ return this;
+ }
+ if (events) events = Events.removeOn(events);
+ for (var type in this.$events){
+ if (events && events != type) continue;
+ var fns = this.$events[type];
+ for (var i = fns.length; i--; i) this.removeEvent(type, fns[i]);
+ }
+ return this;
+ }
+
+});
+
+Events.removeOn = function(string){
+ return string.replace(/^on([A-Z])/, function(full, first) {
+ return first.toLowerCase();
+ });
+};
+
+var Options = new Class({
+
+ setOptions: function(){
+ this.options = $merge.run([this.options].extend(arguments));
+ if (!this.addEvent) return this;
+ for (var option in this.options){
+ if ($type(this.options[option]) != 'function' || !(/^on[A-Z]/).test(option)) continue;
+ this.addEvent(option, this.options[option]);
+ delete this.options[option];
+ }
+ return this;
+ }
+
+});
+
+
+/*
+Script: Element.js
+ One of the most important items in MooTools. Contains the dollar function, the dollars function, and an handful of cross-browser,
+ time-saver methods to let you easily work with HTML Elements.
+
+License:
+ MIT-style license.
+*/
+
+var Element = new Native({
+
+ name: 'Element',
+
+ legacy: window.Element,
+
+ initialize: function(tag, props){
+ var konstructor = Element.Constructors.get(tag);
+ if (konstructor) return konstructor(props);
+ if (typeof tag == 'string') return document.newElement(tag, props);
+ return $(tag).set(props);
+ },
+
+ afterImplement: function(key, value){
+ Element.Prototype[key] = value;
+ if (Array[key]) return;
+ Elements.implement(key, function(){
+ var items = [], elements = true;
+ for (var i = 0, j = this.length; i < j; i++){
+ var returns = this[i][key].apply(this[i], arguments);
+ items.push(returns);
+ if (elements) elements = ($type(returns) == 'element');
+ }
+ return (elements) ? new Elements(items) : items;
+ });
+ }
+
+});
+
+Element.Prototype = {$family: {name: 'element'}};
+
+Element.Constructors = new Hash;
+
+var IFrame = new Native({
+
+ name: 'IFrame',
+
+ generics: false,
+
+ initialize: function(){
+ var params = Array.link(arguments, {properties: Object.type, iframe: $defined});
+ var props = params.properties || {};
+ var iframe = $(params.iframe) || false;
+ var onload = props.onload || $empty;
+ delete props.onload;
+ props.id = props.name = $pick(props.id, props.name, iframe.id, iframe.name, 'IFrame_' + $time());
+ iframe = new Element(iframe || 'iframe', props);
+ var onFrameLoad = function(){
+ var host = $try(function(){
+ return iframe.contentWindow.location.host;
+ });
+ if (host && host == window.location.host){
+ var win = new Window(iframe.contentWindow);
+ new Document(iframe.contentWindow.document);
+ $extend(win.Element.prototype, Element.Prototype);
+ }
+ onload.call(iframe.contentWindow, iframe.contentWindow.document);
+ };
+ (window.frames[props.id]) ? onFrameLoad() : iframe.addListener('load', onFrameLoad);
+ return iframe;
+ }
+
+});
+
+var Elements = new Native({
+
+ initialize: function(elements, options){
+ options = $extend({ddup: true, cash: true}, options);
+ elements = elements || [];
+ if (options.ddup || options.cash){
+ var uniques = {}, returned = [];
+ for (var i = 0, l = elements.length; i < l; i++){
+ var el = $.element(elements[i], !options.cash);
+ if (options.ddup){
+ if (uniques[el.uid]) continue;
+ uniques[el.uid] = true;
+ }
+ returned.push(el);
+ }
+ elements = returned;
+ }
+ return (options.cash) ? $extend(elements, this) : elements;
+ }
+
+});
+
+Elements.implement({
+
+ filter: function(filter, bind){
+ if (!filter) return this;
+ return new Elements(Array.filter(this, (typeof filter == 'string') ? function(item){
+ return item.match(filter);
+ } : filter, bind));
+ }
+
+});
+
+Document.implement({
+
+ newElement: function(tag, props){
+ if (Browser.Engine.trident && props){
+ ['name', 'type', 'checked'].each(function(attribute){
+ if (!props[attribute]) return;
+ tag += ' ' + attribute + '="' + props[attribute] + '"';
+ if (attribute != 'checked') delete props[attribute];
+ });
+ tag = '<' + tag + '>';
+ }
+ return $.element(this.createElement(tag)).set(props);
+ },
+
+ newTextNode: function(text){
+ return this.createTextNode(text);
+ },
+
+ getDocument: function(){
+ return this;
+ },
+
+ getWindow: function(){
+ return this.window;
+ }
+
+});
+
+Window.implement({
+
+ $: function(el, nocash){
+ if (el && el.$family && el.uid) return el;
+ var type = $type(el);
+ return ($[type]) ? $[type](el, nocash, this.document) : null;
+ },
+
+ $$: function(selector){
+ if (arguments.length == 1 && typeof selector == 'string') return this.document.getElements(selector);
+ var elements = [];
+ var args = Array.flatten(arguments);
+ for (var i = 0, l = args.length; i < l; i++){
+ var item = args[i];
+ switch ($type(item)){
+ case 'element': elements.push(item); break;
+ case 'string': elements.extend(this.document.getElements(item, true));
+ }
+ }
+ return new Elements(elements);
+ },
+
+ getDocument: function(){
+ return this.document;
+ },
+
+ getWindow: function(){
+ return this;
+ }
+
+});
+
+$.string = function(id, nocash, doc){
+ id = doc.getElementById(id);
+ return (id) ? $.element(id, nocash) : null;
+};
+
+$.element = function(el, nocash){
+ $uid(el);
+ if (!nocash && !el.$family && !(/^object|embed$/i).test(el.tagName)){
+ var proto = Element.Prototype;
+ for (var p in proto) el[p] = proto[p];
+ };
+ return el;
+};
+
+$.object = function(obj, nocash, doc){
+ if (obj.toElement) return $.element(obj.toElement(doc), nocash);
+ return null;
+};
+
+$.textnode = $.whitespace = $.window = $.document = $arguments(0);
+
+Native.implement([Element, Document], {
+
+ getElement: function(selector, nocash){
+ return $(this.getElements(selector, true)[0] || null, nocash);
+ },
+
+ getElements: function(tags, nocash){
+ tags = tags.split(',');
+ var elements = [];
+ var ddup = (tags.length > 1);
+ tags.each(function(tag){
+ var partial = this.getElementsByTagName(tag.trim());
+ (ddup) ? elements.extend(partial) : elements = partial;
+ }, this);
+ return new Elements(elements, {ddup: ddup, cash: !nocash});
+ }
+
+});
+
+(function(){
+
+var collected = {}, storage = {};
+var props = {input: 'checked', option: 'selected', textarea: (Browser.Engine.webkit && Browser.Engine.version < 420) ? 'innerHTML' : 'value'};
+
+var get = function(uid){
+ return (storage[uid] || (storage[uid] = {}));
+};
+
+var clean = function(item, retain){
+ if (!item) return;
+ var uid = item.uid;
+ if (Browser.Engine.trident){
+ if (item.clearAttributes){
+ var clone = retain && item.cloneNode(false);
+ item.clearAttributes();
+ if (clone) item.mergeAttributes(clone);
+ } else if (item.removeEvents){
+ item.removeEvents();
+ }
+ if ((/object/i).test(item.tagName)){
+ for (var p in item){
+ if (typeof item[p] == 'function') item[p] = $empty;
+ }
+ Element.dispose(item);
+ }
+ }
+ if (!uid) return;
+ collected[uid] = storage[uid] = null;
+};
+
+var purge = function(){
+ Hash.each(collected, clean);
+ if (Browser.Engine.trident) $A(document.getElementsByTagName('object')).each(clean);
+ if (window.CollectGarbage) CollectGarbage();
+ collected = storage = null;
+};
+
+var walk = function(element, walk, start, match, all, nocash){
+ var el = element[start || walk];
+ var elements = [];
+ while (el){
+ if (el.nodeType == 1 && (!match || Element.match(el, match))){
+ if (!all) return $(el, nocash);
+ elements.push(el);
+ }
+ el = el[walk];
+ }
+ return (all) ? new Elements(elements, {ddup: false, cash: !nocash}) : null;
+};
+
+var attributes = {
+ 'html': 'innerHTML',
+ 'class': 'className',
+ 'for': 'htmlFor',
+ 'text': (Browser.Engine.trident || (Browser.Engine.webkit && Browser.Engine.version < 420)) ? 'innerText' : 'textContent'
+};
+var bools = ['compact', 'nowrap', 'ismap', 'declare', 'noshade', 'checked', 'disabled', 'readonly', 'multiple', 'selected', 'noresize', 'defer'];
+var camels = ['value', 'accessKey', 'cellPadding', 'cellSpacing', 'colSpan', 'frameBorder', 'maxLength', 'readOnly', 'rowSpan', 'tabIndex', 'useMap'];
+
+Hash.extend(attributes, bools.associate(bools));
+Hash.extend(attributes, camels.associate(camels.map(String.toLowerCase)));
+
+var inserters = {
+
+ before: function(context, element){
+ if (element.parentNode) element.parentNode.insertBefore(context, element);
+ },
+
+ after: function(context, element){
+ if (!element.parentNode) return;
+ var next = element.nextSibling;
+ (next) ? element.parentNode.insertBefore(context, next) : element.parentNode.appendChild(context);
+ },
+
+ bottom: function(context, element){
+ element.appendChild(context);
+ },
+
+ top: function(context, element){
+ var first = element.firstChild;
+ (first) ? element.insertBefore(context, first) : element.appendChild(context);
+ }
+
+};
+
+inserters.inside = inserters.bottom;
+
+Hash.each(inserters, function(inserter, where){
+
+ where = where.capitalize();
+
+ Element.implement('inject' + where, function(el){
+ inserter(this, $(el, true));
+ return this;
+ });
+
+ Element.implement('grab' + where, function(el){
+ inserter($(el, true), this);
+ return this;
+ });
+
+});
+
+Element.implement({
+
+ set: function(prop, value){
+ switch ($type(prop)){
+ case 'object':
+ for (var p in prop) this.set(p, prop[p]);
+ break;
+ case 'string':
+ var property = Element.Properties.get(prop);
+ (property && property.set) ? property.set.apply(this, Array.slice(arguments, 1)) : this.setProperty(prop, value);
+ }
+ return this;
+ },
+
+ get: function(prop){
+ var property = Element.Properties.get(prop);
+ return (property && property.get) ? property.get.apply(this, Array.slice(arguments, 1)) : this.getProperty(prop);
+ },
+
+ erase: function(prop){
+ var property = Element.Properties.get(prop);
+ (property && property.erase) ? property.erase.apply(this) : this.removeProperty(prop);
+ return this;
+ },
+
+ setProperty: function(attribute, value){
+ var key = attributes[attribute];
+ if (value == undefined) return this.removeProperty(attribute);
+ if (key && bools[attribute]) value = !!value;
+ (key) ? this[key] = value : this.setAttribute(attribute, '' + value);
+ return this;
+ },
+
+ setProperties: function(attributes){
+ for (var attribute in attributes) this.setProperty(attribute, attributes[attribute]);
+ return this;
+ },
+
+ getProperty: function(attribute){
+ var key = attributes[attribute];
+ var value = (key) ? this[key] : this.getAttribute(attribute, 2);
+ return (bools[attribute]) ? !!value : (key) ? value : value || null;
+ },
+
+ getProperties: function(){
+ var args = $A(arguments);
+ return args.map(this.getProperty, this).associate(args);
+ },
+
+ removeProperty: function(attribute){
+ var key = attributes[attribute];
+ (key) ? this[key] = (key && bools[attribute]) ? false : '' : this.removeAttribute(attribute);
+ return this;
+ },
+
+ removeProperties: function(){
+ Array.each(arguments, this.removeProperty, this);
+ return this;
+ },
+
+ hasClass: function(className){
+ return this.className.contains(className, ' ');
+ },
+
+ addClass: function(className){
+ if (!this.hasClass(className)) this.className = (this.className + ' ' + className).clean();
+ return this;
+ },
+
+ removeClass: function(className){
+ this.className = this.className.replace(new RegExp('(^|\\s)' + className + '(?:\\s|$)'), '$1');
+ return this;
+ },
+
+ toggleClass: function(className){
+ return this.hasClass(className) ? this.removeClass(className) : this.addClass(className);
+ },
+
+ adopt: function(){
+ Array.flatten(arguments).each(function(element){
+ element = $(element, true);
+ if (element) this.appendChild(element);
+ }, this);
+ return this;
+ },
+
+ appendText: function(text, where){
+ return this.grab(this.getDocument().newTextNode(text), where);
+ },
+
+ grab: function(el, where){
+ inserters[where || 'bottom']($(el, true), this);
+ return this;
+ },
+
+ inject: function(el, where){
+ inserters[where || 'bottom'](this, $(el, true));
+ return this;
+ },
+
+ replaces: function(el){
+ el = $(el, true);
+ el.parentNode.replaceChild(this, el);
+ return this;
+ },
+
+ wraps: function(el, where){
+ el = $(el, true);
+ return this.replaces(el).grab(el, where);
+ },
+
+ getPrevious: function(match, nocash){
+ return walk(this, 'previousSibling', null, match, false, nocash);
+ },
+
+ getAllPrevious: function(match, nocash){
+ return walk(this, 'previousSibling', null, match, true, nocash);
+ },
+
+ getNext: function(match, nocash){
+ return walk(this, 'nextSibling', null, match, false, nocash);
+ },
+
+ getAllNext: function(match, nocash){
+ return walk(this, 'nextSibling', null, match, true, nocash);
+ },
+
+ getFirst: function(match, nocash){
+ return walk(this, 'nextSibling', 'firstChild', match, false, nocash);
+ },
+
+ getLast: function(match, nocash){
+ return walk(this, 'previousSibling', 'lastChild', match, false, nocash);
+ },
+
+ getParent: function(match, nocash){
+ return walk(this, 'parentNode', null, match, false, nocash);
+ },
+
+ getParents: function(match, nocash){
+ return walk(this, 'parentNode', null, match, true, nocash);
+ },
+
+ getChildren: function(match, nocash){
+ return walk(this, 'nextSibling', 'firstChild', match, true, nocash);
+ },
+
+ getWindow: function(){
+ return this.ownerDocument.window;
+ },
+
+ getDocument: function(){
+ return this.ownerDocument;
+ },
+
+ getElementById: function(id, nocash){
+ var el = this.ownerDocument.getElementById(id);
+ if (!el) return null;
+ for (var parent = el.parentNode; parent != this; parent = parent.parentNode){
+ if (!parent) return null;
+ }
+ return $.element(el, nocash);
+ },
+
+ getSelected: function(){
+ return new Elements($A(this.options).filter(function(option){
+ return option.selected;
+ }));
+ },
+
+ getComputedStyle: function(property){
+ if (this.currentStyle) return this.currentStyle[property.camelCase()];
+ var computed = this.getDocument().defaultView.getComputedStyle(this, null);
+ return (computed) ? computed.getPropertyValue([property.hyphenate()]) : null;
+ },
+
+ toQueryString: function(){
+ var queryString = [];
+ this.getElements('input, select, textarea', true).each(function(el){
+ if (!el.name || el.disabled) return;
+ var value = (el.tagName.toLowerCase() == 'select') ? Element.getSelected(el).map(function(opt){
+ return opt.value;
+ }) : ((el.type == 'radio' || el.type == 'checkbox') && !el.checked) ? null : el.value;
+ $splat(value).each(function(val){
+ if (typeof val != 'undefined') queryString.push(el.name + '=' + encodeURIComponent(val));
+ });
+ });
+ return queryString.join('&');
+ },
+
+ clone: function(contents, keepid){
+ contents = contents !== false;
+ var clone = this.cloneNode(contents);
+ var clean = function(node, element){
+ if (!keepid) node.removeAttribute('id');
+ if (Browser.Engine.trident){
+ node.clearAttributes();
+ node.mergeAttributes(element);
+ node.removeAttribute('uid');
+ if (node.options){
+ var no = node.options, eo = element.options;
+ for (var j = no.length; j--;) no[j].selected = eo[j].selected;
+ }
+ }
+ var prop = props[element.tagName.toLowerCase()];
+ if (prop && element[prop]) node[prop] = element[prop];
+ };
+
+ if (contents){
+ var ce = clone.getElementsByTagName('*'), te = this.getElementsByTagName('*');
+ for (var i = ce.length; i--;) clean(ce[i], te[i]);
+ }
+
+ clean(clone, this);
+ return $(clone);
+ },
+
+ destroy: function(){
+ Element.empty(this);
+ Element.dispose(this);
+ clean(this, true);
+ return null;
+ },
+
+ empty: function(){
+ $A(this.childNodes).each(function(node){
+ Element.destroy(node);
+ });
+ return this;
+ },
+
+ dispose: function(){
+ return (this.parentNode) ? this.parentNode.removeChild(this) : this;
+ },
+
+ hasChild: function(el){
+ el = $(el, true);
+ if (!el) return false;
+ if (Browser.Engine.webkit && Browser.Engine.version < 420) return $A(this.getElementsByTagName(el.tagName)).contains(el);
+ return (this.contains) ? (this != el && this.contains(el)) : !!(this.compareDocumentPosition(el) & 16);
+ },
+
+ match: function(tag){
+ return (!tag || (tag == this) || (Element.get(this, 'tag') == tag));
+ }
+
+});
+
+Native.implement([Element, Window, Document], {
+
+ addListener: function(type, fn){
+ if (type == 'unload'){
+ var old = fn, self = this;
+ fn = function(){
+ self.removeListener('unload', fn);
+ old();
+ };
+ } else {
+ collected[this.uid] = this;
+ }
+ if (this.addEventListener) this.addEventListener(type, fn, false);
+ else this.attachEvent('on' + type, fn);
+ return this;
+ },
+
+ removeListener: function(type, fn){
+ if (this.removeEventListener) this.removeEventListener(type, fn, false);
+ else this.detachEvent('on' + type, fn);
+ return this;
+ },
+
+ retrieve: function(property, dflt){
+ var storage = get(this.uid), prop = storage[property];
+ if (dflt != undefined && prop == undefined) prop = storage[property] = dflt;
+ return $pick(prop);
+ },
+
+ store: function(property, value){
+ var storage = get(this.uid);
+ storage[property] = value;
+ return this;
+ },
+
+ eliminate: function(property){
+ var storage = get(this.uid);
+ delete storage[property];
+ return this;
+ }
+
+});
+
+window.addListener('unload', purge);
+
+})();
+
+Element.Properties = new Hash;
+
+Element.Properties.style = {
+
+ set: function(style){
+ this.style.cssText = style;
+ },
+
+ get: function(){
+ return this.style.cssText;
+ },
+
+ erase: function(){
+ this.style.cssText = '';
+ }
+
+};
+
+Element.Properties.tag = {
+
+ get: function(){
+ return this.tagName.toLowerCase();
+ }
+
+};
+
+Element.Properties.html = (function(){
+ var wrapper = document.createElement('div');
+
+ var translations = {
+ table: [1, '<table>', '</table>'],
+ select: [1, '<select>', '</select>'],
+ tbody: [2, '<table><tbody>', '</tbody></table>'],
+ tr: [3, '<table><tbody><tr>', '</tr></tbody></table>']
+ };
+ translations.thead = translations.tfoot = translations.tbody;
+
+ var html = {
+ set: function(){
+ var html = Array.flatten(arguments).join('');
+ var wrap = Browser.Engine.trident && translations[this.get('tag')];
+ if (wrap){
+ var first = wrapper;
+ first.innerHTML = wrap[1] + html + wrap[2];
+ for (var i = wrap[0]; i--;) first = first.firstChild;
+ this.empty().adopt(first.childNodes);
+ } else {
+ this.innerHTML = html;
+ }
+ }
+ };
+
+ html.erase = html.set;
+
+ return html;
+})();
+
+if (Browser.Engine.webkit && Browser.Engine.version < 420) Element.Properties.text = {
+ get: function(){
+ if (this.innerText) return this.innerText;
+ var temp = this.ownerDocument.newElement('div', {html: this.innerHTML}).inject(this.ownerDocument.body);
+ var text = temp.innerText;
+ temp.destroy();
+ return text;
+ }
+};
+
+
+/*
+Script: Element.Event.js
+ Contains Element methods for dealing with events, and custom Events.
+
+License:
+ MIT-style license.
+*/
+
+Element.Properties.events = {set: function(events){
+ this.addEvents(events);
+}};
+
+Native.implement([Element, Window, Document], {
+
+ addEvent: function(type, fn){
+ var events = this.retrieve('events', {});
+ events[type] = events[type] || {'keys': [], 'values': []};
+ if (events[type].keys.contains(fn)) return this;
+ events[type].keys.push(fn);
+ var realType = type, custom = Element.Events.get(type), condition = fn, self = this;
+ if (custom){
+ if (custom.onAdd) custom.onAdd.call(this, fn);
+ if (custom.condition){
+ condition = function(event){
+ if (custom.condition.call(this, event)) return fn.call(this, event);
+ return true;
+ };
+ }
+ realType = custom.base || realType;
+ }
+ var defn = function(){
+ return fn.call(self);
+ };
+ var nativeEvent = Element.NativeEvents[realType];
+ if (nativeEvent){
+ if (nativeEvent == 2){
+ defn = function(event){
+ event = new Event(event, self.getWindow());
+ if (condition.call(self, event) === false) event.stop();
+ };
+ }
+ this.addListener(realType, defn);
+ }
+ events[type].values.push(defn);
+ return this;
+ },
+
+ removeEvent: function(type, fn){
+ var events = this.retrieve('events');
+ if (!events || !events[type]) return this;
+ var pos = events[type].keys.indexOf(fn);
+ if (pos == -1) return this;
+ events[type].keys.splice(pos, 1);
+ var value = events[type].values.splice(pos, 1)[0];
+ var custom = Element.Events.get(type);
+ if (custom){
+ if (custom.onRemove) custom.onRemove.call(this, fn);
+ type = custom.base || type;
+ }
+ return (Element.NativeEvents[type]) ? this.removeListener(type, value) : this;
+ },
+
+ addEvents: function(events){
+ for (var event in events) this.addEvent(event, events[event]);
+ return this;
+ },
+
+ removeEvents: function(events){
+ if ($type(events) == 'object'){
+ for (var type in events) this.removeEvent(type, events[type]);
+ return this;
+ }
+ var attached = this.retrieve('events');
+ if (!attached) return this;
+ if (!events){
+ for (var type in attached) this.removeEvents(type);
+ this.eliminate('events');
+ } else if (attached[events]){
+ while (attached[events].keys[0]) this.removeEvent(events, attached[events].keys[0]);
+ attached[events] = null;
+ }
+ return this;
+ },
+
+ fireEvent: function(type, args, delay){
+ var events = this.retrieve('events');
+ if (!events || !events[type]) return this;
+ events[type].keys.each(function(fn){
+ fn.create({'bind': this, 'delay': delay, 'arguments': args})();
+ }, this);
+ return this;
+ },
+
+ cloneEvents: function(from, type){
+ from = $(from);
+ var fevents = from.retrieve('events');
+ if (!fevents) return this;
+ if (!type){
+ for (var evType in fevents) this.cloneEvents(from, evType);
+ } else if (fevents[type]){
+ fevents[type].keys.each(function(fn){
+ this.addEvent(type, fn);
+ }, this);
+ }
+ return this;
+ }
+
+});
+
+Element.NativeEvents = {
+ click: 2, dblclick: 2, mouseup: 2, mousedown: 2, contextmenu: 2, //mouse buttons
+ mousewheel: 2, DOMMouseScroll: 2, //mouse wheel
+ mouseover: 2, mouseout: 2, mousemove: 2, selectstart: 2, selectend: 2, //mouse movement
+ keydown: 2, keypress: 2, keyup: 2, //keyboard
+ focus: 2, blur: 2, change: 2, reset: 2, select: 2, submit: 2, //form elements
+ load: 1, unload: 1, beforeunload: 2, resize: 1, move: 1, DOMContentLoaded: 1, readystatechange: 1, //window
+ error: 1, abort: 1, scroll: 1 //misc
+};
+
+(function(){
+
+var $check = function(event){
+ var related = event.relatedTarget;
+ if (related == undefined) return true;
+ if (related === false) return false;
+ return ($type(this) != 'document' && related != this && related.prefix != 'xul' && !this.hasChild(related));
+};
+
+Element.Events = new Hash({
+
+ mouseenter: {
+ base: 'mouseover',
+ condition: $check
+ },
+
+ mouseleave: {
+ base: 'mouseout',
+ condition: $check
+ },
+
+ mousewheel: {
+ base: (Browser.Engine.gecko) ? 'DOMMouseScroll' : 'mousewheel'
+ }
+
+});
+
+})();
+
+
+/*
+Script: Element.Style.js
+ Contains methods for interacting with the styles of Elements in a fashionable way.
+
+License:
+ MIT-style license.
+*/
+
+Element.Properties.styles = {set: function(styles){
+ this.setStyles(styles);
+}};
+
+Element.Properties.opacity = {
+
+ set: function(opacity, novisibility){
+ if (!novisibility){
+ if (opacity == 0){
+ if (this.style.visibility != 'hidden') this.style.visibility = 'hidden';
+ } else {
+ if (this.style.visibility != 'visible') this.style.visibility = 'visible';
+ }
+ }
+ if (!this.currentStyle || !this.currentStyle.hasLayout) this.style.zoom = 1;
+ if (Browser.Engine.trident) this.style.filter = (opacity == 1) ? '' : 'alpha(opacity=' + opacity * 100 + ')';
+ this.style.opacity = opacity;
+ this.store('opacity', opacity);
+ },
+
+ get: function(){
+ return this.retrieve('opacity', 1);
+ }
+
+};
+
+Element.implement({
+
+ setOpacity: function(value){
+ return this.set('opacity', value, true);
+ },
+
+ getOpacity: function(){
+ return this.get('opacity');
+ },
+
+ setStyle: function(property, value){
+ switch (property){
+ case 'opacity': return this.set('opacity', parseFloat(value));
+ case 'float': property = (Browser.Engine.trident) ? 'styleFloat' : 'cssFloat';
+ }
+ property = property.camelCase();
+ if ($type(value) != 'string'){
+ var map = (Element.Styles.get(property) || '@').split(' ');
+ value = $splat(value).map(function(val, i){
+ if (!map[i]) return '';
+ return ($type(val) == 'number') ? map[i].replace('@', Math.round(val)) : val;
+ }).join(' ');
+ } else if (value == String(Number(value))){
+ value = Math.round(value);
+ }
+ this.style[property] = value;
+ return this;
+ },
+
+ getStyle: function(property){
+ switch (property){
+ case 'opacity': return this.get('opacity');
+ case 'float': property = (Browser.Engine.trident) ? 'styleFloat' : 'cssFloat';
+ }
+ property = property.camelCase();
+ var result = this.style[property];
+ if (!$chk(result)){
+ result = [];
+ for (var style in Element.ShortStyles){
+ if (property != style) continue;
+ for (var s in Element.ShortStyles[style]) result.push(this.getStyle(s));
+ return result.join(' ');
+ }
+ result = this.getComputedStyle(property);
+ }
+ if (result){
+ result = String(result);
+ var color = result.match(/rgba?\([\d\s,]+\)/);
+ if (color) result = result.replace(color[0], color[0].rgbToHex());
+ }
+ if (Browser.Engine.presto || (Browser.Engine.trident && !$chk(parseInt(result)))){
+ if (property.test(/^(height|width)$/)){
+ var values = (property == 'width') ? ['left', 'right'] : ['top', 'bottom'], size = 0;
+ values.each(function(value){
+ size += this.getStyle('border-' + value + '-width').toInt() + this.getStyle('padding-' + value).toInt();
+ }, this);
+ return this['offset' + property.capitalize()] - size + 'px';
+ }
+ if ((Browser.Engine.presto) && String(result).test('px')) return result;
+ if (property.test(/(border(.+)Width|margin|padding)/)) return '0px';
+ }
+ return result;
+ },
+
+ setStyles: function(styles){
+ for (var style in styles) this.setStyle(style, styles[style]);
+ return this;
+ },
+
+ getStyles: function(){
+ var result = {};
+ Array.each(arguments, function(key){
+ result[key] = this.getStyle(key);
+ }, this);
+ return result;
+ }
+
+});
+
+Element.Styles = new Hash({
+ left: '@px', top: '@px', bottom: '@px', right: '@px',
+ width: '@px', height: '@px', maxWidth: '@px', maxHeight: '@px', minWidth: '@px', minHeight: '@px',
+ backgroundColor: 'rgb(@, @, @)', backgroundPosition: '@px @px', color: 'rgb(@, @, @)',
+ fontSize: '@px', letterSpacing: '@px', lineHeight: '@px', clip: 'rect(@px @px @px @px)',
+ margin: '@px @px @px @px', padding: '@px @px @px @px', border: '@px @ rgb(@, @, @) @px @ rgb(@, @, @) @px @ rgb(@, @, @)',
+ borderWidth: '@px @px @px @px', borderStyle: '@ @ @ @', borderColor: 'rgb(@, @, @) rgb(@, @, @) rgb(@, @, @) rgb(@, @, @)',
+ zIndex: '@', 'zoom': '@', fontWeight: '@', textIndent: '@px', opacity: '@'
+});
+
+Element.ShortStyles = {margin: {}, padding: {}, border: {}, borderWidth: {}, borderStyle: {}, borderColor: {}};
+
+['Top', 'Right', 'Bottom', 'Left'].each(function(direction){
+ var Short = Element.ShortStyles;
+ var All = Element.Styles;
+ ['margin', 'padding'].each(function(style){
+ var sd = style + direction;
+ Short[style][sd] = All[sd] = '@px';
+ });
+ var bd = 'border' + direction;
+ Short.border[bd] = All[bd] = '@px @ rgb(@, @, @)';
+ var bdw = bd + 'Width', bds = bd + 'Style', bdc = bd + 'Color';
+ Short[bd] = {};
+ Short.borderWidth[bdw] = Short[bd][bdw] = All[bdw] = '@px';
+ Short.borderStyle[bds] = Short[bd][bds] = All[bds] = '@';
+ Short.borderColor[bdc] = Short[bd][bdc] = All[bdc] = 'rgb(@, @, @)';
+});
+
+
+/*
+Script: Element.Dimensions.js
+ Contains methods to work with size, scroll, or positioning of Elements and the window object.
+
+License:
+ MIT-style license.
+
+Credits:
+ - Element positioning based on the [qooxdoo](http://qooxdoo.org/) code and smart browser fixes, [LGPL License](http://www.gnu.org/licenses/lgpl.html).
+ - Viewport dimensions based on [YUI](http://developer.yahoo.com/yui/) code, [BSD License](http://developer.yahoo.com/yui/license.html).
+*/
+
+(function(){
+
+Element.implement({
+
+ scrollTo: function(x, y){
+ if (isBody(this)){
+ this.getWindow().scrollTo(x, y);
+ } else {
+ this.scrollLeft = x;
+ this.scrollTop = y;
+ }
+ return this;
+ },
+
+ getSize: function(){
+ if (isBody(this)) return this.getWindow().getSize();
+ return {x: this.offsetWidth, y: this.offsetHeight};
+ },
+
+ getScrollSize: function(){
+ if (isBody(this)) return this.getWindow().getScrollSize();
+ return {x: this.scrollWidth, y: this.scrollHeight};
+ },
+
+ getScroll: function(){
+ if (isBody(this)) return this.getWindow().getScroll();
+ return {x: this.scrollLeft, y: this.scrollTop};
+ },
+
+ getScrolls: function(){
+ var element = this, position = {x: 0, y: 0};
+ while (element && !isBody(element)){
+ position.x += element.scrollLeft;
+ position.y += element.scrollTop;
+ element = element.parentNode;
+ }
+ return position;
+ },
+
+ getOffsetParent: function(){
+ var element = this;
+ if (isBody(element)) return null;
+ if (!Browser.Engine.trident) return element.offsetParent;
+ while ((element = element.parentNode) && !isBody(element)){
+ if (styleString(element, 'position') != 'static') return element;
+ }
+ return null;
+ },
+
+ getOffsets: function(){
+ if (Browser.Engine.trident){
+ var bound = this.getBoundingClientRect(), html = this.getDocument().documentElement;
+ return {
+ x: bound.left + html.scrollLeft - html.clientLeft,
+ y: bound.top + html.scrollTop - html.clientTop
+ };
+ }
+
+ var element = this, position = {x: 0, y: 0};
+ if (isBody(this)) return position;
+
+ while (element && !isBody(element)){
+ position.x += element.offsetLeft;
+ position.y += element.offsetTop;
+
+ if (Browser.Engine.gecko){
+ if (!borderBox(element)){
+ position.x += leftBorder(element);
+ position.y += topBorder(element);
+ }
+ var parent = element.parentNode;
+ if (parent && styleString(parent, 'overflow') != 'visible'){
+ position.x += leftBorder(parent);
+ position.y += topBorder(parent);
+ }
+ } else if (element != this && Browser.Engine.webkit){
+ position.x += leftBorder(element);
+ position.y += topBorder(element);
+ }
+
+ element = element.offsetParent;
+ }
+ if (Browser.Engine.gecko && !borderBox(this)){
+ position.x -= leftBorder(this);
+ position.y -= topBorder(this);
+ }
+ return position;
+ },
+
+ getPosition: function(relative){
+ if (isBody(this)) return {x: 0, y: 0};
+ var offset = this.getOffsets(), scroll = this.getScrolls();
+ var position = {x: offset.x - scroll.x, y: offset.y - scroll.y};
+ var relativePosition = (relative && (relative = $(relative))) ? relative.getPosition() : {x: 0, y: 0};
+ return {x: position.x - relativePosition.x, y: position.y - relativePosition.y};
+ },
+
+ getCoordinates: function(element){
+ if (isBody(this)) return this.getWindow().getCoordinates();
+ var position = this.getPosition(element), size = this.getSize();
+ var obj = {left: position.x, top: position.y, width: size.x, height: size.y};
+ obj.right = obj.left + obj.width;
+ obj.bottom = obj.top + obj.height;
+ return obj;
+ },
+
+ computePosition: function(obj){
+ return {left: obj.x - styleNumber(this, 'margin-left'), top: obj.y - styleNumber(this, 'margin-top')};
+ },
+
+ position: function(obj){
+ return this.setStyles(this.computePosition(obj));
+ }
+
+});
+
+Native.implement([Document, Window], {
+
+ getSize: function(){
+ var win = this.getWindow();
+ if (Browser.Engine.presto || Browser.Engine.webkit) return {x: win.innerWidth, y: win.innerHeight};
+ var doc = getCompatElement(this);
+ return {x: doc.clientWidth, y: doc.clientHeight};
+ },
+
+ getScroll: function(){
+ var win = this.getWindow();
+ var doc = getCompatElement(this);
+ return {x: win.pageXOffset || doc.scrollLeft, y: win.pageYOffset || doc.scrollTop};
+ },
+
+ getScrollSize: function(){
+ var doc = getCompatElement(this);
+ var min = this.getSize();
+ return {x: Math.max(doc.scrollWidth, min.x), y: Math.max(doc.scrollHeight, min.y)};
+ },
+
+ getPosition: function(){
+ return {x: 0, y: 0};
+ },
+
+ getCoordinates: function(){
+ var size = this.getSize();
+ return {top: 0, left: 0, bottom: size.y, right: size.x, height: size.y, width: size.x};
+ }
+
+});
+
+// private methods
+
+var styleString = Element.getComputedStyle;
+
+function styleNumber(element, style){
+ return styleString(element, style).toInt() || 0;
+};
+
+function borderBox(element){
+ return styleString(element, '-moz-box-sizing') == 'border-box';
+};
+
+function topBorder(element){
+ return styleNumber(element, 'border-top-width');
+};
+
+function leftBorder(element){
+ return styleNumber(element, 'border-left-width');
+};
+
+function isBody(element){
+ return (/^(?:body|html)$/i).test(element.tagName);
+};
+
+function getCompatElement(element){
+ var doc = element.getDocument();
+ return (!doc.compatMode || doc.compatMode == 'CSS1Compat') ? doc.html : doc.body;
+};
+
+})();
+
+//aliases
+
+Native.implement([Window, Document, Element], {
+
+ getHeight: function(){
+ return this.getSize().y;
+ },
+
+ getWidth: function(){
+ return this.getSize().x;
+ },
+
+ getScrollTop: function(){
+ return this.getScroll().y;
+ },
+
+ getScrollLeft: function(){
+ return this.getScroll().x;
+ },
+
+ getScrollHeight: function(){
+ return this.getScrollSize().y;
+ },
+
+ getScrollWidth: function(){
+ return this.getScrollSize().x;
+ },
+
+ getTop: function(){
+ return this.getPosition().y;
+ },
+
+ getLeft: function(){
+ return this.getPosition().x;
+ }
+
+});
+
+
+/*
+Script: Selectors.js
+ Adds advanced CSS Querying capabilities for targeting elements. Also includes pseudoselectors support.
+
+License:
+ MIT-style license.
+*/
+
+Native.implement([Document, Element], {
+
+ getElements: function(expression, nocash){
+ expression = expression.split(',');
+ var items, local = {};
+ for (var i = 0, l = expression.length; i < l; i++){
+ var selector = expression[i], elements = Selectors.Utils.search(this, selector, local);
+ if (i != 0 && elements.item) elements = $A(elements);
+ items = (i == 0) ? elements : (items.item) ? $A(items).concat(elements) : items.concat(elements);
+ }
+ return new Elements(items, {ddup: (expression.length > 1), cash: !nocash});
+ }
+
+});
+
+Element.implement({
+
+ match: function(selector){
+ if (!selector || (selector == this)) return true;
+ var tagid = Selectors.Utils.parseTagAndID(selector);
+ var tag = tagid[0], id = tagid[1];
+ if (!Selectors.Filters.byID(this, id) || !Selectors.Filters.byTag(this, tag)) return false;
+ var parsed = Selectors.Utils.parseSelector(selector);
+ return (parsed) ? Selectors.Utils.filter(this, parsed, {}) : true;
+ }
+
+});
+
+var Selectors = {Cache: {nth: {}, parsed: {}}};
+
+Selectors.RegExps = {
+ id: (/#([\w-]+)/),
+ tag: (/^(\w+|\*)/),
+ quick: (/^(\w+|\*)$/),
+ splitter: (/\s*([+>~\s])\s*([a-zA-Z#.*:\[])/g),
+ combined: (/\.([\w-]+)|\[(\w+)(?:([!*^$~|]?=)(["']?)([^\4]*?)\4)?\]|:([\w-]+)(?:\(["']?(.*?)?["']?\)|$)/g)
+};
+
+Selectors.Utils = {
+
+ chk: function(item, uniques){
+ if (!uniques) return true;
+ var uid = $uid(item);
+ if (!uniques[uid]) return uniques[uid] = true;
+ return false;
+ },
+
+ parseNthArgument: function(argument){
+ if (Selectors.Cache.nth[argument]) return Selectors.Cache.nth[argument];
+ var parsed = argument.match(/^([+-]?\d*)?([a-z]+)?([+-]?\d*)?$/);
+ if (!parsed) return false;
+ var inta = parseInt(parsed[1]);
+ var a = (inta || inta === 0) ? inta : 1;
+ var special = parsed[2] || false;
+ var b = parseInt(parsed[3]) || 0;
+ if (a != 0){
+ b--;
+ while (b < 1) b += a;
+ while (b >= a) b -= a;
+ } else {
+ a = b;
+ special = 'index';
+ }
+ switch (special){
+ case 'n': parsed = {a: a, b: b, special: 'n'}; break;
+ case 'odd': parsed = {a: 2, b: 0, special: 'n'}; break;
+ case 'even': parsed = {a: 2, b: 1, special: 'n'}; break;
+ case 'first': parsed = {a: 0, special: 'index'}; break;
+ case 'last': parsed = {special: 'last-child'}; break;
+ case 'only': parsed = {special: 'only-child'}; break;
+ default: parsed = {a: (a - 1), special: 'index'};
+ }
+
+ return Selectors.Cache.nth[argument] = parsed;
+ },
+
+ parseSelector: function(selector){
+ if (Selectors.Cache.parsed[selector]) return Selectors.Cache.parsed[selector];
+ var m, parsed = {classes: [], pseudos: [], attributes: []};
+ while ((m = Selectors.RegExps.combined.exec(selector))){
+ var cn = m[1], an = m[2], ao = m[3], av = m[5], pn = m[6], pa = m[7];
+ if (cn){
+ parsed.classes.push(cn);
+ } else if (pn){
+ var parser = Selectors.Pseudo.get(pn);
+ if (parser) parsed.pseudos.push({parser: parser, argument: pa});
+ else parsed.attributes.push({name: pn, operator: '=', value: pa});
+ } else if (an){
+ parsed.attributes.push({name: an, operator: ao, value: av});
+ }
+ }
+ if (!parsed.classes.length) delete parsed.classes;
+ if (!parsed.attributes.length) delete parsed.attributes;
+ if (!parsed.pseudos.length) delete parsed.pseudos;
+ if (!parsed.classes && !parsed.attributes && !parsed.pseudos) parsed = null;
+ return Selectors.Cache.parsed[selector] = parsed;
+ },
+
+ parseTagAndID: function(selector){
+ var tag = selector.match(Selectors.RegExps.tag);
+ var id = selector.match(Selectors.RegExps.id);
+ return [(tag) ? tag[1] : '*', (id) ? id[1] : false];
+ },
+
+ filter: function(item, parsed, local){
+ var i;
+ if (parsed.classes){
+ for (i = parsed.classes.length; i--; i){
+ var cn = parsed.classes[i];
+ if (!Selectors.Filters.byClass(item, cn)) return false;
+ }
+ }
+ if (parsed.attributes){
+ for (i = parsed.attributes.length; i--; i){
+ var att = parsed.attributes[i];
+ if (!Selectors.Filters.byAttribute(item, att.name, att.operator, att.value)) return false;
+ }
+ }
+ if (parsed.pseudos){
+ for (i = parsed.pseudos.length; i--; i){
+ var psd = parsed.pseudos[i];
+ if (!Selectors.Filters.byPseudo(item, psd.parser, psd.argument, local)) return false;
+ }
+ }
+ return true;
+ },
+
+ getByTagAndID: function(ctx, tag, id){
+ if (id){
+ var item = (ctx.getElementById) ? ctx.getElementById(id, true) : Element.getElementById(ctx, id, true);
+ return (item && Selectors.Filters.byTag(item, tag)) ? [item] : [];
+ } else {
+ return ctx.getElementsByTagName(tag);
+ }
+ },
+
+ search: function(self, expression, local){
+ var splitters = [];
+
+ var selectors = expression.trim().replace(Selectors.RegExps.splitter, function(m0, m1, m2){
+ splitters.push(m1);
+ return ':)' + m2;
+ }).split(':)');
+
+ var items, filtered, item;
+
+ for (var i = 0, l = selectors.length; i < l; i++){
+
+ var selector = selectors[i];
+
+ if (i == 0 && Selectors.RegExps.quick.test(selector)){
+ items = self.getElementsByTagName(selector);
+ continue;
+ }
+
+ var splitter = splitters[i - 1];
+
+ var tagid = Selectors.Utils.parseTagAndID(selector);
+ var tag = tagid[0], id = tagid[1];
+
+ if (i == 0){
+ items = Selectors.Utils.getByTagAndID(self, tag, id);
+ } else {
+ var uniques = {}, found = [];
+ for (var j = 0, k = items.length; j < k; j++) found = Selectors.Getters[splitter](found, items[j], tag, id, uniques);
+ items = found;
+ }
+
+ var parsed = Selectors.Utils.parseSelector(selector);
+
+ if (parsed){
+ filtered = [];
+ for (var m = 0, n = items.length; m < n; m++){
+ item = items[m];
+ if (Selectors.Utils.filter(item, parsed, local)) filtered.push(item);
+ }
+ items = filtered;
+ }
+
+ }
+
+ return items;
+
+ }
+
+};
+
+Selectors.Getters = {
+
+ ' ': function(found, self, tag, id, uniques){
+ var items = Selectors.Utils.getByTagAndID(self, tag, id);
+ for (var i = 0, l = items.length; i < l; i++){
+ var item = items[i];
+ if (Selectors.Utils.chk(item, uniques)) found.push(item);
+ }
+ return found;
+ },
+
+ '>': function(found, self, tag, id, uniques){
+ var children = Selectors.Utils.getByTagAndID(self, tag, id);
+ for (var i = 0, l = children.length; i < l; i++){
+ var child = children[i];
+ if (child.parentNode == self && Selectors.Utils.chk(child, uniques)) found.push(child);
+ }
+ return found;
+ },
+
+ '+': function(found, self, tag, id, uniques){
+ while ((self = self.nextSibling)){
+ if (self.nodeType == 1){
+ if (Selectors.Utils.chk(self, uniques) && Selectors.Filters.byTag(self, tag) && Selectors.Filters.byID(self, id)) found.push(self);
+ break;
+ }
+ }
+ return found;
+ },
+
+ '~': function(found, self, tag, id, uniques){
+ while ((self = self.nextSibling)){
+ if (self.nodeType == 1){
+ if (!Selectors.Utils.chk(self, uniques)) break;
+ if (Selectors.Filters.byTag(self, tag) && Selectors.Filters.byID(self, id)) found.push(self);
+ }
+ }
+ return found;
+ }
+
+};
+
+Selectors.Filters = {
+
+ byTag: function(self, tag){
+ return (tag == '*' || (self.tagName && self.tagName.toLowerCase() == tag));
+ },
+
+ byID: function(self, id){
+ return (!id || (self.id && self.id == id));
+ },
+
+ byClass: function(self, klass){
+ return (self.className && self.className.contains(klass, ' '));
+ },
+
+ byPseudo: function(self, parser, argument, local){
+ return parser.call(self, argument, local);
+ },
+
+ byAttribute: function(self, name, operator, value){
+ var result = Element.prototype.getProperty.call(self, name);
+ if (!result) return (operator == '!=');
+ if (!operator || value == undefined) return true;
+ switch (operator){
+ case '=': return (result == value);
+ case '*=': return (result.contains(value));
+ case '^=': return (result.substr(0, value.length) == value);
+ case '$=': return (result.substr(result.length - value.length) == value);
+ case '!=': return (result != value);
+ case '~=': return result.contains(value, ' ');
+ case '|=': return result.contains(value, '-');
+ }
+ return false;
+ }
+
+};
+
+Selectors.Pseudo = new Hash({
+
+ // w3c pseudo selectors
+
+ checked: function(){
+ return this.checked;
+ },
+
+ empty: function(){
+ return !(this.innerText || this.textContent || '').length;
+ },
+
+ not: function(selector){
+ return !Element.match(this, selector);
+ },
+
+ contains: function(text){
+ return (this.innerText || this.textContent || '').contains(text);
+ },
+
+ 'first-child': function(){
+ return Selectors.Pseudo.index.call(this, 0);
+ },
+
+ 'last-child': function(){
+ var element = this;
+ while ((element = element.nextSibling)){
+ if (element.nodeType == 1) return false;
+ }
+ return true;
+ },
+
+ 'only-child': function(){
+ var prev = this;
+ while ((prev = prev.previousSibling)){
+ if (prev.nodeType == 1) return false;
+ }
+ var next = this;
+ while ((next = next.nextSibling)){
+ if (next.nodeType == 1) return false;
+ }
+ return true;
+ },
+
+ 'nth-child': function(argument, local){
+ argument = (argument == undefined) ? 'n' : argument;
+ var parsed = Selectors.Utils.parseNthArgument(argument);
+ if (parsed.special != 'n') return Selectors.Pseudo[parsed.special].call(this, parsed.a, local);
+ var count = 0;
+ local.positions = local.positions || {};
+ var uid = $uid(this);
+ if (!local.positions[uid]){
+ var self = this;
+ while ((self = self.previousSibling)){
+ if (self.nodeType != 1) continue;
+ count ++;
+ var position = local.positions[$uid(self)];
+ if (position != undefined){
+ count = position + count;
+ break;
+ }
+ }
+ local.positions[uid] = count;
+ }
+ return (local.positions[uid] % parsed.a == parsed.b);
+ },
+
+ // custom pseudo selectors
+
+ index: function(index){
+ var element = this, count = 0;
+ while ((element = element.previousSibling)){
+ if (element.nodeType == 1 && ++count > index) return false;
+ }
+ return (count == index);
+ },
+
+ even: function(argument, local){
+ return Selectors.Pseudo['nth-child'].call(this, '2n+1', local);
+ },
+
+ odd: function(argument, local){
+ return Selectors.Pseudo['nth-child'].call(this, '2n', local);
+ }
+
+});
+
+
+/*
+Script: Domready.js
+ Contains the domready custom event.
+
+License:
+ MIT-style license.
+*/
+
+Element.Events.domready = {
+
+ onAdd: function(fn){
+ if (Browser.loaded) fn.call(this);
+ }
+
+};
+
+(function(){
+
+ var domready = function(){
+ if (Browser.loaded) return;
+ Browser.loaded = true;
+ window.fireEvent('domready');
+ document.fireEvent('domready');
+ };
+
+ if (Browser.Engine.trident){
+ var temp = document.createElement('div');
+ (function(){
+ ($try(function(){
+ temp.doScroll('left');
+ return $(temp).inject(document.body).set('html', 'temp').dispose();
+ })) ? domready() : arguments.callee.delay(50);
+ })();
+ } else if (Browser.Engine.webkit && Browser.Engine.version < 525){
+ (function(){
+ (['loaded', 'complete'].contains(document.readyState)) ? domready() : arguments.callee.delay(50);
+ })();
+ } else {
+ window.addEvent('load', domready);
+ document.addEvent('DOMContentLoaded', domready);
+ }
+
+})();
+
+
+/*
+Script: JSON.js
+ JSON encoder and decoder.
+
+License:
+ MIT-style license.
+
+See Also:
+ <http://www.json.org/>
+*/
+
+var JSON = new Hash({
+
+ $specialChars: {'\b': '\\b', '\t': '\\t', '\n': '\\n', '\f': '\\f', '\r': '\\r', '"' : '\\"', '\\': '\\\\'},
+
+ $replaceChars: function(chr){
+ return JSON.$specialChars[chr] || '\\u00' + Math.floor(chr.charCodeAt() / 16).toString(16) + (chr.charCodeAt() % 16).toString(16);
+ },
+
+ encode: function(obj){
+ switch ($type(obj)){
+ case 'string':
+ return '"' + obj.replace(/[\x00-\x1f\\"]/g, JSON.$replaceChars) + '"';
+ case 'array':
+ return '[' + String(obj.map(JSON.encode).filter($defined)) + ']';
+ case 'object': case 'hash':
+ var string = [];
+ Hash.each(obj, function(value, key){
+ var json = JSON.encode(value);
+ if (json) string.push(JSON.encode(key) + ':' + json);
+ });
+ return '{' + string + '}';
+ case 'number': case 'boolean': return String(obj);
+ case false: return 'null';
+ }
+ return null;
+ },
+
+ decode: function(string, secure){
+ if ($type(string) != 'string' || !string.length) return null;
+ if (secure && !(/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(string.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, ''))) return null;
+ return eval('(' + string + ')');
+ }
+
+});
+
+Native.implement([Hash, Array, String, Number], {
+
+ toJSON: function(){
+ return JSON.encode(this);
+ }
+
+});
+
+
+/*
+Script: Cookie.js
+ Class for creating, loading, and saving browser Cookies.
+
+License:
+ MIT-style license.
+
+Credits:
+ Based on the functions by Peter-Paul Koch (http://quirksmode.org).
+*/
+
+var Cookie = new Class({
+
+ Implements: Options,
+
+ options: {
+ path: false,
+ domain: false,
+ duration: false,
+ secure: false,
+ document: document
+ },
+
+ initialize: function(key, options){
+ this.key = key;
+ this.setOptions(options);
+ },
+
+ write: function(value){
+ value = encodeURIComponent(value);
+ if (this.options.domain) value += '; domain=' + this.options.domain;
+ if (this.options.path) value += '; path=' + this.options.path;
+ if (this.options.duration){
+ var date = new Date();
+ date.setTime(date.getTime() + this.options.duration * 24 * 60 * 60 * 1000);
+ value += '; expires=' + date.toGMTString();
+ }
+ if (this.options.secure) value += '; secure';
+ this.options.document.cookie = this.key + '=' + value;
+ return this;
+ },
+
+ read: function(){
+ var value = this.options.document.cookie.match('(?:^|;)\\s*' + this.key.escapeRegExp() + '=([^;]*)');
+ return (value) ? decodeURIComponent(value[1]) : null;
+ },
+
+ dispose: function(){
+ new Cookie(this.key, $merge(this.options, {duration: -1})).write('');
+ return this;
+ }
+
+});
+
+Cookie.write = function(key, value, options){
+ return new Cookie(key, options).write(value);
+};
+
+Cookie.read = function(key){
+ return new Cookie(key).read();
+};
+
+Cookie.dispose = function(key, options){
+ return new Cookie(key, options).dispose();
+};
+
+
+/*
+Script: Swiff.js
+ Wrapper for embedding SWF movies. Supports (and fixes) External Interface Communication.
+
+License:
+ MIT-style license.
+
+Credits:
+ Flash detection & Internet Explorer + Flash Player 9 fix inspired by SWFObject.
+*/
+
+var Swiff = new Class({
+
+ Implements: [Options],
+
+ options: {
+ id: null,
+ height: 1,
+ width: 1,
+ container: null,
+ properties: {},
+ params: {
+ quality: 'high',
+ allowScriptAccess: 'always',
+ wMode: 'transparent',
+ swLiveConnect: true
+ },
+ callBacks: {},
+ vars: {}
+ },
+
+ toElement: function(){
+ return this.object;
+ },
+
+ initialize: function(path, options){
+ this.instance = 'Swiff_' + $time();
+
+ this.setOptions(options);
+ options = this.options;
+ var id = this.id = options.id || this.instance;
+ var container = $(options.container);
+
+ Swiff.CallBacks[this.instance] = {};
+
+ var params = options.params, vars = options.vars, callBacks = options.callBacks;
+ var properties = $extend({height: options.height, width: options.width}, options.properties);
+
+ var self = this;
+
+ for (var callBack in callBacks){
+ Swiff.CallBacks[this.instance][callBack] = (function(option){
+ return function(){
+ return option.apply(self.object, arguments);
+ };
+ })(callBacks[callBack]);
+ vars[callBack] = 'Swiff.CallBacks.' + this.instance + '.' + callBack;
+ }
+
+ params.flashVars = Hash.toQueryString(vars);
+ if (Browser.Engine.trident){
+ properties.classid = 'clsid:D27CDB6E-AE6D-11cf-96B8-444553540000';
+ params.movie = path;
+ } else {
+ properties.type = 'application/x-shockwave-flash';
+ properties.data = path;
+ }
+ var build = '<object id="' + id + '"';
+ for (var property in properties) build += ' ' + property + '="' + properties[property] + '"';
+ build += '>';
+ for (var param in params){
+ if (params[param]) build += '<param name="' + param + '" value="' + params[param] + '" />';
+ }
+ build += '</object>';
+ this.object = ((container) ? container.empty() : new Element('div')).set('html', build).firstChild;
+ },
+
+ replaces: function(element){
+ element = $(element, true);
+ element.parentNode.replaceChild(this.toElement(), element);
+ return this;
+ },
+
+ inject: function(element){
+ $(element, true).appendChild(this.toElement());
+ return this;
+ },
+
+ remote: function(){
+ return Swiff.remote.apply(Swiff, [this.toElement()].extend(arguments));
+ }
+
+});
+
+Swiff.CallBacks = {};
+
+Swiff.remote = function(obj, fn){
+ var rs = obj.CallFunction('<invoke name="' + fn + '" returntype="javascript">' + __flash__argumentsToXML(arguments, 2) + '</invoke>');
+ return eval(rs);
+};
+
+
+/*
+Script: Fx.js
+ Contains the basic animation logic to be extended by all other Fx Classes.
+
+License:
+ MIT-style license.
+*/
+
+var Fx = new Class({
+
+ Implements: [Chain, Events, Options],
+
+ options: {
+ /*
+ onStart: $empty,
+ onCancel: $empty,
+ onComplete: $empty,
+ */
+ fps: 50,
+ unit: false,
+ duration: 500,
+ link: 'ignore'
+ },
+
+ initialize: function(options){
+ this.subject = this.subject || this;
+ this.setOptions(options);
+ this.options.duration = Fx.Durations[this.options.duration] || this.options.duration.toInt();
+ var wait = this.options.wait;
+ if (wait === false) this.options.link = 'cancel';
+ },
+
+ getTransition: function(){
+ return function(p){
+ return -(Math.cos(Math.PI * p) - 1) / 2;
+ };
+ },
+
+ step: function(){
+ var time = $time();
+ if (time < this.time + this.options.duration){
+ var delta = this.transition((time - this.time) / this.options.duration);
+ this.set(this.compute(this.from, this.to, delta));
+ } else {
+ this.set(this.compute(this.from, this.to, 1));
+ this.complete();
+ }
+ },
+
+ set: function(now){
+ return now;
+ },
+
+ compute: function(from, to, delta){
+ return Fx.compute(from, to, delta);
+ },
+
+ check: function(caller){
+ if (!this.timer) return true;
+ switch (this.options.link){
+ case 'cancel': this.cancel(); return true;
+ case 'chain': this.chain(caller.bind(this, Array.slice(arguments, 1))); return false;
+ }
+ return false;
+ },
+
+ start: function(from, to){
+ if (!this.check(arguments.callee, from, to)) return this;
+ this.from = from;
+ this.to = to;
+ this.time = 0;
+ this.transition = this.getTransition();
+ this.startTimer();
+ this.onStart();
+ return this;
+ },
+
+ complete: function(){
+ if (this.stopTimer()) this.onComplete();
+ return this;
+ },
+
+ cancel: function(){
+ if (this.stopTimer()) this.onCancel();
+ return this;
+ },
+
+ onStart: function(){
+ this.fireEvent('start', this.subject);
+ },
+
+ onComplete: function(){
+ this.fireEvent('complete', this.subject);
+ if (!this.callChain()) this.fireEvent('chainComplete', this.subject);
+ },
+
+ onCancel: function(){
+ this.fireEvent('cancel', this.subject).clearChain();
+ },
+
+ pause: function(){
+ this.stopTimer();
+ return this;
+ },
+
+ resume: function(){
+ this.startTimer();
+ return this;
+ },
+
+ stopTimer: function(){
+ if (!this.timer) return false;
+ this.time = $time() - this.time;
+ this.timer = $clear(this.timer);
+ return true;
+ },
+
+ startTimer: function(){
+ if (this.timer) return false;
+ this.time = $time() - this.time;
+ this.timer = this.step.periodical(Math.round(1000 / this.options.fps), this);
+ return true;
+ }
+
+});
+
+Fx.compute = function(from, to, delta){
+ return (to - from) * delta + from;
+};
+
+Fx.Durations = {'short': 250, 'normal': 500, 'long': 1000};
+
+
+/*
+Script: Fx.CSS.js
+ Contains the CSS animation logic. Used by Fx.Tween, Fx.Morph, Fx.Elements.
+
+License:
+ MIT-style license.
+*/
+
+Fx.CSS = new Class({
+
+ Extends: Fx,
+
+ //prepares the base from/to object
+
+ prepare: function(element, property, values){
+ values = $splat(values);
+ var values1 = values[1];
+ if (!$chk(values1)){
+ values[1] = values[0];
+ values[0] = element.getStyle(property);
+ }
+ var parsed = values.map(this.parse);
+ return {from: parsed[0], to: parsed[1]};
+ },
+
+ //parses a value into an array
+
+ parse: function(value){
+ value = $lambda(value)();
+ value = (typeof value == 'string') ? value.split(' ') : $splat(value);
+ return value.map(function(val){
+ val = String(val);
+ var found = false;
+ Fx.CSS.Parsers.each(function(parser, key){
+ if (found) return;
+ var parsed = parser.parse(val);
+ if ($chk(parsed)) found = {value: parsed, parser: parser};
+ });
+ found = found || {value: val, parser: Fx.CSS.Parsers.String};
+ return found;
+ });
+ },
+
+ //computes by a from and to prepared objects, using their parsers.
+
+ compute: function(from, to, delta){
+ var computed = [];
+ (Math.min(from.length, to.length)).times(function(i){
+ computed.push({value: from[i].parser.compute(from[i].value, to[i].value, delta), parser: from[i].parser});
+ });
+ computed.$family = {name: 'fx:css:value'};
+ return computed;
+ },
+
+ //serves the value as settable
+
+ serve: function(value, unit){
+ if ($type(value) != 'fx:css:value') value = this.parse(value);
+ var returned = [];
+ value.each(function(bit){
+ returned = returned.concat(bit.parser.serve(bit.value, unit));
+ });
+ return returned;
+ },
+
+ //renders the change to an element
+
+ render: function(element, property, value, unit){
+ element.setStyle(property, this.serve(value, unit));
+ },
+
+ //searches inside the page css to find the values for a selector
+
+ search: function(selector){
+ if (Fx.CSS.Cache[selector]) return Fx.CSS.Cache[selector];
+ var to = {};
+ Array.each(document.styleSheets, function(sheet, j){
+ var href = sheet.href;
+ if (href && href.contains('://') && !href.contains(document.domain)) return;
+ var rules = sheet.rules || sheet.cssRules;
+ Array.each(rules, function(rule, i){
+ if (!rule.style) return;
+ var selectorText = (rule.selectorText) ? rule.selectorText.replace(/^\w+/, function(m){
+ return m.toLowerCase();
+ }) : null;
+ if (!selectorText || !selectorText.test('^' + selector + '$')) return;
+ Element.Styles.each(function(value, style){
+ if (!rule.style[style] || Element.ShortStyles[style]) return;
+ value = String(rule.style[style]);
+ to[style] = (value.test(/^rgb/)) ? value.rgbToHex() : value;
+ });
+ });
+ });
+ return Fx.CSS.Cache[selector] = to;
+ }
+
+});
+
+Fx.CSS.Cache = {};
+
+Fx.CSS.Parsers = new Hash({
+
+ Color: {
+ parse: function(value){
+ if (value.match(/^#[0-9a-f]{3,6}$/i)) return value.hexToRgb(true);
+ return ((value = value.match(/(\d+),\s*(\d+),\s*(\d+)/))) ? [value[1], value[2], value[3]] : false;
+ },
+ compute: function(from, to, delta){
+ return from.map(function(value, i){
+ return Math.round(Fx.compute(from[i], to[i], delta));
+ });
+ },
+ serve: function(value){
+ return value.map(Number);
+ }
+ },
+
+ Number: {
+ parse: parseFloat,
+ compute: Fx.compute,
+ serve: function(value, unit){
+ return (unit) ? value + unit : value;
+ }
+ },
+
+ String: {
+ parse: $lambda(false),
+ compute: $arguments(1),
+ serve: $arguments(0)
+ }
+
+});
+
+
+/*
+Script: Fx.Tween.js
+ Formerly Fx.Style, effect to transition any CSS property for an element.
+
+License:
+ MIT-style license.
+*/
+
+Fx.Tween = new Class({
+
+ Extends: Fx.CSS,
+
+ initialize: function(element, options){
+ this.element = this.subject = $(element);
+ this.parent(options);
+ },
+
+ set: function(property, now){
+ if (arguments.length == 1){
+ now = property;
+ property = this.property || this.options.property;
+ }
+ this.render(this.element, property, now, this.options.unit);
+ return this;
+ },
+
+ start: function(property, from, to){
+ if (!this.check(arguments.callee, property, from, to)) return this;
+ var args = Array.flatten(arguments);
+ this.property = this.options.property || args.shift();
+ var parsed = this.prepare(this.element, this.property, args);
+ return this.parent(parsed.from, parsed.to);
+ }
+
+});
+
+Element.Properties.tween = {
+
+ set: function(options){
+ var tween = this.retrieve('tween');
+ if (tween) tween.cancel();
+ return this.eliminate('tween').store('tween:options', $extend({link: 'cancel'}, options));
+ },
+
+ get: function(options){
+ if (options || !this.retrieve('tween')){
+ if (options || !this.retrieve('tween:options')) this.set('tween', options);
+ this.store('tween', new Fx.Tween(this, this.retrieve('tween:options')));
+ }
+ return this.retrieve('tween');
+ }
+
+};
+
+Element.implement({
+
+ tween: function(property, from, to){
+ this.get('tween').start(arguments);
+ return this;
+ },
+
+ fade: function(how){
+ var fade = this.get('tween'), o = 'opacity', toggle;
+ how = $pick(how, 'toggle');
+ switch (how){
+ case 'in': fade.start(o, 1); break;
+ case 'out': fade.start(o, 0); break;
+ case 'show': fade.set(o, 1); break;
+ case 'hide': fade.set(o, 0); break;
+ case 'toggle':
+ var flag = this.retrieve('fade:flag', this.get('opacity') == 1);
+ fade.start(o, (flag) ? 0 : 1);
+ this.store('fade:flag', !flag);
+ toggle = true;
+ break;
+ default: fade.start(o, arguments);
+ }
+ if (!toggle) this.eliminate('fade:flag');
+ return this;
+ },
+
+ highlight: function(start, end){
+ if (!end){
+ end = this.retrieve('highlight:original', this.getStyle('background-color'));
+ end = (end == 'transparent') ? '#fff' : end;
+ }
+ var tween = this.get('tween');
+ tween.start('background-color', start || '#ffff88', end).chain(function(){
+ this.setStyle('background-color', this.retrieve('highlight:original'));
+ tween.callChain();
+ }.bind(this));
+ return this;
+ }
+
+});
+
+
+/*
+Script: Fx.Morph.js
+ Formerly Fx.Styles, effect to transition any number of CSS properties for an element using an object of rules, or CSS based selector rules.
+
+License:
+ MIT-style license.
+*/
+
+Fx.Morph = new Class({
+
+ Extends: Fx.CSS,
+
+ initialize: function(element, options){
+ this.element = this.subject = $(element);
+ this.parent(options);
+ },
+
+ set: function(now){
+ if (typeof now == 'string') now = this.search(now);
+ for (var p in now) this.render(this.element, p, now[p], this.options.unit);
+ return this;
+ },
+
+ compute: function(from, to, delta){
+ var now = {};
+ for (var p in from) now[p] = this.parent(from[p], to[p], delta);
+ return now;
+ },
+
+ start: function(properties){
+ if (!this.check(arguments.callee, properties)) return this;
+ if (typeof properties == 'string') properties = this.search(properties);
+ var from = {}, to = {};
+ for (var p in properties){
+ var parsed = this.prepare(this.element, p, properties[p]);
+ from[p] = parsed.from;
+ to[p] = parsed.to;
+ }
+ return this.parent(from, to);
+ }
+
+});
+
+Element.Properties.morph = {
+
+ set: function(options){
+ var morph = this.retrieve('morph');
+ if (morph) morph.cancel();
+ return this.eliminate('morph').store('morph:options', $extend({link: 'cancel'}, options));
+ },
+
+ get: function(options){
+ if (options || !this.retrieve('morph')){
+ if (options || !this.retrieve('morph:options')) this.set('morph', options);
+ this.store('morph', new Fx.Morph(this, this.retrieve('morph:options')));
+ }
+ return this.retrieve('morph');
+ }
+
+};
+
+Element.implement({
+
+ morph: function(props){
+ this.get('morph').start(props);
+ return this;
+ }
+
+});
+
+
+/*
+Script: Fx.Transitions.js
+ Contains a set of advanced transitions to be used with any of the Fx Classes.
+
+License:
+ MIT-style license.
+
+Credits:
+ Easing Equations by Robert Penner, <http://www.robertpenner.com/easing/>, modified and optimized to be used with MooTools.
+*/
+
+Fx.implement({
+
+ getTransition: function(){
+ var trans = this.options.transition || Fx.Transitions.Sine.easeInOut;
+ if (typeof trans == 'string'){
+ var data = trans.split(':');
+ trans = Fx.Transitions;
+ trans = trans[data[0]] || trans[data[0].capitalize()];
+ if (data[1]) trans = trans['ease' + data[1].capitalize() + (data[2] ? data[2].capitalize() : '')];
+ }
+ return trans;
+ }
+
+});
+
+Fx.Transition = function(transition, params){
+ params = $splat(params);
+ return $extend(transition, {
+ easeIn: function(pos){
+ return transition(pos, params);
+ },
+ easeOut: function(pos){
+ return 1 - transition(1 - pos, params);
+ },
+ easeInOut: function(pos){
+ return (pos <= 0.5) ? transition(2 * pos, params) / 2 : (2 - transition(2 * (1 - pos), params)) / 2;
+ }
+ });
+};
+
+Fx.Transitions = new Hash({
+
+ linear: $arguments(0)
+
+});
+
+Fx.Transitions.extend = function(transitions){
+ for (var transition in transitions) Fx.Transitions[transition] = new Fx.Transition(transitions[transition]);
+};
+
+Fx.Transitions.extend({
+
+ Pow: function(p, x){
+ return Math.pow(p, x[0] || 6);
+ },
+
+ Expo: function(p){
+ return Math.pow(2, 8 * (p - 1));
+ },
+
+ Circ: function(p){
+ return 1 - Math.sin(Math.acos(p));
+ },
+
+ Sine: function(p){
+ return 1 - Math.sin((1 - p) * Math.PI / 2);
+ },
+
+ Back: function(p, x){
+ x = x[0] || 1.618;
+ return Math.pow(p, 2) * ((x + 1) * p - x);
+ },
+
+ Bounce: function(p){
+ var value;
+ for (var a = 0, b = 1; 1; a += b, b /= 2){
+ if (p >= (7 - 4 * a) / 11){
+ value = b * b - Math.pow((11 - 6 * a - 11 * p) / 4, 2);
+ break;
+ }
+ }
+ return value;
+ },
+
+ Elastic: function(p, x){
+ return Math.pow(2, 10 * --p) * Math.cos(20 * p * Math.PI * (x[0] || 1) / 3);
+ }
+
+});
+
+['Quad', 'Cubic', 'Quart', 'Quint'].each(function(transition, i){
+ Fx.Transitions[transition] = new Fx.Transition(function(p){
+ return Math.pow(p, [i + 2]);
+ });
+});
+
+
+/*
+Script: Request.js
+ Powerful all purpose Request Class. Uses XMLHTTPRequest.
+
+License:
+ MIT-style license.
+*/
+
+var Request = new Class({
+
+ Implements: [Chain, Events, Options],
+
+ options: {/*
+ onRequest: $empty,
+ onComplete: $empty,
+ onCancel: $empty,
+ onSuccess: $empty,
+ onFailure: $empty,
+ onException: $empty,*/
+ url: '',
+ data: '',
+ headers: {
+ 'X-Requested-With': 'XMLHttpRequest',
+ 'Accept': 'text/javascript, text/html, application/xml, text/xml, */*'
+ },
+ async: true,
+ format: false,
+ method: 'post',
+ link: 'ignore',
+ isSuccess: null,
+ emulation: true,
+ urlEncoded: true,
+ encoding: 'utf-8',
+ evalScripts: false,
+ evalResponse: false
+ },
+
+ initialize: function(options){
+ this.xhr = new Browser.Request();
+ this.setOptions(options);
+ this.options.isSuccess = this.options.isSuccess || this.isSuccess;
+ this.headers = new Hash(this.options.headers);
+ },
+
+ onStateChange: function(){
+ if (this.xhr.readyState != 4 || !this.running) return;
+ this.running = false;
+ this.status = 0;
+ $try(function(){
+ this.status = this.xhr.status;
+ }.bind(this));
+ if (this.options.isSuccess.call(this, this.status)){
+ this.response = {text: this.xhr.responseText, xml: this.xhr.responseXML};
+ this.success(this.response.text, this.response.xml);
+ } else {
+ this.response = {text: null, xml: null};
+ this.failure();
+ }
+ this.xhr.onreadystatechange = $empty;
+ },
+
+ isSuccess: function(){
+ return ((this.status >= 200) && (this.status < 300));
+ },
+
+ processScripts: function(text){
+ if (this.options.evalResponse || (/(ecma|java)script/).test(this.getHeader('Content-type'))) return $exec(text);
+ return text.stripScripts(this.options.evalScripts);
+ },
+
+ success: function(text, xml){
+ this.onSuccess(this.processScripts(text), xml);
+ },
+
+ onSuccess: function(){
+ this.fireEvent('complete', arguments).fireEvent('success', arguments).callChain();
+ },
+
+ failure: function(){
+ this.onFailure();
+ },
+
+ onFailure: function(){
+ this.fireEvent('complete').fireEvent('failure', this.xhr);
+ },
+
+ setHeader: function(name, value){
+ this.headers.set(name, value);
+ return this;
+ },
+
+ getHeader: function(name){
+ return $try(function(){
+ return this.xhr.getResponseHeader(name);
+ }.bind(this));
+ },
+
+ check: function(caller){
+ if (!this.running) return true;
+ switch (this.options.link){
+ case 'cancel': this.cancel(); return true;
+ case 'chain': this.chain(caller.bind(this, Array.slice(arguments, 1))); return false;
+ }
+ return false;
+ },
+
+ send: function(options){
+ if (!this.check(arguments.callee, options)) return this;
+ this.running = true;
+
+ var type = $type(options);
+ if (type == 'string' || type == 'element') options = {data: options};
+
+ var old = this.options;
+ options = $extend({data: old.data, url: old.url, method: old.method}, options);
+ var data = options.data, url = options.url, method = options.method;
+
+ switch ($type(data)){
+ case 'element': data = $(data).toQueryString(); break;
+ case 'object': case 'hash': data = Hash.toQueryString(data);
+ }
+
+ if (this.options.format){
+ var format = 'format=' + this.options.format;
+ data = (data) ? format + '&' + data : format;
+ }
+
+ if (this.options.emulation && ['put', 'delete'].contains(method)){
+ var _method = '_method=' + method;
+ data = (data) ? _method + '&' + data : _method;
+ method = 'post';
+ }
+
+ if (this.options.urlEncoded && method == 'post'){
+ var encoding = (this.options.encoding) ? '; charset=' + this.options.encoding : '';
+ this.headers.set('Content-type', 'application/x-www-form-urlencoded' + encoding);
+ }
+
+ if (data && method == 'get'){
+ url = url + (url.contains('?') ? '&' : '?') + data;
+ data = null;
+ }
+
+ this.xhr.open(method.toUpperCase(), url, this.options.async);
+
+ this.xhr.onreadystatechange = this.onStateChange.bind(this);
+
+ this.headers.each(function(value, key){
+ try {
+ this.xhr.setRequestHeader(key, value);
+ } catch (e){
+ this.fireEvent('exception', [key, value]);
+ }
+ }, this);
+
+ this.fireEvent('request');
+ this.xhr.send(data);
+ if (!this.options.async) this.onStateChange();
+ return this;
+ },
+
+ cancel: function(){
+ if (!this.running) return this;
+ this.running = false;
+ this.xhr.abort();
+ this.xhr.onreadystatechange = $empty;
+ this.xhr = new Browser.Request();
+ this.fireEvent('cancel');
+ return this;
+ }
+
+});
+
+(function(){
+
+var methods = {};
+['get', 'post', 'put', 'delete', 'GET', 'POST', 'PUT', 'DELETE'].each(function(method){
+ methods[method] = function(){
+ var params = Array.link(arguments, {url: String.type, data: $defined});
+ return this.send($extend(params, {method: method.toLowerCase()}));
+ };
+});
+
+Request.implement(methods);
+
+})();
+
+Element.Properties.send = {
+
+ set: function(options){
+ var send = this.retrieve('send');
+ if (send) send.cancel();
+ return this.eliminate('send').store('send:options', $extend({
+ data: this, link: 'cancel', method: this.get('method') || 'post', url: this.get('action')
+ }, options));
+ },
+
+ get: function(options){
+ if (options || !this.retrieve('send')){
+ if (options || !this.retrieve('send:options')) this.set('send', options);
+ this.store('send', new Request(this.retrieve('send:options')));
+ }
+ return this.retrieve('send');
+ }
+
+};
+
+Element.implement({
+
+ send: function(url){
+ var sender = this.get('send');
+ sender.send({data: this, url: url || sender.options.url});
+ return this;
+ }
+
+});
+
+
+/*
+Script: Request.HTML.js
+ Extends the basic Request Class with additional methods for interacting with HTML responses.
+
+License:
+ MIT-style license.
+*/
+
+Request.HTML = new Class({
+
+ Extends: Request,
+
+ options: {
+ update: false,
+ evalScripts: true,
+ filter: false
+ },
+
+ processHTML: function(text){
+ var match = text.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
+ text = (match) ? match[1] : text;
+
+ var container = new Element('div');
+
+ return $try(function(){
+ var root = '<root>' + text + '</root>', doc;
+ if (Browser.Engine.trident){
+ doc = new ActiveXObject('Microsoft.XMLDOM');
+ doc.async = false;
+ doc.loadXML(root);
+ } else {
+ doc = new DOMParser().parseFromString(root, 'text/xml');
+ }
+ root = doc.getElementsByTagName('root')[0];
+ for (var i = 0, k = root.childNodes.length; i < k; i++){
+ var child = Element.clone(root.childNodes[i], true, true);
+ if (child) container.grab(child);
+ }
+ return container;
+ }) || container.set('html', text);
+ },
+
+ success: function(text){
+ var options = this.options, response = this.response;
+
+ response.html = text.stripScripts(function(script){
+ response.javascript = script;
+ });
+
+ var temp = this.processHTML(response.html);
+
+ response.tree = temp.childNodes;
+ response.elements = temp.getElements('*');
+
+ if (options.filter) response.tree = response.elements.filter(options.filter);
+ if (options.update) $(options.update).empty().set('html', response.html);
+ if (options.evalScripts) $exec(response.javascript);
+
+ this.onSuccess(response.tree, response.elements, response.html, response.javascript);
+ }
+
+});
+
+Element.Properties.load = {
+
+ set: function(options){
+ var load = this.retrieve('load');
+ if (load) load.cancel();
+ return this.eliminate('load').store('load:options', $extend({data: this, link: 'cancel', update: this, method: 'get'}, options));
+ },
+
+ get: function(options){
+ if (options || ! this.retrieve('load')){
+ if (options || !this.retrieve('load:options')) this.set('load', options);
+ this.store('load', new Request.HTML(this.retrieve('load:options')));
+ }
+ return this.retrieve('load');
+ }
+
+};
+
+Element.implement({
+
+ load: function(){
+ this.get('load').send(Array.link(arguments, {data: Object.type, url: String.type}));
+ return this;
+ }
+
+});
+
+
+/*
+Script: Request.JSON.js
+ Extends the basic Request Class with additional methods for sending and receiving JSON data.
+
+License:
+ MIT-style license.
+*/
+
+Request.JSON = new Class({
+
+ Extends: Request,
+
+ options: {
+ secure: true
+ },
+
+ initialize: function(options){
+ this.parent(options);
+ this.headers.extend({'Accept': 'application/json', 'X-Request': 'JSON'});
+ },
+
+ success: function(text){
+ this.response.json = JSON.decode(text, this.options.secure);
+ this.onSuccess(this.response.json, text);
+ }
+
+});
diff --git a/ipawebui/templates/__init__.py b/ipawebui/templates/__init__.py
new file mode 100644
index 000000000..10d4cf8ca
--- /dev/null
+++ b/ipawebui/templates/__init__.py
@@ -0,0 +1,21 @@
+# 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
+
+"""
+Sub-package containing Kid templates.
+"""
diff --git a/ipawebui/templates/form.kid b/ipawebui/templates/form.kid
new file mode 100644
index 000000000..640157005
--- /dev/null
+++ b/ipawebui/templates/form.kid
@@ -0,0 +1,16 @@
+<?xml version='1.0' encoding='utf-8'?>
+<html xmlns:py="http://purl.org/kid/ns#">
+
+<head>
+ <title>Hello</title>
+</head>
+
+<body>
+ <table>
+ <tr py:for="param in command.params()">
+ <td py:content="param.name"/>
+ </tr>
+ </table>
+</body>
+
+</html>
diff --git a/ipawebui/templates/main.kid b/ipawebui/templates/main.kid
new file mode 100644
index 000000000..692f2b575
--- /dev/null
+++ b/ipawebui/templates/main.kid
@@ -0,0 +1,14 @@
+<?xml version='1.0' encoding='utf-8'?>
+<html xmlns:py="http://purl.org/kid/ns#">
+
+<head>
+ <title>FreeIPA</title>
+</head>
+
+<body>
+ <p py:for="name in api.Command">
+ <a href="${name}" py:content="name"/>
+ </p>
+</body>
+
+</html>
diff --git a/lite-webui.py b/lite-webui.py
new file mode 100755
index 000000000..74f6e0f70
--- /dev/null
+++ b/lite-webui.py
@@ -0,0 +1,45 @@
+#!/usr/bin/env python
+
+# 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
+
+"""
+In-tree Web UI using cherrypy.
+"""
+
+from cherrypy import expose, config, quickstart
+from ipawebui.templates import form, main
+from ipawebui import controller
+from ipalib import api
+
+api.load_plugins()
+api.finalize()
+
+
+class root(object):
+ index = controller.Index(api, main)
+
+ def __init__(self):
+ for cmd in api.Command():
+ ctr = controller.Command(cmd, form)
+ setattr(self, cmd.name, ctr)
+
+
+if __name__ == '__main__':
+ quickstart(root())
diff --git a/lite-xmlrpc.py b/lite-xmlrpc.py
new file mode 100755
index 000000000..ee03adae7
--- /dev/null
+++ b/lite-xmlrpc.py
@@ -0,0 +1,175 @@
+#!/usr/bin/env python
+
+# Authors:
+# Rob Crittenden <rcritten@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
+
+"""
+In-tree XML-RPC server using SimpleXMLRPCServer.
+"""
+
+import sys
+import SimpleXMLRPCServer
+import logging
+import xmlrpclib
+import re
+import threading
+import commands
+from ipalib import api
+from ipalib import config
+from ipaserver import conn
+from ipaserver.servercore import context
+from ipalib.util import xmlrpc_unmarshal
+import traceback
+import krbV
+
+class StoppableXMLRPCServer(SimpleXMLRPCServer.SimpleXMLRPCServer):
+ """Override of TIME_WAIT"""
+ allow_reuse_address = True
+
+ def serve_forever(self):
+ self.stop = False
+ while not self.stop:
+ self.handle_request()
+
+class LoggingSimpleXMLRPCRequestHandler(SimpleXMLRPCServer.SimpleXMLRPCRequestHandler):
+ """Overides the default SimpleXMLRPCRequestHander to support logging.
+ Logs client IP and the XML request and response.
+ """
+
+ def parse(self, given):
+ """Convert the incoming arguments into the format IPA expects"""
+ args = []
+ kw = {}
+ for g in given:
+ kw[g] = unicode(given[g])
+ return (args, kw)
+
+ def _dispatch(self, method, params):
+ """
+ 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
+ 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:
+ params, method = xmlrpclib.loads(data)
+
+ # generate response
+ if dispatch_method is not None:
+ response = dispatch_method(method, params)
+ else:
+ response = self._dispatch(method, params)
+ # wrap response in a singleton tuple
+ response = (response,)
+ response = xmlrpclib.dumps(response, methodresponse=1)
+ except:
+ # report exception back to client. This is needed to report
+ # tracebacks found in server code.
+ e_class, e = sys.exc_info()[:2]
+ # FIXME, need to get this number from somewhere...
+ faultCode = getattr(e_class,'faultCode',1)
+ tb_str = ''.join(traceback.format_exception(*sys.exc_info()))
+ faultString = tb_str
+ response = xmlrpclib.dumps(xmlrpclib.Fault(faultCode, faultString))
+
+ return response
+
+ def do_POST(self):
+ clientIP, port = self.client_address
+ # Log client IP and Port
+ logger.info('Client IP: %s - Port: %s' % (clientIP, port))
+ try:
+ # get arguments
+ data = self.rfile.read(int(self.headers["content-length"]))
+
+ # unmarshal the XML data
+ params, method = xmlrpclib.loads(data)
+ logger.info('Call to %s(%s) from %s:%s', method,
+ ', '.join(repr(p) for p in params),
+ clientIP, port
+ )
+
+ # Log client request
+ logger.debug('Client request: \n%s\n' % data)
+
+ response = self._marshaled_dispatch(
+ data, getattr(self, '_dispatch', None))
+
+ # Log server response
+ logger.debug('Server response: \n%s\n' % response)
+ except Exception, e:
+ # This should only happen if the module is buggy
+ # internal error, report as HTTP server error
+ print e
+ self.send_response(500)
+ self.end_headers()
+ else:
+ # got a valid XML-RPC response
+ self.send_response(200)
+ self.send_header("Content-type", "text/xml")
+ self.send_header("Content-length", str(len(response)))
+ self.end_headers()
+ self.wfile.write(response)
+
+ # shut down the connection
+ self.wfile.flush()
+ self.connection.shutdown(1)
+
+
+if __name__ == '__main__':
+ api.bootstrap_with_global_options(context='server')
+ api.finalize()
+ logger = api.log
+
+ # Set up the server
+ XMLRPCServer = StoppableXMLRPCServer(
+ ('', api.env.lite_xmlrpc_port),
+ LoggingSimpleXMLRPCRequestHandler
+ )
+ XMLRPCServer.register_introspection_functions()
+
+ # Get and register all the methods
+
+ for cmd in api.Command:
+ logger.debug('registering %s', cmd)
+ XMLRPCServer.register_function(api.Command[cmd], cmd)
+ funcs = XMLRPCServer.funcs
+
+ 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()
+ logger.info('Server shutdown.')
diff --git a/make-doc b/make-doc
new file mode 100755
index 000000000..a9376e8a7
--- /dev/null
+++ b/make-doc
@@ -0,0 +1,29 @@
+#!/bin/bash
+
+# Hackish script to generate documentation using epydoc
+
+sources="ipalib ipaserver ipawebui tests"
+out="./freeipa2-dev-doc"
+
+init="./ipalib/__init__.py"
+echo "Looking for $init"
+if [[ ! -f $init ]]
+then
+ echo "Error: You do not appear to be in the project directory"
+ exit 1
+fi
+echo "You appear to be in the project directory"
+
+# Documentation
+if [[ -d $out ]]
+then
+ echo "Removing old $out directory"
+ rm -r $out
+fi
+echo "Creating documentation in $out"
+
+epydoc -v --html --no-frames --include-log \
+ --name="FreeIPA v2 developer documentation" \
+ --docformat=restructuredtext \
+ --output=$out \
+ $sources
diff --git a/make-test b/make-test
new file mode 100755
index 000000000..1a401635c
--- /dev/null
+++ b/make-test
@@ -0,0 +1,32 @@
+#!/bin/bash
+
+# Script to run nosetests under multiple versions of Python
+
+versions="python2.4 python2.5 python2.6"
+
+for name in $versions
+do
+ executable="/usr/bin/$name"
+ if [[ -f $executable ]]; then
+ echo "[ $name: Starting tests... ]"
+ ((runs += 1))
+ if $executable /usr/bin/nosetests -v --with-doctest
+ then
+ echo "[ $name: Tests OK ]"
+ else
+ echo "[ $name: Tests FAILED ]"
+ ((failures += 1))
+ fi
+ else
+ echo "[ $name: Not found ]"
+ fi
+ echo ""
+done
+
+if [ $failures ]; then
+ echo "[ Ran under $runs version(s); FAILED under $failures version(s) ]"
+ echo "FAIL!"
+ exit $failures
+else
+ echo "[ Ran under $runs version(s); all OK ]"
+fi
diff --git a/setup.py b/setup.py
new file mode 100755
index 000000000..ad903748a
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,46 @@
+#!/usr/bin/python
+
+# 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
+
+"""
+Python-level packaging using distutils.
+"""
+
+from distutils.core import setup
+
+setup(
+ name='freeipa',
+ version='1.99.0',
+ license='GPLv2+',
+ url='http://freeipa.org/',
+ packages=[
+ 'ipalib',
+ 'ipalib.plugins',
+ 'ipaserver',
+ 'ipaserver.plugins',
+ 'ipawebui',
+ 'ipawebui.templates',
+ ],
+ package_data={
+ 'ipawebui.templates': ['*.kid'],
+ 'ipawebui': ['static/*'],
+ },
+ scripts=['ipa'],
+)
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 000000000..4550e6bcd
--- /dev/null
+++ b/tests/__init__.py
@@ -0,0 +1,22 @@
+# 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
+
+"""
+Package containing all unit tests.
+"""
diff --git a/tests/data.py b/tests/data.py
new file mode 100644
index 000000000..cf646ea9d
--- /dev/null
+++ b/tests/data.py
@@ -0,0 +1,38 @@
+# 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
+
+"""
+Data frequently used in the unit tests, especially Unicode related tests.
+"""
+
+import struct
+
+
+# A string that should have bytes 'x\00' through '\xff':
+binary_bytes = ''.join(struct.pack('B', d) for d in xrange(256))
+assert '\x00' in binary_bytes and '\xff' in binary_bytes
+assert type(binary_bytes) is str and len(binary_bytes) == 256
+
+# A UTF-8 encoded str:
+utf8_bytes = '\xd0\x9f\xd0\xb0\xd0\xb2\xd0\xb5\xd0\xbb'
+
+# The same UTF-8 data decoded (a unicode instance):
+unicode_str = u'\u041f\u0430\u0432\u0435\u043b'
+assert utf8_bytes.decode('UTF-8') == unicode_str
+assert unicode_str.encode('UTF-8') == utf8_bytes
diff --git a/tests/test_ipalib/__init__.py b/tests/test_ipalib/__init__.py
new file mode 100644
index 000000000..113881ebf
--- /dev/null
+++ b/tests/test_ipalib/__init__.py
@@ -0,0 +1,22 @@
+# 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
+
+"""
+Sub-package containing unit tests for `ipalib` package.
+"""
diff --git a/tests/test_ipalib/test_backend.py b/tests/test_ipalib/test_backend.py
new file mode 100644
index 000000000..88bd2da47
--- /dev/null
+++ b/tests/test_ipalib/test_backend.py
@@ -0,0 +1,55 @@
+# 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
+
+"""
+Test the `ipalib.backend` module.
+"""
+
+from ipalib import backend, plugable, errors
+from tests.util import ClassChecker, raises
+
+
+class test_Backend(ClassChecker):
+ """
+ Test the `ipalib.backend.Backend` class.
+ """
+
+ _cls = backend.Backend
+
+ def test_class(self):
+ assert self.cls.__bases__ == (plugable.Plugin,)
+ assert self.cls.__proxy__ is False
+
+
+class test_Context(ClassChecker):
+ """
+ Test the `ipalib.backend.Context` class.
+ """
+
+ _cls = backend.Context
+
+ def test_get_value(self):
+ """
+ Test the `ipalib.backend.Context.get_value` method.
+ """
+ class Subclass(self.cls):
+ pass
+ o = Subclass()
+ e = raises(NotImplementedError, o.get_value)
+ assert str(e) == 'Subclass.get_value()'
diff --git a/tests/test_ipalib/test_base.py b/tests/test_ipalib/test_base.py
new file mode 100644
index 000000000..ce88f23f8
--- /dev/null
+++ b/tests/test_ipalib/test_base.py
@@ -0,0 +1,352 @@
+# 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
+
+"""
+Test the `ipalib.base` module.
+"""
+
+from tests.util import ClassChecker, raises
+from ipalib.constants import NAME_REGEX, NAME_ERROR
+from ipalib.constants import TYPE_ERROR, SET_ERROR, DEL_ERROR, OVERRIDE_ERROR
+from ipalib import base
+
+
+class test_ReadOnly(ClassChecker):
+ """
+ Test the `ipalib.base.ReadOnly` class
+ """
+ _cls = base.ReadOnly
+
+ def test_lock(self):
+ """
+ Test the `ipalib.base.ReadOnly.__lock__` method.
+ """
+ o = self.cls()
+ assert o._ReadOnly__locked is False
+ o.__lock__()
+ assert o._ReadOnly__locked is True
+ e = raises(AssertionError, o.__lock__) # Can only be locked once
+ assert str(e) == '__lock__() can only be called once'
+ assert o._ReadOnly__locked is True # This should still be True
+
+ def test_islocked(self):
+ """
+ Test the `ipalib.base.ReadOnly.__islocked__` method.
+ """
+ o = self.cls()
+ assert o.__islocked__() is False
+ o.__lock__()
+ assert o.__islocked__() is True
+
+ def test_setattr(self):
+ """
+ Test the `ipalib.base.ReadOnly.__setattr__` method.
+ """
+ o = self.cls()
+ o.attr1 = 'Hello, world!'
+ assert o.attr1 == 'Hello, world!'
+ o.__lock__()
+ for name in ('attr1', 'attr2'):
+ e = raises(AttributeError, setattr, o, name, 'whatever')
+ assert str(e) == SET_ERROR % ('ReadOnly', name, 'whatever')
+ assert o.attr1 == 'Hello, world!'
+
+ def test_delattr(self):
+ """
+ Test the `ipalib.base.ReadOnly.__delattr__` method.
+ """
+ o = self.cls()
+ o.attr1 = 'Hello, world!'
+ o.attr2 = 'How are you?'
+ assert o.attr1 == 'Hello, world!'
+ assert o.attr2 == 'How are you?'
+ del o.attr1
+ assert not hasattr(o, 'attr1')
+ o.__lock__()
+ e = raises(AttributeError, delattr, o, 'attr2')
+ assert str(e) == DEL_ERROR % ('ReadOnly', 'attr2')
+ assert o.attr2 == 'How are you?'
+
+
+def test_lock():
+ """
+ Test the `ipalib.base.lock` function
+ """
+ f = base.lock
+
+ # Test with ReadOnly instance:
+ o = base.ReadOnly()
+ assert o.__islocked__() is False
+ assert f(o) is o
+ assert o.__islocked__() is True
+ e = raises(AssertionError, f, o)
+ assert str(e) == 'already locked: %r' % o
+
+ # Test with another class implemented locking protocol:
+ class Lockable(object):
+ __locked = False
+ def __lock__(self):
+ self.__locked = True
+ def __islocked__(self):
+ return self.__locked
+ o = Lockable()
+ assert o.__islocked__() is False
+ assert f(o) is o
+ assert o.__islocked__() is True
+ e = raises(AssertionError, f, o)
+ assert str(e) == 'already locked: %r' % o
+
+ # Test with a class incorrectly implementing the locking protocol:
+ class Broken(object):
+ def __lock__(self):
+ pass
+ def __islocked__(self):
+ return False
+ o = Broken()
+ e = raises(AssertionError, f, o)
+ assert str(e) == 'failed to lock: %r' % o
+
+
+def test_islocked():
+ """
+ Test the `ipalib.base.islocked` function.
+ """
+ f = base.islocked
+
+ # Test with ReadOnly instance:
+ o = base.ReadOnly()
+ assert f(o) is False
+ o.__lock__()
+ assert f(o) is True
+
+ # Test with another class implemented locking protocol:
+ class Lockable(object):
+ __locked = False
+ def __lock__(self):
+ self.__locked = True
+ def __islocked__(self):
+ return self.__locked
+ o = Lockable()
+ assert f(o) is False
+ o.__lock__()
+ assert f(o) is True
+
+ # Test with a class incorrectly implementing the locking protocol:
+ class Broken(object):
+ __lock__ = False
+ def __islocked__(self):
+ return False
+ o = Broken()
+ e = raises(AssertionError, f, o)
+ assert str(e) == 'no __lock__() method: %r' % o
+
+
+def test_check_name():
+ """
+ Test the `ipalib.base.check_name` function.
+ """
+ f = base.check_name
+ okay = [
+ 'user_add',
+ 'stuff2junk',
+ 'sixty9',
+ ]
+ nope = [
+ '_user_add',
+ '__user_add',
+ 'user_add_',
+ 'user_add__',
+ '_user_add_',
+ '__user_add__',
+ '60nine',
+ ]
+ for name in okay:
+ assert name is f(name)
+ e = raises(TypeError, f, unicode(name))
+ assert str(e) == TYPE_ERROR % ('name', str, unicode(name), unicode)
+ for name in nope:
+ e = raises(ValueError, f, name)
+ assert str(e) == NAME_ERROR % (NAME_REGEX, name)
+ for name in okay:
+ e = raises(ValueError, f, name.upper())
+ assert str(e) == NAME_ERROR % (NAME_REGEX, name.upper())
+
+
+def membername(i):
+ return 'member%03d' % i
+
+
+class DummyMember(object):
+ def __init__(self, i):
+ self.i = i
+ self.name = membername(i)
+
+
+def gen_members(*indexes):
+ return tuple(DummyMember(i) for i in indexes)
+
+
+class test_NameSpace(ClassChecker):
+ """
+ Test the `ipalib.base.NameSpace` class.
+ """
+ _cls = base.NameSpace
+
+ def new(self, count, sort=True):
+ members = tuple(DummyMember(i) for i in xrange(count, 0, -1))
+ assert len(members) == count
+ o = self.cls(members, sort=sort)
+ return (o, members)
+
+ def test_init(self):
+ """
+ Test the `ipalib.base.NameSpace.__init__` method.
+ """
+ o = self.cls([])
+ assert len(o) == 0
+ assert list(o) == []
+ assert list(o()) == []
+
+ # Test members as attribute and item:
+ for cnt in (3, 42):
+ for sort in (True, False):
+ (o, members) = self.new(cnt, sort=sort)
+ assert len(members) == cnt
+ for m in members:
+ assert getattr(o, m.name) is m
+ assert o[m.name] is m
+
+ # Test that TypeError is raised if sort is not a bool:
+ e = raises(TypeError, self.cls, [], sort=None)
+ assert str(e) == TYPE_ERROR % ('sort', bool, None, type(None))
+
+ # Test that AttributeError is raised with duplicate member name:
+ members = gen_members(0, 1, 2, 1, 3)
+ e = raises(AttributeError, self.cls, members)
+ assert str(e) == OVERRIDE_ERROR % (
+ 'NameSpace', membername(1), members[1], members[3]
+ )
+
+ def test_len(self):
+ """
+ Test the `ipalib.base.NameSpace.__len__` method.
+ """
+ for count in (5, 18, 127):
+ (o, members) = self.new(count)
+ assert len(o) == count
+ (o, members) = self.new(count, sort=False)
+ assert len(o) == count
+
+ def test_iter(self):
+ """
+ Test the `ipalib.base.NameSpace.__iter__` method.
+ """
+ (o, members) = self.new(25)
+ assert list(o) == sorted(m.name for m in members)
+ (o, members) = self.new(25, sort=False)
+ assert list(o) == list(m.name for m in members)
+
+ def test_call(self):
+ """
+ Test the `ipalib.base.NameSpace.__call__` method.
+ """
+ (o, members) = self.new(25)
+ assert list(o()) == sorted(members, key=lambda m: m.name)
+ (o, members) = self.new(25, sort=False)
+ assert tuple(o()) == members
+
+ def test_contains(self):
+ """
+ Test the `ipalib.base.NameSpace.__contains__` method.
+ """
+ yes = (99, 3, 777)
+ no = (9, 333, 77)
+ for sort in (True, False):
+ members = gen_members(*yes)
+ o = self.cls(members, sort=sort)
+ for i in yes:
+ assert membername(i) in o
+ assert membername(i).upper() not in o
+ for i in no:
+ assert membername(i) not in o
+
+ def test_getitem(self):
+ """
+ Test the `ipalib.base.NameSpace.__getitem__` method.
+ """
+ cnt = 17
+ for sort in (True, False):
+ (o, members) = self.new(cnt, sort=sort)
+ assert len(members) == cnt
+ if sort is True:
+ members = tuple(sorted(members, key=lambda m: m.name))
+
+ # Test str keys:
+ for m in members:
+ assert o[m.name] is m
+ e = raises(KeyError, o.__getitem__, 'nope')
+
+ # Test int indexes:
+ for i in xrange(cnt):
+ assert o[i] is members[i]
+ e = raises(IndexError, o.__getitem__, cnt)
+
+ # Test negative int indexes:
+ for i in xrange(1, cnt + 1):
+ assert o[-i] is members[-i]
+ e = raises(IndexError, o.__getitem__, -(cnt + 1))
+
+ # Test slicing:
+ assert o[3:] == members[3:]
+ assert o[:10] == members[:10]
+ assert o[3:10] == members[3:10]
+ assert o[-9:] == members[-9:]
+ assert o[:-4] == members[:-4]
+ assert o[-9:-4] == members[-9:-4]
+
+ # Test that TypeError is raised with wrong type
+ e = raises(TypeError, o.__getitem__, 3.0)
+ assert str(e) == TYPE_ERROR % ('key', (str, int, slice), 3.0, float)
+
+ def test_repr(self):
+ """
+ Test the `ipalib.base.NameSpace.__repr__` method.
+ """
+ for cnt in (0, 1, 2):
+ for sort in (True, False):
+ (o, members) = self.new(cnt, sort=sort)
+ if cnt == 1:
+ assert repr(o) == \
+ 'NameSpace(<%d member>, sort=%r)' % (cnt, sort)
+ else:
+ assert repr(o) == \
+ 'NameSpace(<%d members>, sort=%r)' % (cnt, sort)
+
+ def test_todict(self):
+ """
+ Test the `ipalib.base.NameSpace.__todict__` method.
+ """
+ for cnt in (3, 101):
+ for sort in (True, False):
+ (o, members) = self.new(cnt, sort=sort)
+ d = o.__todict__()
+ assert d == dict((m.name, m) for m in members)
+
+ # Test that a copy is returned:
+ assert o.__todict__() is not d
diff --git a/tests/test_ipalib/test_cli.py b/tests/test_ipalib/test_cli.py
new file mode 100644
index 000000000..56297fdf7
--- /dev/null
+++ b/tests/test_ipalib/test_cli.py
@@ -0,0 +1,277 @@
+# 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
+
+"""
+Test the `ipalib.cli` module.
+"""
+
+from tests.util import raises, get_api, ClassChecker
+from ipalib import cli, plugable, frontend, backend
+
+
+class test_textui(ClassChecker):
+ _cls = cli.textui
+
+ def test_max_col_width(self):
+ """
+ Test the `ipalib.cli.textui.max_col_width` method.
+ """
+ o = self.cls()
+ e = raises(TypeError, o.max_col_width, 'hello')
+ assert str(e) == 'rows: need %r or %r; got %r' % (list, tuple, 'hello')
+ rows = [
+ 'hello',
+ 'naughty',
+ 'nurse',
+ ]
+ assert o.max_col_width(rows) == len('naughty')
+ rows = (
+ ( 'a', 'bbb', 'ccccc'),
+ ('aa', 'bbbb', 'cccccc'),
+ )
+ assert o.max_col_width(rows, col=0) == 2
+ assert o.max_col_width(rows, col=1) == 4
+ assert o.max_col_width(rows, col=2) == 6
+
+
+def test_to_cli():
+ """
+ Test the `ipalib.cli.to_cli` function.
+ """
+ f = cli.to_cli
+ assert f('initialize') == 'initialize'
+ assert f('user_add') == 'user-add'
+
+
+def test_from_cli():
+ """
+ Test the `ipalib.cli.from_cli` function.
+ """
+ f = cli.from_cli
+ assert f('initialize') == 'initialize'
+ assert f('user-add') == 'user_add'
+
+
+def get_cmd_name(i):
+ return 'cmd_%d' % i
+
+
+class DummyCommand(object):
+ def __init__(self, name):
+ self.__name = name
+
+ def __get_name(self):
+ return self.__name
+ name = property(__get_name)
+
+
+class DummyAPI(object):
+ def __init__(self, cnt):
+ self.__cmd = plugable.NameSpace(self.__cmd_iter(cnt))
+
+ def __get_cmd(self):
+ return self.__cmd
+ Command = property(__get_cmd)
+
+ def __cmd_iter(self, cnt):
+ for i in xrange(cnt):
+ yield DummyCommand(get_cmd_name(i))
+
+ def finalize(self):
+ pass
+
+ def register(self, *args, **kw):
+ 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 new(self, argv=tuple()):
+ (api, home) = get_api()
+ 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_real(self):
+ """
+ Test the `ipalib.cli.CLI.run_real` method.
+ """
+ self.check_cascade(
+ 'run_real',
+ 'finalize',
+ 'load_plugins',
+ 'bootstrap',
+ 'parse_globals'
+ )
+
+ def test_finalize(self):
+ """
+ Test the `ipalib.cli.CLI.finalize` method.
+ """
+ self.check_cascade(
+ 'finalize',
+ 'load_plugins',
+ 'bootstrap',
+ 'parse_globals'
+ )
+
+ (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.load_plugins` method.
+ """
+ self.check_cascade(
+ 'load_plugins',
+ 'bootstrap',
+ 'parse_globals'
+ )
+ (o, api, home) = self.new()
+ assert api.isdone('load_plugins') is False
+ o.load_plugins()
+ assert api.isdone('load_plugins') is True
+
+ def test_bootstrap(self):
+ """
+ Test the `ipalib.cli.CLI.bootstrap` method.
+ """
+ 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',
+ '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,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 the `ipalib.cli.CLI.parse_globals` method.
+ """
+ # 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.prompt_all is False
+ assert o.options.interactive is True
+ assert o.options.verbose is None
+ assert o.options.conf is None
+ assert o.options.env 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.conf == '/my/config.conf'
+ assert o.options.env == ['my_key=my_val']
+ assert o.cmd_argv == cmd_argv
+ e = raises(StandardError, o.parse_globals)
+ assert str(e) == 'CLI.parse_globals() already called'
+
+ # Test with multiple -e args:
+ argv = ('-e', 'key1=val1', '-e', 'key2=val2')
+ (o, api, home) = self.new(argv)
+ o.parse_globals()
+ assert o.options.env == ['key1=val1', 'key2=val2']
diff --git a/tests/test_ipalib/test_config.py b/tests/test_ipalib/test_config.py
new file mode 100644
index 000000000..d3109f7b3
--- /dev/null
+++ b/tests/test_ipalib/test_config.py
@@ -0,0 +1,608 @@
+# 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
+
+"""
+Test the `ipalib.config` module.
+"""
+
+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.constants import TYPE_ERROR, OVERRIDE_ERROR, SET_ERROR, DEL_ERROR
+from ipalib.constants import NAME_REGEX, NAME_ERROR
+from ipalib import config, constants, base
+
+
+# Valid environment variables in (key, raw, value) tuples:
+# key: the name of the environment variable
+# raw: the value being set (possibly a string repr)
+# value: the expected value after the lightweight conversion
+good_vars = (
+ ('a_string', 'Hello world!', 'Hello world!'),
+ ('trailing_whitespace', ' value ', 'value'),
+ ('an_int', 42, 42),
+ ('int_repr', ' 42 ', 42),
+ ('a_float', 3.14, 3.14),
+ ('float_repr', ' 3.14 ', 3.14),
+ ('true', True, True),
+ ('true_repr', ' True ', True),
+ ('false', False, False),
+ ('false_repr', ' False ', False),
+ ('none', None, None),
+ ('none_repr', ' None ', None),
+ ('empty', '', None),
+
+ # These verify that the implied conversion is case-sensitive:
+ ('not_true', ' true ', 'true'),
+ ('not_false', ' false ', 'false'),
+ ('not_none', ' none ', 'none'),
+)
+
+
+bad_names = (
+ ('CamelCase', 'value'),
+ ('_leading_underscore', 'value'),
+ ('trailing_underscore_', 'value'),
+)
+
+
+# Random base64-encoded data to simulate a misbehaving config file.
+config_bad = """
+/9j/4AAQSkZJRgABAQEAlgCWAAD//gAIT2xpdmVy/9sAQwAQCwwODAoQDg0OEhEQExgoGhgWFhgx
+IyUdKDozPTw5Mzg3QEhcTkBEV0U3OFBtUVdfYmdoZz5NcXlwZHhcZWdj/8AACwgAlgB0AQERAP/E
+ABsAAAEFAQEAAAAAAAAAAAAAAAQAAQIDBQYH/8QAMhAAAgICAAUDBAIABAcAAAAAAQIAAwQRBRIh
+MUEGE1EiMmFxFIEVI0LBFjNSYnKRof/aAAgBAQAAPwDCtzmNRr1o/MEP1D6f7kdkRakgBsAtoQhk
+xls/y3Z113I11mhiUc1ewCf1Oq4anJgINdhLhQoextfedmYrenfcvdzaFQnYAE08XhONTWEK8+js
+Fpo1oqAKoAA8CWjoJJTHM8kJ5jsiOiszAKD1+IV/hmW76rosbfnlh1Pp3Mah2srCnXQE9YXiel/c
+p5r7uVj2CwxPTuFjjmdLbteNwmrLwsYe3TjsD8cmjKV43ycy+3o76D4llFuXmuCoZEPczXVOSsLv
+f5lgGpNZLxJL2jnvMar0/wAOp6jHDH/uO4RViY9f/KpRdfC6k3R9fRyj+pRZVkWKqF10e+hCKaFq
+XlH/ALlmhK7Met/uUGZ5ow8XL57lU8/Yt4lx4jUOJphLobTe/wDaHeZLxHXtJEya9o5lFzCqpmPY
+CUYoPtDfc9TLj0G5jZvHaMFirAs++oEHq9U4rbNiMp8a6wO/1Zbzn2alC+Nx8P1JfdeBboA+AILx
+rin8pfbA1ynvKuFUXZOXXkLbzOp2R56andL2G45MmO0RPWWLEe8GzaffoKb/ADI44Pt9ZXxAuuFa
+axtgp0BOSPCcviNX8n3Aw8KTNHB4FiY9StkobLWHVSeghq8M4bkAhKKyV6Hl8RV8MwMZG1Uuz3Jn
+IcUQJlMFGlJ6D4hfpymy7iChHKqvVtefxO7Ai1txLBIn7pcojN3jGVhQO0ZgCNfM5ZHycTLycSkr
+yhtqD4Bmrfw5cuqsm6xHXyp1seRLcHCp4dQy1bOzslj1MzeJ5dVFnuMVdgOiHxOWzrmyMg2Nrbde
+k3vR2OTddcd6A5R8GdZqOo67k4wXrLAQPMRKnzImMZEzm+P1nFz6cxQeVujagWR6jsYiqivlH/Ux
+1M+7jWY30i7QHx1gF11tjGyxiSfmVc+503pPidVROHYNNY21b/adVZZySo3uOo1qIZQYd9RCzfYm
+TUk/qW71LjGkTA+IYiZmM1T9N9j8Gee5+McXJem0/Wp8GUK6KOi7b5MgzFjsxpJHZGDKSCOxE3cD
+OvsxbbLc9lsT7Vc73KX4ln3q1ZyVrPx2J/uAjLyan37z7B+Zp4vqPJqKi0K4EvzvUt1qBMdfb+T5
+gycfzkXXuc35InfE6nO8Y9SjFc1Yqh2Hdj2mH/xFxR26XgD/AMRJf45mWMqW5bBD3KqAZlZtb++7
+kEqTsHe//sG1CcTBvy7OWpD+Sewhz8CyKCTYAQPiGV0LVWPdxqQNADQ6zL4nWq2gopU6+ofmA8x3
+1MlvfeIGbnBeCHitRt94IFbRGus2U9H08v13sT+BNHjeX/D4bY4OmP0rPPbHLMWJ2Yy2EDQjVsos
+BdeYDx8wo5L5KpSdLWPAE1+G8NrFtBKgOAXPTf6mzViql5ZBoE87eJZkKbOQ8m+Yjf5EBzcO621y
+GCqD0H41Obzq7U6vzM577HTXgzPPeOIvM1eB59nD8xXVj7bHTr8iej1MtlauvUMNgzi/V2ctliYy
+HYTq37nMExpZXRZYpZVJUdzNjg+FXYwZgdhv6nVVUJU/uH7iNf1CARrtF0IB113M7jTNVjFl2xJA
+5ROey88OrVOugOy67TDs+89NRKdSYILdRC8ZQVJ+PHyJs4fqe3EoFPLzBexPxOdusa2xndiWY7JM
+qMUNrzOTAfHC9XO9/E3vT9blVJB0o2Zu3MAoYrsL13Ii0Muw3XvJG9KkDOeqjf6gWcw5A33XN9nX
+tOeyMRFWy3Jch+bX7mXmCsW/5RBXUoHaOIRi2asAJ0IRbjqzll3o/EAaRiltDojgv2E1aePmhEWq
+rsNHZ7wir1K/8Y1vUCSCAd+IXiZ9b1gLYvN07trXTUD4rxN2TkUgEts8p2NDtD0t5MVGchr2Xe99
+hMPNvD1LX5J2TuZhGyYwBijjfiHU5bJXrnYfqBRtRtSbIBWG3+xI6HiLUWz8xA9RuaVNrMAPfB5x
+r6v9MLr4S1il7LaxyjY69Jl5eG+Kyhiv1jYIMGYMO8etGscKoJJ8Cbp4bVg4ivaq22t3G/tmRYo5
+zyjQ+JRFFET01GB0Yid9YiYh1l9KgEHqT8Tco/hewA/NzgdQdwTNGNTY3uU2crL9HN00ZlovNzfV
+oCanBrBRk1rpCHPUkQjjYoW4GtwAw30MDpuxvbAvpJceR5mXFFEY0W4o4mpg0XNXutQxPUHxLb8q
+7mRDyszLr6esz8u++9wL2LcvQb8RXCkhBV3A6mR5rEVSrdFPT8SBLMdsdmWe6P8AUAx+TB4oooxi
+i1Jmt0+5dfuOLbANB2H6MjzNzc2zv5ji1g2+5/MYnbb+Yh+T0kubUY940UUbUWtRpJN8w1CfebkK
+WfUu+/mDOAGOjsRo0UkIo+pPl6Rckl7ehuR1INGAj9u0kW2nXvK45YlQp1odukaICSAjgSQWf//Z
+"""
+
+
+# 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]
+
+string = Hello world!
+null = None
+yes = True
+no = False
+number = 42
+floating = 3.14
+"""
+
+
+# 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 test_init(self):
+ """
+ Test the `ipalib.config.Env.__init__` method.
+ """
+ o = self.cls()
+ assert list(o) == []
+ assert len(o) == 0
+ assert o.__islocked__() is False
+
+ def test_lock(self):
+ """
+ Test the `ipalib.config.Env.__lock__` method.
+ """
+ o = self.cls()
+ assert o.__islocked__() is False
+ o.__lock__()
+ assert o.__islocked__() is True
+ e = raises(StandardError, o.__lock__)
+ assert str(e) == 'Env.__lock__() already called'
+
+ # Also test with base.lock() function:
+ o = self.cls()
+ assert o.__islocked__() is False
+ assert base.lock(o) is o
+ assert o.__islocked__() is True
+ e = raises(AssertionError, base.lock, o)
+ assert str(e) == 'already locked: %r' % o
+
+ def test_islocked(self):
+ """
+ Test the `ipalib.config.Env.__islocked__` method.
+ """
+ o = self.cls()
+ assert o.__islocked__() is False
+ assert base.islocked(o) is False
+ o.__lock__()
+ assert o.__islocked__() is True
+ assert base.islocked(o) is True
+
+ def test_setattr(self):
+ """
+ Test the `ipalib.config.Env.__setattr__` method.
+ """
+ o = self.cls()
+ for (name, raw, value) in good_vars:
+ # Test setting the value:
+ setattr(o, name, raw)
+ result = getattr(o, name)
+ assert type(result) is type(value)
+ assert result == value
+ assert result is o[name]
+
+ # Test that value cannot be overridden once set:
+ e = raises(AttributeError, setattr, o, name, raw)
+ assert str(e) == OVERRIDE_ERROR % ('Env', name, value, raw)
+
+ # Test that values cannot be set once locked:
+ o = self.cls()
+ o.__lock__()
+ for (name, raw, value) in good_vars:
+ e = raises(AttributeError, setattr, o, name, raw)
+ assert str(e) == SET_ERROR % ('Env', name, raw)
+
+ # Test that name is tested with check_name():
+ o = self.cls()
+ for (name, value) in bad_names:
+ e = raises(ValueError, setattr, o, name, value)
+ assert str(e) == NAME_ERROR % (NAME_REGEX, name)
+
+ def test_setitem(self):
+ """
+ Test the `ipalib.config.Env.__setitem__` method.
+ """
+ o = self.cls()
+ for (key, raw, value) in good_vars:
+ # Test setting the value:
+ o[key] = raw
+ result = o[key]
+ assert type(result) is type(value)
+ assert result == value
+ assert result is getattr(o, key)
+
+ # Test that value cannot be overridden once set:
+ e = raises(AttributeError, o.__setitem__, key, raw)
+ assert str(e) == OVERRIDE_ERROR % ('Env', key, value, raw)
+
+ # Test that values cannot be set once locked:
+ o = self.cls()
+ o.__lock__()
+ for (key, raw, value) in good_vars:
+ e = raises(AttributeError, o.__setitem__, key, raw)
+ assert str(e) == SET_ERROR % ('Env', key, raw)
+
+ # Test that name is tested with check_name():
+ o = self.cls()
+ for (key, value) in bad_names:
+ e = raises(ValueError, o.__setitem__, key, value)
+ assert str(e) == NAME_ERROR % (NAME_REGEX, key)
+
+ def test_getitem(self):
+ """
+ Test 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
+ for name in ('one', 'two'):
+ e = raises(KeyError, getitem, o, name)
+ assert str(e) == repr(name)
+
+ 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) == DEL_ERROR % ('Env', 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', 2),
+ ('three', 3),
+ ('four', 4),
+ ]
+ for (key, value) in items:
+ assert key not in o
+ o[key] = value
+ assert key in o
+
+ def test_len(self):
+ """
+ Test the `ipalib.config.Env.__len__` method.
+ """
+ o = self.cls()
+ assert len(o) == 0
+ for i in xrange(1, 11):
+ key = 'key%d' % i
+ value = 'value %d' % i
+ o[key] = value
+ assert o[key] is value
+ assert len(o) == i
+
+ 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_merge(self):
+ """
+ Test the `ipalib.config.Env._merge` method.
+ """
+ group1 = (
+ ('key1', 'value 1'),
+ ('key2', 'value 2'),
+ ('key3', 'value 3'),
+ ('key4', 'value 4'),
+ )
+ group2 = (
+ ('key0', 'Value 0'),
+ ('key2', 'Value 2'),
+ ('key4', 'Value 4'),
+ ('key5', 'Value 5'),
+ )
+ o = self.cls()
+ assert o._merge(**dict(group1)) == (4, 4)
+ assert len(o) == 4
+ assert list(o) == list(key for (key, value) in group1)
+ for (key, value) in group1:
+ assert getattr(o, key) is value
+ assert o[key] is value
+ assert o._merge(**dict(group2)) == (2, 4)
+ assert len(o) == 6
+ expected = dict(group2)
+ expected.update(dict(group1))
+ assert list(o) == sorted(expected)
+ assert expected['key2'] == 'value 2' # And not 'Value 2'
+ for (key, value) in expected.iteritems():
+ assert getattr(o, key) is value
+ assert o[key] is value
+ assert o._merge(**expected) == (0, 6)
+ assert len(o) == 6
+ assert list(o) == sorted(expected)
+
+ def test_merge_from_file(self):
+ """
+ Test the `ipalib.config.Env._merge_from_file` 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()
+ o._bootstrap()
+ keys = tuple(o)
+ orig = dict((k, o[k]) for k in o)
+ assert o._merge_from_file(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_from_file(empty) == (0, 0)
+ 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_from_file(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_from_file(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_from_file(good) == (6, 6)
+ added = ('string', 'null', 'yes', 'no', 'number', 'floating')
+ assert list(o) == sorted(keys + added)
+ assert o.string == 'Hello world!'
+ assert o.null is None
+ assert o.yes is True
+ assert o.no is False
+ assert o.number == 42
+ assert o.floating == 3.14
+
+ def new(self):
+ """
+ Set os.environ['HOME'] to a tempdir.
+
+ Returns tuple with new Env instance and the TempHome instance. This
+ helper method is used in testing the bootstrap related methods below.
+ """
+ home = TempHome()
+ return (self.cls(), home)
+
+ def bootstrap(self, **overrides):
+ """
+ Helper method used in testing bootstrap related methods below.
+ """
+ (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()
+ o._bootstrap()
+ 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')
+ 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):
+ """
+ Helper method used in testing `Env._finalize_core`.
+ """
+ (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 object:
+ continue
+ assert o[key] is value, 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
diff --git a/tests/test_ipalib/test_crud.py b/tests/test_ipalib/test_crud.py
new file mode 100644
index 000000000..ad391e2ea
--- /dev/null
+++ b/tests/test_ipalib/test_crud.py
@@ -0,0 +1,237 @@
+# 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
+
+"""
+Test the `ipalib.crud` module.
+"""
+
+from tests.util import read_only, raises, get_api, ClassChecker
+from ipalib import crud, frontend, plugable, config
+
+
+class CrudChecker(ClassChecker):
+ """
+ Class for testing base classes in `ipalib.crud`.
+ """
+
+ def get_api(self, args=tuple(), options={}):
+ """
+ Return a finalized `ipalib.plugable.API` instance.
+ """
+ assert self.cls.__bases__ == (frontend.Method,)
+ (api, home) = get_api()
+ class user(frontend.Object):
+ takes_params = (
+ 'givenname',
+ 'sn',
+ frontend.Param('uid', primary_key=True),
+ 'initials',
+ )
+ class user_verb(self.cls):
+ takes_args = args
+ takes_options = options
+ api.register(user)
+ api.register(user_verb)
+ api.finalize()
+ return api
+
+
+class test_Add(CrudChecker):
+ """
+ Test the `ipalib.crud.Add` class.
+ """
+
+ _cls = crud.Add
+
+ def test_get_args(self):
+ """
+ Test the `ipalib.crud.Add.get_args` method.
+ """
+ api = self.get_api()
+ assert list(api.Method.user_verb.args) == ['uid']
+ assert api.Method.user_verb.args.uid.required is True
+ api = self.get_api(args=('extra?',))
+ assert list(api.Method.user_verb.args) == ['uid', 'extra']
+ assert api.Method.user_verb.args.uid.required is True
+ assert api.Method.user_verb.args.extra.required is False
+
+ def test_get_options(self):
+ """
+ Test the `ipalib.crud.Add.get_options` method.
+ """
+ api = self.get_api()
+ assert list(api.Method.user_verb.options) == \
+ ['givenname', 'sn', 'initials']
+ for param in api.Method.user_verb.options():
+ assert param.required is True
+ api = self.get_api(options=('extra?',))
+ assert list(api.Method.user_verb.options) == \
+ ['givenname', 'sn', 'initials', 'extra']
+ assert api.Method.user_verb.options.extra.required is False
+
+
+class test_Get(CrudChecker):
+ """
+ Test the `ipalib.crud.Get` class.
+ """
+
+ _cls = crud.Get
+
+ def test_get_args(self):
+ """
+ Test the `ipalib.crud.Get.get_args` method.
+ """
+ api = self.get_api()
+ assert list(api.Method.user_verb.args) == ['uid']
+ assert api.Method.user_verb.args.uid.required is True
+
+ def test_get_options(self):
+ """
+ Test the `ipalib.crud.Get.get_options` method.
+ """
+ api = self.get_api()
+ assert list(api.Method.user_verb.options) == []
+ assert len(api.Method.user_verb.options) == 0
+
+
+class test_Del(CrudChecker):
+ """
+ Test the `ipalib.crud.Del` class.
+ """
+
+ _cls = crud.Del
+
+ def test_get_args(self):
+ """
+ Test the `ipalib.crud.Del.get_args` method.
+ """
+ api = self.get_api()
+ assert list(api.Method.user_verb.args) == ['uid']
+ assert api.Method.user_verb.args.uid.required is True
+
+ def test_get_options(self):
+ """
+ Test the `ipalib.crud.Del.get_options` method.
+ """
+ api = self.get_api()
+ assert list(api.Method.user_verb.options) == []
+ assert len(api.Method.user_verb.options) == 0
+
+
+class test_Mod(CrudChecker):
+ """
+ Test the `ipalib.crud.Mod` class.
+ """
+
+ _cls = crud.Mod
+
+ def test_get_args(self):
+ """
+ Test the `ipalib.crud.Mod.get_args` method.
+ """
+ api = self.get_api()
+ assert list(api.Method.user_verb.args) == ['uid']
+ assert api.Method.user_verb.args.uid.required is True
+
+ def test_get_options(self):
+ """
+ Test the `ipalib.crud.Mod.get_options` method.
+ """
+ api = self.get_api()
+ assert list(api.Method.user_verb.options) == \
+ ['givenname', 'sn', 'initials']
+ for param in api.Method.user_verb.options():
+ assert param.required is False
+
+
+class test_Find(CrudChecker):
+ """
+ Test the `ipalib.crud.Find` class.
+ """
+
+ _cls = crud.Find
+
+ def test_get_args(self):
+ """
+ Test the `ipalib.crud.Find.get_args` method.
+ """
+ api = self.get_api()
+ assert list(api.Method.user_verb.args) == ['uid']
+ assert api.Method.user_verb.args.uid.required is True
+
+ def test_get_options(self):
+ """
+ Test the `ipalib.crud.Find.get_options` method.
+ """
+ api = self.get_api()
+ assert list(api.Method.user_verb.options) == \
+ ['givenname', 'sn', 'initials']
+ for param in api.Method.user_verb.options():
+ assert param.required is False
+
+
+class test_CrudBackend(ClassChecker):
+ """
+ Test the `ipalib.crud.CrudBackend` class.
+ """
+
+ _cls = crud.CrudBackend
+
+ def get_subcls(self):
+ class ldap(self.cls):
+ pass
+ return ldap
+
+ def check_method(self, name, *args):
+ o = self.cls()
+ e = raises(NotImplementedError, getattr(o, name), *args)
+ assert str(e) == 'CrudBackend.%s()' % name
+ sub = self.subcls()
+ e = raises(NotImplementedError, getattr(sub, name), *args)
+ assert str(e) == 'ldap.%s()' % name
+
+ def test_create(self):
+ """
+ Test the `ipalib.crud.CrudBackend.create` method.
+ """
+ self.check_method('create')
+
+ def test_retrieve(self):
+ """
+ Test the `ipalib.crud.CrudBackend.retrieve` method.
+ """
+ self.check_method('retrieve', 'primary key', 'attribute')
+
+ def test_update(self):
+ """
+ Test the `ipalib.crud.CrudBackend.update` method.
+ """
+ self.check_method('update', 'primary key')
+
+ def test_delete(self):
+ """
+ Test the `ipalib.crud.CrudBackend.delete` method.
+ """
+ self.check_method('delete', 'primary key')
+
+ def test_search(self):
+ """
+ Test the `ipalib.crud.CrudBackend.search` method.
+ """
+ self.check_method('search')
diff --git a/tests/test_ipalib/test_error2.py b/tests/test_ipalib/test_error2.py
new file mode 100644
index 000000000..cd13ba775
--- /dev/null
+++ b/tests/test_ipalib/test_error2.py
@@ -0,0 +1,371 @@
+# 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
+
+"""
+Test the `ipalib.error2` module.
+"""
+
+import re
+import inspect
+from tests.util import assert_equal, raises, dummy_ugettext
+from ipalib import errors2, request
+from ipalib.constants import TYPE_ERROR
+
+
+class PrivateExceptionTester(object):
+ _klass = None
+ __klass = None
+
+ def __get_klass(self):
+ if self.__klass is None:
+ self.__klass = self._klass
+ assert issubclass(self.__klass, StandardError)
+ assert issubclass(self.__klass, errors2.PrivateError)
+ assert not issubclass(self.__klass, errors2.PublicError)
+ return self.__klass
+ klass = property(__get_klass)
+
+ def new(self, **kw):
+ for (key, value) in kw.iteritems():
+ assert not hasattr(self.klass, key), key
+ inst = self.klass(**kw)
+ assert isinstance(inst, StandardError)
+ assert isinstance(inst, errors2.PrivateError)
+ assert isinstance(inst, self.klass)
+ assert not isinstance(inst, errors2.PublicError)
+ for (key, value) in kw.iteritems():
+ assert getattr(inst, key) is value
+ assert str(inst) == self.klass.format % kw
+ assert inst.message == str(inst)
+ return inst
+
+
+class test_PrivateError(PrivateExceptionTester):
+ """
+ Test the `ipalib.errors2.PrivateError` exception.
+ """
+ _klass = errors2.PrivateError
+
+ def test_init(self):
+ """
+ Test the `ipalib.errors2.PrivateError.__init__` method.
+ """
+ inst = self.klass(key1='Value 1', key2='Value 2')
+ assert inst.key1 == 'Value 1'
+ assert inst.key2 == 'Value 2'
+ assert str(inst) == ''
+
+ # Test subclass and use of format:
+ class subclass(self.klass):
+ format = '%(true)r %(text)r %(number)r'
+
+ kw = dict(true=True, text='Hello!', number=18)
+ inst = subclass(**kw)
+ assert inst.true is True
+ assert inst.text is kw['text']
+ assert inst.number is kw['number']
+ assert str(inst) == subclass.format % kw
+
+ # Test via PrivateExceptionTester.new()
+ inst = self.new(**kw)
+ assert isinstance(inst, self.klass)
+ assert inst.true is True
+ assert inst.text is kw['text']
+ assert inst.number is kw['number']
+
+
+class test_SubprocessError(PrivateExceptionTester):
+ """
+ Test the `ipalib.errors2.SubprocessError` exception.
+ """
+
+ _klass = errors2.SubprocessError
+
+ def test_init(self):
+ """
+ Test the `ipalib.errors2.SubprocessError.__init__` method.
+ """
+ inst = self.new(returncode=1, argv=('/bin/false',))
+ assert inst.returncode == 1
+ assert inst.argv == ('/bin/false',)
+ assert str(inst) == "return code 1 from ('/bin/false',)"
+ assert inst.message == str(inst)
+
+
+class test_PluginSubclassError(PrivateExceptionTester):
+ """
+ Test the `ipalib.errors2.PluginSubclassError` exception.
+ """
+
+ _klass = errors2.PluginSubclassError
+
+ def test_init(self):
+ """
+ Test the `ipalib.errors2.PluginSubclassError.__init__` method.
+ """
+ inst = self.new(plugin='bad', bases=('base1', 'base2'))
+ assert inst.plugin == 'bad'
+ assert inst.bases == ('base1', 'base2')
+ assert str(inst) == \
+ "'bad' not subclass of any base in ('base1', 'base2')"
+ assert inst.message == str(inst)
+
+
+class test_PluginDuplicateError(PrivateExceptionTester):
+ """
+ Test the `ipalib.errors2.PluginDuplicateError` exception.
+ """
+
+ _klass = errors2.PluginDuplicateError
+
+ def test_init(self):
+ """
+ Test the `ipalib.errors2.PluginDuplicateError.__init__` method.
+ """
+ inst = self.new(plugin='my_plugin')
+ assert inst.plugin == 'my_plugin'
+ assert str(inst) == "'my_plugin' was already registered"
+ assert inst.message == str(inst)
+
+
+class test_PluginOverrideError(PrivateExceptionTester):
+ """
+ Test the `ipalib.errors2.PluginOverrideError` exception.
+ """
+
+ _klass = errors2.PluginOverrideError
+
+ def test_init(self):
+ """
+ Test the `ipalib.errors2.PluginOverrideError.__init__` method.
+ """
+ inst = self.new(base='Base', name='cmd', plugin='my_cmd')
+ assert inst.base == 'Base'
+ assert inst.name == 'cmd'
+ assert inst.plugin == 'my_cmd'
+ assert str(inst) == "unexpected override of Base.cmd with 'my_cmd'"
+ assert inst.message == str(inst)
+
+
+class test_PluginMissingOverrideError(PrivateExceptionTester):
+ """
+ Test the `ipalib.errors2.PluginMissingOverrideError` exception.
+ """
+
+ _klass = errors2.PluginMissingOverrideError
+
+ def test_init(self):
+ """
+ Test the `ipalib.errors2.PluginMissingOverrideError.__init__` method.
+ """
+ inst = self.new(base='Base', name='cmd', plugin='my_cmd')
+ assert inst.base == 'Base'
+ assert inst.name == 'cmd'
+ assert inst.plugin == 'my_cmd'
+ assert str(inst) == "Base.cmd not registered, cannot override with 'my_cmd'"
+ assert inst.message == str(inst)
+
+
+
+##############################################################################
+# Unit tests for public errors:
+
+class PublicExceptionTester(object):
+ _klass = None
+ __klass = None
+
+ def __get_klass(self):
+ if self.__klass is None:
+ self.__klass = self._klass
+ assert issubclass(self.__klass, StandardError)
+ assert issubclass(self.__klass, errors2.PublicError)
+ assert not issubclass(self.__klass, errors2.PrivateError)
+ assert type(self.__klass.errno) is int
+ assert 900 <= self.__klass.errno <= 5999
+ return self.__klass
+ klass = property(__get_klass)
+
+ def new(self, format=None, message=None, **kw):
+ # Test that TypeError is raised if message isn't unicode:
+ e = raises(TypeError, self.klass, message='The message')
+ assert str(e) == TYPE_ERROR % ('message', unicode, 'The message', str)
+
+ # Test the instance:
+ for (key, value) in kw.iteritems():
+ assert not hasattr(self.klass, key), key
+ inst = self.klass(format=format, message=message, **kw)
+ assert isinstance(inst, StandardError)
+ assert isinstance(inst, errors2.PublicError)
+ assert isinstance(inst, self.klass)
+ assert not isinstance(inst, errors2.PrivateError)
+ for (key, value) in kw.iteritems():
+ assert getattr(inst, key) is value
+ return inst
+
+
+class test_PublicError(PublicExceptionTester):
+ """
+ Test the `ipalib.errors2.PublicError` exception.
+ """
+ _klass = errors2.PublicError
+
+ def test_init(self):
+ """
+ Test the `ipalib.errors2.PublicError.__init__` method.
+ """
+ context = request.context
+ message = u'The translated, interpolated message'
+ format = 'key=%(key1)r and key2=%(key2)r'
+ uformat = u'Translated key=%(key1)r and key2=%(key2)r'
+ val1 = 'Value 1'
+ val2 = 'Value 2'
+ kw = dict(key1=val1, key2=val2)
+
+ assert not hasattr(context, 'ugettext')
+
+ # Test with format=str, message=None
+ dummy = dummy_ugettext(uformat)
+ context.ugettext = dummy
+ inst = self.klass(format, **kw)
+ assert dummy.message is format # Means ugettext() called
+ assert inst.format is format
+ assert_equal(inst.message, format % kw)
+ assert_equal(inst.strerror, uformat % kw)
+ assert inst.forwarded is False
+ assert inst.key1 is val1
+ assert inst.key2 is val2
+
+ # Test with format=None, message=unicode
+ dummy = dummy_ugettext(uformat)
+ context.ugettext = dummy
+ inst = self.klass(message=message, **kw)
+ assert not hasattr(dummy, 'message') # Means ugettext() not called
+ assert inst.format is None
+ assert inst.message is message
+ assert inst.strerror is message
+ assert inst.forwarded is True
+ assert inst.key1 is val1
+ assert inst.key2 is val2
+
+ # Test with format=None, message=str
+ e = raises(TypeError, self.klass, message='the message', **kw)
+ assert str(e) == TYPE_ERROR % ('message', unicode, 'the message', str)
+
+ # Test with format=None, message=None
+ e = raises(ValueError, self.klass, **kw)
+ assert str(e) == \
+ 'PublicError.format is None yet format=None, message=None'
+
+
+ ######################################
+ # Test via PublicExceptionTester.new()
+
+ # Test with format=str, message=None
+ dummy = dummy_ugettext(uformat)
+ context.ugettext = dummy
+ inst = self.new(format, **kw)
+ assert isinstance(inst, self.klass)
+ assert dummy.message is format # Means ugettext() called
+ assert inst.format is format
+ assert_equal(inst.message, format % kw)
+ assert_equal(inst.strerror, uformat % kw)
+ assert inst.forwarded is False
+ assert inst.key1 is val1
+ assert inst.key2 is val2
+
+ # Test with format=None, message=unicode
+ dummy = dummy_ugettext(uformat)
+ context.ugettext = dummy
+ inst = self.new(message=message, **kw)
+ assert isinstance(inst, self.klass)
+ assert not hasattr(dummy, 'message') # Means ugettext() not called
+ assert inst.format is None
+ assert inst.message is message
+ assert inst.strerror is message
+ assert inst.forwarded is True
+ assert inst.key1 is val1
+ assert inst.key2 is val2
+
+
+ ##################
+ # Test a subclass:
+ class subclass(self.klass):
+ format = '%(true)r %(text)r %(number)r'
+
+ uformat = u'Translated %(true)r %(text)r %(number)r'
+ kw = dict(true=True, text='Hello!', number=18)
+
+ dummy = dummy_ugettext(uformat)
+ context.ugettext = dummy
+
+ # Test with format=str, message=None
+ e = raises(ValueError, subclass, format, **kw)
+ assert str(e) == 'non-generic %r needs format=None; got format=%r' % (
+ 'subclass', format)
+
+ # Test with format=None, message=None:
+ inst = subclass(**kw)
+ assert dummy.message is subclass.format # Means ugettext() called
+ assert inst.format is subclass.format
+ assert_equal(inst.message, subclass.format % kw)
+ assert_equal(inst.strerror, uformat % kw)
+ assert inst.forwarded is False
+ assert inst.true is True
+ assert inst.text is kw['text']
+ assert inst.number is kw['number']
+
+ # Test with format=None, message=unicode:
+ dummy = dummy_ugettext(uformat)
+ context.ugettext = dummy
+ inst = subclass(message=message, **kw)
+ assert not hasattr(dummy, 'message') # Means ugettext() not called
+ assert inst.format is subclass.format
+ assert inst.message is message
+ assert inst.strerror is message
+ assert inst.forwarded is True
+ assert inst.true is True
+ assert inst.text is kw['text']
+ assert inst.number is kw['number']
+ del context.ugettext
+
+
+def test_public_errors():
+ """
+ Test the `ipalib.errors2.public_errors` module variable.
+ """
+ i = 0
+ for klass in errors2.public_errors:
+ assert issubclass(klass, StandardError)
+ assert issubclass(klass, errors2.PublicError)
+ assert not issubclass(klass, errors2.PrivateError)
+ assert type(klass.errno) is int
+ assert 900 <= klass.errno <= 5999
+ doc = inspect.getdoc(klass)
+ assert doc is not None, 'need class docstring for %s' % klass.__name__
+ m = re.match(r'^\*{2}(\d+)\*{2} ', doc)
+ assert m is not None, "need '**ERRNO**' in %s docstring" % klass.__name__
+ errno = int(m.group(1))
+ assert errno == klass.errno, (
+ 'docstring=%r but errno=%r in %s' % (errno, klass.errno, klass.__name__)
+ )
+
+ # Test format
+ if klass.format is not None:
+ assert klass.format is errors2.__messages[i]
+ i += 1
diff --git a/tests/test_ipalib/test_errors.py b/tests/test_ipalib/test_errors.py
new file mode 100644
index 000000000..f1dd5dc8e
--- /dev/null
+++ b/tests/test_ipalib/test_errors.py
@@ -0,0 +1,289 @@
+# 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
+
+"""
+Test the `ipalib.errors` module.
+"""
+
+from tests.util import raises, ClassChecker
+from ipalib import errors
+
+
+type_format = '%s: need a %r; got %r'
+
+
+def check_TypeError(f, value, type_, name, **kw):
+ e = raises(TypeError, f, value, type_, name, **kw)
+ assert e.value is value
+ assert e.type is type_
+ assert e.name is name
+ assert str(e) == type_format % (name, type_, value)
+
+
+def test_raise_TypeError():
+ """
+ Test the `ipalib.errors.raise_TypeError` function.
+ """
+ f = errors.raise_TypeError
+ value = 'Hello.'
+ type_ = unicode
+ name = 'message'
+
+ check_TypeError(f, value, type_, name)
+
+ # name not an str
+ fail_name = 42
+ e = raises(AssertionError, f, value, type_, fail_name)
+ assert str(e) == type_format % ('name', str, fail_name), str(e)
+
+ # type_ not a type:
+ fail_type = unicode()
+ e = raises(AssertionError, f, value, fail_type, name)
+ assert str(e) == type_format % ('type_', type, fail_type)
+
+ # type(value) is type_:
+ fail_value = u'How are you?'
+ e = raises(AssertionError, f, fail_value, type_, name)
+ assert str(e) == 'value: %r is a %r' % (fail_value, type_)
+
+
+def test_check_type():
+ """
+ Test the `ipalib.errors.check_type` function.
+ """
+ f = errors.check_type
+ value = 'How are you?'
+ type_ = str
+ name = 'greeting'
+
+ # Should pass:
+ assert value is f(value, type_, name)
+ assert None is f(None, type_, name, allow_none=True)
+
+ # Should raise TypeError
+ check_TypeError(f, None, type_, name)
+ check_TypeError(f, value, basestring, name)
+ check_TypeError(f, value, unicode, name)
+
+ # name not an str
+ fail_name = unicode(name)
+ e = raises(AssertionError, f, value, type_, fail_name)
+ assert str(e) == type_format % ('name', str, fail_name)
+
+ # type_ not a type:
+ fail_type = 42
+ e = raises(AssertionError, f, value, fail_type, name)
+ assert str(e) == type_format % ('type_', type, fail_type)
+
+ # allow_none not a bool:
+ fail_bool = 0
+ e = raises(AssertionError, f, value, type_, name, allow_none=fail_bool)
+ assert str(e) == type_format % ('allow_none', bool, fail_bool)
+
+
+def test_check_isinstance():
+ """
+ Test the `ipalib.errors.check_isinstance` function.
+ """
+ f = errors.check_isinstance
+ value = 'How are you?'
+ type_ = str
+ name = 'greeting'
+
+ # Should pass:
+ assert value is f(value, type_, name)
+ assert value is f(value, basestring, name)
+ assert None is f(None, type_, name, allow_none=True)
+
+ # Should raise TypeError
+ check_TypeError(f, None, type_, name)
+ check_TypeError(f, value, unicode, name)
+
+ # name not an str
+ fail_name = unicode(name)
+ e = raises(AssertionError, f, value, type_, fail_name)
+ assert str(e) == type_format % ('name', str, fail_name)
+
+ # type_ not a type:
+ fail_type = 42
+ e = raises(AssertionError, f, value, fail_type, name)
+ assert str(e) == type_format % ('type_', type, fail_type)
+
+ # allow_none not a bool:
+ fail_bool = 0
+ e = raises(AssertionError, f, value, type_, name, allow_none=fail_bool)
+ assert str(e) == type_format % ('allow_none', bool, fail_bool)
+
+
+class test_IPAError(ClassChecker):
+ """
+ Test the `ipalib.errors.IPAError` exception.
+ """
+ _cls = errors.IPAError
+
+ def test_class(self):
+ """
+ Test the `ipalib.errors.IPAError` exception.
+ """
+ assert self.cls.__bases__ == (StandardError,)
+
+ def test_init(self):
+ """
+ Test the `ipalib.errors.IPAError.__init__` method.
+ """
+ args = ('one fish', 'two fish')
+ e = self.cls(*args)
+ assert e.args == args
+ assert self.cls().args == tuple()
+
+ def test_str(self):
+ """
+ Test the `ipalib.errors.IPAError.__str__` method.
+ """
+ f = 'The %s color is %s.'
+ class custom_error(self.cls):
+ format = f
+ for args in [('sexiest', 'red'), ('most-batman-like', 'black')]:
+ e = custom_error(*args)
+ assert e.args == args
+ assert str(e) == f % args
+
+
+class test_ValidationError(ClassChecker):
+ """
+ Test the `ipalib.errors.ValidationError` exception.
+ """
+ _cls = errors.ValidationError
+
+ def test_class(self):
+ """
+ Test the `ipalib.errors.ValidationError` exception.
+ """
+ assert self.cls.__bases__ == (errors.IPAError,)
+
+ def test_init(self):
+ """
+ Test the `ipalib.errors.ValidationError.__init__` method.
+ """
+ name = 'login'
+ value = 'Whatever'
+ error = 'Must be lowercase.'
+ for index in (None, 3):
+ e = self.cls(name, value, error, index=index)
+ assert e.name is name
+ assert e.value is value
+ assert e.error is error
+ assert e.index is index
+ assert str(e) == 'invalid %r value %r: %s' % (name, value, error)
+ # Check that index default is None:
+ assert self.cls(name, value, error).index is None
+ # Check non str name raises AssertionError:
+ raises(AssertionError, self.cls, unicode(name), value, error)
+ # Check non int index raises AssertionError:
+ raises(AssertionError, self.cls, name, value, error, index=5.0)
+ # Check negative index raises AssertionError:
+ raises(AssertionError, self.cls, name, value, error, index=-2)
+
+
+class test_ConversionError(ClassChecker):
+ """
+ Test the `ipalib.errors.ConversionError` exception.
+ """
+ _cls = errors.ConversionError
+
+ def test_class(self):
+ """
+ Test the `ipalib.errors.ConversionError` exception.
+ """
+ assert self.cls.__bases__ == (errors.ValidationError,)
+
+ def test_init(self):
+ """
+ Test the `ipalib.errors.ConversionError.__init__` method.
+ """
+ name = 'some_arg'
+ value = '42.0'
+ class type_(object):
+ conversion_error = 'Not an integer'
+ for index in (None, 7):
+ e = self.cls(name, value, type_, index=index)
+ assert e.name is name
+ assert e.value is value
+ assert e.type is type_
+ assert e.error is type_.conversion_error
+ assert e.index is index
+ assert str(e) == 'invalid %r value %r: %s' % (name, value,
+ type_.conversion_error)
+ # Check that index default is None:
+ assert self.cls(name, value, type_).index is None
+
+
+class test_RuleError(ClassChecker):
+ """
+ Test the `ipalib.errors.RuleError` exception.
+ """
+ _cls = errors.RuleError
+
+ def test_class(self):
+ """
+ Test the `ipalib.errors.RuleError` exception.
+ """
+ assert self.cls.__bases__ == (errors.ValidationError,)
+
+ def test_init(self):
+ """
+ Test the `ipalib.errors.RuleError.__init__` method.
+ """
+ name = 'whatever'
+ value = 'The smallest weird number.'
+ def my_rule(value):
+ return 'Value is bad.'
+ error = my_rule(value)
+ for index in (None, 42):
+ e = self.cls(name, value, error, my_rule, index=index)
+ assert e.name is name
+ assert e.value is value
+ assert e.error is error
+ assert e.rule is my_rule
+ # Check that index default is None:
+ assert self.cls(name, value, error, my_rule).index is None
+
+
+class test_RequirementError(ClassChecker):
+ """
+ Test the `ipalib.errors.RequirementError` exception.
+ """
+ _cls = errors.RequirementError
+
+ def test_class(self):
+ """
+ Test the `ipalib.errors.RequirementError` exception.
+ """
+ assert self.cls.__bases__ == (errors.ValidationError,)
+
+ def test_init(self):
+ """
+ Test the `ipalib.errors.RequirementError.__init__` method.
+ """
+ name = 'givenname'
+ e = self.cls(name)
+ assert e.name is name
+ assert e.value is None
+ assert e.error == 'Required'
+ assert e.index is None
diff --git a/tests/test_ipalib/test_frontend.py b/tests/test_ipalib/test_frontend.py
new file mode 100644
index 000000000..071a70fd5
--- /dev/null
+++ b/tests/test_ipalib/test_frontend.py
@@ -0,0 +1,771 @@
+# 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
+
+"""
+Test the `ipalib.frontend` module.
+"""
+
+from tests.util import raises, getitem, no_set, no_del, read_only
+from tests.util import check_TypeError, ClassChecker, create_test_api
+from tests.util import assert_equal
+from ipalib.constants import TYPE_ERROR
+from ipalib import frontend, backend, plugable, errors2, errors, parameters, config
+
+
+def test_RULE_FLAG():
+ assert frontend.RULE_FLAG == 'validation_rule'
+
+
+def test_rule():
+ """
+ Test the `ipalib.frontend.rule` function.
+ """
+ flag = frontend.RULE_FLAG
+ rule = frontend.rule
+ def my_func():
+ pass
+ assert not hasattr(my_func, flag)
+ rule(my_func)
+ assert getattr(my_func, flag) is True
+ @rule
+ def my_func2():
+ pass
+ assert getattr(my_func2, flag) is True
+
+
+def test_is_rule():
+ """
+ Test the `ipalib.frontend.is_rule` function.
+ """
+ is_rule = frontend.is_rule
+ flag = frontend.RULE_FLAG
+
+ class no_call(object):
+ def __init__(self, value):
+ if value is not None:
+ assert value in (True, False)
+ setattr(self, flag, value)
+
+ class call(no_call):
+ def __call__(self):
+ pass
+
+ assert is_rule(call(True))
+ assert not is_rule(no_call(True))
+ assert not is_rule(call(False))
+ assert not is_rule(call(None))
+
+
+class test_Command(ClassChecker):
+ """
+ Test the `ipalib.frontend.Command` class.
+ """
+
+ _cls = frontend.Command
+
+ def get_subcls(self):
+ """
+ Return a standard subclass of `ipalib.frontend.Command`.
+ """
+ class Rule(object):
+ def __init__(self, name):
+ self.name = name
+
+ def __call__(self, _, value):
+ if value != self.name:
+ return _('must equal %r') % self.name
+
+ default_from = parameters.DefaultFrom(
+ lambda arg: arg,
+ 'default_from'
+ )
+ normalizer = lambda value: value.lower()
+
+ class example(self.cls):
+ takes_options = (
+ parameters.Str('option0', Rule('option0'),
+ normalizer=normalizer,
+ default_from=default_from,
+ ),
+ parameters.Str('option1', Rule('option1'),
+ normalizer=normalizer,
+ default_from=default_from,
+ ),
+ )
+ return example
+
+ def get_instance(self, args=tuple(), options=tuple()):
+ """
+ Helper method used to test args and options.
+ """
+ class example(self.cls):
+ takes_args = args
+ takes_options = options
+ o = example()
+ o.finalize()
+ return o
+
+ def test_class(self):
+ """
+ Test the `ipalib.frontend.Command` class.
+ """
+ assert self.cls.__bases__ == (plugable.Plugin,)
+ assert self.cls.takes_options == tuple()
+ assert self.cls.takes_args == tuple()
+
+ def test_get_args(self):
+ """
+ Test the `ipalib.frontend.Command.get_args` method.
+ """
+ assert list(self.cls().get_args()) == []
+ args = ('login', 'stuff')
+ o = self.get_instance(args=args)
+ assert o.get_args() is args
+
+ def test_get_options(self):
+ """
+ Test the `ipalib.frontend.Command.get_options` method.
+ """
+ assert list(self.cls().get_options()) == []
+ options = ('verbose', 'debug')
+ o = self.get_instance(options=options)
+ assert o.get_options() is options
+
+ def test_args(self):
+ """
+ Test the ``ipalib.frontend.Command.args`` instance attribute.
+ """
+ assert 'args' in self.cls.__public__ # Public
+ assert self.cls().args is None
+ o = self.cls()
+ o.finalize()
+ assert type(o.args) is plugable.NameSpace
+ assert len(o.args) == 0
+ args = ('destination', 'source?')
+ ns = self.get_instance(args=args).args
+ assert type(ns) is plugable.NameSpace
+ assert len(ns) == len(args)
+ assert list(ns) == ['destination', 'source']
+ assert type(ns.destination) is parameters.Str
+ assert type(ns.source) is parameters.Str
+ assert ns.destination.required is True
+ assert ns.destination.multivalue is False
+ assert ns.source.required is False
+ assert ns.source.multivalue is False
+
+ # Test TypeError:
+ e = raises(TypeError, self.get_instance, args=(u'whatever',))
+ assert str(e) == TYPE_ERROR % (
+ 'spec', (str, parameters.Param), u'whatever', unicode)
+
+ # Test ValueError, required after optional:
+ e = raises(ValueError, self.get_instance, args=('arg1?', 'arg2'))
+ assert str(e) == 'arg2: required argument after optional'
+
+ # Test ValueError, scalar after multivalue:
+ e = raises(ValueError, self.get_instance, args=('arg1+', 'arg2'))
+ assert str(e) == 'arg2: only final argument can be multivalue'
+
+ def test_max_args(self):
+ """
+ Test the ``ipalib.frontend.Command.max_args`` instance attribute.
+ """
+ o = self.get_instance()
+ assert o.max_args == 0
+ o = self.get_instance(args=('one?',))
+ assert o.max_args == 1
+ o = self.get_instance(args=('one', 'two?'))
+ assert o.max_args == 2
+ o = self.get_instance(args=('one', 'multi+',))
+ assert o.max_args is None
+ o = self.get_instance(args=('one', 'multi*',))
+ assert o.max_args is None
+
+ def test_options(self):
+ """
+ Test the ``ipalib.frontend.Command.options`` instance attribute.
+ """
+ assert 'options' in self.cls.__public__ # Public
+ assert self.cls().options is None
+ o = self.cls()
+ o.finalize()
+ assert type(o.options) is plugable.NameSpace
+ assert len(o.options) == 0
+ options = ('target', 'files*')
+ ns = self.get_instance(options=options).options
+ assert type(ns) is plugable.NameSpace
+ assert len(ns) == len(options)
+ assert list(ns) == ['target', 'files']
+ assert type(ns.target) is parameters.Str
+ assert type(ns.files) is parameters.Str
+ assert ns.target.required is True
+ assert ns.target.multivalue is False
+ assert ns.files.required is False
+ assert ns.files.multivalue is True
+
+ def test_convert(self):
+ """
+ Test the `ipalib.frontend.Command.convert` method.
+ """
+ assert 'convert' in self.cls.__public__ # Public
+ kw = dict(
+ option0=u'1.5',
+ option1=u'7',
+ )
+ o = self.subcls()
+ o.finalize()
+ for (key, value) in o.convert(**kw).iteritems():
+ assert_equal(unicode(kw[key]), value)
+
+ def test_normalize(self):
+ """
+ Test the `ipalib.frontend.Command.normalize` method.
+ """
+ assert 'normalize' in self.cls.__public__ # Public
+ kw = dict(
+ option0=u'OPTION0',
+ option1=u'OPTION1',
+ )
+ norm = dict((k, v.lower()) for (k, v) in kw.items())
+ sub = self.subcls()
+ sub.finalize()
+ assert sub.normalize(**kw) == norm
+
+ def test_get_default(self):
+ """
+ Test the `ipalib.frontend.Command.get_default` method.
+ """
+ assert 'get_default' in self.cls.__public__ # Public
+ # FIXME: Add an updated unit tests for get_default()
+
+ def test_validate(self):
+ """
+ Test the `ipalib.frontend.Command.validate` method.
+ """
+ assert 'validate' in self.cls.__public__ # Public
+
+ sub = self.subcls()
+ sub.finalize()
+
+ # Check with valid values
+ okay = dict(
+ option0=u'option0',
+ option1=u'option1',
+ another_option='some value',
+ )
+ sub.validate(**okay)
+
+ # Check with an invalid value
+ fail = dict(okay)
+ fail['option0'] = u'whatever'
+ e = raises(errors2.ValidationError, sub.validate, **fail)
+ assert_equal(e.name, 'option0')
+ assert_equal(e.value, u'whatever')
+ assert_equal(e.error, u"must equal 'option0'")
+ assert e.rule.__class__.__name__ == 'Rule'
+ assert e.index is None
+
+ # Check with a missing required arg
+ fail = dict(okay)
+ fail.pop('option1')
+ e = raises(errors.RequirementError, sub.validate, **fail)
+ assert e.name == 'option1'
+ assert e.value is None
+ assert e.index is None
+
+ def test_execute(self):
+ """
+ Test the `ipalib.frontend.Command.execute` method.
+ """
+ assert 'execute' in self.cls.__public__ # Public
+ o = self.cls()
+ e = raises(NotImplementedError, o.execute)
+ assert str(e) == 'Command.execute()'
+
+ def test_args_to_kw(self):
+ """
+ Test the `ipalib.frontend.Command.args_to_kw` method.
+ """
+ assert 'args_to_kw' in self.cls.__public__ # Public
+ o = self.get_instance(args=('one', 'two?'))
+ assert o.args_to_kw(1) == dict(one=1)
+ assert o.args_to_kw(1, 2) == dict(one=1, two=2)
+
+ o = self.get_instance(args=('one', 'two*'))
+ assert o.args_to_kw(1) == dict(one=1)
+ assert o.args_to_kw(1, 2) == dict(one=1, two=(2,))
+ assert o.args_to_kw(1, 2, 3) == dict(one=1, two=(2, 3))
+
+ o = self.get_instance(args=('one', 'two+'))
+ assert o.args_to_kw(1) == dict(one=1)
+ assert o.args_to_kw(1, 2) == dict(one=1, two=(2,))
+ assert o.args_to_kw(1, 2, 3) == dict(one=1, two=(2, 3))
+
+ o = self.get_instance()
+ e = raises(errors.ArgumentError, o.args_to_kw, 1)
+ assert str(e) == 'example takes no arguments'
+
+ o = self.get_instance(args=('one?',))
+ e = raises(errors.ArgumentError, o.args_to_kw, 1, 2)
+ assert str(e) == 'example takes at most 1 argument'
+
+ o = self.get_instance(args=('one', 'two?'))
+ e = raises(errors.ArgumentError, o.args_to_kw, 1, 2, 3)
+ assert str(e) == 'example takes at most 2 arguments'
+
+ def test_params_2_args_options(self):
+ """
+ Test the `ipalib.frontend.Command.params_2_args_options` method.
+ """
+ assert 'params_2_args_options' in self.cls.__public__ # Public
+ o = self.get_instance(args=['one'], options=['two'])
+ assert o.params_2_args_options({}) == ((None,), dict(two=None))
+ assert o.params_2_args_options(dict(one=1)) == ((1,), dict(two=None))
+ assert o.params_2_args_options(dict(two=2)) == ((None,), dict(two=2))
+ assert o.params_2_args_options(dict(two=2, one=1)) == \
+ ((1,), dict(two=2))
+
+ def test_run(self):
+ """
+ Test the `ipalib.frontend.Command.run` method.
+ """
+ class my_cmd(self.cls):
+ def execute(self, *args, **kw):
+ return ('execute', args, kw)
+
+ def forward(self, *args, **kw):
+ return ('forward', args, kw)
+
+ args = ('Hello,', 'world,')
+ kw = dict(how_are='you', on_this='fine day?')
+
+ # Test in server context:
+ (api, home) = create_test_api(in_server=True)
+ api.finalize()
+ o = my_cmd()
+ o.set_api(api)
+ assert o.run.im_func is self.cls.run.im_func
+ assert ('execute', args, kw) == o.run(*args, **kw)
+ assert o.run.im_func is my_cmd.execute.im_func
+
+ # Test in non-server context
+ (api, home) = create_test_api(in_server=False)
+ api.finalize()
+ o = my_cmd()
+ o.set_api(api)
+ assert o.run.im_func is self.cls.run.im_func
+ assert ('forward', args, kw) == o.run(*args, **kw)
+ assert o.run.im_func is my_cmd.forward.im_func
+
+
+class test_LocalOrRemote(ClassChecker):
+ """
+ Test the `ipalib.frontend.LocalOrRemote` class.
+ """
+ _cls = frontend.LocalOrRemote
+
+ def test_init(self):
+ """
+ Test the `ipalib.frontend.LocalOrRemote.__init__` method.
+ """
+ o = self.cls()
+ o.finalize()
+ assert list(o.args) == []
+ assert list(o.options) == ['server']
+ op = o.options.server
+ assert op.required is False
+ assert op.default is False
+
+ def test_run(self):
+ """
+ Test the `ipalib.frontend.LocalOrRemote.run` method.
+ """
+ class example(self.cls):
+ takes_args = ['key?']
+
+ def forward(self, *args, **options):
+ return ('forward', args, options)
+
+ def execute(self, *args, **options):
+ return ('execute', args, options)
+
+ # Test when in_server=False:
+ (api, home) = create_test_api(in_server=False)
+ api.register(example)
+ api.finalize()
+ cmd = api.Command.example
+ assert cmd() == ('execute', (None,), dict(server=False))
+ assert cmd(u'var') == ('execute', (u'var',), dict(server=False))
+ assert cmd(server=True) == ('forward', (None,), dict(server=True))
+ assert cmd(u'var', server=True) == \
+ ('forward', (u'var',), dict(server=True))
+
+ # Test when in_server=True (should always call execute):
+ (api, home) = create_test_api(in_server=True)
+ api.register(example)
+ api.finalize()
+ cmd = api.Command.example
+ assert cmd() == ('execute', (None,), dict(server=False))
+ assert cmd(u'var') == ('execute', (u'var',), dict(server=False))
+ assert cmd(server=True) == ('execute', (None,), dict(server=True))
+ assert cmd(u'var', server=True) == \
+ ('execute', (u'var',), dict(server=True))
+
+
+class test_Object(ClassChecker):
+ """
+ Test the `ipalib.frontend.Object` class.
+ """
+ _cls = frontend.Object
+
+ def test_class(self):
+ """
+ Test the `ipalib.frontend.Object` class.
+ """
+ assert self.cls.__bases__ == (plugable.Plugin,)
+ assert self.cls.backend is None
+ assert self.cls.methods is None
+ assert self.cls.properties is None
+ assert self.cls.params is None
+ assert self.cls.params_minus_pk is None
+ assert self.cls.takes_params == tuple()
+
+ def test_init(self):
+ """
+ Test the `ipalib.frontend.Object.__init__` method.
+ """
+ o = self.cls()
+ assert o.backend is None
+ assert o.methods is None
+ assert o.properties is None
+ assert o.params is None
+ assert o.params_minus_pk is None
+ assert o.properties is None
+
+ def test_set_api(self):
+ """
+ Test the `ipalib.frontend.Object.set_api` method.
+ """
+ # Setup for test:
+ class DummyAttribute(object):
+ def __init__(self, obj_name, attr_name, name=None):
+ self.obj_name = obj_name
+ self.attr_name = attr_name
+ if name is None:
+ self.name = '%s_%s' % (obj_name, attr_name)
+ else:
+ self.name = name
+ self.param = frontend.create_param(attr_name)
+
+ def __clone__(self, attr_name):
+ return self.__class__(
+ self.obj_name,
+ self.attr_name,
+ getattr(self, attr_name)
+ )
+
+ def get_attributes(cnt, format):
+ for name in ['other', 'user', 'another']:
+ for i in xrange(cnt):
+ yield DummyAttribute(name, format % i)
+
+ cnt = 10
+ formats = dict(
+ methods='method_%d',
+ properties='property_%d',
+ )
+
+
+ _d = dict(
+ Method=plugable.NameSpace(
+ get_attributes(cnt, formats['methods'])
+ ),
+ Property=plugable.NameSpace(
+ get_attributes(cnt, formats['properties'])
+ ),
+ )
+ api = plugable.MagicDict(_d)
+ assert len(api.Method) == cnt * 3
+ assert len(api.Property) == cnt * 3
+
+ class user(self.cls):
+ pass
+
+ # Actually perform test:
+ o = user()
+ o.set_api(api)
+ assert read_only(o, 'api') is api
+ for name in ['methods', 'properties']:
+ namespace = getattr(o, name)
+ assert isinstance(namespace, plugable.NameSpace)
+ assert len(namespace) == cnt
+ f = formats[name]
+ for i in xrange(cnt):
+ attr_name = f % i
+ attr = namespace[attr_name]
+ assert isinstance(attr, DummyAttribute)
+ assert attr is getattr(namespace, attr_name)
+ assert attr.obj_name == 'user'
+ assert attr.attr_name == attr_name
+ assert attr.name == attr_name
+
+ # Test params instance attribute
+ o = self.cls()
+ o.set_api(api)
+ ns = o.params
+ assert type(ns) is plugable.NameSpace
+ assert len(ns) == 0
+ class example(self.cls):
+ takes_params = ('banana', 'apple')
+ o = example()
+ o.set_api(api)
+ ns = o.params
+ assert type(ns) is plugable.NameSpace
+ assert len(ns) == 2, repr(ns)
+ assert list(ns) == ['banana', 'apple']
+ for p in ns():
+ assert type(p) is parameters.Str
+ assert p.required is True
+ assert p.multivalue is False
+
+ def test_primary_key(self):
+ """
+ Test the `ipalib.frontend.Object.primary_key` attribute.
+ """
+ (api, home) = create_test_api()
+ api.finalize()
+
+ # Test with no primary keys:
+ class example1(self.cls):
+ takes_params = (
+ 'one',
+ 'two',
+ )
+ o = example1()
+ o.set_api(api)
+ assert o.primary_key is None
+ assert o.params_minus_pk is None
+
+ # Test with 1 primary key:
+ class example2(self.cls):
+ takes_params = (
+ 'one',
+ 'two',
+ parameters.Str('three', primary_key=True),
+ 'four',
+ )
+ o = example2()
+ o.set_api(api)
+ pk = o.primary_key
+ assert type(pk) is parameters.Str
+ assert pk.name == 'three'
+ assert pk.primary_key is True
+ assert o.params[2] is o.primary_key
+ assert isinstance(o.params_minus_pk, plugable.NameSpace)
+ assert list(o.params_minus_pk) == ['one', 'two', 'four']
+
+ # Test with multiple primary_key:
+ class example3(self.cls):
+ takes_params = (
+ parameters.Str('one', primary_key=True),
+ parameters.Str('two', primary_key=True),
+ 'three',
+ parameters.Str('four', primary_key=True),
+ )
+ o = example3()
+ e = raises(ValueError, o.set_api, api)
+ assert str(e) == \
+ 'example3 (Object) has multiple primary keys: one, two, four'
+
+ def test_backend(self):
+ """
+ Test the `ipalib.frontend.Object.backend` attribute.
+ """
+ (api, home) = create_test_api()
+ class ldap(backend.Backend):
+ whatever = 'It worked!'
+ api.register(ldap)
+ class user(frontend.Object):
+ backend_name = 'ldap'
+ api.register(user)
+ api.finalize()
+ b = api.Object.user.backend
+ assert isinstance(b, ldap)
+ assert b.whatever == 'It worked!'
+
+ def test_get_dn(self):
+ """
+ Test the `ipalib.frontend.Object.get_dn` method.
+ """
+ assert 'get_dn' in self.cls.__public__ # Public
+ o = self.cls()
+ e = raises(NotImplementedError, o.get_dn, 'primary key')
+ assert str(e) == 'Object.get_dn()'
+ class user(self.cls):
+ pass
+ o = user()
+ e = raises(NotImplementedError, o.get_dn, 'primary key')
+ assert str(e) == 'user.get_dn()'
+
+
+class test_Attribute(ClassChecker):
+ """
+ Test the `ipalib.frontend.Attribute` class.
+ """
+ _cls = frontend.Attribute
+
+ def test_class(self):
+ """
+ Test the `ipalib.frontend.Attribute` class.
+ """
+ assert self.cls.__bases__ == (plugable.Plugin,)
+ assert type(self.cls.obj) is property
+ assert type(self.cls.obj_name) is property
+ assert type(self.cls.attr_name) is property
+
+ def test_init(self):
+ """
+ Test the `ipalib.frontend.Attribute.__init__` method.
+ """
+ class user_add(self.cls):
+ pass
+ o = user_add()
+ assert read_only(o, 'obj') is None
+ assert read_only(o, 'obj_name') == 'user'
+ assert read_only(o, 'attr_name') == 'add'
+
+ def test_set_api(self):
+ """
+ Test the `ipalib.frontend.Attribute.set_api` method.
+ """
+ user_obj = 'The user frontend.Object instance'
+ class api(object):
+ Object = dict(user=user_obj)
+ class user_add(self.cls):
+ pass
+ o = user_add()
+ assert read_only(o, 'api') is None
+ assert read_only(o, 'obj') is None
+ o.set_api(api)
+ assert read_only(o, 'api') is api
+ assert read_only(o, 'obj') is user_obj
+
+
+class test_Method(ClassChecker):
+ """
+ Test the `ipalib.frontend.Method` class.
+ """
+ _cls = frontend.Method
+
+ def test_class(self):
+ """
+ Test the `ipalib.frontend.Method` class.
+ """
+ assert self.cls.__bases__ == (frontend.Attribute, frontend.Command)
+ assert self.cls.implements(frontend.Command)
+ assert self.cls.implements(frontend.Attribute)
+
+ def test_init(self):
+ """
+ Test the `ipalib.frontend.Method.__init__` method.
+ """
+ class user_add(self.cls):
+ pass
+ o = user_add()
+ assert o.name == 'user_add'
+ assert o.obj_name == 'user'
+ assert o.attr_name == 'add'
+ assert frontend.Command.implemented_by(o)
+ assert frontend.Attribute.implemented_by(o)
+
+
+class test_Property(ClassChecker):
+ """
+ Test the `ipalib.frontend.Property` class.
+ """
+ _cls = frontend.Property
+
+ def get_subcls(self):
+ """
+ Return a standard subclass of `ipalib.frontend.Property`.
+ """
+ class user_givenname(self.cls):
+ 'User first name'
+
+ @frontend.rule
+ def rule0_lowercase(self, value):
+ if not value.islower():
+ return 'Must be lowercase'
+ return user_givenname
+
+ def test_class(self):
+ """
+ Test the `ipalib.frontend.Property` class.
+ """
+ assert self.cls.__bases__ == (frontend.Attribute,)
+ assert self.cls.klass is parameters.Str
+
+ def test_init(self):
+ """
+ Test the `ipalib.frontend.Property.__init__` method.
+ """
+ o = self.subcls()
+ assert len(o.rules) == 1
+ assert o.rules[0].__name__ == 'rule0_lowercase'
+ param = o.param
+ assert isinstance(param, parameters.Str)
+ assert param.name == 'givenname'
+ assert param.doc == 'User first name'
+
+
+class test_Application(ClassChecker):
+ """
+ Test the `ipalib.frontend.Application` class.
+ """
+ _cls = frontend.Application
+
+ def test_class(self):
+ """
+ Test the `ipalib.frontend.Application` class.
+ """
+ assert self.cls.__bases__ == (frontend.Command,)
+ assert type(self.cls.application) is property
+
+ def test_application(self):
+ """
+ Test the `ipalib.frontend.Application.application` property.
+ """
+ assert 'application' in self.cls.__public__ # Public
+ assert 'set_application' in self.cls.__public__ # Public
+ app = 'The external application'
+ class example(self.cls):
+ 'A subclass'
+ for o in (self.cls(), example()):
+ assert read_only(o, 'application') is None
+ e = raises(TypeError, o.set_application, None)
+ assert str(e) == (
+ '%s.application cannot be None' % o.__class__.__name__
+ )
+ o.set_application(app)
+ assert read_only(o, 'application') is app
+ e = raises(AttributeError, o.set_application, app)
+ assert str(e) == (
+ '%s.application can only be set once' % o.__class__.__name__
+ )
+ assert read_only(o, 'application') is app
diff --git a/tests/test_ipalib/test_parameters.py b/tests/test_ipalib/test_parameters.py
new file mode 100644
index 000000000..261e14811
--- /dev/null
+++ b/tests/test_ipalib/test_parameters.py
@@ -0,0 +1,994 @@
+# 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
+
+"""
+Test the `ipalib.parameters` module.
+"""
+
+from types import NoneType
+from inspect import isclass
+from tests.util import raises, ClassChecker, read_only
+from tests.util import dummy_ugettext, assert_equal
+from tests.data import binary_bytes, utf8_bytes, unicode_str
+from ipalib import parameters, request, errors2
+from ipalib.constants import TYPE_ERROR, CALLABLE_ERROR, NULLS
+
+
+class test_DefaultFrom(ClassChecker):
+ """
+ Test the `ipalib.parameters.DefaultFrom` class.
+ """
+ _cls = parameters.DefaultFrom
+
+ def test_init(self):
+ """
+ Test the `ipalib.parameters.DefaultFrom.__init__` method.
+ """
+ def callback(*args):
+ return args
+ keys = ('givenname', 'sn')
+ o = self.cls(callback, *keys)
+ assert read_only(o, 'callback') is callback
+ assert read_only(o, 'keys') == keys
+ lam = lambda first, last: first[0] + last
+ o = self.cls(lam)
+ assert read_only(o, 'keys') == ('first', 'last')
+
+ # Test that TypeError is raised when callback isn't callable:
+ e = raises(TypeError, self.cls, 'whatever')
+ assert str(e) == CALLABLE_ERROR % ('callback', 'whatever', str)
+
+ # Test that TypeError is raised when a key isn't an str:
+ e = raises(TypeError, self.cls, callback, 'givenname', 17)
+ assert str(e) == TYPE_ERROR % ('keys', str, 17, int)
+
+ def test_call(self):
+ """
+ Test the `ipalib.parameters.DefaultFrom.__call__` method.
+ """
+ def callback(givenname, sn):
+ return givenname[0] + sn[0]
+ keys = ('givenname', 'sn')
+ o = self.cls(callback, *keys)
+ kw = dict(
+ givenname='John',
+ sn='Public',
+ hello='world',
+ )
+ assert o(**kw) == 'JP'
+ assert o() is None
+ for key in ('givenname', 'sn'):
+ kw_copy = dict(kw)
+ del kw_copy[key]
+ assert o(**kw_copy) is None
+
+ # Test using implied keys:
+ o = self.cls(lambda first, last: first[0] + last)
+ assert o(first='john', last='doe') == 'jdoe'
+ assert o(first='', last='doe') is None
+ assert o(one='john', two='doe') is None
+
+ # Test that co_varnames slice is used:
+ def callback2(first, last):
+ letter = first[0]
+ return letter + last
+ o = self.cls(callback2)
+ assert o.keys == ('first', 'last')
+ assert o(first='john', last='doe') == 'jdoe'
+
+
+def test_parse_param_spec():
+ """
+ Test the `ipalib.parameters.parse_param_spec` function.
+ """
+ f = parameters.parse_param_spec
+ assert f('name') == ('name', dict(required=True, multivalue=False))
+ assert f('name?') == ('name', dict(required=False, multivalue=False))
+ assert f('name*') == ('name', dict(required=False, multivalue=True))
+ assert f('name+') == ('name', dict(required=True, multivalue=True))
+
+ # Make sure other "funny" endings are *not* treated special:
+ assert f('name^') == ('name^', dict(required=True, multivalue=False))
+
+ # Test that TypeError is raised if spec isn't an str:
+ e = raises(TypeError, f, u'name?')
+ assert str(e) == TYPE_ERROR % ('spec', str, u'name?', unicode)
+
+ # Test that ValueError is raised if len(spec) < 2:
+ e = raises(ValueError, f, 'n')
+ assert str(e) == "spec must be at least 2 characters; got 'n'"
+
+
+class DummyRule(object):
+ def __init__(self, error=None):
+ assert error is None or type(error) is unicode
+ self.error = error
+ self.reset()
+
+ def __call__(self, *args):
+ self.calls.append(args)
+ return self.error
+
+ def reset(self):
+ self.calls = []
+
+
+class test_Param(ClassChecker):
+ """
+ Test the `ipalib.parameters.Param` class.
+ """
+ _cls = parameters.Param
+
+ def test_init(self):
+ """
+ Test the `ipalib.parameters.Param.__init__` method.
+ """
+ name = 'my_param'
+ o = self.cls(name)
+ assert o.param_spec is name
+ assert o.name is name
+ assert o.nice == "Param('my_param')"
+ assert o.__islocked__() is True
+
+ # Test default rules:
+ assert o.rules == tuple()
+ assert o.class_rules == tuple()
+ assert o.all_rules == tuple()
+
+ # Test default kwarg values:
+ assert o.cli_name is name
+ assert o.label is None
+ assert o.doc == ''
+ assert o.required is True
+ assert o.multivalue is False
+ assert o.primary_key is False
+ assert o.normalizer is None
+ assert o.default is None
+ assert o.default_from is None
+ assert o.create_default is None
+ assert o._get_default is None
+ assert o.autofill is False
+ assert o.query is False
+ assert o.flags == frozenset()
+
+ # Test that ValueError is raised when a kwarg from a subclass
+ # conflicts with an attribute:
+ class Subclass(self.cls):
+ kwargs = self.cls.kwargs + (
+ ('convert', callable, None),
+ )
+ e = raises(ValueError, Subclass, name)
+ assert str(e) == "kwarg 'convert' conflicts with attribute on Subclass"
+
+ # Test type validation of keyword arguments:
+ class Subclass(self.cls):
+ kwargs = self.cls.kwargs + (
+ ('extra1', bool, True),
+ ('extra2', str, 'Hello'),
+ ('extra3', (int, float), 42),
+ ('extra4', callable, lambda whatever: whatever + 7),
+ )
+ o = Subclass('my_param') # Test with no **kw:
+ for (key, kind, default) in o.kwargs:
+ # Test with a type invalid for all:
+ value = object()
+ kw = {key: value}
+ e = raises(TypeError, Subclass, 'my_param', **kw)
+ if kind is callable:
+ assert str(e) == CALLABLE_ERROR % (key, value, type(value))
+ else:
+ assert str(e) == TYPE_ERROR % (key, kind, value, type(value))
+ # Test with None:
+ kw = {key: None}
+ Subclass('my_param', **kw)
+
+ # Test when using unknown kwargs:
+ e = raises(TypeError, self.cls, 'my_param',
+ flags=['hello', 'world'],
+ whatever=u'Hooray!',
+ )
+ assert str(e) == \
+ "Param('my_param'): takes no such kwargs: 'whatever'"
+ e = raises(TypeError, self.cls, 'my_param', great='Yes', ape='he is!')
+ assert str(e) == \
+ "Param('my_param'): takes no such kwargs: 'ape', 'great'"
+
+ # Test that ValueError is raised if you provide both default_from and
+ # create_default:
+ e = raises(ValueError, self.cls, 'my_param',
+ default_from=lambda first, last: first[0] + last,
+ create_default=lambda **kw: 'The Default'
+ )
+ assert str(e) == '%s: cannot have both %r and %r' % (
+ "Param('my_param')", 'default_from', 'create_default',
+ )
+
+ # Test that _get_default gets set:
+ call1 = lambda first, last: first[0] + last
+ call2 = lambda **kw: 'The Default'
+ o = self.cls('my_param', default_from=call1)
+ assert o.default_from.callback is call1
+ assert o._get_default is o.default_from
+ o = self.cls('my_param', create_default=call2)
+ assert o.create_default is call2
+ assert o._get_default is call2
+
+ def test_repr(self):
+ """
+ Test the `ipalib.parameters.Param.__repr__` method.
+ """
+ for name in ['name', 'name?', 'name*', 'name+']:
+ o = self.cls(name)
+ assert repr(o) == 'Param(%r)' % name
+ o = self.cls('name', required=False)
+ assert repr(o) == "Param('name', required=False)"
+ o = self.cls('name', multivalue=True)
+ assert repr(o) == "Param('name', multivalue=True)"
+
+ def test_clone(self):
+ """
+ Test the `ipalib.parameters.Param.clone` method.
+ """
+ # Test with the defaults
+ orig = self.cls('my_param')
+ clone = orig.clone()
+ assert clone is not orig
+ assert type(clone) is self.cls
+ assert clone.name is orig.name
+ for (key, kind, default) in self.cls.kwargs:
+ assert getattr(clone, key) is getattr(orig, key)
+
+ # Test with a param spec:
+ orig = self.cls('my_param*')
+ assert orig.param_spec == 'my_param*'
+ clone = orig.clone()
+ assert clone.param_spec == 'my_param'
+ assert clone is not orig
+ assert type(clone) is self.cls
+ for (key, kind, default) in self.cls.kwargs:
+ assert getattr(clone, key) is getattr(orig, key)
+
+ # Test with overrides:
+ orig = self.cls('my_param*')
+ assert orig.required is False
+ assert orig.multivalue is True
+ clone = orig.clone(required=True)
+ assert clone is not orig
+ assert type(clone) is self.cls
+ assert clone.required is True
+ assert clone.multivalue is True
+ assert clone.param_spec == 'my_param'
+ assert clone.name == 'my_param'
+
+ def test_get_label(self):
+ """
+ Test the `ipalib.parameters.get_label` method.
+ """
+ context = request.context
+ cli_name = 'the_cli_name'
+ message = 'The Label'
+ label = lambda _: _(message)
+ o = self.cls('name', cli_name=cli_name, label=label)
+ assert o.label is label
+
+ ## Scenario 1: label=callable (a lambda form)
+
+ # Test with no context.ugettext:
+ assert not hasattr(context, 'ugettext')
+ assert_equal(o.get_label(), u'The Label')
+
+ # Test with dummy context.ugettext:
+ assert not hasattr(context, 'ugettext')
+ dummy = dummy_ugettext()
+ context.ugettext = dummy
+ assert o.get_label() is dummy.translation
+ assert dummy.message is message
+ del context.ugettext
+
+ ## Scenario 2: label=None
+ o = self.cls('name', cli_name=cli_name)
+ assert o.label is None
+
+ # Test with no context.ugettext:
+ assert not hasattr(context, 'ugettext')
+ assert_equal(o.get_label(), u'the_cli_name')
+
+ # Test with dummy context.ugettext:
+ assert not hasattr(context, 'ugettext')
+ dummy = dummy_ugettext()
+ context.ugettext = dummy
+ assert_equal(o.get_label(), u'the_cli_name')
+ assert not hasattr(dummy, 'message')
+
+ # Cleanup
+ del context.ugettext
+ assert not hasattr(context, 'ugettext')
+
+ def test_convert(self):
+ """
+ Test the `ipalib.parameters.Param.convert` method.
+ """
+ okay = ('Hello', u'Hello', 0, 4.2, True, False)
+ class Subclass(self.cls):
+ def _convert_scalar(self, value, index=None):
+ return value
+
+ # Test when multivalue=False:
+ o = Subclass('my_param')
+ for value in NULLS:
+ assert o.convert(value) is None
+ for value in okay:
+ assert o.convert(value) is value
+
+ # Test when multivalue=True:
+ o = Subclass('my_param', multivalue=True)
+ for value in NULLS:
+ assert o.convert(value) is None
+ assert o.convert(okay) == okay
+ assert o.convert(NULLS) is None
+ assert o.convert(okay + NULLS) == okay
+ assert o.convert(NULLS + okay) == okay
+ for value in okay:
+ assert o.convert(value) == (value,)
+ assert o.convert([None, value]) == (value,)
+ assert o.convert([value, None]) == (value,)
+
+ def test_convert_scalar(self):
+ """
+ Test the `ipalib.parameters.Param._convert_scalar` method.
+ """
+ dummy = dummy_ugettext()
+
+ # Test with correct type:
+ o = self.cls('my_param')
+ assert o._convert_scalar(None) is None
+ assert dummy.called() is False
+ # Test with incorrect type
+ e = raises(errors2.ConversionError, o._convert_scalar, 'hello', index=17)
+
+ def test_validate(self):
+ """
+ Test the `ipalib.parameters.Param.validate` method.
+ """
+
+ # Test in default state (with no rules, no kwarg):
+ o = self.cls('my_param')
+ e = raises(errors2.RequirementError, o.validate, None)
+ assert e.name == 'my_param'
+
+ # Test with required=False
+ o = self.cls('my_param', required=False)
+ assert o.required is False
+ assert o.validate(None) is None
+
+ # Test with query=True:
+ o = self.cls('my_param', query=True)
+ assert o.query is True
+ e = raises(errors2.RequirementError, o.validate, None)
+ assert_equal(e.name, 'my_param')
+
+ # Test with multivalue=True:
+ o = self.cls('my_param', multivalue=True)
+ e = raises(TypeError, o.validate, [])
+ assert str(e) == TYPE_ERROR % ('value', tuple, [], list)
+ e = raises(ValueError, o.validate, tuple())
+ assert str(e) == 'value: empty tuple must be converted to None'
+
+ # Test with wrong (scalar) type:
+ e = raises(TypeError, o.validate, (None, None, 42, None))
+ assert str(e) == TYPE_ERROR % ('value[2]', NoneType, 42, int)
+ o = self.cls('my_param')
+ e = raises(TypeError, o.validate, 'Hello')
+ assert str(e) == TYPE_ERROR % ('value', NoneType, 'Hello', str)
+
+ class Example(self.cls):
+ type = int
+
+ # Test with some rules and multivalue=False
+ pass1 = DummyRule()
+ pass2 = DummyRule()
+ fail = DummyRule(u'no good')
+ o = Example('example', pass1, pass2)
+ assert o.multivalue is False
+ assert o.validate(11) is None
+ assert pass1.calls == [(request.ugettext, 11)]
+ assert pass2.calls == [(request.ugettext, 11)]
+ pass1.reset()
+ pass2.reset()
+ o = Example('example', pass1, pass2, fail)
+ e = raises(errors2.ValidationError, o.validate, 42)
+ assert e.name == 'example'
+ assert e.error == u'no good'
+ assert e.index is None
+ assert pass1.calls == [(request.ugettext, 42)]
+ assert pass2.calls == [(request.ugettext, 42)]
+ assert fail.calls == [(request.ugettext, 42)]
+
+ # Test with some rules and multivalue=True
+ pass1 = DummyRule()
+ pass2 = DummyRule()
+ fail = DummyRule(u'this one is not good')
+ o = Example('example', pass1, pass2, multivalue=True)
+ assert o.multivalue is True
+ assert o.validate((3, 9)) is None
+ assert pass1.calls == [
+ (request.ugettext, 3),
+ (request.ugettext, 9),
+ ]
+ assert pass2.calls == [
+ (request.ugettext, 3),
+ (request.ugettext, 9),
+ ]
+ pass1.reset()
+ pass2.reset()
+ o = Example('multi_example', pass1, pass2, fail, multivalue=True)
+ assert o.multivalue is True
+ e = raises(errors2.ValidationError, o.validate, (3, 9))
+ assert e.name == 'multi_example'
+ assert e.error == u'this one is not good'
+ assert e.index == 0
+ assert pass1.calls == [(request.ugettext, 3)]
+ assert pass2.calls == [(request.ugettext, 3)]
+ assert fail.calls == [(request.ugettext, 3)]
+
+ def test_validate_scalar(self):
+ """
+ Test the `ipalib.parameters.Param._validate_scalar` method.
+ """
+ class MyParam(self.cls):
+ type = bool
+ okay = DummyRule()
+ o = MyParam('my_param', okay)
+
+ # Test that TypeError is appropriately raised:
+ e = raises(TypeError, o._validate_scalar, 0)
+ assert str(e) == TYPE_ERROR % ('value', bool, 0, int)
+ e = raises(TypeError, o._validate_scalar, 'Hi', index=4)
+ assert str(e) == TYPE_ERROR % ('value[4]', bool, 'Hi', str)
+ e = raises(TypeError, o._validate_scalar, True, index=3.0)
+ assert str(e) == TYPE_ERROR % ('index', int, 3.0, float)
+
+ # Test with passing rule:
+ assert o._validate_scalar(True, index=None) is None
+ assert o._validate_scalar(False, index=None) is None
+ assert okay.calls == [
+ (request.ugettext, True),
+ (request.ugettext, False),
+ ]
+
+ # Test with a failing rule:
+ okay = DummyRule()
+ fail = DummyRule(u'this describes the error')
+ o = MyParam('my_param', okay, fail)
+ e = raises(errors2.ValidationError, o._validate_scalar, True)
+ assert e.name == 'my_param'
+ assert e.error == u'this describes the error'
+ assert e.index is None
+ e = raises(errors2.ValidationError, o._validate_scalar, False, index=2)
+ assert e.name == 'my_param'
+ assert e.error == u'this describes the error'
+ assert e.index == 2
+ assert okay.calls == [
+ (request.ugettext, True),
+ (request.ugettext, False),
+ ]
+ assert fail.calls == [
+ (request.ugettext, True),
+ (request.ugettext, False),
+ ]
+
+ def test_get_default(self):
+ """
+ Test the `ipalib.parameters.Param._get_default` method.
+ """
+ class PassThrough(object):
+ value = None
+
+ def __call__(self, value):
+ assert self.value is None
+ assert value is not None
+ self.value = value
+ return value
+
+ def reset(self):
+ assert self.value is not None
+ self.value = None
+
+ class Str(self.cls):
+ type = unicode
+
+ def __init__(self, name, **kw):
+ self._convert_scalar = PassThrough()
+ super(Str, self).__init__(name, **kw)
+
+ # Test with only a static default:
+ o = Str('my_str',
+ normalizer=PassThrough(),
+ default=u'Static Default',
+ )
+ assert_equal(o.get_default(), u'Static Default')
+ assert o._convert_scalar.value is None
+ assert o.normalizer.value is None
+
+ # Test with default_from:
+ o = Str('my_str',
+ normalizer=PassThrough(),
+ default=u'Static Default',
+ default_from=lambda first, last: first[0] + last,
+ )
+ assert_equal(o.get_default(), u'Static Default')
+ assert o._convert_scalar.value is None
+ assert o.normalizer.value is None
+ default = o.get_default(first=u'john', last='doe')
+ assert_equal(default, u'jdoe')
+ assert o._convert_scalar.value is default
+ assert o.normalizer.value is default
+
+ # Test with create_default:
+ o = Str('my_str',
+ normalizer=PassThrough(),
+ default=u'Static Default',
+ create_default=lambda **kw: u'The created default',
+ )
+ default = o.get_default(first=u'john', last='doe')
+ assert_equal(default, u'The created default')
+ assert o._convert_scalar.value is default
+ assert o.normalizer.value is default
+
+
+class test_Flag(ClassChecker):
+ """
+ Test the `ipalib.parameters.Flag` class.
+ """
+ _cls = parameters.Flag
+
+ def test_init(self):
+ """
+ Test the `ipalib.parameters.Flag.__init__` method.
+ """
+ # Test with no kwargs:
+ o = self.cls('my_flag')
+ assert o.type is bool
+ assert isinstance(o, parameters.Bool)
+ assert o.autofill is True
+ assert o.default is False
+
+ # Test that TypeError is raise if default is not a bool:
+ e = raises(TypeError, self.cls, 'my_flag', default=None)
+ assert str(e) == TYPE_ERROR % ('default', bool, None, NoneType)
+
+ # Test with autofill=False, default=True
+ o = self.cls('my_flag', autofill=False, default=True)
+ assert o.autofill is True
+ assert o.default is True
+
+ # Test when cloning:
+ orig = self.cls('my_flag')
+ for clone in [orig.clone(), orig.clone(autofill=False)]:
+ assert clone.autofill is True
+ assert clone.default is False
+ assert clone is not orig
+ assert type(clone) is self.cls
+
+ # Test when cloning with default=True/False
+ orig = self.cls('my_flag')
+ assert orig.clone().default is False
+ assert orig.clone(default=True).default is True
+ orig = self.cls('my_flag', default=True)
+ assert orig.clone().default is True
+ assert orig.clone(default=False).default is False
+
+
+class test_Data(ClassChecker):
+ """
+ Test the `ipalib.parameters.Data` class.
+ """
+ _cls = parameters.Data
+
+ def test_init(self):
+ """
+ Test the `ipalib.parameters.Data.__init__` method.
+ """
+ o = self.cls('my_data')
+ assert o.type is NoneType
+ assert o.rules == tuple()
+ assert o.class_rules == tuple()
+ assert o.all_rules == tuple()
+ assert o.minlength is None
+ assert o.maxlength is None
+ assert o.length is None
+ assert not hasattr(o, 'pattern')
+
+ # Test mixing length with minlength or maxlength:
+ o = self.cls('my_data', length=5)
+ assert o.length == 5
+ permutations = [
+ dict(minlength=3),
+ dict(maxlength=7),
+ dict(minlength=3, maxlength=7),
+ ]
+ for kw in permutations:
+ o = self.cls('my_data', **kw)
+ for (key, value) in kw.iteritems():
+ assert getattr(o, key) == value
+ e = raises(ValueError, self.cls, 'my_data', length=5, **kw)
+ assert str(e) == \
+ "Data('my_data'): cannot mix length with minlength or maxlength"
+
+ # Test when minlength or maxlength are less than 1:
+ e = raises(ValueError, self.cls, 'my_data', minlength=0)
+ assert str(e) == "Data('my_data'): minlength must be >= 1; got 0"
+ e = raises(ValueError, self.cls, 'my_data', maxlength=0)
+ assert str(e) == "Data('my_data'): maxlength must be >= 1; got 0"
+
+ # Test when minlength > maxlength:
+ e = raises(ValueError, self.cls, 'my_data', minlength=22, maxlength=15)
+ assert str(e) == \
+ "Data('my_data'): minlength > maxlength (minlength=22, maxlength=15)"
+
+ # Test when minlength == maxlength
+ e = raises(ValueError, self.cls, 'my_data', minlength=7, maxlength=7)
+ assert str(e) == \
+ "Data('my_data'): minlength == maxlength; use length=7 instead"
+
+
+class test_Bytes(ClassChecker):
+ """
+ Test the `ipalib.parameters.Bytes` class.
+ """
+ _cls = parameters.Bytes
+
+ def test_init(self):
+ """
+ Test the `ipalib.parameters.Bytes.__init__` method.
+ """
+ o = self.cls('my_bytes')
+ assert o.type is str
+ assert o.rules == tuple()
+ assert o.class_rules == tuple()
+ assert o.all_rules == tuple()
+ assert o.minlength is None
+ assert o.maxlength is None
+ assert o.length is None
+ assert o.pattern is None
+
+ # Test mixing length with minlength or maxlength:
+ o = self.cls('my_bytes', length=5)
+ assert o.length == 5
+ assert len(o.class_rules) == 1
+ assert len(o.rules) == 0
+ assert len(o.all_rules) == 1
+ permutations = [
+ dict(minlength=3),
+ dict(maxlength=7),
+ dict(minlength=3, maxlength=7),
+ ]
+ for kw in permutations:
+ o = self.cls('my_bytes', **kw)
+ assert len(o.class_rules) == len(kw)
+ assert len(o.rules) == 0
+ assert len(o.all_rules) == len(kw)
+ for (key, value) in kw.iteritems():
+ assert getattr(o, key) == value
+ e = raises(ValueError, self.cls, 'my_bytes', length=5, **kw)
+ assert str(e) == \
+ "Bytes('my_bytes'): cannot mix length with minlength or maxlength"
+
+ # Test when minlength or maxlength are less than 1:
+ e = raises(ValueError, self.cls, 'my_bytes', minlength=0)
+ assert str(e) == "Bytes('my_bytes'): minlength must be >= 1; got 0"
+ e = raises(ValueError, self.cls, 'my_bytes', maxlength=0)
+ assert str(e) == "Bytes('my_bytes'): maxlength must be >= 1; got 0"
+
+ # Test when minlength > maxlength:
+ e = raises(ValueError, self.cls, 'my_bytes', minlength=22, maxlength=15)
+ assert str(e) == \
+ "Bytes('my_bytes'): minlength > maxlength (minlength=22, maxlength=15)"
+
+ # Test when minlength == maxlength
+ e = raises(ValueError, self.cls, 'my_bytes', minlength=7, maxlength=7)
+ assert str(e) == \
+ "Bytes('my_bytes'): minlength == maxlength; use length=7 instead"
+
+ def test_rule_minlength(self):
+ """
+ Test the `ipalib.parameters.Bytes._rule_minlength` method.
+ """
+ o = self.cls('my_bytes', minlength=3)
+ assert o.minlength == 3
+ rule = o._rule_minlength
+ translation = u'minlength=%(minlength)r'
+ dummy = dummy_ugettext(translation)
+ assert dummy.translation is translation
+
+ # Test with passing values:
+ for value in ('abc', 'four', '12345'):
+ assert rule(dummy, value) is None
+ assert dummy.called() is False
+
+ # Test with failing values:
+ for value in ('', 'a', '12'):
+ assert_equal(
+ rule(dummy, value),
+ translation % dict(minlength=3)
+ )
+ assert dummy.message == 'must be at least %(minlength)d bytes'
+ assert dummy.called() is True
+ dummy.reset()
+
+ def test_rule_maxlength(self):
+ """
+ Test the `ipalib.parameters.Bytes._rule_maxlength` method.
+ """
+ o = self.cls('my_bytes', maxlength=4)
+ assert o.maxlength == 4
+ rule = o._rule_maxlength
+ translation = u'maxlength=%(maxlength)r'
+ dummy = dummy_ugettext(translation)
+ assert dummy.translation is translation
+
+ # Test with passing values:
+ for value in ('ab', '123', 'four'):
+ assert rule(dummy, value) is None
+ assert dummy.called() is False
+
+ # Test with failing values:
+ for value in ('12345', 'sixsix'):
+ assert_equal(
+ rule(dummy, value),
+ translation % dict(maxlength=4)
+ )
+ assert dummy.message == 'can be at most %(maxlength)d bytes'
+ assert dummy.called() is True
+ dummy.reset()
+
+ def test_rule_length(self):
+ """
+ Test the `ipalib.parameters.Bytes._rule_length` method.
+ """
+ o = self.cls('my_bytes', length=4)
+ assert o.length == 4
+ rule = o._rule_length
+ translation = u'length=%(length)r'
+ dummy = dummy_ugettext(translation)
+ assert dummy.translation is translation
+
+ # Test with passing values:
+ for value in ('1234', 'four'):
+ assert rule(dummy, value) is None
+ assert dummy.called() is False
+
+ # Test with failing values:
+ for value in ('ab', '123', '12345', 'sixsix'):
+ assert_equal(
+ rule(dummy, value),
+ translation % dict(length=4),
+ )
+ assert dummy.message == 'must be exactly %(length)d bytes'
+ assert dummy.called() is True
+ dummy.reset()
+
+
+class test_Str(ClassChecker):
+ """
+ Test the `ipalib.parameters.Str` class.
+ """
+ _cls = parameters.Str
+
+ def test_init(self):
+ """
+ Test the `ipalib.parameters.Str.__init__` method.
+ """
+ o = self.cls('my_str')
+ assert o.type is unicode
+ assert o.minlength is None
+ assert o.maxlength is None
+ assert o.length is None
+ assert o.pattern is None
+
+ def test_convert_scalar(self):
+ """
+ Test the `ipalib.parameters.Str._convert_scalar` method.
+ """
+ o = self.cls('my_str')
+ mthd = o._convert_scalar
+ for value in (u'Hello', 42, 1.2):
+ assert mthd(value) == unicode(value)
+ for value in [True, 'Hello', (u'Hello',), [42.3], dict(one=1)]:
+ e = raises(errors2.ConversionError, mthd, value)
+ assert e.name == 'my_str'
+ assert e.index is None
+ assert_equal(e.error, u'must be Unicode text')
+ e = raises(errors2.ConversionError, mthd, value, index=18)
+ assert e.name == 'my_str'
+ assert e.index == 18
+ assert_equal(e.error, u'must be Unicode text')
+
+ def test_rule_minlength(self):
+ """
+ Test the `ipalib.parameters.Str._rule_minlength` method.
+ """
+ o = self.cls('my_str', minlength=3)
+ assert o.minlength == 3
+ rule = o._rule_minlength
+ translation = u'minlength=%(minlength)r'
+ dummy = dummy_ugettext(translation)
+ assert dummy.translation is translation
+
+ # Test with passing values:
+ for value in (u'abc', u'four', u'12345'):
+ assert rule(dummy, value) is None
+ assert dummy.called() is False
+
+ # Test with failing values:
+ for value in (u'', u'a', u'12'):
+ assert_equal(
+ rule(dummy, value),
+ translation % dict(minlength=3)
+ )
+ assert dummy.message == 'must be at least %(minlength)d characters'
+ assert dummy.called() is True
+ dummy.reset()
+
+ def test_rule_maxlength(self):
+ """
+ Test the `ipalib.parameters.Str._rule_maxlength` method.
+ """
+ o = self.cls('my_str', maxlength=4)
+ assert o.maxlength == 4
+ rule = o._rule_maxlength
+ translation = u'maxlength=%(maxlength)r'
+ dummy = dummy_ugettext(translation)
+ assert dummy.translation is translation
+
+ # Test with passing values:
+ for value in (u'ab', u'123', u'four'):
+ assert rule(dummy, value) is None
+ assert dummy.called() is False
+
+ # Test with failing values:
+ for value in (u'12345', u'sixsix'):
+ assert_equal(
+ rule(dummy, value),
+ translation % dict(maxlength=4)
+ )
+ assert dummy.message == 'can be at most %(maxlength)d characters'
+ assert dummy.called() is True
+ dummy.reset()
+
+ def test_rule_length(self):
+ """
+ Test the `ipalib.parameters.Str._rule_length` method.
+ """
+ o = self.cls('my_str', length=4)
+ assert o.length == 4
+ rule = o._rule_length
+ translation = u'length=%(length)r'
+ dummy = dummy_ugettext(translation)
+ assert dummy.translation is translation
+
+ # Test with passing values:
+ for value in (u'1234', u'four'):
+ assert rule(dummy, value) is None
+ assert dummy.called() is False
+
+ # Test with failing values:
+ for value in (u'ab', u'123', u'12345', u'sixsix'):
+ assert_equal(
+ rule(dummy, value),
+ translation % dict(length=4),
+ )
+ assert dummy.message == 'must be exactly %(length)d characters'
+ assert dummy.called() is True
+ dummy.reset()
+
+
+class test_StrEnum(ClassChecker):
+ """
+ Test the `ipalib.parameters.StrEnum` class.
+ """
+ _cls = parameters.StrEnum
+
+ def test_init(self):
+ """
+ Test the `ipalib.parameters.StrEnum.__init__` method.
+ """
+ values = (u'Hello', u'naughty', u'nurse!')
+ o = self.cls('my_strenum', values=values)
+ assert o.type is unicode
+ assert o.values is values
+ assert o.class_rules == (o._rule_values,)
+ assert o.rules == tuple()
+ assert o.all_rules == (o._rule_values,)
+
+ badvalues = (u'Hello', 'naughty', u'nurse!')
+ e = raises(TypeError, self.cls, 'my_enum', values=badvalues)
+ assert str(e) == TYPE_ERROR % (
+ "StrEnum('my_enum') values[1]", unicode, 'naughty', str
+ )
+
+ def test_rules_values(self):
+ """
+ Test the `ipalib.parameters.StrEnum._rule_values` method.
+ """
+ values = (u'Hello', u'naughty', u'nurse!')
+ o = self.cls('my_enum', values=values)
+ rule = o._rule_values
+ translation = u'values=%(values)s'
+ dummy = dummy_ugettext(translation)
+
+ # Test with passing values:
+ for v in values:
+ assert rule(dummy, v) is None
+ assert dummy.called() is False
+
+ # Test with failing values:
+ for val in (u'Howdy', u'quiet', u'library!'):
+ assert_equal(
+ rule(dummy, val),
+ translation % dict(values=values),
+ )
+ assert_equal(dummy.message, 'must be one of %(values)r')
+ dummy.reset()
+
+
+def test_create_param():
+ """
+ Test the `ipalib.parameters.create_param` function.
+ """
+ f = parameters.create_param
+
+ # Test that Param instances are returned unchanged:
+ params = (
+ parameters.Param('one?'),
+ parameters.Int('two+'),
+ parameters.Str('three*'),
+ parameters.Bytes('four'),
+ )
+ for p in params:
+ assert f(p) is p
+
+ # Test that the spec creates an Str instance:
+ for spec in ('one?', 'two+', 'three*', 'four'):
+ (name, kw) = parameters.parse_param_spec(spec)
+ p = f(spec)
+ assert p.param_spec is spec
+ assert p.name == name
+ assert p.required is kw['required']
+ assert p.multivalue is kw['multivalue']
+
+ # Test that TypeError is raised when spec is neither a Param nor a str:
+ for spec in (u'one', 42, parameters.Param, parameters.Str):
+ e = raises(TypeError, f, spec)
+ assert str(e) == \
+ TYPE_ERROR % ('spec', (str, parameters.Param), spec, type(spec))
+
+
+def test_messages():
+ """
+ Test module level message in `ipalib.parameters`.
+ """
+ for name in dir(parameters):
+ if name.startswith('_'):
+ continue
+ attr = getattr(parameters, name)
+ if not (isclass(attr) and issubclass(attr, parameters.Param)):
+ continue
+ assert type(attr.type_error) is str
+ assert attr.type_error in parameters.__messages
diff --git a/tests/test_ipalib/test_plugable.py b/tests/test_ipalib/test_plugable.py
new file mode 100644
index 000000000..c6c84fa16
--- /dev/null
+++ b/tests/test_ipalib/test_plugable.py
@@ -0,0 +1,756 @@
+# 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
+
+"""
+Test the `ipalib.plugable` module.
+"""
+
+import inspect
+from tests.util import raises, no_set, no_del, read_only
+from tests.util import getitem, setitem, delitem
+from tests.util import ClassChecker, create_test_api
+from ipalib import plugable, errors, errors2
+
+
+class test_SetProxy(ClassChecker):
+ """
+ Test the `ipalib.plugable.SetProxy` class.
+ """
+ _cls = plugable.SetProxy
+
+ def test_class(self):
+ """
+ Test the `ipalib.plugable.SetProxy` class.
+ """
+ assert self.cls.__bases__ == (plugable.ReadOnly,)
+
+ def test_init(self):
+ """
+ Test the `ipalib.plugable.SetProxy.__init__` method.
+ """
+ okay = (set, frozenset, dict)
+ fail = (list, tuple)
+ for t in okay:
+ self.cls(t())
+ raises(TypeError, self.cls, t)
+ for t in fail:
+ raises(TypeError, self.cls, t())
+ raises(TypeError, self.cls, t)
+
+ def test_SetProxy(self):
+ """
+ Test container emulation of `ipalib.plugable.SetProxy` class.
+ """
+ def get_key(i):
+ return 'key_%d' % i
+
+ cnt = 10
+ target = set()
+ proxy = self.cls(target)
+ for i in xrange(cnt):
+ key = get_key(i)
+
+ # Check initial state
+ assert len(proxy) == len(target)
+ assert list(proxy) == sorted(target)
+ assert key not in proxy
+ assert key not in target
+
+ # Add and test again
+ target.add(key)
+ assert len(proxy) == len(target)
+ assert list(proxy) == sorted(target)
+ assert key in proxy
+ assert key in target
+
+
+class test_DictProxy(ClassChecker):
+ """
+ Test the `ipalib.plugable.DictProxy` class.
+ """
+ _cls = plugable.DictProxy
+
+ def test_class(self):
+ """
+ Test the `ipalib.plugable.DictProxy` class.
+ """
+ assert self.cls.__bases__ == (plugable.SetProxy,)
+
+ def test_init(self):
+ """
+ Test the `ipalib.plugable.DictProxy.__init__` method.
+ """
+ self.cls(dict())
+ raises(TypeError, self.cls, dict)
+ fail = (set, frozenset, list, tuple)
+ for t in fail:
+ raises(TypeError, self.cls, t())
+ raises(TypeError, self.cls, t)
+
+ def test_DictProxy(self):
+ """
+ Test container emulation of `ipalib.plugable.DictProxy` class.
+ """
+ def get_kv(i):
+ return (
+ 'key_%d' % i,
+ 'val_%d' % i,
+ )
+ cnt = 10
+ target = dict()
+ proxy = self.cls(target)
+ for i in xrange(cnt):
+ (key, val) = get_kv(i)
+
+ # Check initial state
+ assert len(proxy) == len(target)
+ assert list(proxy) == sorted(target)
+ assert list(proxy()) == [target[k] for k in sorted(target)]
+ assert key not in proxy
+ raises(KeyError, getitem, proxy, key)
+
+ # Add and test again
+ target[key] = val
+ assert len(proxy) == len(target)
+ assert list(proxy) == sorted(target)
+ assert list(proxy()) == [target[k] for k in sorted(target)]
+
+ # Verify TypeError is raised trying to set/del via proxy
+ raises(TypeError, setitem, proxy, key, val)
+ raises(TypeError, delitem, proxy, key)
+
+
+class test_MagicDict(ClassChecker):
+ """
+ Test the `ipalib.plugable.MagicDict` class.
+ """
+ _cls = plugable.MagicDict
+
+ def test_class(self):
+ """
+ Test the `ipalib.plugable.MagicDict` class.
+ """
+ assert self.cls.__bases__ == (plugable.DictProxy,)
+ for non_dict in ('hello', 69, object):
+ raises(TypeError, self.cls, non_dict)
+
+ def test_MagicDict(self):
+ """
+ Test container emulation of `ipalib.plugable.MagicDict` class.
+ """
+ cnt = 10
+ keys = []
+ d = dict()
+ dictproxy = self.cls(d)
+ for i in xrange(cnt):
+ key = 'key_%d' % i
+ val = 'val_%d' % i
+ keys.append(key)
+
+ # Test thet key does not yet exist
+ assert len(dictproxy) == i
+ assert key not in dictproxy
+ assert not hasattr(dictproxy, key)
+ raises(KeyError, getitem, dictproxy, key)
+ raises(AttributeError, getattr, dictproxy, key)
+
+ # Test that items/attributes cannot be set on dictproxy:
+ raises(TypeError, setitem, dictproxy, key, val)
+ raises(AttributeError, setattr, dictproxy, key, val)
+
+ # Test that additions in d are reflected in dictproxy:
+ d[key] = val
+ assert len(dictproxy) == i + 1
+ assert key in dictproxy
+ assert hasattr(dictproxy, key)
+ assert dictproxy[key] is val
+ assert read_only(dictproxy, key) is val
+
+ # Test __iter__
+ assert list(dictproxy) == keys
+
+ for key in keys:
+ # Test that items cannot be deleted through dictproxy:
+ raises(TypeError, delitem, dictproxy, key)
+ raises(AttributeError, delattr, dictproxy, key)
+
+ # Test that deletions in d are reflected in dictproxy
+ del d[key]
+ assert len(dictproxy) == len(d)
+ assert key not in dictproxy
+ raises(KeyError, getitem, dictproxy, key)
+ raises(AttributeError, getattr, dictproxy, key)
+
+
+class test_Plugin(ClassChecker):
+ """
+ Test the `ipalib.plugable.Plugin` class.
+ """
+ _cls = plugable.Plugin
+
+ def test_class(self):
+ """
+ Test the `ipalib.plugable.Plugin` class.
+ """
+ assert self.cls.__bases__ == (plugable.ReadOnly,)
+ assert self.cls.__public__ == frozenset()
+ assert type(self.cls.api) is property
+
+ def test_init(self):
+ """
+ Test the `ipalib.plugable.Plugin.__init__` method.
+ """
+ o = self.cls()
+ assert o.name == 'Plugin'
+ assert o.module == 'ipalib.plugable'
+ assert o.fullname == 'ipalib.plugable.Plugin'
+ assert o.doc == inspect.getdoc(self.cls)
+ class some_subclass(self.cls):
+ """
+ Do sub-classy things.
+
+ Although it doesn't know how to comport itself and is not for mixed
+ company, this class *is* useful as we all need a little sub-class
+ now and then.
+
+ One more paragraph.
+ """
+ o = some_subclass()
+ assert o.name == 'some_subclass'
+ assert o.module == __name__
+ assert o.fullname == '%s.some_subclass' % __name__
+ assert o.doc == inspect.getdoc(some_subclass)
+ assert o.summary == 'Do sub-classy things.'
+ class another_subclass(self.cls):
+ pass
+ o = another_subclass()
+ assert o.doc is None
+ assert o.summary == '<%s>' % o.fullname
+
+ # Test that Plugin makes sure the subclass hasn't defined attributes
+ # whose names conflict with the logger methods set in Plugin.__init__():
+ class check(self.cls):
+ info = 'whatever'
+ e = raises(StandardError, check)
+ assert str(e) == \
+ "check.info attribute ('whatever') conflicts with Plugin logger"
+
+ def test_implements(self):
+ """
+ Test the `ipalib.plugable.Plugin.implements` classmethod.
+ """
+ class example(self.cls):
+ __public__ = frozenset((
+ 'some_method',
+ 'some_property',
+ ))
+ class superset(self.cls):
+ __public__ = frozenset((
+ 'some_method',
+ 'some_property',
+ 'another_property',
+ ))
+ class subset(self.cls):
+ __public__ = frozenset((
+ 'some_property',
+ ))
+ class any_object(object):
+ __public__ = frozenset((
+ 'some_method',
+ 'some_property',
+ ))
+
+ for ex in (example, example()):
+ # Test using str:
+ assert ex.implements('some_method')
+ assert not ex.implements('another_method')
+
+ # Test using frozenset:
+ assert ex.implements(frozenset(['some_method']))
+ assert not ex.implements(
+ frozenset(['some_method', 'another_method'])
+ )
+
+ # Test using another object/class with __public__ frozenset:
+ assert ex.implements(example)
+ assert ex.implements(example())
+
+ assert ex.implements(subset)
+ assert not subset.implements(ex)
+
+ assert not ex.implements(superset)
+ assert superset.implements(ex)
+
+ assert ex.implements(any_object)
+ assert ex.implements(any_object())
+
+ def test_implemented_by(self):
+ """
+ Test the `ipalib.plugable.Plugin.implemented_by` classmethod.
+ """
+ class base(self.cls):
+ __public__ = frozenset((
+ 'attr0',
+ 'attr1',
+ 'attr2',
+ ))
+
+ class okay(base):
+ def attr0(self):
+ pass
+ def __get_attr1(self):
+ assert False # Make sure property isn't accesed on instance
+ attr1 = property(__get_attr1)
+ attr2 = 'hello world'
+ another_attr = 'whatever'
+
+ class fail(base):
+ def __init__(self):
+ # Check that class, not instance is inspected:
+ self.attr2 = 'hello world'
+ def attr0(self):
+ pass
+ def __get_attr1(self):
+ assert False # Make sure property isn't accesed on instance
+ attr1 = property(__get_attr1)
+ another_attr = 'whatever'
+
+ # Test that AssertionError is raised trying to pass something not
+ # subclass nor instance of base:
+ raises(AssertionError, base.implemented_by, object)
+
+ # Test on subclass with needed attributes:
+ assert base.implemented_by(okay) is True
+ assert base.implemented_by(okay()) is True
+
+ # Test on subclass *without* needed attributes:
+ assert base.implemented_by(fail) is False
+ assert base.implemented_by(fail()) is False
+
+ def test_set_api(self):
+ """
+ Test the `ipalib.plugable.Plugin.set_api` method.
+ """
+ api = 'the api instance'
+ o = self.cls()
+ assert o.api is None
+ e = raises(AssertionError, o.set_api, None)
+ assert str(e) == 'set_api() argument cannot be None'
+ o.set_api(api)
+ assert o.api is api
+ e = raises(AssertionError, o.set_api, api)
+ assert str(e) == 'set_api() can only be called once'
+
+ def test_finalize(self):
+ """
+ Test the `ipalib.plugable.Plugin.finalize` method.
+ """
+ o = self.cls()
+ assert not o.__islocked__()
+ o.finalize()
+ assert o.__islocked__()
+
+ def test_call(self):
+ """
+ Test the `ipalib.plugable.Plugin.call` method.
+ """
+ o = self.cls()
+ o.call('/bin/true') is None
+ e = raises(errors2.SubprocessError, o.call, '/bin/false')
+ assert e.returncode == 1
+ assert e.argv == ('/bin/false',)
+
+
+class test_PluginProxy(ClassChecker):
+ """
+ Test the `ipalib.plugable.PluginProxy` class.
+ """
+ _cls = plugable.PluginProxy
+
+ def test_class(self):
+ """
+ Test the `ipalib.plugable.PluginProxy` class.
+ """
+ assert self.cls.__bases__ == (plugable.SetProxy,)
+
+ def test_proxy(self):
+ """
+ Test proxy behaviour of `ipalib.plugable.PluginProxy` instance.
+ """
+ # Setup:
+ class base(object):
+ __public__ = frozenset((
+ 'public_0',
+ 'public_1',
+ '__call__',
+ ))
+
+ def public_0(self):
+ return 'public_0'
+
+ def public_1(self):
+ return 'public_1'
+
+ def __call__(self, caller):
+ return 'ya called it, %s.' % caller
+
+ def private_0(self):
+ return 'private_0'
+
+ def private_1(self):
+ return 'private_1'
+
+ class plugin(base):
+ name = 'user_add'
+ attr_name = 'add'
+ doc = 'add a new user'
+
+ # Test that TypeError is raised when base is not a class:
+ raises(TypeError, self.cls, base(), None)
+
+ # Test that ValueError is raised when target is not instance of base:
+ raises(ValueError, self.cls, base, object())
+
+ # Test with correct arguments:
+ i = plugin()
+ p = self.cls(base, i)
+ assert read_only(p, 'name') is plugin.name
+ assert read_only(p, 'doc') == plugin.doc
+ assert list(p) == sorted(base.__public__)
+
+ # Test normal methods:
+ for n in xrange(2):
+ pub = 'public_%d' % n
+ priv = 'private_%d' % n
+ assert getattr(i, pub)() == pub
+ assert getattr(p, pub)() == pub
+ assert hasattr(p, pub)
+ assert getattr(i, priv)() == priv
+ assert not hasattr(p, priv)
+
+ # Test __call__:
+ value = 'ya called it, dude.'
+ assert i('dude') == value
+ assert p('dude') == value
+ assert callable(p)
+
+ # Test name_attr='name' kw arg
+ i = plugin()
+ p = self.cls(base, i, 'attr_name')
+ assert read_only(p, 'name') == 'add'
+
+ def test_implements(self):
+ """
+ Test the `ipalib.plugable.PluginProxy.implements` method.
+ """
+ class base(object):
+ __public__ = frozenset()
+ name = 'base'
+ doc = 'doc'
+ @classmethod
+ def implements(cls, arg):
+ return arg + 7
+
+ class sub(base):
+ @classmethod
+ def implements(cls, arg):
+ """
+ Defined to make sure base.implements() is called, not
+ target.implements()
+ """
+ return arg
+
+ o = sub()
+ p = self.cls(base, o)
+ assert p.implements(3) == 10
+
+ def test_clone(self):
+ """
+ Test the `ipalib.plugable.PluginProxy.__clone__` method.
+ """
+ class base(object):
+ __public__ = frozenset()
+ class sub(base):
+ name = 'some_name'
+ doc = 'doc'
+ label = 'another_name'
+
+ p = self.cls(base, sub())
+ assert read_only(p, 'name') == 'some_name'
+ c = p.__clone__('label')
+ assert isinstance(c, self.cls)
+ assert c is not p
+ assert read_only(c, 'name') == 'another_name'
+
+
+def test_Registrar():
+ """
+ Test the `ipalib.plugable.Registrar` class
+ """
+ class Base1(object):
+ pass
+ class Base2(object):
+ pass
+ class Base3(object):
+ pass
+ class plugin1(Base1):
+ pass
+ class plugin2(Base2):
+ pass
+ class plugin3(Base3):
+ pass
+
+ # Test creation of Registrar:
+ r = plugable.Registrar(Base1, Base2)
+
+ # Test __iter__:
+ assert list(r) == ['Base1', 'Base2']
+
+ # Test __hasitem__, __getitem__:
+ for base in [Base1, Base2]:
+ name = base.__name__
+ assert name in r
+ assert r[name] is base
+ magic = getattr(r, name)
+ assert type(magic) is plugable.MagicDict
+ assert len(magic) == 0
+
+ # Check that TypeError is raised trying to register something that isn't
+ # a class:
+ p = plugin1()
+ e = raises(TypeError, r, p)
+ assert str(e) == 'plugin must be a class; got %r' % p
+
+ # Check that SubclassError is raised trying to register a class that is
+ # not a subclass of an allowed base:
+ e = raises(errors2.PluginSubclassError, r, plugin3)
+ assert e.plugin is plugin3
+
+ # Check that registration works
+ r(plugin1)
+ assert len(r.Base1) == 1
+ assert r.Base1['plugin1'] is plugin1
+ assert r.Base1.plugin1 is plugin1
+
+ # Check that DuplicateError is raised trying to register exact class
+ # again:
+ e = raises(errors2.PluginDuplicateError, r, plugin1)
+ assert e.plugin is plugin1
+
+ # Check that OverrideError is raised trying to register class with same
+ # name and same base:
+ orig1 = plugin1
+ class base1_extended(Base1):
+ pass
+ class plugin1(base1_extended):
+ pass
+ e = raises(errors2.PluginOverrideError, r, plugin1)
+ assert e.base == 'Base1'
+ assert e.name == 'plugin1'
+ assert e.plugin is plugin1
+
+ # Check that overriding works
+ r(plugin1, override=True)
+ assert len(r.Base1) == 1
+ assert r.Base1.plugin1 is plugin1
+ assert r.Base1.plugin1 is not orig1
+
+ # Check that MissingOverrideError is raised trying to override a name
+ # not yet registerd:
+ e = raises(errors2.PluginMissingOverrideError, r, plugin2, override=True)
+ assert e.base == 'Base2'
+ assert e.name == 'plugin2'
+ assert e.plugin is plugin2
+
+ # Test that another plugin can be registered:
+ assert len(r.Base2) == 0
+ r(plugin2)
+ assert len(r.Base2) == 1
+ assert r.Base2.plugin2 is plugin2
+
+ # Setup to test more registration:
+ class plugin1a(Base1):
+ pass
+ r(plugin1a)
+
+ class plugin1b(Base1):
+ pass
+ r(plugin1b)
+
+ class plugin2a(Base2):
+ pass
+ r(plugin2a)
+
+ class plugin2b(Base2):
+ pass
+ r(plugin2b)
+
+ # Again test __hasitem__, __getitem__:
+ for base in [Base1, Base2]:
+ name = base.__name__
+ assert name in r
+ assert r[name] is base
+ magic = getattr(r, name)
+ assert len(magic) == 3
+ for key in magic:
+ klass = magic[key]
+ assert getattr(magic, key) is klass
+ assert issubclass(klass, base)
+
+
+class test_API(ClassChecker):
+ """
+ Test the `ipalib.plugable.API` class.
+ """
+
+ _cls = plugable.API
+
+ def test_API(self):
+ """
+ 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',
+ ))
+
+ def method(self, n):
+ return n
+
+ class base1(plugable.Plugin):
+ __public__ = frozenset((
+ 'method',
+ ))
+
+ def method(self, n):
+ return n + 1
+
+ api = plugable.API(base0, base1)
+ api.env.mode = 'unit_test'
+ api.env.in_tree = True
+ r = api.register
+ assert isinstance(r, plugable.Registrar)
+ assert read_only(api, 'register') is r
+
+ class base0_plugin0(base0):
+ pass
+ r(base0_plugin0)
+
+ class base0_plugin1(base0):
+ pass
+ r(base0_plugin1)
+
+ 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)
+ api.env.mode = 'unit_test'
+ 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) = create_test_api()
+ 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) = create_test_api()
+ 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/test_ipalib/test_request.py b/tests/test_ipalib/test_request.py
new file mode 100644
index 000000000..f26c270a7
--- /dev/null
+++ b/tests/test_ipalib/test_request.py
@@ -0,0 +1,161 @@
+# Authors:
+# Jason Gerard DeRose <jderose@redhat.com>
+#
+# Copyright (C) 2008 Red Hat
+# see file 'COPYING' for use and warranty contextrmation
+#
+# 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
+
+"""
+Test the `ipalib.request` module.
+"""
+
+import threading
+import locale
+from tests.util import raises, assert_equal
+from tests.util import TempDir, dummy_ugettext, dummy_ungettext
+from ipalib.constants import OVERRIDE_ERROR
+from ipalib import request
+
+
+def test_ugettext():
+ """
+ Test the `ipalib.request.ugettext` function.
+ """
+ f = request.ugettext
+ context = request.context
+ message = 'Hello, world!'
+
+ # Test with no context.ugettext:
+ assert not hasattr(context, 'ugettext')
+ assert_equal(f(message), u'Hello, world!')
+
+ # Test with dummy context.ugettext:
+ assert not hasattr(context, 'ugettext')
+ dummy = dummy_ugettext()
+ context.ugettext = dummy
+ assert f(message) is dummy.translation
+ assert dummy.message is message
+
+ # Cleanup
+ del context.ugettext
+ assert not hasattr(context, 'ugettext')
+
+
+def test_ungettext():
+ """
+ Test the `ipalib.request.ungettext` function.
+ """
+ f = request.ungettext
+ context = request.context
+ singular = 'Goose'
+ plural = 'Geese'
+
+ # Test with no context.ungettext:
+ assert not hasattr(context, 'ungettext')
+ assert_equal(f(singular, plural, 1), u'Goose')
+ assert_equal(f(singular, plural, 2), u'Geese')
+
+ # Test singular with dummy context.ungettext
+ assert not hasattr(context, 'ungettext')
+ dummy = dummy_ungettext()
+ context.ungettext = dummy
+ assert f(singular, plural, 1) is dummy.translation_singular
+ assert dummy.singular is singular
+ assert dummy.plural is plural
+ assert dummy.n == 1
+ del context.ungettext
+ assert not hasattr(context, 'ungettext')
+
+ # Test plural with dummy context.ungettext
+ assert not hasattr(context, 'ungettext')
+ dummy = dummy_ungettext()
+ context.ungettext = dummy
+ assert f(singular, plural, 2) is dummy.translation_plural
+ assert dummy.singular is singular
+ assert dummy.plural is plural
+ assert dummy.n == 2
+ del context.ungettext
+ assert not hasattr(context, 'ungettext')
+
+
+def test_set_languages():
+ """
+ Test the `ipalib.request.set_languages` function.
+ """
+ f = request.set_languages
+ c = request.context
+ langs = ('ru', 'en')
+
+ # Test that StandardError is raised if languages has already been set:
+ assert not hasattr(c, 'languages')
+ c.languages = None
+ e = raises(StandardError, f, *langs)
+ assert str(e) == OVERRIDE_ERROR % ('context', 'languages', None, langs)
+ del c.languages
+
+ # Test setting the languages:
+ assert not hasattr(c, 'languages')
+ f(*langs)
+ assert c.languages == langs
+ del c.languages
+
+ # Test setting language from locale.getdefaultlocale()
+ assert not hasattr(c, 'languages')
+ f()
+ assert c.languages == locale.getdefaultlocale()[:1]
+ del c.languages
+ assert not hasattr(c, 'languages')
+
+
+def test_create_translation():
+ """
+ Test the `ipalib.request.create_translation` function.
+ """
+ f = request.create_translation
+ c = request.context
+ t = TempDir()
+
+ # Test that StandardError is raised if ugettext or ungettext:
+ assert not (hasattr(c, 'ugettext') or hasattr(c, 'ungettext'))
+ for name in ('ugettext', 'ungettext'):
+ setattr(c, name, None)
+ e = raises(StandardError, f, 'ipa', None)
+ assert str(e) == (
+ 'create_translation() already called in thread %r' %
+ threading.currentThread().getName()
+ )
+ delattr(c, name)
+
+ # Test using default language:
+ assert not hasattr(c, 'ugettext')
+ assert not hasattr(c, 'ungettext')
+ assert not hasattr(c, 'languages')
+ f('ipa', t.path)
+ assert hasattr(c, 'ugettext')
+ assert hasattr(c, 'ungettext')
+ assert c.languages == locale.getdefaultlocale()[:1]
+ del c.ugettext
+ del c.ungettext
+ del c.languages
+
+ # Test using explicit languages:
+ langs = ('de', 'es')
+ f('ipa', t.path, *langs)
+ assert hasattr(c, 'ugettext')
+ assert hasattr(c, 'ungettext')
+ assert c.languages == langs
+ del c.ugettext
+ del c.ungettext
+ del c.languages
diff --git a/tests/test_ipalib/test_rpc.py b/tests/test_ipalib/test_rpc.py
new file mode 100644
index 000000000..296e9bc1c
--- /dev/null
+++ b/tests/test_ipalib/test_rpc.py
@@ -0,0 +1,249 @@
+# 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
+
+"""
+Test the `ipalib.rpc` module.
+"""
+
+import threading
+from xmlrpclib import Binary, Fault, dumps, loads
+from tests.util import raises, assert_equal, PluginTester, DummyClass
+from tests.data import binary_bytes, utf8_bytes, unicode_str
+from ipalib.frontend import Command
+from ipalib.request import context
+from ipalib import rpc, errors2
+
+
+std_compound = (binary_bytes, utf8_bytes, unicode_str)
+
+
+def dump_n_load(value):
+ (param, method) = loads(
+ dumps((value,), allow_none=True)
+ )
+ return param[0]
+
+
+def round_trip(value):
+ return rpc.xml_unwrap(
+ dump_n_load(rpc.xml_wrap(value))
+ )
+
+
+def test_round_trip():
+ """
+ Test `ipalib.rpc.xml_wrap` and `ipalib.rpc.xml_unwrap`.
+
+ This tests the two functions together with ``xmlrpclib.dumps()`` and
+ ``xmlrpclib.loads()`` in a full wrap/dumps/loads/unwrap round trip.
+ """
+ # We first test that our assumptions about xmlrpclib module in the Python
+ # standard library are correct:
+ assert_equal(dump_n_load(utf8_bytes), unicode_str)
+ assert_equal(dump_n_load(unicode_str), unicode_str)
+ assert_equal(dump_n_load(Binary(binary_bytes)).data, binary_bytes)
+ assert isinstance(dump_n_load(Binary(binary_bytes)), Binary)
+ assert type(dump_n_load('hello')) is str
+ assert type(dump_n_load(u'hello')) is str
+ assert_equal(dump_n_load(''), '')
+ assert_equal(dump_n_load(u''), '')
+ assert dump_n_load(None) is None
+
+ # Now we test our wrap and unwrap methods in combination with dumps, loads:
+ # All str should come back str (because they get wrapped in
+ # xmlrpclib.Binary(). All unicode should come back unicode because str
+ # explicity get decoded by rpc.xml_unwrap() if they weren't already
+ # decoded by xmlrpclib.loads().
+ assert_equal(round_trip(utf8_bytes), utf8_bytes)
+ assert_equal(round_trip(unicode_str), unicode_str)
+ assert_equal(round_trip(binary_bytes), binary_bytes)
+ assert type(round_trip('hello')) is str
+ assert type(round_trip(u'hello')) is unicode
+ assert_equal(round_trip(''), '')
+ assert_equal(round_trip(u''), u'')
+ assert round_trip(None) is None
+ compound = [utf8_bytes, None, binary_bytes, (None, unicode_str),
+ dict(utf8=utf8_bytes, chars=unicode_str, data=binary_bytes)
+ ]
+ assert round_trip(compound) == tuple(compound)
+
+
+def test_xml_wrap():
+ """
+ Test the `ipalib.rpc.xml_wrap` function.
+ """
+ f = rpc.xml_wrap
+ assert f([]) == tuple()
+ assert f({}) == dict()
+ b = f('hello')
+ assert isinstance(b, Binary)
+ assert b.data == 'hello'
+ u = f(u'hello')
+ assert type(u) is unicode
+ assert u == u'hello'
+ value = f([dict(one=False, two=u'hello'), None, 'hello'])
+
+
+def test_xml_unwrap():
+ """
+ Test the `ipalib.rpc.xml_unwrap` function.
+ """
+ f = rpc.xml_unwrap
+ assert f([]) == tuple()
+ assert f({}) == dict()
+ value = f(Binary(utf8_bytes))
+ assert type(value) is str
+ assert value == utf8_bytes
+ assert f(utf8_bytes) == unicode_str
+ assert f(unicode_str) == unicode_str
+ value = f([True, Binary('hello'), dict(one=1, two=utf8_bytes, three=None)])
+ assert value == (True, 'hello', dict(one=1, two=unicode_str, three=None))
+ assert type(value[1]) is str
+ assert type(value[2]['two']) is unicode
+
+
+def test_xml_dumps():
+ """
+ Test the `ipalib.rpc.xml_dumps` function.
+ """
+ f = rpc.xml_dumps
+ params = (binary_bytes, utf8_bytes, unicode_str, None)
+
+ # Test serializing an RPC request:
+ data = f(params, 'the_method')
+ (p, m) = loads(data)
+ assert_equal(m, u'the_method')
+ assert type(p) is tuple
+ assert rpc.xml_unwrap(p) == params
+
+ # Test serializing an RPC response:
+ data = f((params,), methodresponse=True)
+ (tup, m) = loads(data)
+ assert m is None
+ assert len(tup) == 1
+ assert type(tup) is tuple
+ assert rpc.xml_unwrap(tup[0]) == params
+
+ # Test serializing an RPC response containing a Fault:
+ fault = Fault(69, unicode_str)
+ data = f(fault, methodresponse=True)
+ e = raises(Fault, loads, data)
+ assert e.faultCode == 69
+ assert_equal(e.faultString, unicode_str)
+
+
+def test_xml_loads():
+ """
+ Test the `ipalib.rpc.xml_loads` function.
+ """
+ f = rpc.xml_loads
+ params = (binary_bytes, utf8_bytes, unicode_str, None)
+ wrapped = rpc.xml_wrap(params)
+
+ # Test un-serializing an RPC request:
+ data = dumps(wrapped, 'the_method', allow_none=True)
+ (p, m) = f(data)
+ assert_equal(m, u'the_method')
+ assert_equal(p, params)
+
+ # Test un-serializing an RPC response:
+ data = dumps((wrapped,), methodresponse=True, allow_none=True)
+ (tup, m) = f(data)
+ assert m is None
+ assert len(tup) == 1
+ assert type(tup) is tuple
+ assert_equal(tup[0], params)
+
+ # Test un-serializing an RPC response containing a Fault:
+ fault = Fault(69, unicode_str)
+ data = dumps(fault, methodresponse=True, allow_none=True)
+ e = raises(Fault, f, data)
+ assert e.faultCode == 69
+ assert_equal(e.faultString, unicode_str)
+
+
+class test_xmlclient(PluginTester):
+ """
+ Test the `ipalib.rpc.xmlclient` plugin.
+ """
+ _plugin = rpc.xmlclient
+
+ def test_forward(self):
+ """
+ Test the `ipalib.rpc.xmlclient.forward` method.
+ """
+ class user_add(Command):
+ pass
+
+ # Test that ValueError is raised when forwarding a command that is not
+ # in api.Command:
+ (o, api, home) = self.instance('Backend', in_server=False)
+ e = raises(ValueError, o.forward, 'user_add')
+ assert str(e) == '%s.forward(): %r not in api.Command' % (
+ 'xmlclient', 'user_add'
+ )
+
+ # Test that StandardError is raised when context.xmlconn does not exist:
+ (o, api, home) = self.instance('Backend', user_add, in_server=False)
+ e = raises(StandardError, o.forward, 'user_add')
+ assert str(e) == '%s.forward(%r): need context.xmlconn in thread %r' % (
+ 'xmlclient', 'user_add', threading.currentThread().getName()
+ )
+
+ args = (binary_bytes, utf8_bytes, unicode_str)
+ kw = dict(one=binary_bytes, two=utf8_bytes, three=unicode_str)
+ params = args + (kw,)
+ result = (unicode_str, binary_bytes, utf8_bytes)
+ context.xmlconn = DummyClass(
+ (
+ 'user_add',
+ (rpc.xml_wrap(params),),
+ {},
+ rpc.xml_wrap(result),
+ ),
+ (
+ 'user_add',
+ (rpc.xml_wrap(params),),
+ {},
+ Fault(3005, u"'four' is required"), # RequirementError
+ ),
+ (
+ 'user_add',
+ (rpc.xml_wrap(params),),
+ {},
+ Fault(700, u'no such error'), # There is no error 700
+ ),
+
+ )
+
+ # Test with a successful return value:
+ assert o.forward('user_add', *args, **kw) == result
+
+ # Test with an errno the client knows:
+ e = raises(errors2.RequirementError, o.forward, 'user_add', *args, **kw)
+ assert_equal(e.message, u"'four' is required")
+
+ # Test with an errno the client doesn't know
+ e = raises(errors2.UnknownError, o.forward, 'user_add', *args, **kw)
+ assert_equal(e.code, 700)
+ assert_equal(e.error, u'no such error')
+
+ assert context.xmlconn._calledall() is True
+
+ del context.xmlconn
diff --git a/tests/test_ipalib/test_util.py b/tests/test_ipalib/test_util.py
new file mode 100644
index 000000000..6729fcda5
--- /dev/null
+++ b/tests/test_ipalib/test_util.py
@@ -0,0 +1,61 @@
+# 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
+
+"""
+Test the `ipalib.util` module.
+"""
+
+from tests.util import raises
+from ipalib import util
+
+
+def test_xmlrpc_marshal():
+ """
+ Test the `ipalib.util.xmlrpc_marshal` function.
+ """
+ f = util.xmlrpc_marshal
+ assert f() == ({},)
+ assert f('one', 'two') == ({}, 'one', 'two')
+ assert f(one=1, two=2) == (dict(one=1, two=2),)
+ assert f('one', 'two', three=3, four=4) == \
+ (dict(three=3, four=4), 'one', 'two')
+
+
+def test_xmlrpc_unmarshal():
+ """
+ Test the `ipalib.util.xmlrpc_unmarshal` function.
+ """
+ f = util.xmlrpc_unmarshal
+ assert f() == (tuple(), {})
+ assert f({}, 'one', 'two') == (('one', 'two'), {})
+ assert f(dict(one=1, two=2)) == (tuple(), dict(one=1, two=2))
+ assert f(dict(three=3, four=4), 'one', 'two') == \
+ (('one', 'two'), dict(three=3, four=4))
+
+
+def test_make_repr():
+ """
+ Test the `ipalib.util.make_repr` function.
+ """
+ f = util.make_repr
+ assert f('my') == 'my()'
+ assert f('my', True, u'hello') == "my(True, u'hello')"
+ assert f('my', one=1, two='two') == "my(one=1, two='two')"
+ assert f('my', None, 3, dog='animal', apple='fruit') == \
+ "my(None, 3, apple='fruit', dog='animal')"
diff --git a/tests/test_ipaserver/__init__.py b/tests/test_ipaserver/__init__.py
new file mode 100644
index 000000000..56a6c533c
--- /dev/null
+++ b/tests/test_ipaserver/__init__.py
@@ -0,0 +1,22 @@
+# 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
+
+"""
+Sub-package containing unit tests for `ipaserver` package.
+"""
diff --git a/tests/test_ipaserver/test_rpcserver.py b/tests/test_ipaserver/test_rpcserver.py
new file mode 100644
index 000000000..48c1d36ef
--- /dev/null
+++ b/tests/test_ipaserver/test_rpcserver.py
@@ -0,0 +1,79 @@
+# 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
+
+"""
+Test the `ipaserver.rpc` module.
+"""
+
+from tests.util import create_test_api, raises, PluginTester
+from tests.data import unicode_str
+from ipalib import errors2, Command
+from ipaserver import rpcserver
+
+
+def test_params_2_args_options():
+ """
+ Test the `ipaserver.rpcserver.params_2_args_options` function.
+ """
+ f = rpcserver.params_2_args_options
+ args = ('Hello', u'world!')
+ options = dict(one=1, two=u'Two', three='Three')
+ assert f(tuple()) == (tuple(), dict())
+ assert f(args) == (args, dict())
+ assert f((options,)) == (tuple(), options)
+ assert f(args + (options,)) == (args, options)
+ assert f((options,) + args) == ((options,) + args, dict())
+
+
+class test_xmlserver(PluginTester):
+ """
+ Test the `ipaserver.rpcserver.xmlserver` plugin.
+ """
+
+ _plugin = rpcserver.xmlserver
+
+ def test_dispatch(self):
+ """
+ Test the `ipaserver.rpcserver.xmlserver.dispatch` method.
+ """
+ (o, api, home) = self.instance('Backend', in_server=True)
+ e = raises(errors2.CommandError, o.dispatch, 'echo', tuple())
+ assert e.name == 'echo'
+
+ class echo(Command):
+ takes_args = ['arg1', 'arg2+']
+ takes_options = ['option1?', 'option2?']
+ def execute(self, *args, **options):
+ assert type(args[1]) is tuple
+ return args + (options,)
+
+ (o, api, home) = self.instance('Backend', echo, in_server=True)
+ def call(params):
+ response = o.dispatch('echo', params)
+ assert type(response) is tuple and len(response) == 1
+ return response[0]
+ arg1 = unicode_str
+ arg2 = (u'Hello', unicode_str, u'world!')
+ options = dict(option1=u'How are you?', option2=unicode_str)
+ assert call((arg1, arg2, options)) == (arg1, arg2, options)
+ assert call((arg1,) + arg2 + (options,)) == (arg1, arg2, options)
+
+
+ def test_execute(self):
+ (o, api, home) = self.instance('Backend', in_server=True)
diff --git a/tests/test_ipawebui/__init__.py b/tests/test_ipawebui/__init__.py
new file mode 100644
index 000000000..f739a8563
--- /dev/null
+++ b/tests/test_ipawebui/__init__.py
@@ -0,0 +1,21 @@
+# 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
+
+"""
+Sub-package containing unit tests for `ipawebui` package.
+"""
diff --git a/tests/test_ipawebui/test_controllers.py b/tests/test_ipawebui/test_controllers.py
new file mode 100644
index 000000000..e236d1a0b
--- /dev/null
+++ b/tests/test_ipawebui/test_controllers.py
@@ -0,0 +1,70 @@
+# 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
+
+"""
+Test the `ipawebui.controller` module.
+"""
+
+from ipawebui import controller
+
+
+
+class test_Controller(object):
+ """
+ Test the `controller.Controller` class.
+ """
+
+ def test_init(self):
+ """
+ Test the `ipawebui.controller.Controller.__init__()` method.
+ """
+ o = controller.Controller()
+ assert o.template is None
+ template = 'The template.'
+ o = controller.Controller(template)
+ assert o.template is template
+
+ def test_output_xhtml(self):
+ """
+ Test the `ipawebui.controller.Controller.output_xhtml` method.
+ """
+ class Template(object):
+ def __init__(self):
+ self.calls = 0
+ self.kw = {}
+
+ def serialize(self, **kw):
+ self.calls += 1
+ self.kw = kw
+ return dict(kw)
+
+ d = dict(output='xhtml-strict', format='pretty')
+ t = Template()
+ o = controller.Controller(t)
+ assert o.output_xhtml() == d
+ assert t.calls == 1
+
+ def test_output_json(self):
+ """
+ Test the `ipawebui.controller.Controller.output_json` method.
+ """
+ o = controller.Controller()
+ assert o.output_json() == '{}'
+ e = '{\n "age": 27, \n "first": "John", \n "last": "Doe"\n}'
+ j = o.output_json(last='Doe', first='John', age=27)
+ assert j == e
diff --git a/tests/test_util.py b/tests/test_util.py
new file mode 100644
index 000000000..7d7038c1a
--- /dev/null
+++ b/tests/test_util.py
@@ -0,0 +1,148 @@
+# 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
+
+"""
+Test the `tests.util` module.
+"""
+
+import util
+
+
+class Prop(object):
+ def __init__(self, *ops):
+ self.__ops = frozenset(ops)
+ self.__prop = 'prop value'
+
+ def __get_prop(self):
+ if 'get' not in self.__ops:
+ raise AttributeError('get prop')
+ return self.__prop
+
+ def __set_prop(self, value):
+ if 'set' not in self.__ops:
+ raise AttributeError('set prop')
+ self.__prop = value
+
+ def __del_prop(self):
+ if 'del' not in self.__ops:
+ raise AttributeError('del prop')
+ self.__prop = None
+
+ prop = property(__get_prop, __set_prop, __del_prop)
+
+
+def test_yes_raised():
+ f = util.raises
+
+ class SomeError(Exception):
+ pass
+
+ class AnotherError(Exception):
+ pass
+
+ def callback1():
+ 'raises correct exception'
+ raise SomeError()
+
+ def callback2():
+ 'raises wrong exception'
+ raise AnotherError()
+
+ def callback3():
+ 'raises no exception'
+
+ f(SomeError, callback1)
+
+ raised = False
+ try:
+ f(SomeError, callback2)
+ except AnotherError:
+ raised = True
+ assert raised
+
+ raised = False
+ try:
+ f(SomeError, callback3)
+ except util.ExceptionNotRaised:
+ raised = True
+ assert raised
+
+
+def test_no_set():
+ # Tests that it works when prop cannot be set:
+ util.no_set(Prop('get', 'del'), 'prop')
+
+ # Tests that ExceptionNotRaised is raised when prop *can* be set:
+ raised = False
+ try:
+ util.no_set(Prop('set'), 'prop')
+ except util.ExceptionNotRaised:
+ raised = True
+ assert raised
+
+
+def test_no_del():
+ # Tests that it works when prop cannot be deleted:
+ util.no_del(Prop('get', 'set'), 'prop')
+
+ # Tests that ExceptionNotRaised is raised when prop *can* be set:
+ raised = False
+ try:
+ util.no_del(Prop('del'), 'prop')
+ except util.ExceptionNotRaised:
+ raised = True
+ assert raised
+
+
+def test_read_only():
+ # Test that it works when prop is read only:
+ assert util.read_only(Prop('get'), 'prop') == 'prop value'
+
+ # Test that ExceptionNotRaised is raised when prop can be set:
+ raised = False
+ try:
+ util.read_only(Prop('get', 'set'), 'prop')
+ except util.ExceptionNotRaised:
+ raised = True
+ assert raised
+
+ # Test that ExceptionNotRaised is raised when prop can be deleted:
+ raised = False
+ try:
+ util.read_only(Prop('get', 'del'), 'prop')
+ except util.ExceptionNotRaised:
+ raised = True
+ assert raised
+
+ # Test that ExceptionNotRaised is raised when prop can be both set and
+ # deleted:
+ raised = False
+ try:
+ util.read_only(Prop('get', 'del'), 'prop')
+ except util.ExceptionNotRaised:
+ raised = True
+ assert raised
+
+ # Test that AttributeError is raised when prop can't be read:
+ raised = False
+ try:
+ util.read_only(Prop(), 'prop')
+ except AttributeError:
+ raised = True
+ assert raised
diff --git a/tests/test_xmlrpc/__init__.py b/tests/test_xmlrpc/__init__.py
new file mode 100644
index 000000000..043007b5e
--- /dev/null
+++ b/tests/test_xmlrpc/__init__.py
@@ -0,0 +1,22 @@
+# 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
+
+"""
+Sub-package containing unit tests for `xmlrpc` package.
+"""
diff --git a/tests/test_xmlrpc/test_automount_plugin.py b/tests/test_xmlrpc/test_automount_plugin.py
new file mode 100644
index 000000000..522ee689a
--- /dev/null
+++ b/tests/test_xmlrpc/test_automount_plugin.py
@@ -0,0 +1,243 @@
+# Authors:
+# Rob Crittenden <rcritten@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
+
+"""
+Test the `ipalib/plugins/f_automount' module.
+"""
+
+import sys
+from xmlrpc_test import XMLRPC_test
+from ipalib import api
+from ipalib import errors
+from ipalib.cli import CLI
+
+try:
+ api.finalize()
+except StandardError:
+ pass
+
+class test_Service(XMLRPC_test):
+ """
+ Test the `f_automount` plugin.
+ """
+ mapname='testmap'
+ keyname='testkey'
+ keyname2='secondkey'
+ description='description of map'
+ info='ro'
+ map_kw={'automountmapname': mapname, 'description': description}
+ key_kw={'automountmapname': mapname, 'automountkey': keyname, 'automountinformation': info}
+ key_kw2={'automountmapname': mapname, 'automountkey': keyname2, 'automountinformation': info}
+
+ def test_add_1map(self):
+ """
+ Test adding a map `xmlrpc.automount_addmap` method.
+ """
+ res = api.Command['automount_addmap'](**self.map_kw)
+ assert res
+ assert res.get('automountmapname','') == self.mapname
+
+ def test_add_2key(self):
+ """
+ Test adding a key using `xmlrpc.automount_addkey` method.
+ """
+ res = api.Command['automount_addkey'](**self.key_kw2)
+ assert res
+ assert res.get('automountkey','') == self.keyname2
+
+ def test_add_3key(self):
+ """
+ Test adding a key using `xmlrpc.automount_addkey` method.
+ """
+ res = api.Command['automount_addkey'](**self.key_kw)
+ assert res
+ assert res.get('automountkey','') == self.keyname
+
+ def test_add_4key(self):
+ """
+ Test adding a duplicate key using `xmlrpc.automount_addkey` method.
+ """
+ try:
+ res = api.Command['automount_addkey'](**self.key_kw)
+ except errors.DuplicateEntry:
+ pass
+ else:
+ assert False
+
+ def test_doshowmap(self):
+ """
+ Test the `xmlrpc.automount_showmap` method.
+ """
+ res = api.Command['automount_showmap'](self.mapname)
+ assert res
+ assert res.get('automountmapname','') == self.mapname
+
+ def test_findmap(self):
+ """
+ Test the `xmlrpc.automount_findmap` method.
+ """
+ res = api.Command['automount_findmap'](self.mapname)
+ assert res
+ assert len(res) == 2
+ assert res[1].get('automountmapname','') == self.mapname
+
+ def test_doshowkey(self):
+ """
+ Test the `xmlrpc.automount_showkey` method.
+ """
+ showkey_kw={'automountmapname': self.mapname, 'automountkey': self.keyname}
+ res = api.Command['automount_showkey'](**showkey_kw)
+ assert res
+ assert res.get('automountkey','') == self.keyname
+ assert res.get('automountinformation','') == self.info
+
+ def test_findkey(self):
+ """
+ Test the `xmlrpc.automount_findkey` method.
+ """
+ res = api.Command['automount_findkey'](self.keyname)
+ assert res
+ assert len(res) == 2
+ assert res[1].get('automountkey','') == self.keyname
+ assert res[1].get('automountinformation','') == self.info
+
+ def test_modkey(self):
+ """
+ Test the `xmlrpc.automount_modkey` method.
+ """
+ self.key_kw['automountinformation'] = 'rw'
+ self.key_kw['description'] = 'new description'
+ res = api.Command['automount_modkey'](**self.key_kw)
+ assert res
+ assert res.get('automountkey','') == self.keyname
+ assert res.get('automountinformation','') == 'rw'
+ assert res.get('description','') == 'new description'
+
+ def test_modmap(self):
+ """
+ Test the `xmlrpc.automount_modmap` method.
+ """
+ self.map_kw['description'] = 'new description'
+ res = api.Command['automount_modmap'](**self.map_kw)
+ assert res
+ assert res.get('automountmapname','') == self.mapname
+ assert res.get('description','') == 'new description'
+
+ def test_remove1key(self):
+ """
+ Test the `xmlrpc.automount_delkey` method.
+ """
+ delkey_kw={'automountmapname': self.mapname, 'automountkey': self.keyname}
+ res = api.Command['automount_delkey'](**delkey_kw)
+ assert res == True
+
+ # Verify that it is gone
+ try:
+ res = api.Command['automount_showkey'](**delkey_kw)
+ except errors.NotFound:
+ pass
+ else:
+ assert False
+
+ def test_remove2map(self):
+ """
+ Test the `xmlrpc.automount_delmap` method.
+ """
+ res = api.Command['automount_delmap'](self.mapname)
+ assert res == True
+
+ # Verify that it is gone
+ try:
+ res = api.Command['automount_showmap'](self.mapname)
+ except errors.NotFound:
+ pass
+ else:
+ assert False
+
+ def test_remove3map(self):
+ """
+ Test that the `xmlrpc.automount_delmap` method removes all keys
+ """
+ # Verify that the second key we added is gone
+ key_kw={'automountmapname': self.mapname, 'automountkey': self.keyname2}
+ try:
+ res = api.Command['automount_showkey'](**key_kw)
+ except errors.NotFound:
+ pass
+ else:
+ assert False
+
+class test_Indirect(XMLRPC_test):
+ """
+ Test the `f_automount` plugin Indirect map function.
+ """
+ mapname='auto.home'
+ keyname='/home'
+ parentmap='auto.master'
+ description='Home directories'
+ map_kw={'automountkey': keyname, 'parentmap': parentmap, 'description': description}
+
+ def test_add_indirect(self):
+ """
+ Test adding an indirect map.
+ """
+ res = api.Command['automount_addindirectmap'](self.mapname, **self.map_kw)
+ assert res
+ assert res.get('automountinformation','') == self.mapname
+
+ def test_doshowkey(self):
+ """
+ Test the `xmlrpc.automount_showkey` method.
+ """
+ showkey_kw={'automountmapname': self.parentmap, 'automountkey': self.keyname}
+ res = api.Command['automount_showkey'](**showkey_kw)
+ assert res
+ assert res.get('automountkey','') == self.keyname
+
+ def test_remove_key(self):
+ """
+ Remove the indirect key /home
+ """
+ delkey_kw={'automountmapname': self.parentmap, 'automountkey': self.keyname}
+ res = api.Command['automount_delkey'](**delkey_kw)
+ assert res == True
+
+ # Verify that it is gone
+ try:
+ res = api.Command['automount_showkey'](**delkey_kw)
+ except errors.NotFound:
+ pass
+ else:
+ assert False
+
+ def test_remove_map(self):
+ """
+ Remove the indirect map for auto.home
+ """
+ res = api.Command['automount_delmap'](self.mapname)
+ assert res == True
+
+ # Verify that it is gone
+ try:
+ res = api.Command['automount_showmap'](self.mapname)
+ except errors.NotFound:
+ pass
+ else:
+ assert False
+
diff --git a/tests/test_xmlrpc/test_group_plugin.py b/tests/test_xmlrpc/test_group_plugin.py
new file mode 100644
index 000000000..2b16cc8a5
--- /dev/null
+++ b/tests/test_xmlrpc/test_group_plugin.py
@@ -0,0 +1,178 @@
+# Authors:
+# Rob Crittenden <rcritten@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
+
+"""
+Test the `ipalib/plugins/f_group` module.
+"""
+
+import sys
+from xmlrpc_test import XMLRPC_test
+from ipalib import api
+from ipalib import errors
+from ipalib.cli import CLI
+
+try:
+ api.finalize()
+except StandardError:
+ pass
+
+class test_Group(XMLRPC_test):
+ """
+ Test the `f_group` plugin.
+ """
+ cn='testgroup'
+ cn2='testgroup2'
+ description='This is a test'
+ kw={'description':description,'cn':cn}
+
+ def test_add(self):
+ """
+ Test the `xmlrpc.group_add` method.
+ """
+ res = api.Command['group_add'](**self.kw)
+ assert res
+ assert res.get('description','') == self.description
+ assert res.get('cn','') == self.cn
+
+ def test_add2(self):
+ """
+ Test the `xmlrpc.group_add` method duplicate detection.
+ """
+ try:
+ res = api.Command['group_add'](**self.kw)
+ except errors.DuplicateEntry:
+ pass
+
+ def test_add2(self):
+ """
+ Test the `xmlrpc.group_add` method.
+ """
+ self.kw['cn'] = self.cn2
+ res = api.Command['group_add'](**self.kw)
+ assert res
+ assert res.get('description','') == self.description
+ assert res.get('cn','') == self.cn2
+
+ def test_add_member(self):
+ """
+ Test the `xmlrpc.group_add_member` method.
+ """
+ kw={}
+ kw['groups'] = self.cn2
+ res = api.Command['group_add_member'](self.cn, **kw)
+ assert res == []
+
+ def test_add_member2(self):
+ """
+ Test the `xmlrpc.group_add_member` with a non-existent member
+ """
+ kw={}
+ kw['groups'] = "notfound"
+ res = api.Command['group_add_member'](self.cn, **kw)
+ # an error isn't thrown, the list of failed members is returned
+ assert res != []
+
+ def test_doshow(self):
+ """
+ Test the `xmlrpc.group_show` method.
+ """
+ res = api.Command['group_show'](self.cn)
+ assert res
+ assert res.get('description','') == self.description
+ assert res.get('cn','') == self.cn
+ assert res.get('member','').startswith('cn=%s' % self.cn2)
+
+ def test_find(self):
+ """
+ Test the `xmlrpc.group_find` method.
+ """
+ res = api.Command['group_find'](self.cn)
+ assert res
+ assert len(res) == 3
+ assert res[1].get('description','') == self.description
+ assert res[1].get('cn','') == self.cn
+
+ def test_mod(self):
+ """
+ Test the `xmlrpc.group_mod` method.
+ """
+ modkw = self.kw
+ modkw['cn'] = self.cn
+ modkw['description'] = 'New description'
+ res = api.Command['group_mod'](**modkw)
+ assert res
+ assert res.get('description','') == 'New description'
+
+ # Ok, double-check that it was changed
+ res = api.Command['group_show'](self.cn)
+ assert res
+ assert res.get('description','') == 'New description'
+ assert res.get('cn','') == self.cn
+
+ def test_remove_member(self):
+ """
+ Test the `xmlrpc.group_remove_member` method.
+ """
+ kw={}
+ kw['groups'] = self.cn2
+ res = api.Command['group_remove_member'](self.cn, **kw)
+
+ res = api.Command['group_show'](self.cn)
+ assert res
+ assert res.get('member','') == ''
+
+ def test_remove_member2(self):
+ """
+ Test the `xmlrpc.group_remove_member` method with non-member
+ """
+ kw={}
+ kw['groups'] = "notfound"
+ # an error isn't thrown, the list of failed members is returned
+ res = api.Command['group_remove_member'](self.cn, **kw)
+ assert res != []
+
+ def test_remove_x(self):
+ """
+ Test the `xmlrpc.group_del` method.
+ """
+ res = api.Command['group_del'](self.cn)
+ assert res == True
+
+ # Verify that it is gone
+ try:
+ res = api.Command['group_show'](self.cn)
+ except errors.NotFound:
+ pass
+ else:
+ assert False
+
+ def test_remove_x2(self):
+ """
+ Test the `xmlrpc.group_del` method.
+ """
+ res = api.Command['group_del'](self.cn2)
+ assert res == True
+
+ # Verify that it is gone
+ try:
+ res = api.Command['group_show'](self.cn2)
+ except errors.NotFound:
+ pass
+ else:
+ assert False
diff --git a/tests/test_xmlrpc/test_host_plugin.py b/tests/test_xmlrpc/test_host_plugin.py
new file mode 100644
index 000000000..515cd703d
--- /dev/null
+++ b/tests/test_xmlrpc/test_host_plugin.py
@@ -0,0 +1,128 @@
+# Authors:
+# Rob Crittenden <rcritten@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
+
+"""
+Test the `ipalib/plugins/f_host` module.
+"""
+
+import sys
+from xmlrpc_test import XMLRPC_test
+from ipalib import api
+from ipalib import errors
+from ipalib.cli import CLI
+
+try:
+ api.finalize()
+except StandardError:
+ pass
+
+class test_Host(XMLRPC_test):
+ """
+ Test the `f_host` plugin.
+ """
+ cn='ipaexample.%s' % api.env.domain
+ description='Test host'
+ localityname='Undisclosed location'
+ kw={'cn': cn, 'description': description, 'localityname': localityname}
+
+ def test_add(self):
+ """
+ Test the `xmlrpc.host_add` method.
+ """
+ res = api.Command['host_add'](**self.kw)
+ assert res
+ assert res.get('description','') == self.description
+ assert res.get('cn','') == self.cn
+ assert res.get('l','') == self.localityname
+
+ def test_doshow_all(self):
+ """
+ Test the `xmlrpc.host_show` method with all attributes.
+ """
+ kw={'cn':self.cn, 'all': True}
+ res = api.Command['host_show'](**kw)
+ assert res
+ assert res.get('description','') == self.description
+ assert res.get('cn','') == self.cn
+ assert res.get('l','') == self.localityname
+
+ def test_doshow_minimal(self):
+ """
+ Test the `xmlrpc.host_show` method with default attributes.
+ """
+ kw={'cn':self.cn}
+ res = api.Command['host_show'](**kw)
+ assert res
+ assert res.get('description','') == self.description
+ assert res.get('cn','') == self.cn
+ assert res.get('localityname','') == self.localityname
+
+ def test_find_all(self):
+ """
+ Test the `xmlrpc.host_find` method with all attributes.
+ """
+ kw={'cn':self.cn, 'all': True}
+ res = api.Command['host_find'](**kw)
+ assert res
+ assert len(res) == 2
+ assert res[1].get('description','') == self.description
+ assert res[1].get('cn','') == self.cn
+ assert res[1].get('l','') == self.localityname
+
+ def test_find_minimal(self):
+ """
+ Test the `xmlrpc.host_find` method with default attributes.
+ """
+ res = api.Command['host_find'](self.cn)
+ assert res
+ assert len(res) == 2
+ assert res[1].get('description','') == self.description
+ assert res[1].get('cn','') == self.cn
+ assert res[1].get('localityname','') == self.localityname
+
+ def test_mod(self):
+ """
+ Test the `xmlrpc.host_mod` method.
+ """
+ newdesc='Updated host'
+ modkw={'cn': self.cn, 'description': newdesc}
+ res = api.Command['host_mod'](**modkw)
+ assert res
+ assert res.get('description','') == newdesc
+
+ # Ok, double-check that it was changed
+ res = api.Command['host_show'](self.cn)
+ assert res
+ assert res.get('description','') == newdesc
+ assert res.get('cn','') == self.cn
+
+ def test_remove(self):
+ """
+ Test the `xmlrpc.host_del` method.
+ """
+ res = api.Command['host_del'](self.cn)
+ assert res == True
+
+ # Verify that it is gone
+ try:
+ res = api.Command['host_show'](self.cn)
+ except errors.NotFound:
+ pass
+ else:
+ assert False
diff --git a/tests/test_xmlrpc/test_hostgroup_plugin.py b/tests/test_xmlrpc/test_hostgroup_plugin.py
new file mode 100644
index 000000000..9180c1dd1
--- /dev/null
+++ b/tests/test_xmlrpc/test_hostgroup_plugin.py
@@ -0,0 +1,149 @@
+# Authors:
+# Rob Crittenden <rcritten@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
+
+"""
+Test the `ipalib/plugins/f_hostgroup` module.
+"""
+
+import sys
+from xmlrpc_test import XMLRPC_test
+from ipalib import api
+from ipalib import errors
+from ipalib.cli import CLI
+
+try:
+ api.finalize()
+except StandardError:
+ pass
+
+class test_Host(XMLRPC_test):
+ """
+ Test the `f_hostgroup` plugin.
+ """
+ cn='testgroup'
+ description='Test host group'
+ kw={'cn': cn, 'description': description}
+
+ host_cn='ipaexample.%s' % api.env.domain
+ host_description='Test host'
+ host_localityname='Undisclosed location'
+
+ def test_add(self):
+ """
+ Test the `xmlrpc.hostgroup_add` method.
+ """
+ res = api.Command['hostgroup_add'](**self.kw)
+ assert res
+ assert res.get('description','') == self.description
+ assert res.get('cn','') == self.cn
+
+ def test_addhost(self):
+ """
+ Add a host to test add/remove member.
+ """
+ kw={'cn': self.host_cn, 'description': self.host_description, 'localityname': self.host_localityname}
+ res = api.Command['host_add'](**kw)
+ assert res
+ assert res.get('description','') == self.host_description
+ assert res.get('cn','') == self.host_cn
+
+ def test_addmember(self):
+ """
+ Test the `xmlrpc.hostgroup_add_member` method.
+ """
+ kw={}
+ kw['hosts'] = self.host_cn
+ res = api.Command['hostgroup_add_member'](self.cn, **kw)
+ assert res == []
+
+ def test_doshow(self):
+ """
+ Test the `xmlrpc.hostgroup_show` method.
+ """
+ res = api.Command['hostgroup_show'](self.cn)
+ assert res
+ assert res.get('description','') == self.description
+ assert res.get('cn','') == self.cn
+ assert res.get('member','').startswith('cn=%s' % self.host_cn)
+
+ def test_find(self):
+ """
+ Test the `xmlrpc.hostgroup_find` method.
+ """
+ res = api.Command['hostgroup_find'](self.cn)
+ assert res
+ assert len(res) == 2
+ assert res[1].get('description','') == self.description
+ assert res[1].get('cn','') == self.cn
+ assert res[1].get('member','').startswith('cn=%s' % self.host_cn)
+
+ def test_mod(self):
+ """
+ Test the `xmlrpc.hostgroup_mod` method.
+ """
+ newdesc='Updated host group'
+ modkw={'cn': self.cn, 'description': newdesc}
+ res = api.Command['hostgroup_mod'](**modkw)
+ assert res
+ assert res.get('description','') == newdesc
+
+ # Ok, double-check that it was changed
+ res = api.Command['hostgroup_show'](self.cn)
+ assert res
+ assert res.get('description','') == newdesc
+ assert res.get('cn','') == self.cn
+
+ def test_member_remove(self):
+ """
+ Test the `xmlrpc.hostgroup_remove_member` method.
+ """
+ kw={}
+ kw['hosts'] = self.host_cn
+ res = api.Command['hostgroup_remove_member'](self.cn, **kw)
+ assert res == []
+
+ def test_remove(self):
+ """
+ Test the `xmlrpc.hostgroup_del` method.
+ """
+ res = api.Command['hostgroup_del'](self.cn)
+ assert res == True
+
+ # Verify that it is gone
+ try:
+ res = api.Command['hostgroup_show'](self.cn)
+ except errors.NotFound:
+ pass
+ else:
+ assert False
+
+ def test_removehost(self):
+ """
+ Test the `xmlrpc.host_del` method.
+ """
+ res = api.Command['host_del'](self.host_cn)
+ assert res == True
+
+ # Verify that it is gone
+ try:
+ res = api.Command['host_show'](self.host_cn)
+ except errors.NotFound:
+ pass
+ else:
+ assert False
diff --git a/tests/test_xmlrpc/test_netgroup_plugin.py b/tests/test_xmlrpc/test_netgroup_plugin.py
new file mode 100644
index 000000000..3d3e4afff
--- /dev/null
+++ b/tests/test_xmlrpc/test_netgroup_plugin.py
@@ -0,0 +1,320 @@
+# Authors:
+# Rob Crittenden <rcritten@redhat.com>
+#
+# Copyright (C) 2009 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
+
+"""
+Test the `ipalib/plugins/f_netgroup` module.
+"""
+
+import sys
+from xmlrpc_test import XMLRPC_test
+from ipalib import api
+from ipalib import errors
+from ipalib.cli import CLI
+
+try:
+ api.finalize()
+except StandardError:
+ pass
+
+def is_member_of(members, candidate):
+ if not isinstance(members, list):
+ members = [members]
+ for m in members:
+ if m.startswith(candidate):
+ return True
+ return False
+
+class test_Netgroup(XMLRPC_test):
+ """
+ Test the `f_netgroup` plugin.
+ """
+ ng_cn='ng1'
+ ng_description='Netgroup'
+ ng_kw={'cn': ng_cn, 'description': ng_description}
+
+ host_cn='ipaexample.%s' % api.env.domain
+ host_description='Test host'
+ host_localityname='Undisclosed location'
+ host_kw={'cn': host_cn, 'description': host_description, 'localityname': host_localityname}
+
+ hg_cn='ng1'
+ hg_description='Netgroup'
+ hg_kw={'cn': hg_cn, 'description': hg_description}
+
+ user_uid='jexample'
+ user_givenname='Jim'
+ user_sn='Example'
+ user_home='/home/%s' % user_uid
+ user_kw={'givenname':user_givenname,'sn':user_sn,'uid':user_uid,'homedirectory':user_home}
+
+ group_cn='testgroup'
+ group_description='This is a test'
+ group_kw={'description':group_description,'cn':group_cn}
+
+ def test_add(self):
+ """
+ Test the `xmlrpc.netgroup_add` method.
+ """
+ res = api.Command['netgroup_add'](**self.ng_kw)
+ assert res
+ assert res.get('description','') == self.ng_description
+ assert res.get('cn','') == self.ng_cn
+
+ def test_adddata(self):
+ """
+ Add the data needed to do additional testing.
+ """
+
+ # Add a host
+ res = api.Command['host_add'](**self.host_kw)
+ assert res
+ assert res.get('description','') == self.host_description
+ assert res.get('cn','') == self.host_cn
+
+ # Add a hostgroup
+ res = api.Command['hostgroup_add'](**self.hg_kw)
+ assert res
+ assert res.get('description','') == self.hg_description
+ assert res.get('cn','') == self.hg_cn
+
+ # Add a user
+ res = api.Command['user_add'](**self.user_kw)
+ assert res
+ assert res.get('givenname','') == self.user_givenname
+ assert res.get('uid','') == self.user_uid
+
+ # Add a group
+ res = api.Command['group_add'](**self.group_kw)
+ assert res
+ assert res.get('description','') == self.group_description
+ assert res.get('cn','') == self.group_cn
+
+ def test_addmembers(self):
+ """
+ Test the `xmlrpc.netgroup_add_member` method.
+ """
+ kw={}
+ kw['hosts'] = self.host_cn
+ res = api.Command['netgroup_add_member'](self.ng_cn, **kw)
+ assert res == []
+
+ kw={}
+ kw['hostgroups'] = self.hg_cn
+ res = api.Command['netgroup_add_member'](self.ng_cn, **kw)
+ assert res == []
+
+ kw={}
+ kw['users'] = self.user_uid
+ res = api.Command['netgroup_add_member'](self.ng_cn, **kw)
+ assert res == []
+
+ kw={}
+ kw['groups'] = self.group_cn
+ res = api.Command['netgroup_add_member'](self.ng_cn, **kw)
+ assert res == []
+
+ def test_addmembers2(self):
+ """
+ Test the `xmlrpc.netgroup_add_member` method again to test dupes.
+ """
+ kw={}
+ kw['hosts'] = self.host_cn
+ res = api.Command['netgroup_add_member'](self.ng_cn, **kw)
+ assert is_member_of(res, 'cn=%s' % self.host_cn)
+
+ kw={}
+ kw['hostgroups'] = self.hg_cn
+ res = api.Command['netgroup_add_member'](self.ng_cn, **kw)
+ assert is_member_of(res, 'cn=%s' % self.hg_cn)
+
+ kw={}
+ kw['users'] = self.user_uid
+ res = api.Command['netgroup_add_member'](self.ng_cn, **kw)
+ assert is_member_of(res, 'uid=%s' % self.user_uid)
+
+ kw={}
+ kw['groups'] = self.group_cn
+ res = api.Command['netgroup_add_member'](self.ng_cn, **kw)
+ assert is_member_of(res, 'cn=%s' % self.group_cn)
+
+ def test_addexternalmembers(self):
+ """
+ Test adding external hosts
+ """
+ kw={}
+ kw['hosts'] = "nosuchhost"
+ res = api.Command['netgroup_add_member'](self.ng_cn, **kw)
+ assert res == []
+ res = api.Command['netgroup_show'](self.ng_cn)
+ assert res
+ assert is_member_of(res.get('externalhost',[]), kw['hosts'])
+
+ def test_doshow(self):
+ """
+ Test the `xmlrpc.netgroup_show` method.
+ """
+ res = api.Command['netgroup_show'](self.ng_cn)
+ assert res
+ assert res.get('description','') == self.ng_description
+ assert res.get('cn','') == self.ng_cn
+ assert is_member_of(res.get('memberhost',[]), 'cn=%s' % self.host_cn)
+ assert is_member_of(res.get('memberhost',[]), 'cn=%s' % self.hg_cn)
+ assert is_member_of(res.get('memberuser',[]), 'uid=%s' % self.user_uid)
+ assert is_member_of(res.get('memberuser',[]), 'cn=%s' % self.group_cn)
+
+ def test_find(self):
+ """
+ Test the `xmlrpc.hostgroup_find` method.
+ """
+ res = api.Command['netgroup_find'](self.ng_cn)
+ assert res
+ assert len(res) == 2
+ assert res[1].get('description','') == self.ng_description
+ assert res[1].get('cn','') == self.ng_cn
+
+ def test_mod(self):
+ """
+ Test the `xmlrpc.hostgroup_mod` method.
+ """
+ newdesc='Updated host group'
+ modkw={'cn': self.ng_cn, 'description': newdesc}
+ res = api.Command['netgroup_mod'](**modkw)
+ assert res
+ assert res.get('description','') == newdesc
+
+ # Ok, double-check that it was changed
+ res = api.Command['netgroup_show'](self.ng_cn)
+ assert res
+ assert res.get('description','') == newdesc
+ assert res.get('cn','') == self.ng_cn
+
+ def test_member_remove(self):
+ """
+ Test the `xmlrpc.hostgroup_remove_member` method.
+ """
+ kw={}
+ kw['hosts'] = self.host_cn
+ res = api.Command['netgroup_remove_member'](self.ng_cn, **kw)
+ assert res == []
+
+ kw={}
+ kw['hostgroups'] = self.hg_cn
+ res = api.Command['netgroup_remove_member'](self.ng_cn, **kw)
+ assert res == []
+
+ kw={}
+ kw['users'] = self.user_uid
+ res = api.Command['netgroup_remove_member'](self.ng_cn, **kw)
+ assert res == []
+
+ kw={}
+ kw['groups'] = self.group_cn
+ res = api.Command['netgroup_remove_member'](self.ng_cn, **kw)
+ assert res == []
+
+ def test_member_remove2(self):
+ """
+ Test the `xmlrpc.netgroup_remove_member` method again to test not found.
+ """
+ kw={}
+ kw['hosts'] = self.host_cn
+ res = api.Command['netgroup_remove_member'](self.ng_cn, **kw)
+ assert is_member_of(res, 'cn=%s' % self.host_cn)
+
+ kw={}
+ kw['hostgroups'] = self.hg_cn
+ res = api.Command['netgroup_remove_member'](self.ng_cn, **kw)
+ assert is_member_of(res, 'cn=%s' % self.hg_cn)
+
+ kw={}
+ kw['users'] = self.user_uid
+ res = api.Command['netgroup_remove_member'](self.ng_cn, **kw)
+ assert is_member_of(res, 'uid=%s' % self.user_uid)
+
+ kw={}
+ kw['groups'] = self.group_cn
+ res = api.Command['netgroup_remove_member'](self.ng_cn, **kw)
+ assert is_member_of(res, 'cn=%s' % self.group_cn)
+
+ def test_remove(self):
+ """
+ Test the `xmlrpc.netgroup_del` method.
+ """
+ res = api.Command['netgroup_del'](self.ng_cn)
+ assert res == True
+
+ # Verify that it is gone
+ try:
+ res = api.Command['netgroup_show'](self.ng_cn)
+ except errors.NotFound:
+ pass
+ else:
+ assert False
+
+ def test_removedata(self):
+ """
+ Remove the test data we added
+ """
+ # Remove the host
+ res = api.Command['host_del'](self.host_cn)
+ assert res == True
+
+ # Verify that it is gone
+ try:
+ res = api.Command['host_show'](self.host_cn)
+ except errors.NotFound:
+ pass
+ else:
+ assert False
+
+ # Remove the hostgroup
+ res = api.Command['hostgroup_del'](self.hg_cn)
+ assert res == True
+
+ # Verify that it is gone
+ try:
+ res = api.Command['hostgroup_show'](self.hg_cn)
+ except errors.NotFound:
+ pass
+ else:
+ assert False
+
+ # Remove the user
+ res = api.Command['user_del'](self.user_uid)
+ assert res == True
+
+ # Verify that it is gone
+ try:
+ res = api.Command['user_show'](self.user_uid)
+ except errors.NotFound:
+ pass
+ else:
+ assert False
+
+ # Remove the group
+ res = api.Command['group_del'](self.group_cn)
+ assert res == True
+
+ # Verify that it is gone
+ try:
+ res = api.Command['group_show'](self.group_cn)
+ except errors.NotFound:
+ pass
+ else:
+ assert False
diff --git a/tests/test_xmlrpc/test_service_plugin.py b/tests/test_xmlrpc/test_service_plugin.py
new file mode 100644
index 000000000..0a843d36e
--- /dev/null
+++ b/tests/test_xmlrpc/test_service_plugin.py
@@ -0,0 +1,93 @@
+# Authors:
+# Rob Crittenden <rcritten@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
+
+"""
+Test the `ipalib/plugins/f_service` module.
+"""
+
+import sys
+from xmlrpc_test import XMLRPC_test
+from ipalib import api
+from ipalib import errors
+from ipalib.cli import CLI
+
+try:
+ api.finalize()
+except StandardError:
+ pass
+
+class test_Service(XMLRPC_test):
+ """
+ Test the `f_service` plugin.
+ """
+ principal='HTTP/ipatest.%s@%s' % (api.env.domain, api.env.realm)
+ hostprincipal='host/ipatest.%s@%s' % (api.env.domain, api.env.realm)
+ kw={'principal':principal}
+
+ def test_add(self):
+ """
+ Test adding a HTTP principal using the `xmlrpc.service_add` method.
+ """
+ res = api.Command['service_add'](**self.kw)
+ assert res
+ assert res.get('krbprincipalname','') == self.principal
+
+ def test_add_host(self):
+ """
+ Test adding a host principal using `xmlrpc.service_add` method.
+ """
+ kw={'principal':self.hostprincipal}
+ try:
+ res = api.Command['service_add'](**kw)
+ except errors.HostService:
+ pass
+ else:
+ assert False
+
+ def test_doshow(self):
+ """
+ Test the `xmlrpc.service_show` method.
+ """
+ res = api.Command['service_show'](self.principal)
+ assert res
+ assert res.get('krbprincipalname','') == self.principal
+
+ def test_find(self):
+ """
+ Test the `xmlrpc.service_find` method.
+ """
+ res = api.Command['service_find'](self.principal)
+ assert res
+ assert len(res) == 2
+ assert res[1].get('krbprincipalname','') == self.principal
+
+ def test_remove(self):
+ """
+ Test the `xmlrpc.service_del` method.
+ """
+ res = api.Command['service_del'](self.principal)
+ assert res == True
+
+ # Verify that it is gone
+ try:
+ res = api.Command['service_show'](self.principal)
+ except errors.NotFound:
+ pass
+ else:
+ assert False
diff --git a/tests/test_xmlrpc/test_user_plugin.py b/tests/test_xmlrpc/test_user_plugin.py
new file mode 100644
index 000000000..0189aa5ac
--- /dev/null
+++ b/tests/test_xmlrpc/test_user_plugin.py
@@ -0,0 +1,151 @@
+# Authors:
+# Rob Crittenden <rcritten@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
+
+"""
+Test the `ipalib/plugins/f_user` module.
+"""
+
+import sys
+from xmlrpc_test import XMLRPC_test
+from ipalib import api
+from ipalib import errors
+from ipalib.cli import CLI
+
+try:
+ api.finalize()
+except StandardError:
+ pass
+
+class test_User(XMLRPC_test):
+ """
+ Test the `f_user` plugin.
+ """
+ uid='jexample'
+ givenname='Jim'
+ sn='Example'
+ home='/home/%s' % uid
+ principalname='%s@%s' % (uid, api.env.realm)
+ kw={'givenname':givenname,'sn':sn,'uid':uid,'homedirectory':home}
+
+ def test_add(self):
+ """
+ Test the `xmlrpc.user_add` method.
+ """
+ res = api.Command['user_add'](**self.kw)
+ assert res
+ assert res.get('givenname','') == self.givenname
+ assert res.get('sn','') == self.sn
+ assert res.get('uid','') == self.uid
+ assert res.get('homedirectory','') == self.home
+
+ def test_add2(self):
+ """
+ Test the `xmlrpc.user_add` method duplicate detection.
+ """
+ try:
+ res = api.Command['user_add'](**self.kw)
+ except errors.DuplicateEntry:
+ pass
+
+ def test_doshow(self):
+ """
+ Test the `xmlrpc.user_show` method.
+ """
+ kw={'uid':self.uid, 'all': True}
+ res = api.Command['user_show'](**kw)
+ assert res
+ assert res.get('givenname','') == self.givenname
+ assert res.get('sn','') == self.sn
+ assert res.get('uid','') == self.uid
+ assert res.get('homedirectory','') == self.home
+ assert res.get('krbprincipalname','') == self.principalname
+
+ def test_find_all(self):
+ """
+ Test the `xmlrpc.user_find` method with all attributes.
+ """
+ kw={'uid':self.uid, 'all': True}
+ res = api.Command['user_find'](**kw)
+ assert res
+ assert len(res) == 2
+ assert res[1].get('givenname','') == self.givenname
+ assert res[1].get('sn','') == self.sn
+ assert res[1].get('uid','') == self.uid
+ assert res[1].get('homedirectory','') == self.home
+ assert res[1].get('krbprincipalname','') == self.principalname
+
+ def test_find_minimal(self):
+ """
+ Test the `xmlrpc.user_find` method with minimal attributes.
+ """
+ res = api.Command['user_find'](self.uid)
+ assert res
+ assert len(res) == 2
+ assert res[1].get('givenname','') == self.givenname
+ assert res[1].get('sn','') == self.sn
+ assert res[1].get('uid','') == self.uid
+ assert res[1].get('homedirectory','') == self.home
+ assert res[1].get('krbprincipalname', None) == None
+
+ def test_lock(self):
+ """
+ Test the `xmlrpc.user_lock` method.
+ """
+ res = api.Command['user_lock'](self.uid)
+ assert res == True
+
+ def test_lockoff(self):
+ """
+ Test the `xmlrpc.user_unlock` method.
+ """
+ res = api.Command['user_unlock'](self.uid)
+ assert res == True
+
+ def test_mod(self):
+ """
+ Test the `xmlrpc.user_mod` method.
+ """
+ modkw = self.kw
+ modkw['givenname'] = 'Finkle'
+ res = api.Command['user_mod'](**modkw)
+ assert res
+ assert res.get('givenname','') == 'Finkle'
+ assert res.get('sn','') == self.sn
+
+ # Ok, double-check that it was changed
+ res = api.Command['user_show'](self.uid)
+ assert res
+ assert res.get('givenname','') == 'Finkle'
+ assert res.get('sn','') == self.sn
+ assert res.get('uid','') == self.uid
+
+ def test_remove(self):
+ """
+ Test the `xmlrpc.user_del` method.
+ """
+ res = api.Command['user_del'](self.uid)
+ assert res == True
+
+ # Verify that it is gone
+ try:
+ res = api.Command['user_show'](self.uid)
+ except errors.NotFound:
+ pass
+ else:
+ assert False
diff --git a/tests/test_xmlrpc/xmlrpc_test.py b/tests/test_xmlrpc/xmlrpc_test.py
new file mode 100644
index 000000000..744c0c277
--- /dev/null
+++ b/tests/test_xmlrpc/xmlrpc_test.py
@@ -0,0 +1,49 @@
+# Authors:
+# Rob Crittenden <rcritten@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
+
+"""
+Base class for all XML-RPC tests
+"""
+
+import sys
+import socket
+import nose
+from ipalib import api
+from ipalib import errors
+from ipalib.cli import CLI
+
+try:
+ api.finalize()
+except StandardError:
+ pass
+
+class XMLRPC_test:
+ """
+ Base class for all XML-RPC plugin tests
+ """
+
+ def setUp(self):
+ # FIXME: changing Plugin.name from a property to an instance attribute
+ # somehow broke this.
+ try:
+ res = api.Command['user_show']('notfound')
+ except socket.error:
+ raise nose.SkipTest
+ except errors.NotFound:
+ pass
diff --git a/tests/util.py b/tests/util.py
new file mode 100644
index 000000000..f5899dfab
--- /dev/null
+++ b/tests/util.py
@@ -0,0 +1,391 @@
+# 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
+
+"""
+Common utility functions and classes for unit tests.
+"""
+
+import inspect
+import os
+from os import path
+import tempfile
+import shutil
+import ipalib
+from ipalib.plugable import Plugin
+from ipalib.request import context
+
+
+
+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
+
+
+class ExceptionNotRaised(Exception):
+ """
+ Exception raised when an *expected* exception is *not* raised during a
+ unit test.
+ """
+ msg = 'expected %s'
+
+ def __init__(self, expected):
+ self.expected = expected
+
+ def __str__(self):
+ return self.msg % self.expected.__name__
+
+
+def assert_equal(val1, val2):
+ """
+ Assert ``val1`` and ``val2`` are the same type and of equal value.
+ """
+ assert type(val1) is type(val2), '%r != %r' % (val1, val2)
+ assert val1 == val2, '%r != %r' % (val1, val2)
+
+
+def raises(exception, callback, *args, **kw):
+ """
+ Tests that the expected exception is raised; raises ExceptionNotRaised
+ if test fails.
+ """
+ raised = False
+ try:
+ callback(*args, **kw)
+ except exception, e:
+ raised = True
+ if not raised:
+ raise ExceptionNotRaised(exception)
+ return e
+
+
+def getitem(obj, key):
+ """
+ Works like getattr but for dictionary interface. Use this in combination
+ with raises() to test that, for example, KeyError is raised.
+ """
+ return obj[key]
+
+
+def setitem(obj, key, value):
+ """
+ Works like setattr but for dictionary interface. Use this in combination
+ with raises() to test that, for example, TypeError is raised.
+ """
+ obj[key] = value
+
+
+def delitem(obj, key):
+ """
+ Works like delattr but for dictionary interface. Use this in combination
+ with raises() to test that, for example, TypeError is raised.
+ """
+ del obj[key]
+
+
+def no_set(obj, name, value='some_new_obj'):
+ """
+ Tests that attribute cannot be set.
+ """
+ raises(AttributeError, setattr, obj, name, value)
+
+
+def no_del(obj, name):
+ """
+ Tests that attribute cannot be deleted.
+ """
+ raises(AttributeError, delattr, obj, name)
+
+
+def read_only(obj, name, value='some_new_obj'):
+ """
+ Tests that attribute is read-only. Returns attribute.
+ """
+ # Test that it cannot be set:
+ no_set(obj, name, value)
+
+ # Test that it cannot be deleted:
+ no_del(obj, name)
+
+ # Return the attribute
+ return getattr(obj, name)
+
+
+def is_prop(prop):
+ return type(prop) is property
+
+
+class ClassChecker(object):
+ __cls = None
+ __subcls = None
+
+ def __get_cls(self):
+ if self.__cls is None:
+ self.__cls = self._cls
+ assert inspect.isclass(self.__cls)
+ return self.__cls
+ cls = property(__get_cls)
+
+ def __get_subcls(self):
+ if self.__subcls is None:
+ self.__subcls = self.get_subcls()
+ assert inspect.isclass(self.__subcls)
+ return self.__subcls
+ subcls = property(__get_subcls)
+
+ def get_subcls(self):
+ raise NotImplementedError(
+ self.__class__.__name__,
+ 'get_subcls()'
+ )
+
+ def tearDown(self):
+ """
+ nose tear-down fixture.
+ """
+ for name in ('ugettext', 'ungettext'):
+ if hasattr(context, name):
+ delattr(context, name)
+
+
+
+
+
+
+
+def check_TypeError(value, type_, name, callback, *args, **kw):
+ """
+ Tests a standard TypeError raised with `errors.raise_TypeError`.
+ """
+ e = raises(TypeError, callback, *args, **kw)
+ assert e.value is value
+ assert e.type is type_
+ assert e.name == name
+ assert type(e.name) is str
+ assert str(e) == ipalib.errors.TYPE_FORMAT % (name, type_, value)
+ return e
+
+
+def get_api(**kw):
+ """
+ Returns (api, home) tuple.
+
+ This function returns a tuple containing an `ipalib.plugable.API`
+ instance and a `TempHome` instance.
+ """
+ home = TempHome()
+ api = ipalib.create_api(mode='unit_test')
+ api.env.in_tree = True
+ for (key, value) in kw.iteritems():
+ api.env[key] = value
+ return (api, home)
+
+
+def create_test_api(**kw):
+ """
+ Returns (api, home) tuple.
+
+ This function returns a tuple containing an `ipalib.plugable.API`
+ instance and a `TempHome` instance.
+ """
+ home = TempHome()
+ api = ipalib.create_api(mode='unit_test')
+ api.env.in_tree = True
+ for (key, value) in kw.iteritems():
+ api.env[key] = value
+ return (api, home)
+
+
+class PluginTester(object):
+ __plugin = None
+
+ def __get_plugin(self):
+ if self.__plugin is None:
+ self.__plugin = self._plugin
+ assert issubclass(self.__plugin, Plugin)
+ return self.__plugin
+ plugin = property(__get_plugin)
+
+ def register(self, *plugins, **kw):
+ """
+ Create a testing api and register ``self.plugin``.
+
+ This method returns an (api, home) tuple.
+
+ :param plugins: Additional \*plugins to register.
+ :param kw: Additional \**kw args to pass to `create_test_api`.
+ """
+ (api, home) = create_test_api(**kw)
+ api.register(self.plugin)
+ for p in plugins:
+ api.register(p)
+ return (api, home)
+
+ def finalize(self, *plugins, **kw):
+ (api, home) = self.register(*plugins, **kw)
+ api.finalize()
+ return (api, home)
+
+ def instance(self, namespace, *plugins, **kw):
+ (api, home) = self.finalize(*plugins, **kw)
+ o = api[namespace][self.plugin.__name__]
+ return (o, api, home)
+
+
+class dummy_ugettext(object):
+ __called = False
+
+ def __init__(self, translation=None):
+ if translation is None:
+ translation = u'The translation'
+ self.translation = translation
+ assert type(self.translation) is unicode
+
+ def __call__(self, message):
+ assert self.__called is False
+ self.__called = True
+ assert type(message) is str
+ assert not hasattr(self, 'message')
+ self.message = message
+ assert type(self.translation) is unicode
+ return self.translation
+
+ def called(self):
+ return self.__called
+
+ def reset(self):
+ assert type(self.translation) is unicode
+ assert type(self.message) is str
+ del self.message
+ assert self.__called is True
+ self.__called = False
+
+
+class dummy_ungettext(object):
+ __called = False
+
+ def __init__(self):
+ self.translation_singular = u'The singular translation'
+ self.translation_plural = u'The plural translation'
+
+ def __call__(self, singular, plural, n):
+ assert type(singular) is str
+ assert type(plural) is str
+ assert type(n) is int
+ assert self.__called is False
+ self.__called = True
+ self.singular = singular
+ self.plural = plural
+ self.n = n
+ if n == 1:
+ return self.translation_singular
+ return self.translation_plural
+
+
+class DummyMethod(object):
+ def __init__(self, callback, name):
+ self.__callback = callback
+ self.__name = name
+
+ def __call__(self, *args, **kw):
+ return self.__callback(self.__name, args, kw)
+
+
+class DummyClass(object):
+ def __init__(self, *calls):
+ self.__calls = calls
+ self.__i = 0
+ for (name, args, kw, result) in calls:
+ method = DummyMethod(self.__process, name)
+ setattr(self, name, method)
+
+ def __process(self, name_, args_, kw_):
+ if self.__i >= len(self.__calls):
+ raise AssertionError(
+ 'extra call: %s, %r, %r' % (name, args, kw)
+ )
+ (name, args, kw, result) = self.__calls[self.__i]
+ self.__i += 1
+ i = self.__i
+ if name_ != name:
+ raise AssertionError(
+ 'call %d should be to method %r; got %r' % (i, name, name_)
+ )
+ if args_ != args:
+ raise AssertionError(
+ 'call %d to %r should have args %r; got %r' % (i, name, args, args_)
+ )
+ if kw_ != kw:
+ raise AssertionError(
+ 'call %d to %r should have kw %r, got %r' % (i, name, kw, kw_)
+ )
+ if isinstance(result, Exception):
+ raise result
+ return result
+
+ def _calledall(self):
+ return self.__i == len(self.__calls)