summaryrefslogtreecommitdiffstats
path: root/ipalib/cli.py
diff options
context:
space:
mode:
Diffstat (limited to 'ipalib/cli.py')
-rw-r--r--ipalib/cli.py351
1 files changed, 233 insertions, 118 deletions
diff --git a/ipalib/cli.py b/ipalib/cli.py
index a802f8ef..021e01ad 100644
--- a/ipalib/cli.py
+++ b/ipalib/cli.py
@@ -85,6 +85,8 @@ class help(frontend.Application):
self.application.build_parser(cmd).print_help()
+
+
class console(frontend.Application):
'Start the IPA interactive Python console.'
@@ -95,6 +97,21 @@ class console(frontend.Application):
)
+class env(frontend.Application):
+ """
+ Show environment variables.
+ """
+
+ def run(self):
+ return tuple(
+ (key, self.api.env[key]) for key in self.api.env
+ )
+
+ def output_for_cli(self, ret):
+ for (key, value) in ret:
+ print '%s = %r' % (key, value)
+
+
class show_api(text_ui):
'Show attributes on dynamic API object'
@@ -181,6 +198,7 @@ cli_application_commands = (
console,
show_api,
plugins,
+ env,
)
@@ -204,17 +222,160 @@ class KWCollector(object):
class CLI(object):
+ """
+ All logic for dispatching over command line interface.
+ """
+
__d = None
__mcl = None
- def __init__(self, api):
- self.__api = api
- self.__all_interactive = False
- self.__not_interactive = False
+ def __init__(self, api, argv):
+ self.api = api
+ self.argv = tuple(argv)
+ self.__done = set()
+
+ def run(self, init_only=False):
+ """
+ Parse ``argv`` and potentially run a command.
+
+ This method requires several initialization steps to be completed
+ first, all of which all automatically called with a single call to
+ `CLI.finalize()`. The initialization steps are broken into separate
+ methods simply to make it easy to write unit tests.
+
+ The initialization involves these steps:
+
+ 1. `CLI.parse_globals` parses the global options, which get stored
+ in ``CLI.options``, and stores the remaining args in
+ ``CLI.cmd_argv``.
+
+ 2. `CLI.bootstrap` initializes the environment information in
+ ``CLI.api.env``.
+
+ 3. `CLI.load_plugins` registers all plugins, including the
+ CLI-specific plugins.
+
+ 4. `CLI.finalize` instantiates all plugins and performs the
+ remaining initialization needed to use the `plugable.API`
+ instance.
+ """
+ self.__doing('run')
+ self.finalize()
+ if self.api.env.mode == 'unit_test':
+ return
+ if len(self.cmd_argv) < 1:
+ self.print_commands()
+ print 'Usage: ipa [global-options] COMMAND'
+ sys.exit(2)
+ key = self.cmd_argv[0]
+ if key not in self:
+ self.print_commands()
+ print 'ipa: ERROR: unknown command %r' % key
+ sys.exit(2)
+ return self.run_cmd(self[key])
+
+ def finalize(self):
+ """
+ Fully initialize ``CLI.api`` `plugable.API` instance.
+
+ This method first calls `CLI.load_plugins` to perform some dependant
+ initialization steps, after which `plugable.API.finalize` is called.
- def __get_api(self):
- return self.__api
- api = property(__get_api)
+ Finally, the CLI-specific commands are passed a reference to this
+ `CLI` instance by calling `frontend.Application.set_application`.
+ """
+ self.__doing('finalize')
+ self.load_plugins()
+ self.api.finalize()
+ for a in self.api.Application():
+ a.set_application(self)
+ assert self.__d is None
+ self.__d = dict(
+ (c.name.replace('_', '-'), c) for c in self.api.Command()
+ )
+
+ def load_plugins(self):
+ """
+ Load all standard plugins plus the CLI-specific plugins.
+
+ This method first calls `CLI.bootstrap` to preform some dependant
+ initialization steps, after which `plugable.API.load_plugins` is
+ called.
+
+ Finally, all the CLI-specific plugins are registered.
+ """
+ self.__doing('load_plugins')
+ self.bootstrap()
+ self.api.load_plugins()
+ for klass in cli_application_commands:
+ self.api.register(klass)
+
+ def bootstrap(self):
+ """
+ Initialize the ``CLI.api.env`` environment variables.
+
+ This method first calls `CLI.parse_globals` to perform some dependant
+ initialization steps. Then, using environment variables that may have
+ been passed in the global options, the ``overrides`` are constructed
+ and `plugable.API.bootstrap` is called.
+ """
+ self.__doing('bootstrap')
+ self.parse_globals()
+ self.api.env.verbose = self.options.verbose
+ if self.options.config_file:
+ self.api.env.conf = self.options.config_file
+ overrides = {}
+ if self.options.environment:
+ for a in self.options.environment.split(','):
+ a = a.split('=', 1)
+ if len(a) < 2:
+ parser.error('badly specified environment string,'\
+ 'use var1=val1[,var2=val2]..')
+ overrides[a[0].strip()] = a[1].strip()
+ overrides['context'] = 'cli'
+ self.api.bootstrap(**overrides)
+
+ def parse_globals(self):
+ """
+ Parse out the global options.
+
+ This method parses the global options out of the ``CLI.argv`` instance
+ attribute, after which two new instance attributes are available:
+
+ 1. ``CLI.options`` - an ``optparse.Values`` instance containing
+ the global options.
+
+ 2. ``CLI.cmd_argv`` - a tuple containing the remainder of
+ ``CLI.argv`` after the global options have been consumed.
+ """
+ self.__doing('parse_globals')
+ parser = optparse.OptionParser()
+ parser.disable_interspersed_args()
+ parser.add_option('-a', dest='prompt_all', action='store_true',
+ help='Prompt for all missing options interactively')
+ parser.add_option('-n', dest='interactive', action='store_false',
+ help='Don\'t prompt for any options interactively')
+ parser.add_option('-c', dest='config_file',
+ help='Specify different configuration file')
+ parser.add_option('-e', dest='environment',
+ help='Specify or override environment variables')
+ parser.add_option('-v', dest='verbose', action='store_true',
+ help='Verbose output')
+ parser.set_defaults(
+ prompt_all=False,
+ interactive=True,
+ verbose=False,
+ )
+ (options, args) = parser.parse_args(list(self.argv))
+ self.options = options
+ self.cmd_argv = tuple(args)
+
+ def __doing(self, name):
+ if name in self.__done:
+ raise StandardError(
+ '%s.%s() already called' % (self.__class__.__name__, name)
+ )
+ self.__done.add(name)
def print_commands(self):
std = set(self.api.Command) - set(self.api.Application)
@@ -234,66 +395,38 @@ class CLI(object):
cmd.doc,
)
- def __contains__(self, key):
- assert self.__d is not None, 'you must call finalize() first'
- return key in self.__d
-
- def __getitem__(self, key):
- assert self.__d is not None, 'you must call finalize() first'
- return self.__d[key]
-
- def finalize(self):
- api = self.api
- for klass in cli_application_commands:
- api.register(klass)
- api.finalize()
- for a in api.Application():
- a.set_application(self)
- self.build_map()
-
- def build_map(self):
- assert self.__d is None
- self.__d = dict(
- (c.name.replace('_', '-'), c) for c in self.api.Command()
- )
-
- def run(self):
- self.finalize()
- set_default_env(self.api.env)
- args = self.parse_globals()
- if len(args) < 1:
- self.print_commands()
- print 'Usage: ipa [global-options] COMMAND'
- sys.exit(2)
- key = args[0]
- if key not in self:
- self.print_commands()
- print 'ipa: ERROR: unknown command %r' % key
- sys.exit(2)
- return self.run_cmd(
- self[key],
- list(s.decode('utf-8') for s in args[1:])
- )
-
- def run_cmd(self, cmd, argv):
- kw = self.parse(cmd, argv)
+ def run_cmd(self, cmd):
+ kw = self.parse(cmd)
+ # If options.interactive, interactively validate params:
+ if self.options.interactive:
+ try:
+ kw = self.prompt_interactively(cmd, kw)
+ except KeyboardInterrupt:
+ return 0
+ # Now run the command
try:
- self.run_interactive(cmd, kw)
- except KeyboardInterrupt:
+ ret = cmd(**kw)
+ if callable(cmd.output_for_cli):
+ cmd.output_for_cli(ret)
return 0
- except errors.RuleError, e:
+ except StandardError, e:
print e
return 2
- return 0
- def run_interactive(self, cmd, kw):
+ def prompt_interactively(self, cmd, kw):
+ """
+ Interactively prompt for missing or invalid values.
+
+ By default this method will only prompt for *required* Param that
+ have a missing or invalid value. However, if
+ ``CLI.options.prompt_all`` is True, this method will prompt for any
+ params that have a missing or required values, even if the param is
+ optional.
+ """
for param in cmd.params():
if param.name not in kw:
- if not param.required:
- if not self.__all_interactive:
- continue
- elif self.__not_interactive:
- exit_error('Not enough arguments given')
+ if not (param.required or self.options.prompt_all):
+ continue
default = param.get_default(**kw)
if default is None:
prompt = '%s: ' % param.cli_name
@@ -311,29 +444,34 @@ class CLI(object):
break
except errors.ValidationError, e:
error = e.error
- if self.api.env.server_context:
- try:
- import krbV
- import ldap
- from ipa_server import conn
- from ipa_server.servercore import context
- krbccache = krbV.default_context().default_ccache().name
- context.conn = conn.IPAConn(self.api.env.ldaphost, self.api.env.ldapport, krbccache)
- except ImportError:
- print >> sys.stderr, "There was a problem importing a Python module: %s" % sys.exc_value
- return 2
- except ldap.LDAPError, e:
- print >> sys.stderr, "There was a problem connecting to the LDAP server: %s" % e[0].get('desc')
- return 2
- ret = cmd(**kw)
- if callable(cmd.output_for_cli):
- return cmd.output_for_cli(ret)
- else:
- return 0
+ return kw
- def parse(self, cmd, argv):
+# FIXME: This should be done as the plugins are loaded
+# if self.api.env.server_context:
+# try:
+# import krbV
+# import ldap
+# from ipa_server import conn
+# from ipa_server.servercore import context
+# krbccache = krbV.default_context().default_ccache().name
+# context.conn = conn.IPAConn(self.api.env.ldaphost, self.api.env.ldapport, krbccache)
+# except ImportError:
+# print >> sys.stderr, "There was a problem importing a Python module: %s" % sys.exc_value
+# return 2
+# except ldap.LDAPError, e:
+# print >> sys.stderr, "There was a problem connecting to the LDAP server: %s" % e[0].get('desc')
+# return 2
+# ret = cmd(**kw)
+# if callable(cmd.output_for_cli):
+# return cmd.output_for_cli(ret)
+# else:
+# return 0
+
+ def parse(self, cmd):
parser = self.build_parser(cmd)
- (kwc, args) = parser.parse_args(argv, KWCollector())
+ (kwc, args) = parser.parse_args(
+ list(self.cmd_argv[1:]), KWCollector()
+ )
kw = kwc.__todict__()
try:
arg_kw = cmd.args_to_kw(*args)
@@ -360,43 +498,6 @@ class CLI(object):
parser.add_option(o)
return parser
- def parse_globals(self, argv=sys.argv[1:]):
- parser = optparse.OptionParser()
- parser.disable_interspersed_args()
- parser.add_option('-a', dest='interactive', action='store_true',
- help='Prompt for all missing options interactively')
- parser.add_option('-n', dest='interactive', action='store_false',
- help='Don\'t prompt for any options interactively')
- parser.add_option('-c', dest='config_file',
- help='Specify different configuration file')
- parser.add_option('-e', dest='environment',
- help='Specify or override environment variables')
- parser.add_option('-v', dest='verbose', action='store_true',
- help='Verbose output')
- (options, args) = parser.parse_args(argv)
-
- if options.interactive == True:
- self.__all_interactive = True
- elif options.interactive == False:
- self.__not_interactive = True
- if options.verbose != None:
- self.api.env.verbose = True
- if options.environment:
- env_dict = {}
- for a in options.environment.split(','):
- a = a.split('=', 1)
- if len(a) < 2:
- parser.error('badly specified environment string,'\
- 'use var1=val1[,var2=val2]..')
- env_dict[a[0].strip()] = a[1].strip()
- self.api.env.update(env_dict, True)
- if options.config_file:
- self.api.env.update(read_config(options.config_file), True)
- else:
- self.api.env.update(read_config(), True)
-
- return args
-
def get_usage(self, cmd):
return ' '.join(self.get_usage_iter(cmd))
@@ -421,3 +522,17 @@ class CLI(object):
self.__mcl = max(len(k) for k in self.__d)
return self.__mcl
mcl = property(__get_mcl)
+
+ def isdone(self, name):
+ """
+ Return True in method named ``name`` has already been called.
+ """
+ return name in self.__done
+
+ def __contains__(self, key):
+ assert self.__d is not None, 'you must call finalize() first'
+ return key in self.__d
+
+ def __getitem__(self, key):
+ assert self.__d is not None, 'you must call finalize() first'
+ return self.__d[key]