diff options
author | Rob Crittenden <rcritten@redhat.com> | 2009-01-22 10:42:41 -0500 |
---|---|---|
committer | Rob Crittenden <rcritten@redhat.com> | 2009-01-22 10:42:41 -0500 |
commit | c2967a675a288e7d31374229fd974d0cb9966f2c (patch) | |
tree | 58be8ca6319f4660d9f18b97a37b9c0c56104d02 /ipalib/__init__.py | |
parent | 2b8b87b4d6c3b4389a0a7bf48c225035c53e7ad1 (diff) | |
parent | 5d82e3b35a8fb2d4c25f282cddad557a7650197c (diff) | |
download | freeipa.git-c2967a675a288e7d31374229fd974d0cb9966f2c.tar.gz freeipa.git-c2967a675a288e7d31374229fd974d0cb9966f2c.tar.xz freeipa.git-c2967a675a288e7d31374229fd974d0cb9966f2c.zip |
Merge branch 'master' of git://fedorapeople.org/~jderose/freeipa2
Diffstat (limited to 'ipalib/__init__.py')
-rw-r--r-- | ipalib/__init__.py | 915 |
1 files changed, 915 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) |