From a8409b7db15d28b3b938e4a0e93dd1c3e3a6a960 Mon Sep 17 00:00:00 2001 From: Pavel Zuna Date: Tue, 20 Apr 2010 18:32:11 +0200 Subject: Add file with example plugins/tutorial. Note that this is still work in progress and will be finished in another patch. Specifically, it currently doesn't cover baseldap.py classes. --- doc/examples/examples.py | 437 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 437 insertions(+) create mode 100644 doc/examples/examples.py (limited to 'doc') diff --git a/doc/examples/examples.py b/doc/examples/examples.py new file mode 100644 index 000000000..d40ad8e3c --- /dev/null +++ b/doc/examples/examples.py @@ -0,0 +1,437 @@ +# Authors: +# Pavel Zuna +# +# Copyright (C) 2010 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 +""" +Example plugins +""" + +# Hey guys, so you're interested in writing plugins for IPA? Great! +# We compiled this small file with examples on how to extend IPA to suit +# your needs. We'll be going from very simple to pretty complex plugins +# hopefully covering most of what our framework has to offer. + +# First, let's import some stuff. + +# api is an object containing references to all plugins and useful classes. +# errors is a module containing all IPA specific exceptions. +from ipalib import api, errors +# Command is the base class for command plugin. +from ipalib import Command +# Str is a subclass of Param, it is used to define string parameters for +# command. We'll go through all other subclasses of Param supported by IPA +# later in this file +from ipalib import Str +# output is a module containing the most common output patterns. +# Command plugin do output validation based on these patterns. +# You can define your own as we're going to show you later. +from ipalib import output + + +# We're going to create an example command plugin, that takes a name as its +# only argument. Commands in IPA support input validation by defining +# functions we're going to call 'validators'. This is an example of such +# function: +def validate_name(ugettext, name): + """ + Validate names for the exhelloworld command. Names starting with 'Y' + (picked at random) are considered invalid. + """ + if name.startswith('Y'): + raise errors.ValidationError( + name='name', + error='Names starting with \'Y\' are invalid!' + ) + # If the validator doesn't return anything (i.e. it returns None), + # the parameter passes validation. + + +class exhelloworld(Command): + """ + Example commnad: Hello world! + """ + # takes_args is an attribute of Command. It's a tuple containing + # instances of Param (or its subclasses such as Str) that define + # what position arguments are accepted by the command. + takes_args = ( + # The first argument of Param constructor is the name that will be + # used to identify this parameter. It can be followed by validator + # functions. The constructor can also take a bunch of keyword + # arguments. Here we use default, to set the parameters default value + # and autofill, that fills the default value if the parameter isn't + # present. + # Note the ? at the end of the parameter name. It makes the parameter + # optional. + Str('name?', validate_name, + default=u'anonymous coward', + autofill=True, + ), + ) + + # has_output is an attribute of Command, it is a tuple containing + # output.Output instances that define its output pattern. + # Commands in IPA return dicts with keys corresponding to items + # in the has_output tuple. + has_output = ( + # output.summary is one of the basic patterns. + # It's a string that should be filled with a user-friendly + # decription of the action performed by the command. + output.summary, + ) + + # Every command needs to override the execute method. + # This is where the command functionality should go. + # It is always executed on the server-side, so don't rely + # on client-side stuff in here! + def execute(self, name): + return dict(summary='Hello world, %s!' % name) + +# register the command, uncomment this line if you want to try it out +#api.register(exhelloworld) + +# Anyway, that was a pretty bad example of a command or, to be more precise, +# a bad example of resource use. When a client executes a command locally, its +# name and parameters are transfered to the server over XML-RPC. The command +# execute method is then executed on the server and results are transfered +# back to the client. The command does nothing, but create a string - a task +# that could be easily done locally. This can be done by overriding the Command +# forward method. It has the same signature as execute and is normally +# responsible for transferring stuff to the server. +# Most commands will, however, need to perfom tasks on the server. I didn't +# want to start with forward and confuse the hell out of you. :) + + +# Okey, time to look at something a little more advance. A command that +# actually communicates with the LDAP backend. + +# Let's import a new parameter type: Flag. +# Parameters of type Flag do not have values per say. They are either enabled +# or disabled (True or False), so there's no need to make then optional, ever. +from ipalib import Flag + +class exshowuser(Command): + """ + Example command: retrieve an user entry from LDAP + """ + takes_args = ( + Str('username'), + ) + + # takes_options is another attribute of Command. It works the same + # way as takes_args, but instead of positional arguments, it enables + # us to define what options the commmand takes. + # Note that an options can be both required and optional. + takes_options = ( + Flag('all', + # the doc keyword argument is what you see when you go + # `ipa COMMAND --help` or `ipa help COMMAND` + doc='retrieve all attributes', + flags=['no_output'], + ), + ) + + has_output = ( + # Here, you can see a custom output pattern. The pattern constructor + # takes the output name (key in the dictionary returned by execute), + # the allowed type(s) (can be a tuple with several types), a + # simple description and a list of flags. Currently, only + # the 'no_display' flag is supported by the Command.output_for_cli + # method, but you can always use your own if you plan + # to override it - I'll show you how later. + output.Output('result', dict, 'user entry whithout DN'), + output.Output('dn', unicode, 'DN of the user entry', ['no_display']), + ) + + # Notice the ** argument notation for options. It is not required, but + # we strongly recommend you to use it. In some cases, special options + # are added automatically to commands and not listing them or using ** + # may lead to exception flying around... and nobody likes exceptions + # flying around. + def execute(self, username, **options): + # OK, I said earlier that this command is going to communicate + # with the LDAP backend, You could always use python-ldap to do + # that, but there's also this nice class we have... it's called + # ldap2 and this is how you get a handle to it: + ldap = self.api.Backend.ldap2 + + # ldap2 enables you to do a lot of crazy stuff with LDAP and it's + # specially crafted to suit IPA plugin needs. I recommend you either + # look at ipaserver/plugins/ldap2 or checkout some of the generated + # HTML docs on www.freeipa.org as I won't be able to cover everything + # it offers in this file. + + # We want to retrieve an user entry from LDAP. We need to know its + # DN first. There's a bunch of method in ldap2 to build DNs. For our + # purpose, this will do: + dn = ldap.make_dn_from_attr( + 'uid', username, self.api.env.container_user + ) + # Note that api.env contains a lot of useful constant. We recommend + # you to check them out and use them whenever possible. + + # Let's check if the --all option is enabled. If it is, let's + # retrieve all of the entry attributes. If not, only retrieve some + # basic stuff like the username, first and last names. + if options.get('all', False): + attrs_list = ['*'] + else: + attrs_list = ['uid', 'givenname', 'sn'] + + # Give us the entry, LDAP! + (dn, entry_attrs) = ldap.get_entry(dn, attrs_list) + + return dict(result=entry_attrs, dn=dn) + +# register the command, uncomment this line if you want to try it out +#api.register(exshowuser) + + +# Now let's a take a look on how you can modify the command output if you don't +# like the default. + +class exshowuser2(exshowuser): + """ + Example command: exusershow with custom output + """ + # Just some values we're going to use for textui.print_entry + attr_order = ['uid', 'givenname', 'sn'] + attr_labels = { + 'uid': 'User login', 'givenname': 'First name', 'sn': 'Last name' + } + + def output_for_cli(self, textui, output, *args, **options): + # Now we've done it! We have overridden the default output_for_cli. + # textui is a class that implements a lot of useful outputting methods, + # please use it when you can + # output contains the dict returned by execute + # args, options contain the command parameters + textui.print_dashed('User entry:') + textui.print_indented('DN: %s' % output['dn']) + textui.print_entry(output['result'], self.attr_order, self.attr_labels) + +# register the command, uncomment this line if you want to try it out +#api.register(exshowuser2) + +# Alright, so now you'll always want to define your own output_for_cli... +# No, you won't! Because the default output_for_cli isn't as stupid as it looks. +# It can take information from the command parameters and output patterns +# to produce nice output like all real IPA commands have. + +class exshowuser3(exshowuser): + """ + Example command: exusershow that takes full advantage of the default output + """ + takes_args = ( + # We're going to rename the username argument to uid to match + # the attribute name it represent. The cli_name kwarg is what + # users will see in the CLI and label is what the default + # output_for_cli is going to use when printing the attribute value. + Str('uid', + cli_name='username', + label='User login', + ), + ) + + # has_output_params works the same way as takes_args and takes_options, + # but is only used to define output attributes. These won't show up + # as parameters for the command. + has_output_params = ( + Str('givenname', + label='First name', + ), + Str('sn', + label='Last name', + ), + ) + + # standard_entry includes an entry 'result' (dict), a summary 'summary' + # and the entry primary key 'value' + # It also makes the command automatically add two special options: + # --all and --raw. Look at the description of nearly any real IPA command + # to see what they're about. + has_output = output.standard_entry + + # Since --all and --raw are added automatically thanks to standard_entry, + # we need to clear takes_options from the base class otherwise we would + # get a parameter conflict. + takes_options = tuple() + + def execute(self, *args, **options): + # Let's just call execute of the base class, extract it's output + # and fit it into the standard_entry output pattern. + output = super(exshowuser3, self).execute(*args, **options) + output['result']['dn'] = output['dn'] + return dict(result=output['result'], value=args[0]) + +# register the command, uncomment this line if you want to try it out +#api.register(exshowuser3) + + +# Pretty cool, right? But you will probably want to implement a set of commands +# to manage a certain type of entries (like users in the above examples). +# To save you the massive PITA of parameter copy&paste, we introduced +# the Object and Method plugin classes. Let's see how they work. + +from ipalib import Object, Method + +# First, we're going to create an object that represent the user entry. +class exuser(Object): + """ + Example plugin: user object + """ + # takes_params is an attribute of Object. It is used to define output + # parameters for associated Methods. Methods can also use them to + # to generate their own parameters as you'll see in a while. + takes_params = ( + Str('uid', + cli_name='username', + label='User login', + # The primary_key kwarg is used to, well, specify the object's + # primary key. + primary_key=True, + ), + Str('givenname?', + cli_name='first', + label='First name', + ), + Str('sn?', + cli_name='last', + label='Last name', + ), + ) + +# register the object, uncomment this line if you want to try it out +#api.register(exuser) + +# Next, we're going to create a set of methods to manage this type of object +# i.e. to manage user entries. We're only going to do "read" commands, because +# we don't want to damage your user entries - adding, deleting, modifying is a +# bit more complicated and will be covered later in this file. + +# Methods are automatically associtad with a parent Object based on class +# names. They can then access their parent Object using self.obj. +# Simply said, Methods are just Commands associated with an Object. + +class exuser_show(Method): + has_output = output.standard_entry + + # get_args is a method of Command used to generate positional arguments + # we're going to use it to extract parameters from the parent + # Object + def get_args(self): + # self.obj.primary_key contains a reference the parameter with + # primary_key kwarg set to True. + # Parameters can be cloned to create new instance with additional + # kwargs. Here we add the attribute kwargs, that tells the framework + # the parameters corresponds to an LDAP attribute. The query kwargs + # tells the framework to skip parameter validation (i.e. do NOT call + # validators). + yield self.obj.primary_key.clone(attribute=True, query=True) + + def execute(self, *args, **options): + ldap = self.api.Backend.ldap2 + + dn = ldap.make_dn_from_attr( + 'uid', args[0], self.api.env.container_user + ) + + if options.get('all', False): + attrs_list = ['*'] + else: + attrs_list = [p.name for p in self.output_params()] + + (dn, entry_attrs) = ldap.get_entry(dn, attrs_list) + entry_attrs['dn'] = dn + + return dict(result=entry_attrs, value=args[0]) + +# register the command, uncomment this line if you want to try it out +#api.register(exuser_show) + +class exuser_find(Method): + # standard_list_of_entries is an output pattern that + # define a dict with a list of entries, their count + # and a truncated flag. The truncated flag is used to mark + # truncated (uncomplete) search results - for example due to + # timeouts. + has_output = output.standard_list_of_entries + + # get_options is similar to get_args, but is used to generate + # options instead of positional arguments + def get_options(self): + for option in self.obj.params(): + yield option.clone( + attribute=True, query=True, required=False + ) + + def execute(self, *args, **options): + ldap = self.api.Backend.ldap2 + + # args_options_2_entry is a helper method of Command used + # to create a dictionary from the command parameters that + # have the attribute kwargs set to True. + search_kw = self.args_options_2_entry(*args, **options) + + # make_filter will create an LDAP filter from attribute values + # exact=False means the values are surrounded with * when constructing + # the filter and rules=ldap.MATCH_ALL means the filter is going + # to use the & operators. More complex filters can be constructed + # by joining simpler filters using ldap2.combine_filters. + attr_filter = ldap.make_filter( + search_kw, exact=False, rules=ldap.MATCH_ALL + ) + + if options.get('all', False): + attrs_list = ['*'] + else: + attrs_list = [p.name for p in self.output_params()] + + # perform the search + (entries, truncated) = ldap.find_entries( + attr_filter, attrs_list, self.api.env.container_user, + scope=ldap.SCOPE_ONELEVEL + ) + + # find_entries returns DNs and attributes separately, but the output + # patter expects them in one dict. We need to arrange that. + for e in entries: + e[1]['dn'] = e[0] + entries = [e for (dn, e) in entries] + + return dict(result=entries, count=len(entries), truncated=truncated) + +# register the command, uncomment this line if you want to try it out +#api.register(exuser_find) + +# As most commands associated with objects are used to manage entries in LDAP, +# we defined a basic set of base classes for your plugins implementing CRUD +# operations. This is maily to save you from defining your own has_output, +# get_args, get_options and to have a standardized way of doing things for the +# sake of consistency. We won't cover them here, because you probably won't +# need to use them. So why did we botter? Well, you're going to see in +# a while. If interested anyway, check them out in ipalib/crud.py. + + +# At this point, if you've already seen some of the real plugins, you might +# be going like "WTH is this !@#^&? The user_show plugin is only like 4 lines +# of code and does much more than the exshowuser crap. Well yes, that's because +# it is based on one of the awesome plugin base classes we created to save +# authors from doing all the dirty work. Let's take a look at them. + +# COMING SOON: baseldap.py classes, extending existing plugins, etc. + + -- cgit