4 # Copyright (C) 2006, 2007 Google Inc.
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 # General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
22 """Module dealing with command line parsing"""
31 from cStringIO import StringIO
33 from ganeti import utils
34 from ganeti import errors
35 from ganeti import constants
36 from ganeti import opcodes
37 from ganeti import luxi
38 from ganeti import ssconf
39 from ganeti import rpc
41 from optparse import (OptionParser, TitledHelpFormatter,
42 Option, OptionValueError)
45 __all__ = ["DEBUG_OPT", "NOHDR_OPT", "SEP_OPT", "GenericMain",
46 "SubmitOpCode", "GetClient",
48 "GenerateTable", "AskUser",
49 "USEUNITS_OPT", "FIELDS_OPT", "FORCE_OPT", "SUBMIT_OPT",
50 "ListTags", "AddTags", "RemoveTags", "TAG_SRC_OPT",
51 "FormatError", "SplitNodeOption", "SubmitOrSend",
52 "JobSubmittedException", "FormatTimestamp", "ParseTimespec",
53 "ToStderr", "ToStdout", "UsesRPC",
54 "GetOnlineNodes", "JobExecutor", "SYNC_OPT", "CONFIRM_OPT",
55 "ArgJobId", "ArgSuggest", "ArgUnknown", "ArgFile", "ArgCommand",
56 "ArgInstance", "ArgNode", "ArgChoice", "ArgHost",
57 "ARGS_NONE", "ARGS_ONE_INSTANCE", "ARGS_ONE_NODE",
58 "ARGS_MANY_INSTANCES", "ARGS_MANY_NODES",
59 "OPT_COMPL_ONE_NODE", "OPT_COMPL_ONE_INSTANCE",
60 "OPT_COMPL_MANY_NODES",
61 "OPT_COMPL_ONE_OS", "OPT_COMPL_ONE_IALLOCATOR",
62 "OPT_COMPL_INST_ADD_NODES",
70 def __init__(self, min=0, max=None):
75 return ("<%s min=%s max=%s>" %
76 (self.__class__.__name__, self.min, self.max))
79 class ArgSuggest(_Argument):
80 """Suggesting argument.
82 Value can be any of the ones passed to the constructor.
85 def __init__(self, min=0, max=None, choices=None):
86 _Argument.__init__(self, min=min, max=max)
87 self.choices = choices
90 return ("<%s min=%s max=%s choices=%r>" %
91 (self.__class__.__name__, self.min, self.max, self.choices))
94 class ArgChoice(ArgSuggest):
97 Value can be any of the ones passed to the constructor. Like L{ArgSuggest},
98 but value must be one of the choices.
103 class ArgUnknown(_Argument):
104 """Unknown argument to program (e.g. determined at runtime).
109 class ArgInstance(_Argument):
110 """Instances argument.
115 class ArgNode(_Argument):
120 class ArgJobId(_Argument):
126 class ArgFile(_Argument):
127 """File path argument.
132 class ArgCommand(_Argument):
138 class ArgHost(_Argument):
145 ARGS_MANY_INSTANCES = [ArgInstance()]
146 ARGS_MANY_NODES = [ArgNode()]
147 ARGS_ONE_INSTANCE = [ArgInstance(min=1, max=1)]
148 ARGS_ONE_NODE = [ArgNode(min=1, max=1)]
152 def _ExtractTagsObject(opts, args):
153 """Extract the tag type object.
155 Note that this function will modify its args parameter.
158 if not hasattr(opts, "tag_type"):
159 raise errors.ProgrammerError("tag_type not passed to _ExtractTagsObject")
161 if kind == constants.TAG_CLUSTER:
163 elif kind == constants.TAG_NODE or kind == constants.TAG_INSTANCE:
165 raise errors.OpPrereqError("no arguments passed to the command")
169 raise errors.ProgrammerError("Unhandled tag type '%s'" % kind)
173 def _ExtendTags(opts, args):
174 """Extend the args if a source file has been given.
176 This function will extend the tags with the contents of the file
177 passed in the 'tags_source' attribute of the opts parameter. A file
178 named '-' will be replaced by stdin.
181 fname = opts.tags_source
187 new_fh = open(fname, "r")
190 # we don't use the nice 'new_data = [line.strip() for line in fh]'
191 # because of python bug 1633941
193 line = new_fh.readline()
196 new_data.append(line.strip())
199 args.extend(new_data)
202 def ListTags(opts, args):
203 """List the tags on a given object.
205 This is a generic implementation that knows how to deal with all
206 three cases of tag objects (cluster, node, instance). The opts
207 argument is expected to contain a tag_type field denoting what
208 object type we work on.
211 kind, name = _ExtractTagsObject(opts, args)
212 op = opcodes.OpGetTags(kind=kind, name=name)
213 result = SubmitOpCode(op)
214 result = list(result)
220 def AddTags(opts, args):
221 """Add tags on a given object.
223 This is a generic implementation that knows how to deal with all
224 three cases of tag objects (cluster, node, instance). The opts
225 argument is expected to contain a tag_type field denoting what
226 object type we work on.
229 kind, name = _ExtractTagsObject(opts, args)
230 _ExtendTags(opts, args)
232 raise errors.OpPrereqError("No tags to be added")
233 op = opcodes.OpAddTags(kind=kind, name=name, tags=args)
237 def RemoveTags(opts, args):
238 """Remove tags from a given object.
240 This is a generic implementation that knows how to deal with all
241 three cases of tag objects (cluster, node, instance). The opts
242 argument is expected to contain a tag_type field denoting what
243 object type we work on.
246 kind, name = _ExtractTagsObject(opts, args)
247 _ExtendTags(opts, args)
249 raise errors.OpPrereqError("No tags to be removed")
250 op = opcodes.OpDelTags(kind=kind, name=name, tags=args)
254 def check_unit(option, opt, value):
255 """OptParsers custom converter for units.
259 return utils.ParseUnit(value)
260 except errors.UnitParseError, err:
261 raise OptionValueError("option %s: %s" % (opt, err))
264 def _SplitKeyVal(opt, data):
265 """Convert a KeyVal string into a dict.
267 This function will convert a key=val[,...] string into a dict. Empty
268 values will be converted specially: keys which have the prefix 'no_'
269 will have the value=False and the prefix stripped, the others will
273 @param opt: a string holding the option name for which we process the
274 data, used in building error messages
276 @param data: a string of the format key=val,key=val,...
278 @return: {key=val, key=val}
279 @raises errors.ParameterError: if there are duplicate keys
284 for elem in data.split(","):
286 key, val = elem.split("=", 1)
288 if elem.startswith(NO_PREFIX):
289 key, val = elem[len(NO_PREFIX):], False
290 elif elem.startswith(UN_PREFIX):
291 key, val = elem[len(UN_PREFIX):], None
293 key, val = elem, True
295 raise errors.ParameterError("Duplicate key '%s' in option %s" %
301 def check_ident_key_val(option, opt, value):
302 """Custom parser for ident:key=val,key=val options.
304 This will store the parsed values as a tuple (ident, {key: val}). As such,
305 multiple uses of this option via action=append is possible.
309 ident, rest = value, ''
311 ident, rest = value.split(":", 1)
313 if ident.startswith(NO_PREFIX):
315 msg = "Cannot pass options when removing parameter groups: %s" % value
316 raise errors.ParameterError(msg)
317 retval = (ident[len(NO_PREFIX):], False)
318 elif ident.startswith(UN_PREFIX):
320 msg = "Cannot pass options when removing parameter groups: %s" % value
321 raise errors.ParameterError(msg)
322 retval = (ident[len(UN_PREFIX):], None)
324 kv_dict = _SplitKeyVal(opt, rest)
325 retval = (ident, kv_dict)
329 def check_key_val(option, opt, value):
330 """Custom parser class for key=val,key=val options.
332 This will store the parsed values as a dict {key: val}.
335 return _SplitKeyVal(opt, value)
338 # completion_suggestion is normally a list. Using numeric values not evaluating
339 # to False for dynamic completion.
340 (OPT_COMPL_MANY_NODES,
342 OPT_COMPL_ONE_INSTANCE,
344 OPT_COMPL_ONE_IALLOCATOR,
345 OPT_COMPL_INST_ADD_NODES) = range(100, 106)
347 OPT_COMPL_ALL = frozenset([
348 OPT_COMPL_MANY_NODES,
350 OPT_COMPL_ONE_INSTANCE,
352 OPT_COMPL_ONE_IALLOCATOR,
353 OPT_COMPL_INST_ADD_NODES,
357 class CliOption(Option):
358 """Custom option class for optparse.
361 ATTRS = Option.ATTRS + [
362 "completion_suggest",
364 TYPES = Option.TYPES + (
369 TYPE_CHECKER = Option.TYPE_CHECKER.copy()
370 TYPE_CHECKER["identkeyval"] = check_ident_key_val
371 TYPE_CHECKER["keyval"] = check_key_val
372 TYPE_CHECKER["unit"] = check_unit
375 # optparse.py sets make_option, so we do it for our own option class, too
376 cli_option = CliOption
379 DEBUG_OPT = cli_option("-d", "--debug", default=False,
381 help="Turn debugging on")
383 NOHDR_OPT = cli_option("--no-headers", default=False,
384 action="store_true", dest="no_headers",
385 help="Don't display column headers")
387 SEP_OPT = cli_option("--separator", default=None,
388 action="store", dest="separator",
389 help=("Separator between output fields"
390 " (defaults to one space)"))
392 USEUNITS_OPT = cli_option("--units", default=None,
393 dest="units", choices=('h', 'm', 'g', 't'),
394 help="Specify units for output (one of hmgt)")
396 FIELDS_OPT = cli_option("-o", "--output", dest="output", action="store",
397 type="string", metavar="FIELDS",
398 help="Comma separated list of output fields")
400 FORCE_OPT = cli_option("-f", "--force", dest="force", action="store_true",
401 default=False, help="Force the operation")
403 CONFIRM_OPT = cli_option("--yes", dest="confirm", action="store_true",
404 default=False, help="Do not require confirmation")
406 TAG_SRC_OPT = cli_option("--from", dest="tags_source",
407 default=None, help="File with tag names")
409 SUBMIT_OPT = cli_option("--submit", dest="submit_only",
410 default=False, action="store_true",
411 help=("Submit the job and return the job ID, but"
412 " don't wait for the job to finish"))
414 SYNC_OPT = cli_option("--sync", dest="do_locking",
415 default=False, action="store_true",
416 help=("Grab locks while doing the queries"
417 " in order to ensure more consistent results"))
419 _DRY_RUN_OPT = cli_option("--dry-run", default=False,
421 help=("Do not execute the operation, just run the"
422 " check steps and verify it it could be"
426 def _ParseArgs(argv, commands, aliases):
427 """Parser for the command line arguments.
429 This function parses the arguments and returns the function which
430 must be executed together with its (modified) arguments.
432 @param argv: the command line
433 @param commands: dictionary with special contents, see the design
434 doc for cmdline handling
435 @param aliases: dictionary with command aliases {'alias': 'target, ...}
441 binary = argv[0].split("/")[-1]
443 if len(argv) > 1 and argv[1] == "--version":
444 ToStdout("%s (ganeti) %s", binary, constants.RELEASE_VERSION)
445 # Quit right away. That way we don't have to care about this special
446 # argument. optparse.py does it the same.
449 if len(argv) < 2 or not (argv[1] in commands or
451 # let's do a nice thing
452 sortedcmds = commands.keys()
455 ToStdout("Usage: %s {command} [options...] [argument...]", binary)
456 ToStdout("%s <command> --help to see details, or man %s", binary, binary)
459 # compute the max line length for cmd + usage
460 mlen = max([len(" %s" % cmd) for cmd in commands])
461 mlen = min(60, mlen) # should not get here...
463 # and format a nice command list
464 ToStdout("Commands:")
465 for cmd in sortedcmds:
466 cmdstr = " %s" % (cmd,)
467 help_text = commands[cmd][4]
468 help_lines = textwrap.wrap(help_text, 79 - 3 - mlen)
469 ToStdout("%-*s - %s", mlen, cmdstr, help_lines.pop(0))
470 for line in help_lines:
471 ToStdout("%-*s %s", mlen, "", line)
475 return None, None, None
477 # get command, unalias it, and look it up in commands
481 raise errors.ProgrammerError("Alias '%s' overrides an existing"
484 if aliases[cmd] not in commands:
485 raise errors.ProgrammerError("Alias '%s' maps to non-existing"
486 " command '%s'" % (cmd, aliases[cmd]))
490 func, args_def, parser_opts, usage, description = commands[cmd]
491 parser = OptionParser(option_list=parser_opts + [_DRY_RUN_OPT],
492 description=description,
493 formatter=TitledHelpFormatter(),
494 usage="%%prog %s %s" % (cmd, usage))
495 parser.disable_interspersed_args()
496 options, args = parser.parse_args()
498 if not _CheckArguments(cmd, args_def, args):
499 return None, None, None
501 return func, options, args
504 def _CheckArguments(cmd, args_def, args):
505 """Verifies the arguments using the argument definition.
509 1. Abort with error if values specified by user but none expected.
511 1. For each argument in definition
513 1. Keep running count of minimum number of values (min_count)
514 1. Keep running count of maximum number of values (max_count)
515 1. If it has an unlimited number of values
517 1. Abort with error if it's not the last argument in the definition
519 1. If last argument has limited number of values
521 1. Abort with error if number of values doesn't match or is too large
523 1. Abort with error if user didn't pass enough values (min_count)
526 if args and not args_def:
527 ToStderr("Error: Command %s expects no arguments", cmd)
534 last_idx = len(args_def) - 1
536 for idx, arg in enumerate(args_def):
537 if min_count is None:
539 elif arg.min is not None:
542 if max_count is None:
544 elif arg.max is not None:
548 check_max = (arg.max is not None)
550 elif arg.max is None:
551 raise errors.ProgrammerError("Only the last argument can have max=None")
554 # Command with exact number of arguments
555 if (min_count is not None and max_count is not None and
556 min_count == max_count and len(args) != min_count):
557 ToStderr("Error: Command %s expects %d argument(s)", cmd, min_count)
560 # Command with limited number of arguments
561 if max_count is not None and len(args) > max_count:
562 ToStderr("Error: Command %s expects only %d argument(s)",
566 # Command with some required arguments
567 if min_count is not None and len(args) < min_count:
568 ToStderr("Error: Command %s expects at least %d argument(s)",
575 def SplitNodeOption(value):
576 """Splits the value of a --node option.
579 if value and ':' in value:
580 return value.split(':', 1)
586 def wrapper(*args, **kwargs):
589 return fn(*args, **kwargs)
595 def AskUser(text, choices=None):
596 """Ask the user a question.
598 @param text: the question to ask
600 @param choices: list with elements tuples (input_char, return_value,
601 description); if not given, it will default to: [('y', True,
602 'Perform the operation'), ('n', False, 'Do no do the operation')];
603 note that the '?' char is reserved for help
605 @return: one of the return values from the choices list; if input is
606 not possible (i.e. not running with a tty, we return the last
611 choices = [('y', True, 'Perform the operation'),
612 ('n', False, 'Do not perform the operation')]
613 if not choices or not isinstance(choices, list):
614 raise errors.ProgrammerError("Invalid choices argument to AskUser")
615 for entry in choices:
616 if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
617 raise errors.ProgrammerError("Invalid choices element to AskUser")
619 answer = choices[-1][1]
621 for line in text.splitlines():
622 new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
623 text = "\n".join(new_text)
625 f = file("/dev/tty", "a+")
629 chars = [entry[0] for entry in choices]
630 chars[-1] = "[%s]" % chars[-1]
632 maps = dict([(entry[0], entry[1]) for entry in choices])
636 f.write("/".join(chars))
638 line = f.readline(2).strip().lower()
643 for entry in choices:
644 f.write(" %s - %s\n" % (entry[0], entry[2]))
652 class JobSubmittedException(Exception):
653 """Job was submitted, client should exit.
655 This exception has one argument, the ID of the job that was
656 submitted. The handler should print this ID.
658 This is not an error, just a structured way to exit from clients.
663 def SendJob(ops, cl=None):
664 """Function to submit an opcode without waiting for the results.
667 @param ops: list of opcodes
668 @type cl: luxi.Client
669 @param cl: the luxi client to use for communicating with the master;
670 if None, a new client will be created
676 job_id = cl.SubmitJob(ops)
681 def PollJob(job_id, cl=None, feedback_fn=None):
682 """Function to poll for the result of a job.
684 @type job_id: job identified
685 @param job_id: the job to poll for results
686 @type cl: luxi.Client
687 @param cl: the luxi client to use for communicating with the master;
688 if None, a new client will be created
695 prev_logmsg_serial = None
698 result = cl.WaitForJobChange(job_id, ["status"], prev_job_info,
701 # job not found, go away!
702 raise errors.JobLost("Job with id %s lost" % job_id)
704 # Split result, a tuple of (field values, log entries)
705 (job_info, log_entries) = result
706 (status, ) = job_info
709 for log_entry in log_entries:
710 (serial, timestamp, _, message) = log_entry
711 if callable(feedback_fn):
712 feedback_fn(log_entry[1:])
714 encoded = utils.SafeEncode(message)
715 ToStdout("%s %s", time.ctime(utils.MergeTime(timestamp)), encoded)
716 prev_logmsg_serial = max(prev_logmsg_serial, serial)
718 # TODO: Handle canceled and archived jobs
719 elif status in (constants.JOB_STATUS_SUCCESS,
720 constants.JOB_STATUS_ERROR,
721 constants.JOB_STATUS_CANCELING,
722 constants.JOB_STATUS_CANCELED):
725 prev_job_info = job_info
727 jobs = cl.QueryJobs([job_id], ["status", "opstatus", "opresult"])
729 raise errors.JobLost("Job with id %s lost" % job_id)
731 status, opstatus, result = jobs[0]
732 if status == constants.JOB_STATUS_SUCCESS:
734 elif status in (constants.JOB_STATUS_CANCELING,
735 constants.JOB_STATUS_CANCELED):
736 raise errors.OpExecError("Job was canceled")
739 for idx, (status, msg) in enumerate(zip(opstatus, result)):
740 if status == constants.OP_STATUS_SUCCESS:
742 elif status == constants.OP_STATUS_ERROR:
743 errors.MaybeRaise(msg)
745 raise errors.OpExecError("partial failure (opcode %d): %s" %
748 raise errors.OpExecError(str(msg))
749 # default failure mode
750 raise errors.OpExecError(result)
753 def SubmitOpCode(op, cl=None, feedback_fn=None):
754 """Legacy function to submit an opcode.
756 This is just a simple wrapper over the construction of the processor
757 instance. It should be extended to better handle feedback and
758 interaction functions.
764 job_id = SendJob([op], cl)
766 op_results = PollJob(job_id, cl=cl, feedback_fn=feedback_fn)
771 def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
772 """Wrapper around SubmitOpCode or SendJob.
774 This function will decide, based on the 'opts' parameter, whether to
775 submit and wait for the result of the opcode (and return it), or
776 whether to just send the job and print its identifier. It is used in
777 order to simplify the implementation of the '--submit' option.
779 It will also add the dry-run parameter from the options passed, if true.
782 if opts and opts.dry_run:
783 op.dry_run = opts.dry_run
784 if opts and opts.submit_only:
785 job_id = SendJob([op], cl=cl)
786 raise JobSubmittedException(job_id)
788 return SubmitOpCode(op, cl=cl, feedback_fn=feedback_fn)
792 # TODO: Cache object?
794 client = luxi.Client()
795 except luxi.NoMasterError:
796 master, myself = ssconf.GetMasterAndMyself()
798 raise errors.OpPrereqError("This is not the master node, please connect"
799 " to node '%s' and rerun the command" %
806 def FormatError(err):
807 """Return a formatted error message for a given error.
809 This function takes an exception instance and returns a tuple
810 consisting of two values: first, the recommended exit code, and
811 second, a string describing the error message (not
818 if isinstance(err, errors.ConfigurationError):
819 txt = "Corrupt configuration file: %s" % msg
821 obuf.write(txt + "\n")
822 obuf.write("Aborting.")
824 elif isinstance(err, errors.HooksAbort):
825 obuf.write("Failure: hooks execution failed:\n")
826 for node, script, out in err.args[0]:
828 obuf.write(" node: %s, script: %s, output: %s\n" %
831 obuf.write(" node: %s, script: %s (no output)\n" %
833 elif isinstance(err, errors.HooksFailure):
834 obuf.write("Failure: hooks general failure: %s" % msg)
835 elif isinstance(err, errors.ResolverError):
836 this_host = utils.HostInfo.SysName()
837 if err.args[0] == this_host:
838 msg = "Failure: can't resolve my own hostname ('%s')"
840 msg = "Failure: can't resolve hostname '%s'"
841 obuf.write(msg % err.args[0])
842 elif isinstance(err, errors.OpPrereqError):
843 obuf.write("Failure: prerequisites not met for this"
844 " operation:\n%s" % msg)
845 elif isinstance(err, errors.OpExecError):
846 obuf.write("Failure: command execution error:\n%s" % msg)
847 elif isinstance(err, errors.TagError):
848 obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
849 elif isinstance(err, errors.JobQueueDrainError):
850 obuf.write("Failure: the job queue is marked for drain and doesn't"
851 " accept new requests\n")
852 elif isinstance(err, errors.JobQueueFull):
853 obuf.write("Failure: the job queue is full and doesn't accept new"
854 " job submissions until old jobs are archived\n")
855 elif isinstance(err, errors.TypeEnforcementError):
856 obuf.write("Parameter Error: %s" % msg)
857 elif isinstance(err, errors.ParameterError):
858 obuf.write("Failure: unknown/wrong parameter name '%s'" % msg)
859 elif isinstance(err, errors.GenericError):
860 obuf.write("Unhandled Ganeti error: %s" % msg)
861 elif isinstance(err, luxi.NoMasterError):
862 obuf.write("Cannot communicate with the master daemon.\nIs it running"
863 " and listening for connections?")
864 elif isinstance(err, luxi.TimeoutError):
865 obuf.write("Timeout while talking to the master daemon. Error:\n"
867 elif isinstance(err, luxi.ProtocolError):
868 obuf.write("Unhandled protocol error while talking to the master daemon:\n"
870 elif isinstance(err, JobSubmittedException):
871 obuf.write("JobID: %s\n" % err.args[0])
874 obuf.write("Unhandled exception: %s" % msg)
875 return retcode, obuf.getvalue().rstrip('\n')
878 def GenericMain(commands, override=None, aliases=None):
879 """Generic main function for all the gnt-* commands.
882 - commands: a dictionary with a special structure, see the design doc
883 for command line handling.
884 - override: if not None, we expect a dictionary with keys that will
885 override command line options; this can be used to pass
886 options from the scripts to generic functions
887 - aliases: dictionary with command aliases {'alias': 'target, ...}
890 # save the program name and the entire command line for later logging
892 binary = os.path.basename(sys.argv[0]) or sys.argv[0]
893 if len(sys.argv) >= 2:
894 binary += " " + sys.argv[1]
895 old_cmdline = " ".join(sys.argv[2:])
899 binary = "<unknown program>"
906 func, options, args = _ParseArgs(sys.argv, commands, aliases)
907 except errors.ParameterError, err:
908 result, err_msg = FormatError(err)
912 if func is None: # parse error
915 if override is not None:
916 for key, val in override.iteritems():
917 setattr(options, key, val)
919 utils.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
920 stderr_logging=True, program=binary)
923 logging.info("run with arguments '%s'", old_cmdline)
925 logging.info("run with no arguments")
928 result = func(options, args)
929 except (errors.GenericError, luxi.ProtocolError,
930 JobSubmittedException), err:
931 result, err_msg = FormatError(err)
932 logging.exception("Error during command processing")
938 def GenerateTable(headers, fields, separator, data,
939 numfields=None, unitfields=None,
941 """Prints a table with headers and different fields.
944 @param headers: dictionary mapping field names to headers for
947 @param fields: the field names corresponding to each row in
949 @param separator: the separator to be used; if this is None,
950 the default 'smart' algorithm is used which computes optimal
951 field width, otherwise just the separator is used between
954 @param data: a list of lists, each sublist being one row to be output
955 @type numfields: list
956 @param numfields: a list with the fields that hold numeric
957 values and thus should be right-aligned
958 @type unitfields: list
959 @param unitfields: a list with the fields that hold numeric
960 values that should be formatted with the units field
961 @type units: string or None
962 @param units: the units we should use for formatting, or None for
963 automatic choice (human-readable for non-separator usage, otherwise
964 megabytes); this is a one-letter string
973 if numfields is None:
975 if unitfields is None:
978 numfields = utils.FieldSet(*numfields)
979 unitfields = utils.FieldSet(*unitfields)
983 if headers and field not in headers:
984 # TODO: handle better unknown fields (either revert to old
985 # style of raising exception, or deal more intelligently with
987 headers[field] = field
988 if separator is not None:
989 format_fields.append("%s")
990 elif numfields.Matches(field):
991 format_fields.append("%*s")
993 format_fields.append("%-*s")
995 if separator is None:
996 mlens = [0 for name in fields]
997 format = ' '.join(format_fields)
999 format = separator.replace("%", "%%").join(format_fields)
1004 for idx, val in enumerate(row):
1005 if unitfields.Matches(fields[idx]):
1011 val = row[idx] = utils.FormatUnit(val, units)
1012 val = row[idx] = str(val)
1013 if separator is None:
1014 mlens[idx] = max(mlens[idx], len(val))
1019 for idx, name in enumerate(fields):
1021 if separator is None:
1022 mlens[idx] = max(mlens[idx], len(hdr))
1023 args.append(mlens[idx])
1025 result.append(format % tuple(args))
1030 line = ['-' for _ in fields]
1031 for idx in xrange(len(fields)):
1032 if separator is None:
1033 args.append(mlens[idx])
1034 args.append(line[idx])
1035 result.append(format % tuple(args))
1040 def FormatTimestamp(ts):
1041 """Formats a given timestamp.
1044 @param ts: a timeval-type timestamp, a tuple of seconds and microseconds
1047 @return: a string with the formatted timestamp
1050 if not isinstance (ts, (tuple, list)) or len(ts) != 2:
1053 return time.strftime("%F %T", time.localtime(sec)) + ".%06d" % usec
1056 def ParseTimespec(value):
1057 """Parse a time specification.
1059 The following suffixed will be recognized:
1067 Without any suffix, the value will be taken to be in seconds.
1072 raise errors.OpPrereqError("Empty time specification passed")
1080 if value[-1] not in suffix_map:
1084 raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1086 multiplier = suffix_map[value[-1]]
1088 if not value: # no data left after stripping the suffix
1089 raise errors.OpPrereqError("Invalid time specification (only"
1092 value = int(value) * multiplier
1094 raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1098 def GetOnlineNodes(nodes, cl=None, nowarn=False):
1099 """Returns the names of online nodes.
1101 This function will also log a warning on stderr with the names of
1104 @param nodes: if not empty, use only this subset of nodes (minus the
1106 @param cl: if not None, luxi client to use
1107 @type nowarn: boolean
1108 @param nowarn: by default, this function will output a note with the
1109 offline nodes that are skipped; if this parameter is True the
1110 note is not displayed
1116 result = cl.QueryNodes(names=nodes, fields=["name", "offline"],
1118 offline = [row[0] for row in result if row[1]]
1119 if offline and not nowarn:
1120 ToStderr("Note: skipping offline node(s): %s" % ", ".join(offline))
1121 return [row[0] for row in result if not row[1]]
1124 def _ToStream(stream, txt, *args):
1125 """Write a message to a stream, bypassing the logging system
1127 @type stream: file object
1128 @param stream: the file to which we should write
1130 @param txt: the message
1135 stream.write(txt % args)
1142 def ToStdout(txt, *args):
1143 """Write a message to stdout only, bypassing the logging system
1145 This is just a wrapper over _ToStream.
1148 @param txt: the message
1151 _ToStream(sys.stdout, txt, *args)
1154 def ToStderr(txt, *args):
1155 """Write a message to stderr only, bypassing the logging system
1157 This is just a wrapper over _ToStream.
1160 @param txt: the message
1163 _ToStream(sys.stderr, txt, *args)
1166 class JobExecutor(object):
1167 """Class which manages the submission and execution of multiple jobs.
1169 Note that instances of this class should not be reused between
1173 def __init__(self, cl=None, verbose=True):
1178 self.verbose = verbose
1181 def QueueJob(self, name, *ops):
1182 """Record a job for later submit.
1185 @param name: a description of the job, will be used in WaitJobSet
1187 self.queue.append((name, ops))
1189 def SubmitPending(self):
1190 """Submit all pending jobs.
1193 results = self.cl.SubmitManyJobs([row[1] for row in self.queue])
1194 for ((status, data), (name, _)) in zip(results, self.queue):
1195 self.jobs.append((status, data, name))
1197 def GetResults(self):
1198 """Wait for and return the results of all jobs.
1201 @return: list of tuples (success, job results), in the same order
1202 as the submitted jobs; if a job has failed, instead of the result
1203 there will be the error message
1207 self.SubmitPending()
1210 ok_jobs = [row[1] for row in self.jobs if row[0]]
1212 ToStdout("Submitted jobs %s", ", ".join(ok_jobs))
1213 for submit_status, jid, name in self.jobs:
1214 if not submit_status:
1215 ToStderr("Failed to submit job for %s: %s", name, jid)
1216 results.append((False, jid))
1219 ToStdout("Waiting for job %s for %s...", jid, name)
1221 job_result = PollJob(jid, cl=self.cl)
1223 except (errors.GenericError, luxi.ProtocolError), err:
1224 _, job_result = FormatError(err)
1226 # the error message will always be shown, verbose or not
1227 ToStderr("Job %s for %s has failed: %s", jid, name, job_result)
1229 results.append((success, job_result))
1232 def WaitOrShow(self, wait):
1233 """Wait for job results or only print the job IDs.
1236 @param wait: whether to wait or not
1240 return self.GetResults()
1243 self.SubmitPending()
1244 for status, result, name in self.jobs:
1246 ToStdout("%s: %s", result, name)
1248 ToStderr("Failure for %s: %s", name, result)