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",
87 # Generic functions for CLI programs
92 "JobSubmittedException",
97 # Formatting functions
98 "ToStderr", "ToStdout",
107 # command line options support infrastructure
108 "ARGS_MANY_INSTANCES",
122 "OPT_COMPL_INST_ADD_NODES",
123 "OPT_COMPL_MANY_NODES",
124 "OPT_COMPL_ONE_IALLOCATOR",
125 "OPT_COMPL_ONE_INSTANCE",
126 "OPT_COMPL_ONE_NODE",
137 def __init__(self, min=0, max=None):
142 return ("<%s min=%s max=%s>" %
143 (self.__class__.__name__, self.min, self.max))
146 class ArgSuggest(_Argument):
147 """Suggesting argument.
149 Value can be any of the ones passed to the constructor.
152 def __init__(self, min=0, max=None, choices=None):
153 _Argument.__init__(self, min=min, max=max)
154 self.choices = choices
157 return ("<%s min=%s max=%s choices=%r>" %
158 (self.__class__.__name__, self.min, self.max, self.choices))
161 class ArgChoice(ArgSuggest):
164 Value can be any of the ones passed to the constructor. Like L{ArgSuggest},
165 but value must be one of the choices.
170 class ArgUnknown(_Argument):
171 """Unknown argument to program (e.g. determined at runtime).
176 class ArgInstance(_Argument):
177 """Instances argument.
182 class ArgNode(_Argument):
187 class ArgJobId(_Argument):
193 class ArgFile(_Argument):
194 """File path argument.
199 class ArgCommand(_Argument):
205 class ArgHost(_Argument):
212 ARGS_MANY_INSTANCES = [ArgInstance()]
213 ARGS_MANY_NODES = [ArgNode()]
214 ARGS_ONE_INSTANCE = [ArgInstance(min=1, max=1)]
215 ARGS_ONE_NODE = [ArgNode(min=1, max=1)]
219 def _ExtractTagsObject(opts, args):
220 """Extract the tag type object.
222 Note that this function will modify its args parameter.
225 if not hasattr(opts, "tag_type"):
226 raise errors.ProgrammerError("tag_type not passed to _ExtractTagsObject")
228 if kind == constants.TAG_CLUSTER:
230 elif kind == constants.TAG_NODE or kind == constants.TAG_INSTANCE:
232 raise errors.OpPrereqError("no arguments passed to the command")
236 raise errors.ProgrammerError("Unhandled tag type '%s'" % kind)
240 def _ExtendTags(opts, args):
241 """Extend the args if a source file has been given.
243 This function will extend the tags with the contents of the file
244 passed in the 'tags_source' attribute of the opts parameter. A file
245 named '-' will be replaced by stdin.
248 fname = opts.tags_source
254 new_fh = open(fname, "r")
257 # we don't use the nice 'new_data = [line.strip() for line in fh]'
258 # because of python bug 1633941
260 line = new_fh.readline()
263 new_data.append(line.strip())
266 args.extend(new_data)
269 def ListTags(opts, args):
270 """List the tags on a given object.
272 This is a generic implementation that knows how to deal with all
273 three cases of tag objects (cluster, node, instance). The opts
274 argument is expected to contain a tag_type field denoting what
275 object type we work on.
278 kind, name = _ExtractTagsObject(opts, args)
279 op = opcodes.OpGetTags(kind=kind, name=name)
280 result = SubmitOpCode(op)
281 result = list(result)
287 def AddTags(opts, args):
288 """Add tags on a given object.
290 This is a generic implementation that knows how to deal with all
291 three cases of tag objects (cluster, node, instance). The opts
292 argument is expected to contain a tag_type field denoting what
293 object type we work on.
296 kind, name = _ExtractTagsObject(opts, args)
297 _ExtendTags(opts, args)
299 raise errors.OpPrereqError("No tags to be added")
300 op = opcodes.OpAddTags(kind=kind, name=name, tags=args)
304 def RemoveTags(opts, args):
305 """Remove tags from a given object.
307 This is a generic implementation that knows how to deal with all
308 three cases of tag objects (cluster, node, instance). The opts
309 argument is expected to contain a tag_type field denoting what
310 object type we work on.
313 kind, name = _ExtractTagsObject(opts, args)
314 _ExtendTags(opts, args)
316 raise errors.OpPrereqError("No tags to be removed")
317 op = opcodes.OpDelTags(kind=kind, name=name, tags=args)
321 def check_unit(option, opt, value):
322 """OptParsers custom converter for units.
326 return utils.ParseUnit(value)
327 except errors.UnitParseError, err:
328 raise OptionValueError("option %s: %s" % (opt, err))
331 def _SplitKeyVal(opt, data):
332 """Convert a KeyVal string into a dict.
334 This function will convert a key=val[,...] string into a dict. Empty
335 values will be converted specially: keys which have the prefix 'no_'
336 will have the value=False and the prefix stripped, the others will
340 @param opt: a string holding the option name for which we process the
341 data, used in building error messages
343 @param data: a string of the format key=val,key=val,...
345 @return: {key=val, key=val}
346 @raises errors.ParameterError: if there are duplicate keys
351 for elem in data.split(","):
353 key, val = elem.split("=", 1)
355 if elem.startswith(NO_PREFIX):
356 key, val = elem[len(NO_PREFIX):], False
357 elif elem.startswith(UN_PREFIX):
358 key, val = elem[len(UN_PREFIX):], None
360 key, val = elem, True
362 raise errors.ParameterError("Duplicate key '%s' in option %s" %
368 def check_ident_key_val(option, opt, value):
369 """Custom parser for ident:key=val,key=val options.
371 This will store the parsed values as a tuple (ident, {key: val}). As such,
372 multiple uses of this option via action=append is possible.
376 ident, rest = value, ''
378 ident, rest = value.split(":", 1)
380 if ident.startswith(NO_PREFIX):
382 msg = "Cannot pass options when removing parameter groups: %s" % value
383 raise errors.ParameterError(msg)
384 retval = (ident[len(NO_PREFIX):], False)
385 elif ident.startswith(UN_PREFIX):
387 msg = "Cannot pass options when removing parameter groups: %s" % value
388 raise errors.ParameterError(msg)
389 retval = (ident[len(UN_PREFIX):], None)
391 kv_dict = _SplitKeyVal(opt, rest)
392 retval = (ident, kv_dict)
396 def check_key_val(option, opt, value):
397 """Custom parser class for key=val,key=val options.
399 This will store the parsed values as a dict {key: val}.
402 return _SplitKeyVal(opt, value)
405 # completion_suggestion is normally a list. Using numeric values not evaluating
406 # to False for dynamic completion.
407 (OPT_COMPL_MANY_NODES,
409 OPT_COMPL_ONE_INSTANCE,
411 OPT_COMPL_ONE_IALLOCATOR,
412 OPT_COMPL_INST_ADD_NODES) = range(100, 106)
414 OPT_COMPL_ALL = frozenset([
415 OPT_COMPL_MANY_NODES,
417 OPT_COMPL_ONE_INSTANCE,
419 OPT_COMPL_ONE_IALLOCATOR,
420 OPT_COMPL_INST_ADD_NODES,
424 class CliOption(Option):
425 """Custom option class for optparse.
428 ATTRS = Option.ATTRS + [
429 "completion_suggest",
431 TYPES = Option.TYPES + (
436 TYPE_CHECKER = Option.TYPE_CHECKER.copy()
437 TYPE_CHECKER["identkeyval"] = check_ident_key_val
438 TYPE_CHECKER["keyval"] = check_key_val
439 TYPE_CHECKER["unit"] = check_unit
442 # optparse.py sets make_option, so we do it for our own option class, too
443 cli_option = CliOption
446 DEBUG_OPT = cli_option("-d", "--debug", default=False,
448 help="Turn debugging on")
450 NOHDR_OPT = cli_option("--no-headers", default=False,
451 action="store_true", dest="no_headers",
452 help="Don't display column headers")
454 SEP_OPT = cli_option("--separator", default=None,
455 action="store", dest="separator",
456 help=("Separator between output fields"
457 " (defaults to one space)"))
459 USEUNITS_OPT = cli_option("--units", default=None,
460 dest="units", choices=('h', 'm', 'g', 't'),
461 help="Specify units for output (one of hmgt)")
463 FIELDS_OPT = cli_option("-o", "--output", dest="output", action="store",
464 type="string", metavar="FIELDS",
465 help="Comma separated list of output fields")
467 FORCE_OPT = cli_option("-f", "--force", dest="force", action="store_true",
468 default=False, help="Force the operation")
470 CONFIRM_OPT = cli_option("--yes", dest="confirm", action="store_true",
471 default=False, help="Do not require confirmation")
473 TAG_SRC_OPT = cli_option("--from", dest="tags_source",
474 default=None, help="File with tag names")
476 SUBMIT_OPT = cli_option("--submit", dest="submit_only",
477 default=False, action="store_true",
478 help=("Submit the job and return the job ID, but"
479 " don't wait for the job to finish"))
481 SYNC_OPT = cli_option("--sync", dest="do_locking",
482 default=False, action="store_true",
483 help=("Grab locks while doing the queries"
484 " in order to ensure more consistent results"))
486 _DRY_RUN_OPT = cli_option("--dry-run", default=False,
488 help=("Do not execute the operation, just run the"
489 " check steps and verify it it could be"
492 VERBOSE_OPT = cli_option("-v", "--verbose", default=False,
494 help="Increase the verbosity of the operation")
496 DEBUG_SIMERR_OPT = cli_option("--debug-simulate-errors", default=False,
497 action="store_true", dest="simulate_errors",
498 help="Debugging option that makes the operation"
499 " treat most runtime checks as failed")
501 NWSYNC_OPT = cli_option("--no-wait-for-sync", dest="wait_for_sync",
502 default=True, action="store_false",
503 help="Don't wait for sync (DANGEROUS!)")
505 DISK_TEMPLATE_OPT = cli_option("-t", "--disk-template", dest="disk_template",
506 help="Custom disk setup (diskless, file,"
508 default=None, metavar="TEMPL",
509 choices=list(constants.DISK_TEMPLATES))
511 NONICS_OPT = cli_option("--no-nics", default=False, action="store_true",
512 help="Do not create any network cards for"
515 FILESTORE_DIR_OPT = cli_option("--file-storage-dir", dest="file_storage_dir",
516 help="Relative path under default cluster-wide"
517 " file storage dir to store file-based disks",
518 default=None, metavar="<DIR>")
520 FILESTORE_DRIVER_OPT = cli_option("--file-driver", dest="file_driver",
521 help="Driver to use for image files",
522 default="loop", metavar="<DRIVER>",
523 choices=list(constants.FILE_DRIVER))
525 IALLOCATOR_OPT = cli_option("-I", "--iallocator", metavar="<NAME>",
526 help="Select nodes for the instance automatically"
527 " using the <NAME> iallocator plugin",
528 default=None, type="string",
529 completion_suggest=OPT_COMPL_ONE_IALLOCATOR)
531 OS_OPT = cli_option("-o", "--os-type", dest="os", help="What OS to run",
533 completion_suggest=OPT_COMPL_ONE_OS)
535 BACKEND_OPT = cli_option("-B", "--backend-parameters", dest="beparams",
536 type="keyval", default={},
537 help="Backend parameters")
539 HVOPTS_OPT = cli_option("-H", "--hypervisor-parameters", type="keyval",
540 default={}, dest="hvparams",
541 help="Hypervisor parameters")
543 HYPERVISOR_OPT = cli_option("-H", "--hypervisor-parameters", dest="hypervisor",
544 help="Hypervisor and hypervisor options, in the"
545 " format hypervisor:option=value,option=value,...",
546 default=None, type="identkeyval")
548 HVLIST_OPT = cli_option("-H", "--hypervisor-parameters", dest="hvparams",
549 help="Hypervisor and hypervisor options, in the"
550 " format hypervisor:option=value,option=value,...",
551 default=[], action="append", type="identkeyval")
553 NOIPCHECK_OPT = cli_option("--no-ip-check", dest="ip_check", default=True,
554 action="store_false",
555 help="Don't check that the instance's IP"
558 NET_OPT = cli_option("--net",
559 help="NIC parameters", default=[],
560 dest="nics", action="append", type="identkeyval")
562 DISK_OPT = cli_option("--disk", help="Disk parameters", default=[],
563 dest="disks", action="append", type="identkeyval")
565 DISKIDX_OPT = cli_option("--disks", dest="disks", default=None,
566 help="Comma-separated list of disks"
567 " indices to act on (e.g. 0,2) (optional,"
568 " defaults to all disks)")
570 OS_SIZE_OPT = cli_option("-s", "--os-size", dest="sd_size",
571 help="Enforces a single-disk configuration using the"
572 " given disk size, in MiB unless a suffix is used",
573 default=None, type="unit", metavar="<size>")
575 IGNORE_CONSIST_OPT = cli_option("--ignore-consistency",
576 dest="ignore_consistency",
577 action="store_true", default=False,
578 help="Ignore the consistency of the disks on"
581 NONLIVE_OPT = cli_option("--non-live", dest="live",
582 default=True, action="store_false",
583 help="Do a non-live migration (this usually means"
584 " freeze the instance, save the state, transfer and"
585 " only then resume running on the secondary node)")
587 NODE_PLACEMENT_OPT = cli_option("-n", "--node", dest="node",
588 help="Target node and optional secondary node",
589 metavar="<pnode>[:<snode>]",
590 completion_suggest=OPT_COMPL_INST_ADD_NODES)
592 NODE_LIST_OPT = cli_option("-n", "--node", dest="nodes", default=[],
593 action="append", metavar="<node>",
594 help="Use only this node (can be used multiple"
595 " times, if not given defaults to all nodes)",
596 completion_suggest=OPT_COMPL_ONE_NODE)
598 SINGLE_NODE_OPT = cli_option("-n", "--node", dest="node", help="Target node",
600 completion_suggest=OPT_COMPL_ONE_NODE)
602 NOSTART_OPT = cli_option("--no-start", dest="start", default=True,
603 action="store_false",
604 help="Don't start the instance after creation")
606 SHOWCMD_OPT = cli_option("--show-cmd", dest="show_command",
607 action="store_true", default=False,
608 help="Show command instead of executing it")
610 CLEANUP_OPT = cli_option("--cleanup", dest="cleanup",
611 default=False, action="store_true",
612 help="Instead of performing the migration, try to"
613 " recover from a failed cleanup. This is safe"
614 " to run even if the instance is healthy, but it"
615 " will create extra replication traffic and "
616 " disrupt briefly the replication (like during the"
619 STATIC_OPT = cli_option("-s", "--static", dest="static",
620 action="store_true", default=False,
621 help="Only show configuration data, not runtime data")
623 ALL_OPT = cli_option("--all", dest="show_all",
624 default=False, action="store_true",
625 help="Show info on all instances on the cluster."
626 " This can take a long time to run, use wisely")
628 SELECT_OS_OPT = cli_option("--select-os", dest="select_os",
629 action="store_true", default=False,
630 help="Interactive OS reinstall, lists available"
631 " OS templates for selection")
633 IGNORE_FAILURES_OPT = cli_option("--ignore-failures", dest="ignore_failures",
634 action="store_true", default=False,
635 help="Remove the instance from the cluster"
636 " configuration even if there are failures"
637 " during the removal process")
640 def _ParseArgs(argv, commands, aliases):
641 """Parser for the command line arguments.
643 This function parses the arguments and returns the function which
644 must be executed together with its (modified) arguments.
646 @param argv: the command line
647 @param commands: dictionary with special contents, see the design
648 doc for cmdline handling
649 @param aliases: dictionary with command aliases {'alias': 'target, ...}
655 binary = argv[0].split("/")[-1]
657 if len(argv) > 1 and argv[1] == "--version":
658 ToStdout("%s (ganeti) %s", binary, constants.RELEASE_VERSION)
659 # Quit right away. That way we don't have to care about this special
660 # argument. optparse.py does it the same.
663 if len(argv) < 2 or not (argv[1] in commands or
665 # let's do a nice thing
666 sortedcmds = commands.keys()
669 ToStdout("Usage: %s {command} [options...] [argument...]", binary)
670 ToStdout("%s <command> --help to see details, or man %s", binary, binary)
673 # compute the max line length for cmd + usage
674 mlen = max([len(" %s" % cmd) for cmd in commands])
675 mlen = min(60, mlen) # should not get here...
677 # and format a nice command list
678 ToStdout("Commands:")
679 for cmd in sortedcmds:
680 cmdstr = " %s" % (cmd,)
681 help_text = commands[cmd][4]
682 help_lines = textwrap.wrap(help_text, 79 - 3 - mlen)
683 ToStdout("%-*s - %s", mlen, cmdstr, help_lines.pop(0))
684 for line in help_lines:
685 ToStdout("%-*s %s", mlen, "", line)
689 return None, None, None
691 # get command, unalias it, and look it up in commands
695 raise errors.ProgrammerError("Alias '%s' overrides an existing"
698 if aliases[cmd] not in commands:
699 raise errors.ProgrammerError("Alias '%s' maps to non-existing"
700 " command '%s'" % (cmd, aliases[cmd]))
704 func, args_def, parser_opts, usage, description = commands[cmd]
705 parser = OptionParser(option_list=parser_opts + [_DRY_RUN_OPT],
706 description=description,
707 formatter=TitledHelpFormatter(),
708 usage="%%prog %s %s" % (cmd, usage))
709 parser.disable_interspersed_args()
710 options, args = parser.parse_args()
712 if not _CheckArguments(cmd, args_def, args):
713 return None, None, None
715 return func, options, args
718 def _CheckArguments(cmd, args_def, args):
719 """Verifies the arguments using the argument definition.
723 1. Abort with error if values specified by user but none expected.
725 1. For each argument in definition
727 1. Keep running count of minimum number of values (min_count)
728 1. Keep running count of maximum number of values (max_count)
729 1. If it has an unlimited number of values
731 1. Abort with error if it's not the last argument in the definition
733 1. If last argument has limited number of values
735 1. Abort with error if number of values doesn't match or is too large
737 1. Abort with error if user didn't pass enough values (min_count)
740 if args and not args_def:
741 ToStderr("Error: Command %s expects no arguments", cmd)
748 last_idx = len(args_def) - 1
750 for idx, arg in enumerate(args_def):
751 if min_count is None:
753 elif arg.min is not None:
756 if max_count is None:
758 elif arg.max is not None:
762 check_max = (arg.max is not None)
764 elif arg.max is None:
765 raise errors.ProgrammerError("Only the last argument can have max=None")
768 # Command with exact number of arguments
769 if (min_count is not None and max_count is not None and
770 min_count == max_count and len(args) != min_count):
771 ToStderr("Error: Command %s expects %d argument(s)", cmd, min_count)
774 # Command with limited number of arguments
775 if max_count is not None and len(args) > max_count:
776 ToStderr("Error: Command %s expects only %d argument(s)",
780 # Command with some required arguments
781 if min_count is not None and len(args) < min_count:
782 ToStderr("Error: Command %s expects at least %d argument(s)",
789 def SplitNodeOption(value):
790 """Splits the value of a --node option.
793 if value and ':' in value:
794 return value.split(':', 1)
800 def wrapper(*args, **kwargs):
803 return fn(*args, **kwargs)
809 def AskUser(text, choices=None):
810 """Ask the user a question.
812 @param text: the question to ask
814 @param choices: list with elements tuples (input_char, return_value,
815 description); if not given, it will default to: [('y', True,
816 'Perform the operation'), ('n', False, 'Do no do the operation')];
817 note that the '?' char is reserved for help
819 @return: one of the return values from the choices list; if input is
820 not possible (i.e. not running with a tty, we return the last
825 choices = [('y', True, 'Perform the operation'),
826 ('n', False, 'Do not perform the operation')]
827 if not choices or not isinstance(choices, list):
828 raise errors.ProgrammerError("Invalid choices argument to AskUser")
829 for entry in choices:
830 if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
831 raise errors.ProgrammerError("Invalid choices element to AskUser")
833 answer = choices[-1][1]
835 for line in text.splitlines():
836 new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
837 text = "\n".join(new_text)
839 f = file("/dev/tty", "a+")
843 chars = [entry[0] for entry in choices]
844 chars[-1] = "[%s]" % chars[-1]
846 maps = dict([(entry[0], entry[1]) for entry in choices])
850 f.write("/".join(chars))
852 line = f.readline(2).strip().lower()
857 for entry in choices:
858 f.write(" %s - %s\n" % (entry[0], entry[2]))
866 class JobSubmittedException(Exception):
867 """Job was submitted, client should exit.
869 This exception has one argument, the ID of the job that was
870 submitted. The handler should print this ID.
872 This is not an error, just a structured way to exit from clients.
877 def SendJob(ops, cl=None):
878 """Function to submit an opcode without waiting for the results.
881 @param ops: list of opcodes
882 @type cl: luxi.Client
883 @param cl: the luxi client to use for communicating with the master;
884 if None, a new client will be created
890 job_id = cl.SubmitJob(ops)
895 def PollJob(job_id, cl=None, feedback_fn=None):
896 """Function to poll for the result of a job.
898 @type job_id: job identified
899 @param job_id: the job to poll for results
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
909 prev_logmsg_serial = None
912 result = cl.WaitForJobChange(job_id, ["status"], prev_job_info,
915 # job not found, go away!
916 raise errors.JobLost("Job with id %s lost" % job_id)
918 # Split result, a tuple of (field values, log entries)
919 (job_info, log_entries) = result
920 (status, ) = job_info
923 for log_entry in log_entries:
924 (serial, timestamp, _, message) = log_entry
925 if callable(feedback_fn):
926 feedback_fn(log_entry[1:])
928 encoded = utils.SafeEncode(message)
929 ToStdout("%s %s", time.ctime(utils.MergeTime(timestamp)), encoded)
930 prev_logmsg_serial = max(prev_logmsg_serial, serial)
932 # TODO: Handle canceled and archived jobs
933 elif status in (constants.JOB_STATUS_SUCCESS,
934 constants.JOB_STATUS_ERROR,
935 constants.JOB_STATUS_CANCELING,
936 constants.JOB_STATUS_CANCELED):
939 prev_job_info = job_info
941 jobs = cl.QueryJobs([job_id], ["status", "opstatus", "opresult"])
943 raise errors.JobLost("Job with id %s lost" % job_id)
945 status, opstatus, result = jobs[0]
946 if status == constants.JOB_STATUS_SUCCESS:
948 elif status in (constants.JOB_STATUS_CANCELING,
949 constants.JOB_STATUS_CANCELED):
950 raise errors.OpExecError("Job was canceled")
953 for idx, (status, msg) in enumerate(zip(opstatus, result)):
954 if status == constants.OP_STATUS_SUCCESS:
956 elif status == constants.OP_STATUS_ERROR:
957 errors.MaybeRaise(msg)
959 raise errors.OpExecError("partial failure (opcode %d): %s" %
962 raise errors.OpExecError(str(msg))
963 # default failure mode
964 raise errors.OpExecError(result)
967 def SubmitOpCode(op, cl=None, feedback_fn=None):
968 """Legacy function to submit an opcode.
970 This is just a simple wrapper over the construction of the processor
971 instance. It should be extended to better handle feedback and
972 interaction functions.
978 job_id = SendJob([op], cl)
980 op_results = PollJob(job_id, cl=cl, feedback_fn=feedback_fn)
985 def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
986 """Wrapper around SubmitOpCode or SendJob.
988 This function will decide, based on the 'opts' parameter, whether to
989 submit and wait for the result of the opcode (and return it), or
990 whether to just send the job and print its identifier. It is used in
991 order to simplify the implementation of the '--submit' option.
993 It will also add the dry-run parameter from the options passed, if true.
996 if opts and opts.dry_run:
997 op.dry_run = opts.dry_run
998 if opts and opts.submit_only:
999 job_id = SendJob([op], cl=cl)
1000 raise JobSubmittedException(job_id)
1002 return SubmitOpCode(op, cl=cl, feedback_fn=feedback_fn)
1006 # TODO: Cache object?
1008 client = luxi.Client()
1009 except luxi.NoMasterError:
1010 master, myself = ssconf.GetMasterAndMyself()
1011 if master != myself:
1012 raise errors.OpPrereqError("This is not the master node, please connect"
1013 " to node '%s' and rerun the command" %
1020 def FormatError(err):
1021 """Return a formatted error message for a given error.
1023 This function takes an exception instance and returns a tuple
1024 consisting of two values: first, the recommended exit code, and
1025 second, a string describing the error message (not
1026 newline-terminated).
1032 if isinstance(err, errors.ConfigurationError):
1033 txt = "Corrupt configuration file: %s" % msg
1035 obuf.write(txt + "\n")
1036 obuf.write("Aborting.")
1038 elif isinstance(err, errors.HooksAbort):
1039 obuf.write("Failure: hooks execution failed:\n")
1040 for node, script, out in err.args[0]:
1042 obuf.write(" node: %s, script: %s, output: %s\n" %
1043 (node, script, out))
1045 obuf.write(" node: %s, script: %s (no output)\n" %
1047 elif isinstance(err, errors.HooksFailure):
1048 obuf.write("Failure: hooks general failure: %s" % msg)
1049 elif isinstance(err, errors.ResolverError):
1050 this_host = utils.HostInfo.SysName()
1051 if err.args[0] == this_host:
1052 msg = "Failure: can't resolve my own hostname ('%s')"
1054 msg = "Failure: can't resolve hostname '%s'"
1055 obuf.write(msg % err.args[0])
1056 elif isinstance(err, errors.OpPrereqError):
1057 obuf.write("Failure: prerequisites not met for this"
1058 " operation:\n%s" % msg)
1059 elif isinstance(err, errors.OpExecError):
1060 obuf.write("Failure: command execution error:\n%s" % msg)
1061 elif isinstance(err, errors.TagError):
1062 obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
1063 elif isinstance(err, errors.JobQueueDrainError):
1064 obuf.write("Failure: the job queue is marked for drain and doesn't"
1065 " accept new requests\n")
1066 elif isinstance(err, errors.JobQueueFull):
1067 obuf.write("Failure: the job queue is full and doesn't accept new"
1068 " job submissions until old jobs are archived\n")
1069 elif isinstance(err, errors.TypeEnforcementError):
1070 obuf.write("Parameter Error: %s" % msg)
1071 elif isinstance(err, errors.ParameterError):
1072 obuf.write("Failure: unknown/wrong parameter name '%s'" % msg)
1073 elif isinstance(err, errors.GenericError):
1074 obuf.write("Unhandled Ganeti error: %s" % msg)
1075 elif isinstance(err, luxi.NoMasterError):
1076 obuf.write("Cannot communicate with the master daemon.\nIs it running"
1077 " and listening for connections?")
1078 elif isinstance(err, luxi.TimeoutError):
1079 obuf.write("Timeout while talking to the master daemon. Error:\n"
1081 elif isinstance(err, luxi.ProtocolError):
1082 obuf.write("Unhandled protocol error while talking to the master daemon:\n"
1084 elif isinstance(err, JobSubmittedException):
1085 obuf.write("JobID: %s\n" % err.args[0])
1088 obuf.write("Unhandled exception: %s" % msg)
1089 return retcode, obuf.getvalue().rstrip('\n')
1092 def GenericMain(commands, override=None, aliases=None):
1093 """Generic main function for all the gnt-* commands.
1096 - commands: a dictionary with a special structure, see the design doc
1097 for command line handling.
1098 - override: if not None, we expect a dictionary with keys that will
1099 override command line options; this can be used to pass
1100 options from the scripts to generic functions
1101 - aliases: dictionary with command aliases {'alias': 'target, ...}
1104 # save the program name and the entire command line for later logging
1106 binary = os.path.basename(sys.argv[0]) or sys.argv[0]
1107 if len(sys.argv) >= 2:
1108 binary += " " + sys.argv[1]
1109 old_cmdline = " ".join(sys.argv[2:])
1113 binary = "<unknown program>"
1120 func, options, args = _ParseArgs(sys.argv, commands, aliases)
1121 except errors.ParameterError, err:
1122 result, err_msg = FormatError(err)
1126 if func is None: # parse error
1129 if override is not None:
1130 for key, val in override.iteritems():
1131 setattr(options, key, val)
1133 utils.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
1134 stderr_logging=True, program=binary)
1137 logging.info("run with arguments '%s'", old_cmdline)
1139 logging.info("run with no arguments")
1142 result = func(options, args)
1143 except (errors.GenericError, luxi.ProtocolError,
1144 JobSubmittedException), err:
1145 result, err_msg = FormatError(err)
1146 logging.exception("Error during command processing")
1152 def GenerateTable(headers, fields, separator, data,
1153 numfields=None, unitfields=None,
1155 """Prints a table with headers and different fields.
1158 @param headers: dictionary mapping field names to headers for
1161 @param fields: the field names corresponding to each row in
1163 @param separator: the separator to be used; if this is None,
1164 the default 'smart' algorithm is used which computes optimal
1165 field width, otherwise just the separator is used between
1168 @param data: a list of lists, each sublist being one row to be output
1169 @type numfields: list
1170 @param numfields: a list with the fields that hold numeric
1171 values and thus should be right-aligned
1172 @type unitfields: list
1173 @param unitfields: a list with the fields that hold numeric
1174 values that should be formatted with the units field
1175 @type units: string or None
1176 @param units: the units we should use for formatting, or None for
1177 automatic choice (human-readable for non-separator usage, otherwise
1178 megabytes); this is a one-letter string
1187 if numfields is None:
1189 if unitfields is None:
1192 numfields = utils.FieldSet(*numfields)
1193 unitfields = utils.FieldSet(*unitfields)
1196 for field in fields:
1197 if headers and field not in headers:
1198 # TODO: handle better unknown fields (either revert to old
1199 # style of raising exception, or deal more intelligently with
1201 headers[field] = field
1202 if separator is not None:
1203 format_fields.append("%s")
1204 elif numfields.Matches(field):
1205 format_fields.append("%*s")
1207 format_fields.append("%-*s")
1209 if separator is None:
1210 mlens = [0 for name in fields]
1211 format = ' '.join(format_fields)
1213 format = separator.replace("%", "%%").join(format_fields)
1218 for idx, val in enumerate(row):
1219 if unitfields.Matches(fields[idx]):
1225 val = row[idx] = utils.FormatUnit(val, units)
1226 val = row[idx] = str(val)
1227 if separator is None:
1228 mlens[idx] = max(mlens[idx], len(val))
1233 for idx, name in enumerate(fields):
1235 if separator is None:
1236 mlens[idx] = max(mlens[idx], len(hdr))
1237 args.append(mlens[idx])
1239 result.append(format % tuple(args))
1244 line = ['-' for _ in fields]
1245 for idx in xrange(len(fields)):
1246 if separator is None:
1247 args.append(mlens[idx])
1248 args.append(line[idx])
1249 result.append(format % tuple(args))
1254 def FormatTimestamp(ts):
1255 """Formats a given timestamp.
1258 @param ts: a timeval-type timestamp, a tuple of seconds and microseconds
1261 @return: a string with the formatted timestamp
1264 if not isinstance (ts, (tuple, list)) or len(ts) != 2:
1267 return time.strftime("%F %T", time.localtime(sec)) + ".%06d" % usec
1270 def ParseTimespec(value):
1271 """Parse a time specification.
1273 The following suffixed will be recognized:
1281 Without any suffix, the value will be taken to be in seconds.
1286 raise errors.OpPrereqError("Empty time specification passed")
1294 if value[-1] not in suffix_map:
1298 raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1300 multiplier = suffix_map[value[-1]]
1302 if not value: # no data left after stripping the suffix
1303 raise errors.OpPrereqError("Invalid time specification (only"
1306 value = int(value) * multiplier
1308 raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1312 def GetOnlineNodes(nodes, cl=None, nowarn=False):
1313 """Returns the names of online nodes.
1315 This function will also log a warning on stderr with the names of
1318 @param nodes: if not empty, use only this subset of nodes (minus the
1320 @param cl: if not None, luxi client to use
1321 @type nowarn: boolean
1322 @param nowarn: by default, this function will output a note with the
1323 offline nodes that are skipped; if this parameter is True the
1324 note is not displayed
1330 result = cl.QueryNodes(names=nodes, fields=["name", "offline"],
1332 offline = [row[0] for row in result if row[1]]
1333 if offline and not nowarn:
1334 ToStderr("Note: skipping offline node(s): %s" % ", ".join(offline))
1335 return [row[0] for row in result if not row[1]]
1338 def _ToStream(stream, txt, *args):
1339 """Write a message to a stream, bypassing the logging system
1341 @type stream: file object
1342 @param stream: the file to which we should write
1344 @param txt: the message
1349 stream.write(txt % args)
1356 def ToStdout(txt, *args):
1357 """Write a message to stdout only, bypassing the logging system
1359 This is just a wrapper over _ToStream.
1362 @param txt: the message
1365 _ToStream(sys.stdout, txt, *args)
1368 def ToStderr(txt, *args):
1369 """Write a message to stderr only, bypassing the logging system
1371 This is just a wrapper over _ToStream.
1374 @param txt: the message
1377 _ToStream(sys.stderr, txt, *args)
1380 class JobExecutor(object):
1381 """Class which manages the submission and execution of multiple jobs.
1383 Note that instances of this class should not be reused between
1387 def __init__(self, cl=None, verbose=True):
1392 self.verbose = verbose
1395 def QueueJob(self, name, *ops):
1396 """Record a job for later submit.
1399 @param name: a description of the job, will be used in WaitJobSet
1401 self.queue.append((name, ops))
1403 def SubmitPending(self):
1404 """Submit all pending jobs.
1407 results = self.cl.SubmitManyJobs([row[1] for row in self.queue])
1408 for ((status, data), (name, _)) in zip(results, self.queue):
1409 self.jobs.append((status, data, name))
1411 def GetResults(self):
1412 """Wait for and return the results of all jobs.
1415 @return: list of tuples (success, job results), in the same order
1416 as the submitted jobs; if a job has failed, instead of the result
1417 there will be the error message
1421 self.SubmitPending()
1424 ok_jobs = [row[1] for row in self.jobs if row[0]]
1426 ToStdout("Submitted jobs %s", ", ".join(ok_jobs))
1427 for submit_status, jid, name in self.jobs:
1428 if not submit_status:
1429 ToStderr("Failed to submit job for %s: %s", name, jid)
1430 results.append((False, jid))
1433 ToStdout("Waiting for job %s for %s...", jid, name)
1435 job_result = PollJob(jid, cl=self.cl)
1437 except (errors.GenericError, luxi.ProtocolError), err:
1438 _, job_result = FormatError(err)
1440 # the error message will always be shown, verbose or not
1441 ToStderr("Job %s for %s has failed: %s", jid, name, job_result)
1443 results.append((success, job_result))
1446 def WaitOrShow(self, wait):
1447 """Wait for job results or only print the job IDs.
1450 @param wait: whether to wait or not
1454 return self.GetResults()
1457 self.SubmitPending()
1458 for status, result, name in self.jobs:
1460 ToStdout("%s: %s", result, name)
1462 ToStderr("Failure for %s: %s", name, result)