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
54 "FILESTORE_DRIVER_OPT",
71 # Generic functions for CLI programs
76 "JobSubmittedException",
81 # Formatting functions
82 "ToStderr", "ToStdout",
91 # command line options support infrastructure
92 "ARGS_MANY_INSTANCES",
106 "OPT_COMPL_INST_ADD_NODES",
107 "OPT_COMPL_MANY_NODES",
108 "OPT_COMPL_ONE_IALLOCATOR",
109 "OPT_COMPL_ONE_INSTANCE",
110 "OPT_COMPL_ONE_NODE",
121 def __init__(self, min=0, max=None):
126 return ("<%s min=%s max=%s>" %
127 (self.__class__.__name__, self.min, self.max))
130 class ArgSuggest(_Argument):
131 """Suggesting argument.
133 Value can be any of the ones passed to the constructor.
136 def __init__(self, min=0, max=None, choices=None):
137 _Argument.__init__(self, min=min, max=max)
138 self.choices = choices
141 return ("<%s min=%s max=%s choices=%r>" %
142 (self.__class__.__name__, self.min, self.max, self.choices))
145 class ArgChoice(ArgSuggest):
148 Value can be any of the ones passed to the constructor. Like L{ArgSuggest},
149 but value must be one of the choices.
154 class ArgUnknown(_Argument):
155 """Unknown argument to program (e.g. determined at runtime).
160 class ArgInstance(_Argument):
161 """Instances argument.
166 class ArgNode(_Argument):
171 class ArgJobId(_Argument):
177 class ArgFile(_Argument):
178 """File path argument.
183 class ArgCommand(_Argument):
189 class ArgHost(_Argument):
196 ARGS_MANY_INSTANCES = [ArgInstance()]
197 ARGS_MANY_NODES = [ArgNode()]
198 ARGS_ONE_INSTANCE = [ArgInstance(min=1, max=1)]
199 ARGS_ONE_NODE = [ArgNode(min=1, max=1)]
203 def _ExtractTagsObject(opts, args):
204 """Extract the tag type object.
206 Note that this function will modify its args parameter.
209 if not hasattr(opts, "tag_type"):
210 raise errors.ProgrammerError("tag_type not passed to _ExtractTagsObject")
212 if kind == constants.TAG_CLUSTER:
214 elif kind == constants.TAG_NODE or kind == constants.TAG_INSTANCE:
216 raise errors.OpPrereqError("no arguments passed to the command")
220 raise errors.ProgrammerError("Unhandled tag type '%s'" % kind)
224 def _ExtendTags(opts, args):
225 """Extend the args if a source file has been given.
227 This function will extend the tags with the contents of the file
228 passed in the 'tags_source' attribute of the opts parameter. A file
229 named '-' will be replaced by stdin.
232 fname = opts.tags_source
238 new_fh = open(fname, "r")
241 # we don't use the nice 'new_data = [line.strip() for line in fh]'
242 # because of python bug 1633941
244 line = new_fh.readline()
247 new_data.append(line.strip())
250 args.extend(new_data)
253 def ListTags(opts, args):
254 """List the tags on a given object.
256 This is a generic implementation that knows how to deal with all
257 three cases of tag objects (cluster, node, instance). The opts
258 argument is expected to contain a tag_type field denoting what
259 object type we work on.
262 kind, name = _ExtractTagsObject(opts, args)
263 op = opcodes.OpGetTags(kind=kind, name=name)
264 result = SubmitOpCode(op)
265 result = list(result)
271 def AddTags(opts, args):
272 """Add tags on a given object.
274 This is a generic implementation that knows how to deal with all
275 three cases of tag objects (cluster, node, instance). The opts
276 argument is expected to contain a tag_type field denoting what
277 object type we work on.
280 kind, name = _ExtractTagsObject(opts, args)
281 _ExtendTags(opts, args)
283 raise errors.OpPrereqError("No tags to be added")
284 op = opcodes.OpAddTags(kind=kind, name=name, tags=args)
288 def RemoveTags(opts, args):
289 """Remove tags from a given object.
291 This is a generic implementation that knows how to deal with all
292 three cases of tag objects (cluster, node, instance). The opts
293 argument is expected to contain a tag_type field denoting what
294 object type we work on.
297 kind, name = _ExtractTagsObject(opts, args)
298 _ExtendTags(opts, args)
300 raise errors.OpPrereqError("No tags to be removed")
301 op = opcodes.OpDelTags(kind=kind, name=name, tags=args)
305 def check_unit(option, opt, value):
306 """OptParsers custom converter for units.
310 return utils.ParseUnit(value)
311 except errors.UnitParseError, err:
312 raise OptionValueError("option %s: %s" % (opt, err))
315 def _SplitKeyVal(opt, data):
316 """Convert a KeyVal string into a dict.
318 This function will convert a key=val[,...] string into a dict. Empty
319 values will be converted specially: keys which have the prefix 'no_'
320 will have the value=False and the prefix stripped, the others will
324 @param opt: a string holding the option name for which we process the
325 data, used in building error messages
327 @param data: a string of the format key=val,key=val,...
329 @return: {key=val, key=val}
330 @raises errors.ParameterError: if there are duplicate keys
335 for elem in data.split(","):
337 key, val = elem.split("=", 1)
339 if elem.startswith(NO_PREFIX):
340 key, val = elem[len(NO_PREFIX):], False
341 elif elem.startswith(UN_PREFIX):
342 key, val = elem[len(UN_PREFIX):], None
344 key, val = elem, True
346 raise errors.ParameterError("Duplicate key '%s' in option %s" %
352 def check_ident_key_val(option, opt, value):
353 """Custom parser for ident:key=val,key=val options.
355 This will store the parsed values as a tuple (ident, {key: val}). As such,
356 multiple uses of this option via action=append is possible.
360 ident, rest = value, ''
362 ident, rest = value.split(":", 1)
364 if ident.startswith(NO_PREFIX):
366 msg = "Cannot pass options when removing parameter groups: %s" % value
367 raise errors.ParameterError(msg)
368 retval = (ident[len(NO_PREFIX):], False)
369 elif ident.startswith(UN_PREFIX):
371 msg = "Cannot pass options when removing parameter groups: %s" % value
372 raise errors.ParameterError(msg)
373 retval = (ident[len(UN_PREFIX):], None)
375 kv_dict = _SplitKeyVal(opt, rest)
376 retval = (ident, kv_dict)
380 def check_key_val(option, opt, value):
381 """Custom parser class for key=val,key=val options.
383 This will store the parsed values as a dict {key: val}.
386 return _SplitKeyVal(opt, value)
389 # completion_suggestion is normally a list. Using numeric values not evaluating
390 # to False for dynamic completion.
391 (OPT_COMPL_MANY_NODES,
393 OPT_COMPL_ONE_INSTANCE,
395 OPT_COMPL_ONE_IALLOCATOR,
396 OPT_COMPL_INST_ADD_NODES) = range(100, 106)
398 OPT_COMPL_ALL = frozenset([
399 OPT_COMPL_MANY_NODES,
401 OPT_COMPL_ONE_INSTANCE,
403 OPT_COMPL_ONE_IALLOCATOR,
404 OPT_COMPL_INST_ADD_NODES,
408 class CliOption(Option):
409 """Custom option class for optparse.
412 ATTRS = Option.ATTRS + [
413 "completion_suggest",
415 TYPES = Option.TYPES + (
420 TYPE_CHECKER = Option.TYPE_CHECKER.copy()
421 TYPE_CHECKER["identkeyval"] = check_ident_key_val
422 TYPE_CHECKER["keyval"] = check_key_val
423 TYPE_CHECKER["unit"] = check_unit
426 # optparse.py sets make_option, so we do it for our own option class, too
427 cli_option = CliOption
430 DEBUG_OPT = cli_option("-d", "--debug", default=False,
432 help="Turn debugging on")
434 NOHDR_OPT = cli_option("--no-headers", default=False,
435 action="store_true", dest="no_headers",
436 help="Don't display column headers")
438 SEP_OPT = cli_option("--separator", default=None,
439 action="store", dest="separator",
440 help=("Separator between output fields"
441 " (defaults to one space)"))
443 USEUNITS_OPT = cli_option("--units", default=None,
444 dest="units", choices=('h', 'm', 'g', 't'),
445 help="Specify units for output (one of hmgt)")
447 FIELDS_OPT = cli_option("-o", "--output", dest="output", action="store",
448 type="string", metavar="FIELDS",
449 help="Comma separated list of output fields")
451 FORCE_OPT = cli_option("-f", "--force", dest="force", action="store_true",
452 default=False, help="Force the operation")
454 CONFIRM_OPT = cli_option("--yes", dest="confirm", action="store_true",
455 default=False, help="Do not require confirmation")
457 TAG_SRC_OPT = cli_option("--from", dest="tags_source",
458 default=None, help="File with tag names")
460 SUBMIT_OPT = cli_option("--submit", dest="submit_only",
461 default=False, action="store_true",
462 help=("Submit the job and return the job ID, but"
463 " don't wait for the job to finish"))
465 SYNC_OPT = cli_option("--sync", dest="do_locking",
466 default=False, action="store_true",
467 help=("Grab locks while doing the queries"
468 " in order to ensure more consistent results"))
470 _DRY_RUN_OPT = cli_option("--dry-run", default=False,
472 help=("Do not execute the operation, just run the"
473 " check steps and verify it it could be"
476 VERBOSE_OPT = cli_option("-v", "--verbose", default=False,
478 help="Increase the verbosity of the operation")
480 DEBUG_SIMERR_OPT = cli_option("--debug-simulate-errors", default=False,
481 action="store_true", dest="simulate_errors",
482 help="Debugging option that makes the operation"
483 " treat most runtime checks as failed")
485 NWSYNC_OPT = cli_option("--no-wait-for-sync", dest="wait_for_sync",
486 default=True, action="store_false",
487 help="Don't wait for sync (DANGEROUS!)")
489 DISK_TEMPLATE_OPT = cli_option("-t", "--disk-template", dest="disk_template",
490 help="Custom disk setup (diskless, file,"
492 default=None, metavar="TEMPL",
493 choices=list(constants.DISK_TEMPLATES))
495 NONICS_OPT = cli_option("--no-nics", default=False, action="store_true",
496 help="Do not create any network cards for"
499 FILESTORE_DIR_OPT = cli_option("--file-storage-dir", dest="file_storage_dir",
500 help="Relative path under default cluster-wide"
501 " file storage dir to store file-based disks",
502 default=None, metavar="<DIR>")
504 FILESTORE_DRIVER_OPT = cli_option("--file-driver", dest="file_driver",
505 help="Driver to use for image files",
506 default="loop", metavar="<DRIVER>",
507 choices=list(constants.FILE_DRIVER))
509 IALLOCATOR_OPT = cli_option("-I", "--iallocator", metavar="<NAME>",
510 help="Select nodes for the instance automatically"
511 " using the <NAME> iallocator plugin",
512 default=None, type="string",
513 completion_suggest=OPT_COMPL_ONE_IALLOCATOR)
515 OS_OPT = cli_option("-o", "--os-type", dest="os", help="What OS to run",
517 completion_suggest=OPT_COMPL_ONE_OS)
519 BACKEND_OPT = cli_option("-B", "--backend-parameters", dest="beparams",
520 type="keyval", default={},
521 help="Backend parameters")
523 HVOPTS_OPT = cli_option("-H", "--hypervisor-parameters", type="keyval",
524 default={}, dest="hvparams",
525 help="Hypervisor parameters")
527 HYPERVISOR_OPT = cli_option("-H", "--hypervisor-parameters", dest="hypervisor",
528 help="Hypervisor and hypervisor options, in the"
529 " format hypervisor:option=value,option=value,...",
530 default=None, type="identkeyval")
532 HVLIST_OPT = cli_option("-H", "--hypervisor-parameters", dest="hvparams",
533 help="Hypervisor and hypervisor options, in the"
534 " format hypervisor:option=value,option=value,...",
535 default=[], action="append", type="identkeyval")
537 NOIPCHECK_OPT = cli_option("--no-ip-check", dest="ip_check", default=True,
538 action="store_false",
539 help="Don't check that the instance's IP"
544 def _ParseArgs(argv, commands, aliases):
545 """Parser for the command line arguments.
547 This function parses the arguments and returns the function which
548 must be executed together with its (modified) arguments.
550 @param argv: the command line
551 @param commands: dictionary with special contents, see the design
552 doc for cmdline handling
553 @param aliases: dictionary with command aliases {'alias': 'target, ...}
559 binary = argv[0].split("/")[-1]
561 if len(argv) > 1 and argv[1] == "--version":
562 ToStdout("%s (ganeti) %s", binary, constants.RELEASE_VERSION)
563 # Quit right away. That way we don't have to care about this special
564 # argument. optparse.py does it the same.
567 if len(argv) < 2 or not (argv[1] in commands or
569 # let's do a nice thing
570 sortedcmds = commands.keys()
573 ToStdout("Usage: %s {command} [options...] [argument...]", binary)
574 ToStdout("%s <command> --help to see details, or man %s", binary, binary)
577 # compute the max line length for cmd + usage
578 mlen = max([len(" %s" % cmd) for cmd in commands])
579 mlen = min(60, mlen) # should not get here...
581 # and format a nice command list
582 ToStdout("Commands:")
583 for cmd in sortedcmds:
584 cmdstr = " %s" % (cmd,)
585 help_text = commands[cmd][4]
586 help_lines = textwrap.wrap(help_text, 79 - 3 - mlen)
587 ToStdout("%-*s - %s", mlen, cmdstr, help_lines.pop(0))
588 for line in help_lines:
589 ToStdout("%-*s %s", mlen, "", line)
593 return None, None, None
595 # get command, unalias it, and look it up in commands
599 raise errors.ProgrammerError("Alias '%s' overrides an existing"
602 if aliases[cmd] not in commands:
603 raise errors.ProgrammerError("Alias '%s' maps to non-existing"
604 " command '%s'" % (cmd, aliases[cmd]))
608 func, args_def, parser_opts, usage, description = commands[cmd]
609 parser = OptionParser(option_list=parser_opts + [_DRY_RUN_OPT],
610 description=description,
611 formatter=TitledHelpFormatter(),
612 usage="%%prog %s %s" % (cmd, usage))
613 parser.disable_interspersed_args()
614 options, args = parser.parse_args()
616 if not _CheckArguments(cmd, args_def, args):
617 return None, None, None
619 return func, options, args
622 def _CheckArguments(cmd, args_def, args):
623 """Verifies the arguments using the argument definition.
627 1. Abort with error if values specified by user but none expected.
629 1. For each argument in definition
631 1. Keep running count of minimum number of values (min_count)
632 1. Keep running count of maximum number of values (max_count)
633 1. If it has an unlimited number of values
635 1. Abort with error if it's not the last argument in the definition
637 1. If last argument has limited number of values
639 1. Abort with error if number of values doesn't match or is too large
641 1. Abort with error if user didn't pass enough values (min_count)
644 if args and not args_def:
645 ToStderr("Error: Command %s expects no arguments", cmd)
652 last_idx = len(args_def) - 1
654 for idx, arg in enumerate(args_def):
655 if min_count is None:
657 elif arg.min is not None:
660 if max_count is None:
662 elif arg.max is not None:
666 check_max = (arg.max is not None)
668 elif arg.max is None:
669 raise errors.ProgrammerError("Only the last argument can have max=None")
672 # Command with exact number of arguments
673 if (min_count is not None and max_count is not None and
674 min_count == max_count and len(args) != min_count):
675 ToStderr("Error: Command %s expects %d argument(s)", cmd, min_count)
678 # Command with limited number of arguments
679 if max_count is not None and len(args) > max_count:
680 ToStderr("Error: Command %s expects only %d argument(s)",
684 # Command with some required arguments
685 if min_count is not None and len(args) < min_count:
686 ToStderr("Error: Command %s expects at least %d argument(s)",
693 def SplitNodeOption(value):
694 """Splits the value of a --node option.
697 if value and ':' in value:
698 return value.split(':', 1)
704 def wrapper(*args, **kwargs):
707 return fn(*args, **kwargs)
713 def AskUser(text, choices=None):
714 """Ask the user a question.
716 @param text: the question to ask
718 @param choices: list with elements tuples (input_char, return_value,
719 description); if not given, it will default to: [('y', True,
720 'Perform the operation'), ('n', False, 'Do no do the operation')];
721 note that the '?' char is reserved for help
723 @return: one of the return values from the choices list; if input is
724 not possible (i.e. not running with a tty, we return the last
729 choices = [('y', True, 'Perform the operation'),
730 ('n', False, 'Do not perform the operation')]
731 if not choices or not isinstance(choices, list):
732 raise errors.ProgrammerError("Invalid choices argument to AskUser")
733 for entry in choices:
734 if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
735 raise errors.ProgrammerError("Invalid choices element to AskUser")
737 answer = choices[-1][1]
739 for line in text.splitlines():
740 new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
741 text = "\n".join(new_text)
743 f = file("/dev/tty", "a+")
747 chars = [entry[0] for entry in choices]
748 chars[-1] = "[%s]" % chars[-1]
750 maps = dict([(entry[0], entry[1]) for entry in choices])
754 f.write("/".join(chars))
756 line = f.readline(2).strip().lower()
761 for entry in choices:
762 f.write(" %s - %s\n" % (entry[0], entry[2]))
770 class JobSubmittedException(Exception):
771 """Job was submitted, client should exit.
773 This exception has one argument, the ID of the job that was
774 submitted. The handler should print this ID.
776 This is not an error, just a structured way to exit from clients.
781 def SendJob(ops, cl=None):
782 """Function to submit an opcode without waiting for the results.
785 @param ops: list of opcodes
786 @type cl: luxi.Client
787 @param cl: the luxi client to use for communicating with the master;
788 if None, a new client will be created
794 job_id = cl.SubmitJob(ops)
799 def PollJob(job_id, cl=None, feedback_fn=None):
800 """Function to poll for the result of a job.
802 @type job_id: job identified
803 @param job_id: the job to poll for results
804 @type cl: luxi.Client
805 @param cl: the luxi client to use for communicating with the master;
806 if None, a new client will be created
813 prev_logmsg_serial = None
816 result = cl.WaitForJobChange(job_id, ["status"], prev_job_info,
819 # job not found, go away!
820 raise errors.JobLost("Job with id %s lost" % job_id)
822 # Split result, a tuple of (field values, log entries)
823 (job_info, log_entries) = result
824 (status, ) = job_info
827 for log_entry in log_entries:
828 (serial, timestamp, _, message) = log_entry
829 if callable(feedback_fn):
830 feedback_fn(log_entry[1:])
832 encoded = utils.SafeEncode(message)
833 ToStdout("%s %s", time.ctime(utils.MergeTime(timestamp)), encoded)
834 prev_logmsg_serial = max(prev_logmsg_serial, serial)
836 # TODO: Handle canceled and archived jobs
837 elif status in (constants.JOB_STATUS_SUCCESS,
838 constants.JOB_STATUS_ERROR,
839 constants.JOB_STATUS_CANCELING,
840 constants.JOB_STATUS_CANCELED):
843 prev_job_info = job_info
845 jobs = cl.QueryJobs([job_id], ["status", "opstatus", "opresult"])
847 raise errors.JobLost("Job with id %s lost" % job_id)
849 status, opstatus, result = jobs[0]
850 if status == constants.JOB_STATUS_SUCCESS:
852 elif status in (constants.JOB_STATUS_CANCELING,
853 constants.JOB_STATUS_CANCELED):
854 raise errors.OpExecError("Job was canceled")
857 for idx, (status, msg) in enumerate(zip(opstatus, result)):
858 if status == constants.OP_STATUS_SUCCESS:
860 elif status == constants.OP_STATUS_ERROR:
861 errors.MaybeRaise(msg)
863 raise errors.OpExecError("partial failure (opcode %d): %s" %
866 raise errors.OpExecError(str(msg))
867 # default failure mode
868 raise errors.OpExecError(result)
871 def SubmitOpCode(op, cl=None, feedback_fn=None):
872 """Legacy function to submit an opcode.
874 This is just a simple wrapper over the construction of the processor
875 instance. It should be extended to better handle feedback and
876 interaction functions.
882 job_id = SendJob([op], cl)
884 op_results = PollJob(job_id, cl=cl, feedback_fn=feedback_fn)
889 def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
890 """Wrapper around SubmitOpCode or SendJob.
892 This function will decide, based on the 'opts' parameter, whether to
893 submit and wait for the result of the opcode (and return it), or
894 whether to just send the job and print its identifier. It is used in
895 order to simplify the implementation of the '--submit' option.
897 It will also add the dry-run parameter from the options passed, if true.
900 if opts and opts.dry_run:
901 op.dry_run = opts.dry_run
902 if opts and opts.submit_only:
903 job_id = SendJob([op], cl=cl)
904 raise JobSubmittedException(job_id)
906 return SubmitOpCode(op, cl=cl, feedback_fn=feedback_fn)
910 # TODO: Cache object?
912 client = luxi.Client()
913 except luxi.NoMasterError:
914 master, myself = ssconf.GetMasterAndMyself()
916 raise errors.OpPrereqError("This is not the master node, please connect"
917 " to node '%s' and rerun the command" %
924 def FormatError(err):
925 """Return a formatted error message for a given error.
927 This function takes an exception instance and returns a tuple
928 consisting of two values: first, the recommended exit code, and
929 second, a string describing the error message (not
936 if isinstance(err, errors.ConfigurationError):
937 txt = "Corrupt configuration file: %s" % msg
939 obuf.write(txt + "\n")
940 obuf.write("Aborting.")
942 elif isinstance(err, errors.HooksAbort):
943 obuf.write("Failure: hooks execution failed:\n")
944 for node, script, out in err.args[0]:
946 obuf.write(" node: %s, script: %s, output: %s\n" %
949 obuf.write(" node: %s, script: %s (no output)\n" %
951 elif isinstance(err, errors.HooksFailure):
952 obuf.write("Failure: hooks general failure: %s" % msg)
953 elif isinstance(err, errors.ResolverError):
954 this_host = utils.HostInfo.SysName()
955 if err.args[0] == this_host:
956 msg = "Failure: can't resolve my own hostname ('%s')"
958 msg = "Failure: can't resolve hostname '%s'"
959 obuf.write(msg % err.args[0])
960 elif isinstance(err, errors.OpPrereqError):
961 obuf.write("Failure: prerequisites not met for this"
962 " operation:\n%s" % msg)
963 elif isinstance(err, errors.OpExecError):
964 obuf.write("Failure: command execution error:\n%s" % msg)
965 elif isinstance(err, errors.TagError):
966 obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
967 elif isinstance(err, errors.JobQueueDrainError):
968 obuf.write("Failure: the job queue is marked for drain and doesn't"
969 " accept new requests\n")
970 elif isinstance(err, errors.JobQueueFull):
971 obuf.write("Failure: the job queue is full and doesn't accept new"
972 " job submissions until old jobs are archived\n")
973 elif isinstance(err, errors.TypeEnforcementError):
974 obuf.write("Parameter Error: %s" % msg)
975 elif isinstance(err, errors.ParameterError):
976 obuf.write("Failure: unknown/wrong parameter name '%s'" % msg)
977 elif isinstance(err, errors.GenericError):
978 obuf.write("Unhandled Ganeti error: %s" % msg)
979 elif isinstance(err, luxi.NoMasterError):
980 obuf.write("Cannot communicate with the master daemon.\nIs it running"
981 " and listening for connections?")
982 elif isinstance(err, luxi.TimeoutError):
983 obuf.write("Timeout while talking to the master daemon. Error:\n"
985 elif isinstance(err, luxi.ProtocolError):
986 obuf.write("Unhandled protocol error while talking to the master daemon:\n"
988 elif isinstance(err, JobSubmittedException):
989 obuf.write("JobID: %s\n" % err.args[0])
992 obuf.write("Unhandled exception: %s" % msg)
993 return retcode, obuf.getvalue().rstrip('\n')
996 def GenericMain(commands, override=None, aliases=None):
997 """Generic main function for all the gnt-* commands.
1000 - commands: a dictionary with a special structure, see the design doc
1001 for command line handling.
1002 - override: if not None, we expect a dictionary with keys that will
1003 override command line options; this can be used to pass
1004 options from the scripts to generic functions
1005 - aliases: dictionary with command aliases {'alias': 'target, ...}
1008 # save the program name and the entire command line for later logging
1010 binary = os.path.basename(sys.argv[0]) or sys.argv[0]
1011 if len(sys.argv) >= 2:
1012 binary += " " + sys.argv[1]
1013 old_cmdline = " ".join(sys.argv[2:])
1017 binary = "<unknown program>"
1024 func, options, args = _ParseArgs(sys.argv, commands, aliases)
1025 except errors.ParameterError, err:
1026 result, err_msg = FormatError(err)
1030 if func is None: # parse error
1033 if override is not None:
1034 for key, val in override.iteritems():
1035 setattr(options, key, val)
1037 utils.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
1038 stderr_logging=True, program=binary)
1041 logging.info("run with arguments '%s'", old_cmdline)
1043 logging.info("run with no arguments")
1046 result = func(options, args)
1047 except (errors.GenericError, luxi.ProtocolError,
1048 JobSubmittedException), err:
1049 result, err_msg = FormatError(err)
1050 logging.exception("Error during command processing")
1056 def GenerateTable(headers, fields, separator, data,
1057 numfields=None, unitfields=None,
1059 """Prints a table with headers and different fields.
1062 @param headers: dictionary mapping field names to headers for
1065 @param fields: the field names corresponding to each row in
1067 @param separator: the separator to be used; if this is None,
1068 the default 'smart' algorithm is used which computes optimal
1069 field width, otherwise just the separator is used between
1072 @param data: a list of lists, each sublist being one row to be output
1073 @type numfields: list
1074 @param numfields: a list with the fields that hold numeric
1075 values and thus should be right-aligned
1076 @type unitfields: list
1077 @param unitfields: a list with the fields that hold numeric
1078 values that should be formatted with the units field
1079 @type units: string or None
1080 @param units: the units we should use for formatting, or None for
1081 automatic choice (human-readable for non-separator usage, otherwise
1082 megabytes); this is a one-letter string
1091 if numfields is None:
1093 if unitfields is None:
1096 numfields = utils.FieldSet(*numfields)
1097 unitfields = utils.FieldSet(*unitfields)
1100 for field in fields:
1101 if headers and field not in headers:
1102 # TODO: handle better unknown fields (either revert to old
1103 # style of raising exception, or deal more intelligently with
1105 headers[field] = field
1106 if separator is not None:
1107 format_fields.append("%s")
1108 elif numfields.Matches(field):
1109 format_fields.append("%*s")
1111 format_fields.append("%-*s")
1113 if separator is None:
1114 mlens = [0 for name in fields]
1115 format = ' '.join(format_fields)
1117 format = separator.replace("%", "%%").join(format_fields)
1122 for idx, val in enumerate(row):
1123 if unitfields.Matches(fields[idx]):
1129 val = row[idx] = utils.FormatUnit(val, units)
1130 val = row[idx] = str(val)
1131 if separator is None:
1132 mlens[idx] = max(mlens[idx], len(val))
1137 for idx, name in enumerate(fields):
1139 if separator is None:
1140 mlens[idx] = max(mlens[idx], len(hdr))
1141 args.append(mlens[idx])
1143 result.append(format % tuple(args))
1148 line = ['-' for _ in fields]
1149 for idx in xrange(len(fields)):
1150 if separator is None:
1151 args.append(mlens[idx])
1152 args.append(line[idx])
1153 result.append(format % tuple(args))
1158 def FormatTimestamp(ts):
1159 """Formats a given timestamp.
1162 @param ts: a timeval-type timestamp, a tuple of seconds and microseconds
1165 @return: a string with the formatted timestamp
1168 if not isinstance (ts, (tuple, list)) or len(ts) != 2:
1171 return time.strftime("%F %T", time.localtime(sec)) + ".%06d" % usec
1174 def ParseTimespec(value):
1175 """Parse a time specification.
1177 The following suffixed will be recognized:
1185 Without any suffix, the value will be taken to be in seconds.
1190 raise errors.OpPrereqError("Empty time specification passed")
1198 if value[-1] not in suffix_map:
1202 raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1204 multiplier = suffix_map[value[-1]]
1206 if not value: # no data left after stripping the suffix
1207 raise errors.OpPrereqError("Invalid time specification (only"
1210 value = int(value) * multiplier
1212 raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1216 def GetOnlineNodes(nodes, cl=None, nowarn=False):
1217 """Returns the names of online nodes.
1219 This function will also log a warning on stderr with the names of
1222 @param nodes: if not empty, use only this subset of nodes (minus the
1224 @param cl: if not None, luxi client to use
1225 @type nowarn: boolean
1226 @param nowarn: by default, this function will output a note with the
1227 offline nodes that are skipped; if this parameter is True the
1228 note is not displayed
1234 result = cl.QueryNodes(names=nodes, fields=["name", "offline"],
1236 offline = [row[0] for row in result if row[1]]
1237 if offline and not nowarn:
1238 ToStderr("Note: skipping offline node(s): %s" % ", ".join(offline))
1239 return [row[0] for row in result if not row[1]]
1242 def _ToStream(stream, txt, *args):
1243 """Write a message to a stream, bypassing the logging system
1245 @type stream: file object
1246 @param stream: the file to which we should write
1248 @param txt: the message
1253 stream.write(txt % args)
1260 def ToStdout(txt, *args):
1261 """Write a message to stdout only, bypassing the logging system
1263 This is just a wrapper over _ToStream.
1266 @param txt: the message
1269 _ToStream(sys.stdout, txt, *args)
1272 def ToStderr(txt, *args):
1273 """Write a message to stderr only, bypassing the logging system
1275 This is just a wrapper over _ToStream.
1278 @param txt: the message
1281 _ToStream(sys.stderr, txt, *args)
1284 class JobExecutor(object):
1285 """Class which manages the submission and execution of multiple jobs.
1287 Note that instances of this class should not be reused between
1291 def __init__(self, cl=None, verbose=True):
1296 self.verbose = verbose
1299 def QueueJob(self, name, *ops):
1300 """Record a job for later submit.
1303 @param name: a description of the job, will be used in WaitJobSet
1305 self.queue.append((name, ops))
1307 def SubmitPending(self):
1308 """Submit all pending jobs.
1311 results = self.cl.SubmitManyJobs([row[1] for row in self.queue])
1312 for ((status, data), (name, _)) in zip(results, self.queue):
1313 self.jobs.append((status, data, name))
1315 def GetResults(self):
1316 """Wait for and return the results of all jobs.
1319 @return: list of tuples (success, job results), in the same order
1320 as the submitted jobs; if a job has failed, instead of the result
1321 there will be the error message
1325 self.SubmitPending()
1328 ok_jobs = [row[1] for row in self.jobs if row[0]]
1330 ToStdout("Submitted jobs %s", ", ".join(ok_jobs))
1331 for submit_status, jid, name in self.jobs:
1332 if not submit_status:
1333 ToStderr("Failed to submit job for %s: %s", name, jid)
1334 results.append((False, jid))
1337 ToStdout("Waiting for job %s for %s...", jid, name)
1339 job_result = PollJob(jid, cl=self.cl)
1341 except (errors.GenericError, luxi.ProtocolError), err:
1342 _, job_result = FormatError(err)
1344 # the error message will always be shown, verbose or not
1345 ToStderr("Job %s for %s has failed: %s", jid, name, job_result)
1347 results.append((success, job_result))
1350 def WaitOrShow(self, wait):
1351 """Wait for job results or only print the job IDs.
1354 @param wait: whether to wait or not
1358 return self.GetResults()
1361 self.SubmitPending()
1362 for status, result, name in self.jobs:
1364 ToStdout("%s: %s", result, name)
1366 ToStderr("Failure for %s: %s", name, result)