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",
86 # Generic functions for CLI programs
91 "JobSubmittedException",
96 # Formatting functions
97 "ToStderr", "ToStdout",
106 # command line options support infrastructure
107 "ARGS_MANY_INSTANCES",
121 "OPT_COMPL_INST_ADD_NODES",
122 "OPT_COMPL_MANY_NODES",
123 "OPT_COMPL_ONE_IALLOCATOR",
124 "OPT_COMPL_ONE_INSTANCE",
125 "OPT_COMPL_ONE_NODE",
136 def __init__(self, min=0, max=None):
141 return ("<%s min=%s max=%s>" %
142 (self.__class__.__name__, self.min, self.max))
145 class ArgSuggest(_Argument):
146 """Suggesting argument.
148 Value can be any of the ones passed to the constructor.
151 def __init__(self, min=0, max=None, choices=None):
152 _Argument.__init__(self, min=min, max=max)
153 self.choices = choices
156 return ("<%s min=%s max=%s choices=%r>" %
157 (self.__class__.__name__, self.min, self.max, self.choices))
160 class ArgChoice(ArgSuggest):
163 Value can be any of the ones passed to the constructor. Like L{ArgSuggest},
164 but value must be one of the choices.
169 class ArgUnknown(_Argument):
170 """Unknown argument to program (e.g. determined at runtime).
175 class ArgInstance(_Argument):
176 """Instances argument.
181 class ArgNode(_Argument):
186 class ArgJobId(_Argument):
192 class ArgFile(_Argument):
193 """File path argument.
198 class ArgCommand(_Argument):
204 class ArgHost(_Argument):
211 ARGS_MANY_INSTANCES = [ArgInstance()]
212 ARGS_MANY_NODES = [ArgNode()]
213 ARGS_ONE_INSTANCE = [ArgInstance(min=1, max=1)]
214 ARGS_ONE_NODE = [ArgNode(min=1, max=1)]
218 def _ExtractTagsObject(opts, args):
219 """Extract the tag type object.
221 Note that this function will modify its args parameter.
224 if not hasattr(opts, "tag_type"):
225 raise errors.ProgrammerError("tag_type not passed to _ExtractTagsObject")
227 if kind == constants.TAG_CLUSTER:
229 elif kind == constants.TAG_NODE or kind == constants.TAG_INSTANCE:
231 raise errors.OpPrereqError("no arguments passed to the command")
235 raise errors.ProgrammerError("Unhandled tag type '%s'" % kind)
239 def _ExtendTags(opts, args):
240 """Extend the args if a source file has been given.
242 This function will extend the tags with the contents of the file
243 passed in the 'tags_source' attribute of the opts parameter. A file
244 named '-' will be replaced by stdin.
247 fname = opts.tags_source
253 new_fh = open(fname, "r")
256 # we don't use the nice 'new_data = [line.strip() for line in fh]'
257 # because of python bug 1633941
259 line = new_fh.readline()
262 new_data.append(line.strip())
265 args.extend(new_data)
268 def ListTags(opts, args):
269 """List the tags on a given object.
271 This is a generic implementation that knows how to deal with all
272 three cases of tag objects (cluster, node, instance). The opts
273 argument is expected to contain a tag_type field denoting what
274 object type we work on.
277 kind, name = _ExtractTagsObject(opts, args)
278 op = opcodes.OpGetTags(kind=kind, name=name)
279 result = SubmitOpCode(op)
280 result = list(result)
286 def AddTags(opts, args):
287 """Add tags on a given object.
289 This is a generic implementation that knows how to deal with all
290 three cases of tag objects (cluster, node, instance). The opts
291 argument is expected to contain a tag_type field denoting what
292 object type we work on.
295 kind, name = _ExtractTagsObject(opts, args)
296 _ExtendTags(opts, args)
298 raise errors.OpPrereqError("No tags to be added")
299 op = opcodes.OpAddTags(kind=kind, name=name, tags=args)
303 def RemoveTags(opts, args):
304 """Remove tags from a given object.
306 This is a generic implementation that knows how to deal with all
307 three cases of tag objects (cluster, node, instance). The opts
308 argument is expected to contain a tag_type field denoting what
309 object type we work on.
312 kind, name = _ExtractTagsObject(opts, args)
313 _ExtendTags(opts, args)
315 raise errors.OpPrereqError("No tags to be removed")
316 op = opcodes.OpDelTags(kind=kind, name=name, tags=args)
320 def check_unit(option, opt, value):
321 """OptParsers custom converter for units.
325 return utils.ParseUnit(value)
326 except errors.UnitParseError, err:
327 raise OptionValueError("option %s: %s" % (opt, err))
330 def _SplitKeyVal(opt, data):
331 """Convert a KeyVal string into a dict.
333 This function will convert a key=val[,...] string into a dict. Empty
334 values will be converted specially: keys which have the prefix 'no_'
335 will have the value=False and the prefix stripped, the others will
339 @param opt: a string holding the option name for which we process the
340 data, used in building error messages
342 @param data: a string of the format key=val,key=val,...
344 @return: {key=val, key=val}
345 @raises errors.ParameterError: if there are duplicate keys
350 for elem in data.split(","):
352 key, val = elem.split("=", 1)
354 if elem.startswith(NO_PREFIX):
355 key, val = elem[len(NO_PREFIX):], False
356 elif elem.startswith(UN_PREFIX):
357 key, val = elem[len(UN_PREFIX):], None
359 key, val = elem, True
361 raise errors.ParameterError("Duplicate key '%s' in option %s" %
367 def check_ident_key_val(option, opt, value):
368 """Custom parser for ident:key=val,key=val options.
370 This will store the parsed values as a tuple (ident, {key: val}). As such,
371 multiple uses of this option via action=append is possible.
375 ident, rest = value, ''
377 ident, rest = value.split(":", 1)
379 if ident.startswith(NO_PREFIX):
381 msg = "Cannot pass options when removing parameter groups: %s" % value
382 raise errors.ParameterError(msg)
383 retval = (ident[len(NO_PREFIX):], False)
384 elif ident.startswith(UN_PREFIX):
386 msg = "Cannot pass options when removing parameter groups: %s" % value
387 raise errors.ParameterError(msg)
388 retval = (ident[len(UN_PREFIX):], None)
390 kv_dict = _SplitKeyVal(opt, rest)
391 retval = (ident, kv_dict)
395 def check_key_val(option, opt, value):
396 """Custom parser class for key=val,key=val options.
398 This will store the parsed values as a dict {key: val}.
401 return _SplitKeyVal(opt, value)
404 # completion_suggestion is normally a list. Using numeric values not evaluating
405 # to False for dynamic completion.
406 (OPT_COMPL_MANY_NODES,
408 OPT_COMPL_ONE_INSTANCE,
410 OPT_COMPL_ONE_IALLOCATOR,
411 OPT_COMPL_INST_ADD_NODES) = range(100, 106)
413 OPT_COMPL_ALL = frozenset([
414 OPT_COMPL_MANY_NODES,
416 OPT_COMPL_ONE_INSTANCE,
418 OPT_COMPL_ONE_IALLOCATOR,
419 OPT_COMPL_INST_ADD_NODES,
423 class CliOption(Option):
424 """Custom option class for optparse.
427 ATTRS = Option.ATTRS + [
428 "completion_suggest",
430 TYPES = Option.TYPES + (
435 TYPE_CHECKER = Option.TYPE_CHECKER.copy()
436 TYPE_CHECKER["identkeyval"] = check_ident_key_val
437 TYPE_CHECKER["keyval"] = check_key_val
438 TYPE_CHECKER["unit"] = check_unit
441 # optparse.py sets make_option, so we do it for our own option class, too
442 cli_option = CliOption
445 DEBUG_OPT = cli_option("-d", "--debug", default=False,
447 help="Turn debugging on")
449 NOHDR_OPT = cli_option("--no-headers", default=False,
450 action="store_true", dest="no_headers",
451 help="Don't display column headers")
453 SEP_OPT = cli_option("--separator", default=None,
454 action="store", dest="separator",
455 help=("Separator between output fields"
456 " (defaults to one space)"))
458 USEUNITS_OPT = cli_option("--units", default=None,
459 dest="units", choices=('h', 'm', 'g', 't'),
460 help="Specify units for output (one of hmgt)")
462 FIELDS_OPT = cli_option("-o", "--output", dest="output", action="store",
463 type="string", metavar="FIELDS",
464 help="Comma separated list of output fields")
466 FORCE_OPT = cli_option("-f", "--force", dest="force", action="store_true",
467 default=False, help="Force the operation")
469 CONFIRM_OPT = cli_option("--yes", dest="confirm", action="store_true",
470 default=False, help="Do not require confirmation")
472 TAG_SRC_OPT = cli_option("--from", dest="tags_source",
473 default=None, help="File with tag names")
475 SUBMIT_OPT = cli_option("--submit", dest="submit_only",
476 default=False, action="store_true",
477 help=("Submit the job and return the job ID, but"
478 " don't wait for the job to finish"))
480 SYNC_OPT = cli_option("--sync", dest="do_locking",
481 default=False, action="store_true",
482 help=("Grab locks while doing the queries"
483 " in order to ensure more consistent results"))
485 _DRY_RUN_OPT = cli_option("--dry-run", default=False,
487 help=("Do not execute the operation, just run the"
488 " check steps and verify it it could be"
491 VERBOSE_OPT = cli_option("-v", "--verbose", default=False,
493 help="Increase the verbosity of the operation")
495 DEBUG_SIMERR_OPT = cli_option("--debug-simulate-errors", default=False,
496 action="store_true", dest="simulate_errors",
497 help="Debugging option that makes the operation"
498 " treat most runtime checks as failed")
500 NWSYNC_OPT = cli_option("--no-wait-for-sync", dest="wait_for_sync",
501 default=True, action="store_false",
502 help="Don't wait for sync (DANGEROUS!)")
504 DISK_TEMPLATE_OPT = cli_option("-t", "--disk-template", dest="disk_template",
505 help="Custom disk setup (diskless, file,"
507 default=None, metavar="TEMPL",
508 choices=list(constants.DISK_TEMPLATES))
510 NONICS_OPT = cli_option("--no-nics", default=False, action="store_true",
511 help="Do not create any network cards for"
514 FILESTORE_DIR_OPT = cli_option("--file-storage-dir", dest="file_storage_dir",
515 help="Relative path under default cluster-wide"
516 " file storage dir to store file-based disks",
517 default=None, metavar="<DIR>")
519 FILESTORE_DRIVER_OPT = cli_option("--file-driver", dest="file_driver",
520 help="Driver to use for image files",
521 default="loop", metavar="<DRIVER>",
522 choices=list(constants.FILE_DRIVER))
524 IALLOCATOR_OPT = cli_option("-I", "--iallocator", metavar="<NAME>",
525 help="Select nodes for the instance automatically"
526 " using the <NAME> iallocator plugin",
527 default=None, type="string",
528 completion_suggest=OPT_COMPL_ONE_IALLOCATOR)
530 OS_OPT = cli_option("-o", "--os-type", dest="os", help="What OS to run",
532 completion_suggest=OPT_COMPL_ONE_OS)
534 BACKEND_OPT = cli_option("-B", "--backend-parameters", dest="beparams",
535 type="keyval", default={},
536 help="Backend parameters")
538 HVOPTS_OPT = cli_option("-H", "--hypervisor-parameters", type="keyval",
539 default={}, dest="hvparams",
540 help="Hypervisor parameters")
542 HYPERVISOR_OPT = cli_option("-H", "--hypervisor-parameters", dest="hypervisor",
543 help="Hypervisor and hypervisor options, in the"
544 " format hypervisor:option=value,option=value,...",
545 default=None, type="identkeyval")
547 HVLIST_OPT = cli_option("-H", "--hypervisor-parameters", dest="hvparams",
548 help="Hypervisor and hypervisor options, in the"
549 " format hypervisor:option=value,option=value,...",
550 default=[], action="append", type="identkeyval")
552 NOIPCHECK_OPT = cli_option("--no-ip-check", dest="ip_check", default=True,
553 action="store_false",
554 help="Don't check that the instance's IP"
557 NET_OPT = cli_option("--net",
558 help="NIC parameters", default=[],
559 dest="nics", action="append", type="identkeyval")
561 DISK_OPT = cli_option("--disk", help="Disk parameters", default=[],
562 dest="disks", action="append", type="identkeyval")
564 DISKIDX_OPT = cli_option("--disks", dest="disks", default=None,
565 help="Comma-separated list of disks"
566 " indices to act on (e.g. 0,2) (optional,"
567 " defaults to all disks)")
569 OS_SIZE_OPT = cli_option("-s", "--os-size", dest="sd_size",
570 help="Enforces a single-disk configuration using the"
571 " given disk size, in MiB unless a suffix is used",
572 default=None, type="unit", metavar="<size>")
574 IGNORE_CONSIST_OPT = cli_option("--ignore-consistency",
575 dest="ignore_consistency",
576 action="store_true", default=False,
577 help="Ignore the consistency of the disks on"
580 NONLIVE_OPT = cli_option("--non-live", dest="live",
581 default=True, action="store_false",
582 help="Do a non-live migration (this usually means"
583 " freeze the instance, save the state, transfer and"
584 " only then resume running on the secondary node)")
586 NODE_PLACEMENT_OPT = cli_option("-n", "--node", dest="node",
587 help="Target node and optional secondary node",
588 metavar="<pnode>[:<snode>]",
589 completion_suggest=OPT_COMPL_INST_ADD_NODES)
591 NODE_LIST_OPT = cli_option("-n", "--node", dest="nodes", default=[],
592 action="append", metavar="<node>",
593 help="Use only this node (can be used multiple"
594 " times, if not given defaults to all nodes)",
595 completion_suggest=OPT_COMPL_ONE_NODE)
597 SINGLE_NODE_OPT = cli_option("-n", "--node", dest="node", help="Target node",
599 completion_suggest=OPT_COMPL_ONE_NODE)
601 NOSTART_OPT = cli_option("--no-start", dest="start", default=True,
602 action="store_false",
603 help="Don't start the instance after creation")
605 SHOWCMD_OPT = cli_option("--show-cmd", dest="show_command",
606 action="store_true", default=False,
607 help="Show command instead of executing it")
609 CLEANUP_OPT = cli_option("--cleanup", dest="cleanup",
610 default=False, action="store_true",
611 help="Instead of performing the migration, try to"
612 " recover from a failed cleanup. This is safe"
613 " to run even if the instance is healthy, but it"
614 " will create extra replication traffic and "
615 " disrupt briefly the replication (like during the"
618 STATIC_OPT = cli_option("-s", "--static", dest="static",
619 action="store_true", default=False,
620 help="Only show configuration data, not runtime data")
622 ALL_OPT = cli_option("--all", dest="show_all",
623 default=False, action="store_true",
624 help="Show info on all instances on the cluster."
625 " This can take a long time to run, use wisely")
627 SELECT_OS_OPT = cli_option("--select-os", dest="select_os",
628 action="store_true", default=False,
629 help="Interactive OS reinstall, lists available"
630 " OS templates for selection")
633 def _ParseArgs(argv, commands, aliases):
634 """Parser for the command line arguments.
636 This function parses the arguments and returns the function which
637 must be executed together with its (modified) arguments.
639 @param argv: the command line
640 @param commands: dictionary with special contents, see the design
641 doc for cmdline handling
642 @param aliases: dictionary with command aliases {'alias': 'target, ...}
648 binary = argv[0].split("/")[-1]
650 if len(argv) > 1 and argv[1] == "--version":
651 ToStdout("%s (ganeti) %s", binary, constants.RELEASE_VERSION)
652 # Quit right away. That way we don't have to care about this special
653 # argument. optparse.py does it the same.
656 if len(argv) < 2 or not (argv[1] in commands or
658 # let's do a nice thing
659 sortedcmds = commands.keys()
662 ToStdout("Usage: %s {command} [options...] [argument...]", binary)
663 ToStdout("%s <command> --help to see details, or man %s", binary, binary)
666 # compute the max line length for cmd + usage
667 mlen = max([len(" %s" % cmd) for cmd in commands])
668 mlen = min(60, mlen) # should not get here...
670 # and format a nice command list
671 ToStdout("Commands:")
672 for cmd in sortedcmds:
673 cmdstr = " %s" % (cmd,)
674 help_text = commands[cmd][4]
675 help_lines = textwrap.wrap(help_text, 79 - 3 - mlen)
676 ToStdout("%-*s - %s", mlen, cmdstr, help_lines.pop(0))
677 for line in help_lines:
678 ToStdout("%-*s %s", mlen, "", line)
682 return None, None, None
684 # get command, unalias it, and look it up in commands
688 raise errors.ProgrammerError("Alias '%s' overrides an existing"
691 if aliases[cmd] not in commands:
692 raise errors.ProgrammerError("Alias '%s' maps to non-existing"
693 " command '%s'" % (cmd, aliases[cmd]))
697 func, args_def, parser_opts, usage, description = commands[cmd]
698 parser = OptionParser(option_list=parser_opts + [_DRY_RUN_OPT],
699 description=description,
700 formatter=TitledHelpFormatter(),
701 usage="%%prog %s %s" % (cmd, usage))
702 parser.disable_interspersed_args()
703 options, args = parser.parse_args()
705 if not _CheckArguments(cmd, args_def, args):
706 return None, None, None
708 return func, options, args
711 def _CheckArguments(cmd, args_def, args):
712 """Verifies the arguments using the argument definition.
716 1. Abort with error if values specified by user but none expected.
718 1. For each argument in definition
720 1. Keep running count of minimum number of values (min_count)
721 1. Keep running count of maximum number of values (max_count)
722 1. If it has an unlimited number of values
724 1. Abort with error if it's not the last argument in the definition
726 1. If last argument has limited number of values
728 1. Abort with error if number of values doesn't match or is too large
730 1. Abort with error if user didn't pass enough values (min_count)
733 if args and not args_def:
734 ToStderr("Error: Command %s expects no arguments", cmd)
741 last_idx = len(args_def) - 1
743 for idx, arg in enumerate(args_def):
744 if min_count is None:
746 elif arg.min is not None:
749 if max_count is None:
751 elif arg.max is not None:
755 check_max = (arg.max is not None)
757 elif arg.max is None:
758 raise errors.ProgrammerError("Only the last argument can have max=None")
761 # Command with exact number of arguments
762 if (min_count is not None and max_count is not None and
763 min_count == max_count and len(args) != min_count):
764 ToStderr("Error: Command %s expects %d argument(s)", cmd, min_count)
767 # Command with limited number of arguments
768 if max_count is not None and len(args) > max_count:
769 ToStderr("Error: Command %s expects only %d argument(s)",
773 # Command with some required arguments
774 if min_count is not None and len(args) < min_count:
775 ToStderr("Error: Command %s expects at least %d argument(s)",
782 def SplitNodeOption(value):
783 """Splits the value of a --node option.
786 if value and ':' in value:
787 return value.split(':', 1)
793 def wrapper(*args, **kwargs):
796 return fn(*args, **kwargs)
802 def AskUser(text, choices=None):
803 """Ask the user a question.
805 @param text: the question to ask
807 @param choices: list with elements tuples (input_char, return_value,
808 description); if not given, it will default to: [('y', True,
809 'Perform the operation'), ('n', False, 'Do no do the operation')];
810 note that the '?' char is reserved for help
812 @return: one of the return values from the choices list; if input is
813 not possible (i.e. not running with a tty, we return the last
818 choices = [('y', True, 'Perform the operation'),
819 ('n', False, 'Do not perform the operation')]
820 if not choices or not isinstance(choices, list):
821 raise errors.ProgrammerError("Invalid choices argument to AskUser")
822 for entry in choices:
823 if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
824 raise errors.ProgrammerError("Invalid choices element to AskUser")
826 answer = choices[-1][1]
828 for line in text.splitlines():
829 new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
830 text = "\n".join(new_text)
832 f = file("/dev/tty", "a+")
836 chars = [entry[0] for entry in choices]
837 chars[-1] = "[%s]" % chars[-1]
839 maps = dict([(entry[0], entry[1]) for entry in choices])
843 f.write("/".join(chars))
845 line = f.readline(2).strip().lower()
850 for entry in choices:
851 f.write(" %s - %s\n" % (entry[0], entry[2]))
859 class JobSubmittedException(Exception):
860 """Job was submitted, client should exit.
862 This exception has one argument, the ID of the job that was
863 submitted. The handler should print this ID.
865 This is not an error, just a structured way to exit from clients.
870 def SendJob(ops, cl=None):
871 """Function to submit an opcode without waiting for the results.
874 @param ops: list of opcodes
875 @type cl: luxi.Client
876 @param cl: the luxi client to use for communicating with the master;
877 if None, a new client will be created
883 job_id = cl.SubmitJob(ops)
888 def PollJob(job_id, cl=None, feedback_fn=None):
889 """Function to poll for the result of a job.
891 @type job_id: job identified
892 @param job_id: the job to poll for results
893 @type cl: luxi.Client
894 @param cl: the luxi client to use for communicating with the master;
895 if None, a new client will be created
902 prev_logmsg_serial = None
905 result = cl.WaitForJobChange(job_id, ["status"], prev_job_info,
908 # job not found, go away!
909 raise errors.JobLost("Job with id %s lost" % job_id)
911 # Split result, a tuple of (field values, log entries)
912 (job_info, log_entries) = result
913 (status, ) = job_info
916 for log_entry in log_entries:
917 (serial, timestamp, _, message) = log_entry
918 if callable(feedback_fn):
919 feedback_fn(log_entry[1:])
921 encoded = utils.SafeEncode(message)
922 ToStdout("%s %s", time.ctime(utils.MergeTime(timestamp)), encoded)
923 prev_logmsg_serial = max(prev_logmsg_serial, serial)
925 # TODO: Handle canceled and archived jobs
926 elif status in (constants.JOB_STATUS_SUCCESS,
927 constants.JOB_STATUS_ERROR,
928 constants.JOB_STATUS_CANCELING,
929 constants.JOB_STATUS_CANCELED):
932 prev_job_info = job_info
934 jobs = cl.QueryJobs([job_id], ["status", "opstatus", "opresult"])
936 raise errors.JobLost("Job with id %s lost" % job_id)
938 status, opstatus, result = jobs[0]
939 if status == constants.JOB_STATUS_SUCCESS:
941 elif status in (constants.JOB_STATUS_CANCELING,
942 constants.JOB_STATUS_CANCELED):
943 raise errors.OpExecError("Job was canceled")
946 for idx, (status, msg) in enumerate(zip(opstatus, result)):
947 if status == constants.OP_STATUS_SUCCESS:
949 elif status == constants.OP_STATUS_ERROR:
950 errors.MaybeRaise(msg)
952 raise errors.OpExecError("partial failure (opcode %d): %s" %
955 raise errors.OpExecError(str(msg))
956 # default failure mode
957 raise errors.OpExecError(result)
960 def SubmitOpCode(op, cl=None, feedback_fn=None):
961 """Legacy function to submit an opcode.
963 This is just a simple wrapper over the construction of the processor
964 instance. It should be extended to better handle feedback and
965 interaction functions.
971 job_id = SendJob([op], cl)
973 op_results = PollJob(job_id, cl=cl, feedback_fn=feedback_fn)
978 def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
979 """Wrapper around SubmitOpCode or SendJob.
981 This function will decide, based on the 'opts' parameter, whether to
982 submit and wait for the result of the opcode (and return it), or
983 whether to just send the job and print its identifier. It is used in
984 order to simplify the implementation of the '--submit' option.
986 It will also add the dry-run parameter from the options passed, if true.
989 if opts and opts.dry_run:
990 op.dry_run = opts.dry_run
991 if opts and opts.submit_only:
992 job_id = SendJob([op], cl=cl)
993 raise JobSubmittedException(job_id)
995 return SubmitOpCode(op, cl=cl, feedback_fn=feedback_fn)
999 # TODO: Cache object?
1001 client = luxi.Client()
1002 except luxi.NoMasterError:
1003 master, myself = ssconf.GetMasterAndMyself()
1004 if master != myself:
1005 raise errors.OpPrereqError("This is not the master node, please connect"
1006 " to node '%s' and rerun the command" %
1013 def FormatError(err):
1014 """Return a formatted error message for a given error.
1016 This function takes an exception instance and returns a tuple
1017 consisting of two values: first, the recommended exit code, and
1018 second, a string describing the error message (not
1019 newline-terminated).
1025 if isinstance(err, errors.ConfigurationError):
1026 txt = "Corrupt configuration file: %s" % msg
1028 obuf.write(txt + "\n")
1029 obuf.write("Aborting.")
1031 elif isinstance(err, errors.HooksAbort):
1032 obuf.write("Failure: hooks execution failed:\n")
1033 for node, script, out in err.args[0]:
1035 obuf.write(" node: %s, script: %s, output: %s\n" %
1036 (node, script, out))
1038 obuf.write(" node: %s, script: %s (no output)\n" %
1040 elif isinstance(err, errors.HooksFailure):
1041 obuf.write("Failure: hooks general failure: %s" % msg)
1042 elif isinstance(err, errors.ResolverError):
1043 this_host = utils.HostInfo.SysName()
1044 if err.args[0] == this_host:
1045 msg = "Failure: can't resolve my own hostname ('%s')"
1047 msg = "Failure: can't resolve hostname '%s'"
1048 obuf.write(msg % err.args[0])
1049 elif isinstance(err, errors.OpPrereqError):
1050 obuf.write("Failure: prerequisites not met for this"
1051 " operation:\n%s" % msg)
1052 elif isinstance(err, errors.OpExecError):
1053 obuf.write("Failure: command execution error:\n%s" % msg)
1054 elif isinstance(err, errors.TagError):
1055 obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
1056 elif isinstance(err, errors.JobQueueDrainError):
1057 obuf.write("Failure: the job queue is marked for drain and doesn't"
1058 " accept new requests\n")
1059 elif isinstance(err, errors.JobQueueFull):
1060 obuf.write("Failure: the job queue is full and doesn't accept new"
1061 " job submissions until old jobs are archived\n")
1062 elif isinstance(err, errors.TypeEnforcementError):
1063 obuf.write("Parameter Error: %s" % msg)
1064 elif isinstance(err, errors.ParameterError):
1065 obuf.write("Failure: unknown/wrong parameter name '%s'" % msg)
1066 elif isinstance(err, errors.GenericError):
1067 obuf.write("Unhandled Ganeti error: %s" % msg)
1068 elif isinstance(err, luxi.NoMasterError):
1069 obuf.write("Cannot communicate with the master daemon.\nIs it running"
1070 " and listening for connections?")
1071 elif isinstance(err, luxi.TimeoutError):
1072 obuf.write("Timeout while talking to the master daemon. Error:\n"
1074 elif isinstance(err, luxi.ProtocolError):
1075 obuf.write("Unhandled protocol error while talking to the master daemon:\n"
1077 elif isinstance(err, JobSubmittedException):
1078 obuf.write("JobID: %s\n" % err.args[0])
1081 obuf.write("Unhandled exception: %s" % msg)
1082 return retcode, obuf.getvalue().rstrip('\n')
1085 def GenericMain(commands, override=None, aliases=None):
1086 """Generic main function for all the gnt-* commands.
1089 - commands: a dictionary with a special structure, see the design doc
1090 for command line handling.
1091 - override: if not None, we expect a dictionary with keys that will
1092 override command line options; this can be used to pass
1093 options from the scripts to generic functions
1094 - aliases: dictionary with command aliases {'alias': 'target, ...}
1097 # save the program name and the entire command line for later logging
1099 binary = os.path.basename(sys.argv[0]) or sys.argv[0]
1100 if len(sys.argv) >= 2:
1101 binary += " " + sys.argv[1]
1102 old_cmdline = " ".join(sys.argv[2:])
1106 binary = "<unknown program>"
1113 func, options, args = _ParseArgs(sys.argv, commands, aliases)
1114 except errors.ParameterError, err:
1115 result, err_msg = FormatError(err)
1119 if func is None: # parse error
1122 if override is not None:
1123 for key, val in override.iteritems():
1124 setattr(options, key, val)
1126 utils.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
1127 stderr_logging=True, program=binary)
1130 logging.info("run with arguments '%s'", old_cmdline)
1132 logging.info("run with no arguments")
1135 result = func(options, args)
1136 except (errors.GenericError, luxi.ProtocolError,
1137 JobSubmittedException), err:
1138 result, err_msg = FormatError(err)
1139 logging.exception("Error during command processing")
1145 def GenerateTable(headers, fields, separator, data,
1146 numfields=None, unitfields=None,
1148 """Prints a table with headers and different fields.
1151 @param headers: dictionary mapping field names to headers for
1154 @param fields: the field names corresponding to each row in
1156 @param separator: the separator to be used; if this is None,
1157 the default 'smart' algorithm is used which computes optimal
1158 field width, otherwise just the separator is used between
1161 @param data: a list of lists, each sublist being one row to be output
1162 @type numfields: list
1163 @param numfields: a list with the fields that hold numeric
1164 values and thus should be right-aligned
1165 @type unitfields: list
1166 @param unitfields: a list with the fields that hold numeric
1167 values that should be formatted with the units field
1168 @type units: string or None
1169 @param units: the units we should use for formatting, or None for
1170 automatic choice (human-readable for non-separator usage, otherwise
1171 megabytes); this is a one-letter string
1180 if numfields is None:
1182 if unitfields is None:
1185 numfields = utils.FieldSet(*numfields)
1186 unitfields = utils.FieldSet(*unitfields)
1189 for field in fields:
1190 if headers and field not in headers:
1191 # TODO: handle better unknown fields (either revert to old
1192 # style of raising exception, or deal more intelligently with
1194 headers[field] = field
1195 if separator is not None:
1196 format_fields.append("%s")
1197 elif numfields.Matches(field):
1198 format_fields.append("%*s")
1200 format_fields.append("%-*s")
1202 if separator is None:
1203 mlens = [0 for name in fields]
1204 format = ' '.join(format_fields)
1206 format = separator.replace("%", "%%").join(format_fields)
1211 for idx, val in enumerate(row):
1212 if unitfields.Matches(fields[idx]):
1218 val = row[idx] = utils.FormatUnit(val, units)
1219 val = row[idx] = str(val)
1220 if separator is None:
1221 mlens[idx] = max(mlens[idx], len(val))
1226 for idx, name in enumerate(fields):
1228 if separator is None:
1229 mlens[idx] = max(mlens[idx], len(hdr))
1230 args.append(mlens[idx])
1232 result.append(format % tuple(args))
1237 line = ['-' for _ in fields]
1238 for idx in xrange(len(fields)):
1239 if separator is None:
1240 args.append(mlens[idx])
1241 args.append(line[idx])
1242 result.append(format % tuple(args))
1247 def FormatTimestamp(ts):
1248 """Formats a given timestamp.
1251 @param ts: a timeval-type timestamp, a tuple of seconds and microseconds
1254 @return: a string with the formatted timestamp
1257 if not isinstance (ts, (tuple, list)) or len(ts) != 2:
1260 return time.strftime("%F %T", time.localtime(sec)) + ".%06d" % usec
1263 def ParseTimespec(value):
1264 """Parse a time specification.
1266 The following suffixed will be recognized:
1274 Without any suffix, the value will be taken to be in seconds.
1279 raise errors.OpPrereqError("Empty time specification passed")
1287 if value[-1] not in suffix_map:
1291 raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1293 multiplier = suffix_map[value[-1]]
1295 if not value: # no data left after stripping the suffix
1296 raise errors.OpPrereqError("Invalid time specification (only"
1299 value = int(value) * multiplier
1301 raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1305 def GetOnlineNodes(nodes, cl=None, nowarn=False):
1306 """Returns the names of online nodes.
1308 This function will also log a warning on stderr with the names of
1311 @param nodes: if not empty, use only this subset of nodes (minus the
1313 @param cl: if not None, luxi client to use
1314 @type nowarn: boolean
1315 @param nowarn: by default, this function will output a note with the
1316 offline nodes that are skipped; if this parameter is True the
1317 note is not displayed
1323 result = cl.QueryNodes(names=nodes, fields=["name", "offline"],
1325 offline = [row[0] for row in result if row[1]]
1326 if offline and not nowarn:
1327 ToStderr("Note: skipping offline node(s): %s" % ", ".join(offline))
1328 return [row[0] for row in result if not row[1]]
1331 def _ToStream(stream, txt, *args):
1332 """Write a message to a stream, bypassing the logging system
1334 @type stream: file object
1335 @param stream: the file to which we should write
1337 @param txt: the message
1342 stream.write(txt % args)
1349 def ToStdout(txt, *args):
1350 """Write a message to stdout only, bypassing the logging system
1352 This is just a wrapper over _ToStream.
1355 @param txt: the message
1358 _ToStream(sys.stdout, txt, *args)
1361 def ToStderr(txt, *args):
1362 """Write a message to stderr only, bypassing the logging system
1364 This is just a wrapper over _ToStream.
1367 @param txt: the message
1370 _ToStream(sys.stderr, txt, *args)
1373 class JobExecutor(object):
1374 """Class which manages the submission and execution of multiple jobs.
1376 Note that instances of this class should not be reused between
1380 def __init__(self, cl=None, verbose=True):
1385 self.verbose = verbose
1388 def QueueJob(self, name, *ops):
1389 """Record a job for later submit.
1392 @param name: a description of the job, will be used in WaitJobSet
1394 self.queue.append((name, ops))
1396 def SubmitPending(self):
1397 """Submit all pending jobs.
1400 results = self.cl.SubmitManyJobs([row[1] for row in self.queue])
1401 for ((status, data), (name, _)) in zip(results, self.queue):
1402 self.jobs.append((status, data, name))
1404 def GetResults(self):
1405 """Wait for and return the results of all jobs.
1408 @return: list of tuples (success, job results), in the same order
1409 as the submitted jobs; if a job has failed, instead of the result
1410 there will be the error message
1414 self.SubmitPending()
1417 ok_jobs = [row[1] for row in self.jobs if row[0]]
1419 ToStdout("Submitted jobs %s", ", ".join(ok_jobs))
1420 for submit_status, jid, name in self.jobs:
1421 if not submit_status:
1422 ToStderr("Failed to submit job for %s: %s", name, jid)
1423 results.append((False, jid))
1426 ToStdout("Waiting for job %s for %s...", jid, name)
1428 job_result = PollJob(jid, cl=self.cl)
1430 except (errors.GenericError, luxi.ProtocolError), err:
1431 _, job_result = FormatError(err)
1433 # the error message will always be shown, verbose or not
1434 ToStderr("Job %s for %s has failed: %s", jid, name, job_result)
1436 results.append((success, job_result))
1439 def WaitOrShow(self, wait):
1440 """Wait for job results or only print the job IDs.
1443 @param wait: whether to wait or not
1447 return self.GetResults()
1450 self.SubmitPending()
1451 for status, result, name in self.jobs:
1453 ToStdout("%s: %s", result, name)
1455 ToStderr("Failure for %s: %s", name, result)