diff options
author | Jason Gerard DeRose <jderose@redhat.com> | 2009-12-09 09:09:53 -0700 |
---|---|---|
committer | Jason Gerard DeRose <jderose@redhat.com> | 2009-12-10 08:29:15 -0700 |
commit | b6e4972e7f6aa08e0392a2cf441b60ab0e7d88b7 (patch) | |
tree | 7e5329a51af169ce34a7d275a1bbd63c1e31c026 /ipalib | |
parent | d08b8858ddc3bf6265f6ea8acae6661b9fff5112 (diff) | |
download | freeipa-b6e4972e7f6aa08e0392a2cf441b60ab0e7d88b7.tar.gz freeipa-b6e4972e7f6aa08e0392a2cf441b60ab0e7d88b7.tar.xz freeipa-b6e4972e7f6aa08e0392a2cf441b60ab0e7d88b7.zip |
Take 2: Extensible return values and validation; steps toward a single output_for_cli(); enable more webUI stuff
Diffstat (limited to 'ipalib')
-rw-r--r-- | ipalib/__init__.py | 60 | ||||
-rw-r--r-- | ipalib/cli.py | 58 | ||||
-rw-r--r-- | ipalib/crud.py | 15 | ||||
-rw-r--r-- | ipalib/frontend.py | 145 | ||||
-rw-r--r-- | ipalib/output.py | 104 | ||||
-rw-r--r-- | ipalib/parameters.py | 22 | ||||
-rw-r--r-- | ipalib/plugins/baseldap.py | 89 | ||||
-rw-r--r-- | ipalib/plugins/group.py | 22 | ||||
-rw-r--r-- | ipalib/plugins/hbac.py | 14 | ||||
-rw-r--r-- | ipalib/plugins/host.py | 29 | ||||
-rw-r--r-- | ipalib/plugins/misc.py | 81 | ||||
-rw-r--r-- | ipalib/plugins/passwd.py | 3 | ||||
-rw-r--r-- | ipalib/plugins/pwpolicy.py | 67 | ||||
-rw-r--r-- | ipalib/plugins/service.py | 3 | ||||
-rw-r--r-- | ipalib/plugins/user.py | 79 | ||||
-rw-r--r-- | ipalib/text.py | 79 |
16 files changed, 677 insertions, 193 deletions
diff --git a/ipalib/__init__.py b/ipalib/__init__.py index c1ad760b6..83956e16d 100644 --- a/ipalib/__init__.py +++ b/ipalib/__init__.py @@ -135,13 +135,13 @@ implement a ``run()`` method, like this: ... """My example plugin with run().""" ... ... def run(self): -... return 'My run() method was called!' +... return dict(result='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!' +{'result': '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()`` @@ -175,10 +175,14 @@ For example, say you have a command plugin like this: ... """Forwarding vs. execution.""" ... ... def forward(self): -... return 'in_server=%r; forward() was called.' % self.env.in_server +... return dict( +... result='forward(): in_server=%r' % self.env.in_server +... ) ... ... def execute(self): -... return 'in_server=%r; execute() was called.' % self.env.in_server +... return dict( +... result='execute(): in_server=%r' % self.env.in_server +... ) ... If ``my_command`` is loaded in a *client* context, ``forward()`` will be @@ -189,7 +193,7 @@ called: >>> api.register(my_command) >>> api.finalize() >>> api.Command.my_command() # Call your command plugin -'in_server=False; forward() was called.' +{'result': 'forward(): in_server=False'} On the other hand, if ``my_command`` is loaded in a *server* context, ``execute()`` will be called: @@ -199,7 +203,7 @@ On the other hand, if ``my_command`` is loaded in a *server* context, >>> api.register(my_command) >>> api.finalize() >>> api.Command.my_command() # Call your command plugin -'in_server=True; execute() was called.' +{'result': 'execute(): in_server=True'} Normally there should be no reason to override `frontend.Command.forward()`, but, as above, it can be done for demonstration purposes. In contrast, there @@ -312,7 +316,7 @@ Second, we have our frontend plugin, the command: ... ... def execute(self): ... """Implemented against Backend.my_backend""" -... return self.Backend.my_backend.do_stuff() +... return dict(result=self.Backend.my_backend.do_stuff()) ... >>> api.register(my_command) @@ -321,7 +325,7 @@ Lastly, we call ``api.finalize()`` and see what happens when we call >>> api.finalize() >>> api.Command.my_command() -'my_backend.do_stuff() indeed did do stuff!' +{'result': '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 @@ -335,10 +339,10 @@ example: ... ... def execute(self): ... """Same as above.""" -... return self.Backend.my_backend.do_stuff() +... return dict(result=self.Backend.my_backend.do_stuff()) ... ... def forward(self): -... return 'Just my_command.forward() getting called here.' +... return dict(result='Just my_command.forward() getting called here.') ... >>> api.register(my_command) >>> api.finalize() @@ -351,7 +355,7 @@ False And yet we can call ``my_command()``: >>> api.Command.my_command() -'Just my_command.forward() getting called here.' +{'result': 'Just my_command.forward() getting called here.'} ---------------------------------------- @@ -369,24 +373,25 @@ several other commands in a single operation. For example: ... ... def execute(self): ... """Calls command_1(), command_2()""" -... return '%s; %s.' % ( -... self.Command.command_1(), -... self.Command.command_2() +... msg = '%s; %s.' % ( +... self.Command.command_1()['result'], +... self.Command.command_2()['result'], ... ) +... return dict(result=msg) >>> class command_1(Command): ... def execute(self): -... return 'command_1.execute() called' +... return dict(result='command_1.execute() called') ... >>> class command_2(Command): ... def execute(self): -... return 'command_2.execute() called' +... return dict(result='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.' +{'result': 'command_1.execute() called; command_2.execute() called.'} Because this is quite useful, we are going to revise our golden rule somewhat: @@ -412,16 +417,18 @@ For example: ... takes_options = (Str('stuff', default=u'documentation')) ... ... def execute(self, programmer, **kw): -... return '%s, go write more %s!' % (programmer, kw['stuff']) +... return dict( +... result='%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!' +{'result': u'Jason, go write more documentation!'} >>> api.Command.nudge(u'Jason', stuff=u'unit tests') -u'Jason, go write more unit tests!' +{'result': 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: @@ -450,7 +457,7 @@ 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!' +{'result': 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 @@ -582,12 +589,14 @@ For example, say we setup a command like this: ... city='Berlin', ... ) ... if key in items: -... return items[key] -... return [ +... return dict(result=items[key]) +... items = [ ... (k, items[k]) for k in sorted(items, reverse=options['reverse']) ... ] +... return dict(result=items) ... ... def output_for_cli(self, textui, result, key, **options): +... result = result['result'] ... if key is not None: ... textui.print_plain('%s = %r' % (key, result)) ... else: @@ -738,14 +747,14 @@ For example: ... """Print message of the day.""" ... ... def execute(self): -... return self.env.message +... return dict(result=self.env.message) ... >>> api = create_api() >>> api.bootstrap(in_server=True, message='Hello, world!') >>> api.register(motd) >>> api.finalize() >>> api.Command.motd() -'Hello, world!' +{'result': 'Hello, world!'} Also see the `plugable.API.bootstrap_with_global_options()` method. @@ -872,6 +881,7 @@ from crud import Create, Retrieve, Update, Delete, Search from parameters import DefaultFrom, Bool, Flag, Int, Float, Bytes, Str, Password,List from parameters import BytesEnum, StrEnum, AccessTime, File from errors import SkipPluginModule +from text import _, gettext, ngettext # We can't import the python uuid since it includes ctypes which makes # httpd throw up when run in in mod_python due to SELinux issues diff --git a/ipalib/cli.py b/ipalib/cli.py index 64ace0359..d0feaaea3 100644 --- a/ipalib/cli.py +++ b/ipalib/cli.py @@ -312,6 +312,37 @@ class textui(backend.Backend): for attr in sorted(entry): print_attr(attr) + def print_entries(self, entries, params=None, format='%s: %s', indent=1): + assert isinstance(entries, (list, tuple)) + first = True + for entry in entries: + if not first: + print '' + first = False + self.print_entry(entry, params, format, indent) + + def print_entry(self, entry, params=None, format='%s: %s', indent=1): + """ + """ + if isinstance(entry, (list, tuple)): + entry = dict(entry) + assert isinstance(entry, dict) + if params: + order = list(params) + labels = dict((p.name, p.label) for p in params()) + else: + order = sorted(entry) + labels = dict((k, k) for k in order) + for key in order: + if key not in entry: + continue + label = labels[key] + value = entry[key] + if isinstance(value, (list, tuple)): + value = ', '.join(value) + self.print_indented(format % (label, 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. @@ -383,6 +414,23 @@ class textui(backend.Backend): """ self.print_dashed('%s:' % to_cli(name)) + def print_header(self, msg, output): + self.print_dashed(msg % output) + + def print_summary(self, msg): + """ + Print a summary at the end of a comand's output. + + For example: + + >>> ui = textui() + >>> ui.print_summary('Added user "jdoe"') + ----------------- + Added user "jdoe" + ----------------- + """ + self.print_dashed(msg) + def print_count(self, count, singular, plural=None): """ Print a summary count. @@ -408,7 +456,7 @@ class textui(backend.Backend): ``len(count)`` is used as the count. """ if type(count) is not int: - assert type(count) in (list, tuple) + assert type(count) in (list, tuple, dict) count = len(count) self.print_dashed( self.choose_number(count, singular, plural) @@ -515,6 +563,8 @@ class help(frontend.Command): takes_args = (Bytes('command?'),) + has_output = tuple() + _PLUGIN_BASE_MODULE = 'ipalib.plugins' def _get_command_module(self, module): @@ -614,6 +664,8 @@ class help(frontend.Command): class console(frontend.Command): """Start the IPA interactive Python console.""" + has_output = tuple() + def run(self): code.interact( '(Custom IPA interactive Python console)', @@ -814,8 +866,8 @@ class cli(backend.Executioner): error = None while True: if error is not None: - print '>>> %s: %s' % (param.cli_name, error) - raw = self.Backend.textui.prompt(param.cli_name, default) + print '>>> %s: %s' % (param.label, error) + raw = self.Backend.textui.prompt(param.label, default) try: value = param(raw, **kw) if value is not None: diff --git a/ipalib/crud.py b/ipalib/crud.py index 6d1a87f32..173fefc72 100644 --- a/ipalib/crud.py +++ b/ipalib/crud.py @@ -20,7 +20,7 @@ Base classes for standard CRUD operations. """ -import backend, frontend, parameters +import backend, frontend, parameters, output class Create(frontend.Method): @@ -28,6 +28,8 @@ class Create(frontend.Method): Create a new entry. """ + has_output = output.standard_entry + def get_args(self): if self.obj.primary_key: yield self.obj.primary_key.clone(attribute=True) @@ -53,18 +55,21 @@ class PKQuery(frontend.Method): yield self.obj.primary_key.clone(attribute=True, query=True) - class Retrieve(PKQuery): """ Retrieve an entry by its primary key. """ + has_output = output.standard_entry + class Update(PKQuery): """ Update one or more attributes on an entry. """ + has_output = output.standard_entry + def get_options(self): if self.extra_options_first: for option in super(Update, self).get_options(): @@ -75,17 +80,22 @@ class Update(PKQuery): for option in super(Update, self).get_options(): yield option + class Delete(PKQuery): """ Delete one or more entries. """ + has_output = output.standard_delete + class Search(frontend.Method): """ Retrieve all entries that match a given search criteria. """ + has_output = output.standard_list_of_entries + def get_args(self): yield parameters.Str('criteria?') @@ -176,4 +186,3 @@ class CrudBackend(backend.Connectible): this method should return an empty iterable. """ raise NotImplementedError('%s.search()' % self.name) - diff --git a/ipalib/frontend.py b/ipalib/frontend.py index e257a0a25..2c1168a5b 100644 --- a/ipalib/frontend.py +++ b/ipalib/frontend.py @@ -27,8 +27,11 @@ from base import lock, check_name, NameSpace from plugable import Plugin from parameters import create_param, parse_param_spec, Param, Str, Flag, Password from util import make_repr +from output import Output +from text import _, ngettext from errors import ZeroArgumentError, MaxArgumentError, OverlapError, RequiresRoot +from errors import InvocationError from constants import TYPE_ERROR @@ -43,6 +46,7 @@ def is_rule(obj): return callable(obj) and getattr(obj, RULE_FLAG, False) is True + class HasParam(Plugin): """ Base class for plugins that have `Param` `NameSpace` attributes. @@ -198,7 +202,7 @@ class HasParam(Plugin): that consider arbitrary ``api.env`` values. """ - def _get_param_iterable(self, name): + def _get_param_iterable(self, name, verb='takes'): """ Return an iterable of params defined by the attribute named ``name``. @@ -257,19 +261,19 @@ class HasParam(Plugin): Also see `HasParam._filter_param_by_context()`. """ - takes_name = 'takes_' + name - takes = getattr(self, takes_name, None) - if type(takes) is tuple: - return takes - if isinstance(takes, (Param, str)): - return (takes,) - if callable(takes): - return takes() - if takes is None: + src_name = verb + '_' + name + src = getattr(self, src_name, None) + if type(src) is tuple: + return src + if isinstance(src, (Param, str)): + return (src,) + if callable(src): + return src() + if src is None: return tuple() raise TypeError( '%s.%s must be a tuple, callable, or spec; got %r' % ( - self.name, takes_name, takes + self.name, src_name, src ) ) @@ -377,6 +381,15 @@ class Command(HasParam): output_for_cli = None obj = None + use_output_validation = True + output = None + has_output = ('result',) + output_params = None + has_output_params = tuple() + + msg_summary = None + msg_truncated = _('Results are truncated, try a more specific search') + def __call__(self, *args, **options): """ Perform validation and then execute the command. @@ -396,9 +409,32 @@ class Command(HasParam): ) self.validate(**params) (args, options) = self.params_2_args_options(**params) - result = self.run(*args, **options) - self.debug('result from %s(): %r', self.name, result) - return result + ret = self.run(*args, **options) + if ( + isinstance(ret, dict) + and 'summary' in self.output + and 'summary' not in ret + ): + if self.msg_summary: + ret['summary'] = self.msg_summary % ret + else: + ret['summary'] = None + if self.use_output_validation and (self.output or ret is not None): + self.validate_output(ret) + return ret + + def soft_validate(self, values): + errors = dict() + for p in self.params(): + try: + value = values.get(p.name) + values[p.name] = p(value, **values) + except InvocationError, e: + errors[p.name] = str(e) + return dict( + values=values, + errors=errors, + ) def _repr_iter(self, **params): """ @@ -511,10 +547,10 @@ class Command(HasParam): yield (name, kw[name]) adddict = {} - if 'setattr' in kw: + if kw.get('setattr'): adddict = self.__convert_2_dict(kw['setattr'], append=False) - if 'addattr' in kw: + if kw.get('addattr'): adddict.update(self.__convert_2_dict(kw['addattr'])) for name in adddict: @@ -691,8 +727,24 @@ class Command(HasParam): sorted(tuple(self.args()) + tuple(self.options()), key=get_key), sort=False ) + self.output = NameSpace(self._iter_output(), sort=False) + self._create_param_namespace('output_params') super(Command, self).finalize() + def _iter_output(self): + if type(self.has_output) is not tuple: + raise TypeError('%s.has_output: need a %r; got a %r: %r' % ( + self.name, tuple, type(self.has_output), self.has_output) + ) + for (i, o) in enumerate(self.has_output): + if isinstance(o, str): + o = Output(o) + if not isinstance(o, Output): + raise TypeError('%s.has_output[%d]: need a %r; got a %r: %r' % ( + self.name, i, (str, Output), type(o), o) + ) + yield o + def get_args(self): """ Iterate through parameters for ``Command.args`` namespace. @@ -741,6 +793,55 @@ class Command(HasParam): for option in self._get_param_iterable('options'): yield option + def validate_output(self, output): + """ + Validate the return value to make sure it meets the interface contract. + """ + nice = '%s.validate_output()' % self.name + if not isinstance(output, dict): + raise TypeError('%s: need a %r; got a %r: %r' % ( + nice, dict, type(output), output) + ) + if len(output) < len(self.output): + missing = sorted(set(self.output).difference(output)) + raise ValueError('%s: missing keys %r in %r' % ( + nice, missing, output) + ) + if len(output) > len(self.output): + extra = sorted(set(output).difference(self.output)) + raise ValueError('%s: unexpected keys %r in %r' % ( + nice, extra, output) + ) + for o in self.output(): + value = output[o.name] + if not (o.type is None or isinstance(value, o.type)): + raise TypeError('%s:\n output[%r]: need %r; got %r: %r' % ( + nice, o.name, o.type, type(value), value) + ) + if callable(o.validate): + o.validate(self, value) + + def get_output_params(self): + for param in self._get_param_iterable('output_params', verb='has'): + yield param + + def output_for_cli(self, textui, output, *args, **options): + if not isinstance(output, dict): + return + result = output.get('result') + summary = output.get('summary') + + if (summary and isinstance(result, (list, tuple, dict)) and result): + textui.print_name(self.name) + + if isinstance(result, (tuple, list)): + textui.print_entries(result, self.output_params) + elif isinstance(result, dict): + textui.print_entry(result, self.output_params) + + if isinstance(summary, unicode): + textui.print_summary(summary) + class LocalOrRemote(Command): """ @@ -972,7 +1073,7 @@ class Method(Attribute, Command): >>> api = create_api() >>> class user_add(Method): ... def run(self): - ... return 'Added the user!' + ... return dict(result='Added the user!') ... >>> class user(Object): ... pass @@ -987,7 +1088,7 @@ class Method(Attribute, Command): >>> list(api.Method) ['user_add'] >>> api.Method.user_add() # Will call user_add.run() - 'Added the user!' + {'result': 'Added the user!'} Second, because `Method` is a subclass of `Command`, the ``user_add`` plugin can also be accessed through the ``api.Command`` namespace: @@ -995,7 +1096,7 @@ class Method(Attribute, Command): >>> list(api.Command) ['user_add'] >>> api.Command.user_add() # Will call user_add.run() - 'Added the user!' + {'result': 'Added the user!'} And third, ``user_add`` can be accessed as an attribute on the ``user`` `Object`: @@ -1005,7 +1106,7 @@ class Method(Attribute, Command): >>> list(api.Object.user.methods) ['add'] >>> api.Object.user.methods.add() # Will call user_add.run() - 'Added the user!' + {'result': 'Added the user!'} The `Attribute` base class implements the naming convention for the attribute-to-object association. Also see the `Object` and the @@ -1018,6 +1119,10 @@ class Method(Attribute, Command): def __init__(self): super(Method, self).__init__() + def get_output_params(self): + for param in self.obj.params(): + yield param + class Property(Attribute): __public__ = frozenset(( diff --git a/ipalib/output.py b/ipalib/output.py new file mode 100644 index 000000000..425ff977c --- /dev/null +++ b/ipalib/output.py @@ -0,0 +1,104 @@ +# Authors: +# 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 + +""" +Simple description of return values. +""" + +from inspect import getdoc +from types import NoneType +from plugable import ReadOnly, lock + + +class Output(ReadOnly): + """ + Simple description of a member in the return value ``dict``. + """ + + type = None + validate = None + doc = None + + def __init__(self, name, type=None, doc=None): + self.name = name + if type is not None: + self.type = type + if doc is not None: + self.doc = doc + lock(self) + + def __repr__(self): + return '%s(%r, %r, %r)' % ( + self.__class__.__name__, self.name, self.type, self.doc, + ) + + +class Entry(Output): + type = dict + doc = 'A dictionary representing an LDAP entry' + + +emsg = """%s.validate_output() => %s.validate(): + output[%r][%d]: need a %r; got a %r: %r""" + +class ListOfEntries(Output): + type = (list, tuple) + doc = 'A list of LDAP entries' + + def validate(self, cmd, entries): + assert isinstance(entries, self.type) + for (i, entry) in enumerate(entries): + if not isinstance(entry, dict): + raise TypeError(emsg % (cmd.name, self.__class__.__name__, + self.name, i, dict, type(entry), entry) + ) + + +result = Output('result', doc='All commands should at least have a result') + +summary = Output('summary', (unicode, NoneType), + 'User-friendly description of action performed' +) + +value = Output('value', unicode, + "The primary_key value of the entry, e.g. 'jdoe' for a user" +) + +standard = (result, summary) + +standard_entry = ( + Entry('result'), + value, + summary, +) + +standard_list_of_entries = ( + ListOfEntries('result'), + Output('count', int, 'Number of entries returned'), + Output('truncated', bool, 'True if not all results were returned'), + summary, +) + +standard_delete = ( + Output('result', bool, 'True means the operation was successful'), + value, + summary, +) + +standard_value = standard_delete diff --git a/ipalib/parameters.py b/ipalib/parameters.py index 00880bd2b..b6133f1b1 100644 --- a/ipalib/parameters.py +++ b/ipalib/parameters.py @@ -233,8 +233,8 @@ class Param(ReadOnly): kwargs = ( ('cli_name', str, None), ('cli_short_name', str, None), - ('label', callable, None), - ('doc', str, ''), + ('label', str, None), + ('doc', str, None), ('required', bool, True), ('multivalue', bool, False), ('primary_key', bool, False), @@ -285,10 +285,16 @@ class Param(ReadOnly): ) ) - # Merge in default for 'cli_name' if not given: - if kw.get('cli_name', None) is None: + # Merge in default for 'cli_name', label, doc if not given: + if kw.get('cli_name') is None: kw['cli_name'] = self.name + if kw.get('label') is None: + kw['label'] = '<%s>' % self.name + + if kw.get('doc') is None: + kw['doc'] = kw['label'] + # Wrap 'default_from' in a DefaultFrom if not already: df = kw.get('default_from', None) if callable(df) and not isinstance(df, DefaultFrom): @@ -505,14 +511,6 @@ class Param(ReadOnly): 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. diff --git a/ipalib/plugins/baseldap.py b/ipalib/plugins/baseldap.py index b0ea57380..bd6ce3010 100644 --- a/ipalib/plugins/baseldap.py +++ b/ipalib/plugins/baseldap.py @@ -26,6 +26,7 @@ from ipalib import Command, Method, Object from ipalib import Flag, List, Str from ipalib.base import NameSpace from ipalib.cli import to_cli, from_cli +from ipalib import output def validate_add_attribute(ugettext, attr): @@ -178,9 +179,12 @@ class LDAPCreate(crud.Create): dn = self.post_callback(ldap, dn, entry_attrs, *keys, **options) self.obj.convert_attribute_members(entry_attrs, *keys, **options) - return (dn, entry_attrs) + return dict( + result=entry_attrs, + value=keys[0], + ) - def output_for_cli(self, textui, entry, *keys, **options): + def dont_output_for_cli(self, textui, entry, *keys, **options): textui.print_name(self.name) self.obj.print_entry(textui, entry, *keys, **options) if len(keys) > 1: @@ -220,6 +224,7 @@ class LDAPRetrieve(LDAPQuery): """ Retrieve an LDAP entry. """ + takes_options = ( Flag('raw', cli_name='raw', @@ -231,6 +236,8 @@ class LDAPRetrieve(LDAPQuery): ), ) + has_output = output.standard_entry + def execute(self, *keys, **options): ldap = self.obj.backend @@ -248,9 +255,14 @@ class LDAPRetrieve(LDAPQuery): dn = self.post_callback(ldap, dn, entry_attrs, *keys, **options) self.obj.convert_attribute_members(entry_attrs, *keys, **options) - return (dn, entry_attrs) + entry_attrs['dn'] = dn + return dict( + result=entry_attrs, + value=keys[0], + ) - def output_for_cli(self, textui, entry, *keys, **options): + + def dont_output_for_cli(self, textui, entry, *keys, **options): textui.print_name(self.name) self.obj.print_entry(textui, entry, *keys, **options) @@ -328,9 +340,12 @@ class LDAPUpdate(LDAPQuery, crud.Update): dn = self.post_callback(ldap, dn, entry_attrs, *keys, **options) self.obj.convert_attribute_members(entry_attrs, *keys, **options) - return (dn, entry_attrs) + return dict( + result=entry_attrs, + value=keys[0], + ) - def output_for_cli(self, textui, entry, *keys, **options): + def dont_output_for_cli(self, textui, entry, *keys, **options): textui.print_name(self.name) self.obj.print_entry(textui, entry, *keys, **options) if len(keys) > 1: @@ -359,6 +374,8 @@ class LDAPDelete(LDAPQuery): """ Delete an LDAP entry and all of its direct subentries. """ + has_output = output.standard_delete + def execute(self, *keys, **options): ldap = self.obj.backend @@ -384,9 +401,13 @@ class LDAPDelete(LDAPQuery): result = self.post_callback(ldap, dn, *keys, **options) - return result + return dict( + result=result, + value=keys[0], + ) + - def output_for_cli(self, textui, result, *keys, **options): + def dont_output_for_cli(self, textui, result, *keys, **options): textui.print_name(self.name) if len(keys) > 1: textui.print_dashed( @@ -454,7 +475,7 @@ class LDAPModMember(LDAPQuery): failed[attr][ldap_obj_name].append(name) return (dns, failed) - def output_for_cli(self, textui, result, *keys, **options): + def dont_output_for_cli(self, textui, result, *keys, **options): (completed, failed, entry) = result for (attr, objs) in failed.iteritems(): @@ -479,6 +500,19 @@ class LDAPAddMember(LDAPModMember): member_param_doc = 'comma-separated list of %s to add' member_count_out = ('%i member added.', '%i members added.') + has_output = ( + output.Entry('result'), + output.Output('completed', + type=int, + doc='Number of members added', + ), + output.Output('failed', + type=dict, + doc='Members that could not be added', + ), + ) + + def execute(self, *keys, **options): ldap = self.obj.backend @@ -511,7 +545,11 @@ class LDAPAddMember(LDAPModMember): ) self.obj.convert_attribute_members(entry_attrs, *keys, **options) - return (completed, failed, (dn, entry_attrs)) + return dict( + completed=completed, + failed=failed, + result=entry_attrs, + ) def pre_callback(self, ldap, dn, found, not_found, *keys, **options): return dn @@ -527,6 +565,18 @@ class LDAPRemoveMember(LDAPModMember): member_param_doc = 'comma-separated list of %s to remove' member_count_out = ('%i member removed.', '%i members removed.') + has_output = ( + output.Entry('result'), + output.Output('completed', + type=int, + doc='Number of members removed', + ), + output.Output('failed', + type=dict, + doc='Members that could not be removed', + ), + ) + def execute(self, *keys, **options): ldap = self.obj.backend @@ -559,7 +609,11 @@ class LDAPRemoveMember(LDAPModMember): ) self.obj.convert_attribute_members(entry_attrs, *keys, **options) - return (completed, failed, (dn, entry_attrs)) + return dict( + completed=completed, + failed=failed, + result=entry_attrs, + ) def pre_callback(self, ldap, dn, found, not_found, *keys, **options): return dn @@ -647,9 +701,18 @@ class LDAPSearch(crud.Search): entries[i][1], *args, **options ) entries[i] = (dn, entries[i][1]) - return (entries, truncated) - def output_for_cli(self, textui, result, *args, **options): + entries = tuple(e for (dn, e) in entries) + + return dict( + result=entries, + count=len(entries), + truncated=truncated, + ) + + + + def dont_output_for_cli(self, textui, result, *args, **options): (entries, truncated) = result textui.print_name(self.name) diff --git a/ipalib/plugins/group.py b/ipalib/plugins/group.py index b5cc7ca5a..322f3fa8b 100644 --- a/ipalib/plugins/group.py +++ b/ipalib/plugins/group.py @@ -24,6 +24,7 @@ Groups of users from ipalib import api from ipalib import Int, Str from ipalib.plugins.baseldap import * +from ipalib import _, ngettext class group(LDAPObject): @@ -57,16 +58,18 @@ class group(LDAPObject): takes_params = ( Str('cn', cli_name='name', - doc='group name', + label='Group name', primary_key=True, normalizer=lambda value: value.lower(), ), Str('description', cli_name='desc', - doc='group description', + label='Description', + doc='Group description', ), Int('gidnumber?', cli_name='gid', + label='GID', doc='GID (use this option to set it manually)', ), ) @@ -78,6 +81,9 @@ class group_add(LDAPCreate): """ Create new group. """ + + msg_summary = _('Added group "%(value)s"') + takes_options = LDAPCreate.takes_options + ( Flag('posix', cli_name='posix', @@ -90,6 +96,7 @@ class group_add(LDAPCreate): entry_attrs['objectclass'].append('posixgroup') return dn + api.register(group_add) @@ -97,6 +104,9 @@ class group_del(LDAPDelete): """ Delete group. """ + + msg_summary = _('Deleted group "%(value)s"') + def pre_callback(self, ldap, dn, *keys, **options): config = ldap.get_ipa_config()[1] def_primary_group = config.get('ipadefaultprimarygroup', '') @@ -112,6 +122,9 @@ class group_mod(LDAPUpdate): """ Modify group. """ + + msg_summary = _('Modified group "%(value)s"') + takes_options = LDAPUpdate.takes_options + ( Flag('posix', cli_name='posix', @@ -138,6 +151,10 @@ class group_find(LDAPSearch): Search for groups. """ + msg_summary = ngettext( + '%(count)d group matched', '%(count)d groups matched', 0 + ) + api.register(group_find) @@ -163,4 +180,3 @@ class group_remove_member(LDAPRemoveMember): """ api.register(group_remove_member) - diff --git a/ipalib/plugins/hbac.py b/ipalib/plugins/hbac.py index 4a7cc9407..6dc13f6d5 100644 --- a/ipalib/plugins/hbac.py +++ b/ipalib/plugins/hbac.py @@ -35,7 +35,7 @@ class hbac(LDAPObject): default_attributes = [ 'cn', 'accessruletype', 'ipaenabledflag', 'servicename', 'accesstime', 'description', - + ] uuid_attribute = 'ipauniqueid' attribute_names = { @@ -128,7 +128,7 @@ class hbac_add(LDAPCreate): if not dn.startswith('cn='): msg = 'HBAC rule with name "%s" already exists' % keys[-1] raise errors.DuplicateEntry(message=msg) - # HBAC rules are enabled by default + # HBAC rules are enabled by default entry_attrs['ipaenabledflag'] = 'TRUE' return ldap.make_dn( entry_attrs, self.obj.uuid_attribute, self.obj.container_dn @@ -184,7 +184,7 @@ class hbac_enable(LDAPQuery): except errors.EmptyModlist: pass - return True + return dict(result=True) def output_for_cli(self, textui, result, cn): textui.print_name(self.name) @@ -208,7 +208,7 @@ class hbac_disable(LDAPQuery): except errors.EmptyModlist: pass - return True + return dict(result=True) def output_for_cli(self, textui, result, cn): textui.print_name(self.name) @@ -242,7 +242,7 @@ class hbac_add_accesstime(LDAPQuery): except errors.EmptyModlist: pass - return True + return dict(result=True) def output_for_cli(self, textui, result, cn, **options): textui.print_name(self.name) @@ -280,7 +280,7 @@ class hbac_remove_accesstime(LDAPQuery): except (ValueError, errors.EmptyModlist): pass - return True + return dict(result=True) def output_for_cli(self, textui, result, cn, **options): textui.print_name(self.name) @@ -351,5 +351,3 @@ class hbac_remove_sourcehost(LDAPRemoveMember): member_count_out = ('%i object removed.', '%i objects removed.') api.register(hbac_remove_sourcehost) - - diff --git a/ipalib/plugins/host.py b/ipalib/plugins/host.py index 8859b5873..dd19362bd 100644 --- a/ipalib/plugins/host.py +++ b/ipalib/plugins/host.py @@ -29,6 +29,7 @@ from ipalib import api, errors, util from ipalib import Str, Flag from ipalib.plugins.baseldap import * from ipalib.plugins.service import split_principal +from ipalib import _, ngettext def validate_host(ugettext, fqdn): @@ -72,32 +73,38 @@ class host(LDAPObject): takes_params = ( Str('fqdn', validate_host, cli_name='hostname', - doc='Hostname', + label='Hostname', primary_key=True, normalizer=lambda value: value.lower(), ), Str('description?', cli_name='desc', + label='Description', doc='Description of the host', ), Str('localityname?', cli_name='locality', + label='Locality', doc='Locality of the host (Baltimore, MD)', ), Str('nshostlocation?', cli_name='location', + label='Location', doc='Location of the host (e.g. Lab 2)', ), Str('nshardwareplatform?', cli_name='platform', + label='Platform', doc='Hardware platform of the host (e.g. Lenovo T61)', ), Str('nsosversion?', cli_name='os', + label='Operating system', doc='Operating System and version of the host (e.g. Fedora 9)', ), Str('userpassword?', cli_name='password', + label='User password', doc='Password used in bulk enrollment', ), ) @@ -125,6 +132,9 @@ class host_add(LDAPCreate): """ Create new host. """ + + msg_summary = _('Added host "%(value)s"') + def pre_callback(self, ldap, dn, entry_attrs, *keys, **options): entry_attrs['cn'] = keys[-1] entry_attrs['serverhostname'] = keys[-1].split('.', 1)[0] @@ -147,16 +157,21 @@ class host_del(LDAPDelete): """ Delete host. """ + + msg_summary = _('Deleted host "%(value)s"') + def pre_callback(self, ldap, dn, *keys, **options): # Remove all service records for this host truncated = True while truncated: try: - (services, truncated) = api.Command['service_find'](keys[-1]) + ret = api.Command['service_find'](keys[-1]) + truncated = ret['truncated'] + services = ret['result'] except errors.NotFound: break else: - for (dn_, entry_attrs) in services: + for entry_attrs in services: principal = entry_attrs['krbprincipalname'][0] (service, hostname, realm) = split_principal(principal) if hostname.lower() == keys[-1]: @@ -170,6 +185,9 @@ class host_mod(LDAPUpdate): """ Modify host. """ + + msg_summary = _('Modified host "%(value)s"') + takes_options = LDAPUpdate.takes_options + ( Str('krbprincipalname?', cli_name='principalname', @@ -201,6 +219,10 @@ class host_find(LDAPSearch): Search for hosts. """ + msg_summary = ngettext( + '%(count)d host matched', '%(count)d hosts matched' + ) + api.register(host_find) @@ -210,4 +232,3 @@ class host_show(LDAPRetrieve): """ api.register(host_show) - diff --git a/ipalib/plugins/misc.py b/ipalib/plugins/misc.py index 8bf9d81fd..0584654f7 100644 --- a/ipalib/plugins/misc.py +++ b/ipalib/plugins/misc.py @@ -22,18 +22,39 @@ Misc plugins """ import re -from ipalib import api, LocalOrRemote - - +from ipalib import api, LocalOrRemote, _, ngettext +from ipalib.output import Output, summary # 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*',) + msg_summary = _('%(count)d variables') + + takes_args = ( + 'variables*', + ) + + has_output = ( + Output('result', + type=dict, + doc='Dictionary mapping variable name to value', + ), + Output('total', + type=int, + doc='Total number of variables env (>= count)', + ), + Output('count', + type=int, + doc='Number of variables returned (<= total)', + ), + summary, + ) def __find_keys(self, variables): keys = set() @@ -52,20 +73,18 @@ class env(LocalOrRemote): keys = self.env else: keys = self.__find_keys(variables) - return dict( - (key, self.env[key]) for key in keys + ret = dict( + result=dict( + (key, self.env[key]) for key in keys + ), + count=len(keys), + total=len(self.env), ) - - def output_for_cli(self, textui, result, variables, **options): - if len(result) == 0: - return - result = tuple((k, result[k]) for k in sorted(result)) - if len(result) == 1: - textui.print_keyval(result) - return - textui.print_name(self.name) - textui.print_keyval(result) - textui.print_count(result, '%d variables') + if len(keys) > 1: + ret['summary'] = self.msg_summary % ret + else: + ret['summary'] = None + return ret api.register(env) @@ -73,18 +92,26 @@ api.register(env) class plugins(LocalOrRemote): """Show all loaded plugins""" + msg_summary = ngettext( + '%(count)d plugin loaded', '%(count)d plugins loaded' + ) + + has_output = ( + Output('result', dict, 'Dictionary mapping plugin names to bases'), + Output('count', + type=int, + doc='Number of plugins loaded', + ), + summary, + ) + def execute(self, **options): plugins = sorted(self.api.plugins, key=lambda o: o.plugin) - return tuple( - (p.plugin, p.bases) for p in plugins + return dict( + result=dict( + (p.plugin, p.bases) for p in plugins + ), + count=len(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/passwd.py b/ipalib/plugins/passwd.py index fbc12263c..6c509c2c7 100644 --- a/ipalib/plugins/passwd.py +++ b/ipalib/plugins/passwd.py @@ -67,7 +67,7 @@ class passwd(Command): ldap.modify_password(dn, password) - return True + return dict(result=True) def output_for_cli(self, textui, result, principal, password): assert password is None @@ -75,4 +75,3 @@ class passwd(Command): textui.print_dashed('Changed password for "%s."' % principal) api.register(passwd) - diff --git a/ipalib/plugins/pwpolicy.py b/ipalib/plugins/pwpolicy.py index 5a07c880a..faf036418 100644 --- a/ipalib/plugins/pwpolicy.py +++ b/ipalib/plugins/pwpolicy.py @@ -25,6 +25,7 @@ Password policy from ipalib import api, crud, errors from ipalib import Command, Object from ipalib import Int, Str +from ipalib import output from ldap.functions import explode_dn _fields = { @@ -54,6 +55,7 @@ def _convert_time_on_input(entry_attrs): if 'krbminpwdlife' in entry_attrs: entry_attrs['krbminpwdlife'] = entry_attrs['krbminpwdlife'] * 3600 + def make_cos_entry(group, cospriority=None): """ Make the CoS dn and entry for this group. @@ -64,9 +66,10 @@ def make_cos_entry(group, cospriority=None): """ try: - (groupdn, group_attrs) = api.Command['group_show'](group) + entry = api.Command['group_show'](group)['result'] except errors.NotFound: raise errors.NotFound(reason="group '%s' does not exist" % group) + groupdn = entry['dn'] cos_entry = {} if cospriority: @@ -76,6 +79,7 @@ def make_cos_entry(group, cospriority=None): return (cos_dn, cos_entry) + def make_policy_entry(group_cn, policy_entry): """ Make the krbpwdpolicy dn and entry for this group. @@ -98,6 +102,7 @@ def make_policy_entry(group_cn, policy_entry): return (policy_dn, policy_entry) + class pwpolicy(Object): """ Password Policy object. @@ -138,6 +143,7 @@ class pwpolicy(Object): api.register(pwpolicy) + class pwpolicy_add(crud.Create): """ Create a new password policy associated with a group. @@ -150,6 +156,7 @@ class pwpolicy_add(crud.Create): ), Int('cospriority', cli_name='priority', + label='Priority', doc='Priority of the policy. Higher number equals higher priority', minvalue=0, attribute=True, @@ -182,22 +189,12 @@ class pwpolicy_add(crud.Create): _convert_time_for_output(entry_attrs) - return (dn, entry_attrs) - - def output_for_cli(self, textui, result, *args, **options): -# textui.print_name(self.name) -# textui.print_dashed("Added policy for '%s'." % options['group']) - (dn, entry_attrs) = result - - textui.print_name(self.name) - textui.print_plain('Password policy:') - for (k, v) in _fields.iteritems(): - if k in entry_attrs: - textui.print_attribute(v, entry_attrs[k]) - textui.print_dashed('Modified password policy.') + entry_attrs['dn'] = dn + return dict(result=entry_attrs, value=group_cn) api.register(pwpolicy_add) + class pwpolicy_mod(crud.Update): """ Modify password policy. @@ -215,6 +212,10 @@ class pwpolicy_mod(crud.Update): ), ) + has_output = ( + output.Entry('result'), + ) + def execute(self, *args, **options): assert 'dn' not in options ldap = self.api.Backend.ldap2 @@ -235,24 +236,16 @@ class pwpolicy_mod(crud.Update): _convert_time_for_output(entry_attrs) - return (dn, entry_attrs) - - def output_for_cli(self, textui, result, *args, **options): - (dn, entry_attrs) = result - - textui.print_name(self.name) - textui.print_plain('Password policy:') - for (k, v) in _fields.iteritems(): - if k in entry_attrs: - textui.print_attribute(v, entry_attrs[k]) - textui.print_dashed('Modified password policy.') + return dict(result=entry_attrs) api.register(pwpolicy_mod) + class pwpolicy_del(crud.Delete): """ Delete a group password policy. """ + takes_options = ( Str('group', doc='Group to remove policy from', @@ -278,12 +271,10 @@ class pwpolicy_del(crud.Delete): ldap.delete_entry(policy_dn, normalize=False) ldap.delete_entry(cos_dn, normalize=False) - - return True - - def output_for_cli(self, textui, result, *args, **options): - textui.print_name(self.name) - textui.print_dashed('Deleted policy "%s".' % options['group']) + return dict( + result=True, + value=group_cn, + ) api.register(pwpolicy_del) @@ -292,6 +283,7 @@ class pwpolicy_show(Command): """ Display password policy. """ + takes_options = ( Str('group?', doc='Group to display policy', @@ -300,6 +292,7 @@ class pwpolicy_show(Command): doc='Display policy applied to a given user', ), ) + def execute(self, *args, **options): ldap = self.api.Backend.ldap2 @@ -333,16 +326,6 @@ class pwpolicy_show(Command): entry_attrs['group'] = 'global' _convert_time_for_output(entry_attrs) - return (dn, entry_attrs) - - def output_for_cli(self, textui, result, *args, **options): - (dn, entry_attrs) = result - - textui.print_name(self.name) - textui.print_plain('Password policy:') - for (k, v) in _fields.iteritems(): - if k in entry_attrs: - textui.print_attribute(v, entry_attrs[k]) + return dict(result=entry_attrs) api.register(pwpolicy_show) - diff --git a/ipalib/plugins/service.py b/ipalib/plugins/service.py index 5b0119151..f65ab3ebd 100644 --- a/ipalib/plugins/service.py +++ b/ipalib/plugins/service.py @@ -149,7 +149,7 @@ class service_add(LDAPCreate): raise errors.HostService() try: - (hostdn, hostentry) = api.Command['host_show'](hostname, **{}) + api.Command['host_show'](hostname) except errors.NotFound: raise errors.NotFound(reason="The host '%s' does not exist to add a service to." % hostname) @@ -267,4 +267,3 @@ class service_remove_host(LDAPRemoveMember): member_attributes = ['managedby'] api.register(service_remove_host) - diff --git a/ipalib/plugins/user.py b/ipalib/plugins/user.py index 643531305..44b0f7d5e 100644 --- a/ipalib/plugins/user.py +++ b/ipalib/plugins/user.py @@ -24,6 +24,7 @@ Users (Identity) from ipalib import api, errors from ipalib import Flag, Int, Password, Str from ipalib.plugins.baseldap import * +from ipalib import _, ngettext class user(LDAPObject): @@ -68,57 +69,59 @@ class user(LDAPObject): } takes_params = ( + Str('uid', + cli_name='login', + label='User login', + primary_key=True, + default_from=lambda givenname, sn: givenname[0] + sn, + normalizer=lambda value: value.lower(), + ), Str('givenname', cli_name='first', - doc='First name', + label='First name', ), Str('sn', cli_name='last', - doc='Last name', + label='Last name', ), - Str('uid', - cli_name='user', - doc='Login name', - primary_key=True, - default_from=lambda givenname, sn: givenname[0] + sn, - normalizer=lambda value: value.lower(), + Str('homedirectory?', + cli_name='homedir', + label='Home directory', + default_from=lambda uid: '/home/%s' % uid, ), Str('gecos?', - doc='GECOS field', + label='GECOS field', default_from=lambda uid: uid, autofill=True, ), - Str('homedirectory?', - cli_name='homedir', - doc='Home directory', - default_from=lambda uid: '/home/%s' % uid, - ), Str('loginshell?', cli_name='shell', + label='Login shell', default=u'/bin/sh', - doc='login shell', ), Str('krbprincipalname?', cli_name='principal', - doc='Kerberos principal name', + label='Kerberos principal', default_from=lambda uid: '%s@%s' % (uid, api.env.realm), autofill=True, ), Str('mail?', cli_name='email', - doc='e-mail address', + label='Email address', ), Password('userpassword?', cli_name='password', - doc='password', + label='Password', + doc='Set the user password', ), Int('uidnumber?', cli_name='uid', + label='UID', doc='UID (use this option to set it manually)', ), Str('street?', cli_name='street', - doc='street address', + label='Street address', ), ) @@ -129,6 +132,9 @@ class user_add(LDAPCreate): """ Create new user. """ + + msg_summary = _('Added user "%(value)s"') + def pre_callback(self, ldap, dn, entry_attrs, *keys, **options): config = ldap.get_ipa_config()[1] entry_attrs.setdefault('loginshell', config.get('ipadefaultloginshell')) @@ -171,6 +177,9 @@ class user_del(LDAPDelete): """ Delete user. """ + + msg_summary = _('Deleted user "%(value)s"') + def pre_callback(self, ldap, dn, *keys, **options): if keys[-1] == 'admin': raise errors.ExecutionError('Cannot delete user "admin".') @@ -188,6 +197,8 @@ class user_mod(LDAPUpdate): Modify user. """ + msg_summary = _('Modified user "%(value)s"') + api.register(user_mod) @@ -196,6 +207,10 @@ class user_find(LDAPSearch): Search for users. """ + msg_summary = ngettext( + '%(count)d user matched', '%(count)d users matched', 0 + ) + api.register(user_find) @@ -211,6 +226,10 @@ class user_lock(LDAPQuery): """ Lock user account. """ + + has_output = output.standard_value + msg_summary = _('Locked user "%(value)s"') + def execute(self, *keys, **options): ldap = self.obj.backend @@ -221,11 +240,10 @@ class user_lock(LDAPQuery): except errors.AlreadyInactive: pass - return True - - def output_for_cli(self, textui, result, *keys, **options): - textui.print_name(self.name) - textui.print_dashed('Locked user "%s".' % keys[-1]) + return dict( + result=True, + value=keys[0], + ) api.register(user_lock) @@ -234,6 +252,10 @@ class user_unlock(LDAPQuery): """ Unlock user account. """ + + has_output = output.standard_value + msg_summary = _('Unlocked user "%(value)s"') + def execute(self, *keys, **options): ldap = self.obj.backend @@ -244,10 +266,9 @@ class user_unlock(LDAPQuery): except errors.AlreadyActive: pass - return True - - def output_for_cli(self, textui, result, *keys, **options): - textui.print_name(self.name) - textui.print_dashed('Unlocked user "%s".' % keys[-1]) + return dict( + result=True, + value=keys[0], + ) api.register(user_unlock) diff --git a/ipalib/text.py b/ipalib/text.py new file mode 100644 index 000000000..0c8684025 --- /dev/null +++ b/ipalib/text.py @@ -0,0 +1,79 @@ +# Authors: +# Jason Gerard DeRose <jderose@redhat.com> +# +# Copyright (C) 2009 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 + +""" +Thread-local lazy gettext service. + +TODO: This aren't hooked up into gettext yet, they currently just provide +placeholders for the rest of the code. +""" + + +class LazyText(object): + def __init__(self, domain, localedir): + self.domain = domain + self.localedir = localedir + + +class Gettext(LazyText): + def __init__(self, msg, domain, localedir): + self.msg = msg + super(Gettext, self).__init__(domain, localedir) + + def __unicode__(self): + return self.msg.decode('utf-8') + + def __mod__(self, value): + return self.__unicode__() % value + + +class NGettext(LazyText): + def __init__(self, singular, plural, domain, localedir): + self.singular = singular + self.plural = plural + super(NGettext, self).__init__(domain, localedir) + + def __mod__(self, kw): + count = kw['count'] + return self(count) % kw + + def __call__(self, count): + if count == 1: + return self.singular.decode('utf-8') + return self.plural.decode('utf-8') + + +class gettext_factory(object): + def __init__(self, domain='ipa', localedir=None): + self.domain = domain + self.localedir = localedir + + def __call__(self, msg): + return Gettext(msg, self.domain, self.localedir) + + +class ngettext_factory(gettext_factory): + def __call__(self, singular, plural, count=0): + return NGettext(singular, plural, self.domain, self.localedir) + + +# Process wide factories: +gettext = gettext_factory() +_ = gettext +ngettext = ngettext_factory() |