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, make_option, TitledHelpFormatter,
42 Option, OptionValueError)
45 __all__ = ["DEBUG_OPT", "NOHDR_OPT", "SEP_OPT", "GenericMain",
46 "SubmitOpCode", "GetClient",
48 "GenerateTable", "AskUser",
49 "ARGS_NONE", "ARGS_FIXED", "ARGS_ATLEAST", "ARGS_ANY", "ARGS_ONE",
50 "USEUNITS_OPT", "FIELDS_OPT", "FORCE_OPT", "SUBMIT_OPT",
51 "ListTags", "AddTags", "RemoveTags", "TAG_SRC_OPT",
52 "FormatError", "SplitNodeOption", "SubmitOrSend",
53 "JobSubmittedException", "FormatTimestamp", "ParseTimespec",
54 "ToStderr", "ToStdout", "UsesRPC",
55 "GetOnlineNodes", "JobExecutor", "SYNC_OPT", "CONFIRM_OPT",
56 "ArgJobId", "ArgSuggest", "ArgUnknown", "ArgFile", "ArgCommand",
57 "ArgInstance", "ArgNode", "ArgChoice",
65 def __init__(self, min=0, max=None, suggest=None):
70 return ("<%s min=%s max=%s>" %
71 (self.__class__.__name__, self.min, self.max))
74 class ArgSuggest(_Argument):
75 """Suggesting argument.
77 Value can be any of the ones passed to the constructor.
80 def __init__(self, min=0, max=None, choices=None):
81 _Argument.__init__(self, min=min, max=max)
82 self.choices = choices
85 return ("<%s min=%s max=%s choices=%r>" %
86 (self.__class__.__name__, self.min, self.max, self.choices))
89 class ArgChoice(ArgSuggest):
92 Value can be any of the ones passed to the constructor. Like L{ArgSuggest},
93 but value must be one of the choices.
98 class ArgUnknown(_Argument):
99 """Unknown argument to program (e.g. determined at runtime).
104 class ArgInstance(_Argument):
105 """Instances argument.
110 class ArgNode(_Argument):
115 class ArgJobId(_Argument):
121 class ArgFile(_Argument):
122 """File path argument.
127 class ArgCommand(_Argument):
133 def _ExtractTagsObject(opts, args):
134 """Extract the tag type object.
136 Note that this function will modify its args parameter.
139 if not hasattr(opts, "tag_type"):
140 raise errors.ProgrammerError("tag_type not passed to _ExtractTagsObject")
142 if kind == constants.TAG_CLUSTER:
144 elif kind == constants.TAG_NODE or kind == constants.TAG_INSTANCE:
146 raise errors.OpPrereqError("no arguments passed to the command")
150 raise errors.ProgrammerError("Unhandled tag type '%s'" % kind)
154 def _ExtendTags(opts, args):
155 """Extend the args if a source file has been given.
157 This function will extend the tags with the contents of the file
158 passed in the 'tags_source' attribute of the opts parameter. A file
159 named '-' will be replaced by stdin.
162 fname = opts.tags_source
168 new_fh = open(fname, "r")
171 # we don't use the nice 'new_data = [line.strip() for line in fh]'
172 # because of python bug 1633941
174 line = new_fh.readline()
177 new_data.append(line.strip())
180 args.extend(new_data)
183 def ListTags(opts, args):
184 """List the tags on a given object.
186 This is a generic implementation that knows how to deal with all
187 three cases of tag objects (cluster, node, instance). The opts
188 argument is expected to contain a tag_type field denoting what
189 object type we work on.
192 kind, name = _ExtractTagsObject(opts, args)
193 op = opcodes.OpGetTags(kind=kind, name=name)
194 result = SubmitOpCode(op)
195 result = list(result)
201 def AddTags(opts, args):
202 """Add tags on a given object.
204 This is a generic implementation that knows how to deal with all
205 three cases of tag objects (cluster, node, instance). The opts
206 argument is expected to contain a tag_type field denoting what
207 object type we work on.
210 kind, name = _ExtractTagsObject(opts, args)
211 _ExtendTags(opts, args)
213 raise errors.OpPrereqError("No tags to be added")
214 op = opcodes.OpAddTags(kind=kind, name=name, tags=args)
218 def RemoveTags(opts, args):
219 """Remove tags from a given object.
221 This is a generic implementation that knows how to deal with all
222 three cases of tag objects (cluster, node, instance). The opts
223 argument is expected to contain a tag_type field denoting what
224 object type we work on.
227 kind, name = _ExtractTagsObject(opts, args)
228 _ExtendTags(opts, args)
230 raise errors.OpPrereqError("No tags to be removed")
231 op = opcodes.OpDelTags(kind=kind, name=name, tags=args)
235 DEBUG_OPT = make_option("-d", "--debug", default=False,
237 help="Turn debugging on")
239 NOHDR_OPT = make_option("--no-headers", default=False,
240 action="store_true", dest="no_headers",
241 help="Don't display column headers")
243 SEP_OPT = make_option("--separator", default=None,
244 action="store", dest="separator",
245 help="Separator between output fields"
246 " (defaults to one space)")
248 USEUNITS_OPT = make_option("--units", default=None,
249 dest="units", choices=('h', 'm', 'g', 't'),
250 help="Specify units for output (one of hmgt)")
252 FIELDS_OPT = make_option("-o", "--output", dest="output", action="store",
253 type="string", help="Comma separated list of"
257 FORCE_OPT = make_option("-f", "--force", dest="force", action="store_true",
258 default=False, help="Force the operation")
260 CONFIRM_OPT = make_option("--yes", dest="confirm", action="store_true",
261 default=False, help="Do not require confirmation")
263 TAG_SRC_OPT = make_option("--from", dest="tags_source",
264 default=None, help="File with tag names")
266 SUBMIT_OPT = make_option("--submit", dest="submit_only",
267 default=False, action="store_true",
268 help="Submit the job and return the job ID, but"
269 " don't wait for the job to finish")
271 SYNC_OPT = make_option("--sync", dest="do_locking",
272 default=False, action="store_true",
273 help="Grab locks while doing the queries"
274 " in order to ensure more consistent results")
276 _DRY_RUN_OPT = make_option("--dry-run", default=False,
278 help="Do not execute the operation, just run the"
279 " check steps and verify it it could be executed")
283 """Macro-like function denoting a fixed number of arguments"""
287 def ARGS_ATLEAST(val):
288 """Macro-like function denoting a minimum number of arguments"""
293 ARGS_ONE = ARGS_FIXED(1)
294 ARGS_ANY = ARGS_ATLEAST(0)
297 def check_unit(option, opt, value):
298 """OptParsers custom converter for units.
302 return utils.ParseUnit(value)
303 except errors.UnitParseError, err:
304 raise OptionValueError("option %s: %s" % (opt, err))
307 def _SplitKeyVal(opt, data):
308 """Convert a KeyVal string into a dict.
310 This function will convert a key=val[,...] string into a dict. Empty
311 values will be converted specially: keys which have the prefix 'no_'
312 will have the value=False and the prefix stripped, the others will
316 @param opt: a string holding the option name for which we process the
317 data, used in building error messages
319 @param data: a string of the format key=val,key=val,...
321 @return: {key=val, key=val}
322 @raises errors.ParameterError: if there are duplicate keys
327 for elem in data.split(","):
329 key, val = elem.split("=", 1)
331 if elem.startswith(NO_PREFIX):
332 key, val = elem[len(NO_PREFIX):], False
333 elif elem.startswith(UN_PREFIX):
334 key, val = elem[len(UN_PREFIX):], None
336 key, val = elem, True
338 raise errors.ParameterError("Duplicate key '%s' in option %s" %
344 def check_ident_key_val(option, opt, value):
345 """Custom parser for ident:key=val,key=val options.
347 This will store the parsed values as a tuple (ident, {key: val}). As such,
348 multiple uses of this option via action=append is possible.
352 ident, rest = value, ''
354 ident, rest = value.split(":", 1)
356 if ident.startswith(NO_PREFIX):
358 msg = "Cannot pass options when removing parameter groups: %s" % value
359 raise errors.ParameterError(msg)
360 retval = (ident[len(NO_PREFIX):], False)
361 elif ident.startswith(UN_PREFIX):
363 msg = "Cannot pass options when removing parameter groups: %s" % value
364 raise errors.ParameterError(msg)
365 retval = (ident[len(UN_PREFIX):], None)
367 kv_dict = _SplitKeyVal(opt, rest)
368 retval = (ident, kv_dict)
372 def check_key_val(option, opt, value):
373 """Custom parser class for key=val,key=val options.
375 This will store the parsed values as a dict {key: val}.
378 return _SplitKeyVal(opt, value)
381 class CliOption(Option):
382 """Custom option class for optparse.
385 ATTRS = Option.ATTRS + [
386 "completion_suggest",
388 TYPES = Option.TYPES + (
393 TYPE_CHECKER = Option.TYPE_CHECKER.copy()
394 TYPE_CHECKER["identkeyval"] = check_ident_key_val
395 TYPE_CHECKER["keyval"] = check_key_val
396 TYPE_CHECKER["unit"] = check_unit
399 # optparse.py sets make_option, so we do it for our own option class, too
400 cli_option = CliOption
403 def _ParseArgs(argv, commands, aliases):
404 """Parser for the command line arguments.
406 This function parses the arguments and returns the function which
407 must be executed together with its (modified) arguments.
409 @param argv: the command line
410 @param commands: dictionary with special contents, see the design
411 doc for cmdline handling
412 @param aliases: dictionary with command aliases {'alias': 'target, ...}
418 binary = argv[0].split("/")[-1]
420 if len(argv) > 1 and argv[1] == "--version":
421 ToStdout("%s (ganeti) %s", binary, constants.RELEASE_VERSION)
422 # Quit right away. That way we don't have to care about this special
423 # argument. optparse.py does it the same.
426 if len(argv) < 2 or not (argv[1] in commands or
428 # let's do a nice thing
429 sortedcmds = commands.keys()
432 ToStdout("Usage: %s {command} [options...] [argument...]", binary)
433 ToStdout("%s <command> --help to see details, or man %s", binary, binary)
436 # compute the max line length for cmd + usage
437 mlen = max([len(" %s" % cmd) for cmd in commands])
438 mlen = min(60, mlen) # should not get here...
440 # and format a nice command list
441 ToStdout("Commands:")
442 for cmd in sortedcmds:
443 cmdstr = " %s" % (cmd,)
444 help_text = commands[cmd][4]
445 help_lines = textwrap.wrap(help_text, 79 - 3 - mlen)
446 ToStdout("%-*s - %s", mlen, cmdstr, help_lines.pop(0))
447 for line in help_lines:
448 ToStdout("%-*s %s", mlen, "", line)
452 return None, None, None
454 # get command, unalias it, and look it up in commands
458 raise errors.ProgrammerError("Alias '%s' overrides an existing"
461 if aliases[cmd] not in commands:
462 raise errors.ProgrammerError("Alias '%s' maps to non-existing"
463 " command '%s'" % (cmd, aliases[cmd]))
467 func, nargs, parser_opts, usage, description = commands[cmd]
468 parser = OptionParser(option_list=parser_opts + [_DRY_RUN_OPT],
469 description=description,
470 formatter=TitledHelpFormatter(),
471 usage="%%prog %s %s" % (cmd, usage))
472 parser.disable_interspersed_args()
473 options, args = parser.parse_args()
476 ToStderr("Error: Command %s expects no arguments", cmd)
477 return None, None, None
478 elif nargs < 0 and len(args) != -nargs:
479 ToStderr("Error: Command %s expects %d argument(s)", cmd, -nargs)
480 return None, None, None
481 elif nargs >= 0 and len(args) < nargs:
482 ToStderr("Error: Command %s expects at least %d argument(s)", cmd, nargs)
483 return None, None, None
485 return func, options, args
488 def SplitNodeOption(value):
489 """Splits the value of a --node option.
492 if value and ':' in value:
493 return value.split(':', 1)
499 def wrapper(*args, **kwargs):
502 return fn(*args, **kwargs)
508 def AskUser(text, choices=None):
509 """Ask the user a question.
511 @param text: the question to ask
513 @param choices: list with elements tuples (input_char, return_value,
514 description); if not given, it will default to: [('y', True,
515 'Perform the operation'), ('n', False, 'Do no do the operation')];
516 note that the '?' char is reserved for help
518 @return: one of the return values from the choices list; if input is
519 not possible (i.e. not running with a tty, we return the last
524 choices = [('y', True, 'Perform the operation'),
525 ('n', False, 'Do not perform the operation')]
526 if not choices or not isinstance(choices, list):
527 raise errors.ProgrammerError("Invalid choices argument to AskUser")
528 for entry in choices:
529 if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
530 raise errors.ProgrammerError("Invalid choices element to AskUser")
532 answer = choices[-1][1]
534 for line in text.splitlines():
535 new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
536 text = "\n".join(new_text)
538 f = file("/dev/tty", "a+")
542 chars = [entry[0] for entry in choices]
543 chars[-1] = "[%s]" % chars[-1]
545 maps = dict([(entry[0], entry[1]) for entry in choices])
549 f.write("/".join(chars))
551 line = f.readline(2).strip().lower()
556 for entry in choices:
557 f.write(" %s - %s\n" % (entry[0], entry[2]))
565 class JobSubmittedException(Exception):
566 """Job was submitted, client should exit.
568 This exception has one argument, the ID of the job that was
569 submitted. The handler should print this ID.
571 This is not an error, just a structured way to exit from clients.
576 def SendJob(ops, cl=None):
577 """Function to submit an opcode without waiting for the results.
580 @param ops: list of opcodes
581 @type cl: luxi.Client
582 @param cl: the luxi client to use for communicating with the master;
583 if None, a new client will be created
589 job_id = cl.SubmitJob(ops)
594 def PollJob(job_id, cl=None, feedback_fn=None):
595 """Function to poll for the result of a job.
597 @type job_id: job identified
598 @param job_id: the job to poll for results
599 @type cl: luxi.Client
600 @param cl: the luxi client to use for communicating with the master;
601 if None, a new client will be created
608 prev_logmsg_serial = None
611 result = cl.WaitForJobChange(job_id, ["status"], prev_job_info,
614 # job not found, go away!
615 raise errors.JobLost("Job with id %s lost" % job_id)
617 # Split result, a tuple of (field values, log entries)
618 (job_info, log_entries) = result
619 (status, ) = job_info
622 for log_entry in log_entries:
623 (serial, timestamp, _, message) = log_entry
624 if callable(feedback_fn):
625 feedback_fn(log_entry[1:])
627 encoded = utils.SafeEncode(message)
628 ToStdout("%s %s", time.ctime(utils.MergeTime(timestamp)), encoded)
629 prev_logmsg_serial = max(prev_logmsg_serial, serial)
631 # TODO: Handle canceled and archived jobs
632 elif status in (constants.JOB_STATUS_SUCCESS,
633 constants.JOB_STATUS_ERROR,
634 constants.JOB_STATUS_CANCELING,
635 constants.JOB_STATUS_CANCELED):
638 prev_job_info = job_info
640 jobs = cl.QueryJobs([job_id], ["status", "opstatus", "opresult"])
642 raise errors.JobLost("Job with id %s lost" % job_id)
644 status, opstatus, result = jobs[0]
645 if status == constants.JOB_STATUS_SUCCESS:
647 elif status in (constants.JOB_STATUS_CANCELING,
648 constants.JOB_STATUS_CANCELED):
649 raise errors.OpExecError("Job was canceled")
652 for idx, (status, msg) in enumerate(zip(opstatus, result)):
653 if status == constants.OP_STATUS_SUCCESS:
655 elif status == constants.OP_STATUS_ERROR:
657 raise errors.OpExecError("partial failure (opcode %d): %s" %
660 raise errors.OpExecError(str(msg))
661 # default failure mode
662 raise errors.OpExecError(result)
665 def SubmitOpCode(op, cl=None, feedback_fn=None):
666 """Legacy function to submit an opcode.
668 This is just a simple wrapper over the construction of the processor
669 instance. It should be extended to better handle feedback and
670 interaction functions.
676 job_id = SendJob([op], cl)
678 op_results = PollJob(job_id, cl=cl, feedback_fn=feedback_fn)
683 def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
684 """Wrapper around SubmitOpCode or SendJob.
686 This function will decide, based on the 'opts' parameter, whether to
687 submit and wait for the result of the opcode (and return it), or
688 whether to just send the job and print its identifier. It is used in
689 order to simplify the implementation of the '--submit' option.
691 It will also add the dry-run parameter from the options passed, if true.
694 if opts and opts.dry_run:
695 op.dry_run = opts.dry_run
696 if opts and opts.submit_only:
697 job_id = SendJob([op], cl=cl)
698 raise JobSubmittedException(job_id)
700 return SubmitOpCode(op, cl=cl, feedback_fn=feedback_fn)
704 # TODO: Cache object?
706 client = luxi.Client()
707 except luxi.NoMasterError:
708 master, myself = ssconf.GetMasterAndMyself()
710 raise errors.OpPrereqError("This is not the master node, please connect"
711 " to node '%s' and rerun the command" %
718 def FormatError(err):
719 """Return a formatted error message for a given error.
721 This function takes an exception instance and returns a tuple
722 consisting of two values: first, the recommended exit code, and
723 second, a string describing the error message (not
730 if isinstance(err, errors.ConfigurationError):
731 txt = "Corrupt configuration file: %s" % msg
733 obuf.write(txt + "\n")
734 obuf.write("Aborting.")
736 elif isinstance(err, errors.HooksAbort):
737 obuf.write("Failure: hooks execution failed:\n")
738 for node, script, out in err.args[0]:
740 obuf.write(" node: %s, script: %s, output: %s\n" %
743 obuf.write(" node: %s, script: %s (no output)\n" %
745 elif isinstance(err, errors.HooksFailure):
746 obuf.write("Failure: hooks general failure: %s" % msg)
747 elif isinstance(err, errors.ResolverError):
748 this_host = utils.HostInfo.SysName()
749 if err.args[0] == this_host:
750 msg = "Failure: can't resolve my own hostname ('%s')"
752 msg = "Failure: can't resolve hostname '%s'"
753 obuf.write(msg % err.args[0])
754 elif isinstance(err, errors.OpPrereqError):
755 obuf.write("Failure: prerequisites not met for this"
756 " operation:\n%s" % msg)
757 elif isinstance(err, errors.OpExecError):
758 obuf.write("Failure: command execution error:\n%s" % msg)
759 elif isinstance(err, errors.TagError):
760 obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
761 elif isinstance(err, errors.JobQueueDrainError):
762 obuf.write("Failure: the job queue is marked for drain and doesn't"
763 " accept new requests\n")
764 elif isinstance(err, errors.JobQueueFull):
765 obuf.write("Failure: the job queue is full and doesn't accept new"
766 " job submissions until old jobs are archived\n")
767 elif isinstance(err, errors.TypeEnforcementError):
768 obuf.write("Parameter Error: %s" % msg)
769 elif isinstance(err, errors.ParameterError):
770 obuf.write("Failure: unknown/wrong parameter name '%s'" % msg)
771 elif isinstance(err, errors.GenericError):
772 obuf.write("Unhandled Ganeti error: %s" % msg)
773 elif isinstance(err, luxi.NoMasterError):
774 obuf.write("Cannot communicate with the master daemon.\nIs it running"
775 " and listening for connections?")
776 elif isinstance(err, luxi.TimeoutError):
777 obuf.write("Timeout while talking to the master daemon. Error:\n"
779 elif isinstance(err, luxi.ProtocolError):
780 obuf.write("Unhandled protocol error while talking to the master daemon:\n"
782 elif isinstance(err, JobSubmittedException):
783 obuf.write("JobID: %s\n" % err.args[0])
786 obuf.write("Unhandled exception: %s" % msg)
787 return retcode, obuf.getvalue().rstrip('\n')
790 def GenericMain(commands, override=None, aliases=None):
791 """Generic main function for all the gnt-* commands.
794 - commands: a dictionary with a special structure, see the design doc
795 for command line handling.
796 - override: if not None, we expect a dictionary with keys that will
797 override command line options; this can be used to pass
798 options from the scripts to generic functions
799 - aliases: dictionary with command aliases {'alias': 'target, ...}
802 # save the program name and the entire command line for later logging
804 binary = os.path.basename(sys.argv[0]) or sys.argv[0]
805 if len(sys.argv) >= 2:
806 binary += " " + sys.argv[1]
807 old_cmdline = " ".join(sys.argv[2:])
811 binary = "<unknown program>"
818 func, options, args = _ParseArgs(sys.argv, commands, aliases)
819 except errors.ParameterError, err:
820 result, err_msg = FormatError(err)
824 if func is None: # parse error
827 if override is not None:
828 for key, val in override.iteritems():
829 setattr(options, key, val)
831 utils.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
832 stderr_logging=True, program=binary)
835 logging.info("run with arguments '%s'", old_cmdline)
837 logging.info("run with no arguments")
840 result = func(options, args)
841 except (errors.GenericError, luxi.ProtocolError,
842 JobSubmittedException), err:
843 result, err_msg = FormatError(err)
844 logging.exception("Error during command processing")
850 def GenerateTable(headers, fields, separator, data,
851 numfields=None, unitfields=None,
853 """Prints a table with headers and different fields.
856 @param headers: dictionary mapping field names to headers for
859 @param fields: the field names corresponding to each row in
861 @param separator: the separator to be used; if this is None,
862 the default 'smart' algorithm is used which computes optimal
863 field width, otherwise just the separator is used between
866 @param data: a list of lists, each sublist being one row to be output
867 @type numfields: list
868 @param numfields: a list with the fields that hold numeric
869 values and thus should be right-aligned
870 @type unitfields: list
871 @param unitfields: a list with the fields that hold numeric
872 values that should be formatted with the units field
873 @type units: string or None
874 @param units: the units we should use for formatting, or None for
875 automatic choice (human-readable for non-separator usage, otherwise
876 megabytes); this is a one-letter string
885 if numfields is None:
887 if unitfields is None:
890 numfields = utils.FieldSet(*numfields)
891 unitfields = utils.FieldSet(*unitfields)
895 if headers and field not in headers:
896 # TODO: handle better unknown fields (either revert to old
897 # style of raising exception, or deal more intelligently with
899 headers[field] = field
900 if separator is not None:
901 format_fields.append("%s")
902 elif numfields.Matches(field):
903 format_fields.append("%*s")
905 format_fields.append("%-*s")
907 if separator is None:
908 mlens = [0 for name in fields]
909 format = ' '.join(format_fields)
911 format = separator.replace("%", "%%").join(format_fields)
916 for idx, val in enumerate(row):
917 if unitfields.Matches(fields[idx]):
923 val = row[idx] = utils.FormatUnit(val, units)
924 val = row[idx] = str(val)
925 if separator is None:
926 mlens[idx] = max(mlens[idx], len(val))
931 for idx, name in enumerate(fields):
933 if separator is None:
934 mlens[idx] = max(mlens[idx], len(hdr))
935 args.append(mlens[idx])
937 result.append(format % tuple(args))
942 line = ['-' for _ in fields]
943 for idx in xrange(len(fields)):
944 if separator is None:
945 args.append(mlens[idx])
946 args.append(line[idx])
947 result.append(format % tuple(args))
952 def FormatTimestamp(ts):
953 """Formats a given timestamp.
956 @param ts: a timeval-type timestamp, a tuple of seconds and microseconds
959 @return: a string with the formatted timestamp
962 if not isinstance (ts, (tuple, list)) or len(ts) != 2:
965 return time.strftime("%F %T", time.localtime(sec)) + ".%06d" % usec
968 def ParseTimespec(value):
969 """Parse a time specification.
971 The following suffixed will be recognized:
979 Without any suffix, the value will be taken to be in seconds.
984 raise errors.OpPrereqError("Empty time specification passed")
992 if value[-1] not in suffix_map:
996 raise errors.OpPrereqError("Invalid time specification '%s'" % value)
998 multiplier = suffix_map[value[-1]]
1000 if not value: # no data left after stripping the suffix
1001 raise errors.OpPrereqError("Invalid time specification (only"
1004 value = int(value) * multiplier
1006 raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1010 def GetOnlineNodes(nodes, cl=None, nowarn=False):
1011 """Returns the names of online nodes.
1013 This function will also log a warning on stderr with the names of
1016 @param nodes: if not empty, use only this subset of nodes (minus the
1018 @param cl: if not None, luxi client to use
1019 @type nowarn: boolean
1020 @param nowarn: by default, this function will output a note with the
1021 offline nodes that are skipped; if this parameter is True the
1022 note is not displayed
1028 result = cl.QueryNodes(names=nodes, fields=["name", "offline"],
1030 offline = [row[0] for row in result if row[1]]
1031 if offline and not nowarn:
1032 ToStderr("Note: skipping offline node(s): %s" % ", ".join(offline))
1033 return [row[0] for row in result if not row[1]]
1036 def _ToStream(stream, txt, *args):
1037 """Write a message to a stream, bypassing the logging system
1039 @type stream: file object
1040 @param stream: the file to which we should write
1042 @param txt: the message
1047 stream.write(txt % args)
1054 def ToStdout(txt, *args):
1055 """Write a message to stdout only, bypassing the logging system
1057 This is just a wrapper over _ToStream.
1060 @param txt: the message
1063 _ToStream(sys.stdout, txt, *args)
1066 def ToStderr(txt, *args):
1067 """Write a message to stderr only, bypassing the logging system
1069 This is just a wrapper over _ToStream.
1072 @param txt: the message
1075 _ToStream(sys.stderr, txt, *args)
1078 class JobExecutor(object):
1079 """Class which manages the submission and execution of multiple jobs.
1081 Note that instances of this class should not be reused between
1085 def __init__(self, cl=None, verbose=True):
1090 self.verbose = verbose
1093 def QueueJob(self, name, *ops):
1094 """Record a job for later submit.
1097 @param name: a description of the job, will be used in WaitJobSet
1099 self.queue.append((name, ops))
1101 def SubmitPending(self):
1102 """Submit all pending jobs.
1105 results = self.cl.SubmitManyJobs([row[1] for row in self.queue])
1106 for ((status, data), (name, _)) in zip(results, self.queue):
1107 self.jobs.append((status, data, name))
1109 def GetResults(self):
1110 """Wait for and return the results of all jobs.
1113 @return: list of tuples (success, job results), in the same order
1114 as the submitted jobs; if a job has failed, instead of the result
1115 there will be the error message
1119 self.SubmitPending()
1122 ok_jobs = [row[1] for row in self.jobs if row[0]]
1124 ToStdout("Submitted jobs %s", ", ".join(ok_jobs))
1125 for submit_status, jid, name in self.jobs:
1126 if not submit_status:
1127 ToStderr("Failed to submit job for %s: %s", name, jid)
1128 results.append((False, jid))
1131 ToStdout("Waiting for job %s for %s...", jid, name)
1133 job_result = PollJob(jid, cl=self.cl)
1135 except (errors.GenericError, luxi.ProtocolError), err:
1136 _, job_result = FormatError(err)
1138 # the error message will always be shown, verbose or not
1139 ToStderr("Job %s for %s has failed: %s", jid, name, job_result)
1141 results.append((success, job_result))
1144 def WaitOrShow(self, wait):
1145 """Wait for job results or only print the job IDs.
1148 @param wait: whether to wait or not
1152 return self.GetResults()
1155 self.SubmitPending()
1156 for status, result, name in self.jobs:
1158 ToStdout("%s: %s", result, name)
1160 ToStderr("Failure for %s: %s", name, result)