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