X-Git-Url: https://code.grnet.gr/git/ganeti-local/blobdiff_plain/e687ec0110d984ef53afc2bfc2a69c2708b98ceb..f037e9d71e0c2b2fcc4b0fe5194228acddd05f5b:/lib/cli.py?ds=sidebyside diff --git a/lib/cli.py b/lib/cli.py index f55804d..f18e65b 100644 --- a/lib/cli.py +++ b/lib/cli.py @@ -1,7 +1,7 @@ # # -# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011 Google Inc. +# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012 Google Inc. # # 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 @@ -29,6 +29,7 @@ import time import logging import errno import itertools +import shlex from cStringIO import StringIO from ganeti import utils @@ -68,6 +69,7 @@ __all__ = [ "DEBUG_SIMERR_OPT", "DISKIDX_OPT", "DISK_OPT", + "DISK_PARAMS_OPT", "DISK_TEMPLATE_OPT", "DRAINED_OPT", "DRY_RUN_OPT", @@ -92,6 +94,7 @@ __all__ = [ "DEFAULT_IALLOCATOR_OPT", "IDENTIFY_DEFAULTS_OPT", "IGNORE_CONSIST_OPT", + "IGNORE_ERRORS_OPT", "IGNORE_FAILURES_OPT", "IGNORE_OFFLINE_OPT", "IGNORE_REMOVE_FAILURES_OPT", @@ -101,6 +104,7 @@ __all__ = [ "MAC_PREFIX_OPT", "MAINTAIN_NODE_HEALTH_OPT", "MASTER_NETDEV_OPT", + "MASTER_NETMASK_OPT", "MC_OPT", "MIGRATION_MODE_OPT", "NET_OPT", @@ -109,6 +113,7 @@ __all__ = [ "NEW_CONFD_HMAC_KEY_OPT", "NEW_RAPI_CERT_OPT", "NEW_SECONDARY_OPT", + "NEW_SPICE_CERT_OPT", "NIC_PARAMS_OPT", "NODE_FORCE_JOIN_OPT", "NODE_LIST_OPT", @@ -127,12 +132,15 @@ __all__ = [ "NONICS_OPT", "NONLIVE_OPT", "NONPLUS1_OPT", + "NORUNTIME_CHGS_OPT", "NOSHUTDOWN_OPT", "NOSTART_OPT", "NOSSH_KEYCHECK_OPT", "NOVOTING_OPT", "NO_REMEMBER_OPT", "NWSYNC_OPT", + "OFFLINE_INST_OPT", + "ONLINE_INST_OPT", "ON_PRIMARY_OPT", "ON_SECONDARY_OPT", "OFFLINE_OPT", @@ -151,6 +159,7 @@ __all__ = [ "REMOVE_INSTANCE_OPT", "REMOVE_UIDS_OPT", "RESERVED_LVS_OPT", + "RUNTIME_MEM_OPT", "ROMAN_OPT", "SECONDARY_IP_OPT", "SECONDARY_ONLY_OPT", @@ -159,6 +168,15 @@ __all__ = [ "SHOWCMD_OPT", "SHUTDOWN_TIMEOUT_OPT", "SINGLE_NODE_OPT", + "SPECS_CPU_COUNT_OPT", + "SPECS_DISK_COUNT_OPT", + "SPECS_DISK_SIZE_OPT", + "SPECS_MEM_SIZE_OPT", + "SPECS_NIC_COUNT_OPT", + "IPOLICY_DISK_TEMPLATES", + "IPOLICY_VCPU_RATIO", + "SPICE_CACERT_OPT", + "SPICE_CERT_OPT", "SRC_DIR_OPT", "SRC_NODE_OPT", "SUBMIT_OPT", @@ -171,10 +189,15 @@ __all__ = [ "TO_GROUP_OPT", "UIDPOOL_OPT", "USEUNITS_OPT", + "USE_EXTERNAL_MIP_SCRIPT", "USE_REPL_NET_OPT", "VERBOSE_OPT", "VG_NAME_OPT", "YES_DOIT_OPT", + "DISK_STATE_OPT", + "HV_STATE_OPT", + "IGNORE_IPOLICY_OPT", + "INSTANCE_POLICY_OPTS", # Generic functions for CLI programs "ConfirmOperation", "GenericMain", @@ -257,9 +280,12 @@ _PRIONAME_TO_VALUE = dict(_PRIORITY_NAMES) QR_UNKNOWN, QR_INCOMPLETE) = range(3) +#: Maximum batch size for ChooseJob +_CHOOSE_BATCH = 25 + class _Argument: - def __init__(self, min=0, max=None): # pylint: disable-msg=W0622 + def __init__(self, min=0, max=None): # pylint: disable=W0622 self.min = min self.max = max @@ -274,7 +300,7 @@ class ArgSuggest(_Argument): Value can be any of the ones passed to the constructor. """ - # pylint: disable-msg=W0622 + # pylint: disable=W0622 def __init__(self, min=0, max=None, choices=None): _Argument.__init__(self, min=min, max=max) self.choices = choices @@ -442,7 +468,7 @@ def AddTags(opts, args): if not args: raise errors.OpPrereqError("No tags to be added") op = opcodes.OpTagsSet(kind=kind, name=name, tags=args) - SubmitOpCode(op, opts=opts) + SubmitOrSend(op, opts) def RemoveTags(opts, args): @@ -459,10 +485,10 @@ def RemoveTags(opts, args): if not args: raise errors.OpPrereqError("No tags to be removed") op = opcodes.OpTagsDel(kind=kind, name=name, tags=args) - SubmitOpCode(op, opts=opts) + SubmitOrSend(op, opts) -def check_unit(option, opt, value): # pylint: disable-msg=W0613 +def check_unit(option, opt, value): # pylint: disable=W0613 """OptParsers custom converter for units. """ @@ -509,7 +535,7 @@ def _SplitKeyVal(opt, data): return kv_dict -def check_ident_key_val(option, opt, value): # pylint: disable-msg=W0613 +def check_ident_key_val(option, opt, value): # pylint: disable=W0613 """Custom parser for ident:key=val,key=val options. This will store the parsed values as a tuple (ident, {key: val}). As such, @@ -526,7 +552,9 @@ def check_ident_key_val(option, opt, value): # pylint: disable-msg=W0613 msg = "Cannot pass options when removing parameter groups: %s" % value raise errors.ParameterError(msg) retval = (ident[len(NO_PREFIX):], False) - elif ident.startswith(UN_PREFIX): + elif (ident.startswith(UN_PREFIX) and + (len(ident) <= len(UN_PREFIX) or + not ident[len(UN_PREFIX)][0].isdigit())): if rest: msg = "Cannot pass options when removing parameter groups: %s" % value raise errors.ParameterError(msg) @@ -537,7 +565,7 @@ def check_ident_key_val(option, opt, value): # pylint: disable-msg=W0613 return retval -def check_key_val(option, opt, value): # pylint: disable-msg=W0613 +def check_key_val(option, opt, value): # pylint: disable=W0613 """Custom parser class for key=val,key=val options. This will store the parsed values as a dict {key: val}. @@ -546,7 +574,7 @@ def check_key_val(option, opt, value): # pylint: disable-msg=W0613 return _SplitKeyVal(opt, value) -def check_bool(option, opt, value): # pylint: disable-msg=W0613 +def check_bool(option, opt, value): # pylint: disable=W0613 """Custom parser for yes/no options. This will store the parsed value as either True or False. @@ -561,6 +589,18 @@ def check_bool(option, opt, value): # pylint: disable-msg=W0613 raise errors.ParameterError("Invalid boolean value '%s'" % value) +def check_list(option, opt, value): # pylint: disable=W0613 + """Custom parser for comma-separated lists. + + """ + # we have to make this explicit check since "".split(",") is [""], + # not an empty list :( + if not value: + return [] + else: + return utils.UnescapeAndSplit(value) + + # completion_suggestion is normally a list. Using numeric values not evaluating # to False for dynamic completion. (OPT_COMPL_MANY_NODES, @@ -594,12 +634,14 @@ class CliOption(Option): "keyval", "unit", "bool", + "list", ) TYPE_CHECKER = Option.TYPE_CHECKER.copy() TYPE_CHECKER["identkeyval"] = check_ident_key_val TYPE_CHECKER["keyval"] = check_key_val TYPE_CHECKER["unit"] = check_unit TYPE_CHECKER["bool"] = check_bool + TYPE_CHECKER["list"] = check_list # optparse.py sets make_option, so we do it for our own option class, too @@ -675,6 +717,14 @@ NWSYNC_OPT = cli_option("--no-wait-for-sync", dest="wait_for_sync", default=True, action="store_false", help="Don't wait for sync (DANGEROUS!)") +ONLINE_INST_OPT = cli_option("--online", dest="online_inst", + action="store_true", default=False, + help="Enable offline instance") + +OFFLINE_INST_OPT = cli_option("--offline", dest="offline_inst", + action="store_true", default=False, + help="Disable down instance") + DISK_TEMPLATE_OPT = cli_option("-t", "--disk-template", dest="disk_template", help=("Custom disk setup (%s)" % utils.CommaJoin(constants.DISK_TEMPLATES)), @@ -724,6 +774,11 @@ NO_INSTALL_OPT = cli_option("--no-install", dest="no_install", help="Do not install the OS (will" " enable no-start)") +NORUNTIME_CHGS_OPT = cli_option("--no-runtime-changes", + dest="allow_runtime_chgs", + default=True, action="store_false", + help="Don't allow runtime changes") + BACKEND_OPT = cli_option("-B", "--backend-parameters", dest="beparams", type="keyval", default={}, help="Backend parameters") @@ -732,6 +787,44 @@ HVOPTS_OPT = cli_option("-H", "--hypervisor-parameters", type="keyval", default={}, dest="hvparams", help="Hypervisor parameters") +DISK_PARAMS_OPT = cli_option("-D", "--disk-parameters", dest="diskparams", + help="Disk template parameters, in the format" + " template:option=value,option=value,...", + type="identkeyval", action="append", default=[]) + +SPECS_MEM_SIZE_OPT = cli_option("--specs-mem-size", dest="ispecs_mem_size", + type="keyval", default={}, + help="Memory count specs: min, max, std" + " (in MB)") + +SPECS_CPU_COUNT_OPT = cli_option("--specs-cpu-count", dest="ispecs_cpu_count", + type="keyval", default={}, + help="CPU count specs: min, max, std") + +SPECS_DISK_COUNT_OPT = cli_option("--specs-disk-count", + dest="ispecs_disk_count", + type="keyval", default={}, + help="Disk count specs: min, max, std") + +SPECS_DISK_SIZE_OPT = cli_option("--specs-disk-size", dest="ispecs_disk_size", + type="keyval", default={}, + help="Disk size specs: min, max, std (in MB)") + +SPECS_NIC_COUNT_OPT = cli_option("--specs-nic-count", dest="ispecs_nic_count", + type="keyval", default={}, + help="NIC count specs: min, max, std") + +IPOLICY_DISK_TEMPLATES = cli_option("--ipolicy-disk-templates", + dest="ipolicy_disk_templates", + type="list", default=None, + help="Comma-separated list of" + " enabled disk templates") + +IPOLICY_VCPU_RATIO = cli_option("--ipolicy-vcpu-ratio", + dest="ipolicy_vcpu_ratio", + type="float", default=None, + help="The maximum allowed vcpu-to-cpu ratio") + HYPERVISOR_OPT = cli_option("-H", "--hypervisor-parameters", dest="hypervisor", help="Hypervisor and hypervisor options, in the" " format hypervisor:option=value,option=value,...", @@ -1002,6 +1095,18 @@ MASTER_NETDEV_OPT = cli_option("--master-netdev", dest="master_netdev", metavar="NETDEV", default=None) +MASTER_NETMASK_OPT = cli_option("--master-netmask", dest="master_netmask", + help="Specify the netmask of the master IP", + metavar="NETMASK", + default=None) + +USE_EXTERNAL_MIP_SCRIPT = cli_option("--use-external-mip-script", + dest="use_external_mip_script", + help="Specify whether to run a user-provided" + " script for the master IP address turnup and" + " turndown operations", + type="bool", metavar=_YORNO, default=None) + GLOBAL_FILEDIR_OPT = cli_option("--file-storage-dir", dest="file_storage_dir", help="Specify the default directory (cluster-" "wide) for storing the file-based disks [%s]" % @@ -1083,6 +1188,21 @@ NEW_RAPI_CERT_OPT = cli_option("--new-rapi-certificate", dest="new_rapi_cert", help=("Generate a new self-signed RAPI" " certificate")) +SPICE_CERT_OPT = cli_option("--spice-certificate", dest="spice_cert", + default=None, + help="File containing new SPICE certificate") + +SPICE_CACERT_OPT = cli_option("--spice-ca-certificate", dest="spice_cacert", + default=None, + help="File containing the certificate of the CA" + " which signed the SPICE certificate") + +NEW_SPICE_CERT_OPT = cli_option("--new-spice-certificate", + dest="new_spice_cert", default=None, + action="store_true", + help=("Generate a new self-signed SPICE" + " certificate")) + NEW_CONFD_HMAC_KEY_OPT = cli_option("--new-confd-hmac-key", dest="new_confd_hmac_key", default=False, action="store_true", @@ -1235,6 +1355,31 @@ TO_GROUP_OPT = cli_option("--to", dest="to", metavar="", default=None, action="append", completion_suggest=OPT_COMPL_ONE_NODEGROUP) +IGNORE_ERRORS_OPT = cli_option("-I", "--ignore-errors", default=[], + action="append", dest="ignore_errors", + choices=list(constants.CV_ALL_ECODES_STRINGS), + help="Error code to be ignored") + +DISK_STATE_OPT = cli_option("--disk-state", default=[], dest="disk_state", + action="append", + help=("Specify disk state information in the format" + " storage_type/identifier:option=value,..."), + type="identkeyval") + +HV_STATE_OPT = cli_option("--hypervisor-state", default=[], dest="hv_state", + action="append", + help=("Specify hypervisor state information in the" + " format hypervisor:option=value,..."), + type="identkeyval") + +IGNORE_IPOLICY_OPT = cli_option("--ignore-ipolicy", dest="ignore_ipolicy", + action="store_true", default=False, + help="Ignore instance policy violations") + +RUNTIME_MEM_OPT = cli_option("-m", "--runtime-memory", dest="runtime_mem", + help="Sets the instance's runtime memory," + " ballooning it up or down to the new value", + default=None, type="unit", metavar="") #: Options provided by all commands COMMON_OPTS = [DEBUG_OPT] @@ -1263,8 +1408,19 @@ COMMON_CREATE_OPTS = [ PRIORITY_OPT, ] +# common instance policy options +INSTANCE_POLICY_OPTS = [ + SPECS_CPU_COUNT_OPT, + SPECS_DISK_COUNT_OPT, + SPECS_DISK_SIZE_OPT, + SPECS_MEM_SIZE_OPT, + SPECS_NIC_COUNT_OPT, + IPOLICY_DISK_TEMPLATES, + IPOLICY_VCPU_RATIO, + ] + -def _ParseArgs(argv, commands, aliases): +def _ParseArgs(argv, commands, aliases, env_override): """Parser for the command line arguments. This function parses the arguments and returns the function which @@ -1274,8 +1430,11 @@ def _ParseArgs(argv, commands, aliases): @param commands: dictionary with special contents, see the design doc for cmdline handling @param aliases: dictionary with command aliases {'alias': 'target, ...} + @param env_override: list of env variables allowed for default args """ + assert not (env_override - set(commands)) + if len(argv) == 0: binary = "" else: @@ -1329,13 +1488,19 @@ def _ParseArgs(argv, commands, aliases): cmd = aliases[cmd] + if cmd in env_override: + args_env_name = ("%s_%s" % (binary.replace("-", "_"), cmd)).upper() + env_args = os.environ.get(args_env_name) + if env_args: + argv = utils.InsertAtPos(argv, 1, shlex.split(env_args)) + func, args_def, parser_opts, usage, description = commands[cmd] parser = OptionParser(option_list=parser_opts + COMMON_OPTS, description=description, formatter=TitledHelpFormatter(), usage="%%prog %s %s" % (cmd, usage)) parser.disable_interspersed_args() - options, args = parser.parse_args() + options, args = parser.parse_args(args=argv[1:]) if not _CheckArguments(cmd, args_def, args): return None, None, None @@ -1969,35 +2134,41 @@ def FormatError(err): return retcode, obuf.getvalue().rstrip("\n") -def GenericMain(commands, override=None, aliases=None): +def GenericMain(commands, override=None, aliases=None, + env_override=frozenset()): """Generic main function for all the gnt-* commands. - Arguments: - - commands: a dictionary with a special structure, see the design doc - for command line handling. - - override: if not None, we expect a dictionary with keys that will - override command line options; this can be used to pass - options from the scripts to generic functions - - aliases: dictionary with command aliases {'alias': 'target, ...} + @param commands: a dictionary with a special structure, see the design doc + for command line handling. + @param override: if not None, we expect a dictionary with keys that will + override command line options; this can be used to pass + options from the scripts to generic functions + @param aliases: dictionary with command aliases {'alias': 'target, ...} + @param env_override: list of environment names which are allowed to submit + default args for commands """ # save the program name and the entire command line for later logging if sys.argv: - binary = os.path.basename(sys.argv[0]) or sys.argv[0] + binary = os.path.basename(sys.argv[0]) + if not binary: + binary = sys.argv[0] + if len(sys.argv) >= 2: - binary += " " + sys.argv[1] - old_cmdline = " ".join(sys.argv[2:]) + logname = utils.ShellQuoteArgs([binary, sys.argv[1]]) else: - old_cmdline = "" + logname = binary + + cmdline = utils.ShellQuoteArgs([binary] + sys.argv[1:]) else: binary = "" - old_cmdline = "" + cmdline = "" if aliases is None: aliases = {} try: - func, options, args = _ParseArgs(sys.argv, commands, aliases) + func, options, args = _ParseArgs(sys.argv, commands, aliases, env_override) except errors.ParameterError, err: result, err_msg = FormatError(err) ToStderr(err_msg) @@ -2010,13 +2181,10 @@ def GenericMain(commands, override=None, aliases=None): for key, val in override.iteritems(): setattr(options, key, val) - utils.SetupLogging(constants.LOG_COMMANDS, binary, debug=options.debug, + utils.SetupLogging(constants.LOG_COMMANDS, logname, debug=options.debug, stderr_logging=True) - if old_cmdline: - logging.info("run with arguments '%s'", old_cmdline) - else: - logging.info("run with no arguments") + logging.info("Command line: %s", cmdline) try: result = func(options, args) @@ -2149,7 +2317,7 @@ def GenericInstanceCreate(mode, opts, args): else: tags = [] - utils.ForceDictType(opts.beparams, constants.BES_PARAMETER_TYPES) + utils.ForceDictType(opts.beparams, constants.BES_PARAMETER_COMPAT) utils.ForceDictType(hvparams, constants.HVS_PARAMETER_TYPES) if mode == constants.INSTANCE_CREATE: @@ -2194,7 +2362,8 @@ def GenericInstanceCreate(mode, opts, args): src_path=src_path, tags=tags, no_install=no_install, - identify_defaults=identify_defaults) + identify_defaults=identify_defaults, + ignore_ipolicy=opts.ignore_ipolicy) SubmitOrSend(op, opts) return 0 @@ -2363,8 +2532,8 @@ def GenerateTable(headers, fields, separator, data, if unitfields is None: unitfields = [] - numfields = utils.FieldSet(*numfields) # pylint: disable-msg=W0142 - unitfields = utils.FieldSet(*unitfields) # pylint: disable-msg=W0142 + numfields = utils.FieldSet(*numfields) # pylint: disable=W0142 + unitfields = utils.FieldSet(*unitfields) # pylint: disable=W0142 format_fields = [] for field in fields: @@ -2630,7 +2799,8 @@ def _WarnUnknownFields(fdefs): def GenericList(resource, fields, names, unit, separator, header, cl=None, - format_override=None, verbose=False, force_filter=False): + format_override=None, verbose=False, force_filter=False, + namefield=None, qfilter=None): """Generic implementation for listing all items of a resource. @param resource: One of L{constants.QR_VIA_LUXI} @@ -2653,17 +2823,27 @@ def GenericList(resource, fields, names, unit, separator, header, cl=None, indexed by field name, contents like L{_DEFAULT_FORMAT_QUERY} @type verbose: boolean @param verbose: whether to use verbose field descriptions or not + @type namefield: string + @param namefield: Name of field to use for simple filters (see + L{qlang.MakeFilter} for details) + @type qfilter: list or None + @param qfilter: Query filter (in addition to names) """ - if cl is None: - cl = GetClient() - if not names: names = None - filter_ = qlang.MakeFilter(names, force_filter) + namefilter = qlang.MakeFilter(names, force_filter, namefield=namefield) + + if qfilter is None: + qfilter = namefilter + elif namefilter is not None: + qfilter = [qlang.OP_AND, namefilter, qfilter] + + if cl is None: + cl = GetClient() - response = cl.Query(resource, fields, filter_) + response = cl.Query(resource, fields, qfilter) found_unknown = _WarnUnknownFields(response.fields) @@ -2817,8 +2997,9 @@ def FormatTimestamp(ts): """ if not isinstance(ts, (tuple, list)) or len(ts) != 2: return "?" - sec, usec = ts - return time.strftime("%F %T", time.localtime(sec)) + ".%06d" % usec + + (sec, usecs) = ts + return utils.FormatTime(sec, usecs=usecs) def ParseTimespec(value): @@ -2892,24 +3073,24 @@ def GetOnlineNodes(nodes, cl=None, nowarn=False, secondary_ips=False, if cl is None: cl = GetClient() - filter_ = [] + qfilter = [] if nodes: - filter_.append(qlang.MakeSimpleFilter("name", nodes)) + qfilter.append(qlang.MakeSimpleFilter("name", nodes)) if nodegroup is not None: - filter_.append([qlang.OP_OR, [qlang.OP_EQUAL, "group", nodegroup], + qfilter.append([qlang.OP_OR, [qlang.OP_EQUAL, "group", nodegroup], [qlang.OP_EQUAL, "group.uuid", nodegroup]]) if filter_master: - filter_.append([qlang.OP_NOT, [qlang.OP_TRUE, "master"]]) + qfilter.append([qlang.OP_NOT, [qlang.OP_TRUE, "master"]]) - if filter_: - if len(filter_) > 1: - final_filter = [qlang.OP_AND] + filter_ + if qfilter: + if len(qfilter) > 1: + final_filter = [qlang.OP_AND] + qfilter else: - assert len(filter_) == 1 - final_filter = filter_[0] + assert len(qfilter) == 1 + final_filter = qfilter[0] else: final_filter = None @@ -3043,7 +3224,7 @@ class JobExecutor(object): for (_, _, ops) in self.queue: # SubmitJob will remove the success status, but raise an exception if # the submission fails, so we'll notice that anyway. - results.append([True, self.cl.SubmitJob(ops)]) + results.append([True, self.cl.SubmitJob(ops)[0]]) else: results = self.cl.SubmitManyJobs([ops for (_, _, ops) in self.queue]) for ((status, data), (idx, name, _)) in zip(results, self.queue): @@ -3055,7 +3236,8 @@ class JobExecutor(object): """ assert self.jobs, "_ChooseJob called with empty job list" - result = self.cl.QueryJobs([i[2] for i in self.jobs], ["status"]) + result = self.cl.QueryJobs([i[2] for i in self.jobs[:_CHOOSE_BATCH]], + ["status"]) assert result for job_data, status in zip(self.jobs, result):