Started reworking CLI class into cli plugin

This commit is contained in:
Jason Gerard DeRose 2009-01-27 16:28:50 -07:00 committed by Rob Crittenden
parent 9efda29d60
commit db0168f7af
7 changed files with 231 additions and 41 deletions

6
ipa
View File

@ -26,9 +26,7 @@ The CLI functionality is implemented in ipalib/cli.py
""" """
import sys import sys
from ipalib import api from ipalib import api, cli
from ipalib.cli import CLI
if __name__ == '__main__': if __name__ == '__main__':
cli = CLI(api, sys.argv[1:]) cli.run(api)
sys.exit(cli.run())

View File

@ -95,6 +95,13 @@ class Connectible(Backend):
class Executioner(Backend): class Executioner(Backend):
def create_context(self, ccache=None, client_ip=None):
if self.env.in_server:
self.Backend.ldap.connect(ccache=ccache)
else:
self.Backend.xmlclient.connect(ccache=ccache)
def execute(self, name, *args, **options): def execute(self, name, *args, **options):
error = None error = None
try: try:

View File

@ -392,40 +392,43 @@ class textui(backend.Backend):
self.print_error(_('Cancelled.')) self.print_error(_('Cancelled.'))
class help(frontend.Application): class help(frontend.Command):
'''Display help on a command.''' """
Display help for a command or topic.
"""
takes_args = ['command?'] takes_args = [Bytes('command?')]
def run(self, command): def finalize(self):
self.__topics = dict(
(to_cli(c.name), c) for c in self.Command()
)
super(help, self).finalize()
def run(self, key):
textui = self.Backend.textui textui = self.Backend.textui
if command is None: if key is None:
self.print_commands() self.print_commands()
return return
key = str(command) name = from_cli(key)
if key not in self.application: if name not in self.Command:
raise HelpError(topic=key) raise HelpError(topic=key)
cmd = self.application[key] cmd = self.application[key]
print 'Purpose: %s' % cmd.doc print 'Purpose: %s' % cmd.doc
self.application.build_parser(cmd).print_help() self.application.build_parser(cmd).print_help()
def print_commands(self): def print_commands(self):
std = set(self.Command) - set(self.Application) mcl = self.get_mcl()
print '\nStandard IPA commands:' for cmd in self.api.Command():
for key in sorted(std): print ' %s %s' % (
cmd = self.api.Command[key] to_cli(cmd.name).ljust(mcl),
self.print_cmd(cmd) cmd.doc,
print '\nSpecial CLI commands:' )
for cmd in self.api.Application():
self.print_cmd(cmd)
print '\nUse the --help option to see all the global options' print '\nUse the --help option to see all the global options'
print '' print ''
def print_cmd(self, cmd): def get_mcl(self):
print ' %s %s' % ( return max(len(k) for k in self.Command)
to_cli(cmd.name).ljust(self.application.mcl),
cmd.doc,
)
class console(frontend.Application): class console(frontend.Application):
@ -499,22 +502,26 @@ cli_application_commands = (
) )
class KWCollector(object): class Collector(object):
def __init__(self): def __init__(self, **extra):
object.__setattr__(self, '_KWCollector__d', {}) object.__setattr__(self, '_Collector__options', {})
object.__setattr__(self, '_Collector__extra', frozenset(extra))
for (key, value) in extra.iteritems():
object.__setattr__(self, key, value)
def __setattr__(self, name, value): def __setattr__(self, name, value):
if name in self.__d: if name not in self.__extra:
v = self.__d[name] if name in self.__options:
if type(v) is tuple: v = self.__options[name]
value = v + (value,) if type(v) is tuple:
else: value = v + (value,)
value = (v, value) else:
self.__d[name] = value value = (v, value)
self.__options[name] = value
object.__setattr__(self, name, value) object.__setattr__(self, name, value)
def __todict__(self): def __todict__(self):
return dict(self.__d) return dict(self.__options)
class CLI(object): class CLI(object):
@ -819,3 +826,146 @@ class CLI(object):
def __getitem__(self, key): def __getitem__(self, key):
assert self.__d is not None, 'you must call finalize() first' assert self.__d is not None, 'you must call finalize() first'
return self.__d[key] return self.__d[key]
class cli(backend.Executioner):
"""
Backend plugin for executing from command line interface.
"""
def run(self, argv):
if len(argv) == 0:
self.Command.help()
return
(key, argv) = (argv[0], argv[1:])
cmd = self.get_command(key)
(kw, collector) = self.parse(cmd, argv)
if collector._interactive:
self.prompt_interactively(cmd, kw, collector)
self.create_context()
def prompt_interactively(self, cmd, kw, collector):
"""
Interactively prompt for missing or invalid values.
By default this method will only prompt for *required* Param that
have a missing or invalid value. However, if
``CLI.options.prompt_all`` is True, this method will prompt for any
params that have a missing or required values, even if the param is
optional.
"""
for param in cmd.params():
if param.password or param.autofill:
continue
elif param.name not in kw:
if not param.required and not collector._prompt_all:
continue
default = param.get_default(**kw)
error = None
while True:
if error is not None:
print '>>> %s: %s' % (param.cli_name, error)
raw = self.Backend.textui.prompt(param.cli_name, default)
try:
value = param(raw, **kw)
if value is not None:
kw[param.name] = value
break
except errors.ValidationError, e:
error = e.error
def get_command(self, key):
name = from_cli(key)
if name not in self.Command:
raise CommandError(name=key)
return self.Command[name]
def parse(self, cmd, argv):
parser = self.build_parser(cmd)
(collector, args) = parser.parse_args(argv,
Collector(_prompt_all=False, interactive=True)
)
options = collector.__todict__()
kw = cmd.args_options_2_params(*args, **options)
return (dict(self.parse_iter(cmd, kw)), collector)
# FIXME: Move decoding to Command, use same regardless of request source
def parse_iter(self, cmd, kw):
"""
Decode param values if appropriate.
"""
for (key, value) in kw.iteritems():
param = cmd.params[key]
if isinstance(param, Bytes):
yield (key, value)
else:
yield (key, self.Backend.textui.decode(value))
def build_parser(self, cmd):
parser = optparse.OptionParser(
usage=' '.join(self.usage_iter(cmd))
)
if len(cmd.params) > 0:
parser.add_option('-a', dest='_prompt_all', action='store_true',
help='Prompt for all values interactively')
parser.add_option('-n', dest='_interactive', action='store_false',
help="Don\'t prompt for any values interactively")
for option in cmd.options():
kw = dict(
dest=option.name,
help=option.doc,
)
if option.password:
kw['action'] = 'store_true'
elif option.type is bool:
if option.default is True:
kw['action'] = 'store_false'
else:
kw['action'] = 'store_true'
else:
kw['metavar'] = metavar=option.__class__.__name__.upper()
o = optparse.make_option('--%s' % to_cli(option.cli_name), **kw)
parser.add_option(o)
return parser
def usage_iter(self, cmd):
yield 'Usage: %%prog [global-options] %s' % to_cli(cmd.name)
for arg in cmd.args():
if arg.password:
continue
name = to_cli(arg.cli_name).upper()
if arg.multivalue:
name = '%s...' % name
if arg.required:
yield name
else:
yield '[%s]' % name
cli_plugins = (
cli,
textui,
help,
)
def run(api):
try:
argv = api.bootstrap_with_global_options(context='cli')
for klass in cli_plugins:
api.register(klass)
api.load_plugins()
api.finalize()
api.Backend.cli.run(sys.argv[1:])
sys.exit()
except KeyboardInterrupt:
print ''
api.log.info('operation aborted')
sys.exit()
except PublicError, e:
error = e
except Exception, e:
api.log.exception('%s: %s', e.__class__.__name__, str(e))
error = InternalError()
api.log.error(error.strerror)
sys.exit(error.errno)

View File

@ -122,7 +122,7 @@ class Search(frontend.Method):
""" """
class CrudBackend(backend.Backend): class CrudBackend(backend.Connectible):
""" """
Base class defining generic CRUD backend API. Base class defining generic CRUD backend API.
""" """

View File

@ -33,6 +33,7 @@ import logging
import os import os
from os import path from os import path
import subprocess import subprocess
import optparse
import errors2 import errors2
from config import Env from config import Env
import util import util
@ -575,12 +576,37 @@ class API(DictProxy):
handler.setLevel(logging.INFO) handler.setLevel(logging.INFO)
log.addHandler(handler) log.addHandler(handler)
def bootstrap_with_global_options(self, options=None, context=None): def add_global_options(self, parser=None, context=None):
if options is None: """
parser = util.add_global_options() Add global options to an optparse.OptionParser instance.
(options, args) = parser.parse_args( """
list(s.decode('utf-8') for s in sys.argv[1:]) if parser is None:
parser = optparse.OptionParser()
parser.disable_interspersed_args()
parser.add_option('-e', dest='env', metavar='KEY=VAL', action='append',
help='Set environment variable KEY to VAL',
)
parser.add_option('-c', dest='conf', metavar='FILE',
help='Load configuration from FILE',
)
parser.add_option('-d', '--debug', action='store_true',
help='Produce full debuging output',
)
parser.add_option('-v', '--verbose', action='store_true',
help='Produce more verbose output',
)
if context == 'cli':
parser.add_option('-a', '--prompt-all', action='store_true',
help='Prompt for all values interactively'
) )
parser.add_option('-n', '--no-prompt', action='store_false',
help="Don\'t prompt for values interactively"
)
return parser
def bootstrap_with_global_options(self, parser=None, context=None):
parser = self.add_global_options(parser, context)
(options, args) = parser.parse_args()
overrides = {} overrides = {}
if options.env is not None: if options.env is not None:
assert type(options.env) is list assert type(options.env) is list
@ -600,6 +626,7 @@ class API(DictProxy):
if context is not None: if context is not None:
overrides['context'] = context overrides['context'] = context
self.bootstrap(**overrides) self.bootstrap(**overrides)
return args
def load_plugins(self): def load_plugins(self):
""" """
@ -686,6 +713,7 @@ class API(DictProxy):
for p in plugins.itervalues(): for p in plugins.itervalues():
p.instance.finalize() p.instance.finalize()
assert islocked(p.instance) is True
object.__setattr__(self, '_API__finalized', True) object.__setattr__(self, '_API__finalized', True)
tuple(PluginInfo(p) for p in plugins.itervalues()) tuple(PluginInfo(p) for p in plugins.itervalues())
object.__setattr__(self, 'plugins', object.__setattr__(self, 'plugins',

View File

@ -120,6 +120,7 @@ def add_global_options(parser=None):
""" """
if parser is None: if parser is None:
parser = optparse.OptionParser() parser = optparse.OptionParser()
parser.disable_interspersed_args()
parser.add_option('-e', dest='env', metavar='KEY=VAL', action='append', parser.add_option('-e', dest='env', metavar='KEY=VAL', action='append',
help='Set environment variable KEY to VAL', help='Set environment variable KEY to VAL',
) )

View File

@ -41,6 +41,12 @@ class ldap(CrudBackend):
self.dn = _ldap.dn self.dn = _ldap.dn
super(ldap, self).__init__() super(ldap, self).__init__()
def create_connection(self, ccache=None):
return 'The LDAP connection.'
def destroy_connection(self):
pass
def make_user_dn(self, uid): def make_user_dn(self, uid):
""" """
Construct user dn from uid. Construct user dn from uid.