X-Git-Url: https://code.grnet.gr/git/ganeti-local/blobdiff_plain/c270ee079abdb966c8b50b8090cc70a36db035b7..def6577f00a482c310f4e20bb315fa90290ad5b7:/lib/cli.py diff --git a/lib/cli.py b/lib/cli.py index 03ea3b4..2e436b6 100644 --- a/lib/cli.py +++ b/lib/cli.py @@ -81,7 +81,7 @@ __all__ = [ "DST_NODE_OPT", "EARLY_RELEASE_OPT", "ENABLED_HV_OPT", - "ENABLED_STORAGE_TYPES_OPT", + "ENABLED_DISK_TEMPLATES_OPT", "ERROR_CODES_OPT", "FAILURE_ONLY_OPT", "FIELDS_OPT", @@ -108,6 +108,7 @@ __all__ = [ "IGNORE_REMOVE_FAILURES_OPT", "IGNORE_SECONDARIES_OPT", "IGNORE_SIZE_OPT", + "INCLUDEDEFAULTS_OPT", "INTERVAL_OPT", "MAC_PREFIX_OPT", "MAINTAIN_NODE_HEALTH_OPT", @@ -122,6 +123,7 @@ __all__ = [ "NEW_CLUSTER_DOMAIN_SECRET_OPT", "NEW_CONFD_HMAC_KEY_OPT", "NEW_RAPI_CERT_OPT", + "NEW_PRIMARY_OPT", "NEW_SECONDARY_OPT", "NEW_SPICE_CERT_OPT", "NIC_PARAMS_OPT", @@ -163,6 +165,7 @@ __all__ = [ "PREALLOC_WIPE_DISKS_OPT", "PRIMARY_IP_VERSION_OPT", "PRIMARY_ONLY_OPT", + "PRINT_JOBID_OPT", "PRIORITY_OPT", "RAPI_CERT_OPT", "READD_OPT", @@ -187,6 +190,8 @@ __all__ = [ "SPECS_DISK_SIZE_OPT", "SPECS_MEM_SIZE_OPT", "SPECS_NIC_COUNT_OPT", + "SPLIT_ISPECS_OPTS", + "IPOLICY_STD_SPECS_OPT", "IPOLICY_DISK_TEMPLATES", "IPOLICY_VCPU_RATIO", "SPICE_CACERT_OPT", @@ -194,6 +199,7 @@ __all__ = [ "SRC_DIR_OPT", "SRC_NODE_OPT", "SUBMIT_OPT", + "SUBMIT_OPTS", "STARTUP_PAUSED_OPT", "STATIC_OPT", "SYNC_OPT", @@ -233,7 +239,10 @@ __all__ = [ "ToStderr", "ToStdout", "FormatError", "FormatQueryResult", - "FormatParameterDict", + "FormatParamsDictInfo", + "FormatPolicyInfo", + "PrintIPolicyCommand", + "PrintGenericInfo", "GenerateTable", "AskUser", "FormatTimestamp", @@ -560,12 +569,13 @@ def check_unit(option, opt, value): # pylint: disable=W0613 raise OptionValueError("option %s: %s" % (opt, err)) -def _SplitKeyVal(opt, data): +def _SplitKeyVal(opt, data, parse_prefixes): """Convert a KeyVal string into a dict. This function will convert a key=val[,...] string into a dict. Empty values will be converted specially: keys which have the prefix 'no_' - will have the value=False and the prefix stripped, the others will + will have the value=False and the prefix stripped, keys with the prefix + "-" will have value=None and the prefix stripped, and the others will have value=True. @type opt: string @@ -573,6 +583,8 @@ def _SplitKeyVal(opt, data): data, used in building error messages @type data: string @param data: a string of the format key=val,key=val,... + @type parse_prefixes: bool + @param parse_prefixes: whether to handle prefixes specially @rtype: dict @return: {key=val, key=val} @raises errors.ParameterError: if there are duplicate keys @@ -583,13 +595,16 @@ def _SplitKeyVal(opt, data): for elem in utils.UnescapeAndSplit(data, sep=","): if "=" in elem: key, val = elem.split("=", 1) - else: + elif parse_prefixes: if elem.startswith(NO_PREFIX): key, val = elem[len(NO_PREFIX):], False elif elem.startswith(UN_PREFIX): key, val = elem[len(UN_PREFIX):], None else: key, val = elem, True + else: + raise errors.ParameterError("Missing value for key '%s' in option %s" % + (elem, opt)) if key in kv_dict: raise errors.ParameterError("Duplicate key '%s' in option %s" % (key, opt)) @@ -597,11 +612,19 @@ def _SplitKeyVal(opt, data): return kv_dict -def check_ident_key_val(option, opt, value): # pylint: disable=W0613 - """Custom parser for ident:key=val,key=val options. +def _SplitIdentKeyVal(opt, value, parse_prefixes): + """Helper function to parse "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. + @type opt: string + @param opt: option name, used in error messages + @type value: string + @param value: expected to be in the format "ident:key=val,key=val,..." + @type parse_prefixes: bool + @param parse_prefixes: whether to handle prefixes specially (see + L{_SplitKeyVal}) + @rtype: tuple + @return: (ident, {key=val, key=val}) + @raises errors.ParameterError: in case of duplicates or other parsing errors """ if ":" not in value: @@ -609,31 +632,67 @@ def check_ident_key_val(option, opt, value): # pylint: disable=W0613 else: ident, rest = value.split(":", 1) - if ident.startswith(NO_PREFIX): + if parse_prefixes and ident.startswith(NO_PREFIX): if rest: 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) and - (len(ident) <= len(UN_PREFIX) or - not ident[len(UN_PREFIX)][0].isdigit())): + elif (parse_prefixes and ident.startswith(UN_PREFIX) and + (len(ident) <= len(UN_PREFIX) or not ident[len(UN_PREFIX)].isdigit())): if rest: msg = "Cannot pass options when removing parameter groups: %s" % value raise errors.ParameterError(msg) retval = (ident[len(UN_PREFIX):], None) else: - kv_dict = _SplitKeyVal(opt, rest) + kv_dict = _SplitKeyVal(opt, rest, parse_prefixes) retval = (ident, kv_dict) return retval +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, + multiple uses of this option via action=append is possible. + + """ + return _SplitIdentKeyVal(opt, value, True) + + 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}. """ - return _SplitKeyVal(opt, value) + return _SplitKeyVal(opt, value, True) + + +def _SplitListKeyVal(opt, value): + retval = {} + for elem in value.split("/"): + if not elem: + raise errors.ParameterError("Empty section in option '%s'" % opt) + (ident, valdict) = _SplitIdentKeyVal(opt, elem, False) + if ident in retval: + msg = ("Duplicated parameter '%s' in parsing %s: %s" % + (ident, opt, elem)) + raise errors.ParameterError(msg) + retval[ident] = valdict + return retval + + +def check_multilist_ident_key_val(_, opt, value): + """Custom parser for "ident:key=val,key=val/ident:key=val//ident:.." options. + + @rtype: list of dictionary + @return: [{ident: {key: val, key: val}, ident: {key: val}}, {ident:..}] + + """ + retval = [] + for line in value.split("//"): + retval.append(_SplitListKeyVal(opt, line)) + return retval def check_bool(option, opt, value): # pylint: disable=W0613 @@ -708,6 +767,7 @@ class CliOption(Option): "completion_suggest", ] TYPES = Option.TYPES + ( + "multilistidentkeyval", "identkeyval", "keyval", "unit", @@ -716,6 +776,7 @@ class CliOption(Option): "maybefloat", ) TYPE_CHECKER = Option.TYPE_CHECKER.copy() + TYPE_CHECKER["multilistidentkeyval"] = check_multilist_ident_key_val TYPE_CHECKER["identkeyval"] = check_ident_key_val TYPE_CHECKER["keyval"] = check_key_val TYPE_CHECKER["unit"] = check_unit @@ -773,6 +834,11 @@ SUBMIT_OPT = cli_option("--submit", dest="submit_only", help=("Submit the job and return the job ID, but" " don't wait for the job to finish")) +PRINT_JOBID_OPT = cli_option("--print-jobid", dest="print_jobid", + default=False, action="store_true", + help=("Additionally print the job as first line" + " on stdout (for scripting).")) + SYNC_OPT = cli_option("--sync", dest="do_locking", default=False, action="store_true", help=("Grab locks while doing the queries" @@ -905,6 +971,18 @@ SPECS_NIC_COUNT_OPT = cli_option("--specs-nic-count", dest="ispecs_nic_count", help="NIC count specs: list of key=value," " where key is one of min, max, std") +IPOLICY_BOUNDS_SPECS_STR = "--ipolicy-bounds-specs" +IPOLICY_BOUNDS_SPECS_OPT = cli_option(IPOLICY_BOUNDS_SPECS_STR, + dest="ipolicy_bounds_specs", + type="multilistidentkeyval", default=None, + help="Complete instance specs limits") + +IPOLICY_STD_SPECS_STR = "--ipolicy-std-specs" +IPOLICY_STD_SPECS_OPT = cli_option(IPOLICY_STD_SPECS_STR, + dest="ipolicy_std_specs", + type="keyval", default=None, + help="Complte standard instance specs") + IPOLICY_DISK_TEMPLATES = cli_option("--ipolicy-disk-templates", dest="ipolicy_disk_templates", type="list", default=None, @@ -1065,6 +1143,11 @@ NEW_SECONDARY_OPT = cli_option("-n", "--new-secondary", dest="dst_node", metavar="NODE", default=None, completion_suggest=OPT_COMPL_ONE_NODE) +NEW_PRIMARY_OPT = cli_option("--new-primary", dest="new_primary_node", + help="Specifies the new primary node", + metavar="", default=None, + completion_suggest=OPT_COMPL_ONE_NODE) + ON_PRIMARY_OPT = cli_option("-p", "--on-primary", dest="on_primary", default=False, action="store_true", help="Replace the disk(s) on the primary" @@ -1157,11 +1240,11 @@ ENABLED_HV_OPT = cli_option("--enabled-hypervisors", help="Comma-separated list of hypervisors", type="string", default=None) -ENABLED_STORAGE_TYPES_OPT = cli_option("--enabled-storage-types", - dest="enabled_storage_types", - help="Comma-separated list of " - "storage methods", - type="string", default=None) +ENABLED_DISK_TEMPLATES_OPT = cli_option("--enabled-disk-templates", + dest="enabled_disk_templates", + help="Comma-separated list of " + "disk templates", + type="string", default=None) NIC_PARAMS_OPT = cli_option("-N", "--nic-parameters", dest="nicparams", type="keyval", default={}, @@ -1398,8 +1481,7 @@ FAILURE_ONLY_OPT = cli_option("--failure-only", default=False, " only (determined by the exit code)")) REASON_OPT = cli_option("--reason", default=None, - help="The reason for executing a VM-state-changing" - " operation") + help="The reason for executing the command") def _PriorityOptionCb(option, _, value, parser): @@ -1552,8 +1634,19 @@ NOCONFLICTSCHECK_OPT = cli_option("--no-conflicts-check", action="store_false", help="Don't check for conflicting IPs") +INCLUDEDEFAULTS_OPT = cli_option("--include-defaults", dest="include_defaults", + default=False, action="store_true", + help="Include default values") + #: Options provided by all commands -COMMON_OPTS = [DEBUG_OPT] +COMMON_OPTS = [DEBUG_OPT, REASON_OPT] + +# options related to asynchronous job handling + +SUBMIT_OPTS = [ + SUBMIT_OPT, + PRINT_JOBID_OPT, + ] # common options for creating instances. add and import then add their own # specific ones. @@ -1575,6 +1668,7 @@ COMMON_CREATE_OPTS = [ OSPARAMS_OPT, OS_SIZE_OPT, SUBMIT_OPT, + PRINT_JOBID_OPT, TAG_ADD_OPT, DRY_RUN_OPT, PRIORITY_OPT, @@ -1582,14 +1676,19 @@ COMMON_CREATE_OPTS = [ # common instance policy options INSTANCE_POLICY_OPTS = [ + IPOLICY_BOUNDS_SPECS_OPT, + IPOLICY_DISK_TEMPLATES, + IPOLICY_VCPU_RATIO, + IPOLICY_SPINDLE_RATIO, + ] + +# instance policy split specs options +SPLIT_ISPECS_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, - IPOLICY_SPINDLE_RATIO, ] @@ -2164,6 +2263,8 @@ def SubmitOpCode(op, cl=None, feedback_fn=None, opts=None, reporter=None): SetGenericOpcodeOpts([op], opts) job_id = SendJob([op], cl=cl) + if hasattr(opts, "print_jobid") and opts.print_jobid: + ToStdout("%d" % job_id) op_results = PollJob(job_id, cl=cl, feedback_fn=feedback_fn, reporter=reporter) @@ -2187,11 +2288,38 @@ def SubmitOrSend(op, opts, cl=None, feedback_fn=None): job = [op] SetGenericOpcodeOpts(job, opts) job_id = SendJob(job, cl=cl) + if opts.print_jobid: + ToStdout("%d" % job_id) raise JobSubmittedException(job_id) else: return SubmitOpCode(op, cl=cl, feedback_fn=feedback_fn, opts=opts) +def _InitReasonTrail(op, opts): + """Builds the first part of the reason trail + + Builds the initial part of the reason trail, adding the user provided reason + (if it exists) and the name of the command starting the operation. + + @param op: the opcode the reason trail will be added to + @param opts: the command line options selected by the user + + """ + assert len(sys.argv) >= 2 + trail = [] + + if opts.reason: + trail.append((constants.OPCODE_REASON_SRC_USER, + opts.reason, + utils.EpochNano())) + + binary = os.path.basename(sys.argv[0]) + source = "%s:%s" % (constants.OPCODE_REASON_SRC_CLIENT, binary) + command = sys.argv[1] + trail.append((source, command, utils.EpochNano())) + op.reason = trail + + def SetGenericOpcodeOpts(opcode_list, options): """Processor for generic options. @@ -2211,6 +2339,7 @@ def SetGenericOpcodeOpts(opcode_list, options): op.dry_run = options.dry_run if getattr(options, "priority", None) is not None: op.priority = options.priority + _InitReasonTrail(op, options) def GetClient(query=False): @@ -2537,6 +2666,9 @@ def GenericInstanceCreate(mode, opts, args): raise errors.OpPrereqError("Invalid disk size for disk %d: %s" % (didx, err), errors.ECODE_INVAL) elif constants.IDISK_ADOPT in ddict: + if constants.IDISK_SPINDLES in ddict: + raise errors.OpPrereqError("spindles is not a valid option when" + " adopting a disk", errors.ECODE_INVAL) if mode == constants.INSTANCE_IMPORT: raise errors.OpPrereqError("Disk adoption not allowed for instance" " import", errors.ECODE_INVAL) @@ -3583,31 +3715,127 @@ class JobExecutor(object): return [row[1:3] for row in self.jobs] -def FormatParameterDict(buf, param_dict, actual, level=1): +def FormatParamsDictInfo(param_dict, actual): """Formats a parameter dictionary. - @type buf: L{StringIO} - @param buf: the buffer into which to write @type param_dict: dict @param param_dict: the own parameters @type actual: dict @param actual: the current parameter set (including defaults) - @param level: Level of indent + @rtype: dict + @return: dictionary where the value of each parameter is either a fully + formatted string or a dictionary containing formatted strings """ - indent = " " * level - - for key in sorted(actual): - data = actual[key] - buf.write("%s- %s:" % (indent, key)) - + ret = {} + for (key, data) in actual.items(): if isinstance(data, dict) and data: - buf.write("\n") - FormatParameterDict(buf, param_dict.get(key, {}), data, - level=level + 1) + ret[key] = FormatParamsDictInfo(param_dict.get(key, {}), data) else: - val = param_dict.get(key, "default (%s)" % data) - buf.write(" %s\n" % val) + ret[key] = str(param_dict.get(key, "default (%s)" % data)) + return ret + + +def _FormatListInfoDefault(data, def_data): + if data is not None: + ret = utils.CommaJoin(data) + else: + ret = "default (%s)" % utils.CommaJoin(def_data) + return ret + + +def FormatPolicyInfo(custom_ipolicy, eff_ipolicy, iscluster): + """Formats an instance policy. + + @type custom_ipolicy: dict + @param custom_ipolicy: own policy + @type eff_ipolicy: dict + @param eff_ipolicy: effective policy (including defaults); ignored for + cluster + @type iscluster: bool + @param iscluster: the policy is at cluster level + @rtype: list of pairs + @return: formatted data, suitable for L{PrintGenericInfo} + + """ + if iscluster: + eff_ipolicy = custom_ipolicy + + minmax_out = [] + custom_minmax = custom_ipolicy.get(constants.ISPECS_MINMAX) + if custom_minmax: + for (k, minmax) in enumerate(custom_minmax): + minmax_out.append([ + ("%s/%s" % (key, k), + FormatParamsDictInfo(minmax[key], minmax[key])) + for key in constants.ISPECS_MINMAX_KEYS + ]) + else: + for (k, minmax) in enumerate(eff_ipolicy[constants.ISPECS_MINMAX]): + minmax_out.append([ + ("%s/%s" % (key, k), + FormatParamsDictInfo({}, minmax[key])) + for key in constants.ISPECS_MINMAX_KEYS + ]) + ret = [("bounds specs", minmax_out)] + + if iscluster: + stdspecs = custom_ipolicy[constants.ISPECS_STD] + ret.append( + (constants.ISPECS_STD, + FormatParamsDictInfo(stdspecs, stdspecs)) + ) + + ret.append( + ("allowed disk templates", + _FormatListInfoDefault(custom_ipolicy.get(constants.IPOLICY_DTS), + eff_ipolicy[constants.IPOLICY_DTS])) + ) + ret.extend([ + (key, str(custom_ipolicy.get(key, "default (%s)" % eff_ipolicy[key]))) + for key in constants.IPOLICY_PARAMETERS + ]) + return ret + + +def _PrintSpecsParameters(buf, specs): + values = ("%s=%s" % (par, val) for (par, val) in sorted(specs.items())) + buf.write(",".join(values)) + + +def PrintIPolicyCommand(buf, ipolicy, isgroup): + """Print the command option used to generate the given instance policy. + + Currently only the parts dealing with specs are supported. + + @type buf: StringIO + @param buf: stream to write into + @type ipolicy: dict + @param ipolicy: instance policy + @type isgroup: bool + @param isgroup: whether the policy is at group level + + """ + if not isgroup: + stdspecs = ipolicy.get("std") + if stdspecs: + buf.write(" %s " % IPOLICY_STD_SPECS_STR) + _PrintSpecsParameters(buf, stdspecs) + minmaxes = ipolicy.get("minmax", []) + first = True + for minmax in minmaxes: + minspecs = minmax.get("min") + maxspecs = minmax.get("max") + if minspecs and maxspecs: + if first: + buf.write(" %s " % IPOLICY_BOUNDS_SPECS_STR) + first = False + else: + buf.write("//") + buf.write("min:") + _PrintSpecsParameters(buf, minspecs) + buf.write("/max:") + _PrintSpecsParameters(buf, maxspecs) def ConfirmOperation(names, list_type, text, extra=""): @@ -3662,24 +3890,9 @@ def _MaybeParseUnit(elements): return parsed -def CreateIPolicyFromOpts(ispecs_mem_size=None, - ispecs_cpu_count=None, - ispecs_disk_count=None, - ispecs_disk_size=None, - ispecs_nic_count=None, - ipolicy_disk_templates=None, - ipolicy_vcpu_ratio=None, - ipolicy_spindle_ratio=None, - group_ipolicy=False, - allowed_values=None, - fill_all=False): - """Creation of instance policy based on command line options. - - @param fill_all: whether for cluster policies we should ensure that - all values are filled - - - """ +def _InitISpecsFromSplitOpts(ipolicy, ispecs_mem_size, ispecs_cpu_count, + ispecs_disk_count, ispecs_disk_size, + ispecs_nic_count, group_ipolicy, fill_all): try: if ispecs_mem_size: ispecs_mem_size = _MaybeParseUnit(ispecs_mem_size) @@ -3692,7 +3905,7 @@ def CreateIPolicyFromOpts(ispecs_mem_size=None, errors.ECODE_INVAL) # prepare ipolicy dict - ipolicy_transposed = { + ispecs_transposed = { constants.ISPEC_MEM_SIZE: ispecs_mem_size, constants.ISPEC_CPU_COUNT: ispecs_cpu_count, constants.ISPEC_DISK_COUNT: ispecs_disk_count, @@ -3705,29 +3918,136 @@ def CreateIPolicyFromOpts(ispecs_mem_size=None, forced_type = TISPECS_GROUP_TYPES else: forced_type = TISPECS_CLUSTER_TYPES - - for specs in ipolicy_transposed.values(): - utils.ForceDictType(specs, forced_type, allowed_values=allowed_values) + for specs in ispecs_transposed.values(): + assert type(specs) is dict + utils.ForceDictType(specs, forced_type) # then transpose - ipolicy_out = objects.MakeEmptyIPolicy() - for name, specs in ipolicy_transposed.iteritems(): + ispecs = { + constants.ISPECS_MIN: {}, + constants.ISPECS_MAX: {}, + constants.ISPECS_STD: {}, + } + for (name, specs) in ispecs_transposed.iteritems(): assert name in constants.ISPECS_PARAMETERS for key, val in specs.items(): # {min: .. ,max: .., std: ..} - ipolicy_out[key][name] = val + assert key in ispecs + ispecs[key][name] = val + minmax_out = {} + for key in constants.ISPECS_MINMAX_KEYS: + if fill_all: + minmax_out[key] = \ + objects.FillDict(constants.ISPECS_MINMAX_DEFAULTS[key], ispecs[key]) + else: + minmax_out[key] = ispecs[key] + ipolicy[constants.ISPECS_MINMAX] = [minmax_out] + if fill_all: + ipolicy[constants.ISPECS_STD] = \ + objects.FillDict(constants.IPOLICY_DEFAULTS[constants.ISPECS_STD], + ispecs[constants.ISPECS_STD]) + else: + ipolicy[constants.ISPECS_STD] = ispecs[constants.ISPECS_STD] + + +def _ParseSpecUnit(spec, keyname): + ret = spec.copy() + for k in [constants.ISPEC_DISK_SIZE, constants.ISPEC_MEM_SIZE]: + if k in ret: + try: + ret[k] = utils.ParseUnit(ret[k]) + except (TypeError, ValueError, errors.UnitParseError), err: + raise errors.OpPrereqError(("Invalid parameter %s (%s) in %s instance" + " specs: %s" % (k, ret[k], keyname, err)), + errors.ECODE_INVAL) + return ret + + +def _ParseISpec(spec, keyname, required): + ret = _ParseSpecUnit(spec, keyname) + utils.ForceDictType(ret, constants.ISPECS_PARAMETER_TYPES) + missing = constants.ISPECS_PARAMETERS - frozenset(ret.keys()) + if required and missing: + raise errors.OpPrereqError("Missing parameters in ipolicy spec %s: %s" % + (keyname, utils.CommaJoin(missing)), + errors.ECODE_INVAL) + return ret + + +def _GetISpecsInAllowedValues(minmax_ispecs, allowed_values): + ret = None + if (minmax_ispecs and allowed_values and len(minmax_ispecs) == 1 and + len(minmax_ispecs[0]) == 1): + for (key, spec) in minmax_ispecs[0].items(): + # This loop is executed exactly once + if key in allowed_values and not spec: + ret = key + return ret + + +def _InitISpecsFromFullOpts(ipolicy_out, minmax_ispecs, std_ispecs, + group_ipolicy, allowed_values): + found_allowed = _GetISpecsInAllowedValues(minmax_ispecs, allowed_values) + if found_allowed is not None: + ipolicy_out[constants.ISPECS_MINMAX] = found_allowed + elif minmax_ispecs is not None: + minmax_out = [] + for mmpair in minmax_ispecs: + mmpair_out = {} + for (key, spec) in mmpair.items(): + if key not in constants.ISPECS_MINMAX_KEYS: + msg = "Invalid key in bounds instance specifications: %s" % key + raise errors.OpPrereqError(msg, errors.ECODE_INVAL) + mmpair_out[key] = _ParseISpec(spec, key, True) + minmax_out.append(mmpair_out) + ipolicy_out[constants.ISPECS_MINMAX] = minmax_out + if std_ispecs is not None: + assert not group_ipolicy # This is not an option for gnt-group + ipolicy_out[constants.ISPECS_STD] = _ParseISpec(std_ispecs, "std", False) + + +def CreateIPolicyFromOpts(ispecs_mem_size=None, + ispecs_cpu_count=None, + ispecs_disk_count=None, + ispecs_disk_size=None, + ispecs_nic_count=None, + minmax_ispecs=None, + std_ispecs=None, + ipolicy_disk_templates=None, + ipolicy_vcpu_ratio=None, + ipolicy_spindle_ratio=None, + group_ipolicy=False, + allowed_values=None, + fill_all=False): + """Creation of instance policy based on command line options. + + @param fill_all: whether for cluster policies we should ensure that + all values are filled + + """ + assert not (fill_all and allowed_values) + + split_specs = (ispecs_mem_size or ispecs_cpu_count or ispecs_disk_count or + ispecs_disk_size or ispecs_nic_count) + if (split_specs and (minmax_ispecs is not None or std_ispecs is not None)): + raise errors.OpPrereqError("A --specs-xxx option cannot be specified" + " together with any --ipolicy-xxx-specs option", + errors.ECODE_INVAL) + + ipolicy_out = objects.MakeEmptyIPolicy() + if split_specs: + assert fill_all + _InitISpecsFromSplitOpts(ipolicy_out, ispecs_mem_size, ispecs_cpu_count, + ispecs_disk_count, ispecs_disk_size, + ispecs_nic_count, group_ipolicy, fill_all) + elif (minmax_ispecs is not None or std_ispecs is not None): + _InitISpecsFromFullOpts(ipolicy_out, minmax_ispecs, std_ispecs, + group_ipolicy, allowed_values) - # no filldict for non-dicts - if not group_ipolicy and fill_all: - if ipolicy_disk_templates is None: - ipolicy_disk_templates = constants.DISK_TEMPLATES - if ipolicy_vcpu_ratio is None: - ipolicy_vcpu_ratio = \ - constants.IPOLICY_DEFAULTS[constants.IPOLICY_VCPU_RATIO] - if ipolicy_spindle_ratio is None: - ipolicy_spindle_ratio = \ - constants.IPOLICY_DEFAULTS[constants.IPOLICY_SPINDLE_RATIO] if ipolicy_disk_templates is not None: - ipolicy_out[constants.IPOLICY_DTS] = list(ipolicy_disk_templates) + if allowed_values and ipolicy_disk_templates in allowed_values: + ipolicy_out[constants.IPOLICY_DTS] = ipolicy_disk_templates + else: + ipolicy_out[constants.IPOLICY_DTS] = list(ipolicy_disk_templates) if ipolicy_vcpu_ratio is not None: ipolicy_out[constants.IPOLICY_VCPU_RATIO] = ipolicy_vcpu_ratio if ipolicy_spindle_ratio is not None: @@ -3735,4 +4055,97 @@ def CreateIPolicyFromOpts(ispecs_mem_size=None, assert not (frozenset(ipolicy_out.keys()) - constants.IPOLICY_ALL_KEYS) + if not group_ipolicy and fill_all: + ipolicy_out = objects.FillIPolicy(constants.IPOLICY_DEFAULTS, ipolicy_out) + return ipolicy_out + + +def _SerializeGenericInfo(buf, data, level, afterkey=False): + """Formatting core of L{PrintGenericInfo}. + + @param buf: (string) stream to accumulate the result into + @param data: data to format + @type level: int + @param level: depth in the data hierarchy, used for indenting + @type afterkey: bool + @param afterkey: True when we are in the middle of a line after a key (used + to properly add newlines or indentation) + + """ + baseind = " " + if isinstance(data, dict): + if not data: + buf.write("\n") + else: + if afterkey: + buf.write("\n") + doindent = True + else: + doindent = False + for key in sorted(data): + if doindent: + buf.write(baseind * level) + else: + doindent = True + buf.write(key) + buf.write(": ") + _SerializeGenericInfo(buf, data[key], level + 1, afterkey=True) + elif isinstance(data, list) and len(data) > 0 and isinstance(data[0], tuple): + # list of tuples (an ordered dictionary) + if afterkey: + buf.write("\n") + doindent = True + else: + doindent = False + for (key, val) in data: + if doindent: + buf.write(baseind * level) + else: + doindent = True + buf.write(key) + buf.write(": ") + _SerializeGenericInfo(buf, val, level + 1, afterkey=True) + elif isinstance(data, list): + if not data: + buf.write("\n") + else: + if afterkey: + buf.write("\n") + doindent = True + else: + doindent = False + for item in data: + if doindent: + buf.write(baseind * level) + else: + doindent = True + buf.write("-") + buf.write(baseind[1:]) + _SerializeGenericInfo(buf, item, level + 1) + else: + # This branch should be only taken for strings, but it's practically + # impossible to guarantee that no other types are produced somewhere + buf.write(str(data)) + buf.write("\n") + + +def PrintGenericInfo(data): + """Print information formatted according to the hierarchy. + + The output is a valid YAML string. + + @param data: the data to print. It's a hierarchical structure whose elements + can be: + - dictionaries, where keys are strings and values are of any of the + types listed here + - lists of pairs (key, value), where key is a string and value is of + any of the types listed here; it's a way to encode ordered + dictionaries + - lists of any of the types listed here + - strings + + """ + buf = StringIO() + _SerializeGenericInfo(buf, data, 0) + ToStdout(buf.getvalue().rstrip("\n"))