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
58 "FILESTORE_DRIVER_OPT",
64 "IGNORE_FAILURES_OPT",
90 # Generic functions for CLI programs
95 "JobSubmittedException",
100 # Formatting functions
101 "ToStderr", "ToStdout",
110 # command line options support infrastructure
111 "ARGS_MANY_INSTANCES",
125 "OPT_COMPL_INST_ADD_NODES",
126 "OPT_COMPL_MANY_NODES",
127 "OPT_COMPL_ONE_IALLOCATOR",
128 "OPT_COMPL_ONE_INSTANCE",
129 "OPT_COMPL_ONE_NODE",
140 def __init__(self, min=0, max=None):
145 return ("<%s min=%s max=%s>" %
146 (self.__class__.__name__, self.min, self.max))
149 class ArgSuggest(_Argument):
150 """Suggesting argument.
152 Value can be any of the ones passed to the constructor.
155 def __init__(self, min=0, max=None, choices=None):
156 _Argument.__init__(self, min=min, max=max)
157 self.choices = choices
160 return ("<%s min=%s max=%s choices=%r>" %
161 (self.__class__.__name__, self.min, self.max, self.choices))
164 class ArgChoice(ArgSuggest):
167 Value can be any of the ones passed to the constructor. Like L{ArgSuggest},
168 but value must be one of the choices.
173 class ArgUnknown(_Argument):
174 """Unknown argument to program (e.g. determined at runtime).
179 class ArgInstance(_Argument):
180 """Instances argument.
185 class ArgNode(_Argument):
190 class ArgJobId(_Argument):
196 class ArgFile(_Argument):
197 """File path argument.
202 class ArgCommand(_Argument):
208 class ArgHost(_Argument):
215 ARGS_MANY_INSTANCES = [ArgInstance()]
216 ARGS_MANY_NODES = [ArgNode()]
217 ARGS_ONE_INSTANCE = [ArgInstance(min=1, max=1)]
218 ARGS_ONE_NODE = [ArgNode(min=1, max=1)]
222 def _ExtractTagsObject(opts, args):
223 """Extract the tag type object.
225 Note that this function will modify its args parameter.
228 if not hasattr(opts, "tag_type"):
229 raise errors.ProgrammerError("tag_type not passed to _ExtractTagsObject")
231 if kind == constants.TAG_CLUSTER:
233 elif kind == constants.TAG_NODE or kind == constants.TAG_INSTANCE:
235 raise errors.OpPrereqError("no arguments passed to the command")
239 raise errors.ProgrammerError("Unhandled tag type '%s'" % kind)
243 def _ExtendTags(opts, args):
244 """Extend the args if a source file has been given.
246 This function will extend the tags with the contents of the file
247 passed in the 'tags_source' attribute of the opts parameter. A file
248 named '-' will be replaced by stdin.
251 fname = opts.tags_source
257 new_fh = open(fname, "r")
260 # we don't use the nice 'new_data = [line.strip() for line in fh]'
261 # because of python bug 1633941
263 line = new_fh.readline()
266 new_data.append(line.strip())
269 args.extend(new_data)
272 def ListTags(opts, args):
273 """List the tags on a given object.
275 This is a generic implementation that knows how to deal with all
276 three cases of tag objects (cluster, node, instance). The opts
277 argument is expected to contain a tag_type field denoting what
278 object type we work on.
281 kind, name = _ExtractTagsObject(opts, args)
282 op = opcodes.OpGetTags(kind=kind, name=name)
283 result = SubmitOpCode(op)
284 result = list(result)
290 def AddTags(opts, args):
291 """Add tags on a given object.
293 This is a generic implementation that knows how to deal with all
294 three cases of tag objects (cluster, node, instance). The opts
295 argument is expected to contain a tag_type field denoting what
296 object type we work on.
299 kind, name = _ExtractTagsObject(opts, args)
300 _ExtendTags(opts, args)
302 raise errors.OpPrereqError("No tags to be added")
303 op = opcodes.OpAddTags(kind=kind, name=name, tags=args)
307 def RemoveTags(opts, args):
308 """Remove tags from a given object.
310 This is a generic implementation that knows how to deal with all
311 three cases of tag objects (cluster, node, instance). The opts
312 argument is expected to contain a tag_type field denoting what
313 object type we work on.
316 kind, name = _ExtractTagsObject(opts, args)
317 _ExtendTags(opts, args)
319 raise errors.OpPrereqError("No tags to be removed")
320 op = opcodes.OpDelTags(kind=kind, name=name, tags=args)
324 def check_unit(option, opt, value):
325 """OptParsers custom converter for units.
329 return utils.ParseUnit(value)
330 except errors.UnitParseError, err:
331 raise OptionValueError("option %s: %s" % (opt, err))
334 def _SplitKeyVal(opt, data):
335 """Convert a KeyVal string into a dict.
337 This function will convert a key=val[,...] string into a dict. Empty
338 values will be converted specially: keys which have the prefix 'no_'
339 will have the value=False and the prefix stripped, the others will
343 @param opt: a string holding the option name for which we process the
344 data, used in building error messages
346 @param data: a string of the format key=val,key=val,...
348 @return: {key=val, key=val}
349 @raises errors.ParameterError: if there are duplicate keys
354 for elem in data.split(","):
356 key, val = elem.split("=", 1)
358 if elem.startswith(NO_PREFIX):
359 key, val = elem[len(NO_PREFIX):], False
360 elif elem.startswith(UN_PREFIX):
361 key, val = elem[len(UN_PREFIX):], None
363 key, val = elem, True
365 raise errors.ParameterError("Duplicate key '%s' in option %s" %
371 def check_ident_key_val(option, opt, value):
372 """Custom parser for ident:key=val,key=val options.
374 This will store the parsed values as a tuple (ident, {key: val}). As such,
375 multiple uses of this option via action=append is possible.
379 ident, rest = value, ''
381 ident, rest = value.split(":", 1)
383 if ident.startswith(NO_PREFIX):
385 msg = "Cannot pass options when removing parameter groups: %s" % value
386 raise errors.ParameterError(msg)
387 retval = (ident[len(NO_PREFIX):], False)
388 elif ident.startswith(UN_PREFIX):
390 msg = "Cannot pass options when removing parameter groups: %s" % value
391 raise errors.ParameterError(msg)
392 retval = (ident[len(UN_PREFIX):], None)
394 kv_dict = _SplitKeyVal(opt, rest)
395 retval = (ident, kv_dict)
399 def check_key_val(option, opt, value):
400 """Custom parser class for key=val,key=val options.
402 This will store the parsed values as a dict {key: val}.
405 return _SplitKeyVal(opt, value)
408 # completion_suggestion is normally a list. Using numeric values not evaluating
409 # to False for dynamic completion.
410 (OPT_COMPL_MANY_NODES,
412 OPT_COMPL_ONE_INSTANCE,
414 OPT_COMPL_ONE_IALLOCATOR,
415 OPT_COMPL_INST_ADD_NODES) = range(100, 106)
417 OPT_COMPL_ALL = frozenset([
418 OPT_COMPL_MANY_NODES,
420 OPT_COMPL_ONE_INSTANCE,
422 OPT_COMPL_ONE_IALLOCATOR,
423 OPT_COMPL_INST_ADD_NODES,
427 class CliOption(Option):
428 """Custom option class for optparse.
431 ATTRS = Option.ATTRS + [
432 "completion_suggest",
434 TYPES = Option.TYPES + (
439 TYPE_CHECKER = Option.TYPE_CHECKER.copy()
440 TYPE_CHECKER["identkeyval"] = check_ident_key_val
441 TYPE_CHECKER["keyval"] = check_key_val
442 TYPE_CHECKER["unit"] = check_unit
445 # optparse.py sets make_option, so we do it for our own option class, too
446 cli_option = CliOption
449 DEBUG_OPT = cli_option("-d", "--debug", default=False,
451 help="Turn debugging on")
453 NOHDR_OPT = cli_option("--no-headers", default=False,
454 action="store_true", dest="no_headers",
455 help="Don't display column headers")
457 SEP_OPT = cli_option("--separator", default=None,
458 action="store", dest="separator",
459 help=("Separator between output fields"
460 " (defaults to one space)"))
462 USEUNITS_OPT = cli_option("--units", default=None,
463 dest="units", choices=('h', 'm', 'g', 't'),
464 help="Specify units for output (one of hmgt)")
466 FIELDS_OPT = cli_option("-o", "--output", dest="output", action="store",
467 type="string", metavar="FIELDS",
468 help="Comma separated list of output fields")
470 FORCE_OPT = cli_option("-f", "--force", dest="force", action="store_true",
471 default=False, help="Force the operation")
473 CONFIRM_OPT = cli_option("--yes", dest="confirm", action="store_true",
474 default=False, help="Do not require confirmation")
476 TAG_SRC_OPT = cli_option("--from", dest="tags_source",
477 default=None, help="File with tag names")
479 SUBMIT_OPT = cli_option("--submit", dest="submit_only",
480 default=False, action="store_true",
481 help=("Submit the job and return the job ID, but"
482 " don't wait for the job to finish"))
484 SYNC_OPT = cli_option("--sync", dest="do_locking",
485 default=False, action="store_true",
486 help=("Grab locks while doing the queries"
487 " in order to ensure more consistent results"))
489 _DRY_RUN_OPT = cli_option("--dry-run", default=False,
491 help=("Do not execute the operation, just run the"
492 " check steps and verify it it could be"
495 VERBOSE_OPT = cli_option("-v", "--verbose", default=False,
497 help="Increase the verbosity of the operation")
499 DEBUG_SIMERR_OPT = cli_option("--debug-simulate-errors", default=False,
500 action="store_true", dest="simulate_errors",
501 help="Debugging option that makes the operation"
502 " treat most runtime checks as failed")
504 NWSYNC_OPT = cli_option("--no-wait-for-sync", dest="wait_for_sync",
505 default=True, action="store_false",
506 help="Don't wait for sync (DANGEROUS!)")
508 DISK_TEMPLATE_OPT = cli_option("-t", "--disk-template", dest="disk_template",
509 help="Custom disk setup (diskless, file,"
511 default=None, metavar="TEMPL",
512 choices=list(constants.DISK_TEMPLATES))
514 NONICS_OPT = cli_option("--no-nics", default=False, action="store_true",
515 help="Do not create any network cards for"
518 FILESTORE_DIR_OPT = cli_option("--file-storage-dir", dest="file_storage_dir",
519 help="Relative path under default cluster-wide"
520 " file storage dir to store file-based disks",
521 default=None, metavar="<DIR>")
523 FILESTORE_DRIVER_OPT = cli_option("--file-driver", dest="file_driver",
524 help="Driver to use for image files",
525 default="loop", metavar="<DRIVER>",
526 choices=list(constants.FILE_DRIVER))
528 IALLOCATOR_OPT = cli_option("-I", "--iallocator", metavar="<NAME>",
529 help="Select nodes for the instance automatically"
530 " using the <NAME> iallocator plugin",
531 default=None, type="string",
532 completion_suggest=OPT_COMPL_ONE_IALLOCATOR)
534 OS_OPT = cli_option("-o", "--os-type", dest="os", help="What OS to run",
536 completion_suggest=OPT_COMPL_ONE_OS)
538 BACKEND_OPT = cli_option("-B", "--backend-parameters", dest="beparams",
539 type="keyval", default={},
540 help="Backend parameters")
542 HVOPTS_OPT = cli_option("-H", "--hypervisor-parameters", type="keyval",
543 default={}, dest="hvparams",
544 help="Hypervisor parameters")
546 HYPERVISOR_OPT = cli_option("-H", "--hypervisor-parameters", dest="hypervisor",
547 help="Hypervisor and hypervisor options, in the"
548 " format hypervisor:option=value,option=value,...",
549 default=None, type="identkeyval")
551 HVLIST_OPT = cli_option("-H", "--hypervisor-parameters", dest="hvparams",
552 help="Hypervisor and hypervisor options, in the"
553 " format hypervisor:option=value,option=value,...",
554 default=[], action="append", type="identkeyval")
556 NOIPCHECK_OPT = cli_option("--no-ip-check", dest="ip_check", default=True,
557 action="store_false",
558 help="Don't check that the instance's IP"
561 NET_OPT = cli_option("--net",
562 help="NIC parameters", default=[],
563 dest="nics", action="append", type="identkeyval")
565 DISK_OPT = cli_option("--disk", help="Disk parameters", default=[],
566 dest="disks", action="append", type="identkeyval")
568 DISKIDX_OPT = cli_option("--disks", dest="disks", default=None,
569 help="Comma-separated list of disks"
570 " indices to act on (e.g. 0,2) (optional,"
571 " defaults to all disks)")
573 OS_SIZE_OPT = cli_option("-s", "--os-size", dest="sd_size",
574 help="Enforces a single-disk configuration using the"
575 " given disk size, in MiB unless a suffix is used",
576 default=None, type="unit", metavar="<size>")
578 IGNORE_CONSIST_OPT = cli_option("--ignore-consistency",
579 dest="ignore_consistency",
580 action="store_true", default=False,
581 help="Ignore the consistency of the disks on"
584 NONLIVE_OPT = cli_option("--non-live", dest="live",
585 default=True, action="store_false",
586 help="Do a non-live migration (this usually means"
587 " freeze the instance, save the state, transfer and"
588 " only then resume running on the secondary node)")
590 NODE_PLACEMENT_OPT = cli_option("-n", "--node", dest="node",
591 help="Target node and optional secondary node",
592 metavar="<pnode>[:<snode>]",
593 completion_suggest=OPT_COMPL_INST_ADD_NODES)
595 NODE_LIST_OPT = cli_option("-n", "--node", dest="nodes", default=[],
596 action="append", metavar="<node>",
597 help="Use only this node (can be used multiple"
598 " times, if not given defaults to all nodes)",
599 completion_suggest=OPT_COMPL_ONE_NODE)
601 SINGLE_NODE_OPT = cli_option("-n", "--node", dest="node", help="Target node",
603 completion_suggest=OPT_COMPL_ONE_NODE)
605 NOSTART_OPT = cli_option("--no-start", dest="start", default=True,
606 action="store_false",
607 help="Don't start the instance after creation")
609 SHOWCMD_OPT = cli_option("--show-cmd", dest="show_command",
610 action="store_true", default=False,
611 help="Show command instead of executing it")
613 CLEANUP_OPT = cli_option("--cleanup", dest="cleanup",
614 default=False, action="store_true",
615 help="Instead of performing the migration, try to"
616 " recover from a failed cleanup. This is safe"
617 " to run even if the instance is healthy, but it"
618 " will create extra replication traffic and "
619 " disrupt briefly the replication (like during the"
622 STATIC_OPT = cli_option("-s", "--static", dest="static",
623 action="store_true", default=False,
624 help="Only show configuration data, not runtime data")
626 ALL_OPT = cli_option("--all", dest="show_all",
627 default=False, action="store_true",
628 help="Show info on all instances on the cluster."
629 " This can take a long time to run, use wisely")
631 SELECT_OS_OPT = cli_option("--select-os", dest="select_os",
632 action="store_true", default=False,
633 help="Interactive OS reinstall, lists available"
634 " OS templates for selection")
636 IGNORE_FAILURES_OPT = cli_option("--ignore-failures", dest="ignore_failures",
637 action="store_true", default=False,
638 help="Remove the instance from the cluster"
639 " configuration even if there are failures"
640 " during the removal process")
642 NEW_SECONDARY_OPT = cli_option("-n", "--new-secondary", dest="dst_node",
643 help="Specifies the new secondary node",
644 metavar="NODE", default=None,
645 completion_suggest=OPT_COMPL_ONE_NODE)
647 ON_PRIMARY_OPT = cli_option("-p", "--on-primary", dest="on_primary",
648 default=False, action="store_true",
649 help="Replace the disk(s) on the primary"
650 " node (only for the drbd template)")
652 ON_SECONDARY_OPT = cli_option("-s", "--on-secondary", dest="on_secondary",
653 default=False, action="store_true",
654 help="Replace the disk(s) on the secondary"
655 " node (only for the drbd template)")
658 def _ParseArgs(argv, commands, aliases):
659 """Parser for the command line arguments.
661 This function parses the arguments and returns the function which
662 must be executed together with its (modified) arguments.
664 @param argv: the command line
665 @param commands: dictionary with special contents, see the design
666 doc for cmdline handling
667 @param aliases: dictionary with command aliases {'alias': 'target, ...}
673 binary = argv[0].split("/")[-1]
675 if len(argv) > 1 and argv[1] == "--version":
676 ToStdout("%s (ganeti) %s", binary, constants.RELEASE_VERSION)
677 # Quit right away. That way we don't have to care about this special
678 # argument. optparse.py does it the same.
681 if len(argv) < 2 or not (argv[1] in commands or
683 # let's do a nice thing
684 sortedcmds = commands.keys()
687 ToStdout("Usage: %s {command} [options...] [argument...]", binary)
688 ToStdout("%s <command> --help to see details, or man %s", binary, binary)
691 # compute the max line length for cmd + usage
692 mlen = max([len(" %s" % cmd) for cmd in commands])
693 mlen = min(60, mlen) # should not get here...
695 # and format a nice command list
696 ToStdout("Commands:")
697 for cmd in sortedcmds:
698 cmdstr = " %s" % (cmd,)
699 help_text = commands[cmd][4]
700 help_lines = textwrap.wrap(help_text, 79 - 3 - mlen)
701 ToStdout("%-*s - %s", mlen, cmdstr, help_lines.pop(0))
702 for line in help_lines:
703 ToStdout("%-*s %s", mlen, "", line)
707 return None, None, None
709 # get command, unalias it, and look it up in commands
713 raise errors.ProgrammerError("Alias '%s' overrides an existing"
716 if aliases[cmd] not in commands:
717 raise errors.ProgrammerError("Alias '%s' maps to non-existing"
718 " command '%s'" % (cmd, aliases[cmd]))
722 func, args_def, parser_opts, usage, description = commands[cmd]
723 parser = OptionParser(option_list=parser_opts + [_DRY_RUN_OPT],
724 description=description,
725 formatter=TitledHelpFormatter(),
726 usage="%%prog %s %s" % (cmd, usage))
727 parser.disable_interspersed_args()
728 options, args = parser.parse_args()
730 if not _CheckArguments(cmd, args_def, args):
731 return None, None, None
733 return func, options, args
736 def _CheckArguments(cmd, args_def, args):
737 """Verifies the arguments using the argument definition.
741 1. Abort with error if values specified by user but none expected.
743 1. For each argument in definition
745 1. Keep running count of minimum number of values (min_count)
746 1. Keep running count of maximum number of values (max_count)
747 1. If it has an unlimited number of values
749 1. Abort with error if it's not the last argument in the definition
751 1. If last argument has limited number of values
753 1. Abort with error if number of values doesn't match or is too large
755 1. Abort with error if user didn't pass enough values (min_count)
758 if args and not args_def:
759 ToStderr("Error: Command %s expects no arguments", cmd)
766 last_idx = len(args_def) - 1
768 for idx, arg in enumerate(args_def):
769 if min_count is None:
771 elif arg.min is not None:
774 if max_count is None:
776 elif arg.max is not None:
780 check_max = (arg.max is not None)
782 elif arg.max is None:
783 raise errors.ProgrammerError("Only the last argument can have max=None")
786 # Command with exact number of arguments
787 if (min_count is not None and max_count is not None and
788 min_count == max_count and len(args) != min_count):
789 ToStderr("Error: Command %s expects %d argument(s)", cmd, min_count)
792 # Command with limited number of arguments
793 if max_count is not None and len(args) > max_count:
794 ToStderr("Error: Command %s expects only %d argument(s)",
798 # Command with some required arguments
799 if min_count is not None and len(args) < min_count:
800 ToStderr("Error: Command %s expects at least %d argument(s)",
807 def SplitNodeOption(value):
808 """Splits the value of a --node option.
811 if value and ':' in value:
812 return value.split(':', 1)
818 def wrapper(*args, **kwargs):
821 return fn(*args, **kwargs)
827 def AskUser(text, choices=None):
828 """Ask the user a question.
830 @param text: the question to ask
832 @param choices: list with elements tuples (input_char, return_value,
833 description); if not given, it will default to: [('y', True,
834 'Perform the operation'), ('n', False, 'Do no do the operation')];
835 note that the '?' char is reserved for help
837 @return: one of the return values from the choices list; if input is
838 not possible (i.e. not running with a tty, we return the last
843 choices = [('y', True, 'Perform the operation'),
844 ('n', False, 'Do not perform the operation')]
845 if not choices or not isinstance(choices, list):
846 raise errors.ProgrammerError("Invalid choices argument to AskUser")
847 for entry in choices:
848 if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
849 raise errors.ProgrammerError("Invalid choices element to AskUser")
851 answer = choices[-1][1]
853 for line in text.splitlines():
854 new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
855 text = "\n".join(new_text)
857 f = file("/dev/tty", "a+")
861 chars = [entry[0] for entry in choices]
862 chars[-1] = "[%s]" % chars[-1]
864 maps = dict([(entry[0], entry[1]) for entry in choices])
868 f.write("/".join(chars))
870 line = f.readline(2).strip().lower()
875 for entry in choices:
876 f.write(" %s - %s\n" % (entry[0], entry[2]))
884 class JobSubmittedException(Exception):
885 """Job was submitted, client should exit.
887 This exception has one argument, the ID of the job that was
888 submitted. The handler should print this ID.
890 This is not an error, just a structured way to exit from clients.
895 def SendJob(ops, cl=None):
896 """Function to submit an opcode without waiting for the results.
899 @param ops: list of opcodes
900 @type cl: luxi.Client
901 @param cl: the luxi client to use for communicating with the master;
902 if None, a new client will be created
908 job_id = cl.SubmitJob(ops)
913 def PollJob(job_id, cl=None, feedback_fn=None):
914 """Function to poll for the result of a job.
916 @type job_id: job identified
917 @param job_id: the job to poll for results
918 @type cl: luxi.Client
919 @param cl: the luxi client to use for communicating with the master;
920 if None, a new client will be created
927 prev_logmsg_serial = None
930 result = cl.WaitForJobChange(job_id, ["status"], prev_job_info,
933 # job not found, go away!
934 raise errors.JobLost("Job with id %s lost" % job_id)
936 # Split result, a tuple of (field values, log entries)
937 (job_info, log_entries) = result
938 (status, ) = job_info
941 for log_entry in log_entries:
942 (serial, timestamp, _, message) = log_entry
943 if callable(feedback_fn):
944 feedback_fn(log_entry[1:])
946 encoded = utils.SafeEncode(message)
947 ToStdout("%s %s", time.ctime(utils.MergeTime(timestamp)), encoded)
948 prev_logmsg_serial = max(prev_logmsg_serial, serial)
950 # TODO: Handle canceled and archived jobs
951 elif status in (constants.JOB_STATUS_SUCCESS,
952 constants.JOB_STATUS_ERROR,
953 constants.JOB_STATUS_CANCELING,
954 constants.JOB_STATUS_CANCELED):
957 prev_job_info = job_info
959 jobs = cl.QueryJobs([job_id], ["status", "opstatus", "opresult"])
961 raise errors.JobLost("Job with id %s lost" % job_id)
963 status, opstatus, result = jobs[0]
964 if status == constants.JOB_STATUS_SUCCESS:
966 elif status in (constants.JOB_STATUS_CANCELING,
967 constants.JOB_STATUS_CANCELED):
968 raise errors.OpExecError("Job was canceled")
971 for idx, (status, msg) in enumerate(zip(opstatus, result)):
972 if status == constants.OP_STATUS_SUCCESS:
974 elif status == constants.OP_STATUS_ERROR:
975 errors.MaybeRaise(msg)
977 raise errors.OpExecError("partial failure (opcode %d): %s" %
980 raise errors.OpExecError(str(msg))
981 # default failure mode
982 raise errors.OpExecError(result)
985 def SubmitOpCode(op, cl=None, feedback_fn=None):
986 """Legacy function to submit an opcode.
988 This is just a simple wrapper over the construction of the processor
989 instance. It should be extended to better handle feedback and
990 interaction functions.
996 job_id = SendJob([op], cl)
998 op_results = PollJob(job_id, cl=cl, feedback_fn=feedback_fn)
1000 return op_results[0]
1003 def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
1004 """Wrapper around SubmitOpCode or SendJob.
1006 This function will decide, based on the 'opts' parameter, whether to
1007 submit and wait for the result of the opcode (and return it), or
1008 whether to just send the job and print its identifier. It is used in
1009 order to simplify the implementation of the '--submit' option.
1011 It will also add the dry-run parameter from the options passed, if true.
1014 if opts and opts.dry_run:
1015 op.dry_run = opts.dry_run
1016 if opts and opts.submit_only:
1017 job_id = SendJob([op], cl=cl)
1018 raise JobSubmittedException(job_id)
1020 return SubmitOpCode(op, cl=cl, feedback_fn=feedback_fn)
1024 # TODO: Cache object?
1026 client = luxi.Client()
1027 except luxi.NoMasterError:
1028 master, myself = ssconf.GetMasterAndMyself()
1029 if master != myself:
1030 raise errors.OpPrereqError("This is not the master node, please connect"
1031 " to node '%s' and rerun the command" %
1038 def FormatError(err):
1039 """Return a formatted error message for a given error.
1041 This function takes an exception instance and returns a tuple
1042 consisting of two values: first, the recommended exit code, and
1043 second, a string describing the error message (not
1044 newline-terminated).
1050 if isinstance(err, errors.ConfigurationError):
1051 txt = "Corrupt configuration file: %s" % msg
1053 obuf.write(txt + "\n")
1054 obuf.write("Aborting.")
1056 elif isinstance(err, errors.HooksAbort):
1057 obuf.write("Failure: hooks execution failed:\n")
1058 for node, script, out in err.args[0]:
1060 obuf.write(" node: %s, script: %s, output: %s\n" %
1061 (node, script, out))
1063 obuf.write(" node: %s, script: %s (no output)\n" %
1065 elif isinstance(err, errors.HooksFailure):
1066 obuf.write("Failure: hooks general failure: %s" % msg)
1067 elif isinstance(err, errors.ResolverError):
1068 this_host = utils.HostInfo.SysName()
1069 if err.args[0] == this_host:
1070 msg = "Failure: can't resolve my own hostname ('%s')"
1072 msg = "Failure: can't resolve hostname '%s'"
1073 obuf.write(msg % err.args[0])
1074 elif isinstance(err, errors.OpPrereqError):
1075 obuf.write("Failure: prerequisites not met for this"
1076 " operation:\n%s" % msg)
1077 elif isinstance(err, errors.OpExecError):
1078 obuf.write("Failure: command execution error:\n%s" % msg)
1079 elif isinstance(err, errors.TagError):
1080 obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
1081 elif isinstance(err, errors.JobQueueDrainError):
1082 obuf.write("Failure: the job queue is marked for drain and doesn't"
1083 " accept new requests\n")
1084 elif isinstance(err, errors.JobQueueFull):
1085 obuf.write("Failure: the job queue is full and doesn't accept new"
1086 " job submissions until old jobs are archived\n")
1087 elif isinstance(err, errors.TypeEnforcementError):
1088 obuf.write("Parameter Error: %s" % msg)
1089 elif isinstance(err, errors.ParameterError):
1090 obuf.write("Failure: unknown/wrong parameter name '%s'" % msg)
1091 elif isinstance(err, errors.GenericError):
1092 obuf.write("Unhandled Ganeti error: %s" % msg)
1093 elif isinstance(err, luxi.NoMasterError):
1094 obuf.write("Cannot communicate with the master daemon.\nIs it running"
1095 " and listening for connections?")
1096 elif isinstance(err, luxi.TimeoutError):
1097 obuf.write("Timeout while talking to the master daemon. Error:\n"
1099 elif isinstance(err, luxi.ProtocolError):
1100 obuf.write("Unhandled protocol error while talking to the master daemon:\n"
1102 elif isinstance(err, JobSubmittedException):
1103 obuf.write("JobID: %s\n" % err.args[0])
1106 obuf.write("Unhandled exception: %s" % msg)
1107 return retcode, obuf.getvalue().rstrip('\n')
1110 def GenericMain(commands, override=None, aliases=None):
1111 """Generic main function for all the gnt-* commands.
1114 - commands: a dictionary with a special structure, see the design doc
1115 for command line handling.
1116 - override: if not None, we expect a dictionary with keys that will
1117 override command line options; this can be used to pass
1118 options from the scripts to generic functions
1119 - aliases: dictionary with command aliases {'alias': 'target, ...}
1122 # save the program name and the entire command line for later logging
1124 binary = os.path.basename(sys.argv[0]) or sys.argv[0]
1125 if len(sys.argv) >= 2:
1126 binary += " " + sys.argv[1]
1127 old_cmdline = " ".join(sys.argv[2:])
1131 binary = "<unknown program>"
1138 func, options, args = _ParseArgs(sys.argv, commands, aliases)
1139 except errors.ParameterError, err:
1140 result, err_msg = FormatError(err)
1144 if func is None: # parse error
1147 if override is not None:
1148 for key, val in override.iteritems():
1149 setattr(options, key, val)
1151 utils.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
1152 stderr_logging=True, program=binary)
1155 logging.info("run with arguments '%s'", old_cmdline)
1157 logging.info("run with no arguments")
1160 result = func(options, args)
1161 except (errors.GenericError, luxi.ProtocolError,
1162 JobSubmittedException), err:
1163 result, err_msg = FormatError(err)
1164 logging.exception("Error during command processing")
1170 def GenerateTable(headers, fields, separator, data,
1171 numfields=None, unitfields=None,
1173 """Prints a table with headers and different fields.
1176 @param headers: dictionary mapping field names to headers for
1179 @param fields: the field names corresponding to each row in
1181 @param separator: the separator to be used; if this is None,
1182 the default 'smart' algorithm is used which computes optimal
1183 field width, otherwise just the separator is used between
1186 @param data: a list of lists, each sublist being one row to be output
1187 @type numfields: list
1188 @param numfields: a list with the fields that hold numeric
1189 values and thus should be right-aligned
1190 @type unitfields: list
1191 @param unitfields: a list with the fields that hold numeric
1192 values that should be formatted with the units field
1193 @type units: string or None
1194 @param units: the units we should use for formatting, or None for
1195 automatic choice (human-readable for non-separator usage, otherwise
1196 megabytes); this is a one-letter string
1205 if numfields is None:
1207 if unitfields is None:
1210 numfields = utils.FieldSet(*numfields)
1211 unitfields = utils.FieldSet(*unitfields)
1214 for field in fields:
1215 if headers and field not in headers:
1216 # TODO: handle better unknown fields (either revert to old
1217 # style of raising exception, or deal more intelligently with
1219 headers[field] = field
1220 if separator is not None:
1221 format_fields.append("%s")
1222 elif numfields.Matches(field):
1223 format_fields.append("%*s")
1225 format_fields.append("%-*s")
1227 if separator is None:
1228 mlens = [0 for name in fields]
1229 format = ' '.join(format_fields)
1231 format = separator.replace("%", "%%").join(format_fields)
1236 for idx, val in enumerate(row):
1237 if unitfields.Matches(fields[idx]):
1243 val = row[idx] = utils.FormatUnit(val, units)
1244 val = row[idx] = str(val)
1245 if separator is None:
1246 mlens[idx] = max(mlens[idx], len(val))
1251 for idx, name in enumerate(fields):
1253 if separator is None:
1254 mlens[idx] = max(mlens[idx], len(hdr))
1255 args.append(mlens[idx])
1257 result.append(format % tuple(args))
1262 line = ['-' for _ in fields]
1263 for idx in xrange(len(fields)):
1264 if separator is None:
1265 args.append(mlens[idx])
1266 args.append(line[idx])
1267 result.append(format % tuple(args))
1272 def FormatTimestamp(ts):
1273 """Formats a given timestamp.
1276 @param ts: a timeval-type timestamp, a tuple of seconds and microseconds
1279 @return: a string with the formatted timestamp
1282 if not isinstance (ts, (tuple, list)) or len(ts) != 2:
1285 return time.strftime("%F %T", time.localtime(sec)) + ".%06d" % usec
1288 def ParseTimespec(value):
1289 """Parse a time specification.
1291 The following suffixed will be recognized:
1299 Without any suffix, the value will be taken to be in seconds.
1304 raise errors.OpPrereqError("Empty time specification passed")
1312 if value[-1] not in suffix_map:
1316 raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1318 multiplier = suffix_map[value[-1]]
1320 if not value: # no data left after stripping the suffix
1321 raise errors.OpPrereqError("Invalid time specification (only"
1324 value = int(value) * multiplier
1326 raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1330 def GetOnlineNodes(nodes, cl=None, nowarn=False):
1331 """Returns the names of online nodes.
1333 This function will also log a warning on stderr with the names of
1336 @param nodes: if not empty, use only this subset of nodes (minus the
1338 @param cl: if not None, luxi client to use
1339 @type nowarn: boolean
1340 @param nowarn: by default, this function will output a note with the
1341 offline nodes that are skipped; if this parameter is True the
1342 note is not displayed
1348 result = cl.QueryNodes(names=nodes, fields=["name", "offline"],
1350 offline = [row[0] for row in result if row[1]]
1351 if offline and not nowarn:
1352 ToStderr("Note: skipping offline node(s): %s" % ", ".join(offline))
1353 return [row[0] for row in result if not row[1]]
1356 def _ToStream(stream, txt, *args):
1357 """Write a message to a stream, bypassing the logging system
1359 @type stream: file object
1360 @param stream: the file to which we should write
1362 @param txt: the message
1367 stream.write(txt % args)
1374 def ToStdout(txt, *args):
1375 """Write a message to stdout only, bypassing the logging system
1377 This is just a wrapper over _ToStream.
1380 @param txt: the message
1383 _ToStream(sys.stdout, txt, *args)
1386 def ToStderr(txt, *args):
1387 """Write a message to stderr only, bypassing the logging system
1389 This is just a wrapper over _ToStream.
1392 @param txt: the message
1395 _ToStream(sys.stderr, txt, *args)
1398 class JobExecutor(object):
1399 """Class which manages the submission and execution of multiple jobs.
1401 Note that instances of this class should not be reused between
1405 def __init__(self, cl=None, verbose=True):
1410 self.verbose = verbose
1413 def QueueJob(self, name, *ops):
1414 """Record a job for later submit.
1417 @param name: a description of the job, will be used in WaitJobSet
1419 self.queue.append((name, ops))
1421 def SubmitPending(self):
1422 """Submit all pending jobs.
1425 results = self.cl.SubmitManyJobs([row[1] for row in self.queue])
1426 for ((status, data), (name, _)) in zip(results, self.queue):
1427 self.jobs.append((status, data, name))
1429 def GetResults(self):
1430 """Wait for and return the results of all jobs.
1433 @return: list of tuples (success, job results), in the same order
1434 as the submitted jobs; if a job has failed, instead of the result
1435 there will be the error message
1439 self.SubmitPending()
1442 ok_jobs = [row[1] for row in self.jobs if row[0]]
1444 ToStdout("Submitted jobs %s", ", ".join(ok_jobs))
1445 for submit_status, jid, name in self.jobs:
1446 if not submit_status:
1447 ToStderr("Failed to submit job for %s: %s", name, jid)
1448 results.append((False, jid))
1451 ToStdout("Waiting for job %s for %s...", jid, name)
1453 job_result = PollJob(jid, cl=self.cl)
1455 except (errors.GenericError, luxi.ProtocolError), err:
1456 _, job_result = FormatError(err)
1458 # the error message will always be shown, verbose or not
1459 ToStderr("Job %s for %s has failed: %s", jid, name, job_result)
1461 results.append((success, job_result))
1464 def WaitOrShow(self, wait):
1465 """Wait for job results or only print the job IDs.
1468 @param wait: whether to wait or not
1472 return self.GetResults()
1475 self.SubmitPending()
1476 for status, result, name in self.jobs:
1478 ToStdout("%s: %s", result, name)
1480 ToStderr("Failure for %s: %s", name, result)