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",
78 # Generic functions for CLI programs
83 "JobSubmittedException",
88 # Formatting functions
89 "ToStderr", "ToStdout",
98 # command line options support infrastructure
99 "ARGS_MANY_INSTANCES",
113 "OPT_COMPL_INST_ADD_NODES",
114 "OPT_COMPL_MANY_NODES",
115 "OPT_COMPL_ONE_IALLOCATOR",
116 "OPT_COMPL_ONE_INSTANCE",
117 "OPT_COMPL_ONE_NODE",
128 def __init__(self, min=0, max=None):
133 return ("<%s min=%s max=%s>" %
134 (self.__class__.__name__, self.min, self.max))
137 class ArgSuggest(_Argument):
138 """Suggesting argument.
140 Value can be any of the ones passed to the constructor.
143 def __init__(self, min=0, max=None, choices=None):
144 _Argument.__init__(self, min=min, max=max)
145 self.choices = choices
148 return ("<%s min=%s max=%s choices=%r>" %
149 (self.__class__.__name__, self.min, self.max, self.choices))
152 class ArgChoice(ArgSuggest):
155 Value can be any of the ones passed to the constructor. Like L{ArgSuggest},
156 but value must be one of the choices.
161 class ArgUnknown(_Argument):
162 """Unknown argument to program (e.g. determined at runtime).
167 class ArgInstance(_Argument):
168 """Instances argument.
173 class ArgNode(_Argument):
178 class ArgJobId(_Argument):
184 class ArgFile(_Argument):
185 """File path argument.
190 class ArgCommand(_Argument):
196 class ArgHost(_Argument):
203 ARGS_MANY_INSTANCES = [ArgInstance()]
204 ARGS_MANY_NODES = [ArgNode()]
205 ARGS_ONE_INSTANCE = [ArgInstance(min=1, max=1)]
206 ARGS_ONE_NODE = [ArgNode(min=1, max=1)]
210 def _ExtractTagsObject(opts, args):
211 """Extract the tag type object.
213 Note that this function will modify its args parameter.
216 if not hasattr(opts, "tag_type"):
217 raise errors.ProgrammerError("tag_type not passed to _ExtractTagsObject")
219 if kind == constants.TAG_CLUSTER:
221 elif kind == constants.TAG_NODE or kind == constants.TAG_INSTANCE:
223 raise errors.OpPrereqError("no arguments passed to the command")
227 raise errors.ProgrammerError("Unhandled tag type '%s'" % kind)
231 def _ExtendTags(opts, args):
232 """Extend the args if a source file has been given.
234 This function will extend the tags with the contents of the file
235 passed in the 'tags_source' attribute of the opts parameter. A file
236 named '-' will be replaced by stdin.
239 fname = opts.tags_source
245 new_fh = open(fname, "r")
248 # we don't use the nice 'new_data = [line.strip() for line in fh]'
249 # because of python bug 1633941
251 line = new_fh.readline()
254 new_data.append(line.strip())
257 args.extend(new_data)
260 def ListTags(opts, args):
261 """List the tags on a given object.
263 This is a generic implementation that knows how to deal with all
264 three cases of tag objects (cluster, node, instance). The opts
265 argument is expected to contain a tag_type field denoting what
266 object type we work on.
269 kind, name = _ExtractTagsObject(opts, args)
270 op = opcodes.OpGetTags(kind=kind, name=name)
271 result = SubmitOpCode(op)
272 result = list(result)
278 def AddTags(opts, args):
279 """Add tags on a given object.
281 This is a generic implementation that knows how to deal with all
282 three cases of tag objects (cluster, node, instance). The opts
283 argument is expected to contain a tag_type field denoting what
284 object type we work on.
287 kind, name = _ExtractTagsObject(opts, args)
288 _ExtendTags(opts, args)
290 raise errors.OpPrereqError("No tags to be added")
291 op = opcodes.OpAddTags(kind=kind, name=name, tags=args)
295 def RemoveTags(opts, args):
296 """Remove tags from a given object.
298 This is a generic implementation that knows how to deal with all
299 three cases of tag objects (cluster, node, instance). The opts
300 argument is expected to contain a tag_type field denoting what
301 object type we work on.
304 kind, name = _ExtractTagsObject(opts, args)
305 _ExtendTags(opts, args)
307 raise errors.OpPrereqError("No tags to be removed")
308 op = opcodes.OpDelTags(kind=kind, name=name, tags=args)
312 def check_unit(option, opt, value):
313 """OptParsers custom converter for units.
317 return utils.ParseUnit(value)
318 except errors.UnitParseError, err:
319 raise OptionValueError("option %s: %s" % (opt, err))
322 def _SplitKeyVal(opt, data):
323 """Convert a KeyVal string into a dict.
325 This function will convert a key=val[,...] string into a dict. Empty
326 values will be converted specially: keys which have the prefix 'no_'
327 will have the value=False and the prefix stripped, the others will
331 @param opt: a string holding the option name for which we process the
332 data, used in building error messages
334 @param data: a string of the format key=val,key=val,...
336 @return: {key=val, key=val}
337 @raises errors.ParameterError: if there are duplicate keys
342 for elem in data.split(","):
344 key, val = elem.split("=", 1)
346 if elem.startswith(NO_PREFIX):
347 key, val = elem[len(NO_PREFIX):], False
348 elif elem.startswith(UN_PREFIX):
349 key, val = elem[len(UN_PREFIX):], None
351 key, val = elem, True
353 raise errors.ParameterError("Duplicate key '%s' in option %s" %
359 def check_ident_key_val(option, opt, value):
360 """Custom parser for ident:key=val,key=val options.
362 This will store the parsed values as a tuple (ident, {key: val}). As such,
363 multiple uses of this option via action=append is possible.
367 ident, rest = value, ''
369 ident, rest = value.split(":", 1)
371 if ident.startswith(NO_PREFIX):
373 msg = "Cannot pass options when removing parameter groups: %s" % value
374 raise errors.ParameterError(msg)
375 retval = (ident[len(NO_PREFIX):], False)
376 elif ident.startswith(UN_PREFIX):
378 msg = "Cannot pass options when removing parameter groups: %s" % value
379 raise errors.ParameterError(msg)
380 retval = (ident[len(UN_PREFIX):], None)
382 kv_dict = _SplitKeyVal(opt, rest)
383 retval = (ident, kv_dict)
387 def check_key_val(option, opt, value):
388 """Custom parser class for key=val,key=val options.
390 This will store the parsed values as a dict {key: val}.
393 return _SplitKeyVal(opt, value)
396 # completion_suggestion is normally a list. Using numeric values not evaluating
397 # to False for dynamic completion.
398 (OPT_COMPL_MANY_NODES,
400 OPT_COMPL_ONE_INSTANCE,
402 OPT_COMPL_ONE_IALLOCATOR,
403 OPT_COMPL_INST_ADD_NODES) = range(100, 106)
405 OPT_COMPL_ALL = frozenset([
406 OPT_COMPL_MANY_NODES,
408 OPT_COMPL_ONE_INSTANCE,
410 OPT_COMPL_ONE_IALLOCATOR,
411 OPT_COMPL_INST_ADD_NODES,
415 class CliOption(Option):
416 """Custom option class for optparse.
419 ATTRS = Option.ATTRS + [
420 "completion_suggest",
422 TYPES = Option.TYPES + (
427 TYPE_CHECKER = Option.TYPE_CHECKER.copy()
428 TYPE_CHECKER["identkeyval"] = check_ident_key_val
429 TYPE_CHECKER["keyval"] = check_key_val
430 TYPE_CHECKER["unit"] = check_unit
433 # optparse.py sets make_option, so we do it for our own option class, too
434 cli_option = CliOption
437 DEBUG_OPT = cli_option("-d", "--debug", default=False,
439 help="Turn debugging on")
441 NOHDR_OPT = cli_option("--no-headers", default=False,
442 action="store_true", dest="no_headers",
443 help="Don't display column headers")
445 SEP_OPT = cli_option("--separator", default=None,
446 action="store", dest="separator",
447 help=("Separator between output fields"
448 " (defaults to one space)"))
450 USEUNITS_OPT = cli_option("--units", default=None,
451 dest="units", choices=('h', 'm', 'g', 't'),
452 help="Specify units for output (one of hmgt)")
454 FIELDS_OPT = cli_option("-o", "--output", dest="output", action="store",
455 type="string", metavar="FIELDS",
456 help="Comma separated list of output fields")
458 FORCE_OPT = cli_option("-f", "--force", dest="force", action="store_true",
459 default=False, help="Force the operation")
461 CONFIRM_OPT = cli_option("--yes", dest="confirm", action="store_true",
462 default=False, help="Do not require confirmation")
464 TAG_SRC_OPT = cli_option("--from", dest="tags_source",
465 default=None, help="File with tag names")
467 SUBMIT_OPT = cli_option("--submit", dest="submit_only",
468 default=False, action="store_true",
469 help=("Submit the job and return the job ID, but"
470 " don't wait for the job to finish"))
472 SYNC_OPT = cli_option("--sync", dest="do_locking",
473 default=False, action="store_true",
474 help=("Grab locks while doing the queries"
475 " in order to ensure more consistent results"))
477 _DRY_RUN_OPT = cli_option("--dry-run", default=False,
479 help=("Do not execute the operation, just run the"
480 " check steps and verify it it could be"
483 VERBOSE_OPT = cli_option("-v", "--verbose", default=False,
485 help="Increase the verbosity of the operation")
487 DEBUG_SIMERR_OPT = cli_option("--debug-simulate-errors", default=False,
488 action="store_true", dest="simulate_errors",
489 help="Debugging option that makes the operation"
490 " treat most runtime checks as failed")
492 NWSYNC_OPT = cli_option("--no-wait-for-sync", dest="wait_for_sync",
493 default=True, action="store_false",
494 help="Don't wait for sync (DANGEROUS!)")
496 DISK_TEMPLATE_OPT = cli_option("-t", "--disk-template", dest="disk_template",
497 help="Custom disk setup (diskless, file,"
499 default=None, metavar="TEMPL",
500 choices=list(constants.DISK_TEMPLATES))
502 NONICS_OPT = cli_option("--no-nics", default=False, action="store_true",
503 help="Do not create any network cards for"
506 FILESTORE_DIR_OPT = cli_option("--file-storage-dir", dest="file_storage_dir",
507 help="Relative path under default cluster-wide"
508 " file storage dir to store file-based disks",
509 default=None, metavar="<DIR>")
511 FILESTORE_DRIVER_OPT = cli_option("--file-driver", dest="file_driver",
512 help="Driver to use for image files",
513 default="loop", metavar="<DRIVER>",
514 choices=list(constants.FILE_DRIVER))
516 IALLOCATOR_OPT = cli_option("-I", "--iallocator", metavar="<NAME>",
517 help="Select nodes for the instance automatically"
518 " using the <NAME> iallocator plugin",
519 default=None, type="string",
520 completion_suggest=OPT_COMPL_ONE_IALLOCATOR)
522 OS_OPT = cli_option("-o", "--os-type", dest="os", help="What OS to run",
524 completion_suggest=OPT_COMPL_ONE_OS)
526 BACKEND_OPT = cli_option("-B", "--backend-parameters", dest="beparams",
527 type="keyval", default={},
528 help="Backend parameters")
530 HVOPTS_OPT = cli_option("-H", "--hypervisor-parameters", type="keyval",
531 default={}, dest="hvparams",
532 help="Hypervisor parameters")
534 HYPERVISOR_OPT = cli_option("-H", "--hypervisor-parameters", dest="hypervisor",
535 help="Hypervisor and hypervisor options, in the"
536 " format hypervisor:option=value,option=value,...",
537 default=None, type="identkeyval")
539 HVLIST_OPT = cli_option("-H", "--hypervisor-parameters", dest="hvparams",
540 help="Hypervisor and hypervisor options, in the"
541 " format hypervisor:option=value,option=value,...",
542 default=[], action="append", type="identkeyval")
544 NOIPCHECK_OPT = cli_option("--no-ip-check", dest="ip_check", default=True,
545 action="store_false",
546 help="Don't check that the instance's IP"
549 NET_OPT = cli_option("--net",
550 help="NIC parameters", default=[],
551 dest="nics", action="append", type="identkeyval")
553 DISK_OPT = cli_option("--disk", help="Disk parameters", default=[],
554 dest="disks", action="append", type="identkeyval")
556 DISKIDX_OPT = cli_option("--disks", dest="disks", default=None,
557 help="Comma-separated list of disks"
558 " indices to act on (e.g. 0,2) (optional,"
559 " defaults to all disks)")
561 OS_SIZE_OPT = cli_option("-s", "--os-size", dest="sd_size",
562 help="Enforces a single-disk configuration using the"
563 " given disk size, in MiB unless a suffix is used",
564 default=None, type="unit", metavar="<size>")
566 IGNORE_CONSIST_OPT = cli_option("--ignore-consistency",
567 dest="ignore_consistency",
568 action="store_true", default=False,
569 help="Ignore the consistency of the disks on"
572 NONLIVE_OPT = cli_option("--non-live", dest="live",
573 default=True, action="store_false",
574 help="Do a non-live migration (this usually means"
575 " freeze the instance, save the state, transfer and"
576 " only then resume running on the secondary node)")
578 NODE_PLACEMENT_OPT = cli_option("-n", "--node", dest="node",
579 help="Target node and optional secondary node",
580 metavar="<pnode>[:<snode>]",
581 completion_suggest=OPT_COMPL_INST_ADD_NODES)
584 def _ParseArgs(argv, commands, aliases):
585 """Parser for the command line arguments.
587 This function parses the arguments and returns the function which
588 must be executed together with its (modified) arguments.
590 @param argv: the command line
591 @param commands: dictionary with special contents, see the design
592 doc for cmdline handling
593 @param aliases: dictionary with command aliases {'alias': 'target, ...}
599 binary = argv[0].split("/")[-1]
601 if len(argv) > 1 and argv[1] == "--version":
602 ToStdout("%s (ganeti) %s", binary, constants.RELEASE_VERSION)
603 # Quit right away. That way we don't have to care about this special
604 # argument. optparse.py does it the same.
607 if len(argv) < 2 or not (argv[1] in commands or
609 # let's do a nice thing
610 sortedcmds = commands.keys()
613 ToStdout("Usage: %s {command} [options...] [argument...]", binary)
614 ToStdout("%s <command> --help to see details, or man %s", binary, binary)
617 # compute the max line length for cmd + usage
618 mlen = max([len(" %s" % cmd) for cmd in commands])
619 mlen = min(60, mlen) # should not get here...
621 # and format a nice command list
622 ToStdout("Commands:")
623 for cmd in sortedcmds:
624 cmdstr = " %s" % (cmd,)
625 help_text = commands[cmd][4]
626 help_lines = textwrap.wrap(help_text, 79 - 3 - mlen)
627 ToStdout("%-*s - %s", mlen, cmdstr, help_lines.pop(0))
628 for line in help_lines:
629 ToStdout("%-*s %s", mlen, "", line)
633 return None, None, None
635 # get command, unalias it, and look it up in commands
639 raise errors.ProgrammerError("Alias '%s' overrides an existing"
642 if aliases[cmd] not in commands:
643 raise errors.ProgrammerError("Alias '%s' maps to non-existing"
644 " command '%s'" % (cmd, aliases[cmd]))
648 func, args_def, parser_opts, usage, description = commands[cmd]
649 parser = OptionParser(option_list=parser_opts + [_DRY_RUN_OPT],
650 description=description,
651 formatter=TitledHelpFormatter(),
652 usage="%%prog %s %s" % (cmd, usage))
653 parser.disable_interspersed_args()
654 options, args = parser.parse_args()
656 if not _CheckArguments(cmd, args_def, args):
657 return None, None, None
659 return func, options, args
662 def _CheckArguments(cmd, args_def, args):
663 """Verifies the arguments using the argument definition.
667 1. Abort with error if values specified by user but none expected.
669 1. For each argument in definition
671 1. Keep running count of minimum number of values (min_count)
672 1. Keep running count of maximum number of values (max_count)
673 1. If it has an unlimited number of values
675 1. Abort with error if it's not the last argument in the definition
677 1. If last argument has limited number of values
679 1. Abort with error if number of values doesn't match or is too large
681 1. Abort with error if user didn't pass enough values (min_count)
684 if args and not args_def:
685 ToStderr("Error: Command %s expects no arguments", cmd)
692 last_idx = len(args_def) - 1
694 for idx, arg in enumerate(args_def):
695 if min_count is None:
697 elif arg.min is not None:
700 if max_count is None:
702 elif arg.max is not None:
706 check_max = (arg.max is not None)
708 elif arg.max is None:
709 raise errors.ProgrammerError("Only the last argument can have max=None")
712 # Command with exact number of arguments
713 if (min_count is not None and max_count is not None and
714 min_count == max_count and len(args) != min_count):
715 ToStderr("Error: Command %s expects %d argument(s)", cmd, min_count)
718 # Command with limited number of arguments
719 if max_count is not None and len(args) > max_count:
720 ToStderr("Error: Command %s expects only %d argument(s)",
724 # Command with some required arguments
725 if min_count is not None and len(args) < min_count:
726 ToStderr("Error: Command %s expects at least %d argument(s)",
733 def SplitNodeOption(value):
734 """Splits the value of a --node option.
737 if value and ':' in value:
738 return value.split(':', 1)
744 def wrapper(*args, **kwargs):
747 return fn(*args, **kwargs)
753 def AskUser(text, choices=None):
754 """Ask the user a question.
756 @param text: the question to ask
758 @param choices: list with elements tuples (input_char, return_value,
759 description); if not given, it will default to: [('y', True,
760 'Perform the operation'), ('n', False, 'Do no do the operation')];
761 note that the '?' char is reserved for help
763 @return: one of the return values from the choices list; if input is
764 not possible (i.e. not running with a tty, we return the last
769 choices = [('y', True, 'Perform the operation'),
770 ('n', False, 'Do not perform the operation')]
771 if not choices or not isinstance(choices, list):
772 raise errors.ProgrammerError("Invalid choices argument to AskUser")
773 for entry in choices:
774 if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
775 raise errors.ProgrammerError("Invalid choices element to AskUser")
777 answer = choices[-1][1]
779 for line in text.splitlines():
780 new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
781 text = "\n".join(new_text)
783 f = file("/dev/tty", "a+")
787 chars = [entry[0] for entry in choices]
788 chars[-1] = "[%s]" % chars[-1]
790 maps = dict([(entry[0], entry[1]) for entry in choices])
794 f.write("/".join(chars))
796 line = f.readline(2).strip().lower()
801 for entry in choices:
802 f.write(" %s - %s\n" % (entry[0], entry[2]))
810 class JobSubmittedException(Exception):
811 """Job was submitted, client should exit.
813 This exception has one argument, the ID of the job that was
814 submitted. The handler should print this ID.
816 This is not an error, just a structured way to exit from clients.
821 def SendJob(ops, cl=None):
822 """Function to submit an opcode without waiting for the results.
825 @param ops: list of opcodes
826 @type cl: luxi.Client
827 @param cl: the luxi client to use for communicating with the master;
828 if None, a new client will be created
834 job_id = cl.SubmitJob(ops)
839 def PollJob(job_id, cl=None, feedback_fn=None):
840 """Function to poll for the result of a job.
842 @type job_id: job identified
843 @param job_id: the job to poll for results
844 @type cl: luxi.Client
845 @param cl: the luxi client to use for communicating with the master;
846 if None, a new client will be created
853 prev_logmsg_serial = None
856 result = cl.WaitForJobChange(job_id, ["status"], prev_job_info,
859 # job not found, go away!
860 raise errors.JobLost("Job with id %s lost" % job_id)
862 # Split result, a tuple of (field values, log entries)
863 (job_info, log_entries) = result
864 (status, ) = job_info
867 for log_entry in log_entries:
868 (serial, timestamp, _, message) = log_entry
869 if callable(feedback_fn):
870 feedback_fn(log_entry[1:])
872 encoded = utils.SafeEncode(message)
873 ToStdout("%s %s", time.ctime(utils.MergeTime(timestamp)), encoded)
874 prev_logmsg_serial = max(prev_logmsg_serial, serial)
876 # TODO: Handle canceled and archived jobs
877 elif status in (constants.JOB_STATUS_SUCCESS,
878 constants.JOB_STATUS_ERROR,
879 constants.JOB_STATUS_CANCELING,
880 constants.JOB_STATUS_CANCELED):
883 prev_job_info = job_info
885 jobs = cl.QueryJobs([job_id], ["status", "opstatus", "opresult"])
887 raise errors.JobLost("Job with id %s lost" % job_id)
889 status, opstatus, result = jobs[0]
890 if status == constants.JOB_STATUS_SUCCESS:
892 elif status in (constants.JOB_STATUS_CANCELING,
893 constants.JOB_STATUS_CANCELED):
894 raise errors.OpExecError("Job was canceled")
897 for idx, (status, msg) in enumerate(zip(opstatus, result)):
898 if status == constants.OP_STATUS_SUCCESS:
900 elif status == constants.OP_STATUS_ERROR:
901 errors.MaybeRaise(msg)
903 raise errors.OpExecError("partial failure (opcode %d): %s" %
906 raise errors.OpExecError(str(msg))
907 # default failure mode
908 raise errors.OpExecError(result)
911 def SubmitOpCode(op, cl=None, feedback_fn=None):
912 """Legacy function to submit an opcode.
914 This is just a simple wrapper over the construction of the processor
915 instance. It should be extended to better handle feedback and
916 interaction functions.
922 job_id = SendJob([op], cl)
924 op_results = PollJob(job_id, cl=cl, feedback_fn=feedback_fn)
929 def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
930 """Wrapper around SubmitOpCode or SendJob.
932 This function will decide, based on the 'opts' parameter, whether to
933 submit and wait for the result of the opcode (and return it), or
934 whether to just send the job and print its identifier. It is used in
935 order to simplify the implementation of the '--submit' option.
937 It will also add the dry-run parameter from the options passed, if true.
940 if opts and opts.dry_run:
941 op.dry_run = opts.dry_run
942 if opts and opts.submit_only:
943 job_id = SendJob([op], cl=cl)
944 raise JobSubmittedException(job_id)
946 return SubmitOpCode(op, cl=cl, feedback_fn=feedback_fn)
950 # TODO: Cache object?
952 client = luxi.Client()
953 except luxi.NoMasterError:
954 master, myself = ssconf.GetMasterAndMyself()
956 raise errors.OpPrereqError("This is not the master node, please connect"
957 " to node '%s' and rerun the command" %
964 def FormatError(err):
965 """Return a formatted error message for a given error.
967 This function takes an exception instance and returns a tuple
968 consisting of two values: first, the recommended exit code, and
969 second, a string describing the error message (not
976 if isinstance(err, errors.ConfigurationError):
977 txt = "Corrupt configuration file: %s" % msg
979 obuf.write(txt + "\n")
980 obuf.write("Aborting.")
982 elif isinstance(err, errors.HooksAbort):
983 obuf.write("Failure: hooks execution failed:\n")
984 for node, script, out in err.args[0]:
986 obuf.write(" node: %s, script: %s, output: %s\n" %
989 obuf.write(" node: %s, script: %s (no output)\n" %
991 elif isinstance(err, errors.HooksFailure):
992 obuf.write("Failure: hooks general failure: %s" % msg)
993 elif isinstance(err, errors.ResolverError):
994 this_host = utils.HostInfo.SysName()
995 if err.args[0] == this_host:
996 msg = "Failure: can't resolve my own hostname ('%s')"
998 msg = "Failure: can't resolve hostname '%s'"
999 obuf.write(msg % err.args[0])
1000 elif isinstance(err, errors.OpPrereqError):
1001 obuf.write("Failure: prerequisites not met for this"
1002 " operation:\n%s" % msg)
1003 elif isinstance(err, errors.OpExecError):
1004 obuf.write("Failure: command execution error:\n%s" % msg)
1005 elif isinstance(err, errors.TagError):
1006 obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
1007 elif isinstance(err, errors.JobQueueDrainError):
1008 obuf.write("Failure: the job queue is marked for drain and doesn't"
1009 " accept new requests\n")
1010 elif isinstance(err, errors.JobQueueFull):
1011 obuf.write("Failure: the job queue is full and doesn't accept new"
1012 " job submissions until old jobs are archived\n")
1013 elif isinstance(err, errors.TypeEnforcementError):
1014 obuf.write("Parameter Error: %s" % msg)
1015 elif isinstance(err, errors.ParameterError):
1016 obuf.write("Failure: unknown/wrong parameter name '%s'" % msg)
1017 elif isinstance(err, errors.GenericError):
1018 obuf.write("Unhandled Ganeti error: %s" % msg)
1019 elif isinstance(err, luxi.NoMasterError):
1020 obuf.write("Cannot communicate with the master daemon.\nIs it running"
1021 " and listening for connections?")
1022 elif isinstance(err, luxi.TimeoutError):
1023 obuf.write("Timeout while talking to the master daemon. Error:\n"
1025 elif isinstance(err, luxi.ProtocolError):
1026 obuf.write("Unhandled protocol error while talking to the master daemon:\n"
1028 elif isinstance(err, JobSubmittedException):
1029 obuf.write("JobID: %s\n" % err.args[0])
1032 obuf.write("Unhandled exception: %s" % msg)
1033 return retcode, obuf.getvalue().rstrip('\n')
1036 def GenericMain(commands, override=None, aliases=None):
1037 """Generic main function for all the gnt-* commands.
1040 - commands: a dictionary with a special structure, see the design doc
1041 for command line handling.
1042 - override: if not None, we expect a dictionary with keys that will
1043 override command line options; this can be used to pass
1044 options from the scripts to generic functions
1045 - aliases: dictionary with command aliases {'alias': 'target, ...}
1048 # save the program name and the entire command line for later logging
1050 binary = os.path.basename(sys.argv[0]) or sys.argv[0]
1051 if len(sys.argv) >= 2:
1052 binary += " " + sys.argv[1]
1053 old_cmdline = " ".join(sys.argv[2:])
1057 binary = "<unknown program>"
1064 func, options, args = _ParseArgs(sys.argv, commands, aliases)
1065 except errors.ParameterError, err:
1066 result, err_msg = FormatError(err)
1070 if func is None: # parse error
1073 if override is not None:
1074 for key, val in override.iteritems():
1075 setattr(options, key, val)
1077 utils.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
1078 stderr_logging=True, program=binary)
1081 logging.info("run with arguments '%s'", old_cmdline)
1083 logging.info("run with no arguments")
1086 result = func(options, args)
1087 except (errors.GenericError, luxi.ProtocolError,
1088 JobSubmittedException), err:
1089 result, err_msg = FormatError(err)
1090 logging.exception("Error during command processing")
1096 def GenerateTable(headers, fields, separator, data,
1097 numfields=None, unitfields=None,
1099 """Prints a table with headers and different fields.
1102 @param headers: dictionary mapping field names to headers for
1105 @param fields: the field names corresponding to each row in
1107 @param separator: the separator to be used; if this is None,
1108 the default 'smart' algorithm is used which computes optimal
1109 field width, otherwise just the separator is used between
1112 @param data: a list of lists, each sublist being one row to be output
1113 @type numfields: list
1114 @param numfields: a list with the fields that hold numeric
1115 values and thus should be right-aligned
1116 @type unitfields: list
1117 @param unitfields: a list with the fields that hold numeric
1118 values that should be formatted with the units field
1119 @type units: string or None
1120 @param units: the units we should use for formatting, or None for
1121 automatic choice (human-readable for non-separator usage, otherwise
1122 megabytes); this is a one-letter string
1131 if numfields is None:
1133 if unitfields is None:
1136 numfields = utils.FieldSet(*numfields)
1137 unitfields = utils.FieldSet(*unitfields)
1140 for field in fields:
1141 if headers and field not in headers:
1142 # TODO: handle better unknown fields (either revert to old
1143 # style of raising exception, or deal more intelligently with
1145 headers[field] = field
1146 if separator is not None:
1147 format_fields.append("%s")
1148 elif numfields.Matches(field):
1149 format_fields.append("%*s")
1151 format_fields.append("%-*s")
1153 if separator is None:
1154 mlens = [0 for name in fields]
1155 format = ' '.join(format_fields)
1157 format = separator.replace("%", "%%").join(format_fields)
1162 for idx, val in enumerate(row):
1163 if unitfields.Matches(fields[idx]):
1169 val = row[idx] = utils.FormatUnit(val, units)
1170 val = row[idx] = str(val)
1171 if separator is None:
1172 mlens[idx] = max(mlens[idx], len(val))
1177 for idx, name in enumerate(fields):
1179 if separator is None:
1180 mlens[idx] = max(mlens[idx], len(hdr))
1181 args.append(mlens[idx])
1183 result.append(format % tuple(args))
1188 line = ['-' for _ in fields]
1189 for idx in xrange(len(fields)):
1190 if separator is None:
1191 args.append(mlens[idx])
1192 args.append(line[idx])
1193 result.append(format % tuple(args))
1198 def FormatTimestamp(ts):
1199 """Formats a given timestamp.
1202 @param ts: a timeval-type timestamp, a tuple of seconds and microseconds
1205 @return: a string with the formatted timestamp
1208 if not isinstance (ts, (tuple, list)) or len(ts) != 2:
1211 return time.strftime("%F %T", time.localtime(sec)) + ".%06d" % usec
1214 def ParseTimespec(value):
1215 """Parse a time specification.
1217 The following suffixed will be recognized:
1225 Without any suffix, the value will be taken to be in seconds.
1230 raise errors.OpPrereqError("Empty time specification passed")
1238 if value[-1] not in suffix_map:
1242 raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1244 multiplier = suffix_map[value[-1]]
1246 if not value: # no data left after stripping the suffix
1247 raise errors.OpPrereqError("Invalid time specification (only"
1250 value = int(value) * multiplier
1252 raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1256 def GetOnlineNodes(nodes, cl=None, nowarn=False):
1257 """Returns the names of online nodes.
1259 This function will also log a warning on stderr with the names of
1262 @param nodes: if not empty, use only this subset of nodes (minus the
1264 @param cl: if not None, luxi client to use
1265 @type nowarn: boolean
1266 @param nowarn: by default, this function will output a note with the
1267 offline nodes that are skipped; if this parameter is True the
1268 note is not displayed
1274 result = cl.QueryNodes(names=nodes, fields=["name", "offline"],
1276 offline = [row[0] for row in result if row[1]]
1277 if offline and not nowarn:
1278 ToStderr("Note: skipping offline node(s): %s" % ", ".join(offline))
1279 return [row[0] for row in result if not row[1]]
1282 def _ToStream(stream, txt, *args):
1283 """Write a message to a stream, bypassing the logging system
1285 @type stream: file object
1286 @param stream: the file to which we should write
1288 @param txt: the message
1293 stream.write(txt % args)
1300 def ToStdout(txt, *args):
1301 """Write a message to stdout only, bypassing the logging system
1303 This is just a wrapper over _ToStream.
1306 @param txt: the message
1309 _ToStream(sys.stdout, txt, *args)
1312 def ToStderr(txt, *args):
1313 """Write a message to stderr only, bypassing the logging system
1315 This is just a wrapper over _ToStream.
1318 @param txt: the message
1321 _ToStream(sys.stderr, txt, *args)
1324 class JobExecutor(object):
1325 """Class which manages the submission and execution of multiple jobs.
1327 Note that instances of this class should not be reused between
1331 def __init__(self, cl=None, verbose=True):
1336 self.verbose = verbose
1339 def QueueJob(self, name, *ops):
1340 """Record a job for later submit.
1343 @param name: a description of the job, will be used in WaitJobSet
1345 self.queue.append((name, ops))
1347 def SubmitPending(self):
1348 """Submit all pending jobs.
1351 results = self.cl.SubmitManyJobs([row[1] for row in self.queue])
1352 for ((status, data), (name, _)) in zip(results, self.queue):
1353 self.jobs.append((status, data, name))
1355 def GetResults(self):
1356 """Wait for and return the results of all jobs.
1359 @return: list of tuples (success, job results), in the same order
1360 as the submitted jobs; if a job has failed, instead of the result
1361 there will be the error message
1365 self.SubmitPending()
1368 ok_jobs = [row[1] for row in self.jobs if row[0]]
1370 ToStdout("Submitted jobs %s", ", ".join(ok_jobs))
1371 for submit_status, jid, name in self.jobs:
1372 if not submit_status:
1373 ToStderr("Failed to submit job for %s: %s", name, jid)
1374 results.append((False, jid))
1377 ToStdout("Waiting for job %s for %s...", jid, name)
1379 job_result = PollJob(jid, cl=self.cl)
1381 except (errors.GenericError, luxi.ProtocolError), err:
1382 _, job_result = FormatError(err)
1384 # the error message will always be shown, verbose or not
1385 ToStderr("Job %s for %s has failed: %s", jid, name, job_result)
1387 results.append((success, job_result))
1390 def WaitOrShow(self, wait):
1391 """Wait for job results or only print the job IDs.
1394 @param wait: whether to wait or not
1398 return self.GetResults()
1401 self.SubmitPending()
1402 for status, result, name in self.jobs:
1404 ToStdout("%s: %s", result, name)
1406 ToStderr("Failure for %s: %s", name, result)