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",
75 # Generic functions for CLI programs
80 "JobSubmittedException",
85 # Formatting functions
86 "ToStderr", "ToStdout",
95 # command line options support infrastructure
96 "ARGS_MANY_INSTANCES",
110 "OPT_COMPL_INST_ADD_NODES",
111 "OPT_COMPL_MANY_NODES",
112 "OPT_COMPL_ONE_IALLOCATOR",
113 "OPT_COMPL_ONE_INSTANCE",
114 "OPT_COMPL_ONE_NODE",
125 def __init__(self, min=0, max=None):
130 return ("<%s min=%s max=%s>" %
131 (self.__class__.__name__, self.min, self.max))
134 class ArgSuggest(_Argument):
135 """Suggesting argument.
137 Value can be any of the ones passed to the constructor.
140 def __init__(self, min=0, max=None, choices=None):
141 _Argument.__init__(self, min=min, max=max)
142 self.choices = choices
145 return ("<%s min=%s max=%s choices=%r>" %
146 (self.__class__.__name__, self.min, self.max, self.choices))
149 class ArgChoice(ArgSuggest):
152 Value can be any of the ones passed to the constructor. Like L{ArgSuggest},
153 but value must be one of the choices.
158 class ArgUnknown(_Argument):
159 """Unknown argument to program (e.g. determined at runtime).
164 class ArgInstance(_Argument):
165 """Instances argument.
170 class ArgNode(_Argument):
175 class ArgJobId(_Argument):
181 class ArgFile(_Argument):
182 """File path argument.
187 class ArgCommand(_Argument):
193 class ArgHost(_Argument):
200 ARGS_MANY_INSTANCES = [ArgInstance()]
201 ARGS_MANY_NODES = [ArgNode()]
202 ARGS_ONE_INSTANCE = [ArgInstance(min=1, max=1)]
203 ARGS_ONE_NODE = [ArgNode(min=1, max=1)]
207 def _ExtractTagsObject(opts, args):
208 """Extract the tag type object.
210 Note that this function will modify its args parameter.
213 if not hasattr(opts, "tag_type"):
214 raise errors.ProgrammerError("tag_type not passed to _ExtractTagsObject")
216 if kind == constants.TAG_CLUSTER:
218 elif kind == constants.TAG_NODE or kind == constants.TAG_INSTANCE:
220 raise errors.OpPrereqError("no arguments passed to the command")
224 raise errors.ProgrammerError("Unhandled tag type '%s'" % kind)
228 def _ExtendTags(opts, args):
229 """Extend the args if a source file has been given.
231 This function will extend the tags with the contents of the file
232 passed in the 'tags_source' attribute of the opts parameter. A file
233 named '-' will be replaced by stdin.
236 fname = opts.tags_source
242 new_fh = open(fname, "r")
245 # we don't use the nice 'new_data = [line.strip() for line in fh]'
246 # because of python bug 1633941
248 line = new_fh.readline()
251 new_data.append(line.strip())
254 args.extend(new_data)
257 def ListTags(opts, args):
258 """List the tags on a given object.
260 This is a generic implementation that knows how to deal with all
261 three cases of tag objects (cluster, node, instance). The opts
262 argument is expected to contain a tag_type field denoting what
263 object type we work on.
266 kind, name = _ExtractTagsObject(opts, args)
267 op = opcodes.OpGetTags(kind=kind, name=name)
268 result = SubmitOpCode(op)
269 result = list(result)
275 def AddTags(opts, args):
276 """Add tags on a given object.
278 This is a generic implementation that knows how to deal with all
279 three cases of tag objects (cluster, node, instance). The opts
280 argument is expected to contain a tag_type field denoting what
281 object type we work on.
284 kind, name = _ExtractTagsObject(opts, args)
285 _ExtendTags(opts, args)
287 raise errors.OpPrereqError("No tags to be added")
288 op = opcodes.OpAddTags(kind=kind, name=name, tags=args)
292 def RemoveTags(opts, args):
293 """Remove tags from a given object.
295 This is a generic implementation that knows how to deal with all
296 three cases of tag objects (cluster, node, instance). The opts
297 argument is expected to contain a tag_type field denoting what
298 object type we work on.
301 kind, name = _ExtractTagsObject(opts, args)
302 _ExtendTags(opts, args)
304 raise errors.OpPrereqError("No tags to be removed")
305 op = opcodes.OpDelTags(kind=kind, name=name, tags=args)
309 def check_unit(option, opt, value):
310 """OptParsers custom converter for units.
314 return utils.ParseUnit(value)
315 except errors.UnitParseError, err:
316 raise OptionValueError("option %s: %s" % (opt, err))
319 def _SplitKeyVal(opt, data):
320 """Convert a KeyVal string into a dict.
322 This function will convert a key=val[,...] string into a dict. Empty
323 values will be converted specially: keys which have the prefix 'no_'
324 will have the value=False and the prefix stripped, the others will
328 @param opt: a string holding the option name for which we process the
329 data, used in building error messages
331 @param data: a string of the format key=val,key=val,...
333 @return: {key=val, key=val}
334 @raises errors.ParameterError: if there are duplicate keys
339 for elem in data.split(","):
341 key, val = elem.split("=", 1)
343 if elem.startswith(NO_PREFIX):
344 key, val = elem[len(NO_PREFIX):], False
345 elif elem.startswith(UN_PREFIX):
346 key, val = elem[len(UN_PREFIX):], None
348 key, val = elem, True
350 raise errors.ParameterError("Duplicate key '%s' in option %s" %
356 def check_ident_key_val(option, opt, value):
357 """Custom parser for ident:key=val,key=val options.
359 This will store the parsed values as a tuple (ident, {key: val}). As such,
360 multiple uses of this option via action=append is possible.
364 ident, rest = value, ''
366 ident, rest = value.split(":", 1)
368 if ident.startswith(NO_PREFIX):
370 msg = "Cannot pass options when removing parameter groups: %s" % value
371 raise errors.ParameterError(msg)
372 retval = (ident[len(NO_PREFIX):], False)
373 elif ident.startswith(UN_PREFIX):
375 msg = "Cannot pass options when removing parameter groups: %s" % value
376 raise errors.ParameterError(msg)
377 retval = (ident[len(UN_PREFIX):], None)
379 kv_dict = _SplitKeyVal(opt, rest)
380 retval = (ident, kv_dict)
384 def check_key_val(option, opt, value):
385 """Custom parser class for key=val,key=val options.
387 This will store the parsed values as a dict {key: val}.
390 return _SplitKeyVal(opt, value)
393 # completion_suggestion is normally a list. Using numeric values not evaluating
394 # to False for dynamic completion.
395 (OPT_COMPL_MANY_NODES,
397 OPT_COMPL_ONE_INSTANCE,
399 OPT_COMPL_ONE_IALLOCATOR,
400 OPT_COMPL_INST_ADD_NODES) = range(100, 106)
402 OPT_COMPL_ALL = frozenset([
403 OPT_COMPL_MANY_NODES,
405 OPT_COMPL_ONE_INSTANCE,
407 OPT_COMPL_ONE_IALLOCATOR,
408 OPT_COMPL_INST_ADD_NODES,
412 class CliOption(Option):
413 """Custom option class for optparse.
416 ATTRS = Option.ATTRS + [
417 "completion_suggest",
419 TYPES = Option.TYPES + (
424 TYPE_CHECKER = Option.TYPE_CHECKER.copy()
425 TYPE_CHECKER["identkeyval"] = check_ident_key_val
426 TYPE_CHECKER["keyval"] = check_key_val
427 TYPE_CHECKER["unit"] = check_unit
430 # optparse.py sets make_option, so we do it for our own option class, too
431 cli_option = CliOption
434 DEBUG_OPT = cli_option("-d", "--debug", default=False,
436 help="Turn debugging on")
438 NOHDR_OPT = cli_option("--no-headers", default=False,
439 action="store_true", dest="no_headers",
440 help="Don't display column headers")
442 SEP_OPT = cli_option("--separator", default=None,
443 action="store", dest="separator",
444 help=("Separator between output fields"
445 " (defaults to one space)"))
447 USEUNITS_OPT = cli_option("--units", default=None,
448 dest="units", choices=('h', 'm', 'g', 't'),
449 help="Specify units for output (one of hmgt)")
451 FIELDS_OPT = cli_option("-o", "--output", dest="output", action="store",
452 type="string", metavar="FIELDS",
453 help="Comma separated list of output fields")
455 FORCE_OPT = cli_option("-f", "--force", dest="force", action="store_true",
456 default=False, help="Force the operation")
458 CONFIRM_OPT = cli_option("--yes", dest="confirm", action="store_true",
459 default=False, help="Do not require confirmation")
461 TAG_SRC_OPT = cli_option("--from", dest="tags_source",
462 default=None, help="File with tag names")
464 SUBMIT_OPT = cli_option("--submit", dest="submit_only",
465 default=False, action="store_true",
466 help=("Submit the job and return the job ID, but"
467 " don't wait for the job to finish"))
469 SYNC_OPT = cli_option("--sync", dest="do_locking",
470 default=False, action="store_true",
471 help=("Grab locks while doing the queries"
472 " in order to ensure more consistent results"))
474 _DRY_RUN_OPT = cli_option("--dry-run", default=False,
476 help=("Do not execute the operation, just run the"
477 " check steps and verify it it could be"
480 VERBOSE_OPT = cli_option("-v", "--verbose", default=False,
482 help="Increase the verbosity of the operation")
484 DEBUG_SIMERR_OPT = cli_option("--debug-simulate-errors", default=False,
485 action="store_true", dest="simulate_errors",
486 help="Debugging option that makes the operation"
487 " treat most runtime checks as failed")
489 NWSYNC_OPT = cli_option("--no-wait-for-sync", dest="wait_for_sync",
490 default=True, action="store_false",
491 help="Don't wait for sync (DANGEROUS!)")
493 DISK_TEMPLATE_OPT = cli_option("-t", "--disk-template", dest="disk_template",
494 help="Custom disk setup (diskless, file,"
496 default=None, metavar="TEMPL",
497 choices=list(constants.DISK_TEMPLATES))
499 NONICS_OPT = cli_option("--no-nics", default=False, action="store_true",
500 help="Do not create any network cards for"
503 FILESTORE_DIR_OPT = cli_option("--file-storage-dir", dest="file_storage_dir",
504 help="Relative path under default cluster-wide"
505 " file storage dir to store file-based disks",
506 default=None, metavar="<DIR>")
508 FILESTORE_DRIVER_OPT = cli_option("--file-driver", dest="file_driver",
509 help="Driver to use for image files",
510 default="loop", metavar="<DRIVER>",
511 choices=list(constants.FILE_DRIVER))
513 IALLOCATOR_OPT = cli_option("-I", "--iallocator", metavar="<NAME>",
514 help="Select nodes for the instance automatically"
515 " using the <NAME> iallocator plugin",
516 default=None, type="string",
517 completion_suggest=OPT_COMPL_ONE_IALLOCATOR)
519 OS_OPT = cli_option("-o", "--os-type", dest="os", help="What OS to run",
521 completion_suggest=OPT_COMPL_ONE_OS)
523 BACKEND_OPT = cli_option("-B", "--backend-parameters", dest="beparams",
524 type="keyval", default={},
525 help="Backend parameters")
527 HVOPTS_OPT = cli_option("-H", "--hypervisor-parameters", type="keyval",
528 default={}, dest="hvparams",
529 help="Hypervisor parameters")
531 HYPERVISOR_OPT = cli_option("-H", "--hypervisor-parameters", dest="hypervisor",
532 help="Hypervisor and hypervisor options, in the"
533 " format hypervisor:option=value,option=value,...",
534 default=None, type="identkeyval")
536 HVLIST_OPT = cli_option("-H", "--hypervisor-parameters", dest="hvparams",
537 help="Hypervisor and hypervisor options, in the"
538 " format hypervisor:option=value,option=value,...",
539 default=[], action="append", type="identkeyval")
541 NOIPCHECK_OPT = cli_option("--no-ip-check", dest="ip_check", default=True,
542 action="store_false",
543 help="Don't check that the instance's IP"
546 NET_OPT = cli_option("--net",
547 help="NIC parameters", default=[],
548 dest="nics", action="append", type="identkeyval")
550 DISK_OPT = cli_option("--disk", help="Disk parameters", default=[],
551 dest="disks", action="append", type="identkeyval")
553 DISKIDX_OPT = cli_option("--disks", dest="disks", default=None,
554 help="Comma-separated list of disks"
555 " indices to act on (e.g. 0,2) (optional,"
556 " defaults to all disks)")
558 OS_SIZE_OPT = cli_option("-s", "--os-size", dest="sd_size",
559 help="Enforces a single-disk configuration using the"
560 " given disk size, in MiB unless a suffix is used",
561 default=None, type="unit", metavar="<size>")
564 def _ParseArgs(argv, commands, aliases):
565 """Parser for the command line arguments.
567 This function parses the arguments and returns the function which
568 must be executed together with its (modified) arguments.
570 @param argv: the command line
571 @param commands: dictionary with special contents, see the design
572 doc for cmdline handling
573 @param aliases: dictionary with command aliases {'alias': 'target, ...}
579 binary = argv[0].split("/")[-1]
581 if len(argv) > 1 and argv[1] == "--version":
582 ToStdout("%s (ganeti) %s", binary, constants.RELEASE_VERSION)
583 # Quit right away. That way we don't have to care about this special
584 # argument. optparse.py does it the same.
587 if len(argv) < 2 or not (argv[1] in commands or
589 # let's do a nice thing
590 sortedcmds = commands.keys()
593 ToStdout("Usage: %s {command} [options...] [argument...]", binary)
594 ToStdout("%s <command> --help to see details, or man %s", binary, binary)
597 # compute the max line length for cmd + usage
598 mlen = max([len(" %s" % cmd) for cmd in commands])
599 mlen = min(60, mlen) # should not get here...
601 # and format a nice command list
602 ToStdout("Commands:")
603 for cmd in sortedcmds:
604 cmdstr = " %s" % (cmd,)
605 help_text = commands[cmd][4]
606 help_lines = textwrap.wrap(help_text, 79 - 3 - mlen)
607 ToStdout("%-*s - %s", mlen, cmdstr, help_lines.pop(0))
608 for line in help_lines:
609 ToStdout("%-*s %s", mlen, "", line)
613 return None, None, None
615 # get command, unalias it, and look it up in commands
619 raise errors.ProgrammerError("Alias '%s' overrides an existing"
622 if aliases[cmd] not in commands:
623 raise errors.ProgrammerError("Alias '%s' maps to non-existing"
624 " command '%s'" % (cmd, aliases[cmd]))
628 func, args_def, parser_opts, usage, description = commands[cmd]
629 parser = OptionParser(option_list=parser_opts + [_DRY_RUN_OPT],
630 description=description,
631 formatter=TitledHelpFormatter(),
632 usage="%%prog %s %s" % (cmd, usage))
633 parser.disable_interspersed_args()
634 options, args = parser.parse_args()
636 if not _CheckArguments(cmd, args_def, args):
637 return None, None, None
639 return func, options, args
642 def _CheckArguments(cmd, args_def, args):
643 """Verifies the arguments using the argument definition.
647 1. Abort with error if values specified by user but none expected.
649 1. For each argument in definition
651 1. Keep running count of minimum number of values (min_count)
652 1. Keep running count of maximum number of values (max_count)
653 1. If it has an unlimited number of values
655 1. Abort with error if it's not the last argument in the definition
657 1. If last argument has limited number of values
659 1. Abort with error if number of values doesn't match or is too large
661 1. Abort with error if user didn't pass enough values (min_count)
664 if args and not args_def:
665 ToStderr("Error: Command %s expects no arguments", cmd)
672 last_idx = len(args_def) - 1
674 for idx, arg in enumerate(args_def):
675 if min_count is None:
677 elif arg.min is not None:
680 if max_count is None:
682 elif arg.max is not None:
686 check_max = (arg.max is not None)
688 elif arg.max is None:
689 raise errors.ProgrammerError("Only the last argument can have max=None")
692 # Command with exact number of arguments
693 if (min_count is not None and max_count is not None and
694 min_count == max_count and len(args) != min_count):
695 ToStderr("Error: Command %s expects %d argument(s)", cmd, min_count)
698 # Command with limited number of arguments
699 if max_count is not None and len(args) > max_count:
700 ToStderr("Error: Command %s expects only %d argument(s)",
704 # Command with some required arguments
705 if min_count is not None and len(args) < min_count:
706 ToStderr("Error: Command %s expects at least %d argument(s)",
713 def SplitNodeOption(value):
714 """Splits the value of a --node option.
717 if value and ':' in value:
718 return value.split(':', 1)
724 def wrapper(*args, **kwargs):
727 return fn(*args, **kwargs)
733 def AskUser(text, choices=None):
734 """Ask the user a question.
736 @param text: the question to ask
738 @param choices: list with elements tuples (input_char, return_value,
739 description); if not given, it will default to: [('y', True,
740 'Perform the operation'), ('n', False, 'Do no do the operation')];
741 note that the '?' char is reserved for help
743 @return: one of the return values from the choices list; if input is
744 not possible (i.e. not running with a tty, we return the last
749 choices = [('y', True, 'Perform the operation'),
750 ('n', False, 'Do not perform the operation')]
751 if not choices or not isinstance(choices, list):
752 raise errors.ProgrammerError("Invalid choices argument to AskUser")
753 for entry in choices:
754 if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
755 raise errors.ProgrammerError("Invalid choices element to AskUser")
757 answer = choices[-1][1]
759 for line in text.splitlines():
760 new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
761 text = "\n".join(new_text)
763 f = file("/dev/tty", "a+")
767 chars = [entry[0] for entry in choices]
768 chars[-1] = "[%s]" % chars[-1]
770 maps = dict([(entry[0], entry[1]) for entry in choices])
774 f.write("/".join(chars))
776 line = f.readline(2).strip().lower()
781 for entry in choices:
782 f.write(" %s - %s\n" % (entry[0], entry[2]))
790 class JobSubmittedException(Exception):
791 """Job was submitted, client should exit.
793 This exception has one argument, the ID of the job that was
794 submitted. The handler should print this ID.
796 This is not an error, just a structured way to exit from clients.
801 def SendJob(ops, cl=None):
802 """Function to submit an opcode without waiting for the results.
805 @param ops: list of opcodes
806 @type cl: luxi.Client
807 @param cl: the luxi client to use for communicating with the master;
808 if None, a new client will be created
814 job_id = cl.SubmitJob(ops)
819 def PollJob(job_id, cl=None, feedback_fn=None):
820 """Function to poll for the result of a job.
822 @type job_id: job identified
823 @param job_id: the job to poll for results
824 @type cl: luxi.Client
825 @param cl: the luxi client to use for communicating with the master;
826 if None, a new client will be created
833 prev_logmsg_serial = None
836 result = cl.WaitForJobChange(job_id, ["status"], prev_job_info,
839 # job not found, go away!
840 raise errors.JobLost("Job with id %s lost" % job_id)
842 # Split result, a tuple of (field values, log entries)
843 (job_info, log_entries) = result
844 (status, ) = job_info
847 for log_entry in log_entries:
848 (serial, timestamp, _, message) = log_entry
849 if callable(feedback_fn):
850 feedback_fn(log_entry[1:])
852 encoded = utils.SafeEncode(message)
853 ToStdout("%s %s", time.ctime(utils.MergeTime(timestamp)), encoded)
854 prev_logmsg_serial = max(prev_logmsg_serial, serial)
856 # TODO: Handle canceled and archived jobs
857 elif status in (constants.JOB_STATUS_SUCCESS,
858 constants.JOB_STATUS_ERROR,
859 constants.JOB_STATUS_CANCELING,
860 constants.JOB_STATUS_CANCELED):
863 prev_job_info = job_info
865 jobs = cl.QueryJobs([job_id], ["status", "opstatus", "opresult"])
867 raise errors.JobLost("Job with id %s lost" % job_id)
869 status, opstatus, result = jobs[0]
870 if status == constants.JOB_STATUS_SUCCESS:
872 elif status in (constants.JOB_STATUS_CANCELING,
873 constants.JOB_STATUS_CANCELED):
874 raise errors.OpExecError("Job was canceled")
877 for idx, (status, msg) in enumerate(zip(opstatus, result)):
878 if status == constants.OP_STATUS_SUCCESS:
880 elif status == constants.OP_STATUS_ERROR:
881 errors.MaybeRaise(msg)
883 raise errors.OpExecError("partial failure (opcode %d): %s" %
886 raise errors.OpExecError(str(msg))
887 # default failure mode
888 raise errors.OpExecError(result)
891 def SubmitOpCode(op, cl=None, feedback_fn=None):
892 """Legacy function to submit an opcode.
894 This is just a simple wrapper over the construction of the processor
895 instance. It should be extended to better handle feedback and
896 interaction functions.
902 job_id = SendJob([op], cl)
904 op_results = PollJob(job_id, cl=cl, feedback_fn=feedback_fn)
909 def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
910 """Wrapper around SubmitOpCode or SendJob.
912 This function will decide, based on the 'opts' parameter, whether to
913 submit and wait for the result of the opcode (and return it), or
914 whether to just send the job and print its identifier. It is used in
915 order to simplify the implementation of the '--submit' option.
917 It will also add the dry-run parameter from the options passed, if true.
920 if opts and opts.dry_run:
921 op.dry_run = opts.dry_run
922 if opts and opts.submit_only:
923 job_id = SendJob([op], cl=cl)
924 raise JobSubmittedException(job_id)
926 return SubmitOpCode(op, cl=cl, feedback_fn=feedback_fn)
930 # TODO: Cache object?
932 client = luxi.Client()
933 except luxi.NoMasterError:
934 master, myself = ssconf.GetMasterAndMyself()
936 raise errors.OpPrereqError("This is not the master node, please connect"
937 " to node '%s' and rerun the command" %
944 def FormatError(err):
945 """Return a formatted error message for a given error.
947 This function takes an exception instance and returns a tuple
948 consisting of two values: first, the recommended exit code, and
949 second, a string describing the error message (not
956 if isinstance(err, errors.ConfigurationError):
957 txt = "Corrupt configuration file: %s" % msg
959 obuf.write(txt + "\n")
960 obuf.write("Aborting.")
962 elif isinstance(err, errors.HooksAbort):
963 obuf.write("Failure: hooks execution failed:\n")
964 for node, script, out in err.args[0]:
966 obuf.write(" node: %s, script: %s, output: %s\n" %
969 obuf.write(" node: %s, script: %s (no output)\n" %
971 elif isinstance(err, errors.HooksFailure):
972 obuf.write("Failure: hooks general failure: %s" % msg)
973 elif isinstance(err, errors.ResolverError):
974 this_host = utils.HostInfo.SysName()
975 if err.args[0] == this_host:
976 msg = "Failure: can't resolve my own hostname ('%s')"
978 msg = "Failure: can't resolve hostname '%s'"
979 obuf.write(msg % err.args[0])
980 elif isinstance(err, errors.OpPrereqError):
981 obuf.write("Failure: prerequisites not met for this"
982 " operation:\n%s" % msg)
983 elif isinstance(err, errors.OpExecError):
984 obuf.write("Failure: command execution error:\n%s" % msg)
985 elif isinstance(err, errors.TagError):
986 obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
987 elif isinstance(err, errors.JobQueueDrainError):
988 obuf.write("Failure: the job queue is marked for drain and doesn't"
989 " accept new requests\n")
990 elif isinstance(err, errors.JobQueueFull):
991 obuf.write("Failure: the job queue is full and doesn't accept new"
992 " job submissions until old jobs are archived\n")
993 elif isinstance(err, errors.TypeEnforcementError):
994 obuf.write("Parameter Error: %s" % msg)
995 elif isinstance(err, errors.ParameterError):
996 obuf.write("Failure: unknown/wrong parameter name '%s'" % msg)
997 elif isinstance(err, errors.GenericError):
998 obuf.write("Unhandled Ganeti error: %s" % msg)
999 elif isinstance(err, luxi.NoMasterError):
1000 obuf.write("Cannot communicate with the master daemon.\nIs it running"
1001 " and listening for connections?")
1002 elif isinstance(err, luxi.TimeoutError):
1003 obuf.write("Timeout while talking to the master daemon. Error:\n"
1005 elif isinstance(err, luxi.ProtocolError):
1006 obuf.write("Unhandled protocol error while talking to the master daemon:\n"
1008 elif isinstance(err, JobSubmittedException):
1009 obuf.write("JobID: %s\n" % err.args[0])
1012 obuf.write("Unhandled exception: %s" % msg)
1013 return retcode, obuf.getvalue().rstrip('\n')
1016 def GenericMain(commands, override=None, aliases=None):
1017 """Generic main function for all the gnt-* commands.
1020 - commands: a dictionary with a special structure, see the design doc
1021 for command line handling.
1022 - override: if not None, we expect a dictionary with keys that will
1023 override command line options; this can be used to pass
1024 options from the scripts to generic functions
1025 - aliases: dictionary with command aliases {'alias': 'target, ...}
1028 # save the program name and the entire command line for later logging
1030 binary = os.path.basename(sys.argv[0]) or sys.argv[0]
1031 if len(sys.argv) >= 2:
1032 binary += " " + sys.argv[1]
1033 old_cmdline = " ".join(sys.argv[2:])
1037 binary = "<unknown program>"
1044 func, options, args = _ParseArgs(sys.argv, commands, aliases)
1045 except errors.ParameterError, err:
1046 result, err_msg = FormatError(err)
1050 if func is None: # parse error
1053 if override is not None:
1054 for key, val in override.iteritems():
1055 setattr(options, key, val)
1057 utils.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
1058 stderr_logging=True, program=binary)
1061 logging.info("run with arguments '%s'", old_cmdline)
1063 logging.info("run with no arguments")
1066 result = func(options, args)
1067 except (errors.GenericError, luxi.ProtocolError,
1068 JobSubmittedException), err:
1069 result, err_msg = FormatError(err)
1070 logging.exception("Error during command processing")
1076 def GenerateTable(headers, fields, separator, data,
1077 numfields=None, unitfields=None,
1079 """Prints a table with headers and different fields.
1082 @param headers: dictionary mapping field names to headers for
1085 @param fields: the field names corresponding to each row in
1087 @param separator: the separator to be used; if this is None,
1088 the default 'smart' algorithm is used which computes optimal
1089 field width, otherwise just the separator is used between
1092 @param data: a list of lists, each sublist being one row to be output
1093 @type numfields: list
1094 @param numfields: a list with the fields that hold numeric
1095 values and thus should be right-aligned
1096 @type unitfields: list
1097 @param unitfields: a list with the fields that hold numeric
1098 values that should be formatted with the units field
1099 @type units: string or None
1100 @param units: the units we should use for formatting, or None for
1101 automatic choice (human-readable for non-separator usage, otherwise
1102 megabytes); this is a one-letter string
1111 if numfields is None:
1113 if unitfields is None:
1116 numfields = utils.FieldSet(*numfields)
1117 unitfields = utils.FieldSet(*unitfields)
1120 for field in fields:
1121 if headers and field not in headers:
1122 # TODO: handle better unknown fields (either revert to old
1123 # style of raising exception, or deal more intelligently with
1125 headers[field] = field
1126 if separator is not None:
1127 format_fields.append("%s")
1128 elif numfields.Matches(field):
1129 format_fields.append("%*s")
1131 format_fields.append("%-*s")
1133 if separator is None:
1134 mlens = [0 for name in fields]
1135 format = ' '.join(format_fields)
1137 format = separator.replace("%", "%%").join(format_fields)
1142 for idx, val in enumerate(row):
1143 if unitfields.Matches(fields[idx]):
1149 val = row[idx] = utils.FormatUnit(val, units)
1150 val = row[idx] = str(val)
1151 if separator is None:
1152 mlens[idx] = max(mlens[idx], len(val))
1157 for idx, name in enumerate(fields):
1159 if separator is None:
1160 mlens[idx] = max(mlens[idx], len(hdr))
1161 args.append(mlens[idx])
1163 result.append(format % tuple(args))
1168 line = ['-' for _ in fields]
1169 for idx in xrange(len(fields)):
1170 if separator is None:
1171 args.append(mlens[idx])
1172 args.append(line[idx])
1173 result.append(format % tuple(args))
1178 def FormatTimestamp(ts):
1179 """Formats a given timestamp.
1182 @param ts: a timeval-type timestamp, a tuple of seconds and microseconds
1185 @return: a string with the formatted timestamp
1188 if not isinstance (ts, (tuple, list)) or len(ts) != 2:
1191 return time.strftime("%F %T", time.localtime(sec)) + ".%06d" % usec
1194 def ParseTimespec(value):
1195 """Parse a time specification.
1197 The following suffixed will be recognized:
1205 Without any suffix, the value will be taken to be in seconds.
1210 raise errors.OpPrereqError("Empty time specification passed")
1218 if value[-1] not in suffix_map:
1222 raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1224 multiplier = suffix_map[value[-1]]
1226 if not value: # no data left after stripping the suffix
1227 raise errors.OpPrereqError("Invalid time specification (only"
1230 value = int(value) * multiplier
1232 raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1236 def GetOnlineNodes(nodes, cl=None, nowarn=False):
1237 """Returns the names of online nodes.
1239 This function will also log a warning on stderr with the names of
1242 @param nodes: if not empty, use only this subset of nodes (minus the
1244 @param cl: if not None, luxi client to use
1245 @type nowarn: boolean
1246 @param nowarn: by default, this function will output a note with the
1247 offline nodes that are skipped; if this parameter is True the
1248 note is not displayed
1254 result = cl.QueryNodes(names=nodes, fields=["name", "offline"],
1256 offline = [row[0] for row in result if row[1]]
1257 if offline and not nowarn:
1258 ToStderr("Note: skipping offline node(s): %s" % ", ".join(offline))
1259 return [row[0] for row in result if not row[1]]
1262 def _ToStream(stream, txt, *args):
1263 """Write a message to a stream, bypassing the logging system
1265 @type stream: file object
1266 @param stream: the file to which we should write
1268 @param txt: the message
1273 stream.write(txt % args)
1280 def ToStdout(txt, *args):
1281 """Write a message to stdout only, bypassing the logging system
1283 This is just a wrapper over _ToStream.
1286 @param txt: the message
1289 _ToStream(sys.stdout, txt, *args)
1292 def ToStderr(txt, *args):
1293 """Write a message to stderr only, bypassing the logging system
1295 This is just a wrapper over _ToStream.
1298 @param txt: the message
1301 _ToStream(sys.stderr, txt, *args)
1304 class JobExecutor(object):
1305 """Class which manages the submission and execution of multiple jobs.
1307 Note that instances of this class should not be reused between
1311 def __init__(self, cl=None, verbose=True):
1316 self.verbose = verbose
1319 def QueueJob(self, name, *ops):
1320 """Record a job for later submit.
1323 @param name: a description of the job, will be used in WaitJobSet
1325 self.queue.append((name, ops))
1327 def SubmitPending(self):
1328 """Submit all pending jobs.
1331 results = self.cl.SubmitManyJobs([row[1] for row in self.queue])
1332 for ((status, data), (name, _)) in zip(results, self.queue):
1333 self.jobs.append((status, data, name))
1335 def GetResults(self):
1336 """Wait for and return the results of all jobs.
1339 @return: list of tuples (success, job results), in the same order
1340 as the submitted jobs; if a job has failed, instead of the result
1341 there will be the error message
1345 self.SubmitPending()
1348 ok_jobs = [row[1] for row in self.jobs if row[0]]
1350 ToStdout("Submitted jobs %s", ", ".join(ok_jobs))
1351 for submit_status, jid, name in self.jobs:
1352 if not submit_status:
1353 ToStderr("Failed to submit job for %s: %s", name, jid)
1354 results.append((False, jid))
1357 ToStdout("Waiting for job %s for %s...", jid, name)
1359 job_result = PollJob(jid, cl=self.cl)
1361 except (errors.GenericError, luxi.ProtocolError), err:
1362 _, job_result = FormatError(err)
1364 # the error message will always be shown, verbose or not
1365 ToStderr("Job %s for %s has failed: %s", jid, name, job_result)
1367 results.append((success, job_result))
1370 def WaitOrShow(self, wait):
1371 """Wait for job results or only print the job IDs.
1374 @param wait: whether to wait or not
1378 return self.GetResults()
1381 self.SubmitPending()
1382 for status, result, name in self.jobs:
1384 ToStdout("%s: %s", result, name)
1386 ToStderr("Failure for %s: %s", name, result)