X-Git-Url: https://code.grnet.gr/git/ganeti-local/blobdiff_plain/aa06f8c68704d2f9dd2a70361fd13916039734ff..b82d4c5e4e5751f12b61a71b16592fb20b8f822e:/lib/cli.py diff --git a/lib/cli.py b/lib/cli.py index e2d5702..ce97e99 100644 --- a/lib/cli.py +++ b/lib/cli.py @@ -1,7 +1,7 @@ # # -# Copyright (C) 2006, 2007, 2008, 2009, 2010 Google Inc. +# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011 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 @@ -39,6 +39,7 @@ from ganeti import rpc from ganeti import ssh from ganeti import compat from ganeti import netutils +from ganeti import qlang from optparse import (OptionParser, TitledHelpFormatter, Option, OptionValueError) @@ -48,10 +49,14 @@ __all__ = [ # Command line options "ADD_UIDS_OPT", "ALLOCATABLE_OPT", + "ALLOC_POLICY_OPT", "ALL_OPT", "AUTO_PROMOTE_OPT", "AUTO_REPLACE_OPT", "BACKEND_OPT", + "BLK_OS_OPT", + "CAPAB_MASTER_OPT", + "CAPAB_VM_OPT", "CLEANUP_OPT", "CLUSTER_DOMAIN_SECRET_OPT", "CONFIRM_OPT", @@ -73,6 +78,7 @@ __all__ = [ "FORCE_OPT", "FORCE_VARIANT_OPT", "GLOBAL_FILEDIR_OPT", + "HID_OS_OPT", "HVLIST_OPT", "HVOPTS_OPT", "HYPERVISOR_OPT", @@ -81,6 +87,7 @@ __all__ = [ "IDENTIFY_DEFAULTS_OPT", "IGNORE_CONSIST_OPT", "IGNORE_FAILURES_OPT", + "IGNORE_OFFLINE_OPT", "IGNORE_REMOVE_FAILURES_OPT", "IGNORE_SECONDARIES_OPT", "IGNORE_SIZE_OPT", @@ -100,6 +107,8 @@ __all__ = [ "NODE_LIST_OPT", "NODE_PLACEMENT_OPT", "NODEGROUP_OPT", + "NODE_PARAMS_OPT", + "NODE_POWERED_OPT", "NODRBD_STORAGE_OPT", "NOHDR_OPT", "NOIPCHECK_OPT", @@ -122,6 +131,7 @@ __all__ = [ "OSPARAMS_OPT", "OS_OPT", "OS_SIZE_OPT", + "PREALLOC_WIPE_DISKS_OPT", "PRIMARY_IP_VERSION_OPT", "PRIORITY_OPT", "RAPI_CERT_OPT", @@ -153,6 +163,8 @@ __all__ = [ # Generic functions for CLI programs "GenericMain", "GenericInstanceCreate", + "GenericList", + "GenericListFields", "GetClient", "GetOnlineNodes", "JobExecutor", @@ -165,6 +177,8 @@ __all__ = [ # Formatting functions "ToStderr", "ToStdout", "FormatError", + "FormatQueryResult", + "FormatParameterDict", "GenerateTable", "AskUser", "FormatTimestamp", @@ -176,13 +190,16 @@ __all__ = [ # command line options support infrastructure "ARGS_MANY_INSTANCES", "ARGS_MANY_NODES", + "ARGS_MANY_GROUPS", "ARGS_NONE", "ARGS_ONE_INSTANCE", "ARGS_ONE_NODE", + "ARGS_ONE_GROUP", "ARGS_ONE_OS", "ArgChoice", "ArgCommand", "ArgFile", + "ArgGroup", "ArgHost", "ArgInstance", "ArgJobId", @@ -201,6 +218,7 @@ __all__ = [ "SplitNodeOption", "CalculateOSNames", "ParseFields", + "COMMON_CREATE_OPTS", ] NO_PREFIX = "no_" @@ -218,6 +236,11 @@ _PRIORITY_NAMES = [ # we migrate to Python 2.6 _PRIONAME_TO_VALUE = dict(_PRIORITY_NAMES) +# Query result status for clients +(QR_NORMAL, + QR_UNKNOWN, + QR_INCOMPLETE) = range(3) + class _Argument: def __init__(self, min=0, max=None): # pylint: disable-msg=W0622 @@ -271,6 +294,13 @@ class ArgNode(_Argument): """ + +class ArgGroup(_Argument): + """Node group argument. + + """ + + class ArgJobId(_Argument): """Job ID argument. @@ -304,8 +334,10 @@ class ArgOs(_Argument): ARGS_NONE = [] ARGS_MANY_INSTANCES = [ArgInstance()] ARGS_MANY_NODES = [ArgNode()] +ARGS_MANY_GROUPS = [ArgGroup()] ARGS_ONE_INSTANCE = [ArgInstance(min=1, max=1)] ARGS_ONE_NODE = [ArgNode(min=1, max=1)] +ARGS_ONE_GROUP = [ArgInstance(min=1, max=1)] ARGS_ONE_OS = [ArgOs(min=1, max=1)] @@ -390,8 +422,8 @@ def AddTags(opts, args): _ExtendTags(opts, args) if not args: raise errors.OpPrereqError("No tags to be added") - op = opcodes.OpAddTags(kind=kind, name=name, tags=args) - SubmitOpCode(op) + op = opcodes.OpTagsSet(kind=kind, name=name, tags=args) + SubmitOpCode(op, opts=opts) def RemoveTags(opts, args): @@ -407,8 +439,8 @@ def RemoveTags(opts, args): _ExtendTags(opts, args) if not args: raise errors.OpPrereqError("No tags to be removed") - op = opcodes.OpDelTags(kind=kind, name=name, tags=args) - SubmitOpCode(op) + op = opcodes.OpTagsDel(kind=kind, name=name, tags=args) + SubmitOpCode(op, opts=opts) def check_unit(option, opt, value): # pylint: disable-msg=W0613 @@ -571,7 +603,7 @@ SEP_OPT = cli_option("--separator", default=None, USEUNITS_OPT = cli_option("--units", default=None, dest="units", choices=('h', 'm', 'g', 't'), - help="Specify units for output (one of hmgt)") + help="Specify units for output (one of h/m/g/t)") FIELDS_OPT = cli_option("-o", "--output", dest="output", action="store", type="string", metavar="FIELDS", @@ -583,6 +615,11 @@ FORCE_OPT = cli_option("-f", "--force", dest="force", action="store_true", CONFIRM_OPT = cli_option("--yes", dest="confirm", action="store_true", default=False, help="Do not require confirmation") +IGNORE_OFFLINE_OPT = cli_option("--ignore-offline", dest="ignore_offline", + action="store_true", default=False, + help=("Ignore offline nodes and do as much" + " as possible")) + TAG_SRC_OPT = cli_option("--from", dest="tags_source", default=None, help="File with tag names") @@ -738,7 +775,7 @@ NODE_LIST_OPT = cli_option("-n", "--node", dest="nodes", default=[], " times, if not given defaults to all nodes)", completion_suggest=OPT_COMPL_ONE_NODE) -NODEGROUP_OPT = cli_option("-g", "--nodegroup", +NODEGROUP_OPT = cli_option("-g", "--node-group", dest="nodegroup", help="Node group (name or uuid)", metavar="", @@ -848,18 +885,28 @@ NOSSH_KEYCHECK_OPT = cli_option("--no-ssh-key-check", dest="ssh_key_check", default=True, action="store_false", help="Disable SSH key fingerprint checking") - MC_OPT = cli_option("-C", "--master-candidate", dest="master_candidate", type="bool", default=None, metavar=_YORNO, help="Set the master_candidate flag on the node") OFFLINE_OPT = cli_option("-O", "--offline", dest="offline", metavar=_YORNO, type="bool", default=None, - help="Set the offline flag on the node") + help=("Set the offline flag on the node" + " (cluster does not communicate with offline" + " nodes)")) DRAINED_OPT = cli_option("-D", "--drained", dest="drained", metavar=_YORNO, type="bool", default=None, - help="Set the drained flag on the node") + help=("Set the drained flag on the node" + " (excluded from allocation operations)")) + +CAPAB_MASTER_OPT = cli_option("--master-capable", dest="master_capable", + type="bool", default=None, metavar=_YORNO, + help="Set the master_capable flag on the node") + +CAPAB_VM_OPT = cli_option("--vm-capable", dest="vm_capable", + type="bool", default=None, metavar=_YORNO, + help="Set the vm_capable flag on the node") ALLOCATABLE_OPT = cli_option("--allocatable", dest="allocatable", type="bool", default=None, metavar=_YORNO, @@ -883,9 +930,10 @@ CP_SIZE_OPT = cli_option("-C", "--candidate-pool-size", default=None, dest="candidate_pool_size", type="int", help="Set the candidate pool size") -VG_NAME_OPT = cli_option("-g", "--vg-name", dest="vg_name", - help="Enables LVM and specifies the volume group" - " name (cluster-wide) for disk allocation [xenvg]", +VG_NAME_OPT = cli_option("--vg-name", dest="vg_name", + help=("Enables LVM and specifies the volume group" + " name (cluster-wide) for disk allocation" + " [%s]" % constants.DEFAULT_VG), metavar="VG", default=None) YES_DOIT_OPT = cli_option("--yes-do-it", dest="yes_do_it", @@ -903,10 +951,11 @@ MAC_PREFIX_OPT = cli_option("-m", "--mac-prefix", dest="mac_prefix", MASTER_NETDEV_OPT = cli_option("--master-netdev", dest="master_netdev", help="Specify the node interface (cluster-wide)" - " on which the master IP address will be added " - " [%s]" % constants.DEFAULT_BRIDGE, + " on which the master IP address will be added" + " (cluster init default: %s)" % + constants.DEFAULT_BRIDGE, metavar="NETDEV", - default=constants.DEFAULT_BRIDGE) + default=None) GLOBAL_FILEDIR_OPT = cli_option("--file-storage-dir", dest="file_storage_dir", help="Specify the default directory (cluster-" @@ -1066,9 +1115,60 @@ PRIORITY_OPT = cli_option("--priority", default=None, dest="priority", choices=_PRIONAME_TO_VALUE.keys(), help="Priority for opcode processing") +HID_OS_OPT = cli_option("--hidden", dest="hidden", + type="bool", default=None, metavar=_YORNO, + help="Sets the hidden flag on the OS") + +BLK_OS_OPT = cli_option("--blacklisted", dest="blacklisted", + type="bool", default=None, metavar=_YORNO, + help="Sets the blacklisted flag on the OS") + +PREALLOC_WIPE_DISKS_OPT = cli_option("--prealloc-wipe-disks", default=None, + type="bool", metavar=_YORNO, + dest="prealloc_wipe_disks", + help=("Wipe disks prior to instance" + " creation")) + +NODE_PARAMS_OPT = cli_option("--node-parameters", dest="ndparams", + type="keyval", default=None, + help="Node parameters") + +ALLOC_POLICY_OPT = cli_option("--alloc-policy", dest="alloc_policy", + action="store", metavar="POLICY", default=None, + help="Allocation policy for the node group") + +NODE_POWERED_OPT = cli_option("--node-powered", default=None, + type="bool", metavar=_YORNO, + dest="node_powered", + help="Specify if the SoR for node is powered") + + #: Options provided by all commands COMMON_OPTS = [DEBUG_OPT] +# common options for creating instances. add and import then add their own +# specific ones. +COMMON_CREATE_OPTS = [ + BACKEND_OPT, + DISK_OPT, + DISK_TEMPLATE_OPT, + FILESTORE_DIR_OPT, + FILESTORE_DRIVER_OPT, + HYPERVISOR_OPT, + IALLOCATOR_OPT, + NET_OPT, + NODE_PLACEMENT_OPT, + NOIPCHECK_OPT, + NONAMECHECK_OPT, + NONICS_OPT, + NWSYNC_OPT, + OSPARAMS_OPT, + OS_SIZE_OPT, + SUBMIT_OPT, + DRY_RUN_OPT, + PRIORITY_OPT, + ] + def _ParseArgs(argv, commands, aliases): """Parser for the command line arguments. @@ -1749,8 +1849,11 @@ def FormatError(err): obuf.write("Cannot communicate with the master daemon.\nIs it running" " and listening for connections?") elif isinstance(err, luxi.TimeoutError): - obuf.write("Timeout while talking to the master daemon. Error:\n" - "%s" % msg) + obuf.write("Timeout while talking to the master daemon. Jobs might have" + " been submitted and will continue to run even if the call" + " timed out. Useful commands in this situation are \"gnt-job" + " list\", \"gnt-job cancel\" and \"gnt-job watch\". Error:\n") + obuf.write(msg) elif isinstance(err, luxi.PermissionError): obuf.write("It seems you don't have permissions to connect to the" " master daemon.\nPlease retry as a different user.") @@ -1829,6 +1932,30 @@ def GenericMain(commands, override=None, aliases=None): return result +def ParseNicOption(optvalue): + """Parses the value of the --net option(s). + + """ + try: + nic_max = max(int(nidx[0]) + 1 for nidx in optvalue) + except (TypeError, ValueError), err: + raise errors.OpPrereqError("Invalid NIC index passed: %s" % str(err)) + + nics = [{}] * nic_max + for nidx, ndict in optvalue: + nidx = int(nidx) + + if not isinstance(ndict, dict): + raise errors.OpPrereqError("Invalid nic/%d value: expected dict," + " got %s" % (nidx, ndict)) + + utils.ForceDictType(ndict, constants.INIC_PARAMS_TYPES) + + nics[nidx] = ndict + + return nics + + def GenericInstanceCreate(mode, opts, args): """Add an instance to the cluster via either creation or import. @@ -1850,17 +1977,7 @@ def GenericInstanceCreate(mode, opts, args): hypervisor, hvparams = opts.hypervisor if opts.nics: - try: - nic_max = max(int(nidx[0]) + 1 for nidx in opts.nics) - except ValueError, err: - raise errors.OpPrereqError("Invalid NIC index passed: %s" % str(err)) - nics = [{}] * nic_max - for nidx, ndict in opts.nics: - nidx = int(nidx) - if not isinstance(ndict, dict): - msg = "Invalid nic/%d value: expected dict, got %s" % (nidx, ndict) - raise errors.OpPrereqError(msg) - nics[nidx] = ndict + nics = ParseNicOption(opts.nics) elif opts.no_nics: # no nics nics = [] @@ -1940,7 +2057,7 @@ def GenericInstanceCreate(mode, opts, args): else: raise errors.ProgrammerError("Invalid creation mode %s" % mode) - op = opcodes.OpCreateInstance(instance_name=instance, + op = opcodes.OpInstanceCreate(instance_name=instance, disks=disks, disk_template=opts.disk_template, nics=nics, @@ -2199,6 +2316,358 @@ def GenerateTable(headers, fields, separator, data, return result +def _FormatBool(value): + """Formats a boolean value as a string. + + """ + if value: + return "Y" + return "N" + + +#: Default formatting for query results; (callback, align right) +_DEFAULT_FORMAT_QUERY = { + constants.QFT_TEXT: (str, False), + constants.QFT_BOOL: (_FormatBool, False), + constants.QFT_NUMBER: (str, True), + constants.QFT_TIMESTAMP: (utils.FormatTime, False), + constants.QFT_OTHER: (str, False), + constants.QFT_UNKNOWN: (str, False), + } + + +def _GetColumnFormatter(fdef, override, unit): + """Returns formatting function for a field. + + @type fdef: L{objects.QueryFieldDefinition} + @type override: dict + @param override: Dictionary for overriding field formatting functions, + indexed by field name, contents like L{_DEFAULT_FORMAT_QUERY} + @type unit: string + @param unit: Unit used for formatting fields of type L{constants.QFT_UNIT} + @rtype: tuple; (callable, bool) + @return: Returns the function to format a value (takes one parameter) and a + boolean for aligning the value on the right-hand side + + """ + fmt = override.get(fdef.name, None) + if fmt is not None: + return fmt + + assert constants.QFT_UNIT not in _DEFAULT_FORMAT_QUERY + + if fdef.kind == constants.QFT_UNIT: + # Can't keep this information in the static dictionary + return (lambda value: utils.FormatUnit(value, unit), True) + + fmt = _DEFAULT_FORMAT_QUERY.get(fdef.kind, None) + if fmt is not None: + return fmt + + raise NotImplementedError("Can't format column type '%s'" % fdef.kind) + + +class _QueryColumnFormatter: + """Callable class for formatting fields of a query. + + """ + def __init__(self, fn, status_fn): + """Initializes this class. + + @type fn: callable + @param fn: Formatting function + @type status_fn: callable + @param status_fn: Function to report fields' status + + """ + self._fn = fn + self._status_fn = status_fn + + def __call__(self, data): + """Returns a field's string representation. + + """ + (status, value) = data + + # Report status + self._status_fn(status) + + if status == constants.RS_NORMAL: + return self._fn(value) + + assert value is None, \ + "Found value %r for abnormal status %s" % (value, status) + + if status == constants.RS_UNKNOWN: + return "(unknown)" + + if status == constants.RS_NODATA: + return "(nodata)" + + if status == constants.RS_UNAVAIL: + return "(unavail)" + + if status == constants.RS_OFFLINE: + return "(offline)" + + raise NotImplementedError("Unknown status %s" % status) + + +def FormatQueryResult(result, unit=None, format_override=None, separator=None, + header=False): + """Formats data in L{objects.QueryResponse}. + + @type result: L{objects.QueryResponse} + @param result: result of query operation + @type unit: string + @param unit: Unit used for formatting fields of type L{constants.QFT_UNIT}, + see L{utils.text.FormatUnit} + @type format_override: dict + @param format_override: Dictionary for overriding field formatting functions, + indexed by field name, contents like L{_DEFAULT_FORMAT_QUERY} + @type separator: string or None + @param separator: String used to separate fields + @type header: bool + @param header: Whether to output header row + + """ + if unit is None: + if separator: + unit = "m" + else: + unit = "h" + + if format_override is None: + format_override = {} + + stats = dict.fromkeys(constants.RS_ALL, 0) + + def _RecordStatus(status): + if status in stats: + stats[status] += 1 + + columns = [] + for fdef in result.fields: + assert fdef.title and fdef.name + (fn, align_right) = _GetColumnFormatter(fdef, format_override, unit) + columns.append(TableColumn(fdef.title, + _QueryColumnFormatter(fn, _RecordStatus), + align_right)) + + table = FormatTable(result.data, columns, header, separator) + + # Collect statistics + assert len(stats) == len(constants.RS_ALL) + assert compat.all(count >= 0 for count in stats.values()) + + # Determine overall status. If there was no data, unknown fields must be + # detected via the field definitions. + if (stats[constants.RS_UNKNOWN] or + (not result.data and _GetUnknownFields(result.fields))): + status = QR_UNKNOWN + elif compat.any(count > 0 for key, count in stats.items() + if key != constants.RS_NORMAL): + status = QR_INCOMPLETE + else: + status = QR_NORMAL + + return (status, table) + + +def _GetUnknownFields(fdefs): + """Returns list of unknown fields included in C{fdefs}. + + @type fdefs: list of L{objects.QueryFieldDefinition} + + """ + return [fdef for fdef in fdefs + if fdef.kind == constants.QFT_UNKNOWN] + + +def _WarnUnknownFields(fdefs): + """Prints a warning to stderr if a query included unknown fields. + + @type fdefs: list of L{objects.QueryFieldDefinition} + + """ + unknown = _GetUnknownFields(fdefs) + if unknown: + ToStderr("Warning: Queried for unknown fields %s", + utils.CommaJoin(fdef.name for fdef in unknown)) + return True + + return False + + +def GenericList(resource, fields, names, unit, separator, header, cl=None, + format_override=None): + """Generic implementation for listing all items of a resource. + + @param resource: One of L{constants.QR_OP_LUXI} + @type fields: list of strings + @param fields: List of fields to query for + @type names: list of strings + @param names: Names of items to query for + @type unit: string or None + @param unit: Unit used for formatting fields of type L{constants.QFT_UNIT} or + None for automatic choice (human-readable for non-separator usage, + otherwise megabytes); this is a one-letter string + @type separator: string or None + @param separator: String used to separate fields + @type header: bool + @param header: Whether to show header row + @type format_override: dict + @param format_override: Dictionary for overriding field formatting functions, + indexed by field name, contents like L{_DEFAULT_FORMAT_QUERY} + + """ + if cl is None: + cl = GetClient() + + if not names: + names = None + + response = cl.Query(resource, fields, qlang.MakeSimpleFilter("name", names)) + + found_unknown = _WarnUnknownFields(response.fields) + + (status, data) = FormatQueryResult(response, unit=unit, separator=separator, + header=header, + format_override=format_override) + + for line in data: + ToStdout(line) + + assert ((found_unknown and status == QR_UNKNOWN) or + (not found_unknown and status != QR_UNKNOWN)) + + if status == QR_UNKNOWN: + return constants.EXIT_UNKNOWN_FIELD + + # TODO: Should the list command fail if not all data could be collected? + return constants.EXIT_SUCCESS + + +def GenericListFields(resource, fields, separator, header, cl=None): + """Generic implementation for listing fields for a resource. + + @param resource: One of L{constants.QR_OP_LUXI} + @type fields: list of strings + @param fields: List of fields to query for + @type separator: string or None + @param separator: String used to separate fields + @type header: bool + @param header: Whether to show header row + + """ + if cl is None: + cl = GetClient() + + if not fields: + fields = None + + response = cl.QueryFields(resource, fields) + + found_unknown = _WarnUnknownFields(response.fields) + + columns = [ + TableColumn("Name", str, False), + TableColumn("Title", str, False), + # TODO: Add field description to master daemon + ] + + rows = [[fdef.name, fdef.title] for fdef in response.fields] + + for line in FormatTable(rows, columns, header, separator): + ToStdout(line) + + if found_unknown: + return constants.EXIT_UNKNOWN_FIELD + + return constants.EXIT_SUCCESS + + +class TableColumn: + """Describes a column for L{FormatTable}. + + """ + def __init__(self, title, fn, align_right): + """Initializes this class. + + @type title: string + @param title: Column title + @type fn: callable + @param fn: Formatting function + @type align_right: bool + @param align_right: Whether to align values on the right-hand side + + """ + self.title = title + self.format = fn + self.align_right = align_right + + +def _GetColFormatString(width, align_right): + """Returns the format string for a field. + + """ + if align_right: + sign = "" + else: + sign = "-" + + return "%%%s%ss" % (sign, width) + + +def FormatTable(rows, columns, header, separator): + """Formats data as a table. + + @type rows: list of lists + @param rows: Row data, one list per row + @type columns: list of L{TableColumn} + @param columns: Column descriptions + @type header: bool + @param header: Whether to show header row + @type separator: string or None + @param separator: String used to separate columns + + """ + if header: + data = [[col.title for col in columns]] + colwidth = [len(col.title) for col in columns] + else: + data = [] + colwidth = [0 for _ in columns] + + # Format row data + for row in rows: + assert len(row) == len(columns) + + formatted = [col.format(value) for value, col in zip(row, columns)] + + if separator is None: + # Update column widths + for idx, (oldwidth, value) in enumerate(zip(colwidth, formatted)): + # Modifying a list's items while iterating is fine + colwidth[idx] = max(oldwidth, len(value)) + + data.append(formatted) + + if separator is not None: + # Return early if a separator is used + return [separator.join(row) for row in data] + + if columns and not columns[-1].align_right: + # Avoid unnecessary spaces at end of line + colwidth[-1] = 0 + + # Build format string + fmt = " ".join([_GetColFormatString(width, col.align_right) + for col, width in zip(columns, colwidth)]) + + return [fmt % tuple(row) for row in data] + + def FormatTimestamp(ts): """Formats a given timestamp. @@ -2476,3 +2945,21 @@ class JobExecutor(object): else: ToStderr("Failure for %s: %s", name, result) return [row[1:3] for row in self.jobs] + + +def FormatParameterDict(buf, param_dict, actual, level=1): + """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 + + """ + indent = " " * level + for key in sorted(actual): + val = param_dict.get(key, "default (%s)" % actual[key]) + buf.write("%s- %s: %s\n" % (indent, key, val))