X-Git-Url: https://code.grnet.gr/git/ganeti-local/blobdiff_plain/863d7f46442636bfbc8ae6e60d43dfd44a2ee41f..91e0748c38b1b629961564362c90a29b218193b9:/lib/cli.py diff --git a/lib/cli.py b/lib/cli.py index 8065f3d..c81bf54 100644 --- a/lib/cli.py +++ b/lib/cli.py @@ -38,31 +38,87 @@ 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", - "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", - "ArgJobId", "ArgSuggest", "ArgUnknown", "ArgFile", "ArgCommand", - "ArgInstance", "ArgNode", "ArgChoice", - ] +__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, suggest=None): + def __init__(self, min=0, max=None): self.min = min self.max = max @@ -130,6 +186,20 @@ class ArgCommand(_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. @@ -232,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. @@ -378,6 +386,25 @@ def check_key_val(option, opt, value): 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 CliOption(Option): """Custom option class for optparse. @@ -400,6 +427,120 @@ class CliOption(Option): cli_option = CliOption +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): """Parser for the command line arguments. @@ -464,27 +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: - ToStderr("Error: Command %s expects no arguments", cmd) - return None, None, None - elif nargs < 0 and len(args) != -nargs: - ToStderr("Error: Command %s expects %d argument(s)", cmd, -nargs) - return None, None, None - elif nargs >= 0 and len(args) < nargs: - ToStderr("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. @@ -653,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))