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
64 "FILESTORE_DRIVER_OPT",
71 "IGNORE_FAILURES_OPT",
72 "IGNORE_SECONDARIES_OPT",
86 "NOMODIFY_ETCHOSTS_OPT",
117 # Generic functions for CLI programs
122 "JobSubmittedException",
127 # Formatting functions
128 "ToStderr", "ToStdout",
137 # command line options support infrastructure
138 "ARGS_MANY_INSTANCES",
152 "OPT_COMPL_INST_ADD_NODES",
153 "OPT_COMPL_MANY_NODES",
154 "OPT_COMPL_ONE_IALLOCATOR",
155 "OPT_COMPL_ONE_INSTANCE",
156 "OPT_COMPL_ONE_NODE",
167 def __init__(self, min=0, max=None):
172 return ("<%s min=%s max=%s>" %
173 (self.__class__.__name__, self.min, self.max))
176 class ArgSuggest(_Argument):
177 """Suggesting argument.
179 Value can be any of the ones passed to the constructor.
182 def __init__(self, min=0, max=None, choices=None):
183 _Argument.__init__(self, min=min, max=max)
184 self.choices = choices
187 return ("<%s min=%s max=%s choices=%r>" %
188 (self.__class__.__name__, self.min, self.max, self.choices))
191 class ArgChoice(ArgSuggest):
194 Value can be any of the ones passed to the constructor. Like L{ArgSuggest},
195 but value must be one of the choices.
200 class ArgUnknown(_Argument):
201 """Unknown argument to program (e.g. determined at runtime).
206 class ArgInstance(_Argument):
207 """Instances argument.
212 class ArgNode(_Argument):
217 class ArgJobId(_Argument):
223 class ArgFile(_Argument):
224 """File path argument.
229 class ArgCommand(_Argument):
235 class ArgHost(_Argument):
242 ARGS_MANY_INSTANCES = [ArgInstance()]
243 ARGS_MANY_NODES = [ArgNode()]
244 ARGS_ONE_INSTANCE = [ArgInstance(min=1, max=1)]
245 ARGS_ONE_NODE = [ArgNode(min=1, max=1)]
249 def _ExtractTagsObject(opts, args):
250 """Extract the tag type object.
252 Note that this function will modify its args parameter.
255 if not hasattr(opts, "tag_type"):
256 raise errors.ProgrammerError("tag_type not passed to _ExtractTagsObject")
258 if kind == constants.TAG_CLUSTER:
260 elif kind == constants.TAG_NODE or kind == constants.TAG_INSTANCE:
262 raise errors.OpPrereqError("no arguments passed to the command")
266 raise errors.ProgrammerError("Unhandled tag type '%s'" % kind)
270 def _ExtendTags(opts, args):
271 """Extend the args if a source file has been given.
273 This function will extend the tags with the contents of the file
274 passed in the 'tags_source' attribute of the opts parameter. A file
275 named '-' will be replaced by stdin.
278 fname = opts.tags_source
284 new_fh = open(fname, "r")
287 # we don't use the nice 'new_data = [line.strip() for line in fh]'
288 # because of python bug 1633941
290 line = new_fh.readline()
293 new_data.append(line.strip())
296 args.extend(new_data)
299 def ListTags(opts, args):
300 """List the tags on a given object.
302 This is a generic implementation that knows how to deal with all
303 three cases of tag objects (cluster, node, instance). The opts
304 argument is expected to contain a tag_type field denoting what
305 object type we work on.
308 kind, name = _ExtractTagsObject(opts, args)
309 op = opcodes.OpGetTags(kind=kind, name=name)
310 result = SubmitOpCode(op)
311 result = list(result)
317 def AddTags(opts, args):
318 """Add tags on 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 added")
330 op = opcodes.OpAddTags(kind=kind, name=name, tags=args)
334 def RemoveTags(opts, args):
335 """Remove tags from a given object.
337 This is a generic implementation that knows how to deal with all
338 three cases of tag objects (cluster, node, instance). The opts
339 argument is expected to contain a tag_type field denoting what
340 object type we work on.
343 kind, name = _ExtractTagsObject(opts, args)
344 _ExtendTags(opts, args)
346 raise errors.OpPrereqError("No tags to be removed")
347 op = opcodes.OpDelTags(kind=kind, name=name, tags=args)
351 def check_unit(option, opt, value):
352 """OptParsers custom converter for units.
356 return utils.ParseUnit(value)
357 except errors.UnitParseError, err:
358 raise OptionValueError("option %s: %s" % (opt, err))
361 def _SplitKeyVal(opt, data):
362 """Convert a KeyVal string into a dict.
364 This function will convert a key=val[,...] string into a dict. Empty
365 values will be converted specially: keys which have the prefix 'no_'
366 will have the value=False and the prefix stripped, the others will
370 @param opt: a string holding the option name for which we process the
371 data, used in building error messages
373 @param data: a string of the format key=val,key=val,...
375 @return: {key=val, key=val}
376 @raises errors.ParameterError: if there are duplicate keys
381 for elem in data.split(","):
383 key, val = elem.split("=", 1)
385 if elem.startswith(NO_PREFIX):
386 key, val = elem[len(NO_PREFIX):], False
387 elif elem.startswith(UN_PREFIX):
388 key, val = elem[len(UN_PREFIX):], None
390 key, val = elem, True
392 raise errors.ParameterError("Duplicate key '%s' in option %s" %
398 def check_ident_key_val(option, opt, value):
399 """Custom parser for ident:key=val,key=val options.
401 This will store the parsed values as a tuple (ident, {key: val}). As such,
402 multiple uses of this option via action=append is possible.
406 ident, rest = value, ''
408 ident, rest = value.split(":", 1)
410 if ident.startswith(NO_PREFIX):
412 msg = "Cannot pass options when removing parameter groups: %s" % value
413 raise errors.ParameterError(msg)
414 retval = (ident[len(NO_PREFIX):], False)
415 elif ident.startswith(UN_PREFIX):
417 msg = "Cannot pass options when removing parameter groups: %s" % value
418 raise errors.ParameterError(msg)
419 retval = (ident[len(UN_PREFIX):], None)
421 kv_dict = _SplitKeyVal(opt, rest)
422 retval = (ident, kv_dict)
426 def check_key_val(option, opt, value):
427 """Custom parser class for key=val,key=val options.
429 This will store the parsed values as a dict {key: val}.
432 return _SplitKeyVal(opt, value)
435 # completion_suggestion is normally a list. Using numeric values not evaluating
436 # to False for dynamic completion.
437 (OPT_COMPL_MANY_NODES,
439 OPT_COMPL_ONE_INSTANCE,
441 OPT_COMPL_ONE_IALLOCATOR,
442 OPT_COMPL_INST_ADD_NODES) = range(100, 106)
444 OPT_COMPL_ALL = frozenset([
445 OPT_COMPL_MANY_NODES,
447 OPT_COMPL_ONE_INSTANCE,
449 OPT_COMPL_ONE_IALLOCATOR,
450 OPT_COMPL_INST_ADD_NODES,
454 class CliOption(Option):
455 """Custom option class for optparse.
458 ATTRS = Option.ATTRS + [
459 "completion_suggest",
461 TYPES = Option.TYPES + (
466 TYPE_CHECKER = Option.TYPE_CHECKER.copy()
467 TYPE_CHECKER["identkeyval"] = check_ident_key_val
468 TYPE_CHECKER["keyval"] = check_key_val
469 TYPE_CHECKER["unit"] = check_unit
472 # optparse.py sets make_option, so we do it for our own option class, too
473 cli_option = CliOption
476 _YESNO = ("yes", "no")
479 DEBUG_OPT = cli_option("-d", "--debug", default=False,
481 help="Turn debugging on")
483 NOHDR_OPT = cli_option("--no-headers", default=False,
484 action="store_true", dest="no_headers",
485 help="Don't display column headers")
487 SEP_OPT = cli_option("--separator", default=None,
488 action="store", dest="separator",
489 help=("Separator between output fields"
490 " (defaults to one space)"))
492 USEUNITS_OPT = cli_option("--units", default=None,
493 dest="units", choices=('h', 'm', 'g', 't'),
494 help="Specify units for output (one of hmgt)")
496 FIELDS_OPT = cli_option("-o", "--output", dest="output", action="store",
497 type="string", metavar="FIELDS",
498 help="Comma separated list of output fields")
500 FORCE_OPT = cli_option("-f", "--force", dest="force", action="store_true",
501 default=False, help="Force the operation")
503 CONFIRM_OPT = cli_option("--yes", dest="confirm", action="store_true",
504 default=False, help="Do not require confirmation")
506 TAG_SRC_OPT = cli_option("--from", dest="tags_source",
507 default=None, help="File with tag names")
509 SUBMIT_OPT = cli_option("--submit", dest="submit_only",
510 default=False, action="store_true",
511 help=("Submit the job and return the job ID, but"
512 " don't wait for the job to finish"))
514 SYNC_OPT = cli_option("--sync", dest="do_locking",
515 default=False, action="store_true",
516 help=("Grab locks while doing the queries"
517 " in order to ensure more consistent results"))
519 _DRY_RUN_OPT = cli_option("--dry-run", default=False,
521 help=("Do not execute the operation, just run the"
522 " check steps and verify it it could be"
525 VERBOSE_OPT = cli_option("-v", "--verbose", default=False,
527 help="Increase the verbosity of the operation")
529 DEBUG_SIMERR_OPT = cli_option("--debug-simulate-errors", default=False,
530 action="store_true", dest="simulate_errors",
531 help="Debugging option that makes the operation"
532 " treat most runtime checks as failed")
534 NWSYNC_OPT = cli_option("--no-wait-for-sync", dest="wait_for_sync",
535 default=True, action="store_false",
536 help="Don't wait for sync (DANGEROUS!)")
538 DISK_TEMPLATE_OPT = cli_option("-t", "--disk-template", dest="disk_template",
539 help="Custom disk setup (diskless, file,"
541 default=None, metavar="TEMPL",
542 choices=list(constants.DISK_TEMPLATES))
544 NONICS_OPT = cli_option("--no-nics", default=False, action="store_true",
545 help="Do not create any network cards for"
548 FILESTORE_DIR_OPT = cli_option("--file-storage-dir", dest="file_storage_dir",
549 help="Relative path under default cluster-wide"
550 " file storage dir to store file-based disks",
551 default=None, metavar="<DIR>")
553 FILESTORE_DRIVER_OPT = cli_option("--file-driver", dest="file_driver",
554 help="Driver to use for image files",
555 default="loop", metavar="<DRIVER>",
556 choices=list(constants.FILE_DRIVER))
558 IALLOCATOR_OPT = cli_option("-I", "--iallocator", metavar="<NAME>",
559 help="Select nodes for the instance automatically"
560 " using the <NAME> iallocator plugin",
561 default=None, type="string",
562 completion_suggest=OPT_COMPL_ONE_IALLOCATOR)
564 OS_OPT = cli_option("-o", "--os-type", dest="os", help="What OS to run",
566 completion_suggest=OPT_COMPL_ONE_OS)
568 BACKEND_OPT = cli_option("-B", "--backend-parameters", dest="beparams",
569 type="keyval", default={},
570 help="Backend parameters")
572 HVOPTS_OPT = cli_option("-H", "--hypervisor-parameters", type="keyval",
573 default={}, dest="hvparams",
574 help="Hypervisor parameters")
576 HYPERVISOR_OPT = cli_option("-H", "--hypervisor-parameters", dest="hypervisor",
577 help="Hypervisor and hypervisor options, in the"
578 " format hypervisor:option=value,option=value,...",
579 default=None, type="identkeyval")
581 HVLIST_OPT = cli_option("-H", "--hypervisor-parameters", dest="hvparams",
582 help="Hypervisor and hypervisor options, in the"
583 " format hypervisor:option=value,option=value,...",
584 default=[], action="append", type="identkeyval")
586 NOIPCHECK_OPT = cli_option("--no-ip-check", dest="ip_check", default=True,
587 action="store_false",
588 help="Don't check that the instance's IP"
591 NET_OPT = cli_option("--net",
592 help="NIC parameters", default=[],
593 dest="nics", action="append", type="identkeyval")
595 DISK_OPT = cli_option("--disk", help="Disk parameters", default=[],
596 dest="disks", action="append", type="identkeyval")
598 DISKIDX_OPT = cli_option("--disks", dest="disks", default=None,
599 help="Comma-separated list of disks"
600 " indices to act on (e.g. 0,2) (optional,"
601 " defaults to all disks)")
603 OS_SIZE_OPT = cli_option("-s", "--os-size", dest="sd_size",
604 help="Enforces a single-disk configuration using the"
605 " given disk size, in MiB unless a suffix is used",
606 default=None, type="unit", metavar="<size>")
608 IGNORE_CONSIST_OPT = cli_option("--ignore-consistency",
609 dest="ignore_consistency",
610 action="store_true", default=False,
611 help="Ignore the consistency of the disks on"
614 NONLIVE_OPT = cli_option("--non-live", dest="live",
615 default=True, action="store_false",
616 help="Do a non-live migration (this usually means"
617 " freeze the instance, save the state, transfer and"
618 " only then resume running on the secondary node)")
620 NODE_PLACEMENT_OPT = cli_option("-n", "--node", dest="node",
621 help="Target node and optional secondary node",
622 metavar="<pnode>[:<snode>]",
623 completion_suggest=OPT_COMPL_INST_ADD_NODES)
625 NODE_LIST_OPT = cli_option("-n", "--node", dest="nodes", default=[],
626 action="append", metavar="<node>",
627 help="Use only this node (can be used multiple"
628 " times, if not given defaults to all nodes)",
629 completion_suggest=OPT_COMPL_ONE_NODE)
631 SINGLE_NODE_OPT = cli_option("-n", "--node", dest="node", help="Target node",
633 completion_suggest=OPT_COMPL_ONE_NODE)
635 NOSTART_OPT = cli_option("--no-start", dest="start", default=True,
636 action="store_false",
637 help="Don't start the instance after creation")
639 SHOWCMD_OPT = cli_option("--show-cmd", dest="show_command",
640 action="store_true", default=False,
641 help="Show command instead of executing it")
643 CLEANUP_OPT = cli_option("--cleanup", dest="cleanup",
644 default=False, action="store_true",
645 help="Instead of performing the migration, try to"
646 " recover from a failed cleanup. This is safe"
647 " to run even if the instance is healthy, but it"
648 " will create extra replication traffic and "
649 " disrupt briefly the replication (like during the"
652 STATIC_OPT = cli_option("-s", "--static", dest="static",
653 action="store_true", default=False,
654 help="Only show configuration data, not runtime data")
656 ALL_OPT = cli_option("--all", dest="show_all",
657 default=False, action="store_true",
658 help="Show info on all instances on the cluster."
659 " This can take a long time to run, use wisely")
661 SELECT_OS_OPT = cli_option("--select-os", dest="select_os",
662 action="store_true", default=False,
663 help="Interactive OS reinstall, lists available"
664 " OS templates for selection")
666 IGNORE_FAILURES_OPT = cli_option("--ignore-failures", dest="ignore_failures",
667 action="store_true", default=False,
668 help="Remove the instance from the cluster"
669 " configuration even if there are failures"
670 " during the removal process")
672 NEW_SECONDARY_OPT = cli_option("-n", "--new-secondary", dest="dst_node",
673 help="Specifies the new secondary node",
674 metavar="NODE", default=None,
675 completion_suggest=OPT_COMPL_ONE_NODE)
677 ON_PRIMARY_OPT = cli_option("-p", "--on-primary", dest="on_primary",
678 default=False, action="store_true",
679 help="Replace the disk(s) on the primary"
680 " node (only for the drbd template)")
682 ON_SECONDARY_OPT = cli_option("-s", "--on-secondary", dest="on_secondary",
683 default=False, action="store_true",
684 help="Replace the disk(s) on the secondary"
685 " node (only for the drbd template)")
687 AUTO_REPLACE_OPT = cli_option("-a", "--auto", dest="auto",
688 default=False, action="store_true",
689 help="Automatically replace faulty disks"
690 " (only for the drbd template)")
692 IGNORE_SIZE_OPT = cli_option("--ignore-size", dest="ignore_size",
693 default=False, action="store_true",
694 help="Ignore current recorded size"
695 " (useful for forcing activation when"
696 " the recorded size is wrong)")
698 SRC_NODE_OPT = cli_option("--src-node", dest="src_node", help="Source node",
700 completion_suggest=OPT_COMPL_ONE_NODE)
702 SRC_DIR_OPT = cli_option("--src-dir", dest="src_dir", help="Source directory",
705 SECONDARY_IP_OPT = cli_option("-s", "--secondary-ip", dest="secondary_ip",
706 help="Specify the secondary ip for the node",
707 metavar="ADDRESS", default=None)
709 READD_OPT = cli_option("--readd", dest="readd",
710 default=False, action="store_true",
711 help="Readd old node after replacing it")
713 NOSSH_KEYCHECK_OPT = cli_option("--no-ssh-key-check", dest="ssh_key_check",
714 default=True, action="store_false",
715 help="Disable SSH key fingerprint checking")
718 MC_OPT = cli_option("-C", "--master-candidate", dest="master_candidate",
719 choices=_YESNO, default=None, metavar=_YORNO,
720 help="Set the master_candidate flag on the node")
722 OFFLINE_OPT = cli_option("-O", "--offline", dest="offline", metavar=_YORNO,
723 choices=_YESNO, default=None,
724 help="Set the offline flag on the node")
726 DRAINED_OPT = cli_option("-D", "--drained", dest="drained", metavar=_YORNO,
727 choices=_YESNO, default=None,
728 help="Set the drained flag on the node")
730 ALLOCATABLE_OPT = cli_option("--allocatable", dest="allocatable",
731 choices=_YESNO, default=None, metavar=_YORNO,
732 help="Set the allocatable flag on a volume")
734 NOLVM_STORAGE_OPT = cli_option("--no-lvm-storage", dest="lvm_storage",
735 help="Disable support for lvm based instances"
737 action="store_false", default=True)
739 ENABLED_HV_OPT = cli_option("--enabled-hypervisors",
740 dest="enabled_hypervisors",
741 help="Comma-separated list of hypervisors",
742 type="string", default=None)
744 NIC_PARAMS_OPT = cli_option("-N", "--nic-parameters", dest="nicparams",
745 type="keyval", default={},
746 help="NIC parameters")
748 CP_SIZE_OPT = cli_option("-C", "--candidate-pool-size", default=None,
749 dest="candidate_pool_size", type="int",
750 help="Set the candidate pool size")
752 VG_NAME_OPT = cli_option("-g", "--vg-name", dest="vg_name",
753 help="Enables LVM and specifies the volume group"
754 " name (cluster-wide) for disk allocation [xenvg]",
755 metavar="VG", default=None)
757 YES_DOIT_OPT = cli_option("--yes-do-it", dest="yes_do_it",
758 help="Destroy cluster", action="store_true")
760 NOVOTING_OPT = cli_option("--no-voting", dest="no_voting",
761 help="Skip node agreement check (dangerous)",
762 action="store_true", default=False)
764 MAC_PREFIX_OPT = cli_option("-m", "--mac-prefix", dest="mac_prefix",
765 help="Specify the mac prefix for the instance IP"
766 " addresses, in the format XX:XX:XX",
770 MASTER_NETDEV_OPT = cli_option("--master-netdev", dest="master_netdev",
771 help="Specify the node interface (cluster-wide)"
772 " on which the master IP address will be added "
773 " [%s]" % constants.DEFAULT_BRIDGE,
775 default=constants.DEFAULT_BRIDGE)
778 GLOBAL_FILEDIR_OPT = cli_option("--file-storage-dir", dest="file_storage_dir",
779 help="Specify the default directory (cluster-"
780 "wide) for storing the file-based disks [%s]" %
781 constants.DEFAULT_FILE_STORAGE_DIR,
783 default=constants.DEFAULT_FILE_STORAGE_DIR)
785 NOMODIFY_ETCHOSTS_OPT = cli_option("--no-etc-hosts", dest="modify_etc_hosts",
786 help="Don't modify /etc/hosts",
787 action="store_false", default=True)
789 ERROR_CODES_OPT = cli_option("--error-codes", dest="error_codes",
790 help="Enable parseable error messages",
791 action="store_true", default=False)
793 NONPLUS1_OPT = cli_option("--no-nplus1-mem", dest="skip_nplusone_mem",
794 help="Skip N+1 memory redundancy tests",
795 action="store_true", default=False)
797 REBOOT_TYPE_OPT = cli_option("-t", "--type", dest="reboot_type",
798 help="Type of reboot: soft/hard/full",
799 default=constants.INSTANCE_REBOOT_HARD,
801 choices=list(constants.REBOOT_TYPES))
803 IGNORE_SECONDARIES_OPT = cli_option("--ignore-secondaries",
804 dest="ignore_secondaries",
805 default=False, action="store_true",
806 help="Ignore errors from secondaries")
808 NOSHUTDOWN_OPT = cli_option("","--noshutdown", dest="shutdown",
809 action="store_false", default=True,
810 help="Don't shutdown the instance (unsafe)")
814 def _ParseArgs(argv, commands, aliases):
815 """Parser for the command line arguments.
817 This function parses the arguments and returns the function which
818 must be executed together with its (modified) arguments.
820 @param argv: the command line
821 @param commands: dictionary with special contents, see the design
822 doc for cmdline handling
823 @param aliases: dictionary with command aliases {'alias': 'target, ...}
829 binary = argv[0].split("/")[-1]
831 if len(argv) > 1 and argv[1] == "--version":
832 ToStdout("%s (ganeti) %s", binary, constants.RELEASE_VERSION)
833 # Quit right away. That way we don't have to care about this special
834 # argument. optparse.py does it the same.
837 if len(argv) < 2 or not (argv[1] in commands or
839 # let's do a nice thing
840 sortedcmds = commands.keys()
843 ToStdout("Usage: %s {command} [options...] [argument...]", binary)
844 ToStdout("%s <command> --help to see details, or man %s", binary, binary)
847 # compute the max line length for cmd + usage
848 mlen = max([len(" %s" % cmd) for cmd in commands])
849 mlen = min(60, mlen) # should not get here...
851 # and format a nice command list
852 ToStdout("Commands:")
853 for cmd in sortedcmds:
854 cmdstr = " %s" % (cmd,)
855 help_text = commands[cmd][4]
856 help_lines = textwrap.wrap(help_text, 79 - 3 - mlen)
857 ToStdout("%-*s - %s", mlen, cmdstr, help_lines.pop(0))
858 for line in help_lines:
859 ToStdout("%-*s %s", mlen, "", line)
863 return None, None, None
865 # get command, unalias it, and look it up in commands
869 raise errors.ProgrammerError("Alias '%s' overrides an existing"
872 if aliases[cmd] not in commands:
873 raise errors.ProgrammerError("Alias '%s' maps to non-existing"
874 " command '%s'" % (cmd, aliases[cmd]))
878 func, args_def, parser_opts, usage, description = commands[cmd]
879 parser = OptionParser(option_list=parser_opts + [_DRY_RUN_OPT, DEBUG_OPT],
880 description=description,
881 formatter=TitledHelpFormatter(),
882 usage="%%prog %s %s" % (cmd, usage))
883 parser.disable_interspersed_args()
884 options, args = parser.parse_args()
886 if not _CheckArguments(cmd, args_def, args):
887 return None, None, None
889 return func, options, args
892 def _CheckArguments(cmd, args_def, args):
893 """Verifies the arguments using the argument definition.
897 1. Abort with error if values specified by user but none expected.
899 1. For each argument in definition
901 1. Keep running count of minimum number of values (min_count)
902 1. Keep running count of maximum number of values (max_count)
903 1. If it has an unlimited number of values
905 1. Abort with error if it's not the last argument in the definition
907 1. If last argument has limited number of values
909 1. Abort with error if number of values doesn't match or is too large
911 1. Abort with error if user didn't pass enough values (min_count)
914 if args and not args_def:
915 ToStderr("Error: Command %s expects no arguments", cmd)
922 last_idx = len(args_def) - 1
924 for idx, arg in enumerate(args_def):
925 if min_count is None:
927 elif arg.min is not None:
930 if max_count is None:
932 elif arg.max is not None:
936 check_max = (arg.max is not None)
938 elif arg.max is None:
939 raise errors.ProgrammerError("Only the last argument can have max=None")
942 # Command with exact number of arguments
943 if (min_count is not None and max_count is not None and
944 min_count == max_count and len(args) != min_count):
945 ToStderr("Error: Command %s expects %d argument(s)", cmd, min_count)
948 # Command with limited number of arguments
949 if max_count is not None and len(args) > max_count:
950 ToStderr("Error: Command %s expects only %d argument(s)",
954 # Command with some required arguments
955 if min_count is not None and len(args) < min_count:
956 ToStderr("Error: Command %s expects at least %d argument(s)",
963 def SplitNodeOption(value):
964 """Splits the value of a --node option.
967 if value and ':' in value:
968 return value.split(':', 1)
974 def wrapper(*args, **kwargs):
977 return fn(*args, **kwargs)
983 def AskUser(text, choices=None):
984 """Ask the user a question.
986 @param text: the question to ask
988 @param choices: list with elements tuples (input_char, return_value,
989 description); if not given, it will default to: [('y', True,
990 'Perform the operation'), ('n', False, 'Do no do the operation')];
991 note that the '?' char is reserved for help
993 @return: one of the return values from the choices list; if input is
994 not possible (i.e. not running with a tty, we return the last
999 choices = [('y', True, 'Perform the operation'),
1000 ('n', False, 'Do not perform the operation')]
1001 if not choices or not isinstance(choices, list):
1002 raise errors.ProgrammerError("Invalid choices argument to AskUser")
1003 for entry in choices:
1004 if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
1005 raise errors.ProgrammerError("Invalid choices element to AskUser")
1007 answer = choices[-1][1]
1009 for line in text.splitlines():
1010 new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
1011 text = "\n".join(new_text)
1013 f = file("/dev/tty", "a+")
1017 chars = [entry[0] for entry in choices]
1018 chars[-1] = "[%s]" % chars[-1]
1020 maps = dict([(entry[0], entry[1]) for entry in choices])
1024 f.write("/".join(chars))
1026 line = f.readline(2).strip().lower()
1031 for entry in choices:
1032 f.write(" %s - %s\n" % (entry[0], entry[2]))
1040 class JobSubmittedException(Exception):
1041 """Job was submitted, client should exit.
1043 This exception has one argument, the ID of the job that was
1044 submitted. The handler should print this ID.
1046 This is not an error, just a structured way to exit from clients.
1051 def SendJob(ops, cl=None):
1052 """Function to submit an opcode without waiting for the results.
1055 @param ops: list of opcodes
1056 @type cl: luxi.Client
1057 @param cl: the luxi client to use for communicating with the master;
1058 if None, a new client will be created
1064 job_id = cl.SubmitJob(ops)
1069 def PollJob(job_id, cl=None, feedback_fn=None):
1070 """Function to poll for the result of a job.
1072 @type job_id: job identified
1073 @param job_id: the job to poll for results
1074 @type cl: luxi.Client
1075 @param cl: the luxi client to use for communicating with the master;
1076 if None, a new client will be created
1082 prev_job_info = None
1083 prev_logmsg_serial = None
1086 result = cl.WaitForJobChange(job_id, ["status"], prev_job_info,
1089 # job not found, go away!
1090 raise errors.JobLost("Job with id %s lost" % job_id)
1092 # Split result, a tuple of (field values, log entries)
1093 (job_info, log_entries) = result
1094 (status, ) = job_info
1097 for log_entry in log_entries:
1098 (serial, timestamp, _, message) = log_entry
1099 if callable(feedback_fn):
1100 feedback_fn(log_entry[1:])
1102 encoded = utils.SafeEncode(message)
1103 ToStdout("%s %s", time.ctime(utils.MergeTime(timestamp)), encoded)
1104 prev_logmsg_serial = max(prev_logmsg_serial, serial)
1106 # TODO: Handle canceled and archived jobs
1107 elif status in (constants.JOB_STATUS_SUCCESS,
1108 constants.JOB_STATUS_ERROR,
1109 constants.JOB_STATUS_CANCELING,
1110 constants.JOB_STATUS_CANCELED):
1113 prev_job_info = job_info
1115 jobs = cl.QueryJobs([job_id], ["status", "opstatus", "opresult"])
1117 raise errors.JobLost("Job with id %s lost" % job_id)
1119 status, opstatus, result = jobs[0]
1120 if status == constants.JOB_STATUS_SUCCESS:
1122 elif status in (constants.JOB_STATUS_CANCELING,
1123 constants.JOB_STATUS_CANCELED):
1124 raise errors.OpExecError("Job was canceled")
1127 for idx, (status, msg) in enumerate(zip(opstatus, result)):
1128 if status == constants.OP_STATUS_SUCCESS:
1130 elif status == constants.OP_STATUS_ERROR:
1131 errors.MaybeRaise(msg)
1133 raise errors.OpExecError("partial failure (opcode %d): %s" %
1136 raise errors.OpExecError(str(msg))
1137 # default failure mode
1138 raise errors.OpExecError(result)
1141 def SubmitOpCode(op, cl=None, feedback_fn=None):
1142 """Legacy function to submit an opcode.
1144 This is just a simple wrapper over the construction of the processor
1145 instance. It should be extended to better handle feedback and
1146 interaction functions.
1152 job_id = SendJob([op], cl)
1154 op_results = PollJob(job_id, cl=cl, feedback_fn=feedback_fn)
1156 return op_results[0]
1159 def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
1160 """Wrapper around SubmitOpCode or SendJob.
1162 This function will decide, based on the 'opts' parameter, whether to
1163 submit and wait for the result of the opcode (and return it), or
1164 whether to just send the job and print its identifier. It is used in
1165 order to simplify the implementation of the '--submit' option.
1167 It will also add the dry-run parameter from the options passed, if true.
1170 if opts and opts.dry_run:
1171 op.dry_run = opts.dry_run
1172 if opts and opts.submit_only:
1173 job_id = SendJob([op], cl=cl)
1174 raise JobSubmittedException(job_id)
1176 return SubmitOpCode(op, cl=cl, feedback_fn=feedback_fn)
1180 # TODO: Cache object?
1182 client = luxi.Client()
1183 except luxi.NoMasterError:
1184 master, myself = ssconf.GetMasterAndMyself()
1185 if master != myself:
1186 raise errors.OpPrereqError("This is not the master node, please connect"
1187 " to node '%s' and rerun the command" %
1194 def FormatError(err):
1195 """Return a formatted error message for a given error.
1197 This function takes an exception instance and returns a tuple
1198 consisting of two values: first, the recommended exit code, and
1199 second, a string describing the error message (not
1200 newline-terminated).
1206 if isinstance(err, errors.ConfigurationError):
1207 txt = "Corrupt configuration file: %s" % msg
1209 obuf.write(txt + "\n")
1210 obuf.write("Aborting.")
1212 elif isinstance(err, errors.HooksAbort):
1213 obuf.write("Failure: hooks execution failed:\n")
1214 for node, script, out in err.args[0]:
1216 obuf.write(" node: %s, script: %s, output: %s\n" %
1217 (node, script, out))
1219 obuf.write(" node: %s, script: %s (no output)\n" %
1221 elif isinstance(err, errors.HooksFailure):
1222 obuf.write("Failure: hooks general failure: %s" % msg)
1223 elif isinstance(err, errors.ResolverError):
1224 this_host = utils.HostInfo.SysName()
1225 if err.args[0] == this_host:
1226 msg = "Failure: can't resolve my own hostname ('%s')"
1228 msg = "Failure: can't resolve hostname '%s'"
1229 obuf.write(msg % err.args[0])
1230 elif isinstance(err, errors.OpPrereqError):
1231 obuf.write("Failure: prerequisites not met for this"
1232 " operation:\n%s" % msg)
1233 elif isinstance(err, errors.OpExecError):
1234 obuf.write("Failure: command execution error:\n%s" % msg)
1235 elif isinstance(err, errors.TagError):
1236 obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
1237 elif isinstance(err, errors.JobQueueDrainError):
1238 obuf.write("Failure: the job queue is marked for drain and doesn't"
1239 " accept new requests\n")
1240 elif isinstance(err, errors.JobQueueFull):
1241 obuf.write("Failure: the job queue is full and doesn't accept new"
1242 " job submissions until old jobs are archived\n")
1243 elif isinstance(err, errors.TypeEnforcementError):
1244 obuf.write("Parameter Error: %s" % msg)
1245 elif isinstance(err, errors.ParameterError):
1246 obuf.write("Failure: unknown/wrong parameter name '%s'" % msg)
1247 elif isinstance(err, errors.GenericError):
1248 obuf.write("Unhandled Ganeti error: %s" % msg)
1249 elif isinstance(err, luxi.NoMasterError):
1250 obuf.write("Cannot communicate with the master daemon.\nIs it running"
1251 " and listening for connections?")
1252 elif isinstance(err, luxi.TimeoutError):
1253 obuf.write("Timeout while talking to the master daemon. Error:\n"
1255 elif isinstance(err, luxi.ProtocolError):
1256 obuf.write("Unhandled protocol error while talking to the master daemon:\n"
1258 elif isinstance(err, JobSubmittedException):
1259 obuf.write("JobID: %s\n" % err.args[0])
1262 obuf.write("Unhandled exception: %s" % msg)
1263 return retcode, obuf.getvalue().rstrip('\n')
1266 def GenericMain(commands, override=None, aliases=None):
1267 """Generic main function for all the gnt-* commands.
1270 - commands: a dictionary with a special structure, see the design doc
1271 for command line handling.
1272 - override: if not None, we expect a dictionary with keys that will
1273 override command line options; this can be used to pass
1274 options from the scripts to generic functions
1275 - aliases: dictionary with command aliases {'alias': 'target, ...}
1278 # save the program name and the entire command line for later logging
1280 binary = os.path.basename(sys.argv[0]) or sys.argv[0]
1281 if len(sys.argv) >= 2:
1282 binary += " " + sys.argv[1]
1283 old_cmdline = " ".join(sys.argv[2:])
1287 binary = "<unknown program>"
1294 func, options, args = _ParseArgs(sys.argv, commands, aliases)
1295 except errors.ParameterError, err:
1296 result, err_msg = FormatError(err)
1300 if func is None: # parse error
1303 if override is not None:
1304 for key, val in override.iteritems():
1305 setattr(options, key, val)
1307 utils.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
1308 stderr_logging=True, program=binary)
1311 logging.info("run with arguments '%s'", old_cmdline)
1313 logging.info("run with no arguments")
1316 result = func(options, args)
1317 except (errors.GenericError, luxi.ProtocolError,
1318 JobSubmittedException), err:
1319 result, err_msg = FormatError(err)
1320 logging.exception("Error during command processing")
1326 def GenerateTable(headers, fields, separator, data,
1327 numfields=None, unitfields=None,
1329 """Prints a table with headers and different fields.
1332 @param headers: dictionary mapping field names to headers for
1335 @param fields: the field names corresponding to each row in
1337 @param separator: the separator to be used; if this is None,
1338 the default 'smart' algorithm is used which computes optimal
1339 field width, otherwise just the separator is used between
1342 @param data: a list of lists, each sublist being one row to be output
1343 @type numfields: list
1344 @param numfields: a list with the fields that hold numeric
1345 values and thus should be right-aligned
1346 @type unitfields: list
1347 @param unitfields: a list with the fields that hold numeric
1348 values that should be formatted with the units field
1349 @type units: string or None
1350 @param units: the units we should use for formatting, or None for
1351 automatic choice (human-readable for non-separator usage, otherwise
1352 megabytes); this is a one-letter string
1361 if numfields is None:
1363 if unitfields is None:
1366 numfields = utils.FieldSet(*numfields)
1367 unitfields = utils.FieldSet(*unitfields)
1370 for field in fields:
1371 if headers and field not in headers:
1372 # TODO: handle better unknown fields (either revert to old
1373 # style of raising exception, or deal more intelligently with
1375 headers[field] = field
1376 if separator is not None:
1377 format_fields.append("%s")
1378 elif numfields.Matches(field):
1379 format_fields.append("%*s")
1381 format_fields.append("%-*s")
1383 if separator is None:
1384 mlens = [0 for name in fields]
1385 format = ' '.join(format_fields)
1387 format = separator.replace("%", "%%").join(format_fields)
1392 for idx, val in enumerate(row):
1393 if unitfields.Matches(fields[idx]):
1399 val = row[idx] = utils.FormatUnit(val, units)
1400 val = row[idx] = str(val)
1401 if separator is None:
1402 mlens[idx] = max(mlens[idx], len(val))
1407 for idx, name in enumerate(fields):
1409 if separator is None:
1410 mlens[idx] = max(mlens[idx], len(hdr))
1411 args.append(mlens[idx])
1413 result.append(format % tuple(args))
1418 line = ['-' for _ in fields]
1419 for idx in xrange(len(fields)):
1420 if separator is None:
1421 args.append(mlens[idx])
1422 args.append(line[idx])
1423 result.append(format % tuple(args))
1428 def FormatTimestamp(ts):
1429 """Formats a given timestamp.
1432 @param ts: a timeval-type timestamp, a tuple of seconds and microseconds
1435 @return: a string with the formatted timestamp
1438 if not isinstance (ts, (tuple, list)) or len(ts) != 2:
1441 return time.strftime("%F %T", time.localtime(sec)) + ".%06d" % usec
1444 def ParseTimespec(value):
1445 """Parse a time specification.
1447 The following suffixed will be recognized:
1455 Without any suffix, the value will be taken to be in seconds.
1460 raise errors.OpPrereqError("Empty time specification passed")
1468 if value[-1] not in suffix_map:
1472 raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1474 multiplier = suffix_map[value[-1]]
1476 if not value: # no data left after stripping the suffix
1477 raise errors.OpPrereqError("Invalid time specification (only"
1480 value = int(value) * multiplier
1482 raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1486 def GetOnlineNodes(nodes, cl=None, nowarn=False):
1487 """Returns the names of online nodes.
1489 This function will also log a warning on stderr with the names of
1492 @param nodes: if not empty, use only this subset of nodes (minus the
1494 @param cl: if not None, luxi client to use
1495 @type nowarn: boolean
1496 @param nowarn: by default, this function will output a note with the
1497 offline nodes that are skipped; if this parameter is True the
1498 note is not displayed
1504 result = cl.QueryNodes(names=nodes, fields=["name", "offline"],
1506 offline = [row[0] for row in result if row[1]]
1507 if offline and not nowarn:
1508 ToStderr("Note: skipping offline node(s): %s" % ", ".join(offline))
1509 return [row[0] for row in result if not row[1]]
1512 def _ToStream(stream, txt, *args):
1513 """Write a message to a stream, bypassing the logging system
1515 @type stream: file object
1516 @param stream: the file to which we should write
1518 @param txt: the message
1523 stream.write(txt % args)
1530 def ToStdout(txt, *args):
1531 """Write a message to stdout only, bypassing the logging system
1533 This is just a wrapper over _ToStream.
1536 @param txt: the message
1539 _ToStream(sys.stdout, txt, *args)
1542 def ToStderr(txt, *args):
1543 """Write a message to stderr only, bypassing the logging system
1545 This is just a wrapper over _ToStream.
1548 @param txt: the message
1551 _ToStream(sys.stderr, txt, *args)
1554 class JobExecutor(object):
1555 """Class which manages the submission and execution of multiple jobs.
1557 Note that instances of this class should not be reused between
1561 def __init__(self, cl=None, verbose=True):
1566 self.verbose = verbose
1569 def QueueJob(self, name, *ops):
1570 """Record a job for later submit.
1573 @param name: a description of the job, will be used in WaitJobSet
1575 self.queue.append((name, ops))
1577 def SubmitPending(self):
1578 """Submit all pending jobs.
1581 results = self.cl.SubmitManyJobs([row[1] for row in self.queue])
1582 for ((status, data), (name, _)) in zip(results, self.queue):
1583 self.jobs.append((status, data, name))
1585 def GetResults(self):
1586 """Wait for and return the results of all jobs.
1589 @return: list of tuples (success, job results), in the same order
1590 as the submitted jobs; if a job has failed, instead of the result
1591 there will be the error message
1595 self.SubmitPending()
1598 ok_jobs = [row[1] for row in self.jobs if row[0]]
1600 ToStdout("Submitted jobs %s", ", ".join(ok_jobs))
1601 for submit_status, jid, name in self.jobs:
1602 if not submit_status:
1603 ToStderr("Failed to submit job for %s: %s", name, jid)
1604 results.append((False, jid))
1607 ToStdout("Waiting for job %s for %s...", jid, name)
1609 job_result = PollJob(jid, cl=self.cl)
1611 except (errors.GenericError, luxi.ProtocolError), err:
1612 _, job_result = FormatError(err)
1614 # the error message will always be shown, verbose or not
1615 ToStderr("Job %s for %s has failed: %s", jid, name, job_result)
1617 results.append((success, job_result))
1620 def WaitOrShow(self, wait):
1621 """Wait for job results or only print the job IDs.
1624 @param wait: whether to wait or not
1628 return self.GetResults()
1631 self.SubmitPending()
1632 for status, result, name in self.jobs:
1634 ToStdout("%s: %s", result, name)
1636 ToStderr("Failure for %s: %s", name, result)