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
60 "FILESTORE_DRIVER_OPT",
66 "IGNORE_FAILURES_OPT",
100 # Generic functions for CLI programs
105 "JobSubmittedException",
110 # Formatting functions
111 "ToStderr", "ToStdout",
120 # command line options support infrastructure
121 "ARGS_MANY_INSTANCES",
135 "OPT_COMPL_INST_ADD_NODES",
136 "OPT_COMPL_MANY_NODES",
137 "OPT_COMPL_ONE_IALLOCATOR",
138 "OPT_COMPL_ONE_INSTANCE",
139 "OPT_COMPL_ONE_NODE",
150 def __init__(self, min=0, max=None):
155 return ("<%s min=%s max=%s>" %
156 (self.__class__.__name__, self.min, self.max))
159 class ArgSuggest(_Argument):
160 """Suggesting argument.
162 Value can be any of the ones passed to the constructor.
165 def __init__(self, min=0, max=None, choices=None):
166 _Argument.__init__(self, min=min, max=max)
167 self.choices = choices
170 return ("<%s min=%s max=%s choices=%r>" %
171 (self.__class__.__name__, self.min, self.max, self.choices))
174 class ArgChoice(ArgSuggest):
177 Value can be any of the ones passed to the constructor. Like L{ArgSuggest},
178 but value must be one of the choices.
183 class ArgUnknown(_Argument):
184 """Unknown argument to program (e.g. determined at runtime).
189 class ArgInstance(_Argument):
190 """Instances argument.
195 class ArgNode(_Argument):
200 class ArgJobId(_Argument):
206 class ArgFile(_Argument):
207 """File path argument.
212 class ArgCommand(_Argument):
218 class ArgHost(_Argument):
225 ARGS_MANY_INSTANCES = [ArgInstance()]
226 ARGS_MANY_NODES = [ArgNode()]
227 ARGS_ONE_INSTANCE = [ArgInstance(min=1, max=1)]
228 ARGS_ONE_NODE = [ArgNode(min=1, max=1)]
232 def _ExtractTagsObject(opts, args):
233 """Extract the tag type object.
235 Note that this function will modify its args parameter.
238 if not hasattr(opts, "tag_type"):
239 raise errors.ProgrammerError("tag_type not passed to _ExtractTagsObject")
241 if kind == constants.TAG_CLUSTER:
243 elif kind == constants.TAG_NODE or kind == constants.TAG_INSTANCE:
245 raise errors.OpPrereqError("no arguments passed to the command")
249 raise errors.ProgrammerError("Unhandled tag type '%s'" % kind)
253 def _ExtendTags(opts, args):
254 """Extend the args if a source file has been given.
256 This function will extend the tags with the contents of the file
257 passed in the 'tags_source' attribute of the opts parameter. A file
258 named '-' will be replaced by stdin.
261 fname = opts.tags_source
267 new_fh = open(fname, "r")
270 # we don't use the nice 'new_data = [line.strip() for line in fh]'
271 # because of python bug 1633941
273 line = new_fh.readline()
276 new_data.append(line.strip())
279 args.extend(new_data)
282 def ListTags(opts, args):
283 """List the tags on a given object.
285 This is a generic implementation that knows how to deal with all
286 three cases of tag objects (cluster, node, instance). The opts
287 argument is expected to contain a tag_type field denoting what
288 object type we work on.
291 kind, name = _ExtractTagsObject(opts, args)
292 op = opcodes.OpGetTags(kind=kind, name=name)
293 result = SubmitOpCode(op)
294 result = list(result)
300 def AddTags(opts, args):
301 """Add tags on a given object.
303 This is a generic implementation that knows how to deal with all
304 three cases of tag objects (cluster, node, instance). The opts
305 argument is expected to contain a tag_type field denoting what
306 object type we work on.
309 kind, name = _ExtractTagsObject(opts, args)
310 _ExtendTags(opts, args)
312 raise errors.OpPrereqError("No tags to be added")
313 op = opcodes.OpAddTags(kind=kind, name=name, tags=args)
317 def RemoveTags(opts, args):
318 """Remove tags from a given object.
320 This is a generic implementation that knows how to deal with all
321 three cases of tag objects (cluster, node, instance). The opts
322 argument is expected to contain a tag_type field denoting what
323 object type we work on.
326 kind, name = _ExtractTagsObject(opts, args)
327 _ExtendTags(opts, args)
329 raise errors.OpPrereqError("No tags to be removed")
330 op = opcodes.OpDelTags(kind=kind, name=name, tags=args)
334 def check_unit(option, opt, value):
335 """OptParsers custom converter for units.
339 return utils.ParseUnit(value)
340 except errors.UnitParseError, err:
341 raise OptionValueError("option %s: %s" % (opt, err))
344 def _SplitKeyVal(opt, data):
345 """Convert a KeyVal string into a dict.
347 This function will convert a key=val[,...] string into a dict. Empty
348 values will be converted specially: keys which have the prefix 'no_'
349 will have the value=False and the prefix stripped, the others will
353 @param opt: a string holding the option name for which we process the
354 data, used in building error messages
356 @param data: a string of the format key=val,key=val,...
358 @return: {key=val, key=val}
359 @raises errors.ParameterError: if there are duplicate keys
364 for elem in data.split(","):
366 key, val = elem.split("=", 1)
368 if elem.startswith(NO_PREFIX):
369 key, val = elem[len(NO_PREFIX):], False
370 elif elem.startswith(UN_PREFIX):
371 key, val = elem[len(UN_PREFIX):], None
373 key, val = elem, True
375 raise errors.ParameterError("Duplicate key '%s' in option %s" %
381 def check_ident_key_val(option, opt, value):
382 """Custom parser for ident:key=val,key=val options.
384 This will store the parsed values as a tuple (ident, {key: val}). As such,
385 multiple uses of this option via action=append is possible.
389 ident, rest = value, ''
391 ident, rest = value.split(":", 1)
393 if ident.startswith(NO_PREFIX):
395 msg = "Cannot pass options when removing parameter groups: %s" % value
396 raise errors.ParameterError(msg)
397 retval = (ident[len(NO_PREFIX):], False)
398 elif ident.startswith(UN_PREFIX):
400 msg = "Cannot pass options when removing parameter groups: %s" % value
401 raise errors.ParameterError(msg)
402 retval = (ident[len(UN_PREFIX):], None)
404 kv_dict = _SplitKeyVal(opt, rest)
405 retval = (ident, kv_dict)
409 def check_key_val(option, opt, value):
410 """Custom parser class for key=val,key=val options.
412 This will store the parsed values as a dict {key: val}.
415 return _SplitKeyVal(opt, value)
418 # completion_suggestion is normally a list. Using numeric values not evaluating
419 # to False for dynamic completion.
420 (OPT_COMPL_MANY_NODES,
422 OPT_COMPL_ONE_INSTANCE,
424 OPT_COMPL_ONE_IALLOCATOR,
425 OPT_COMPL_INST_ADD_NODES) = range(100, 106)
427 OPT_COMPL_ALL = frozenset([
428 OPT_COMPL_MANY_NODES,
430 OPT_COMPL_ONE_INSTANCE,
432 OPT_COMPL_ONE_IALLOCATOR,
433 OPT_COMPL_INST_ADD_NODES,
437 class CliOption(Option):
438 """Custom option class for optparse.
441 ATTRS = Option.ATTRS + [
442 "completion_suggest",
444 TYPES = Option.TYPES + (
449 TYPE_CHECKER = Option.TYPE_CHECKER.copy()
450 TYPE_CHECKER["identkeyval"] = check_ident_key_val
451 TYPE_CHECKER["keyval"] = check_key_val
452 TYPE_CHECKER["unit"] = check_unit
455 # optparse.py sets make_option, so we do it for our own option class, too
456 cli_option = CliOption
459 _YESNO = ("yes", "no")
462 DEBUG_OPT = cli_option("-d", "--debug", default=False,
464 help="Turn debugging on")
466 NOHDR_OPT = cli_option("--no-headers", default=False,
467 action="store_true", dest="no_headers",
468 help="Don't display column headers")
470 SEP_OPT = cli_option("--separator", default=None,
471 action="store", dest="separator",
472 help=("Separator between output fields"
473 " (defaults to one space)"))
475 USEUNITS_OPT = cli_option("--units", default=None,
476 dest="units", choices=('h', 'm', 'g', 't'),
477 help="Specify units for output (one of hmgt)")
479 FIELDS_OPT = cli_option("-o", "--output", dest="output", action="store",
480 type="string", metavar="FIELDS",
481 help="Comma separated list of output fields")
483 FORCE_OPT = cli_option("-f", "--force", dest="force", action="store_true",
484 default=False, help="Force the operation")
486 CONFIRM_OPT = cli_option("--yes", dest="confirm", action="store_true",
487 default=False, help="Do not require confirmation")
489 TAG_SRC_OPT = cli_option("--from", dest="tags_source",
490 default=None, help="File with tag names")
492 SUBMIT_OPT = cli_option("--submit", dest="submit_only",
493 default=False, action="store_true",
494 help=("Submit the job and return the job ID, but"
495 " don't wait for the job to finish"))
497 SYNC_OPT = cli_option("--sync", dest="do_locking",
498 default=False, action="store_true",
499 help=("Grab locks while doing the queries"
500 " in order to ensure more consistent results"))
502 _DRY_RUN_OPT = cli_option("--dry-run", default=False,
504 help=("Do not execute the operation, just run the"
505 " check steps and verify it it could be"
508 VERBOSE_OPT = cli_option("-v", "--verbose", default=False,
510 help="Increase the verbosity of the operation")
512 DEBUG_SIMERR_OPT = cli_option("--debug-simulate-errors", default=False,
513 action="store_true", dest="simulate_errors",
514 help="Debugging option that makes the operation"
515 " treat most runtime checks as failed")
517 NWSYNC_OPT = cli_option("--no-wait-for-sync", dest="wait_for_sync",
518 default=True, action="store_false",
519 help="Don't wait for sync (DANGEROUS!)")
521 DISK_TEMPLATE_OPT = cli_option("-t", "--disk-template", dest="disk_template",
522 help="Custom disk setup (diskless, file,"
524 default=None, metavar="TEMPL",
525 choices=list(constants.DISK_TEMPLATES))
527 NONICS_OPT = cli_option("--no-nics", default=False, action="store_true",
528 help="Do not create any network cards for"
531 FILESTORE_DIR_OPT = cli_option("--file-storage-dir", dest="file_storage_dir",
532 help="Relative path under default cluster-wide"
533 " file storage dir to store file-based disks",
534 default=None, metavar="<DIR>")
536 FILESTORE_DRIVER_OPT = cli_option("--file-driver", dest="file_driver",
537 help="Driver to use for image files",
538 default="loop", metavar="<DRIVER>",
539 choices=list(constants.FILE_DRIVER))
541 IALLOCATOR_OPT = cli_option("-I", "--iallocator", metavar="<NAME>",
542 help="Select nodes for the instance automatically"
543 " using the <NAME> iallocator plugin",
544 default=None, type="string",
545 completion_suggest=OPT_COMPL_ONE_IALLOCATOR)
547 OS_OPT = cli_option("-o", "--os-type", dest="os", help="What OS to run",
549 completion_suggest=OPT_COMPL_ONE_OS)
551 BACKEND_OPT = cli_option("-B", "--backend-parameters", dest="beparams",
552 type="keyval", default={},
553 help="Backend parameters")
555 HVOPTS_OPT = cli_option("-H", "--hypervisor-parameters", type="keyval",
556 default={}, dest="hvparams",
557 help="Hypervisor parameters")
559 HYPERVISOR_OPT = cli_option("-H", "--hypervisor-parameters", dest="hypervisor",
560 help="Hypervisor and hypervisor options, in the"
561 " format hypervisor:option=value,option=value,...",
562 default=None, type="identkeyval")
564 HVLIST_OPT = cli_option("-H", "--hypervisor-parameters", dest="hvparams",
565 help="Hypervisor and hypervisor options, in the"
566 " format hypervisor:option=value,option=value,...",
567 default=[], action="append", type="identkeyval")
569 NOIPCHECK_OPT = cli_option("--no-ip-check", dest="ip_check", default=True,
570 action="store_false",
571 help="Don't check that the instance's IP"
574 NET_OPT = cli_option("--net",
575 help="NIC parameters", default=[],
576 dest="nics", action="append", type="identkeyval")
578 DISK_OPT = cli_option("--disk", help="Disk parameters", default=[],
579 dest="disks", action="append", type="identkeyval")
581 DISKIDX_OPT = cli_option("--disks", dest="disks", default=None,
582 help="Comma-separated list of disks"
583 " indices to act on (e.g. 0,2) (optional,"
584 " defaults to all disks)")
586 OS_SIZE_OPT = cli_option("-s", "--os-size", dest="sd_size",
587 help="Enforces a single-disk configuration using the"
588 " given disk size, in MiB unless a suffix is used",
589 default=None, type="unit", metavar="<size>")
591 IGNORE_CONSIST_OPT = cli_option("--ignore-consistency",
592 dest="ignore_consistency",
593 action="store_true", default=False,
594 help="Ignore the consistency of the disks on"
597 NONLIVE_OPT = cli_option("--non-live", dest="live",
598 default=True, action="store_false",
599 help="Do a non-live migration (this usually means"
600 " freeze the instance, save the state, transfer and"
601 " only then resume running on the secondary node)")
603 NODE_PLACEMENT_OPT = cli_option("-n", "--node", dest="node",
604 help="Target node and optional secondary node",
605 metavar="<pnode>[:<snode>]",
606 completion_suggest=OPT_COMPL_INST_ADD_NODES)
608 NODE_LIST_OPT = cli_option("-n", "--node", dest="nodes", default=[],
609 action="append", metavar="<node>",
610 help="Use only this node (can be used multiple"
611 " times, if not given defaults to all nodes)",
612 completion_suggest=OPT_COMPL_ONE_NODE)
614 SINGLE_NODE_OPT = cli_option("-n", "--node", dest="node", help="Target node",
616 completion_suggest=OPT_COMPL_ONE_NODE)
618 NOSTART_OPT = cli_option("--no-start", dest="start", default=True,
619 action="store_false",
620 help="Don't start the instance after creation")
622 SHOWCMD_OPT = cli_option("--show-cmd", dest="show_command",
623 action="store_true", default=False,
624 help="Show command instead of executing it")
626 CLEANUP_OPT = cli_option("--cleanup", dest="cleanup",
627 default=False, action="store_true",
628 help="Instead of performing the migration, try to"
629 " recover from a failed cleanup. This is safe"
630 " to run even if the instance is healthy, but it"
631 " will create extra replication traffic and "
632 " disrupt briefly the replication (like during the"
635 STATIC_OPT = cli_option("-s", "--static", dest="static",
636 action="store_true", default=False,
637 help="Only show configuration data, not runtime data")
639 ALL_OPT = cli_option("--all", dest="show_all",
640 default=False, action="store_true",
641 help="Show info on all instances on the cluster."
642 " This can take a long time to run, use wisely")
644 SELECT_OS_OPT = cli_option("--select-os", dest="select_os",
645 action="store_true", default=False,
646 help="Interactive OS reinstall, lists available"
647 " OS templates for selection")
649 IGNORE_FAILURES_OPT = cli_option("--ignore-failures", dest="ignore_failures",
650 action="store_true", default=False,
651 help="Remove the instance from the cluster"
652 " configuration even if there are failures"
653 " during the removal process")
655 NEW_SECONDARY_OPT = cli_option("-n", "--new-secondary", dest="dst_node",
656 help="Specifies the new secondary node",
657 metavar="NODE", default=None,
658 completion_suggest=OPT_COMPL_ONE_NODE)
660 ON_PRIMARY_OPT = cli_option("-p", "--on-primary", dest="on_primary",
661 default=False, action="store_true",
662 help="Replace the disk(s) on the primary"
663 " node (only for the drbd template)")
665 ON_SECONDARY_OPT = cli_option("-s", "--on-secondary", dest="on_secondary",
666 default=False, action="store_true",
667 help="Replace the disk(s) on the secondary"
668 " node (only for the drbd template)")
670 AUTO_REPLACE_OPT = cli_option("-a", "--auto", dest="auto",
671 default=False, action="store_true",
672 help="Automatically replace faulty disks"
673 " (only for the drbd template)")
675 IGNORE_SIZE_OPT = cli_option("--ignore-size", dest="ignore_size",
676 default=False, action="store_true",
677 help="Ignore current recorded size"
678 " (useful for forcing activation when"
679 " the recorded size is wrong)")
681 SRC_NODE_OPT = cli_option("--src-node", dest="src_node", help="Source node",
683 completion_suggest=OPT_COMPL_ONE_NODE)
685 SRC_DIR_OPT = cli_option("--src-dir", dest="src_dir", help="Source directory",
688 SECONDARY_IP_OPT = cli_option("-s", "--secondary-ip", dest="secondary_ip",
689 help="Specify the secondary ip for the node",
690 metavar="ADDRESS", default=None)
692 READD_OPT = cli_option("--readd", dest="readd",
693 default=False, action="store_true",
694 help="Readd old node after replacing it")
696 NOSSH_KEYCHECK_OPT = cli_option("--no-ssh-key-check", dest="ssh_key_check",
697 default=True, action="store_false",
698 help="Disable SSH key fingerprint checking")
701 MC_OPT = cli_option("-C", "--master-candidate", dest="master_candidate",
702 choices=_YESNO, default=None, metavar=_YORNO,
703 help="Set the master_candidate flag on the node")
705 OFFLINE_OPT = cli_option("-O", "--offline", dest="offline", metavar=_YORNO,
706 choices=_YESNO, default=None,
707 help="Set the offline flag on the node")
709 DRAINED_OPT = cli_option("-D", "--drained", dest="drained", metavar=_YORNO,
710 choices=_YESNO, default=None,
711 help="Set the drained flag on the node")
714 def _ParseArgs(argv, commands, aliases):
715 """Parser for the command line arguments.
717 This function parses the arguments and returns the function which
718 must be executed together with its (modified) arguments.
720 @param argv: the command line
721 @param commands: dictionary with special contents, see the design
722 doc for cmdline handling
723 @param aliases: dictionary with command aliases {'alias': 'target, ...}
729 binary = argv[0].split("/")[-1]
731 if len(argv) > 1 and argv[1] == "--version":
732 ToStdout("%s (ganeti) %s", binary, constants.RELEASE_VERSION)
733 # Quit right away. That way we don't have to care about this special
734 # argument. optparse.py does it the same.
737 if len(argv) < 2 or not (argv[1] in commands or
739 # let's do a nice thing
740 sortedcmds = commands.keys()
743 ToStdout("Usage: %s {command} [options...] [argument...]", binary)
744 ToStdout("%s <command> --help to see details, or man %s", binary, binary)
747 # compute the max line length for cmd + usage
748 mlen = max([len(" %s" % cmd) for cmd in commands])
749 mlen = min(60, mlen) # should not get here...
751 # and format a nice command list
752 ToStdout("Commands:")
753 for cmd in sortedcmds:
754 cmdstr = " %s" % (cmd,)
755 help_text = commands[cmd][4]
756 help_lines = textwrap.wrap(help_text, 79 - 3 - mlen)
757 ToStdout("%-*s - %s", mlen, cmdstr, help_lines.pop(0))
758 for line in help_lines:
759 ToStdout("%-*s %s", mlen, "", line)
763 return None, None, None
765 # get command, unalias it, and look it up in commands
769 raise errors.ProgrammerError("Alias '%s' overrides an existing"
772 if aliases[cmd] not in commands:
773 raise errors.ProgrammerError("Alias '%s' maps to non-existing"
774 " command '%s'" % (cmd, aliases[cmd]))
778 func, args_def, parser_opts, usage, description = commands[cmd]
779 parser = OptionParser(option_list=parser_opts + [_DRY_RUN_OPT],
780 description=description,
781 formatter=TitledHelpFormatter(),
782 usage="%%prog %s %s" % (cmd, usage))
783 parser.disable_interspersed_args()
784 options, args = parser.parse_args()
786 if not _CheckArguments(cmd, args_def, args):
787 return None, None, None
789 return func, options, args
792 def _CheckArguments(cmd, args_def, args):
793 """Verifies the arguments using the argument definition.
797 1. Abort with error if values specified by user but none expected.
799 1. For each argument in definition
801 1. Keep running count of minimum number of values (min_count)
802 1. Keep running count of maximum number of values (max_count)
803 1. If it has an unlimited number of values
805 1. Abort with error if it's not the last argument in the definition
807 1. If last argument has limited number of values
809 1. Abort with error if number of values doesn't match or is too large
811 1. Abort with error if user didn't pass enough values (min_count)
814 if args and not args_def:
815 ToStderr("Error: Command %s expects no arguments", cmd)
822 last_idx = len(args_def) - 1
824 for idx, arg in enumerate(args_def):
825 if min_count is None:
827 elif arg.min is not None:
830 if max_count is None:
832 elif arg.max is not None:
836 check_max = (arg.max is not None)
838 elif arg.max is None:
839 raise errors.ProgrammerError("Only the last argument can have max=None")
842 # Command with exact number of arguments
843 if (min_count is not None and max_count is not None and
844 min_count == max_count and len(args) != min_count):
845 ToStderr("Error: Command %s expects %d argument(s)", cmd, min_count)
848 # Command with limited number of arguments
849 if max_count is not None and len(args) > max_count:
850 ToStderr("Error: Command %s expects only %d argument(s)",
854 # Command with some required arguments
855 if min_count is not None and len(args) < min_count:
856 ToStderr("Error: Command %s expects at least %d argument(s)",
863 def SplitNodeOption(value):
864 """Splits the value of a --node option.
867 if value and ':' in value:
868 return value.split(':', 1)
874 def wrapper(*args, **kwargs):
877 return fn(*args, **kwargs)
883 def AskUser(text, choices=None):
884 """Ask the user a question.
886 @param text: the question to ask
888 @param choices: list with elements tuples (input_char, return_value,
889 description); if not given, it will default to: [('y', True,
890 'Perform the operation'), ('n', False, 'Do no do the operation')];
891 note that the '?' char is reserved for help
893 @return: one of the return values from the choices list; if input is
894 not possible (i.e. not running with a tty, we return the last
899 choices = [('y', True, 'Perform the operation'),
900 ('n', False, 'Do not perform the operation')]
901 if not choices or not isinstance(choices, list):
902 raise errors.ProgrammerError("Invalid choices argument to AskUser")
903 for entry in choices:
904 if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
905 raise errors.ProgrammerError("Invalid choices element to AskUser")
907 answer = choices[-1][1]
909 for line in text.splitlines():
910 new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
911 text = "\n".join(new_text)
913 f = file("/dev/tty", "a+")
917 chars = [entry[0] for entry in choices]
918 chars[-1] = "[%s]" % chars[-1]
920 maps = dict([(entry[0], entry[1]) for entry in choices])
924 f.write("/".join(chars))
926 line = f.readline(2).strip().lower()
931 for entry in choices:
932 f.write(" %s - %s\n" % (entry[0], entry[2]))
940 class JobSubmittedException(Exception):
941 """Job was submitted, client should exit.
943 This exception has one argument, the ID of the job that was
944 submitted. The handler should print this ID.
946 This is not an error, just a structured way to exit from clients.
951 def SendJob(ops, cl=None):
952 """Function to submit an opcode without waiting for the results.
955 @param ops: list of opcodes
956 @type cl: luxi.Client
957 @param cl: the luxi client to use for communicating with the master;
958 if None, a new client will be created
964 job_id = cl.SubmitJob(ops)
969 def PollJob(job_id, cl=None, feedback_fn=None):
970 """Function to poll for the result of a job.
972 @type job_id: job identified
973 @param job_id: the job to poll for results
974 @type cl: luxi.Client
975 @param cl: the luxi client to use for communicating with the master;
976 if None, a new client will be created
983 prev_logmsg_serial = None
986 result = cl.WaitForJobChange(job_id, ["status"], prev_job_info,
989 # job not found, go away!
990 raise errors.JobLost("Job with id %s lost" % job_id)
992 # Split result, a tuple of (field values, log entries)
993 (job_info, log_entries) = result
994 (status, ) = job_info
997 for log_entry in log_entries:
998 (serial, timestamp, _, message) = log_entry
999 if callable(feedback_fn):
1000 feedback_fn(log_entry[1:])
1002 encoded = utils.SafeEncode(message)
1003 ToStdout("%s %s", time.ctime(utils.MergeTime(timestamp)), encoded)
1004 prev_logmsg_serial = max(prev_logmsg_serial, serial)
1006 # TODO: Handle canceled and archived jobs
1007 elif status in (constants.JOB_STATUS_SUCCESS,
1008 constants.JOB_STATUS_ERROR,
1009 constants.JOB_STATUS_CANCELING,
1010 constants.JOB_STATUS_CANCELED):
1013 prev_job_info = job_info
1015 jobs = cl.QueryJobs([job_id], ["status", "opstatus", "opresult"])
1017 raise errors.JobLost("Job with id %s lost" % job_id)
1019 status, opstatus, result = jobs[0]
1020 if status == constants.JOB_STATUS_SUCCESS:
1022 elif status in (constants.JOB_STATUS_CANCELING,
1023 constants.JOB_STATUS_CANCELED):
1024 raise errors.OpExecError("Job was canceled")
1027 for idx, (status, msg) in enumerate(zip(opstatus, result)):
1028 if status == constants.OP_STATUS_SUCCESS:
1030 elif status == constants.OP_STATUS_ERROR:
1031 errors.MaybeRaise(msg)
1033 raise errors.OpExecError("partial failure (opcode %d): %s" %
1036 raise errors.OpExecError(str(msg))
1037 # default failure mode
1038 raise errors.OpExecError(result)
1041 def SubmitOpCode(op, cl=None, feedback_fn=None):
1042 """Legacy function to submit an opcode.
1044 This is just a simple wrapper over the construction of the processor
1045 instance. It should be extended to better handle feedback and
1046 interaction functions.
1052 job_id = SendJob([op], cl)
1054 op_results = PollJob(job_id, cl=cl, feedback_fn=feedback_fn)
1056 return op_results[0]
1059 def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
1060 """Wrapper around SubmitOpCode or SendJob.
1062 This function will decide, based on the 'opts' parameter, whether to
1063 submit and wait for the result of the opcode (and return it), or
1064 whether to just send the job and print its identifier. It is used in
1065 order to simplify the implementation of the '--submit' option.
1067 It will also add the dry-run parameter from the options passed, if true.
1070 if opts and opts.dry_run:
1071 op.dry_run = opts.dry_run
1072 if opts and opts.submit_only:
1073 job_id = SendJob([op], cl=cl)
1074 raise JobSubmittedException(job_id)
1076 return SubmitOpCode(op, cl=cl, feedback_fn=feedback_fn)
1080 # TODO: Cache object?
1082 client = luxi.Client()
1083 except luxi.NoMasterError:
1084 master, myself = ssconf.GetMasterAndMyself()
1085 if master != myself:
1086 raise errors.OpPrereqError("This is not the master node, please connect"
1087 " to node '%s' and rerun the command" %
1094 def FormatError(err):
1095 """Return a formatted error message for a given error.
1097 This function takes an exception instance and returns a tuple
1098 consisting of two values: first, the recommended exit code, and
1099 second, a string describing the error message (not
1100 newline-terminated).
1106 if isinstance(err, errors.ConfigurationError):
1107 txt = "Corrupt configuration file: %s" % msg
1109 obuf.write(txt + "\n")
1110 obuf.write("Aborting.")
1112 elif isinstance(err, errors.HooksAbort):
1113 obuf.write("Failure: hooks execution failed:\n")
1114 for node, script, out in err.args[0]:
1116 obuf.write(" node: %s, script: %s, output: %s\n" %
1117 (node, script, out))
1119 obuf.write(" node: %s, script: %s (no output)\n" %
1121 elif isinstance(err, errors.HooksFailure):
1122 obuf.write("Failure: hooks general failure: %s" % msg)
1123 elif isinstance(err, errors.ResolverError):
1124 this_host = utils.HostInfo.SysName()
1125 if err.args[0] == this_host:
1126 msg = "Failure: can't resolve my own hostname ('%s')"
1128 msg = "Failure: can't resolve hostname '%s'"
1129 obuf.write(msg % err.args[0])
1130 elif isinstance(err, errors.OpPrereqError):
1131 obuf.write("Failure: prerequisites not met for this"
1132 " operation:\n%s" % msg)
1133 elif isinstance(err, errors.OpExecError):
1134 obuf.write("Failure: command execution error:\n%s" % msg)
1135 elif isinstance(err, errors.TagError):
1136 obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
1137 elif isinstance(err, errors.JobQueueDrainError):
1138 obuf.write("Failure: the job queue is marked for drain and doesn't"
1139 " accept new requests\n")
1140 elif isinstance(err, errors.JobQueueFull):
1141 obuf.write("Failure: the job queue is full and doesn't accept new"
1142 " job submissions until old jobs are archived\n")
1143 elif isinstance(err, errors.TypeEnforcementError):
1144 obuf.write("Parameter Error: %s" % msg)
1145 elif isinstance(err, errors.ParameterError):
1146 obuf.write("Failure: unknown/wrong parameter name '%s'" % msg)
1147 elif isinstance(err, errors.GenericError):
1148 obuf.write("Unhandled Ganeti error: %s" % msg)
1149 elif isinstance(err, luxi.NoMasterError):
1150 obuf.write("Cannot communicate with the master daemon.\nIs it running"
1151 " and listening for connections?")
1152 elif isinstance(err, luxi.TimeoutError):
1153 obuf.write("Timeout while talking to the master daemon. Error:\n"
1155 elif isinstance(err, luxi.ProtocolError):
1156 obuf.write("Unhandled protocol error while talking to the master daemon:\n"
1158 elif isinstance(err, JobSubmittedException):
1159 obuf.write("JobID: %s\n" % err.args[0])
1162 obuf.write("Unhandled exception: %s" % msg)
1163 return retcode, obuf.getvalue().rstrip('\n')
1166 def GenericMain(commands, override=None, aliases=None):
1167 """Generic main function for all the gnt-* commands.
1170 - commands: a dictionary with a special structure, see the design doc
1171 for command line handling.
1172 - override: if not None, we expect a dictionary with keys that will
1173 override command line options; this can be used to pass
1174 options from the scripts to generic functions
1175 - aliases: dictionary with command aliases {'alias': 'target, ...}
1178 # save the program name and the entire command line for later logging
1180 binary = os.path.basename(sys.argv[0]) or sys.argv[0]
1181 if len(sys.argv) >= 2:
1182 binary += " " + sys.argv[1]
1183 old_cmdline = " ".join(sys.argv[2:])
1187 binary = "<unknown program>"
1194 func, options, args = _ParseArgs(sys.argv, commands, aliases)
1195 except errors.ParameterError, err:
1196 result, err_msg = FormatError(err)
1200 if func is None: # parse error
1203 if override is not None:
1204 for key, val in override.iteritems():
1205 setattr(options, key, val)
1207 utils.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
1208 stderr_logging=True, program=binary)
1211 logging.info("run with arguments '%s'", old_cmdline)
1213 logging.info("run with no arguments")
1216 result = func(options, args)
1217 except (errors.GenericError, luxi.ProtocolError,
1218 JobSubmittedException), err:
1219 result, err_msg = FormatError(err)
1220 logging.exception("Error during command processing")
1226 def GenerateTable(headers, fields, separator, data,
1227 numfields=None, unitfields=None,
1229 """Prints a table with headers and different fields.
1232 @param headers: dictionary mapping field names to headers for
1235 @param fields: the field names corresponding to each row in
1237 @param separator: the separator to be used; if this is None,
1238 the default 'smart' algorithm is used which computes optimal
1239 field width, otherwise just the separator is used between
1242 @param data: a list of lists, each sublist being one row to be output
1243 @type numfields: list
1244 @param numfields: a list with the fields that hold numeric
1245 values and thus should be right-aligned
1246 @type unitfields: list
1247 @param unitfields: a list with the fields that hold numeric
1248 values that should be formatted with the units field
1249 @type units: string or None
1250 @param units: the units we should use for formatting, or None for
1251 automatic choice (human-readable for non-separator usage, otherwise
1252 megabytes); this is a one-letter string
1261 if numfields is None:
1263 if unitfields is None:
1266 numfields = utils.FieldSet(*numfields)
1267 unitfields = utils.FieldSet(*unitfields)
1270 for field in fields:
1271 if headers and field not in headers:
1272 # TODO: handle better unknown fields (either revert to old
1273 # style of raising exception, or deal more intelligently with
1275 headers[field] = field
1276 if separator is not None:
1277 format_fields.append("%s")
1278 elif numfields.Matches(field):
1279 format_fields.append("%*s")
1281 format_fields.append("%-*s")
1283 if separator is None:
1284 mlens = [0 for name in fields]
1285 format = ' '.join(format_fields)
1287 format = separator.replace("%", "%%").join(format_fields)
1292 for idx, val in enumerate(row):
1293 if unitfields.Matches(fields[idx]):
1299 val = row[idx] = utils.FormatUnit(val, units)
1300 val = row[idx] = str(val)
1301 if separator is None:
1302 mlens[idx] = max(mlens[idx], len(val))
1307 for idx, name in enumerate(fields):
1309 if separator is None:
1310 mlens[idx] = max(mlens[idx], len(hdr))
1311 args.append(mlens[idx])
1313 result.append(format % tuple(args))
1318 line = ['-' for _ in fields]
1319 for idx in xrange(len(fields)):
1320 if separator is None:
1321 args.append(mlens[idx])
1322 args.append(line[idx])
1323 result.append(format % tuple(args))
1328 def FormatTimestamp(ts):
1329 """Formats a given timestamp.
1332 @param ts: a timeval-type timestamp, a tuple of seconds and microseconds
1335 @return: a string with the formatted timestamp
1338 if not isinstance (ts, (tuple, list)) or len(ts) != 2:
1341 return time.strftime("%F %T", time.localtime(sec)) + ".%06d" % usec
1344 def ParseTimespec(value):
1345 """Parse a time specification.
1347 The following suffixed will be recognized:
1355 Without any suffix, the value will be taken to be in seconds.
1360 raise errors.OpPrereqError("Empty time specification passed")
1368 if value[-1] not in suffix_map:
1372 raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1374 multiplier = suffix_map[value[-1]]
1376 if not value: # no data left after stripping the suffix
1377 raise errors.OpPrereqError("Invalid time specification (only"
1380 value = int(value) * multiplier
1382 raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1386 def GetOnlineNodes(nodes, cl=None, nowarn=False):
1387 """Returns the names of online nodes.
1389 This function will also log a warning on stderr with the names of
1392 @param nodes: if not empty, use only this subset of nodes (minus the
1394 @param cl: if not None, luxi client to use
1395 @type nowarn: boolean
1396 @param nowarn: by default, this function will output a note with the
1397 offline nodes that are skipped; if this parameter is True the
1398 note is not displayed
1404 result = cl.QueryNodes(names=nodes, fields=["name", "offline"],
1406 offline = [row[0] for row in result if row[1]]
1407 if offline and not nowarn:
1408 ToStderr("Note: skipping offline node(s): %s" % ", ".join(offline))
1409 return [row[0] for row in result if not row[1]]
1412 def _ToStream(stream, txt, *args):
1413 """Write a message to a stream, bypassing the logging system
1415 @type stream: file object
1416 @param stream: the file to which we should write
1418 @param txt: the message
1423 stream.write(txt % args)
1430 def ToStdout(txt, *args):
1431 """Write a message to stdout only, bypassing the logging system
1433 This is just a wrapper over _ToStream.
1436 @param txt: the message
1439 _ToStream(sys.stdout, txt, *args)
1442 def ToStderr(txt, *args):
1443 """Write a message to stderr only, bypassing the logging system
1445 This is just a wrapper over _ToStream.
1448 @param txt: the message
1451 _ToStream(sys.stderr, txt, *args)
1454 class JobExecutor(object):
1455 """Class which manages the submission and execution of multiple jobs.
1457 Note that instances of this class should not be reused between
1461 def __init__(self, cl=None, verbose=True):
1466 self.verbose = verbose
1469 def QueueJob(self, name, *ops):
1470 """Record a job for later submit.
1473 @param name: a description of the job, will be used in WaitJobSet
1475 self.queue.append((name, ops))
1477 def SubmitPending(self):
1478 """Submit all pending jobs.
1481 results = self.cl.SubmitManyJobs([row[1] for row in self.queue])
1482 for ((status, data), (name, _)) in zip(results, self.queue):
1483 self.jobs.append((status, data, name))
1485 def GetResults(self):
1486 """Wait for and return the results of all jobs.
1489 @return: list of tuples (success, job results), in the same order
1490 as the submitted jobs; if a job has failed, instead of the result
1491 there will be the error message
1495 self.SubmitPending()
1498 ok_jobs = [row[1] for row in self.jobs if row[0]]
1500 ToStdout("Submitted jobs %s", ", ".join(ok_jobs))
1501 for submit_status, jid, name in self.jobs:
1502 if not submit_status:
1503 ToStderr("Failed to submit job for %s: %s", name, jid)
1504 results.append((False, jid))
1507 ToStdout("Waiting for job %s for %s...", jid, name)
1509 job_result = PollJob(jid, cl=self.cl)
1511 except (errors.GenericError, luxi.ProtocolError), err:
1512 _, job_result = FormatError(err)
1514 # the error message will always be shown, verbose or not
1515 ToStderr("Job %s for %s has failed: %s", jid, name, job_result)
1517 results.append((success, job_result))
1520 def WaitOrShow(self, wait):
1521 """Wait for job results or only print the job IDs.
1524 @param wait: whether to wait or not
1528 return self.GetResults()
1531 self.SubmitPending()
1532 for status, result, name in self.jobs:
1534 ToStdout("%s: %s", result, name)
1536 ToStderr("Failure for %s: %s", name, result)