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",
80 # Generic functions for CLI programs
85 "JobSubmittedException",
90 # Formatting functions
91 "ToStderr", "ToStdout",
100 # command line options support infrastructure
101 "ARGS_MANY_INSTANCES",
115 "OPT_COMPL_INST_ADD_NODES",
116 "OPT_COMPL_MANY_NODES",
117 "OPT_COMPL_ONE_IALLOCATOR",
118 "OPT_COMPL_ONE_INSTANCE",
119 "OPT_COMPL_ONE_NODE",
130 def __init__(self, min=0, max=None):
135 return ("<%s min=%s max=%s>" %
136 (self.__class__.__name__, self.min, self.max))
139 class ArgSuggest(_Argument):
140 """Suggesting argument.
142 Value can be any of the ones passed to the constructor.
145 def __init__(self, min=0, max=None, choices=None):
146 _Argument.__init__(self, min=min, max=max)
147 self.choices = choices
150 return ("<%s min=%s max=%s choices=%r>" %
151 (self.__class__.__name__, self.min, self.max, self.choices))
154 class ArgChoice(ArgSuggest):
157 Value can be any of the ones passed to the constructor. Like L{ArgSuggest},
158 but value must be one of the choices.
163 class ArgUnknown(_Argument):
164 """Unknown argument to program (e.g. determined at runtime).
169 class ArgInstance(_Argument):
170 """Instances argument.
175 class ArgNode(_Argument):
180 class ArgJobId(_Argument):
186 class ArgFile(_Argument):
187 """File path argument.
192 class ArgCommand(_Argument):
198 class ArgHost(_Argument):
205 ARGS_MANY_INSTANCES = [ArgInstance()]
206 ARGS_MANY_NODES = [ArgNode()]
207 ARGS_ONE_INSTANCE = [ArgInstance(min=1, max=1)]
208 ARGS_ONE_NODE = [ArgNode(min=1, max=1)]
212 def _ExtractTagsObject(opts, args):
213 """Extract the tag type object.
215 Note that this function will modify its args parameter.
218 if not hasattr(opts, "tag_type"):
219 raise errors.ProgrammerError("tag_type not passed to _ExtractTagsObject")
221 if kind == constants.TAG_CLUSTER:
223 elif kind == constants.TAG_NODE or kind == constants.TAG_INSTANCE:
225 raise errors.OpPrereqError("no arguments passed to the command")
229 raise errors.ProgrammerError("Unhandled tag type '%s'" % kind)
233 def _ExtendTags(opts, args):
234 """Extend the args if a source file has been given.
236 This function will extend the tags with the contents of the file
237 passed in the 'tags_source' attribute of the opts parameter. A file
238 named '-' will be replaced by stdin.
241 fname = opts.tags_source
247 new_fh = open(fname, "r")
250 # we don't use the nice 'new_data = [line.strip() for line in fh]'
251 # because of python bug 1633941
253 line = new_fh.readline()
256 new_data.append(line.strip())
259 args.extend(new_data)
262 def ListTags(opts, args):
263 """List the tags on a given object.
265 This is a generic implementation that knows how to deal with all
266 three cases of tag objects (cluster, node, instance). The opts
267 argument is expected to contain a tag_type field denoting what
268 object type we work on.
271 kind, name = _ExtractTagsObject(opts, args)
272 op = opcodes.OpGetTags(kind=kind, name=name)
273 result = SubmitOpCode(op)
274 result = list(result)
280 def AddTags(opts, args):
281 """Add tags on a given object.
283 This is a generic implementation that knows how to deal with all
284 three cases of tag objects (cluster, node, instance). The opts
285 argument is expected to contain a tag_type field denoting what
286 object type we work on.
289 kind, name = _ExtractTagsObject(opts, args)
290 _ExtendTags(opts, args)
292 raise errors.OpPrereqError("No tags to be added")
293 op = opcodes.OpAddTags(kind=kind, name=name, tags=args)
297 def RemoveTags(opts, args):
298 """Remove tags from a given object.
300 This is a generic implementation that knows how to deal with all
301 three cases of tag objects (cluster, node, instance). The opts
302 argument is expected to contain a tag_type field denoting what
303 object type we work on.
306 kind, name = _ExtractTagsObject(opts, args)
307 _ExtendTags(opts, args)
309 raise errors.OpPrereqError("No tags to be removed")
310 op = opcodes.OpDelTags(kind=kind, name=name, tags=args)
314 def check_unit(option, opt, value):
315 """OptParsers custom converter for units.
319 return utils.ParseUnit(value)
320 except errors.UnitParseError, err:
321 raise OptionValueError("option %s: %s" % (opt, err))
324 def _SplitKeyVal(opt, data):
325 """Convert a KeyVal string into a dict.
327 This function will convert a key=val[,...] string into a dict. Empty
328 values will be converted specially: keys which have the prefix 'no_'
329 will have the value=False and the prefix stripped, the others will
333 @param opt: a string holding the option name for which we process the
334 data, used in building error messages
336 @param data: a string of the format key=val,key=val,...
338 @return: {key=val, key=val}
339 @raises errors.ParameterError: if there are duplicate keys
344 for elem in data.split(","):
346 key, val = elem.split("=", 1)
348 if elem.startswith(NO_PREFIX):
349 key, val = elem[len(NO_PREFIX):], False
350 elif elem.startswith(UN_PREFIX):
351 key, val = elem[len(UN_PREFIX):], None
353 key, val = elem, True
355 raise errors.ParameterError("Duplicate key '%s' in option %s" %
361 def check_ident_key_val(option, opt, value):
362 """Custom parser for ident:key=val,key=val options.
364 This will store the parsed values as a tuple (ident, {key: val}). As such,
365 multiple uses of this option via action=append is possible.
369 ident, rest = value, ''
371 ident, rest = value.split(":", 1)
373 if ident.startswith(NO_PREFIX):
375 msg = "Cannot pass options when removing parameter groups: %s" % value
376 raise errors.ParameterError(msg)
377 retval = (ident[len(NO_PREFIX):], False)
378 elif ident.startswith(UN_PREFIX):
380 msg = "Cannot pass options when removing parameter groups: %s" % value
381 raise errors.ParameterError(msg)
382 retval = (ident[len(UN_PREFIX):], None)
384 kv_dict = _SplitKeyVal(opt, rest)
385 retval = (ident, kv_dict)
389 def check_key_val(option, opt, value):
390 """Custom parser class for key=val,key=val options.
392 This will store the parsed values as a dict {key: val}.
395 return _SplitKeyVal(opt, value)
398 # completion_suggestion is normally a list. Using numeric values not evaluating
399 # to False for dynamic completion.
400 (OPT_COMPL_MANY_NODES,
402 OPT_COMPL_ONE_INSTANCE,
404 OPT_COMPL_ONE_IALLOCATOR,
405 OPT_COMPL_INST_ADD_NODES) = range(100, 106)
407 OPT_COMPL_ALL = frozenset([
408 OPT_COMPL_MANY_NODES,
410 OPT_COMPL_ONE_INSTANCE,
412 OPT_COMPL_ONE_IALLOCATOR,
413 OPT_COMPL_INST_ADD_NODES,
417 class CliOption(Option):
418 """Custom option class for optparse.
421 ATTRS = Option.ATTRS + [
422 "completion_suggest",
424 TYPES = Option.TYPES + (
429 TYPE_CHECKER = Option.TYPE_CHECKER.copy()
430 TYPE_CHECKER["identkeyval"] = check_ident_key_val
431 TYPE_CHECKER["keyval"] = check_key_val
432 TYPE_CHECKER["unit"] = check_unit
435 # optparse.py sets make_option, so we do it for our own option class, too
436 cli_option = CliOption
439 DEBUG_OPT = cli_option("-d", "--debug", default=False,
441 help="Turn debugging on")
443 NOHDR_OPT = cli_option("--no-headers", default=False,
444 action="store_true", dest="no_headers",
445 help="Don't display column headers")
447 SEP_OPT = cli_option("--separator", default=None,
448 action="store", dest="separator",
449 help=("Separator between output fields"
450 " (defaults to one space)"))
452 USEUNITS_OPT = cli_option("--units", default=None,
453 dest="units", choices=('h', 'm', 'g', 't'),
454 help="Specify units for output (one of hmgt)")
456 FIELDS_OPT = cli_option("-o", "--output", dest="output", action="store",
457 type="string", metavar="FIELDS",
458 help="Comma separated list of output fields")
460 FORCE_OPT = cli_option("-f", "--force", dest="force", action="store_true",
461 default=False, help="Force the operation")
463 CONFIRM_OPT = cli_option("--yes", dest="confirm", action="store_true",
464 default=False, help="Do not require confirmation")
466 TAG_SRC_OPT = cli_option("--from", dest="tags_source",
467 default=None, help="File with tag names")
469 SUBMIT_OPT = cli_option("--submit", dest="submit_only",
470 default=False, action="store_true",
471 help=("Submit the job and return the job ID, but"
472 " don't wait for the job to finish"))
474 SYNC_OPT = cli_option("--sync", dest="do_locking",
475 default=False, action="store_true",
476 help=("Grab locks while doing the queries"
477 " in order to ensure more consistent results"))
479 _DRY_RUN_OPT = cli_option("--dry-run", default=False,
481 help=("Do not execute the operation, just run the"
482 " check steps and verify it it could be"
485 VERBOSE_OPT = cli_option("-v", "--verbose", default=False,
487 help="Increase the verbosity of the operation")
489 DEBUG_SIMERR_OPT = cli_option("--debug-simulate-errors", default=False,
490 action="store_true", dest="simulate_errors",
491 help="Debugging option that makes the operation"
492 " treat most runtime checks as failed")
494 NWSYNC_OPT = cli_option("--no-wait-for-sync", dest="wait_for_sync",
495 default=True, action="store_false",
496 help="Don't wait for sync (DANGEROUS!)")
498 DISK_TEMPLATE_OPT = cli_option("-t", "--disk-template", dest="disk_template",
499 help="Custom disk setup (diskless, file,"
501 default=None, metavar="TEMPL",
502 choices=list(constants.DISK_TEMPLATES))
504 NONICS_OPT = cli_option("--no-nics", default=False, action="store_true",
505 help="Do not create any network cards for"
508 FILESTORE_DIR_OPT = cli_option("--file-storage-dir", dest="file_storage_dir",
509 help="Relative path under default cluster-wide"
510 " file storage dir to store file-based disks",
511 default=None, metavar="<DIR>")
513 FILESTORE_DRIVER_OPT = cli_option("--file-driver", dest="file_driver",
514 help="Driver to use for image files",
515 default="loop", metavar="<DRIVER>",
516 choices=list(constants.FILE_DRIVER))
518 IALLOCATOR_OPT = cli_option("-I", "--iallocator", metavar="<NAME>",
519 help="Select nodes for the instance automatically"
520 " using the <NAME> iallocator plugin",
521 default=None, type="string",
522 completion_suggest=OPT_COMPL_ONE_IALLOCATOR)
524 OS_OPT = cli_option("-o", "--os-type", dest="os", help="What OS to run",
526 completion_suggest=OPT_COMPL_ONE_OS)
528 BACKEND_OPT = cli_option("-B", "--backend-parameters", dest="beparams",
529 type="keyval", default={},
530 help="Backend parameters")
532 HVOPTS_OPT = cli_option("-H", "--hypervisor-parameters", type="keyval",
533 default={}, dest="hvparams",
534 help="Hypervisor parameters")
536 HYPERVISOR_OPT = cli_option("-H", "--hypervisor-parameters", dest="hypervisor",
537 help="Hypervisor and hypervisor options, in the"
538 " format hypervisor:option=value,option=value,...",
539 default=None, type="identkeyval")
541 HVLIST_OPT = cli_option("-H", "--hypervisor-parameters", dest="hvparams",
542 help="Hypervisor and hypervisor options, in the"
543 " format hypervisor:option=value,option=value,...",
544 default=[], action="append", type="identkeyval")
546 NOIPCHECK_OPT = cli_option("--no-ip-check", dest="ip_check", default=True,
547 action="store_false",
548 help="Don't check that the instance's IP"
551 NET_OPT = cli_option("--net",
552 help="NIC parameters", default=[],
553 dest="nics", action="append", type="identkeyval")
555 DISK_OPT = cli_option("--disk", help="Disk parameters", default=[],
556 dest="disks", action="append", type="identkeyval")
558 DISKIDX_OPT = cli_option("--disks", dest="disks", default=None,
559 help="Comma-separated list of disks"
560 " indices to act on (e.g. 0,2) (optional,"
561 " defaults to all disks)")
563 OS_SIZE_OPT = cli_option("-s", "--os-size", dest="sd_size",
564 help="Enforces a single-disk configuration using the"
565 " given disk size, in MiB unless a suffix is used",
566 default=None, type="unit", metavar="<size>")
568 IGNORE_CONSIST_OPT = cli_option("--ignore-consistency",
569 dest="ignore_consistency",
570 action="store_true", default=False,
571 help="Ignore the consistency of the disks on"
574 NONLIVE_OPT = cli_option("--non-live", dest="live",
575 default=True, action="store_false",
576 help="Do a non-live migration (this usually means"
577 " freeze the instance, save the state, transfer and"
578 " only then resume running on the secondary node)")
580 NODE_PLACEMENT_OPT = cli_option("-n", "--node", dest="node",
581 help="Target node and optional secondary node",
582 metavar="<pnode>[:<snode>]",
583 completion_suggest=OPT_COMPL_INST_ADD_NODES)
585 NODE_LIST_OPT = cli_option("-n", "--node", dest="nodes", default=[],
586 action="append", metavar="<node>",
587 help="Use only this node (can be used multiple"
588 " times, if not given defaults to all nodes)",
589 completion_suggest=OPT_COMPL_ONE_NODE)
591 SINGLE_NODE_OPT = cli_option("-n", "--node", dest="node", help="Target node",
593 completion_suggest=OPT_COMPL_ONE_NODE)
596 def _ParseArgs(argv, commands, aliases):
597 """Parser for the command line arguments.
599 This function parses the arguments and returns the function which
600 must be executed together with its (modified) arguments.
602 @param argv: the command line
603 @param commands: dictionary with special contents, see the design
604 doc for cmdline handling
605 @param aliases: dictionary with command aliases {'alias': 'target, ...}
611 binary = argv[0].split("/")[-1]
613 if len(argv) > 1 and argv[1] == "--version":
614 ToStdout("%s (ganeti) %s", binary, constants.RELEASE_VERSION)
615 # Quit right away. That way we don't have to care about this special
616 # argument. optparse.py does it the same.
619 if len(argv) < 2 or not (argv[1] in commands or
621 # let's do a nice thing
622 sortedcmds = commands.keys()
625 ToStdout("Usage: %s {command} [options...] [argument...]", binary)
626 ToStdout("%s <command> --help to see details, or man %s", binary, binary)
629 # compute the max line length for cmd + usage
630 mlen = max([len(" %s" % cmd) for cmd in commands])
631 mlen = min(60, mlen) # should not get here...
633 # and format a nice command list
634 ToStdout("Commands:")
635 for cmd in sortedcmds:
636 cmdstr = " %s" % (cmd,)
637 help_text = commands[cmd][4]
638 help_lines = textwrap.wrap(help_text, 79 - 3 - mlen)
639 ToStdout("%-*s - %s", mlen, cmdstr, help_lines.pop(0))
640 for line in help_lines:
641 ToStdout("%-*s %s", mlen, "", line)
645 return None, None, None
647 # get command, unalias it, and look it up in commands
651 raise errors.ProgrammerError("Alias '%s' overrides an existing"
654 if aliases[cmd] not in commands:
655 raise errors.ProgrammerError("Alias '%s' maps to non-existing"
656 " command '%s'" % (cmd, aliases[cmd]))
660 func, args_def, parser_opts, usage, description = commands[cmd]
661 parser = OptionParser(option_list=parser_opts + [_DRY_RUN_OPT],
662 description=description,
663 formatter=TitledHelpFormatter(),
664 usage="%%prog %s %s" % (cmd, usage))
665 parser.disable_interspersed_args()
666 options, args = parser.parse_args()
668 if not _CheckArguments(cmd, args_def, args):
669 return None, None, None
671 return func, options, args
674 def _CheckArguments(cmd, args_def, args):
675 """Verifies the arguments using the argument definition.
679 1. Abort with error if values specified by user but none expected.
681 1. For each argument in definition
683 1. Keep running count of minimum number of values (min_count)
684 1. Keep running count of maximum number of values (max_count)
685 1. If it has an unlimited number of values
687 1. Abort with error if it's not the last argument in the definition
689 1. If last argument has limited number of values
691 1. Abort with error if number of values doesn't match or is too large
693 1. Abort with error if user didn't pass enough values (min_count)
696 if args and not args_def:
697 ToStderr("Error: Command %s expects no arguments", cmd)
704 last_idx = len(args_def) - 1
706 for idx, arg in enumerate(args_def):
707 if min_count is None:
709 elif arg.min is not None:
712 if max_count is None:
714 elif arg.max is not None:
718 check_max = (arg.max is not None)
720 elif arg.max is None:
721 raise errors.ProgrammerError("Only the last argument can have max=None")
724 # Command with exact number of arguments
725 if (min_count is not None and max_count is not None and
726 min_count == max_count and len(args) != min_count):
727 ToStderr("Error: Command %s expects %d argument(s)", cmd, min_count)
730 # Command with limited number of arguments
731 if max_count is not None and len(args) > max_count:
732 ToStderr("Error: Command %s expects only %d argument(s)",
736 # Command with some required arguments
737 if min_count is not None and len(args) < min_count:
738 ToStderr("Error: Command %s expects at least %d argument(s)",
745 def SplitNodeOption(value):
746 """Splits the value of a --node option.
749 if value and ':' in value:
750 return value.split(':', 1)
756 def wrapper(*args, **kwargs):
759 return fn(*args, **kwargs)
765 def AskUser(text, choices=None):
766 """Ask the user a question.
768 @param text: the question to ask
770 @param choices: list with elements tuples (input_char, return_value,
771 description); if not given, it will default to: [('y', True,
772 'Perform the operation'), ('n', False, 'Do no do the operation')];
773 note that the '?' char is reserved for help
775 @return: one of the return values from the choices list; if input is
776 not possible (i.e. not running with a tty, we return the last
781 choices = [('y', True, 'Perform the operation'),
782 ('n', False, 'Do not perform the operation')]
783 if not choices or not isinstance(choices, list):
784 raise errors.ProgrammerError("Invalid choices argument to AskUser")
785 for entry in choices:
786 if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
787 raise errors.ProgrammerError("Invalid choices element to AskUser")
789 answer = choices[-1][1]
791 for line in text.splitlines():
792 new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
793 text = "\n".join(new_text)
795 f = file("/dev/tty", "a+")
799 chars = [entry[0] for entry in choices]
800 chars[-1] = "[%s]" % chars[-1]
802 maps = dict([(entry[0], entry[1]) for entry in choices])
806 f.write("/".join(chars))
808 line = f.readline(2).strip().lower()
813 for entry in choices:
814 f.write(" %s - %s\n" % (entry[0], entry[2]))
822 class JobSubmittedException(Exception):
823 """Job was submitted, client should exit.
825 This exception has one argument, the ID of the job that was
826 submitted. The handler should print this ID.
828 This is not an error, just a structured way to exit from clients.
833 def SendJob(ops, cl=None):
834 """Function to submit an opcode without waiting for the results.
837 @param ops: list of opcodes
838 @type cl: luxi.Client
839 @param cl: the luxi client to use for communicating with the master;
840 if None, a new client will be created
846 job_id = cl.SubmitJob(ops)
851 def PollJob(job_id, cl=None, feedback_fn=None):
852 """Function to poll for the result of a job.
854 @type job_id: job identified
855 @param job_id: the job to poll for results
856 @type cl: luxi.Client
857 @param cl: the luxi client to use for communicating with the master;
858 if None, a new client will be created
865 prev_logmsg_serial = None
868 result = cl.WaitForJobChange(job_id, ["status"], prev_job_info,
871 # job not found, go away!
872 raise errors.JobLost("Job with id %s lost" % job_id)
874 # Split result, a tuple of (field values, log entries)
875 (job_info, log_entries) = result
876 (status, ) = job_info
879 for log_entry in log_entries:
880 (serial, timestamp, _, message) = log_entry
881 if callable(feedback_fn):
882 feedback_fn(log_entry[1:])
884 encoded = utils.SafeEncode(message)
885 ToStdout("%s %s", time.ctime(utils.MergeTime(timestamp)), encoded)
886 prev_logmsg_serial = max(prev_logmsg_serial, serial)
888 # TODO: Handle canceled and archived jobs
889 elif status in (constants.JOB_STATUS_SUCCESS,
890 constants.JOB_STATUS_ERROR,
891 constants.JOB_STATUS_CANCELING,
892 constants.JOB_STATUS_CANCELED):
895 prev_job_info = job_info
897 jobs = cl.QueryJobs([job_id], ["status", "opstatus", "opresult"])
899 raise errors.JobLost("Job with id %s lost" % job_id)
901 status, opstatus, result = jobs[0]
902 if status == constants.JOB_STATUS_SUCCESS:
904 elif status in (constants.JOB_STATUS_CANCELING,
905 constants.JOB_STATUS_CANCELED):
906 raise errors.OpExecError("Job was canceled")
909 for idx, (status, msg) in enumerate(zip(opstatus, result)):
910 if status == constants.OP_STATUS_SUCCESS:
912 elif status == constants.OP_STATUS_ERROR:
913 errors.MaybeRaise(msg)
915 raise errors.OpExecError("partial failure (opcode %d): %s" %
918 raise errors.OpExecError(str(msg))
919 # default failure mode
920 raise errors.OpExecError(result)
923 def SubmitOpCode(op, cl=None, feedback_fn=None):
924 """Legacy function to submit an opcode.
926 This is just a simple wrapper over the construction of the processor
927 instance. It should be extended to better handle feedback and
928 interaction functions.
934 job_id = SendJob([op], cl)
936 op_results = PollJob(job_id, cl=cl, feedback_fn=feedback_fn)
941 def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
942 """Wrapper around SubmitOpCode or SendJob.
944 This function will decide, based on the 'opts' parameter, whether to
945 submit and wait for the result of the opcode (and return it), or
946 whether to just send the job and print its identifier. It is used in
947 order to simplify the implementation of the '--submit' option.
949 It will also add the dry-run parameter from the options passed, if true.
952 if opts and opts.dry_run:
953 op.dry_run = opts.dry_run
954 if opts and opts.submit_only:
955 job_id = SendJob([op], cl=cl)
956 raise JobSubmittedException(job_id)
958 return SubmitOpCode(op, cl=cl, feedback_fn=feedback_fn)
962 # TODO: Cache object?
964 client = luxi.Client()
965 except luxi.NoMasterError:
966 master, myself = ssconf.GetMasterAndMyself()
968 raise errors.OpPrereqError("This is not the master node, please connect"
969 " to node '%s' and rerun the command" %
976 def FormatError(err):
977 """Return a formatted error message for a given error.
979 This function takes an exception instance and returns a tuple
980 consisting of two values: first, the recommended exit code, and
981 second, a string describing the error message (not
988 if isinstance(err, errors.ConfigurationError):
989 txt = "Corrupt configuration file: %s" % msg
991 obuf.write(txt + "\n")
992 obuf.write("Aborting.")
994 elif isinstance(err, errors.HooksAbort):
995 obuf.write("Failure: hooks execution failed:\n")
996 for node, script, out in err.args[0]:
998 obuf.write(" node: %s, script: %s, output: %s\n" %
1001 obuf.write(" node: %s, script: %s (no output)\n" %
1003 elif isinstance(err, errors.HooksFailure):
1004 obuf.write("Failure: hooks general failure: %s" % msg)
1005 elif isinstance(err, errors.ResolverError):
1006 this_host = utils.HostInfo.SysName()
1007 if err.args[0] == this_host:
1008 msg = "Failure: can't resolve my own hostname ('%s')"
1010 msg = "Failure: can't resolve hostname '%s'"
1011 obuf.write(msg % err.args[0])
1012 elif isinstance(err, errors.OpPrereqError):
1013 obuf.write("Failure: prerequisites not met for this"
1014 " operation:\n%s" % msg)
1015 elif isinstance(err, errors.OpExecError):
1016 obuf.write("Failure: command execution error:\n%s" % msg)
1017 elif isinstance(err, errors.TagError):
1018 obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
1019 elif isinstance(err, errors.JobQueueDrainError):
1020 obuf.write("Failure: the job queue is marked for drain and doesn't"
1021 " accept new requests\n")
1022 elif isinstance(err, errors.JobQueueFull):
1023 obuf.write("Failure: the job queue is full and doesn't accept new"
1024 " job submissions until old jobs are archived\n")
1025 elif isinstance(err, errors.TypeEnforcementError):
1026 obuf.write("Parameter Error: %s" % msg)
1027 elif isinstance(err, errors.ParameterError):
1028 obuf.write("Failure: unknown/wrong parameter name '%s'" % msg)
1029 elif isinstance(err, errors.GenericError):
1030 obuf.write("Unhandled Ganeti error: %s" % msg)
1031 elif isinstance(err, luxi.NoMasterError):
1032 obuf.write("Cannot communicate with the master daemon.\nIs it running"
1033 " and listening for connections?")
1034 elif isinstance(err, luxi.TimeoutError):
1035 obuf.write("Timeout while talking to the master daemon. Error:\n"
1037 elif isinstance(err, luxi.ProtocolError):
1038 obuf.write("Unhandled protocol error while talking to the master daemon:\n"
1040 elif isinstance(err, JobSubmittedException):
1041 obuf.write("JobID: %s\n" % err.args[0])
1044 obuf.write("Unhandled exception: %s" % msg)
1045 return retcode, obuf.getvalue().rstrip('\n')
1048 def GenericMain(commands, override=None, aliases=None):
1049 """Generic main function for all the gnt-* commands.
1052 - commands: a dictionary with a special structure, see the design doc
1053 for command line handling.
1054 - override: if not None, we expect a dictionary with keys that will
1055 override command line options; this can be used to pass
1056 options from the scripts to generic functions
1057 - aliases: dictionary with command aliases {'alias': 'target, ...}
1060 # save the program name and the entire command line for later logging
1062 binary = os.path.basename(sys.argv[0]) or sys.argv[0]
1063 if len(sys.argv) >= 2:
1064 binary += " " + sys.argv[1]
1065 old_cmdline = " ".join(sys.argv[2:])
1069 binary = "<unknown program>"
1076 func, options, args = _ParseArgs(sys.argv, commands, aliases)
1077 except errors.ParameterError, err:
1078 result, err_msg = FormatError(err)
1082 if func is None: # parse error
1085 if override is not None:
1086 for key, val in override.iteritems():
1087 setattr(options, key, val)
1089 utils.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
1090 stderr_logging=True, program=binary)
1093 logging.info("run with arguments '%s'", old_cmdline)
1095 logging.info("run with no arguments")
1098 result = func(options, args)
1099 except (errors.GenericError, luxi.ProtocolError,
1100 JobSubmittedException), err:
1101 result, err_msg = FormatError(err)
1102 logging.exception("Error during command processing")
1108 def GenerateTable(headers, fields, separator, data,
1109 numfields=None, unitfields=None,
1111 """Prints a table with headers and different fields.
1114 @param headers: dictionary mapping field names to headers for
1117 @param fields: the field names corresponding to each row in
1119 @param separator: the separator to be used; if this is None,
1120 the default 'smart' algorithm is used which computes optimal
1121 field width, otherwise just the separator is used between
1124 @param data: a list of lists, each sublist being one row to be output
1125 @type numfields: list
1126 @param numfields: a list with the fields that hold numeric
1127 values and thus should be right-aligned
1128 @type unitfields: list
1129 @param unitfields: a list with the fields that hold numeric
1130 values that should be formatted with the units field
1131 @type units: string or None
1132 @param units: the units we should use for formatting, or None for
1133 automatic choice (human-readable for non-separator usage, otherwise
1134 megabytes); this is a one-letter string
1143 if numfields is None:
1145 if unitfields is None:
1148 numfields = utils.FieldSet(*numfields)
1149 unitfields = utils.FieldSet(*unitfields)
1152 for field in fields:
1153 if headers and field not in headers:
1154 # TODO: handle better unknown fields (either revert to old
1155 # style of raising exception, or deal more intelligently with
1157 headers[field] = field
1158 if separator is not None:
1159 format_fields.append("%s")
1160 elif numfields.Matches(field):
1161 format_fields.append("%*s")
1163 format_fields.append("%-*s")
1165 if separator is None:
1166 mlens = [0 for name in fields]
1167 format = ' '.join(format_fields)
1169 format = separator.replace("%", "%%").join(format_fields)
1174 for idx, val in enumerate(row):
1175 if unitfields.Matches(fields[idx]):
1181 val = row[idx] = utils.FormatUnit(val, units)
1182 val = row[idx] = str(val)
1183 if separator is None:
1184 mlens[idx] = max(mlens[idx], len(val))
1189 for idx, name in enumerate(fields):
1191 if separator is None:
1192 mlens[idx] = max(mlens[idx], len(hdr))
1193 args.append(mlens[idx])
1195 result.append(format % tuple(args))
1200 line = ['-' for _ in fields]
1201 for idx in xrange(len(fields)):
1202 if separator is None:
1203 args.append(mlens[idx])
1204 args.append(line[idx])
1205 result.append(format % tuple(args))
1210 def FormatTimestamp(ts):
1211 """Formats a given timestamp.
1214 @param ts: a timeval-type timestamp, a tuple of seconds and microseconds
1217 @return: a string with the formatted timestamp
1220 if not isinstance (ts, (tuple, list)) or len(ts) != 2:
1223 return time.strftime("%F %T", time.localtime(sec)) + ".%06d" % usec
1226 def ParseTimespec(value):
1227 """Parse a time specification.
1229 The following suffixed will be recognized:
1237 Without any suffix, the value will be taken to be in seconds.
1242 raise errors.OpPrereqError("Empty time specification passed")
1250 if value[-1] not in suffix_map:
1254 raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1256 multiplier = suffix_map[value[-1]]
1258 if not value: # no data left after stripping the suffix
1259 raise errors.OpPrereqError("Invalid time specification (only"
1262 value = int(value) * multiplier
1264 raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1268 def GetOnlineNodes(nodes, cl=None, nowarn=False):
1269 """Returns the names of online nodes.
1271 This function will also log a warning on stderr with the names of
1274 @param nodes: if not empty, use only this subset of nodes (minus the
1276 @param cl: if not None, luxi client to use
1277 @type nowarn: boolean
1278 @param nowarn: by default, this function will output a note with the
1279 offline nodes that are skipped; if this parameter is True the
1280 note is not displayed
1286 result = cl.QueryNodes(names=nodes, fields=["name", "offline"],
1288 offline = [row[0] for row in result if row[1]]
1289 if offline and not nowarn:
1290 ToStderr("Note: skipping offline node(s): %s" % ", ".join(offline))
1291 return [row[0] for row in result if not row[1]]
1294 def _ToStream(stream, txt, *args):
1295 """Write a message to a stream, bypassing the logging system
1297 @type stream: file object
1298 @param stream: the file to which we should write
1300 @param txt: the message
1305 stream.write(txt % args)
1312 def ToStdout(txt, *args):
1313 """Write a message to stdout only, bypassing the logging system
1315 This is just a wrapper over _ToStream.
1318 @param txt: the message
1321 _ToStream(sys.stdout, txt, *args)
1324 def ToStderr(txt, *args):
1325 """Write a message to stderr only, bypassing the logging system
1327 This is just a wrapper over _ToStream.
1330 @param txt: the message
1333 _ToStream(sys.stderr, txt, *args)
1336 class JobExecutor(object):
1337 """Class which manages the submission and execution of multiple jobs.
1339 Note that instances of this class should not be reused between
1343 def __init__(self, cl=None, verbose=True):
1348 self.verbose = verbose
1351 def QueueJob(self, name, *ops):
1352 """Record a job for later submit.
1355 @param name: a description of the job, will be used in WaitJobSet
1357 self.queue.append((name, ops))
1359 def SubmitPending(self):
1360 """Submit all pending jobs.
1363 results = self.cl.SubmitManyJobs([row[1] for row in self.queue])
1364 for ((status, data), (name, _)) in zip(results, self.queue):
1365 self.jobs.append((status, data, name))
1367 def GetResults(self):
1368 """Wait for and return the results of all jobs.
1371 @return: list of tuples (success, job results), in the same order
1372 as the submitted jobs; if a job has failed, instead of the result
1373 there will be the error message
1377 self.SubmitPending()
1380 ok_jobs = [row[1] for row in self.jobs if row[0]]
1382 ToStdout("Submitted jobs %s", ", ".join(ok_jobs))
1383 for submit_status, jid, name in self.jobs:
1384 if not submit_status:
1385 ToStderr("Failed to submit job for %s: %s", name, jid)
1386 results.append((False, jid))
1389 ToStdout("Waiting for job %s for %s...", jid, name)
1391 job_result = PollJob(jid, cl=self.cl)
1393 except (errors.GenericError, luxi.ProtocolError), err:
1394 _, job_result = FormatError(err)
1396 # the error message will always be shown, verbose or not
1397 ToStderr("Job %s for %s has failed: %s", jid, name, job_result)
1399 results.append((success, job_result))
1402 def WaitOrShow(self, wait):
1403 """Wait for job results or only print the job IDs.
1406 @param wait: whether to wait or not
1410 return self.GetResults()
1413 self.SubmitPending()
1414 for status, result, name in self.jobs:
1416 ToStdout("%s: %s", result, name)
1418 ToStderr("Failure for %s: %s", name, result)