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",
81 # Generic functions for CLI programs
86 "JobSubmittedException",
91 # Formatting functions
92 "ToStderr", "ToStdout",
101 # command line options support infrastructure
102 "ARGS_MANY_INSTANCES",
116 "OPT_COMPL_INST_ADD_NODES",
117 "OPT_COMPL_MANY_NODES",
118 "OPT_COMPL_ONE_IALLOCATOR",
119 "OPT_COMPL_ONE_INSTANCE",
120 "OPT_COMPL_ONE_NODE",
131 def __init__(self, min=0, max=None):
136 return ("<%s min=%s max=%s>" %
137 (self.__class__.__name__, self.min, self.max))
140 class ArgSuggest(_Argument):
141 """Suggesting argument.
143 Value can be any of the ones passed to the constructor.
146 def __init__(self, min=0, max=None, choices=None):
147 _Argument.__init__(self, min=min, max=max)
148 self.choices = choices
151 return ("<%s min=%s max=%s choices=%r>" %
152 (self.__class__.__name__, self.min, self.max, self.choices))
155 class ArgChoice(ArgSuggest):
158 Value can be any of the ones passed to the constructor. Like L{ArgSuggest},
159 but value must be one of the choices.
164 class ArgUnknown(_Argument):
165 """Unknown argument to program (e.g. determined at runtime).
170 class ArgInstance(_Argument):
171 """Instances argument.
176 class ArgNode(_Argument):
181 class ArgJobId(_Argument):
187 class ArgFile(_Argument):
188 """File path argument.
193 class ArgCommand(_Argument):
199 class ArgHost(_Argument):
206 ARGS_MANY_INSTANCES = [ArgInstance()]
207 ARGS_MANY_NODES = [ArgNode()]
208 ARGS_ONE_INSTANCE = [ArgInstance(min=1, max=1)]
209 ARGS_ONE_NODE = [ArgNode(min=1, max=1)]
213 def _ExtractTagsObject(opts, args):
214 """Extract the tag type object.
216 Note that this function will modify its args parameter.
219 if not hasattr(opts, "tag_type"):
220 raise errors.ProgrammerError("tag_type not passed to _ExtractTagsObject")
222 if kind == constants.TAG_CLUSTER:
224 elif kind == constants.TAG_NODE or kind == constants.TAG_INSTANCE:
226 raise errors.OpPrereqError("no arguments passed to the command")
230 raise errors.ProgrammerError("Unhandled tag type '%s'" % kind)
234 def _ExtendTags(opts, args):
235 """Extend the args if a source file has been given.
237 This function will extend the tags with the contents of the file
238 passed in the 'tags_source' attribute of the opts parameter. A file
239 named '-' will be replaced by stdin.
242 fname = opts.tags_source
248 new_fh = open(fname, "r")
251 # we don't use the nice 'new_data = [line.strip() for line in fh]'
252 # because of python bug 1633941
254 line = new_fh.readline()
257 new_data.append(line.strip())
260 args.extend(new_data)
263 def ListTags(opts, args):
264 """List the tags on a given object.
266 This is a generic implementation that knows how to deal with all
267 three cases of tag objects (cluster, node, instance). The opts
268 argument is expected to contain a tag_type field denoting what
269 object type we work on.
272 kind, name = _ExtractTagsObject(opts, args)
273 op = opcodes.OpGetTags(kind=kind, name=name)
274 result = SubmitOpCode(op)
275 result = list(result)
281 def AddTags(opts, args):
282 """Add tags on a given object.
284 This is a generic implementation that knows how to deal with all
285 three cases of tag objects (cluster, node, instance). The opts
286 argument is expected to contain a tag_type field denoting what
287 object type we work on.
290 kind, name = _ExtractTagsObject(opts, args)
291 _ExtendTags(opts, args)
293 raise errors.OpPrereqError("No tags to be added")
294 op = opcodes.OpAddTags(kind=kind, name=name, tags=args)
298 def RemoveTags(opts, args):
299 """Remove tags from a given object.
301 This is a generic implementation that knows how to deal with all
302 three cases of tag objects (cluster, node, instance). The opts
303 argument is expected to contain a tag_type field denoting what
304 object type we work on.
307 kind, name = _ExtractTagsObject(opts, args)
308 _ExtendTags(opts, args)
310 raise errors.OpPrereqError("No tags to be removed")
311 op = opcodes.OpDelTags(kind=kind, name=name, tags=args)
315 def check_unit(option, opt, value):
316 """OptParsers custom converter for units.
320 return utils.ParseUnit(value)
321 except errors.UnitParseError, err:
322 raise OptionValueError("option %s: %s" % (opt, err))
325 def _SplitKeyVal(opt, data):
326 """Convert a KeyVal string into a dict.
328 This function will convert a key=val[,...] string into a dict. Empty
329 values will be converted specially: keys which have the prefix 'no_'
330 will have the value=False and the prefix stripped, the others will
334 @param opt: a string holding the option name for which we process the
335 data, used in building error messages
337 @param data: a string of the format key=val,key=val,...
339 @return: {key=val, key=val}
340 @raises errors.ParameterError: if there are duplicate keys
345 for elem in data.split(","):
347 key, val = elem.split("=", 1)
349 if elem.startswith(NO_PREFIX):
350 key, val = elem[len(NO_PREFIX):], False
351 elif elem.startswith(UN_PREFIX):
352 key, val = elem[len(UN_PREFIX):], None
354 key, val = elem, True
356 raise errors.ParameterError("Duplicate key '%s' in option %s" %
362 def check_ident_key_val(option, opt, value):
363 """Custom parser for ident:key=val,key=val options.
365 This will store the parsed values as a tuple (ident, {key: val}). As such,
366 multiple uses of this option via action=append is possible.
370 ident, rest = value, ''
372 ident, rest = value.split(":", 1)
374 if ident.startswith(NO_PREFIX):
376 msg = "Cannot pass options when removing parameter groups: %s" % value
377 raise errors.ParameterError(msg)
378 retval = (ident[len(NO_PREFIX):], False)
379 elif ident.startswith(UN_PREFIX):
381 msg = "Cannot pass options when removing parameter groups: %s" % value
382 raise errors.ParameterError(msg)
383 retval = (ident[len(UN_PREFIX):], None)
385 kv_dict = _SplitKeyVal(opt, rest)
386 retval = (ident, kv_dict)
390 def check_key_val(option, opt, value):
391 """Custom parser class for key=val,key=val options.
393 This will store the parsed values as a dict {key: val}.
396 return _SplitKeyVal(opt, value)
399 # completion_suggestion is normally a list. Using numeric values not evaluating
400 # to False for dynamic completion.
401 (OPT_COMPL_MANY_NODES,
403 OPT_COMPL_ONE_INSTANCE,
405 OPT_COMPL_ONE_IALLOCATOR,
406 OPT_COMPL_INST_ADD_NODES) = range(100, 106)
408 OPT_COMPL_ALL = frozenset([
409 OPT_COMPL_MANY_NODES,
411 OPT_COMPL_ONE_INSTANCE,
413 OPT_COMPL_ONE_IALLOCATOR,
414 OPT_COMPL_INST_ADD_NODES,
418 class CliOption(Option):
419 """Custom option class for optparse.
422 ATTRS = Option.ATTRS + [
423 "completion_suggest",
425 TYPES = Option.TYPES + (
430 TYPE_CHECKER = Option.TYPE_CHECKER.copy()
431 TYPE_CHECKER["identkeyval"] = check_ident_key_val
432 TYPE_CHECKER["keyval"] = check_key_val
433 TYPE_CHECKER["unit"] = check_unit
436 # optparse.py sets make_option, so we do it for our own option class, too
437 cli_option = CliOption
440 DEBUG_OPT = cli_option("-d", "--debug", default=False,
442 help="Turn debugging on")
444 NOHDR_OPT = cli_option("--no-headers", default=False,
445 action="store_true", dest="no_headers",
446 help="Don't display column headers")
448 SEP_OPT = cli_option("--separator", default=None,
449 action="store", dest="separator",
450 help=("Separator between output fields"
451 " (defaults to one space)"))
453 USEUNITS_OPT = cli_option("--units", default=None,
454 dest="units", choices=('h', 'm', 'g', 't'),
455 help="Specify units for output (one of hmgt)")
457 FIELDS_OPT = cli_option("-o", "--output", dest="output", action="store",
458 type="string", metavar="FIELDS",
459 help="Comma separated list of output fields")
461 FORCE_OPT = cli_option("-f", "--force", dest="force", action="store_true",
462 default=False, help="Force the operation")
464 CONFIRM_OPT = cli_option("--yes", dest="confirm", action="store_true",
465 default=False, help="Do not require confirmation")
467 TAG_SRC_OPT = cli_option("--from", dest="tags_source",
468 default=None, help="File with tag names")
470 SUBMIT_OPT = cli_option("--submit", dest="submit_only",
471 default=False, action="store_true",
472 help=("Submit the job and return the job ID, but"
473 " don't wait for the job to finish"))
475 SYNC_OPT = cli_option("--sync", dest="do_locking",
476 default=False, action="store_true",
477 help=("Grab locks while doing the queries"
478 " in order to ensure more consistent results"))
480 _DRY_RUN_OPT = cli_option("--dry-run", default=False,
482 help=("Do not execute the operation, just run the"
483 " check steps and verify it it could be"
486 VERBOSE_OPT = cli_option("-v", "--verbose", default=False,
488 help="Increase the verbosity of the operation")
490 DEBUG_SIMERR_OPT = cli_option("--debug-simulate-errors", default=False,
491 action="store_true", dest="simulate_errors",
492 help="Debugging option that makes the operation"
493 " treat most runtime checks as failed")
495 NWSYNC_OPT = cli_option("--no-wait-for-sync", dest="wait_for_sync",
496 default=True, action="store_false",
497 help="Don't wait for sync (DANGEROUS!)")
499 DISK_TEMPLATE_OPT = cli_option("-t", "--disk-template", dest="disk_template",
500 help="Custom disk setup (diskless, file,"
502 default=None, metavar="TEMPL",
503 choices=list(constants.DISK_TEMPLATES))
505 NONICS_OPT = cli_option("--no-nics", default=False, action="store_true",
506 help="Do not create any network cards for"
509 FILESTORE_DIR_OPT = cli_option("--file-storage-dir", dest="file_storage_dir",
510 help="Relative path under default cluster-wide"
511 " file storage dir to store file-based disks",
512 default=None, metavar="<DIR>")
514 FILESTORE_DRIVER_OPT = cli_option("--file-driver", dest="file_driver",
515 help="Driver to use for image files",
516 default="loop", metavar="<DRIVER>",
517 choices=list(constants.FILE_DRIVER))
519 IALLOCATOR_OPT = cli_option("-I", "--iallocator", metavar="<NAME>",
520 help="Select nodes for the instance automatically"
521 " using the <NAME> iallocator plugin",
522 default=None, type="string",
523 completion_suggest=OPT_COMPL_ONE_IALLOCATOR)
525 OS_OPT = cli_option("-o", "--os-type", dest="os", help="What OS to run",
527 completion_suggest=OPT_COMPL_ONE_OS)
529 BACKEND_OPT = cli_option("-B", "--backend-parameters", dest="beparams",
530 type="keyval", default={},
531 help="Backend parameters")
533 HVOPTS_OPT = cli_option("-H", "--hypervisor-parameters", type="keyval",
534 default={}, dest="hvparams",
535 help="Hypervisor parameters")
537 HYPERVISOR_OPT = cli_option("-H", "--hypervisor-parameters", dest="hypervisor",
538 help="Hypervisor and hypervisor options, in the"
539 " format hypervisor:option=value,option=value,...",
540 default=None, type="identkeyval")
542 HVLIST_OPT = cli_option("-H", "--hypervisor-parameters", dest="hvparams",
543 help="Hypervisor and hypervisor options, in the"
544 " format hypervisor:option=value,option=value,...",
545 default=[], action="append", type="identkeyval")
547 NOIPCHECK_OPT = cli_option("--no-ip-check", dest="ip_check", default=True,
548 action="store_false",
549 help="Don't check that the instance's IP"
552 NET_OPT = cli_option("--net",
553 help="NIC parameters", default=[],
554 dest="nics", action="append", type="identkeyval")
556 DISK_OPT = cli_option("--disk", help="Disk parameters", default=[],
557 dest="disks", action="append", type="identkeyval")
559 DISKIDX_OPT = cli_option("--disks", dest="disks", default=None,
560 help="Comma-separated list of disks"
561 " indices to act on (e.g. 0,2) (optional,"
562 " defaults to all disks)")
564 OS_SIZE_OPT = cli_option("-s", "--os-size", dest="sd_size",
565 help="Enforces a single-disk configuration using the"
566 " given disk size, in MiB unless a suffix is used",
567 default=None, type="unit", metavar="<size>")
569 IGNORE_CONSIST_OPT = cli_option("--ignore-consistency",
570 dest="ignore_consistency",
571 action="store_true", default=False,
572 help="Ignore the consistency of the disks on"
575 NONLIVE_OPT = cli_option("--non-live", dest="live",
576 default=True, action="store_false",
577 help="Do a non-live migration (this usually means"
578 " freeze the instance, save the state, transfer and"
579 " only then resume running on the secondary node)")
581 NODE_PLACEMENT_OPT = cli_option("-n", "--node", dest="node",
582 help="Target node and optional secondary node",
583 metavar="<pnode>[:<snode>]",
584 completion_suggest=OPT_COMPL_INST_ADD_NODES)
586 NODE_LIST_OPT = cli_option("-n", "--node", dest="nodes", default=[],
587 action="append", metavar="<node>",
588 help="Use only this node (can be used multiple"
589 " times, if not given defaults to all nodes)",
590 completion_suggest=OPT_COMPL_ONE_NODE)
592 SINGLE_NODE_OPT = cli_option("-n", "--node", dest="node", help="Target node",
594 completion_suggest=OPT_COMPL_ONE_NODE)
596 NOSTART_OPT = cli_option("--no-start", dest="start", default=True,
597 action="store_false",
598 help="Don't start the instance after creation")
601 def _ParseArgs(argv, commands, aliases):
602 """Parser for the command line arguments.
604 This function parses the arguments and returns the function which
605 must be executed together with its (modified) arguments.
607 @param argv: the command line
608 @param commands: dictionary with special contents, see the design
609 doc for cmdline handling
610 @param aliases: dictionary with command aliases {'alias': 'target, ...}
616 binary = argv[0].split("/")[-1]
618 if len(argv) > 1 and argv[1] == "--version":
619 ToStdout("%s (ganeti) %s", binary, constants.RELEASE_VERSION)
620 # Quit right away. That way we don't have to care about this special
621 # argument. optparse.py does it the same.
624 if len(argv) < 2 or not (argv[1] in commands or
626 # let's do a nice thing
627 sortedcmds = commands.keys()
630 ToStdout("Usage: %s {command} [options...] [argument...]", binary)
631 ToStdout("%s <command> --help to see details, or man %s", binary, binary)
634 # compute the max line length for cmd + usage
635 mlen = max([len(" %s" % cmd) for cmd in commands])
636 mlen = min(60, mlen) # should not get here...
638 # and format a nice command list
639 ToStdout("Commands:")
640 for cmd in sortedcmds:
641 cmdstr = " %s" % (cmd,)
642 help_text = commands[cmd][4]
643 help_lines = textwrap.wrap(help_text, 79 - 3 - mlen)
644 ToStdout("%-*s - %s", mlen, cmdstr, help_lines.pop(0))
645 for line in help_lines:
646 ToStdout("%-*s %s", mlen, "", line)
650 return None, None, None
652 # get command, unalias it, and look it up in commands
656 raise errors.ProgrammerError("Alias '%s' overrides an existing"
659 if aliases[cmd] not in commands:
660 raise errors.ProgrammerError("Alias '%s' maps to non-existing"
661 " command '%s'" % (cmd, aliases[cmd]))
665 func, args_def, parser_opts, usage, description = commands[cmd]
666 parser = OptionParser(option_list=parser_opts + [_DRY_RUN_OPT],
667 description=description,
668 formatter=TitledHelpFormatter(),
669 usage="%%prog %s %s" % (cmd, usage))
670 parser.disable_interspersed_args()
671 options, args = parser.parse_args()
673 if not _CheckArguments(cmd, args_def, args):
674 return None, None, None
676 return func, options, args
679 def _CheckArguments(cmd, args_def, args):
680 """Verifies the arguments using the argument definition.
684 1. Abort with error if values specified by user but none expected.
686 1. For each argument in definition
688 1. Keep running count of minimum number of values (min_count)
689 1. Keep running count of maximum number of values (max_count)
690 1. If it has an unlimited number of values
692 1. Abort with error if it's not the last argument in the definition
694 1. If last argument has limited number of values
696 1. Abort with error if number of values doesn't match or is too large
698 1. Abort with error if user didn't pass enough values (min_count)
701 if args and not args_def:
702 ToStderr("Error: Command %s expects no arguments", cmd)
709 last_idx = len(args_def) - 1
711 for idx, arg in enumerate(args_def):
712 if min_count is None:
714 elif arg.min is not None:
717 if max_count is None:
719 elif arg.max is not None:
723 check_max = (arg.max is not None)
725 elif arg.max is None:
726 raise errors.ProgrammerError("Only the last argument can have max=None")
729 # Command with exact number of arguments
730 if (min_count is not None and max_count is not None and
731 min_count == max_count and len(args) != min_count):
732 ToStderr("Error: Command %s expects %d argument(s)", cmd, min_count)
735 # Command with limited number of arguments
736 if max_count is not None and len(args) > max_count:
737 ToStderr("Error: Command %s expects only %d argument(s)",
741 # Command with some required arguments
742 if min_count is not None and len(args) < min_count:
743 ToStderr("Error: Command %s expects at least %d argument(s)",
750 def SplitNodeOption(value):
751 """Splits the value of a --node option.
754 if value and ':' in value:
755 return value.split(':', 1)
761 def wrapper(*args, **kwargs):
764 return fn(*args, **kwargs)
770 def AskUser(text, choices=None):
771 """Ask the user a question.
773 @param text: the question to ask
775 @param choices: list with elements tuples (input_char, return_value,
776 description); if not given, it will default to: [('y', True,
777 'Perform the operation'), ('n', False, 'Do no do the operation')];
778 note that the '?' char is reserved for help
780 @return: one of the return values from the choices list; if input is
781 not possible (i.e. not running with a tty, we return the last
786 choices = [('y', True, 'Perform the operation'),
787 ('n', False, 'Do not perform the operation')]
788 if not choices or not isinstance(choices, list):
789 raise errors.ProgrammerError("Invalid choices argument to AskUser")
790 for entry in choices:
791 if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
792 raise errors.ProgrammerError("Invalid choices element to AskUser")
794 answer = choices[-1][1]
796 for line in text.splitlines():
797 new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
798 text = "\n".join(new_text)
800 f = file("/dev/tty", "a+")
804 chars = [entry[0] for entry in choices]
805 chars[-1] = "[%s]" % chars[-1]
807 maps = dict([(entry[0], entry[1]) for entry in choices])
811 f.write("/".join(chars))
813 line = f.readline(2).strip().lower()
818 for entry in choices:
819 f.write(" %s - %s\n" % (entry[0], entry[2]))
827 class JobSubmittedException(Exception):
828 """Job was submitted, client should exit.
830 This exception has one argument, the ID of the job that was
831 submitted. The handler should print this ID.
833 This is not an error, just a structured way to exit from clients.
838 def SendJob(ops, cl=None):
839 """Function to submit an opcode without waiting for the results.
842 @param ops: list of opcodes
843 @type cl: luxi.Client
844 @param cl: the luxi client to use for communicating with the master;
845 if None, a new client will be created
851 job_id = cl.SubmitJob(ops)
856 def PollJob(job_id, cl=None, feedback_fn=None):
857 """Function to poll for the result of a job.
859 @type job_id: job identified
860 @param job_id: the job to poll for results
861 @type cl: luxi.Client
862 @param cl: the luxi client to use for communicating with the master;
863 if None, a new client will be created
870 prev_logmsg_serial = None
873 result = cl.WaitForJobChange(job_id, ["status"], prev_job_info,
876 # job not found, go away!
877 raise errors.JobLost("Job with id %s lost" % job_id)
879 # Split result, a tuple of (field values, log entries)
880 (job_info, log_entries) = result
881 (status, ) = job_info
884 for log_entry in log_entries:
885 (serial, timestamp, _, message) = log_entry
886 if callable(feedback_fn):
887 feedback_fn(log_entry[1:])
889 encoded = utils.SafeEncode(message)
890 ToStdout("%s %s", time.ctime(utils.MergeTime(timestamp)), encoded)
891 prev_logmsg_serial = max(prev_logmsg_serial, serial)
893 # TODO: Handle canceled and archived jobs
894 elif status in (constants.JOB_STATUS_SUCCESS,
895 constants.JOB_STATUS_ERROR,
896 constants.JOB_STATUS_CANCELING,
897 constants.JOB_STATUS_CANCELED):
900 prev_job_info = job_info
902 jobs = cl.QueryJobs([job_id], ["status", "opstatus", "opresult"])
904 raise errors.JobLost("Job with id %s lost" % job_id)
906 status, opstatus, result = jobs[0]
907 if status == constants.JOB_STATUS_SUCCESS:
909 elif status in (constants.JOB_STATUS_CANCELING,
910 constants.JOB_STATUS_CANCELED):
911 raise errors.OpExecError("Job was canceled")
914 for idx, (status, msg) in enumerate(zip(opstatus, result)):
915 if status == constants.OP_STATUS_SUCCESS:
917 elif status == constants.OP_STATUS_ERROR:
918 errors.MaybeRaise(msg)
920 raise errors.OpExecError("partial failure (opcode %d): %s" %
923 raise errors.OpExecError(str(msg))
924 # default failure mode
925 raise errors.OpExecError(result)
928 def SubmitOpCode(op, cl=None, feedback_fn=None):
929 """Legacy function to submit an opcode.
931 This is just a simple wrapper over the construction of the processor
932 instance. It should be extended to better handle feedback and
933 interaction functions.
939 job_id = SendJob([op], cl)
941 op_results = PollJob(job_id, cl=cl, feedback_fn=feedback_fn)
946 def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
947 """Wrapper around SubmitOpCode or SendJob.
949 This function will decide, based on the 'opts' parameter, whether to
950 submit and wait for the result of the opcode (and return it), or
951 whether to just send the job and print its identifier. It is used in
952 order to simplify the implementation of the '--submit' option.
954 It will also add the dry-run parameter from the options passed, if true.
957 if opts and opts.dry_run:
958 op.dry_run = opts.dry_run
959 if opts and opts.submit_only:
960 job_id = SendJob([op], cl=cl)
961 raise JobSubmittedException(job_id)
963 return SubmitOpCode(op, cl=cl, feedback_fn=feedback_fn)
967 # TODO: Cache object?
969 client = luxi.Client()
970 except luxi.NoMasterError:
971 master, myself = ssconf.GetMasterAndMyself()
973 raise errors.OpPrereqError("This is not the master node, please connect"
974 " to node '%s' and rerun the command" %
981 def FormatError(err):
982 """Return a formatted error message for a given error.
984 This function takes an exception instance and returns a tuple
985 consisting of two values: first, the recommended exit code, and
986 second, a string describing the error message (not
993 if isinstance(err, errors.ConfigurationError):
994 txt = "Corrupt configuration file: %s" % msg
996 obuf.write(txt + "\n")
997 obuf.write("Aborting.")
999 elif isinstance(err, errors.HooksAbort):
1000 obuf.write("Failure: hooks execution failed:\n")
1001 for node, script, out in err.args[0]:
1003 obuf.write(" node: %s, script: %s, output: %s\n" %
1004 (node, script, out))
1006 obuf.write(" node: %s, script: %s (no output)\n" %
1008 elif isinstance(err, errors.HooksFailure):
1009 obuf.write("Failure: hooks general failure: %s" % msg)
1010 elif isinstance(err, errors.ResolverError):
1011 this_host = utils.HostInfo.SysName()
1012 if err.args[0] == this_host:
1013 msg = "Failure: can't resolve my own hostname ('%s')"
1015 msg = "Failure: can't resolve hostname '%s'"
1016 obuf.write(msg % err.args[0])
1017 elif isinstance(err, errors.OpPrereqError):
1018 obuf.write("Failure: prerequisites not met for this"
1019 " operation:\n%s" % msg)
1020 elif isinstance(err, errors.OpExecError):
1021 obuf.write("Failure: command execution error:\n%s" % msg)
1022 elif isinstance(err, errors.TagError):
1023 obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
1024 elif isinstance(err, errors.JobQueueDrainError):
1025 obuf.write("Failure: the job queue is marked for drain and doesn't"
1026 " accept new requests\n")
1027 elif isinstance(err, errors.JobQueueFull):
1028 obuf.write("Failure: the job queue is full and doesn't accept new"
1029 " job submissions until old jobs are archived\n")
1030 elif isinstance(err, errors.TypeEnforcementError):
1031 obuf.write("Parameter Error: %s" % msg)
1032 elif isinstance(err, errors.ParameterError):
1033 obuf.write("Failure: unknown/wrong parameter name '%s'" % msg)
1034 elif isinstance(err, errors.GenericError):
1035 obuf.write("Unhandled Ganeti error: %s" % msg)
1036 elif isinstance(err, luxi.NoMasterError):
1037 obuf.write("Cannot communicate with the master daemon.\nIs it running"
1038 " and listening for connections?")
1039 elif isinstance(err, luxi.TimeoutError):
1040 obuf.write("Timeout while talking to the master daemon. Error:\n"
1042 elif isinstance(err, luxi.ProtocolError):
1043 obuf.write("Unhandled protocol error while talking to the master daemon:\n"
1045 elif isinstance(err, JobSubmittedException):
1046 obuf.write("JobID: %s\n" % err.args[0])
1049 obuf.write("Unhandled exception: %s" % msg)
1050 return retcode, obuf.getvalue().rstrip('\n')
1053 def GenericMain(commands, override=None, aliases=None):
1054 """Generic main function for all the gnt-* commands.
1057 - commands: a dictionary with a special structure, see the design doc
1058 for command line handling.
1059 - override: if not None, we expect a dictionary with keys that will
1060 override command line options; this can be used to pass
1061 options from the scripts to generic functions
1062 - aliases: dictionary with command aliases {'alias': 'target, ...}
1065 # save the program name and the entire command line for later logging
1067 binary = os.path.basename(sys.argv[0]) or sys.argv[0]
1068 if len(sys.argv) >= 2:
1069 binary += " " + sys.argv[1]
1070 old_cmdline = " ".join(sys.argv[2:])
1074 binary = "<unknown program>"
1081 func, options, args = _ParseArgs(sys.argv, commands, aliases)
1082 except errors.ParameterError, err:
1083 result, err_msg = FormatError(err)
1087 if func is None: # parse error
1090 if override is not None:
1091 for key, val in override.iteritems():
1092 setattr(options, key, val)
1094 utils.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
1095 stderr_logging=True, program=binary)
1098 logging.info("run with arguments '%s'", old_cmdline)
1100 logging.info("run with no arguments")
1103 result = func(options, args)
1104 except (errors.GenericError, luxi.ProtocolError,
1105 JobSubmittedException), err:
1106 result, err_msg = FormatError(err)
1107 logging.exception("Error during command processing")
1113 def GenerateTable(headers, fields, separator, data,
1114 numfields=None, unitfields=None,
1116 """Prints a table with headers and different fields.
1119 @param headers: dictionary mapping field names to headers for
1122 @param fields: the field names corresponding to each row in
1124 @param separator: the separator to be used; if this is None,
1125 the default 'smart' algorithm is used which computes optimal
1126 field width, otherwise just the separator is used between
1129 @param data: a list of lists, each sublist being one row to be output
1130 @type numfields: list
1131 @param numfields: a list with the fields that hold numeric
1132 values and thus should be right-aligned
1133 @type unitfields: list
1134 @param unitfields: a list with the fields that hold numeric
1135 values that should be formatted with the units field
1136 @type units: string or None
1137 @param units: the units we should use for formatting, or None for
1138 automatic choice (human-readable for non-separator usage, otherwise
1139 megabytes); this is a one-letter string
1148 if numfields is None:
1150 if unitfields is None:
1153 numfields = utils.FieldSet(*numfields)
1154 unitfields = utils.FieldSet(*unitfields)
1157 for field in fields:
1158 if headers and field not in headers:
1159 # TODO: handle better unknown fields (either revert to old
1160 # style of raising exception, or deal more intelligently with
1162 headers[field] = field
1163 if separator is not None:
1164 format_fields.append("%s")
1165 elif numfields.Matches(field):
1166 format_fields.append("%*s")
1168 format_fields.append("%-*s")
1170 if separator is None:
1171 mlens = [0 for name in fields]
1172 format = ' '.join(format_fields)
1174 format = separator.replace("%", "%%").join(format_fields)
1179 for idx, val in enumerate(row):
1180 if unitfields.Matches(fields[idx]):
1186 val = row[idx] = utils.FormatUnit(val, units)
1187 val = row[idx] = str(val)
1188 if separator is None:
1189 mlens[idx] = max(mlens[idx], len(val))
1194 for idx, name in enumerate(fields):
1196 if separator is None:
1197 mlens[idx] = max(mlens[idx], len(hdr))
1198 args.append(mlens[idx])
1200 result.append(format % tuple(args))
1205 line = ['-' for _ in fields]
1206 for idx in xrange(len(fields)):
1207 if separator is None:
1208 args.append(mlens[idx])
1209 args.append(line[idx])
1210 result.append(format % tuple(args))
1215 def FormatTimestamp(ts):
1216 """Formats a given timestamp.
1219 @param ts: a timeval-type timestamp, a tuple of seconds and microseconds
1222 @return: a string with the formatted timestamp
1225 if not isinstance (ts, (tuple, list)) or len(ts) != 2:
1228 return time.strftime("%F %T", time.localtime(sec)) + ".%06d" % usec
1231 def ParseTimespec(value):
1232 """Parse a time specification.
1234 The following suffixed will be recognized:
1242 Without any suffix, the value will be taken to be in seconds.
1247 raise errors.OpPrereqError("Empty time specification passed")
1255 if value[-1] not in suffix_map:
1259 raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1261 multiplier = suffix_map[value[-1]]
1263 if not value: # no data left after stripping the suffix
1264 raise errors.OpPrereqError("Invalid time specification (only"
1267 value = int(value) * multiplier
1269 raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1273 def GetOnlineNodes(nodes, cl=None, nowarn=False):
1274 """Returns the names of online nodes.
1276 This function will also log a warning on stderr with the names of
1279 @param nodes: if not empty, use only this subset of nodes (minus the
1281 @param cl: if not None, luxi client to use
1282 @type nowarn: boolean
1283 @param nowarn: by default, this function will output a note with the
1284 offline nodes that are skipped; if this parameter is True the
1285 note is not displayed
1291 result = cl.QueryNodes(names=nodes, fields=["name", "offline"],
1293 offline = [row[0] for row in result if row[1]]
1294 if offline and not nowarn:
1295 ToStderr("Note: skipping offline node(s): %s" % ", ".join(offline))
1296 return [row[0] for row in result if not row[1]]
1299 def _ToStream(stream, txt, *args):
1300 """Write a message to a stream, bypassing the logging system
1302 @type stream: file object
1303 @param stream: the file to which we should write
1305 @param txt: the message
1310 stream.write(txt % args)
1317 def ToStdout(txt, *args):
1318 """Write a message to stdout only, bypassing the logging system
1320 This is just a wrapper over _ToStream.
1323 @param txt: the message
1326 _ToStream(sys.stdout, txt, *args)
1329 def ToStderr(txt, *args):
1330 """Write a message to stderr only, bypassing the logging system
1332 This is just a wrapper over _ToStream.
1335 @param txt: the message
1338 _ToStream(sys.stderr, txt, *args)
1341 class JobExecutor(object):
1342 """Class which manages the submission and execution of multiple jobs.
1344 Note that instances of this class should not be reused between
1348 def __init__(self, cl=None, verbose=True):
1353 self.verbose = verbose
1356 def QueueJob(self, name, *ops):
1357 """Record a job for later submit.
1360 @param name: a description of the job, will be used in WaitJobSet
1362 self.queue.append((name, ops))
1364 def SubmitPending(self):
1365 """Submit all pending jobs.
1368 results = self.cl.SubmitManyJobs([row[1] for row in self.queue])
1369 for ((status, data), (name, _)) in zip(results, self.queue):
1370 self.jobs.append((status, data, name))
1372 def GetResults(self):
1373 """Wait for and return the results of all jobs.
1376 @return: list of tuples (success, job results), in the same order
1377 as the submitted jobs; if a job has failed, instead of the result
1378 there will be the error message
1382 self.SubmitPending()
1385 ok_jobs = [row[1] for row in self.jobs if row[0]]
1387 ToStdout("Submitted jobs %s", ", ".join(ok_jobs))
1388 for submit_status, jid, name in self.jobs:
1389 if not submit_status:
1390 ToStderr("Failed to submit job for %s: %s", name, jid)
1391 results.append((False, jid))
1394 ToStdout("Waiting for job %s for %s...", jid, name)
1396 job_result = PollJob(jid, cl=self.cl)
1398 except (errors.GenericError, luxi.ProtocolError), err:
1399 _, job_result = FormatError(err)
1401 # the error message will always be shown, verbose or not
1402 ToStderr("Job %s for %s has failed: %s", jid, name, job_result)
1404 results.append((success, job_result))
1407 def WaitOrShow(self, wait):
1408 """Wait for job results or only print the job IDs.
1411 @param wait: whether to wait or not
1415 return self.GetResults()
1418 self.SubmitPending()
1419 for status, result, name in self.jobs:
1421 ToStdout("%s: %s", result, name)
1423 ToStderr("Failure for %s: %s", name, result)