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
55 "FILESTORE_DRIVER_OPT",
73 # Generic functions for CLI programs
78 "JobSubmittedException",
83 # Formatting functions
84 "ToStderr", "ToStdout",
93 # command line options support infrastructure
94 "ARGS_MANY_INSTANCES",
108 "OPT_COMPL_INST_ADD_NODES",
109 "OPT_COMPL_MANY_NODES",
110 "OPT_COMPL_ONE_IALLOCATOR",
111 "OPT_COMPL_ONE_INSTANCE",
112 "OPT_COMPL_ONE_NODE",
123 def __init__(self, min=0, max=None):
128 return ("<%s min=%s max=%s>" %
129 (self.__class__.__name__, self.min, self.max))
132 class ArgSuggest(_Argument):
133 """Suggesting argument.
135 Value can be any of the ones passed to the constructor.
138 def __init__(self, min=0, max=None, choices=None):
139 _Argument.__init__(self, min=min, max=max)
140 self.choices = choices
143 return ("<%s min=%s max=%s choices=%r>" %
144 (self.__class__.__name__, self.min, self.max, self.choices))
147 class ArgChoice(ArgSuggest):
150 Value can be any of the ones passed to the constructor. Like L{ArgSuggest},
151 but value must be one of the choices.
156 class ArgUnknown(_Argument):
157 """Unknown argument to program (e.g. determined at runtime).
162 class ArgInstance(_Argument):
163 """Instances argument.
168 class ArgNode(_Argument):
173 class ArgJobId(_Argument):
179 class ArgFile(_Argument):
180 """File path argument.
185 class ArgCommand(_Argument):
191 class ArgHost(_Argument):
198 ARGS_MANY_INSTANCES = [ArgInstance()]
199 ARGS_MANY_NODES = [ArgNode()]
200 ARGS_ONE_INSTANCE = [ArgInstance(min=1, max=1)]
201 ARGS_ONE_NODE = [ArgNode(min=1, max=1)]
205 def _ExtractTagsObject(opts, args):
206 """Extract the tag type object.
208 Note that this function will modify its args parameter.
211 if not hasattr(opts, "tag_type"):
212 raise errors.ProgrammerError("tag_type not passed to _ExtractTagsObject")
214 if kind == constants.TAG_CLUSTER:
216 elif kind == constants.TAG_NODE or kind == constants.TAG_INSTANCE:
218 raise errors.OpPrereqError("no arguments passed to the command")
222 raise errors.ProgrammerError("Unhandled tag type '%s'" % kind)
226 def _ExtendTags(opts, args):
227 """Extend the args if a source file has been given.
229 This function will extend the tags with the contents of the file
230 passed in the 'tags_source' attribute of the opts parameter. A file
231 named '-' will be replaced by stdin.
234 fname = opts.tags_source
240 new_fh = open(fname, "r")
243 # we don't use the nice 'new_data = [line.strip() for line in fh]'
244 # because of python bug 1633941
246 line = new_fh.readline()
249 new_data.append(line.strip())
252 args.extend(new_data)
255 def ListTags(opts, args):
256 """List the tags on a given object.
258 This is a generic implementation that knows how to deal with all
259 three cases of tag objects (cluster, node, instance). The opts
260 argument is expected to contain a tag_type field denoting what
261 object type we work on.
264 kind, name = _ExtractTagsObject(opts, args)
265 op = opcodes.OpGetTags(kind=kind, name=name)
266 result = SubmitOpCode(op)
267 result = list(result)
273 def AddTags(opts, args):
274 """Add tags on a given object.
276 This is a generic implementation that knows how to deal with all
277 three cases of tag objects (cluster, node, instance). The opts
278 argument is expected to contain a tag_type field denoting what
279 object type we work on.
282 kind, name = _ExtractTagsObject(opts, args)
283 _ExtendTags(opts, args)
285 raise errors.OpPrereqError("No tags to be added")
286 op = opcodes.OpAddTags(kind=kind, name=name, tags=args)
290 def RemoveTags(opts, args):
291 """Remove tags from a given object.
293 This is a generic implementation that knows how to deal with all
294 three cases of tag objects (cluster, node, instance). The opts
295 argument is expected to contain a tag_type field denoting what
296 object type we work on.
299 kind, name = _ExtractTagsObject(opts, args)
300 _ExtendTags(opts, args)
302 raise errors.OpPrereqError("No tags to be removed")
303 op = opcodes.OpDelTags(kind=kind, name=name, tags=args)
307 def check_unit(option, opt, value):
308 """OptParsers custom converter for units.
312 return utils.ParseUnit(value)
313 except errors.UnitParseError, err:
314 raise OptionValueError("option %s: %s" % (opt, err))
317 def _SplitKeyVal(opt, data):
318 """Convert a KeyVal string into a dict.
320 This function will convert a key=val[,...] string into a dict. Empty
321 values will be converted specially: keys which have the prefix 'no_'
322 will have the value=False and the prefix stripped, the others will
326 @param opt: a string holding the option name for which we process the
327 data, used in building error messages
329 @param data: a string of the format key=val,key=val,...
331 @return: {key=val, key=val}
332 @raises errors.ParameterError: if there are duplicate keys
337 for elem in data.split(","):
339 key, val = elem.split("=", 1)
341 if elem.startswith(NO_PREFIX):
342 key, val = elem[len(NO_PREFIX):], False
343 elif elem.startswith(UN_PREFIX):
344 key, val = elem[len(UN_PREFIX):], None
346 key, val = elem, True
348 raise errors.ParameterError("Duplicate key '%s' in option %s" %
354 def check_ident_key_val(option, opt, value):
355 """Custom parser for ident:key=val,key=val options.
357 This will store the parsed values as a tuple (ident, {key: val}). As such,
358 multiple uses of this option via action=append is possible.
362 ident, rest = value, ''
364 ident, rest = value.split(":", 1)
366 if ident.startswith(NO_PREFIX):
368 msg = "Cannot pass options when removing parameter groups: %s" % value
369 raise errors.ParameterError(msg)
370 retval = (ident[len(NO_PREFIX):], False)
371 elif ident.startswith(UN_PREFIX):
373 msg = "Cannot pass options when removing parameter groups: %s" % value
374 raise errors.ParameterError(msg)
375 retval = (ident[len(UN_PREFIX):], None)
377 kv_dict = _SplitKeyVal(opt, rest)
378 retval = (ident, kv_dict)
382 def check_key_val(option, opt, value):
383 """Custom parser class for key=val,key=val options.
385 This will store the parsed values as a dict {key: val}.
388 return _SplitKeyVal(opt, value)
391 # completion_suggestion is normally a list. Using numeric values not evaluating
392 # to False for dynamic completion.
393 (OPT_COMPL_MANY_NODES,
395 OPT_COMPL_ONE_INSTANCE,
397 OPT_COMPL_ONE_IALLOCATOR,
398 OPT_COMPL_INST_ADD_NODES) = range(100, 106)
400 OPT_COMPL_ALL = frozenset([
401 OPT_COMPL_MANY_NODES,
403 OPT_COMPL_ONE_INSTANCE,
405 OPT_COMPL_ONE_IALLOCATOR,
406 OPT_COMPL_INST_ADD_NODES,
410 class CliOption(Option):
411 """Custom option class for optparse.
414 ATTRS = Option.ATTRS + [
415 "completion_suggest",
417 TYPES = Option.TYPES + (
422 TYPE_CHECKER = Option.TYPE_CHECKER.copy()
423 TYPE_CHECKER["identkeyval"] = check_ident_key_val
424 TYPE_CHECKER["keyval"] = check_key_val
425 TYPE_CHECKER["unit"] = check_unit
428 # optparse.py sets make_option, so we do it for our own option class, too
429 cli_option = CliOption
432 DEBUG_OPT = cli_option("-d", "--debug", default=False,
434 help="Turn debugging on")
436 NOHDR_OPT = cli_option("--no-headers", default=False,
437 action="store_true", dest="no_headers",
438 help="Don't display column headers")
440 SEP_OPT = cli_option("--separator", default=None,
441 action="store", dest="separator",
442 help=("Separator between output fields"
443 " (defaults to one space)"))
445 USEUNITS_OPT = cli_option("--units", default=None,
446 dest="units", choices=('h', 'm', 'g', 't'),
447 help="Specify units for output (one of hmgt)")
449 FIELDS_OPT = cli_option("-o", "--output", dest="output", action="store",
450 type="string", metavar="FIELDS",
451 help="Comma separated list of output fields")
453 FORCE_OPT = cli_option("-f", "--force", dest="force", action="store_true",
454 default=False, help="Force the operation")
456 CONFIRM_OPT = cli_option("--yes", dest="confirm", action="store_true",
457 default=False, help="Do not require confirmation")
459 TAG_SRC_OPT = cli_option("--from", dest="tags_source",
460 default=None, help="File with tag names")
462 SUBMIT_OPT = cli_option("--submit", dest="submit_only",
463 default=False, action="store_true",
464 help=("Submit the job and return the job ID, but"
465 " don't wait for the job to finish"))
467 SYNC_OPT = cli_option("--sync", dest="do_locking",
468 default=False, action="store_true",
469 help=("Grab locks while doing the queries"
470 " in order to ensure more consistent results"))
472 _DRY_RUN_OPT = cli_option("--dry-run", default=False,
474 help=("Do not execute the operation, just run the"
475 " check steps and verify it it could be"
478 VERBOSE_OPT = cli_option("-v", "--verbose", default=False,
480 help="Increase the verbosity of the operation")
482 DEBUG_SIMERR_OPT = cli_option("--debug-simulate-errors", default=False,
483 action="store_true", dest="simulate_errors",
484 help="Debugging option that makes the operation"
485 " treat most runtime checks as failed")
487 NWSYNC_OPT = cli_option("--no-wait-for-sync", dest="wait_for_sync",
488 default=True, action="store_false",
489 help="Don't wait for sync (DANGEROUS!)")
491 DISK_TEMPLATE_OPT = cli_option("-t", "--disk-template", dest="disk_template",
492 help="Custom disk setup (diskless, file,"
494 default=None, metavar="TEMPL",
495 choices=list(constants.DISK_TEMPLATES))
497 NONICS_OPT = cli_option("--no-nics", default=False, action="store_true",
498 help="Do not create any network cards for"
501 FILESTORE_DIR_OPT = cli_option("--file-storage-dir", dest="file_storage_dir",
502 help="Relative path under default cluster-wide"
503 " file storage dir to store file-based disks",
504 default=None, metavar="<DIR>")
506 FILESTORE_DRIVER_OPT = cli_option("--file-driver", dest="file_driver",
507 help="Driver to use for image files",
508 default="loop", metavar="<DRIVER>",
509 choices=list(constants.FILE_DRIVER))
511 IALLOCATOR_OPT = cli_option("-I", "--iallocator", metavar="<NAME>",
512 help="Select nodes for the instance automatically"
513 " using the <NAME> iallocator plugin",
514 default=None, type="string",
515 completion_suggest=OPT_COMPL_ONE_IALLOCATOR)
517 OS_OPT = cli_option("-o", "--os-type", dest="os", help="What OS to run",
519 completion_suggest=OPT_COMPL_ONE_OS)
521 BACKEND_OPT = cli_option("-B", "--backend-parameters", dest="beparams",
522 type="keyval", default={},
523 help="Backend parameters")
525 HVOPTS_OPT = cli_option("-H", "--hypervisor-parameters", type="keyval",
526 default={}, dest="hvparams",
527 help="Hypervisor parameters")
529 HYPERVISOR_OPT = cli_option("-H", "--hypervisor-parameters", dest="hypervisor",
530 help="Hypervisor and hypervisor options, in the"
531 " format hypervisor:option=value,option=value,...",
532 default=None, type="identkeyval")
534 HVLIST_OPT = cli_option("-H", "--hypervisor-parameters", dest="hvparams",
535 help="Hypervisor and hypervisor options, in the"
536 " format hypervisor:option=value,option=value,...",
537 default=[], action="append", type="identkeyval")
539 NOIPCHECK_OPT = cli_option("--no-ip-check", dest="ip_check", default=True,
540 action="store_false",
541 help="Don't check that the instance's IP"
544 NET_OPT = cli_option("--net",
545 help="NIC parameters", default=[],
546 dest="nics", action="append", type="identkeyval")
548 DISK_OPT = cli_option("--disk", help="Disk parameters", default=[],
549 dest="disks", action="append", type="identkeyval")
552 def _ParseArgs(argv, commands, aliases):
553 """Parser for the command line arguments.
555 This function parses the arguments and returns the function which
556 must be executed together with its (modified) arguments.
558 @param argv: the command line
559 @param commands: dictionary with special contents, see the design
560 doc for cmdline handling
561 @param aliases: dictionary with command aliases {'alias': 'target, ...}
567 binary = argv[0].split("/")[-1]
569 if len(argv) > 1 and argv[1] == "--version":
570 ToStdout("%s (ganeti) %s", binary, constants.RELEASE_VERSION)
571 # Quit right away. That way we don't have to care about this special
572 # argument. optparse.py does it the same.
575 if len(argv) < 2 or not (argv[1] in commands or
577 # let's do a nice thing
578 sortedcmds = commands.keys()
581 ToStdout("Usage: %s {command} [options...] [argument...]", binary)
582 ToStdout("%s <command> --help to see details, or man %s", binary, binary)
585 # compute the max line length for cmd + usage
586 mlen = max([len(" %s" % cmd) for cmd in commands])
587 mlen = min(60, mlen) # should not get here...
589 # and format a nice command list
590 ToStdout("Commands:")
591 for cmd in sortedcmds:
592 cmdstr = " %s" % (cmd,)
593 help_text = commands[cmd][4]
594 help_lines = textwrap.wrap(help_text, 79 - 3 - mlen)
595 ToStdout("%-*s - %s", mlen, cmdstr, help_lines.pop(0))
596 for line in help_lines:
597 ToStdout("%-*s %s", mlen, "", line)
601 return None, None, None
603 # get command, unalias it, and look it up in commands
607 raise errors.ProgrammerError("Alias '%s' overrides an existing"
610 if aliases[cmd] not in commands:
611 raise errors.ProgrammerError("Alias '%s' maps to non-existing"
612 " command '%s'" % (cmd, aliases[cmd]))
616 func, args_def, parser_opts, usage, description = commands[cmd]
617 parser = OptionParser(option_list=parser_opts + [_DRY_RUN_OPT],
618 description=description,
619 formatter=TitledHelpFormatter(),
620 usage="%%prog %s %s" % (cmd, usage))
621 parser.disable_interspersed_args()
622 options, args = parser.parse_args()
624 if not _CheckArguments(cmd, args_def, args):
625 return None, None, None
627 return func, options, args
630 def _CheckArguments(cmd, args_def, args):
631 """Verifies the arguments using the argument definition.
635 1. Abort with error if values specified by user but none expected.
637 1. For each argument in definition
639 1. Keep running count of minimum number of values (min_count)
640 1. Keep running count of maximum number of values (max_count)
641 1. If it has an unlimited number of values
643 1. Abort with error if it's not the last argument in the definition
645 1. If last argument has limited number of values
647 1. Abort with error if number of values doesn't match or is too large
649 1. Abort with error if user didn't pass enough values (min_count)
652 if args and not args_def:
653 ToStderr("Error: Command %s expects no arguments", cmd)
660 last_idx = len(args_def) - 1
662 for idx, arg in enumerate(args_def):
663 if min_count is None:
665 elif arg.min is not None:
668 if max_count is None:
670 elif arg.max is not None:
674 check_max = (arg.max is not None)
676 elif arg.max is None:
677 raise errors.ProgrammerError("Only the last argument can have max=None")
680 # Command with exact number of arguments
681 if (min_count is not None and max_count is not None and
682 min_count == max_count and len(args) != min_count):
683 ToStderr("Error: Command %s expects %d argument(s)", cmd, min_count)
686 # Command with limited number of arguments
687 if max_count is not None and len(args) > max_count:
688 ToStderr("Error: Command %s expects only %d argument(s)",
692 # Command with some required arguments
693 if min_count is not None and len(args) < min_count:
694 ToStderr("Error: Command %s expects at least %d argument(s)",
701 def SplitNodeOption(value):
702 """Splits the value of a --node option.
705 if value and ':' in value:
706 return value.split(':', 1)
712 def wrapper(*args, **kwargs):
715 return fn(*args, **kwargs)
721 def AskUser(text, choices=None):
722 """Ask the user a question.
724 @param text: the question to ask
726 @param choices: list with elements tuples (input_char, return_value,
727 description); if not given, it will default to: [('y', True,
728 'Perform the operation'), ('n', False, 'Do no do the operation')];
729 note that the '?' char is reserved for help
731 @return: one of the return values from the choices list; if input is
732 not possible (i.e. not running with a tty, we return the last
737 choices = [('y', True, 'Perform the operation'),
738 ('n', False, 'Do not perform the operation')]
739 if not choices or not isinstance(choices, list):
740 raise errors.ProgrammerError("Invalid choices argument to AskUser")
741 for entry in choices:
742 if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
743 raise errors.ProgrammerError("Invalid choices element to AskUser")
745 answer = choices[-1][1]
747 for line in text.splitlines():
748 new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
749 text = "\n".join(new_text)
751 f = file("/dev/tty", "a+")
755 chars = [entry[0] for entry in choices]
756 chars[-1] = "[%s]" % chars[-1]
758 maps = dict([(entry[0], entry[1]) for entry in choices])
762 f.write("/".join(chars))
764 line = f.readline(2).strip().lower()
769 for entry in choices:
770 f.write(" %s - %s\n" % (entry[0], entry[2]))
778 class JobSubmittedException(Exception):
779 """Job was submitted, client should exit.
781 This exception has one argument, the ID of the job that was
782 submitted. The handler should print this ID.
784 This is not an error, just a structured way to exit from clients.
789 def SendJob(ops, cl=None):
790 """Function to submit an opcode without waiting for the results.
793 @param ops: list of opcodes
794 @type cl: luxi.Client
795 @param cl: the luxi client to use for communicating with the master;
796 if None, a new client will be created
802 job_id = cl.SubmitJob(ops)
807 def PollJob(job_id, cl=None, feedback_fn=None):
808 """Function to poll for the result of a job.
810 @type job_id: job identified
811 @param job_id: the job to poll for results
812 @type cl: luxi.Client
813 @param cl: the luxi client to use for communicating with the master;
814 if None, a new client will be created
821 prev_logmsg_serial = None
824 result = cl.WaitForJobChange(job_id, ["status"], prev_job_info,
827 # job not found, go away!
828 raise errors.JobLost("Job with id %s lost" % job_id)
830 # Split result, a tuple of (field values, log entries)
831 (job_info, log_entries) = result
832 (status, ) = job_info
835 for log_entry in log_entries:
836 (serial, timestamp, _, message) = log_entry
837 if callable(feedback_fn):
838 feedback_fn(log_entry[1:])
840 encoded = utils.SafeEncode(message)
841 ToStdout("%s %s", time.ctime(utils.MergeTime(timestamp)), encoded)
842 prev_logmsg_serial = max(prev_logmsg_serial, serial)
844 # TODO: Handle canceled and archived jobs
845 elif status in (constants.JOB_STATUS_SUCCESS,
846 constants.JOB_STATUS_ERROR,
847 constants.JOB_STATUS_CANCELING,
848 constants.JOB_STATUS_CANCELED):
851 prev_job_info = job_info
853 jobs = cl.QueryJobs([job_id], ["status", "opstatus", "opresult"])
855 raise errors.JobLost("Job with id %s lost" % job_id)
857 status, opstatus, result = jobs[0]
858 if status == constants.JOB_STATUS_SUCCESS:
860 elif status in (constants.JOB_STATUS_CANCELING,
861 constants.JOB_STATUS_CANCELED):
862 raise errors.OpExecError("Job was canceled")
865 for idx, (status, msg) in enumerate(zip(opstatus, result)):
866 if status == constants.OP_STATUS_SUCCESS:
868 elif status == constants.OP_STATUS_ERROR:
869 errors.MaybeRaise(msg)
871 raise errors.OpExecError("partial failure (opcode %d): %s" %
874 raise errors.OpExecError(str(msg))
875 # default failure mode
876 raise errors.OpExecError(result)
879 def SubmitOpCode(op, cl=None, feedback_fn=None):
880 """Legacy function to submit an opcode.
882 This is just a simple wrapper over the construction of the processor
883 instance. It should be extended to better handle feedback and
884 interaction functions.
890 job_id = SendJob([op], cl)
892 op_results = PollJob(job_id, cl=cl, feedback_fn=feedback_fn)
897 def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
898 """Wrapper around SubmitOpCode or SendJob.
900 This function will decide, based on the 'opts' parameter, whether to
901 submit and wait for the result of the opcode (and return it), or
902 whether to just send the job and print its identifier. It is used in
903 order to simplify the implementation of the '--submit' option.
905 It will also add the dry-run parameter from the options passed, if true.
908 if opts and opts.dry_run:
909 op.dry_run = opts.dry_run
910 if opts and opts.submit_only:
911 job_id = SendJob([op], cl=cl)
912 raise JobSubmittedException(job_id)
914 return SubmitOpCode(op, cl=cl, feedback_fn=feedback_fn)
918 # TODO: Cache object?
920 client = luxi.Client()
921 except luxi.NoMasterError:
922 master, myself = ssconf.GetMasterAndMyself()
924 raise errors.OpPrereqError("This is not the master node, please connect"
925 " to node '%s' and rerun the command" %
932 def FormatError(err):
933 """Return a formatted error message for a given error.
935 This function takes an exception instance and returns a tuple
936 consisting of two values: first, the recommended exit code, and
937 second, a string describing the error message (not
944 if isinstance(err, errors.ConfigurationError):
945 txt = "Corrupt configuration file: %s" % msg
947 obuf.write(txt + "\n")
948 obuf.write("Aborting.")
950 elif isinstance(err, errors.HooksAbort):
951 obuf.write("Failure: hooks execution failed:\n")
952 for node, script, out in err.args[0]:
954 obuf.write(" node: %s, script: %s, output: %s\n" %
957 obuf.write(" node: %s, script: %s (no output)\n" %
959 elif isinstance(err, errors.HooksFailure):
960 obuf.write("Failure: hooks general failure: %s" % msg)
961 elif isinstance(err, errors.ResolverError):
962 this_host = utils.HostInfo.SysName()
963 if err.args[0] == this_host:
964 msg = "Failure: can't resolve my own hostname ('%s')"
966 msg = "Failure: can't resolve hostname '%s'"
967 obuf.write(msg % err.args[0])
968 elif isinstance(err, errors.OpPrereqError):
969 obuf.write("Failure: prerequisites not met for this"
970 " operation:\n%s" % msg)
971 elif isinstance(err, errors.OpExecError):
972 obuf.write("Failure: command execution error:\n%s" % msg)
973 elif isinstance(err, errors.TagError):
974 obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
975 elif isinstance(err, errors.JobQueueDrainError):
976 obuf.write("Failure: the job queue is marked for drain and doesn't"
977 " accept new requests\n")
978 elif isinstance(err, errors.JobQueueFull):
979 obuf.write("Failure: the job queue is full and doesn't accept new"
980 " job submissions until old jobs are archived\n")
981 elif isinstance(err, errors.TypeEnforcementError):
982 obuf.write("Parameter Error: %s" % msg)
983 elif isinstance(err, errors.ParameterError):
984 obuf.write("Failure: unknown/wrong parameter name '%s'" % msg)
985 elif isinstance(err, errors.GenericError):
986 obuf.write("Unhandled Ganeti error: %s" % msg)
987 elif isinstance(err, luxi.NoMasterError):
988 obuf.write("Cannot communicate with the master daemon.\nIs it running"
989 " and listening for connections?")
990 elif isinstance(err, luxi.TimeoutError):
991 obuf.write("Timeout while talking to the master daemon. Error:\n"
993 elif isinstance(err, luxi.ProtocolError):
994 obuf.write("Unhandled protocol error while talking to the master daemon:\n"
996 elif isinstance(err, JobSubmittedException):
997 obuf.write("JobID: %s\n" % err.args[0])
1000 obuf.write("Unhandled exception: %s" % msg)
1001 return retcode, obuf.getvalue().rstrip('\n')
1004 def GenericMain(commands, override=None, aliases=None):
1005 """Generic main function for all the gnt-* commands.
1008 - commands: a dictionary with a special structure, see the design doc
1009 for command line handling.
1010 - override: if not None, we expect a dictionary with keys that will
1011 override command line options; this can be used to pass
1012 options from the scripts to generic functions
1013 - aliases: dictionary with command aliases {'alias': 'target, ...}
1016 # save the program name and the entire command line for later logging
1018 binary = os.path.basename(sys.argv[0]) or sys.argv[0]
1019 if len(sys.argv) >= 2:
1020 binary += " " + sys.argv[1]
1021 old_cmdline = " ".join(sys.argv[2:])
1025 binary = "<unknown program>"
1032 func, options, args = _ParseArgs(sys.argv, commands, aliases)
1033 except errors.ParameterError, err:
1034 result, err_msg = FormatError(err)
1038 if func is None: # parse error
1041 if override is not None:
1042 for key, val in override.iteritems():
1043 setattr(options, key, val)
1045 utils.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
1046 stderr_logging=True, program=binary)
1049 logging.info("run with arguments '%s'", old_cmdline)
1051 logging.info("run with no arguments")
1054 result = func(options, args)
1055 except (errors.GenericError, luxi.ProtocolError,
1056 JobSubmittedException), err:
1057 result, err_msg = FormatError(err)
1058 logging.exception("Error during command processing")
1064 def GenerateTable(headers, fields, separator, data,
1065 numfields=None, unitfields=None,
1067 """Prints a table with headers and different fields.
1070 @param headers: dictionary mapping field names to headers for
1073 @param fields: the field names corresponding to each row in
1075 @param separator: the separator to be used; if this is None,
1076 the default 'smart' algorithm is used which computes optimal
1077 field width, otherwise just the separator is used between
1080 @param data: a list of lists, each sublist being one row to be output
1081 @type numfields: list
1082 @param numfields: a list with the fields that hold numeric
1083 values and thus should be right-aligned
1084 @type unitfields: list
1085 @param unitfields: a list with the fields that hold numeric
1086 values that should be formatted with the units field
1087 @type units: string or None
1088 @param units: the units we should use for formatting, or None for
1089 automatic choice (human-readable for non-separator usage, otherwise
1090 megabytes); this is a one-letter string
1099 if numfields is None:
1101 if unitfields is None:
1104 numfields = utils.FieldSet(*numfields)
1105 unitfields = utils.FieldSet(*unitfields)
1108 for field in fields:
1109 if headers and field not in headers:
1110 # TODO: handle better unknown fields (either revert to old
1111 # style of raising exception, or deal more intelligently with
1113 headers[field] = field
1114 if separator is not None:
1115 format_fields.append("%s")
1116 elif numfields.Matches(field):
1117 format_fields.append("%*s")
1119 format_fields.append("%-*s")
1121 if separator is None:
1122 mlens = [0 for name in fields]
1123 format = ' '.join(format_fields)
1125 format = separator.replace("%", "%%").join(format_fields)
1130 for idx, val in enumerate(row):
1131 if unitfields.Matches(fields[idx]):
1137 val = row[idx] = utils.FormatUnit(val, units)
1138 val = row[idx] = str(val)
1139 if separator is None:
1140 mlens[idx] = max(mlens[idx], len(val))
1145 for idx, name in enumerate(fields):
1147 if separator is None:
1148 mlens[idx] = max(mlens[idx], len(hdr))
1149 args.append(mlens[idx])
1151 result.append(format % tuple(args))
1156 line = ['-' for _ in fields]
1157 for idx in xrange(len(fields)):
1158 if separator is None:
1159 args.append(mlens[idx])
1160 args.append(line[idx])
1161 result.append(format % tuple(args))
1166 def FormatTimestamp(ts):
1167 """Formats a given timestamp.
1170 @param ts: a timeval-type timestamp, a tuple of seconds and microseconds
1173 @return: a string with the formatted timestamp
1176 if not isinstance (ts, (tuple, list)) or len(ts) != 2:
1179 return time.strftime("%F %T", time.localtime(sec)) + ".%06d" % usec
1182 def ParseTimespec(value):
1183 """Parse a time specification.
1185 The following suffixed will be recognized:
1193 Without any suffix, the value will be taken to be in seconds.
1198 raise errors.OpPrereqError("Empty time specification passed")
1206 if value[-1] not in suffix_map:
1210 raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1212 multiplier = suffix_map[value[-1]]
1214 if not value: # no data left after stripping the suffix
1215 raise errors.OpPrereqError("Invalid time specification (only"
1218 value = int(value) * multiplier
1220 raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1224 def GetOnlineNodes(nodes, cl=None, nowarn=False):
1225 """Returns the names of online nodes.
1227 This function will also log a warning on stderr with the names of
1230 @param nodes: if not empty, use only this subset of nodes (minus the
1232 @param cl: if not None, luxi client to use
1233 @type nowarn: boolean
1234 @param nowarn: by default, this function will output a note with the
1235 offline nodes that are skipped; if this parameter is True the
1236 note is not displayed
1242 result = cl.QueryNodes(names=nodes, fields=["name", "offline"],
1244 offline = [row[0] for row in result if row[1]]
1245 if offline and not nowarn:
1246 ToStderr("Note: skipping offline node(s): %s" % ", ".join(offline))
1247 return [row[0] for row in result if not row[1]]
1250 def _ToStream(stream, txt, *args):
1251 """Write a message to a stream, bypassing the logging system
1253 @type stream: file object
1254 @param stream: the file to which we should write
1256 @param txt: the message
1261 stream.write(txt % args)
1268 def ToStdout(txt, *args):
1269 """Write a message to stdout only, bypassing the logging system
1271 This is just a wrapper over _ToStream.
1274 @param txt: the message
1277 _ToStream(sys.stdout, txt, *args)
1280 def ToStderr(txt, *args):
1281 """Write a message to stderr only, bypassing the logging system
1283 This is just a wrapper over _ToStream.
1286 @param txt: the message
1289 _ToStream(sys.stderr, txt, *args)
1292 class JobExecutor(object):
1293 """Class which manages the submission and execution of multiple jobs.
1295 Note that instances of this class should not be reused between
1299 def __init__(self, cl=None, verbose=True):
1304 self.verbose = verbose
1307 def QueueJob(self, name, *ops):
1308 """Record a job for later submit.
1311 @param name: a description of the job, will be used in WaitJobSet
1313 self.queue.append((name, ops))
1315 def SubmitPending(self):
1316 """Submit all pending jobs.
1319 results = self.cl.SubmitManyJobs([row[1] for row in self.queue])
1320 for ((status, data), (name, _)) in zip(results, self.queue):
1321 self.jobs.append((status, data, name))
1323 def GetResults(self):
1324 """Wait for and return the results of all jobs.
1327 @return: list of tuples (success, job results), in the same order
1328 as the submitted jobs; if a job has failed, instead of the result
1329 there will be the error message
1333 self.SubmitPending()
1336 ok_jobs = [row[1] for row in self.jobs if row[0]]
1338 ToStdout("Submitted jobs %s", ", ".join(ok_jobs))
1339 for submit_status, jid, name in self.jobs:
1340 if not submit_status:
1341 ToStderr("Failed to submit job for %s: %s", name, jid)
1342 results.append((False, jid))
1345 ToStdout("Waiting for job %s for %s...", jid, name)
1347 job_result = PollJob(jid, cl=self.cl)
1349 except (errors.GenericError, luxi.ProtocolError), err:
1350 _, job_result = FormatError(err)
1352 # the error message will always be shown, verbose or not
1353 ToStderr("Job %s for %s has failed: %s", jid, name, job_result)
1355 results.append((success, job_result))
1358 def WaitOrShow(self, wait):
1359 """Wait for job results or only print the job IDs.
1362 @param wait: whether to wait or not
1366 return self.GetResults()
1369 self.SubmitPending()
1370 for status, result, name in self.jobs:
1372 ToStdout("%s: %s", result, name)
1374 ToStderr("Failure for %s: %s", name, result)