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
56 "FILESTORE_DRIVER_OPT",
76 # Generic functions for CLI programs
81 "JobSubmittedException",
86 # Formatting functions
87 "ToStderr", "ToStdout",
96 # command line options support infrastructure
97 "ARGS_MANY_INSTANCES",
111 "OPT_COMPL_INST_ADD_NODES",
112 "OPT_COMPL_MANY_NODES",
113 "OPT_COMPL_ONE_IALLOCATOR",
114 "OPT_COMPL_ONE_INSTANCE",
115 "OPT_COMPL_ONE_NODE",
126 def __init__(self, min=0, max=None):
131 return ("<%s min=%s max=%s>" %
132 (self.__class__.__name__, self.min, self.max))
135 class ArgSuggest(_Argument):
136 """Suggesting argument.
138 Value can be any of the ones passed to the constructor.
141 def __init__(self, min=0, max=None, choices=None):
142 _Argument.__init__(self, min=min, max=max)
143 self.choices = choices
146 return ("<%s min=%s max=%s choices=%r>" %
147 (self.__class__.__name__, self.min, self.max, self.choices))
150 class ArgChoice(ArgSuggest):
153 Value can be any of the ones passed to the constructor. Like L{ArgSuggest},
154 but value must be one of the choices.
159 class ArgUnknown(_Argument):
160 """Unknown argument to program (e.g. determined at runtime).
165 class ArgInstance(_Argument):
166 """Instances argument.
171 class ArgNode(_Argument):
176 class ArgJobId(_Argument):
182 class ArgFile(_Argument):
183 """File path argument.
188 class ArgCommand(_Argument):
194 class ArgHost(_Argument):
201 ARGS_MANY_INSTANCES = [ArgInstance()]
202 ARGS_MANY_NODES = [ArgNode()]
203 ARGS_ONE_INSTANCE = [ArgInstance(min=1, max=1)]
204 ARGS_ONE_NODE = [ArgNode(min=1, max=1)]
208 def _ExtractTagsObject(opts, args):
209 """Extract the tag type object.
211 Note that this function will modify its args parameter.
214 if not hasattr(opts, "tag_type"):
215 raise errors.ProgrammerError("tag_type not passed to _ExtractTagsObject")
217 if kind == constants.TAG_CLUSTER:
219 elif kind == constants.TAG_NODE or kind == constants.TAG_INSTANCE:
221 raise errors.OpPrereqError("no arguments passed to the command")
225 raise errors.ProgrammerError("Unhandled tag type '%s'" % kind)
229 def _ExtendTags(opts, args):
230 """Extend the args if a source file has been given.
232 This function will extend the tags with the contents of the file
233 passed in the 'tags_source' attribute of the opts parameter. A file
234 named '-' will be replaced by stdin.
237 fname = opts.tags_source
243 new_fh = open(fname, "r")
246 # we don't use the nice 'new_data = [line.strip() for line in fh]'
247 # because of python bug 1633941
249 line = new_fh.readline()
252 new_data.append(line.strip())
255 args.extend(new_data)
258 def ListTags(opts, args):
259 """List the tags on a given object.
261 This is a generic implementation that knows how to deal with all
262 three cases of tag objects (cluster, node, instance). The opts
263 argument is expected to contain a tag_type field denoting what
264 object type we work on.
267 kind, name = _ExtractTagsObject(opts, args)
268 op = opcodes.OpGetTags(kind=kind, name=name)
269 result = SubmitOpCode(op)
270 result = list(result)
276 def AddTags(opts, args):
277 """Add tags on a given object.
279 This is a generic implementation that knows how to deal with all
280 three cases of tag objects (cluster, node, instance). The opts
281 argument is expected to contain a tag_type field denoting what
282 object type we work on.
285 kind, name = _ExtractTagsObject(opts, args)
286 _ExtendTags(opts, args)
288 raise errors.OpPrereqError("No tags to be added")
289 op = opcodes.OpAddTags(kind=kind, name=name, tags=args)
293 def RemoveTags(opts, args):
294 """Remove tags from a given object.
296 This is a generic implementation that knows how to deal with all
297 three cases of tag objects (cluster, node, instance). The opts
298 argument is expected to contain a tag_type field denoting what
299 object type we work on.
302 kind, name = _ExtractTagsObject(opts, args)
303 _ExtendTags(opts, args)
305 raise errors.OpPrereqError("No tags to be removed")
306 op = opcodes.OpDelTags(kind=kind, name=name, tags=args)
310 def check_unit(option, opt, value):
311 """OptParsers custom converter for units.
315 return utils.ParseUnit(value)
316 except errors.UnitParseError, err:
317 raise OptionValueError("option %s: %s" % (opt, err))
320 def _SplitKeyVal(opt, data):
321 """Convert a KeyVal string into a dict.
323 This function will convert a key=val[,...] string into a dict. Empty
324 values will be converted specially: keys which have the prefix 'no_'
325 will have the value=False and the prefix stripped, the others will
329 @param opt: a string holding the option name for which we process the
330 data, used in building error messages
332 @param data: a string of the format key=val,key=val,...
334 @return: {key=val, key=val}
335 @raises errors.ParameterError: if there are duplicate keys
340 for elem in data.split(","):
342 key, val = elem.split("=", 1)
344 if elem.startswith(NO_PREFIX):
345 key, val = elem[len(NO_PREFIX):], False
346 elif elem.startswith(UN_PREFIX):
347 key, val = elem[len(UN_PREFIX):], None
349 key, val = elem, True
351 raise errors.ParameterError("Duplicate key '%s' in option %s" %
357 def check_ident_key_val(option, opt, value):
358 """Custom parser for ident:key=val,key=val options.
360 This will store the parsed values as a tuple (ident, {key: val}). As such,
361 multiple uses of this option via action=append is possible.
365 ident, rest = value, ''
367 ident, rest = value.split(":", 1)
369 if ident.startswith(NO_PREFIX):
371 msg = "Cannot pass options when removing parameter groups: %s" % value
372 raise errors.ParameterError(msg)
373 retval = (ident[len(NO_PREFIX):], False)
374 elif ident.startswith(UN_PREFIX):
376 msg = "Cannot pass options when removing parameter groups: %s" % value
377 raise errors.ParameterError(msg)
378 retval = (ident[len(UN_PREFIX):], None)
380 kv_dict = _SplitKeyVal(opt, rest)
381 retval = (ident, kv_dict)
385 def check_key_val(option, opt, value):
386 """Custom parser class for key=val,key=val options.
388 This will store the parsed values as a dict {key: val}.
391 return _SplitKeyVal(opt, value)
394 # completion_suggestion is normally a list. Using numeric values not evaluating
395 # to False for dynamic completion.
396 (OPT_COMPL_MANY_NODES,
398 OPT_COMPL_ONE_INSTANCE,
400 OPT_COMPL_ONE_IALLOCATOR,
401 OPT_COMPL_INST_ADD_NODES) = range(100, 106)
403 OPT_COMPL_ALL = frozenset([
404 OPT_COMPL_MANY_NODES,
406 OPT_COMPL_ONE_INSTANCE,
408 OPT_COMPL_ONE_IALLOCATOR,
409 OPT_COMPL_INST_ADD_NODES,
413 class CliOption(Option):
414 """Custom option class for optparse.
417 ATTRS = Option.ATTRS + [
418 "completion_suggest",
420 TYPES = Option.TYPES + (
425 TYPE_CHECKER = Option.TYPE_CHECKER.copy()
426 TYPE_CHECKER["identkeyval"] = check_ident_key_val
427 TYPE_CHECKER["keyval"] = check_key_val
428 TYPE_CHECKER["unit"] = check_unit
431 # optparse.py sets make_option, so we do it for our own option class, too
432 cli_option = CliOption
435 DEBUG_OPT = cli_option("-d", "--debug", default=False,
437 help="Turn debugging on")
439 NOHDR_OPT = cli_option("--no-headers", default=False,
440 action="store_true", dest="no_headers",
441 help="Don't display column headers")
443 SEP_OPT = cli_option("--separator", default=None,
444 action="store", dest="separator",
445 help=("Separator between output fields"
446 " (defaults to one space)"))
448 USEUNITS_OPT = cli_option("--units", default=None,
449 dest="units", choices=('h', 'm', 'g', 't'),
450 help="Specify units for output (one of hmgt)")
452 FIELDS_OPT = cli_option("-o", "--output", dest="output", action="store",
453 type="string", metavar="FIELDS",
454 help="Comma separated list of output fields")
456 FORCE_OPT = cli_option("-f", "--force", dest="force", action="store_true",
457 default=False, help="Force the operation")
459 CONFIRM_OPT = cli_option("--yes", dest="confirm", action="store_true",
460 default=False, help="Do not require confirmation")
462 TAG_SRC_OPT = cli_option("--from", dest="tags_source",
463 default=None, help="File with tag names")
465 SUBMIT_OPT = cli_option("--submit", dest="submit_only",
466 default=False, action="store_true",
467 help=("Submit the job and return the job ID, but"
468 " don't wait for the job to finish"))
470 SYNC_OPT = cli_option("--sync", dest="do_locking",
471 default=False, action="store_true",
472 help=("Grab locks while doing the queries"
473 " in order to ensure more consistent results"))
475 _DRY_RUN_OPT = cli_option("--dry-run", default=False,
477 help=("Do not execute the operation, just run the"
478 " check steps and verify it it could be"
481 VERBOSE_OPT = cli_option("-v", "--verbose", default=False,
483 help="Increase the verbosity of the operation")
485 DEBUG_SIMERR_OPT = cli_option("--debug-simulate-errors", default=False,
486 action="store_true", dest="simulate_errors",
487 help="Debugging option that makes the operation"
488 " treat most runtime checks as failed")
490 NWSYNC_OPT = cli_option("--no-wait-for-sync", dest="wait_for_sync",
491 default=True, action="store_false",
492 help="Don't wait for sync (DANGEROUS!)")
494 DISK_TEMPLATE_OPT = cli_option("-t", "--disk-template", dest="disk_template",
495 help="Custom disk setup (diskless, file,"
497 default=None, metavar="TEMPL",
498 choices=list(constants.DISK_TEMPLATES))
500 NONICS_OPT = cli_option("--no-nics", default=False, action="store_true",
501 help="Do not create any network cards for"
504 FILESTORE_DIR_OPT = cli_option("--file-storage-dir", dest="file_storage_dir",
505 help="Relative path under default cluster-wide"
506 " file storage dir to store file-based disks",
507 default=None, metavar="<DIR>")
509 FILESTORE_DRIVER_OPT = cli_option("--file-driver", dest="file_driver",
510 help="Driver to use for image files",
511 default="loop", metavar="<DRIVER>",
512 choices=list(constants.FILE_DRIVER))
514 IALLOCATOR_OPT = cli_option("-I", "--iallocator", metavar="<NAME>",
515 help="Select nodes for the instance automatically"
516 " using the <NAME> iallocator plugin",
517 default=None, type="string",
518 completion_suggest=OPT_COMPL_ONE_IALLOCATOR)
520 OS_OPT = cli_option("-o", "--os-type", dest="os", help="What OS to run",
522 completion_suggest=OPT_COMPL_ONE_OS)
524 BACKEND_OPT = cli_option("-B", "--backend-parameters", dest="beparams",
525 type="keyval", default={},
526 help="Backend parameters")
528 HVOPTS_OPT = cli_option("-H", "--hypervisor-parameters", type="keyval",
529 default={}, dest="hvparams",
530 help="Hypervisor parameters")
532 HYPERVISOR_OPT = cli_option("-H", "--hypervisor-parameters", dest="hypervisor",
533 help="Hypervisor and hypervisor options, in the"
534 " format hypervisor:option=value,option=value,...",
535 default=None, type="identkeyval")
537 HVLIST_OPT = cli_option("-H", "--hypervisor-parameters", dest="hvparams",
538 help="Hypervisor and hypervisor options, in the"
539 " format hypervisor:option=value,option=value,...",
540 default=[], action="append", type="identkeyval")
542 NOIPCHECK_OPT = cli_option("--no-ip-check", dest="ip_check", default=True,
543 action="store_false",
544 help="Don't check that the instance's IP"
547 NET_OPT = cli_option("--net",
548 help="NIC parameters", default=[],
549 dest="nics", action="append", type="identkeyval")
551 DISK_OPT = cli_option("--disk", help="Disk parameters", default=[],
552 dest="disks", action="append", type="identkeyval")
554 DISKIDX_OPT = cli_option("--disks", dest="disks", default=None,
555 help="Comma-separated list of disks"
556 " indices to act on (e.g. 0,2) (optional,"
557 " defaults to all disks)")
559 OS_SIZE_OPT = cli_option("-s", "--os-size", dest="sd_size",
560 help="Enforces a single-disk configuration using the"
561 " given disk size, in MiB unless a suffix is used",
562 default=None, type="unit", metavar="<size>")
564 IGNORE_CONSIST_OPT = cli_option("--ignore-consistency",
565 dest="ignore_consistency",
566 action="store_true", default=False,
567 help="Ignore the consistency of the disks on"
571 def _ParseArgs(argv, commands, aliases):
572 """Parser for the command line arguments.
574 This function parses the arguments and returns the function which
575 must be executed together with its (modified) arguments.
577 @param argv: the command line
578 @param commands: dictionary with special contents, see the design
579 doc for cmdline handling
580 @param aliases: dictionary with command aliases {'alias': 'target, ...}
586 binary = argv[0].split("/")[-1]
588 if len(argv) > 1 and argv[1] == "--version":
589 ToStdout("%s (ganeti) %s", binary, constants.RELEASE_VERSION)
590 # Quit right away. That way we don't have to care about this special
591 # argument. optparse.py does it the same.
594 if len(argv) < 2 or not (argv[1] in commands or
596 # let's do a nice thing
597 sortedcmds = commands.keys()
600 ToStdout("Usage: %s {command} [options...] [argument...]", binary)
601 ToStdout("%s <command> --help to see details, or man %s", binary, binary)
604 # compute the max line length for cmd + usage
605 mlen = max([len(" %s" % cmd) for cmd in commands])
606 mlen = min(60, mlen) # should not get here...
608 # and format a nice command list
609 ToStdout("Commands:")
610 for cmd in sortedcmds:
611 cmdstr = " %s" % (cmd,)
612 help_text = commands[cmd][4]
613 help_lines = textwrap.wrap(help_text, 79 - 3 - mlen)
614 ToStdout("%-*s - %s", mlen, cmdstr, help_lines.pop(0))
615 for line in help_lines:
616 ToStdout("%-*s %s", mlen, "", line)
620 return None, None, None
622 # get command, unalias it, and look it up in commands
626 raise errors.ProgrammerError("Alias '%s' overrides an existing"
629 if aliases[cmd] not in commands:
630 raise errors.ProgrammerError("Alias '%s' maps to non-existing"
631 " command '%s'" % (cmd, aliases[cmd]))
635 func, args_def, parser_opts, usage, description = commands[cmd]
636 parser = OptionParser(option_list=parser_opts + [_DRY_RUN_OPT],
637 description=description,
638 formatter=TitledHelpFormatter(),
639 usage="%%prog %s %s" % (cmd, usage))
640 parser.disable_interspersed_args()
641 options, args = parser.parse_args()
643 if not _CheckArguments(cmd, args_def, args):
644 return None, None, None
646 return func, options, args
649 def _CheckArguments(cmd, args_def, args):
650 """Verifies the arguments using the argument definition.
654 1. Abort with error if values specified by user but none expected.
656 1. For each argument in definition
658 1. Keep running count of minimum number of values (min_count)
659 1. Keep running count of maximum number of values (max_count)
660 1. If it has an unlimited number of values
662 1. Abort with error if it's not the last argument in the definition
664 1. If last argument has limited number of values
666 1. Abort with error if number of values doesn't match or is too large
668 1. Abort with error if user didn't pass enough values (min_count)
671 if args and not args_def:
672 ToStderr("Error: Command %s expects no arguments", cmd)
679 last_idx = len(args_def) - 1
681 for idx, arg in enumerate(args_def):
682 if min_count is None:
684 elif arg.min is not None:
687 if max_count is None:
689 elif arg.max is not None:
693 check_max = (arg.max is not None)
695 elif arg.max is None:
696 raise errors.ProgrammerError("Only the last argument can have max=None")
699 # Command with exact number of arguments
700 if (min_count is not None and max_count is not None and
701 min_count == max_count and len(args) != min_count):
702 ToStderr("Error: Command %s expects %d argument(s)", cmd, min_count)
705 # Command with limited number of arguments
706 if max_count is not None and len(args) > max_count:
707 ToStderr("Error: Command %s expects only %d argument(s)",
711 # Command with some required arguments
712 if min_count is not None and len(args) < min_count:
713 ToStderr("Error: Command %s expects at least %d argument(s)",
720 def SplitNodeOption(value):
721 """Splits the value of a --node option.
724 if value and ':' in value:
725 return value.split(':', 1)
731 def wrapper(*args, **kwargs):
734 return fn(*args, **kwargs)
740 def AskUser(text, choices=None):
741 """Ask the user a question.
743 @param text: the question to ask
745 @param choices: list with elements tuples (input_char, return_value,
746 description); if not given, it will default to: [('y', True,
747 'Perform the operation'), ('n', False, 'Do no do the operation')];
748 note that the '?' char is reserved for help
750 @return: one of the return values from the choices list; if input is
751 not possible (i.e. not running with a tty, we return the last
756 choices = [('y', True, 'Perform the operation'),
757 ('n', False, 'Do not perform the operation')]
758 if not choices or not isinstance(choices, list):
759 raise errors.ProgrammerError("Invalid choices argument to AskUser")
760 for entry in choices:
761 if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
762 raise errors.ProgrammerError("Invalid choices element to AskUser")
764 answer = choices[-1][1]
766 for line in text.splitlines():
767 new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
768 text = "\n".join(new_text)
770 f = file("/dev/tty", "a+")
774 chars = [entry[0] for entry in choices]
775 chars[-1] = "[%s]" % chars[-1]
777 maps = dict([(entry[0], entry[1]) for entry in choices])
781 f.write("/".join(chars))
783 line = f.readline(2).strip().lower()
788 for entry in choices:
789 f.write(" %s - %s\n" % (entry[0], entry[2]))
797 class JobSubmittedException(Exception):
798 """Job was submitted, client should exit.
800 This exception has one argument, the ID of the job that was
801 submitted. The handler should print this ID.
803 This is not an error, just a structured way to exit from clients.
808 def SendJob(ops, cl=None):
809 """Function to submit an opcode without waiting for the results.
812 @param ops: list of opcodes
813 @type cl: luxi.Client
814 @param cl: the luxi client to use for communicating with the master;
815 if None, a new client will be created
821 job_id = cl.SubmitJob(ops)
826 def PollJob(job_id, cl=None, feedback_fn=None):
827 """Function to poll for the result of a job.
829 @type job_id: job identified
830 @param job_id: the job to poll for results
831 @type cl: luxi.Client
832 @param cl: the luxi client to use for communicating with the master;
833 if None, a new client will be created
840 prev_logmsg_serial = None
843 result = cl.WaitForJobChange(job_id, ["status"], prev_job_info,
846 # job not found, go away!
847 raise errors.JobLost("Job with id %s lost" % job_id)
849 # Split result, a tuple of (field values, log entries)
850 (job_info, log_entries) = result
851 (status, ) = job_info
854 for log_entry in log_entries:
855 (serial, timestamp, _, message) = log_entry
856 if callable(feedback_fn):
857 feedback_fn(log_entry[1:])
859 encoded = utils.SafeEncode(message)
860 ToStdout("%s %s", time.ctime(utils.MergeTime(timestamp)), encoded)
861 prev_logmsg_serial = max(prev_logmsg_serial, serial)
863 # TODO: Handle canceled and archived jobs
864 elif status in (constants.JOB_STATUS_SUCCESS,
865 constants.JOB_STATUS_ERROR,
866 constants.JOB_STATUS_CANCELING,
867 constants.JOB_STATUS_CANCELED):
870 prev_job_info = job_info
872 jobs = cl.QueryJobs([job_id], ["status", "opstatus", "opresult"])
874 raise errors.JobLost("Job with id %s lost" % job_id)
876 status, opstatus, result = jobs[0]
877 if status == constants.JOB_STATUS_SUCCESS:
879 elif status in (constants.JOB_STATUS_CANCELING,
880 constants.JOB_STATUS_CANCELED):
881 raise errors.OpExecError("Job was canceled")
884 for idx, (status, msg) in enumerate(zip(opstatus, result)):
885 if status == constants.OP_STATUS_SUCCESS:
887 elif status == constants.OP_STATUS_ERROR:
888 errors.MaybeRaise(msg)
890 raise errors.OpExecError("partial failure (opcode %d): %s" %
893 raise errors.OpExecError(str(msg))
894 # default failure mode
895 raise errors.OpExecError(result)
898 def SubmitOpCode(op, cl=None, feedback_fn=None):
899 """Legacy function to submit an opcode.
901 This is just a simple wrapper over the construction of the processor
902 instance. It should be extended to better handle feedback and
903 interaction functions.
909 job_id = SendJob([op], cl)
911 op_results = PollJob(job_id, cl=cl, feedback_fn=feedback_fn)
916 def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
917 """Wrapper around SubmitOpCode or SendJob.
919 This function will decide, based on the 'opts' parameter, whether to
920 submit and wait for the result of the opcode (and return it), or
921 whether to just send the job and print its identifier. It is used in
922 order to simplify the implementation of the '--submit' option.
924 It will also add the dry-run parameter from the options passed, if true.
927 if opts and opts.dry_run:
928 op.dry_run = opts.dry_run
929 if opts and opts.submit_only:
930 job_id = SendJob([op], cl=cl)
931 raise JobSubmittedException(job_id)
933 return SubmitOpCode(op, cl=cl, feedback_fn=feedback_fn)
937 # TODO: Cache object?
939 client = luxi.Client()
940 except luxi.NoMasterError:
941 master, myself = ssconf.GetMasterAndMyself()
943 raise errors.OpPrereqError("This is not the master node, please connect"
944 " to node '%s' and rerun the command" %
951 def FormatError(err):
952 """Return a formatted error message for a given error.
954 This function takes an exception instance and returns a tuple
955 consisting of two values: first, the recommended exit code, and
956 second, a string describing the error message (not
963 if isinstance(err, errors.ConfigurationError):
964 txt = "Corrupt configuration file: %s" % msg
966 obuf.write(txt + "\n")
967 obuf.write("Aborting.")
969 elif isinstance(err, errors.HooksAbort):
970 obuf.write("Failure: hooks execution failed:\n")
971 for node, script, out in err.args[0]:
973 obuf.write(" node: %s, script: %s, output: %s\n" %
976 obuf.write(" node: %s, script: %s (no output)\n" %
978 elif isinstance(err, errors.HooksFailure):
979 obuf.write("Failure: hooks general failure: %s" % msg)
980 elif isinstance(err, errors.ResolverError):
981 this_host = utils.HostInfo.SysName()
982 if err.args[0] == this_host:
983 msg = "Failure: can't resolve my own hostname ('%s')"
985 msg = "Failure: can't resolve hostname '%s'"
986 obuf.write(msg % err.args[0])
987 elif isinstance(err, errors.OpPrereqError):
988 obuf.write("Failure: prerequisites not met for this"
989 " operation:\n%s" % msg)
990 elif isinstance(err, errors.OpExecError):
991 obuf.write("Failure: command execution error:\n%s" % msg)
992 elif isinstance(err, errors.TagError):
993 obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
994 elif isinstance(err, errors.JobQueueDrainError):
995 obuf.write("Failure: the job queue is marked for drain and doesn't"
996 " accept new requests\n")
997 elif isinstance(err, errors.JobQueueFull):
998 obuf.write("Failure: the job queue is full and doesn't accept new"
999 " job submissions until old jobs are archived\n")
1000 elif isinstance(err, errors.TypeEnforcementError):
1001 obuf.write("Parameter Error: %s" % msg)
1002 elif isinstance(err, errors.ParameterError):
1003 obuf.write("Failure: unknown/wrong parameter name '%s'" % msg)
1004 elif isinstance(err, errors.GenericError):
1005 obuf.write("Unhandled Ganeti error: %s" % msg)
1006 elif isinstance(err, luxi.NoMasterError):
1007 obuf.write("Cannot communicate with the master daemon.\nIs it running"
1008 " and listening for connections?")
1009 elif isinstance(err, luxi.TimeoutError):
1010 obuf.write("Timeout while talking to the master daemon. Error:\n"
1012 elif isinstance(err, luxi.ProtocolError):
1013 obuf.write("Unhandled protocol error while talking to the master daemon:\n"
1015 elif isinstance(err, JobSubmittedException):
1016 obuf.write("JobID: %s\n" % err.args[0])
1019 obuf.write("Unhandled exception: %s" % msg)
1020 return retcode, obuf.getvalue().rstrip('\n')
1023 def GenericMain(commands, override=None, aliases=None):
1024 """Generic main function for all the gnt-* commands.
1027 - commands: a dictionary with a special structure, see the design doc
1028 for command line handling.
1029 - override: if not None, we expect a dictionary with keys that will
1030 override command line options; this can be used to pass
1031 options from the scripts to generic functions
1032 - aliases: dictionary with command aliases {'alias': 'target, ...}
1035 # save the program name and the entire command line for later logging
1037 binary = os.path.basename(sys.argv[0]) or sys.argv[0]
1038 if len(sys.argv) >= 2:
1039 binary += " " + sys.argv[1]
1040 old_cmdline = " ".join(sys.argv[2:])
1044 binary = "<unknown program>"
1051 func, options, args = _ParseArgs(sys.argv, commands, aliases)
1052 except errors.ParameterError, err:
1053 result, err_msg = FormatError(err)
1057 if func is None: # parse error
1060 if override is not None:
1061 for key, val in override.iteritems():
1062 setattr(options, key, val)
1064 utils.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
1065 stderr_logging=True, program=binary)
1068 logging.info("run with arguments '%s'", old_cmdline)
1070 logging.info("run with no arguments")
1073 result = func(options, args)
1074 except (errors.GenericError, luxi.ProtocolError,
1075 JobSubmittedException), err:
1076 result, err_msg = FormatError(err)
1077 logging.exception("Error during command processing")
1083 def GenerateTable(headers, fields, separator, data,
1084 numfields=None, unitfields=None,
1086 """Prints a table with headers and different fields.
1089 @param headers: dictionary mapping field names to headers for
1092 @param fields: the field names corresponding to each row in
1094 @param separator: the separator to be used; if this is None,
1095 the default 'smart' algorithm is used which computes optimal
1096 field width, otherwise just the separator is used between
1099 @param data: a list of lists, each sublist being one row to be output
1100 @type numfields: list
1101 @param numfields: a list with the fields that hold numeric
1102 values and thus should be right-aligned
1103 @type unitfields: list
1104 @param unitfields: a list with the fields that hold numeric
1105 values that should be formatted with the units field
1106 @type units: string or None
1107 @param units: the units we should use for formatting, or None for
1108 automatic choice (human-readable for non-separator usage, otherwise
1109 megabytes); this is a one-letter string
1118 if numfields is None:
1120 if unitfields is None:
1123 numfields = utils.FieldSet(*numfields)
1124 unitfields = utils.FieldSet(*unitfields)
1127 for field in fields:
1128 if headers and field not in headers:
1129 # TODO: handle better unknown fields (either revert to old
1130 # style of raising exception, or deal more intelligently with
1132 headers[field] = field
1133 if separator is not None:
1134 format_fields.append("%s")
1135 elif numfields.Matches(field):
1136 format_fields.append("%*s")
1138 format_fields.append("%-*s")
1140 if separator is None:
1141 mlens = [0 for name in fields]
1142 format = ' '.join(format_fields)
1144 format = separator.replace("%", "%%").join(format_fields)
1149 for idx, val in enumerate(row):
1150 if unitfields.Matches(fields[idx]):
1156 val = row[idx] = utils.FormatUnit(val, units)
1157 val = row[idx] = str(val)
1158 if separator is None:
1159 mlens[idx] = max(mlens[idx], len(val))
1164 for idx, name in enumerate(fields):
1166 if separator is None:
1167 mlens[idx] = max(mlens[idx], len(hdr))
1168 args.append(mlens[idx])
1170 result.append(format % tuple(args))
1175 line = ['-' for _ in fields]
1176 for idx in xrange(len(fields)):
1177 if separator is None:
1178 args.append(mlens[idx])
1179 args.append(line[idx])
1180 result.append(format % tuple(args))
1185 def FormatTimestamp(ts):
1186 """Formats a given timestamp.
1189 @param ts: a timeval-type timestamp, a tuple of seconds and microseconds
1192 @return: a string with the formatted timestamp
1195 if not isinstance (ts, (tuple, list)) or len(ts) != 2:
1198 return time.strftime("%F %T", time.localtime(sec)) + ".%06d" % usec
1201 def ParseTimespec(value):
1202 """Parse a time specification.
1204 The following suffixed will be recognized:
1212 Without any suffix, the value will be taken to be in seconds.
1217 raise errors.OpPrereqError("Empty time specification passed")
1225 if value[-1] not in suffix_map:
1229 raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1231 multiplier = suffix_map[value[-1]]
1233 if not value: # no data left after stripping the suffix
1234 raise errors.OpPrereqError("Invalid time specification (only"
1237 value = int(value) * multiplier
1239 raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1243 def GetOnlineNodes(nodes, cl=None, nowarn=False):
1244 """Returns the names of online nodes.
1246 This function will also log a warning on stderr with the names of
1249 @param nodes: if not empty, use only this subset of nodes (minus the
1251 @param cl: if not None, luxi client to use
1252 @type nowarn: boolean
1253 @param nowarn: by default, this function will output a note with the
1254 offline nodes that are skipped; if this parameter is True the
1255 note is not displayed
1261 result = cl.QueryNodes(names=nodes, fields=["name", "offline"],
1263 offline = [row[0] for row in result if row[1]]
1264 if offline and not nowarn:
1265 ToStderr("Note: skipping offline node(s): %s" % ", ".join(offline))
1266 return [row[0] for row in result if not row[1]]
1269 def _ToStream(stream, txt, *args):
1270 """Write a message to a stream, bypassing the logging system
1272 @type stream: file object
1273 @param stream: the file to which we should write
1275 @param txt: the message
1280 stream.write(txt % args)
1287 def ToStdout(txt, *args):
1288 """Write a message to stdout only, bypassing the logging system
1290 This is just a wrapper over _ToStream.
1293 @param txt: the message
1296 _ToStream(sys.stdout, txt, *args)
1299 def ToStderr(txt, *args):
1300 """Write a message to stderr only, bypassing the logging system
1302 This is just a wrapper over _ToStream.
1305 @param txt: the message
1308 _ToStream(sys.stderr, txt, *args)
1311 class JobExecutor(object):
1312 """Class which manages the submission and execution of multiple jobs.
1314 Note that instances of this class should not be reused between
1318 def __init__(self, cl=None, verbose=True):
1323 self.verbose = verbose
1326 def QueueJob(self, name, *ops):
1327 """Record a job for later submit.
1330 @param name: a description of the job, will be used in WaitJobSet
1332 self.queue.append((name, ops))
1334 def SubmitPending(self):
1335 """Submit all pending jobs.
1338 results = self.cl.SubmitManyJobs([row[1] for row in self.queue])
1339 for ((status, data), (name, _)) in zip(results, self.queue):
1340 self.jobs.append((status, data, name))
1342 def GetResults(self):
1343 """Wait for and return the results of all jobs.
1346 @return: list of tuples (success, job results), in the same order
1347 as the submitted jobs; if a job has failed, instead of the result
1348 there will be the error message
1352 self.SubmitPending()
1355 ok_jobs = [row[1] for row in self.jobs if row[0]]
1357 ToStdout("Submitted jobs %s", ", ".join(ok_jobs))
1358 for submit_status, jid, name in self.jobs:
1359 if not submit_status:
1360 ToStderr("Failed to submit job for %s: %s", name, jid)
1361 results.append((False, jid))
1364 ToStdout("Waiting for job %s for %s...", jid, name)
1366 job_result = PollJob(jid, cl=self.cl)
1368 except (errors.GenericError, luxi.ProtocolError), err:
1369 _, job_result = FormatError(err)
1371 # the error message will always be shown, verbose or not
1372 ToStderr("Job %s for %s has failed: %s", jid, name, job_result)
1374 results.append((success, job_result))
1377 def WaitOrShow(self, wait):
1378 """Wait for job results or only print the job IDs.
1381 @param wait: whether to wait or not
1385 return self.GetResults()
1388 self.SubmitPending()
1389 for status, result, name in self.jobs:
1391 ToStdout("%s: %s", result, name)
1393 ToStderr("Failure for %s: %s", name, result)