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)
46 # Command line options
58 # Generic functions for CLI programs
63 "JobSubmittedException",
68 # Formatting functions
69 "ToStderr", "ToStdout",
78 # command line options support infrastructure
79 "ARGS_MANY_INSTANCES",
93 "OPT_COMPL_INST_ADD_NODES",
94 "OPT_COMPL_MANY_NODES",
95 "OPT_COMPL_ONE_IALLOCATOR",
96 "OPT_COMPL_ONE_INSTANCE",
108 def __init__(self, min=0, max=None):
113 return ("<%s min=%s max=%s>" %
114 (self.__class__.__name__, self.min, self.max))
117 class ArgSuggest(_Argument):
118 """Suggesting argument.
120 Value can be any of the ones passed to the constructor.
123 def __init__(self, min=0, max=None, choices=None):
124 _Argument.__init__(self, min=min, max=max)
125 self.choices = choices
128 return ("<%s min=%s max=%s choices=%r>" %
129 (self.__class__.__name__, self.min, self.max, self.choices))
132 class ArgChoice(ArgSuggest):
135 Value can be any of the ones passed to the constructor. Like L{ArgSuggest},
136 but value must be one of the choices.
141 class ArgUnknown(_Argument):
142 """Unknown argument to program (e.g. determined at runtime).
147 class ArgInstance(_Argument):
148 """Instances argument.
153 class ArgNode(_Argument):
158 class ArgJobId(_Argument):
164 class ArgFile(_Argument):
165 """File path argument.
170 class ArgCommand(_Argument):
176 class ArgHost(_Argument):
183 ARGS_MANY_INSTANCES = [ArgInstance()]
184 ARGS_MANY_NODES = [ArgNode()]
185 ARGS_ONE_INSTANCE = [ArgInstance(min=1, max=1)]
186 ARGS_ONE_NODE = [ArgNode(min=1, max=1)]
190 def _ExtractTagsObject(opts, args):
191 """Extract the tag type object.
193 Note that this function will modify its args parameter.
196 if not hasattr(opts, "tag_type"):
197 raise errors.ProgrammerError("tag_type not passed to _ExtractTagsObject")
199 if kind == constants.TAG_CLUSTER:
201 elif kind == constants.TAG_NODE or kind == constants.TAG_INSTANCE:
203 raise errors.OpPrereqError("no arguments passed to the command")
207 raise errors.ProgrammerError("Unhandled tag type '%s'" % kind)
211 def _ExtendTags(opts, args):
212 """Extend the args if a source file has been given.
214 This function will extend the tags with the contents of the file
215 passed in the 'tags_source' attribute of the opts parameter. A file
216 named '-' will be replaced by stdin.
219 fname = opts.tags_source
225 new_fh = open(fname, "r")
228 # we don't use the nice 'new_data = [line.strip() for line in fh]'
229 # because of python bug 1633941
231 line = new_fh.readline()
234 new_data.append(line.strip())
237 args.extend(new_data)
240 def ListTags(opts, args):
241 """List the tags on a given object.
243 This is a generic implementation that knows how to deal with all
244 three cases of tag objects (cluster, node, instance). The opts
245 argument is expected to contain a tag_type field denoting what
246 object type we work on.
249 kind, name = _ExtractTagsObject(opts, args)
250 op = opcodes.OpGetTags(kind=kind, name=name)
251 result = SubmitOpCode(op)
252 result = list(result)
258 def AddTags(opts, args):
259 """Add tags on a given object.
261 This is a generic implementation that knows how to deal with all
262 three cases of tag objects (cluster, node, instance). The opts
263 argument is expected to contain a tag_type field denoting what
264 object type we work on.
267 kind, name = _ExtractTagsObject(opts, args)
268 _ExtendTags(opts, args)
270 raise errors.OpPrereqError("No tags to be added")
271 op = opcodes.OpAddTags(kind=kind, name=name, tags=args)
275 def RemoveTags(opts, args):
276 """Remove tags from a given object.
278 This is a generic implementation that knows how to deal with all
279 three cases of tag objects (cluster, node, instance). The opts
280 argument is expected to contain a tag_type field denoting what
281 object type we work on.
284 kind, name = _ExtractTagsObject(opts, args)
285 _ExtendTags(opts, args)
287 raise errors.OpPrereqError("No tags to be removed")
288 op = opcodes.OpDelTags(kind=kind, name=name, tags=args)
292 def check_unit(option, opt, value):
293 """OptParsers custom converter for units.
297 return utils.ParseUnit(value)
298 except errors.UnitParseError, err:
299 raise OptionValueError("option %s: %s" % (opt, err))
302 def _SplitKeyVal(opt, data):
303 """Convert a KeyVal string into a dict.
305 This function will convert a key=val[,...] string into a dict. Empty
306 values will be converted specially: keys which have the prefix 'no_'
307 will have the value=False and the prefix stripped, the others will
311 @param opt: a string holding the option name for which we process the
312 data, used in building error messages
314 @param data: a string of the format key=val,key=val,...
316 @return: {key=val, key=val}
317 @raises errors.ParameterError: if there are duplicate keys
322 for elem in data.split(","):
324 key, val = elem.split("=", 1)
326 if elem.startswith(NO_PREFIX):
327 key, val = elem[len(NO_PREFIX):], False
328 elif elem.startswith(UN_PREFIX):
329 key, val = elem[len(UN_PREFIX):], None
331 key, val = elem, True
333 raise errors.ParameterError("Duplicate key '%s' in option %s" %
339 def check_ident_key_val(option, opt, value):
340 """Custom parser for ident:key=val,key=val options.
342 This will store the parsed values as a tuple (ident, {key: val}). As such,
343 multiple uses of this option via action=append is possible.
347 ident, rest = value, ''
349 ident, rest = value.split(":", 1)
351 if ident.startswith(NO_PREFIX):
353 msg = "Cannot pass options when removing parameter groups: %s" % value
354 raise errors.ParameterError(msg)
355 retval = (ident[len(NO_PREFIX):], False)
356 elif ident.startswith(UN_PREFIX):
358 msg = "Cannot pass options when removing parameter groups: %s" % value
359 raise errors.ParameterError(msg)
360 retval = (ident[len(UN_PREFIX):], None)
362 kv_dict = _SplitKeyVal(opt, rest)
363 retval = (ident, kv_dict)
367 def check_key_val(option, opt, value):
368 """Custom parser class for key=val,key=val options.
370 This will store the parsed values as a dict {key: val}.
373 return _SplitKeyVal(opt, value)
376 # completion_suggestion is normally a list. Using numeric values not evaluating
377 # to False for dynamic completion.
378 (OPT_COMPL_MANY_NODES,
380 OPT_COMPL_ONE_INSTANCE,
382 OPT_COMPL_ONE_IALLOCATOR,
383 OPT_COMPL_INST_ADD_NODES) = range(100, 106)
385 OPT_COMPL_ALL = frozenset([
386 OPT_COMPL_MANY_NODES,
388 OPT_COMPL_ONE_INSTANCE,
390 OPT_COMPL_ONE_IALLOCATOR,
391 OPT_COMPL_INST_ADD_NODES,
395 class CliOption(Option):
396 """Custom option class for optparse.
399 ATTRS = Option.ATTRS + [
400 "completion_suggest",
402 TYPES = Option.TYPES + (
407 TYPE_CHECKER = Option.TYPE_CHECKER.copy()
408 TYPE_CHECKER["identkeyval"] = check_ident_key_val
409 TYPE_CHECKER["keyval"] = check_key_val
410 TYPE_CHECKER["unit"] = check_unit
413 # optparse.py sets make_option, so we do it for our own option class, too
414 cli_option = CliOption
417 DEBUG_OPT = cli_option("-d", "--debug", default=False,
419 help="Turn debugging on")
421 NOHDR_OPT = cli_option("--no-headers", default=False,
422 action="store_true", dest="no_headers",
423 help="Don't display column headers")
425 SEP_OPT = cli_option("--separator", default=None,
426 action="store", dest="separator",
427 help=("Separator between output fields"
428 " (defaults to one space)"))
430 USEUNITS_OPT = cli_option("--units", default=None,
431 dest="units", choices=('h', 'm', 'g', 't'),
432 help="Specify units for output (one of hmgt)")
434 FIELDS_OPT = cli_option("-o", "--output", dest="output", action="store",
435 type="string", metavar="FIELDS",
436 help="Comma separated list of output fields")
438 FORCE_OPT = cli_option("-f", "--force", dest="force", action="store_true",
439 default=False, help="Force the operation")
441 CONFIRM_OPT = cli_option("--yes", dest="confirm", action="store_true",
442 default=False, help="Do not require confirmation")
444 TAG_SRC_OPT = cli_option("--from", dest="tags_source",
445 default=None, help="File with tag names")
447 SUBMIT_OPT = cli_option("--submit", dest="submit_only",
448 default=False, action="store_true",
449 help=("Submit the job and return the job ID, but"
450 " don't wait for the job to finish"))
452 SYNC_OPT = cli_option("--sync", dest="do_locking",
453 default=False, action="store_true",
454 help=("Grab locks while doing the queries"
455 " in order to ensure more consistent results"))
457 _DRY_RUN_OPT = cli_option("--dry-run", default=False,
459 help=("Do not execute the operation, just run the"
460 " check steps and verify it it could be"
463 VERBOSE_OPT = cli_option("-v", "--verbose", default=False,
465 help="Increase the verbosity of the operation")
468 def _ParseArgs(argv, commands, aliases):
469 """Parser for the command line arguments.
471 This function parses the arguments and returns the function which
472 must be executed together with its (modified) arguments.
474 @param argv: the command line
475 @param commands: dictionary with special contents, see the design
476 doc for cmdline handling
477 @param aliases: dictionary with command aliases {'alias': 'target, ...}
483 binary = argv[0].split("/")[-1]
485 if len(argv) > 1 and argv[1] == "--version":
486 ToStdout("%s (ganeti) %s", binary, constants.RELEASE_VERSION)
487 # Quit right away. That way we don't have to care about this special
488 # argument. optparse.py does it the same.
491 if len(argv) < 2 or not (argv[1] in commands or
493 # let's do a nice thing
494 sortedcmds = commands.keys()
497 ToStdout("Usage: %s {command} [options...] [argument...]", binary)
498 ToStdout("%s <command> --help to see details, or man %s", binary, binary)
501 # compute the max line length for cmd + usage
502 mlen = max([len(" %s" % cmd) for cmd in commands])
503 mlen = min(60, mlen) # should not get here...
505 # and format a nice command list
506 ToStdout("Commands:")
507 for cmd in sortedcmds:
508 cmdstr = " %s" % (cmd,)
509 help_text = commands[cmd][4]
510 help_lines = textwrap.wrap(help_text, 79 - 3 - mlen)
511 ToStdout("%-*s - %s", mlen, cmdstr, help_lines.pop(0))
512 for line in help_lines:
513 ToStdout("%-*s %s", mlen, "", line)
517 return None, None, None
519 # get command, unalias it, and look it up in commands
523 raise errors.ProgrammerError("Alias '%s' overrides an existing"
526 if aliases[cmd] not in commands:
527 raise errors.ProgrammerError("Alias '%s' maps to non-existing"
528 " command '%s'" % (cmd, aliases[cmd]))
532 func, args_def, parser_opts, usage, description = commands[cmd]
533 parser = OptionParser(option_list=parser_opts + [_DRY_RUN_OPT],
534 description=description,
535 formatter=TitledHelpFormatter(),
536 usage="%%prog %s %s" % (cmd, usage))
537 parser.disable_interspersed_args()
538 options, args = parser.parse_args()
540 if not _CheckArguments(cmd, args_def, args):
541 return None, None, None
543 return func, options, args
546 def _CheckArguments(cmd, args_def, args):
547 """Verifies the arguments using the argument definition.
551 1. Abort with error if values specified by user but none expected.
553 1. For each argument in definition
555 1. Keep running count of minimum number of values (min_count)
556 1. Keep running count of maximum number of values (max_count)
557 1. If it has an unlimited number of values
559 1. Abort with error if it's not the last argument in the definition
561 1. If last argument has limited number of values
563 1. Abort with error if number of values doesn't match or is too large
565 1. Abort with error if user didn't pass enough values (min_count)
568 if args and not args_def:
569 ToStderr("Error: Command %s expects no arguments", cmd)
576 last_idx = len(args_def) - 1
578 for idx, arg in enumerate(args_def):
579 if min_count is None:
581 elif arg.min is not None:
584 if max_count is None:
586 elif arg.max is not None:
590 check_max = (arg.max is not None)
592 elif arg.max is None:
593 raise errors.ProgrammerError("Only the last argument can have max=None")
596 # Command with exact number of arguments
597 if (min_count is not None and max_count is not None and
598 min_count == max_count and len(args) != min_count):
599 ToStderr("Error: Command %s expects %d argument(s)", cmd, min_count)
602 # Command with limited number of arguments
603 if max_count is not None and len(args) > max_count:
604 ToStderr("Error: Command %s expects only %d argument(s)",
608 # Command with some required arguments
609 if min_count is not None and len(args) < min_count:
610 ToStderr("Error: Command %s expects at least %d argument(s)",
617 def SplitNodeOption(value):
618 """Splits the value of a --node option.
621 if value and ':' in value:
622 return value.split(':', 1)
628 def wrapper(*args, **kwargs):
631 return fn(*args, **kwargs)
637 def AskUser(text, choices=None):
638 """Ask the user a question.
640 @param text: the question to ask
642 @param choices: list with elements tuples (input_char, return_value,
643 description); if not given, it will default to: [('y', True,
644 'Perform the operation'), ('n', False, 'Do no do the operation')];
645 note that the '?' char is reserved for help
647 @return: one of the return values from the choices list; if input is
648 not possible (i.e. not running with a tty, we return the last
653 choices = [('y', True, 'Perform the operation'),
654 ('n', False, 'Do not perform the operation')]
655 if not choices or not isinstance(choices, list):
656 raise errors.ProgrammerError("Invalid choices argument to AskUser")
657 for entry in choices:
658 if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
659 raise errors.ProgrammerError("Invalid choices element to AskUser")
661 answer = choices[-1][1]
663 for line in text.splitlines():
664 new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
665 text = "\n".join(new_text)
667 f = file("/dev/tty", "a+")
671 chars = [entry[0] for entry in choices]
672 chars[-1] = "[%s]" % chars[-1]
674 maps = dict([(entry[0], entry[1]) for entry in choices])
678 f.write("/".join(chars))
680 line = f.readline(2).strip().lower()
685 for entry in choices:
686 f.write(" %s - %s\n" % (entry[0], entry[2]))
694 class JobSubmittedException(Exception):
695 """Job was submitted, client should exit.
697 This exception has one argument, the ID of the job that was
698 submitted. The handler should print this ID.
700 This is not an error, just a structured way to exit from clients.
705 def SendJob(ops, cl=None):
706 """Function to submit an opcode without waiting for the results.
709 @param ops: list of opcodes
710 @type cl: luxi.Client
711 @param cl: the luxi client to use for communicating with the master;
712 if None, a new client will be created
718 job_id = cl.SubmitJob(ops)
723 def PollJob(job_id, cl=None, feedback_fn=None):
724 """Function to poll for the result of a job.
726 @type job_id: job identified
727 @param job_id: the job to poll for results
728 @type cl: luxi.Client
729 @param cl: the luxi client to use for communicating with the master;
730 if None, a new client will be created
737 prev_logmsg_serial = None
740 result = cl.WaitForJobChange(job_id, ["status"], prev_job_info,
743 # job not found, go away!
744 raise errors.JobLost("Job with id %s lost" % job_id)
746 # Split result, a tuple of (field values, log entries)
747 (job_info, log_entries) = result
748 (status, ) = job_info
751 for log_entry in log_entries:
752 (serial, timestamp, _, message) = log_entry
753 if callable(feedback_fn):
754 feedback_fn(log_entry[1:])
756 encoded = utils.SafeEncode(message)
757 ToStdout("%s %s", time.ctime(utils.MergeTime(timestamp)), encoded)
758 prev_logmsg_serial = max(prev_logmsg_serial, serial)
760 # TODO: Handle canceled and archived jobs
761 elif status in (constants.JOB_STATUS_SUCCESS,
762 constants.JOB_STATUS_ERROR,
763 constants.JOB_STATUS_CANCELING,
764 constants.JOB_STATUS_CANCELED):
767 prev_job_info = job_info
769 jobs = cl.QueryJobs([job_id], ["status", "opstatus", "opresult"])
771 raise errors.JobLost("Job with id %s lost" % job_id)
773 status, opstatus, result = jobs[0]
774 if status == constants.JOB_STATUS_SUCCESS:
776 elif status in (constants.JOB_STATUS_CANCELING,
777 constants.JOB_STATUS_CANCELED):
778 raise errors.OpExecError("Job was canceled")
781 for idx, (status, msg) in enumerate(zip(opstatus, result)):
782 if status == constants.OP_STATUS_SUCCESS:
784 elif status == constants.OP_STATUS_ERROR:
785 errors.MaybeRaise(msg)
787 raise errors.OpExecError("partial failure (opcode %d): %s" %
790 raise errors.OpExecError(str(msg))
791 # default failure mode
792 raise errors.OpExecError(result)
795 def SubmitOpCode(op, cl=None, feedback_fn=None):
796 """Legacy function to submit an opcode.
798 This is just a simple wrapper over the construction of the processor
799 instance. It should be extended to better handle feedback and
800 interaction functions.
806 job_id = SendJob([op], cl)
808 op_results = PollJob(job_id, cl=cl, feedback_fn=feedback_fn)
813 def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
814 """Wrapper around SubmitOpCode or SendJob.
816 This function will decide, based on the 'opts' parameter, whether to
817 submit and wait for the result of the opcode (and return it), or
818 whether to just send the job and print its identifier. It is used in
819 order to simplify the implementation of the '--submit' option.
821 It will also add the dry-run parameter from the options passed, if true.
824 if opts and opts.dry_run:
825 op.dry_run = opts.dry_run
826 if opts and opts.submit_only:
827 job_id = SendJob([op], cl=cl)
828 raise JobSubmittedException(job_id)
830 return SubmitOpCode(op, cl=cl, feedback_fn=feedback_fn)
834 # TODO: Cache object?
836 client = luxi.Client()
837 except luxi.NoMasterError:
838 master, myself = ssconf.GetMasterAndMyself()
840 raise errors.OpPrereqError("This is not the master node, please connect"
841 " to node '%s' and rerun the command" %
848 def FormatError(err):
849 """Return a formatted error message for a given error.
851 This function takes an exception instance and returns a tuple
852 consisting of two values: first, the recommended exit code, and
853 second, a string describing the error message (not
860 if isinstance(err, errors.ConfigurationError):
861 txt = "Corrupt configuration file: %s" % msg
863 obuf.write(txt + "\n")
864 obuf.write("Aborting.")
866 elif isinstance(err, errors.HooksAbort):
867 obuf.write("Failure: hooks execution failed:\n")
868 for node, script, out in err.args[0]:
870 obuf.write(" node: %s, script: %s, output: %s\n" %
873 obuf.write(" node: %s, script: %s (no output)\n" %
875 elif isinstance(err, errors.HooksFailure):
876 obuf.write("Failure: hooks general failure: %s" % msg)
877 elif isinstance(err, errors.ResolverError):
878 this_host = utils.HostInfo.SysName()
879 if err.args[0] == this_host:
880 msg = "Failure: can't resolve my own hostname ('%s')"
882 msg = "Failure: can't resolve hostname '%s'"
883 obuf.write(msg % err.args[0])
884 elif isinstance(err, errors.OpPrereqError):
885 obuf.write("Failure: prerequisites not met for this"
886 " operation:\n%s" % msg)
887 elif isinstance(err, errors.OpExecError):
888 obuf.write("Failure: command execution error:\n%s" % msg)
889 elif isinstance(err, errors.TagError):
890 obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
891 elif isinstance(err, errors.JobQueueDrainError):
892 obuf.write("Failure: the job queue is marked for drain and doesn't"
893 " accept new requests\n")
894 elif isinstance(err, errors.JobQueueFull):
895 obuf.write("Failure: the job queue is full and doesn't accept new"
896 " job submissions until old jobs are archived\n")
897 elif isinstance(err, errors.TypeEnforcementError):
898 obuf.write("Parameter Error: %s" % msg)
899 elif isinstance(err, errors.ParameterError):
900 obuf.write("Failure: unknown/wrong parameter name '%s'" % msg)
901 elif isinstance(err, errors.GenericError):
902 obuf.write("Unhandled Ganeti error: %s" % msg)
903 elif isinstance(err, luxi.NoMasterError):
904 obuf.write("Cannot communicate with the master daemon.\nIs it running"
905 " and listening for connections?")
906 elif isinstance(err, luxi.TimeoutError):
907 obuf.write("Timeout while talking to the master daemon. Error:\n"
909 elif isinstance(err, luxi.ProtocolError):
910 obuf.write("Unhandled protocol error while talking to the master daemon:\n"
912 elif isinstance(err, JobSubmittedException):
913 obuf.write("JobID: %s\n" % err.args[0])
916 obuf.write("Unhandled exception: %s" % msg)
917 return retcode, obuf.getvalue().rstrip('\n')
920 def GenericMain(commands, override=None, aliases=None):
921 """Generic main function for all the gnt-* commands.
924 - commands: a dictionary with a special structure, see the design doc
925 for command line handling.
926 - override: if not None, we expect a dictionary with keys that will
927 override command line options; this can be used to pass
928 options from the scripts to generic functions
929 - aliases: dictionary with command aliases {'alias': 'target, ...}
932 # save the program name and the entire command line for later logging
934 binary = os.path.basename(sys.argv[0]) or sys.argv[0]
935 if len(sys.argv) >= 2:
936 binary += " " + sys.argv[1]
937 old_cmdline = " ".join(sys.argv[2:])
941 binary = "<unknown program>"
948 func, options, args = _ParseArgs(sys.argv, commands, aliases)
949 except errors.ParameterError, err:
950 result, err_msg = FormatError(err)
954 if func is None: # parse error
957 if override is not None:
958 for key, val in override.iteritems():
959 setattr(options, key, val)
961 utils.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
962 stderr_logging=True, program=binary)
965 logging.info("run with arguments '%s'", old_cmdline)
967 logging.info("run with no arguments")
970 result = func(options, args)
971 except (errors.GenericError, luxi.ProtocolError,
972 JobSubmittedException), err:
973 result, err_msg = FormatError(err)
974 logging.exception("Error during command processing")
980 def GenerateTable(headers, fields, separator, data,
981 numfields=None, unitfields=None,
983 """Prints a table with headers and different fields.
986 @param headers: dictionary mapping field names to headers for
989 @param fields: the field names corresponding to each row in
991 @param separator: the separator to be used; if this is None,
992 the default 'smart' algorithm is used which computes optimal
993 field width, otherwise just the separator is used between
996 @param data: a list of lists, each sublist being one row to be output
997 @type numfields: list
998 @param numfields: a list with the fields that hold numeric
999 values and thus should be right-aligned
1000 @type unitfields: list
1001 @param unitfields: a list with the fields that hold numeric
1002 values that should be formatted with the units field
1003 @type units: string or None
1004 @param units: the units we should use for formatting, or None for
1005 automatic choice (human-readable for non-separator usage, otherwise
1006 megabytes); this is a one-letter string
1015 if numfields is None:
1017 if unitfields is None:
1020 numfields = utils.FieldSet(*numfields)
1021 unitfields = utils.FieldSet(*unitfields)
1024 for field in fields:
1025 if headers and field not in headers:
1026 # TODO: handle better unknown fields (either revert to old
1027 # style of raising exception, or deal more intelligently with
1029 headers[field] = field
1030 if separator is not None:
1031 format_fields.append("%s")
1032 elif numfields.Matches(field):
1033 format_fields.append("%*s")
1035 format_fields.append("%-*s")
1037 if separator is None:
1038 mlens = [0 for name in fields]
1039 format = ' '.join(format_fields)
1041 format = separator.replace("%", "%%").join(format_fields)
1046 for idx, val in enumerate(row):
1047 if unitfields.Matches(fields[idx]):
1053 val = row[idx] = utils.FormatUnit(val, units)
1054 val = row[idx] = str(val)
1055 if separator is None:
1056 mlens[idx] = max(mlens[idx], len(val))
1061 for idx, name in enumerate(fields):
1063 if separator is None:
1064 mlens[idx] = max(mlens[idx], len(hdr))
1065 args.append(mlens[idx])
1067 result.append(format % tuple(args))
1072 line = ['-' for _ in fields]
1073 for idx in xrange(len(fields)):
1074 if separator is None:
1075 args.append(mlens[idx])
1076 args.append(line[idx])
1077 result.append(format % tuple(args))
1082 def FormatTimestamp(ts):
1083 """Formats a given timestamp.
1086 @param ts: a timeval-type timestamp, a tuple of seconds and microseconds
1089 @return: a string with the formatted timestamp
1092 if not isinstance (ts, (tuple, list)) or len(ts) != 2:
1095 return time.strftime("%F %T", time.localtime(sec)) + ".%06d" % usec
1098 def ParseTimespec(value):
1099 """Parse a time specification.
1101 The following suffixed will be recognized:
1109 Without any suffix, the value will be taken to be in seconds.
1114 raise errors.OpPrereqError("Empty time specification passed")
1122 if value[-1] not in suffix_map:
1126 raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1128 multiplier = suffix_map[value[-1]]
1130 if not value: # no data left after stripping the suffix
1131 raise errors.OpPrereqError("Invalid time specification (only"
1134 value = int(value) * multiplier
1136 raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1140 def GetOnlineNodes(nodes, cl=None, nowarn=False):
1141 """Returns the names of online nodes.
1143 This function will also log a warning on stderr with the names of
1146 @param nodes: if not empty, use only this subset of nodes (minus the
1148 @param cl: if not None, luxi client to use
1149 @type nowarn: boolean
1150 @param nowarn: by default, this function will output a note with the
1151 offline nodes that are skipped; if this parameter is True the
1152 note is not displayed
1158 result = cl.QueryNodes(names=nodes, fields=["name", "offline"],
1160 offline = [row[0] for row in result if row[1]]
1161 if offline and not nowarn:
1162 ToStderr("Note: skipping offline node(s): %s" % ", ".join(offline))
1163 return [row[0] for row in result if not row[1]]
1166 def _ToStream(stream, txt, *args):
1167 """Write a message to a stream, bypassing the logging system
1169 @type stream: file object
1170 @param stream: the file to which we should write
1172 @param txt: the message
1177 stream.write(txt % args)
1184 def ToStdout(txt, *args):
1185 """Write a message to stdout only, bypassing the logging system
1187 This is just a wrapper over _ToStream.
1190 @param txt: the message
1193 _ToStream(sys.stdout, txt, *args)
1196 def ToStderr(txt, *args):
1197 """Write a message to stderr only, bypassing the logging system
1199 This is just a wrapper over _ToStream.
1202 @param txt: the message
1205 _ToStream(sys.stderr, txt, *args)
1208 class JobExecutor(object):
1209 """Class which manages the submission and execution of multiple jobs.
1211 Note that instances of this class should not be reused between
1215 def __init__(self, cl=None, verbose=True):
1220 self.verbose = verbose
1223 def QueueJob(self, name, *ops):
1224 """Record a job for later submit.
1227 @param name: a description of the job, will be used in WaitJobSet
1229 self.queue.append((name, ops))
1231 def SubmitPending(self):
1232 """Submit all pending jobs.
1235 results = self.cl.SubmitManyJobs([row[1] for row in self.queue])
1236 for ((status, data), (name, _)) in zip(results, self.queue):
1237 self.jobs.append((status, data, name))
1239 def GetResults(self):
1240 """Wait for and return the results of all jobs.
1243 @return: list of tuples (success, job results), in the same order
1244 as the submitted jobs; if a job has failed, instead of the result
1245 there will be the error message
1249 self.SubmitPending()
1252 ok_jobs = [row[1] for row in self.jobs if row[0]]
1254 ToStdout("Submitted jobs %s", ", ".join(ok_jobs))
1255 for submit_status, jid, name in self.jobs:
1256 if not submit_status:
1257 ToStderr("Failed to submit job for %s: %s", name, jid)
1258 results.append((False, jid))
1261 ToStdout("Waiting for job %s for %s...", jid, name)
1263 job_result = PollJob(jid, cl=self.cl)
1265 except (errors.GenericError, luxi.ProtocolError), err:
1266 _, job_result = FormatError(err)
1268 # the error message will always be shown, verbose or not
1269 ToStderr("Job %s for %s has failed: %s", jid, name, job_result)
1271 results.append((success, job_result))
1274 def WaitOrShow(self, wait):
1275 """Wait for job results or only print the job IDs.
1278 @param wait: whether to wait or not
1282 return self.GetResults()
1285 self.SubmitPending()
1286 for status, result, name in self.jobs:
1288 ToStdout("%s: %s", result, name)
1290 ToStderr("Failure for %s: %s", name, result)