X-Git-Url: https://code.grnet.gr/git/ganeti-local/blobdiff_plain/775c6d3eed068b7825523043b774e77a89d895f1..91e0748c38b1b629961564362c90a29b218193b9:/lib/cli.py diff --git a/lib/cli.py b/lib/cli.py index fd32969..c81bf54 100644 --- a/lib/cli.py +++ b/lib/cli.py @@ -38,25 +38,168 @@ from ganeti import luxi from ganeti import ssconf from ganeti import rpc -from optparse import (OptionParser, make_option, TitledHelpFormatter, +from optparse import (OptionParser, TitledHelpFormatter, Option, OptionValueError) -__all__ = ["DEBUG_OPT", "NOHDR_OPT", "SEP_OPT", "GenericMain", - "SubmitOpCode", "GetClient", - "cli_option", "ikv_option", "keyval_option", - "GenerateTable", "AskUser", - "ARGS_NONE", "ARGS_FIXED", "ARGS_ATLEAST", "ARGS_ANY", "ARGS_ONE", - "USEUNITS_OPT", "FIELDS_OPT", "FORCE_OPT", "SUBMIT_OPT", - "ListTags", "AddTags", "RemoveTags", "TAG_SRC_OPT", - "FormatError", "SplitNodeOption", "SubmitOrSend", - "JobSubmittedException", "FormatTimestamp", "ParseTimespec", - "ToStderr", "ToStdout", "UsesRPC", - "GetOnlineNodes", "JobExecutor", "SYNC_OPT", "CONFIRM_OPT", - ] + +__all__ = [ + # Command line options + "BACKEND_OPT", + "CONFIRM_OPT", + "DEBUG_OPT", + "DEBUG_SIMERR_OPT", + "DISK_TEMPLATE_OPT", + "FIELDS_OPT", + "FILESTORE_DIR_OPT", + "FILESTORE_DRIVER_OPT", + "HVLIST_OPT", + "HVOPTS_OPT", + "HYPERVISOR_OPT", + "IALLOCATOR_OPT", + "FORCE_OPT", + "NOHDR_OPT", + "NOIPCHECK_OPT", + "NONICS_OPT", + "NWSYNC_OPT", + "OS_OPT", + "SEP_OPT", + "SUBMIT_OPT", + "SYNC_OPT", + "TAG_SRC_OPT", + "USEUNITS_OPT", + "VERBOSE_OPT", + # Generic functions for CLI programs + "GenericMain", + "GetClient", + "GetOnlineNodes", + "JobExecutor", + "JobSubmittedException", + "ParseTimespec", + "SubmitOpCode", + "SubmitOrSend", + "UsesRPC", + # Formatting functions + "ToStderr", "ToStdout", + "FormatError", + "GenerateTable", + "AskUser", + "FormatTimestamp", + # Tags functions + "ListTags", + "AddTags", + "RemoveTags", + # command line options support infrastructure + "ARGS_MANY_INSTANCES", + "ARGS_MANY_NODES", + "ARGS_NONE", + "ARGS_ONE_INSTANCE", + "ARGS_ONE_NODE", + "ArgChoice", + "ArgCommand", + "ArgFile", + "ArgHost", + "ArgInstance", + "ArgJobId", + "ArgNode", + "ArgSuggest", + "ArgUnknown", + "OPT_COMPL_INST_ADD_NODES", + "OPT_COMPL_MANY_NODES", + "OPT_COMPL_ONE_IALLOCATOR", + "OPT_COMPL_ONE_INSTANCE", + "OPT_COMPL_ONE_NODE", + "OPT_COMPL_ONE_OS", + "cli_option", + "SplitNodeOption", + ] NO_PREFIX = "no_" UN_PREFIX = "-" + +class _Argument: + def __init__(self, min=0, max=None): + self.min = min + self.max = max + + def __repr__(self): + return ("<%s min=%s max=%s>" % + (self.__class__.__name__, self.min, self.max)) + + +class ArgSuggest(_Argument): + """Suggesting argument. + + Value can be any of the ones passed to the constructor. + + """ + def __init__(self, min=0, max=None, choices=None): + _Argument.__init__(self, min=min, max=max) + self.choices = choices + + def __repr__(self): + return ("<%s min=%s max=%s choices=%r>" % + (self.__class__.__name__, self.min, self.max, self.choices)) + + +class ArgChoice(ArgSuggest): + """Choice argument. + + Value can be any of the ones passed to the constructor. Like L{ArgSuggest}, + but value must be one of the choices. + + """ + + +class ArgUnknown(_Argument): + """Unknown argument to program (e.g. determined at runtime). + + """ + + +class ArgInstance(_Argument): + """Instances argument. + + """ + + +class ArgNode(_Argument): + """Node argument. + + """ + +class ArgJobId(_Argument): + """Job ID argument. + + """ + + +class ArgFile(_Argument): + """File path argument. + + """ + + +class ArgCommand(_Argument): + """Command argument. + + """ + + +class ArgHost(_Argument): + """Host argument. + + """ + + +ARGS_NONE = [] +ARGS_MANY_INSTANCES = [ArgInstance()] +ARGS_MANY_NODES = [ArgNode()] +ARGS_ONE_INSTANCE = [ArgInstance(min=1, max=1)] +ARGS_ONE_NODE = [ArgNode(min=1, max=1)] + + + def _ExtractTagsObject(opts, args): """Extract the tag type object. @@ -122,7 +265,7 @@ def ListTags(opts, args): result = list(result) result.sort() for tag in result: - print tag + ToStdout(tag) def AddTags(opts, args): @@ -159,68 +302,6 @@ def RemoveTags(opts, args): SubmitOpCode(op) -DEBUG_OPT = make_option("-d", "--debug", default=False, - action="store_true", - help="Turn debugging on") - -NOHDR_OPT = make_option("--no-headers", default=False, - action="store_true", dest="no_headers", - help="Don't display column headers") - -SEP_OPT = make_option("--separator", default=None, - action="store", dest="separator", - help="Separator between output fields" - " (defaults to one space)") - -USEUNITS_OPT = make_option("--units", default=None, - dest="units", choices=('h', 'm', 'g', 't'), - help="Specify units for output (one of hmgt)") - -FIELDS_OPT = make_option("-o", "--output", dest="output", action="store", - type="string", help="Comma separated list of" - " output fields", - metavar="FIELDS") - -FORCE_OPT = make_option("-f", "--force", dest="force", action="store_true", - default=False, help="Force the operation") - -CONFIRM_OPT = make_option("--yes", dest="confirm", action="store_true", - default=False, help="Do not require confirmation") - -TAG_SRC_OPT = make_option("--from", dest="tags_source", - default=None, help="File with tag names") - -SUBMIT_OPT = make_option("--submit", dest="submit_only", - default=False, action="store_true", - help="Submit the job and return the job ID, but" - " don't wait for the job to finish") - -SYNC_OPT = make_option("--sync", dest="do_locking", - default=False, action="store_true", - help="Grab locks while doing the queries" - " in order to ensure more consistent results") - -_DRY_RUN_OPT = make_option("--dry-run", default=False, - action="store_true", - help="Do not execute the operation, just run the" - " check steps and verify it it could be executed") - - -def ARGS_FIXED(val): - """Macro-like function denoting a fixed number of arguments""" - return -val - - -def ARGS_ATLEAST(val): - """Macro-like function denoting a minimum number of arguments""" - return val - - -ARGS_NONE = None -ARGS_ONE = ARGS_FIXED(1) -ARGS_ANY = ARGS_ATLEAST(0) - - def check_unit(option, opt, value): """OptParsers custom converter for units. @@ -231,15 +312,6 @@ def check_unit(option, opt, value): raise OptionValueError("option %s: %s" % (opt, err)) -class CliOption(Option): - """Custom option class for optparse. - - """ - TYPES = Option.TYPES + ("unit",) - TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER) - TYPE_CHECKER["unit"] = check_unit - - def _SplitKeyVal(opt, data): """Convert a KeyVal string into a dict. @@ -278,7 +350,10 @@ def _SplitKeyVal(opt, data): def check_ident_key_val(option, opt, value): - """Custom parser for the IdentKeyVal option type. + """Custom parser for ident:key=val,key=val options. + + This will store the parsed values as a tuple (ident, {key: val}). As such, + multiple uses of this option via action=append is possible. """ if ":" not in value: @@ -302,40 +377,168 @@ def check_ident_key_val(option, opt, value): return retval -class IdentKeyValOption(Option): - """Custom option class for ident:key=val,key=val options. +def check_key_val(option, opt, value): + """Custom parser class for key=val,key=val options. - This will store the parsed values as a tuple (ident, {key: val}). As - such, multiple uses of this option via action=append is possible. + This will store the parsed values as a dict {key: val}. """ - TYPES = Option.TYPES + ("identkeyval",) - TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER) - TYPE_CHECKER["identkeyval"] = check_ident_key_val - + return _SplitKeyVal(opt, value) -def check_key_val(option, opt, value): - """Custom parser for the KeyVal option type. - """ - return _SplitKeyVal(opt, value) +# completion_suggestion is normally a list. Using numeric values not evaluating +# to False for dynamic completion. +(OPT_COMPL_MANY_NODES, + OPT_COMPL_ONE_NODE, + OPT_COMPL_ONE_INSTANCE, + OPT_COMPL_ONE_OS, + OPT_COMPL_ONE_IALLOCATOR, + OPT_COMPL_INST_ADD_NODES) = range(100, 106) +OPT_COMPL_ALL = frozenset([ + OPT_COMPL_MANY_NODES, + OPT_COMPL_ONE_NODE, + OPT_COMPL_ONE_INSTANCE, + OPT_COMPL_ONE_OS, + OPT_COMPL_ONE_IALLOCATOR, + OPT_COMPL_INST_ADD_NODES, + ]) -class KeyValOption(Option): - """Custom option class for key=val,key=val options. - This will store the parsed values as a dict {key: val}. +class CliOption(Option): + """Custom option class for optparse. """ - TYPES = Option.TYPES + ("keyval",) - TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER) + ATTRS = Option.ATTRS + [ + "completion_suggest", + ] + TYPES = Option.TYPES + ( + "identkeyval", + "keyval", + "unit", + ) + TYPE_CHECKER = Option.TYPE_CHECKER.copy() + TYPE_CHECKER["identkeyval"] = check_ident_key_val TYPE_CHECKER["keyval"] = check_key_val + TYPE_CHECKER["unit"] = check_unit # optparse.py sets make_option, so we do it for our own option class, too cli_option = CliOption -ikv_option = IdentKeyValOption -keyval_option = KeyValOption + + +DEBUG_OPT = cli_option("-d", "--debug", default=False, + action="store_true", + help="Turn debugging on") + +NOHDR_OPT = cli_option("--no-headers", default=False, + action="store_true", dest="no_headers", + help="Don't display column headers") + +SEP_OPT = cli_option("--separator", default=None, + action="store", dest="separator", + help=("Separator between output fields" + " (defaults to one space)")) + +USEUNITS_OPT = cli_option("--units", default=None, + dest="units", choices=('h', 'm', 'g', 't'), + help="Specify units for output (one of hmgt)") + +FIELDS_OPT = cli_option("-o", "--output", dest="output", action="store", + type="string", metavar="FIELDS", + help="Comma separated list of output fields") + +FORCE_OPT = cli_option("-f", "--force", dest="force", action="store_true", + default=False, help="Force the operation") + +CONFIRM_OPT = cli_option("--yes", dest="confirm", action="store_true", + default=False, help="Do not require confirmation") + +TAG_SRC_OPT = cli_option("--from", dest="tags_source", + default=None, help="File with tag names") + +SUBMIT_OPT = cli_option("--submit", dest="submit_only", + default=False, action="store_true", + help=("Submit the job and return the job ID, but" + " don't wait for the job to finish")) + +SYNC_OPT = cli_option("--sync", dest="do_locking", + default=False, action="store_true", + help=("Grab locks while doing the queries" + " in order to ensure more consistent results")) + +_DRY_RUN_OPT = cli_option("--dry-run", default=False, + action="store_true", + help=("Do not execute the operation, just run the" + " check steps and verify it it could be" + " executed")) + +VERBOSE_OPT = cli_option("-v", "--verbose", default=False, + action="store_true", + help="Increase the verbosity of the operation") + +DEBUG_SIMERR_OPT = cli_option("--debug-simulate-errors", default=False, + action="store_true", dest="simulate_errors", + help="Debugging option that makes the operation" + " treat most runtime checks as failed") + +NWSYNC_OPT = cli_option("--no-wait-for-sync", dest="wait_for_sync", + default=True, action="store_false", + help="Don't wait for sync (DANGEROUS!)") + +DISK_TEMPLATE_OPT = cli_option("-t", "--disk-template", dest="disk_template", + help="Custom disk setup (diskless, file," + " plain or drbd)", + default=None, metavar="TEMPL", + choices=list(constants.DISK_TEMPLATES)) + +NONICS_OPT = cli_option("--no-nics", default=False, action="store_true", + help="Do not create any network cards for" + " the instance") + +FILESTORE_DIR_OPT = cli_option("--file-storage-dir", dest="file_storage_dir", + help="Relative path under default cluster-wide" + " file storage dir to store file-based disks", + default=None, metavar="") + +FILESTORE_DRIVER_OPT = cli_option("--file-driver", dest="file_driver", + help="Driver to use for image files", + default="loop", metavar="", + choices=list(constants.FILE_DRIVER)) + +IALLOCATOR_OPT = cli_option("-I", "--iallocator", metavar="", + help="Select nodes for the instance automatically" + " using the iallocator plugin", + default=None, type="string", + completion_suggest=OPT_COMPL_ONE_IALLOCATOR) + +OS_OPT = cli_option("-o", "--os-type", dest="os", help="What OS to run", + metavar="", + completion_suggest=OPT_COMPL_ONE_OS) + +BACKEND_OPT = cli_option("-B", "--backend-parameters", dest="beparams", + type="keyval", default={}, + help="Backend parameters") + +HVOPTS_OPT = cli_option("-H", "--hypervisor-parameters", type="keyval", + default={}, dest="hvparams", + help="Hypervisor parameters") + +HYPERVISOR_OPT = cli_option("-H", "--hypervisor-parameters", dest="hypervisor", + help="Hypervisor and hypervisor options, in the" + " format hypervisor:option=value,option=value,...", + default=None, type="identkeyval") + +HVLIST_OPT = cli_option("-H", "--hypervisor-parameters", dest="hvparams", + help="Hypervisor and hypervisor options, in the" + " format hypervisor:option=value,option=value,...", + default=[], action="append", type="identkeyval") + +NOIPCHECK_OPT = cli_option("--no-ip-check", dest="ip_check", default=True, + action="store_false", + help="Don't check that the instance's IP" + " is alive") + def _ParseArgs(argv, commands, aliases): @@ -356,7 +559,7 @@ def _ParseArgs(argv, commands, aliases): binary = argv[0].split("/")[-1] if len(argv) > 1 and argv[1] == "--version": - print "%s (ganeti) %s" % (binary, constants.RELEASE_VERSION) + ToStdout("%s (ganeti) %s", binary, constants.RELEASE_VERSION) # Quit right away. That way we don't have to care about this special # argument. optparse.py does it the same. sys.exit(0) @@ -366,22 +569,27 @@ def _ParseArgs(argv, commands, aliases): # let's do a nice thing sortedcmds = commands.keys() sortedcmds.sort() - print ("Usage: %(bin)s {command} [options...] [argument...]" - "\n%(bin)s --help to see details, or" - " man %(bin)s\n" % {"bin": binary}) + + ToStdout("Usage: %s {command} [options...] [argument...]", binary) + ToStdout("%s --help to see details, or man %s", binary, binary) + ToStdout("") + # compute the max line length for cmd + usage mlen = max([len(" %s" % cmd) for cmd in commands]) mlen = min(60, mlen) # should not get here... + # and format a nice command list - print "Commands:" + ToStdout("Commands:") for cmd in sortedcmds: cmdstr = " %s" % (cmd,) help_text = commands[cmd][4] - help_lines = textwrap.wrap(help_text, 79-3-mlen) - print "%-*s - %s" % (mlen, cmdstr, help_lines.pop(0)) + help_lines = textwrap.wrap(help_text, 79 - 3 - mlen) + ToStdout("%-*s - %s", mlen, cmdstr, help_lines.pop(0)) for line in help_lines: - print "%-*s %s" % (mlen, "", line) - print + ToStdout("%-*s %s", mlen, "", line) + + ToStdout("") + return None, None, None # get command, unalias it, and look it up in commands @@ -397,29 +605,91 @@ def _ParseArgs(argv, commands, aliases): cmd = aliases[cmd] - func, nargs, parser_opts, usage, description = commands[cmd] + func, args_def, parser_opts, usage, description = commands[cmd] parser = OptionParser(option_list=parser_opts + [_DRY_RUN_OPT], description=description, formatter=TitledHelpFormatter(), usage="%%prog %s %s" % (cmd, usage)) parser.disable_interspersed_args() options, args = parser.parse_args() - if nargs is None: - if len(args) != 0: - print >> sys.stderr, ("Error: Command %s expects no arguments" % cmd) - return None, None, None - elif nargs < 0 and len(args) != -nargs: - print >> sys.stderr, ("Error: Command %s expects %d argument(s)" % - (cmd, -nargs)) - return None, None, None - elif nargs >= 0 and len(args) < nargs: - print >> sys.stderr, ("Error: Command %s expects at least %d argument(s)" % - (cmd, nargs)) + + if not _CheckArguments(cmd, args_def, args): return None, None, None return func, options, args +def _CheckArguments(cmd, args_def, args): + """Verifies the arguments using the argument definition. + + Algorithm: + + 1. Abort with error if values specified by user but none expected. + + 1. For each argument in definition + + 1. Keep running count of minimum number of values (min_count) + 1. Keep running count of maximum number of values (max_count) + 1. If it has an unlimited number of values + + 1. Abort with error if it's not the last argument in the definition + + 1. If last argument has limited number of values + + 1. Abort with error if number of values doesn't match or is too large + + 1. Abort with error if user didn't pass enough values (min_count) + + """ + if args and not args_def: + ToStderr("Error: Command %s expects no arguments", cmd) + return False + + min_count = None + max_count = None + check_max = None + + last_idx = len(args_def) - 1 + + for idx, arg in enumerate(args_def): + if min_count is None: + min_count = arg.min + elif arg.min is not None: + min_count += arg.min + + if max_count is None: + max_count = arg.max + elif arg.max is not None: + max_count += arg.max + + if idx == last_idx: + check_max = (arg.max is not None) + + elif arg.max is None: + raise errors.ProgrammerError("Only the last argument can have max=None") + + if check_max: + # Command with exact number of arguments + if (min_count is not None and max_count is not None and + min_count == max_count and len(args) != min_count): + ToStderr("Error: Command %s expects %d argument(s)", cmd, min_count) + return False + + # Command with limited number of arguments + if max_count is not None and len(args) > max_count: + ToStderr("Error: Command %s expects only %d argument(s)", + cmd, max_count) + return False + + # Command with some required arguments + if min_count is not None and len(args) < min_count: + ToStderr("Error: Command %s expects at least %d argument(s)", + cmd, min_count) + return False + + return True + + def SplitNodeOption(value): """Splits the value of a --node option. @@ -560,7 +830,7 @@ def PollJob(job_id, cl=None, feedback_fn=None): feedback_fn(log_entry[1:]) else: encoded = utils.SafeEncode(message) - print "%s %s" % (time.ctime(utils.MergeTime(timestamp)), encoded) + ToStdout("%s %s", time.ctime(utils.MergeTime(timestamp)), encoded) prev_logmsg_serial = max(prev_logmsg_serial, serial) # TODO: Handle canceled and archived jobs @@ -588,6 +858,7 @@ def PollJob(job_id, cl=None, feedback_fn=None): if status == constants.OP_STATUS_SUCCESS: has_ok = True elif status == constants.OP_STATUS_ERROR: + errors.MaybeRaise(msg) if has_ok: raise errors.OpExecError("partial failure (opcode %d): %s" % (idx, msg))