4 # Copyright (C) 2006, 2007 Google Inc.
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 # General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
22 """Module dealing with command line parsing"""
31 from cStringIO import StringIO
33 from ganeti import utils
34 from ganeti import errors
35 from ganeti import constants
36 from ganeti import opcodes
37 from ganeti import luxi
38 from ganeti import ssconf
39 from ganeti import rpc
41 from optparse import (OptionParser, TitledHelpFormatter,
42 Option, OptionValueError)
46 # Command line options
58 "FILESTORE_DRIVER_OPT",
64 "IGNORE_FAILURES_OPT",
89 # Generic functions for CLI programs
94 "JobSubmittedException",
99 # Formatting functions
100 "ToStderr", "ToStdout",
109 # command line options support infrastructure
110 "ARGS_MANY_INSTANCES",
124 "OPT_COMPL_INST_ADD_NODES",
125 "OPT_COMPL_MANY_NODES",
126 "OPT_COMPL_ONE_IALLOCATOR",
127 "OPT_COMPL_ONE_INSTANCE",
128 "OPT_COMPL_ONE_NODE",
139 def __init__(self, min=0, max=None):
144 return ("<%s min=%s max=%s>" %
145 (self.__class__.__name__, self.min, self.max))
148 class ArgSuggest(_Argument):
149 """Suggesting argument.
151 Value can be any of the ones passed to the constructor.
154 def __init__(self, min=0, max=None, choices=None):
155 _Argument.__init__(self, min=min, max=max)
156 self.choices = choices
159 return ("<%s min=%s max=%s choices=%r>" %
160 (self.__class__.__name__, self.min, self.max, self.choices))
163 class ArgChoice(ArgSuggest):
166 Value can be any of the ones passed to the constructor. Like L{ArgSuggest},
167 but value must be one of the choices.
172 class ArgUnknown(_Argument):
173 """Unknown argument to program (e.g. determined at runtime).
178 class ArgInstance(_Argument):
179 """Instances argument.
184 class ArgNode(_Argument):
189 class ArgJobId(_Argument):
195 class ArgFile(_Argument):
196 """File path argument.
201 class ArgCommand(_Argument):
207 class ArgHost(_Argument):
214 ARGS_MANY_INSTANCES = [ArgInstance()]
215 ARGS_MANY_NODES = [ArgNode()]
216 ARGS_ONE_INSTANCE = [ArgInstance(min=1, max=1)]
217 ARGS_ONE_NODE = [ArgNode(min=1, max=1)]
221 def _ExtractTagsObject(opts, args):
222 """Extract the tag type object.
224 Note that this function will modify its args parameter.
227 if not hasattr(opts, "tag_type"):
228 raise errors.ProgrammerError("tag_type not passed to _ExtractTagsObject")
230 if kind == constants.TAG_CLUSTER:
232 elif kind == constants.TAG_NODE or kind == constants.TAG_INSTANCE:
234 raise errors.OpPrereqError("no arguments passed to the command")
238 raise errors.ProgrammerError("Unhandled tag type '%s'" % kind)
242 def _ExtendTags(opts, args):
243 """Extend the args if a source file has been given.
245 This function will extend the tags with the contents of the file
246 passed in the 'tags_source' attribute of the opts parameter. A file
247 named '-' will be replaced by stdin.
250 fname = opts.tags_source
256 new_fh = open(fname, "r")
259 # we don't use the nice 'new_data = [line.strip() for line in fh]'
260 # because of python bug 1633941
262 line = new_fh.readline()
265 new_data.append(line.strip())
268 args.extend(new_data)
271 def ListTags(opts, args):
272 """List the tags on a given object.
274 This is a generic implementation that knows how to deal with all
275 three cases of tag objects (cluster, node, instance). The opts
276 argument is expected to contain a tag_type field denoting what
277 object type we work on.
280 kind, name = _ExtractTagsObject(opts, args)
281 op = opcodes.OpGetTags(kind=kind, name=name)
282 result = SubmitOpCode(op)
283 result = list(result)
289 def AddTags(opts, args):
290 """Add tags on a given object.
292 This is a generic implementation that knows how to deal with all
293 three cases of tag objects (cluster, node, instance). The opts
294 argument is expected to contain a tag_type field denoting what
295 object type we work on.
298 kind, name = _ExtractTagsObject(opts, args)
299 _ExtendTags(opts, args)
301 raise errors.OpPrereqError("No tags to be added")
302 op = opcodes.OpAddTags(kind=kind, name=name, tags=args)
306 def RemoveTags(opts, args):
307 """Remove tags from a given object.
309 This is a generic implementation that knows how to deal with all
310 three cases of tag objects (cluster, node, instance). The opts
311 argument is expected to contain a tag_type field denoting what
312 object type we work on.
315 kind, name = _ExtractTagsObject(opts, args)
316 _ExtendTags(opts, args)
318 raise errors.OpPrereqError("No tags to be removed")
319 op = opcodes.OpDelTags(kind=kind, name=name, tags=args)
323 def check_unit(option, opt, value):
324 """OptParsers custom converter for units.
328 return utils.ParseUnit(value)
329 except errors.UnitParseError, err:
330 raise OptionValueError("option %s: %s" % (opt, err))
333 def _SplitKeyVal(opt, data):
334 """Convert a KeyVal string into a dict.
336 This function will convert a key=val[,...] string into a dict. Empty
337 values will be converted specially: keys which have the prefix 'no_'
338 will have the value=False and the prefix stripped, the others will
342 @param opt: a string holding the option name for which we process the
343 data, used in building error messages
345 @param data: a string of the format key=val,key=val,...
347 @return: {key=val, key=val}
348 @raises errors.ParameterError: if there are duplicate keys
353 for elem in data.split(","):
355 key, val = elem.split("=", 1)
357 if elem.startswith(NO_PREFIX):
358 key, val = elem[len(NO_PREFIX):], False
359 elif elem.startswith(UN_PREFIX):
360 key, val = elem[len(UN_PREFIX):], None
362 key, val = elem, True
364 raise errors.ParameterError("Duplicate key '%s' in option %s" %
370 def check_ident_key_val(option, opt, value):
371 """Custom parser for ident:key=val,key=val options.
373 This will store the parsed values as a tuple (ident, {key: val}). As such,
374 multiple uses of this option via action=append is possible.
378 ident, rest = value, ''
380 ident, rest = value.split(":", 1)
382 if ident.startswith(NO_PREFIX):
384 msg = "Cannot pass options when removing parameter groups: %s" % value
385 raise errors.ParameterError(msg)
386 retval = (ident[len(NO_PREFIX):], False)
387 elif ident.startswith(UN_PREFIX):
389 msg = "Cannot pass options when removing parameter groups: %s" % value
390 raise errors.ParameterError(msg)
391 retval = (ident[len(UN_PREFIX):], None)
393 kv_dict = _SplitKeyVal(opt, rest)
394 retval = (ident, kv_dict)
398 def check_key_val(option, opt, value):
399 """Custom parser class for key=val,key=val options.
401 This will store the parsed values as a dict {key: val}.
404 return _SplitKeyVal(opt, value)
407 # completion_suggestion is normally a list. Using numeric values not evaluating
408 # to False for dynamic completion.
409 (OPT_COMPL_MANY_NODES,
411 OPT_COMPL_ONE_INSTANCE,
413 OPT_COMPL_ONE_IALLOCATOR,
414 OPT_COMPL_INST_ADD_NODES) = range(100, 106)
416 OPT_COMPL_ALL = frozenset([
417 OPT_COMPL_MANY_NODES,
419 OPT_COMPL_ONE_INSTANCE,
421 OPT_COMPL_ONE_IALLOCATOR,
422 OPT_COMPL_INST_ADD_NODES,
426 class CliOption(Option):
427 """Custom option class for optparse.
430 ATTRS = Option.ATTRS + [
431 "completion_suggest",
433 TYPES = Option.TYPES + (
438 TYPE_CHECKER = Option.TYPE_CHECKER.copy()
439 TYPE_CHECKER["identkeyval"] = check_ident_key_val
440 TYPE_CHECKER["keyval"] = check_key_val
441 TYPE_CHECKER["unit"] = check_unit
444 # optparse.py sets make_option, so we do it for our own option class, too
445 cli_option = CliOption
448 DEBUG_OPT = cli_option("-d", "--debug", default=False,
450 help="Turn debugging on")
452 NOHDR_OPT = cli_option("--no-headers", default=False,
453 action="store_true", dest="no_headers",
454 help="Don't display column headers")
456 SEP_OPT = cli_option("--separator", default=None,
457 action="store", dest="separator",
458 help=("Separator between output fields"
459 " (defaults to one space)"))
461 USEUNITS_OPT = cli_option("--units", default=None,
462 dest="units", choices=('h', 'm', 'g', 't'),
463 help="Specify units for output (one of hmgt)")
465 FIELDS_OPT = cli_option("-o", "--output", dest="output", action="store",
466 type="string", metavar="FIELDS",
467 help="Comma separated list of output fields")
469 FORCE_OPT = cli_option("-f", "--force", dest="force", action="store_true",
470 default=False, help="Force the operation")
472 CONFIRM_OPT = cli_option("--yes", dest="confirm", action="store_true",
473 default=False, help="Do not require confirmation")
475 TAG_SRC_OPT = cli_option("--from", dest="tags_source",
476 default=None, help="File with tag names")
478 SUBMIT_OPT = cli_option("--submit", dest="submit_only",
479 default=False, action="store_true",
480 help=("Submit the job and return the job ID, but"
481 " don't wait for the job to finish"))
483 SYNC_OPT = cli_option("--sync", dest="do_locking",
484 default=False, action="store_true",
485 help=("Grab locks while doing the queries"
486 " in order to ensure more consistent results"))
488 _DRY_RUN_OPT = cli_option("--dry-run", default=False,
490 help=("Do not execute the operation, just run the"
491 " check steps and verify it it could be"
494 VERBOSE_OPT = cli_option("-v", "--verbose", default=False,
496 help="Increase the verbosity of the operation")
498 DEBUG_SIMERR_OPT = cli_option("--debug-simulate-errors", default=False,
499 action="store_true", dest="simulate_errors",
500 help="Debugging option that makes the operation"
501 " treat most runtime checks as failed")
503 NWSYNC_OPT = cli_option("--no-wait-for-sync", dest="wait_for_sync",
504 default=True, action="store_false",
505 help="Don't wait for sync (DANGEROUS!)")
507 DISK_TEMPLATE_OPT = cli_option("-t", "--disk-template", dest="disk_template",
508 help="Custom disk setup (diskless, file,"
510 default=None, metavar="TEMPL",
511 choices=list(constants.DISK_TEMPLATES))
513 NONICS_OPT = cli_option("--no-nics", default=False, action="store_true",
514 help="Do not create any network cards for"
517 FILESTORE_DIR_OPT = cli_option("--file-storage-dir", dest="file_storage_dir",
518 help="Relative path under default cluster-wide"
519 " file storage dir to store file-based disks",
520 default=None, metavar="<DIR>")
522 FILESTORE_DRIVER_OPT = cli_option("--file-driver", dest="file_driver",
523 help="Driver to use for image files",
524 default="loop", metavar="<DRIVER>",
525 choices=list(constants.FILE_DRIVER))
527 IALLOCATOR_OPT = cli_option("-I", "--iallocator", metavar="<NAME>",
528 help="Select nodes for the instance automatically"
529 " using the <NAME> iallocator plugin",
530 default=None, type="string",
531 completion_suggest=OPT_COMPL_ONE_IALLOCATOR)
533 OS_OPT = cli_option("-o", "--os-type", dest="os", help="What OS to run",
535 completion_suggest=OPT_COMPL_ONE_OS)
537 BACKEND_OPT = cli_option("-B", "--backend-parameters", dest="beparams",
538 type="keyval", default={},
539 help="Backend parameters")
541 HVOPTS_OPT = cli_option("-H", "--hypervisor-parameters", type="keyval",
542 default={}, dest="hvparams",
543 help="Hypervisor parameters")
545 HYPERVISOR_OPT = cli_option("-H", "--hypervisor-parameters", dest="hypervisor",
546 help="Hypervisor and hypervisor options, in the"
547 " format hypervisor:option=value,option=value,...",
548 default=None, type="identkeyval")
550 HVLIST_OPT = cli_option("-H", "--hypervisor-parameters", dest="hvparams",
551 help="Hypervisor and hypervisor options, in the"
552 " format hypervisor:option=value,option=value,...",
553 default=[], action="append", type="identkeyval")
555 NOIPCHECK_OPT = cli_option("--no-ip-check", dest="ip_check", default=True,
556 action="store_false",
557 help="Don't check that the instance's IP"
560 NET_OPT = cli_option("--net",
561 help="NIC parameters", default=[],
562 dest="nics", action="append", type="identkeyval")
564 DISK_OPT = cli_option("--disk", help="Disk parameters", default=[],
565 dest="disks", action="append", type="identkeyval")
567 DISKIDX_OPT = cli_option("--disks", dest="disks", default=None,
568 help="Comma-separated list of disks"
569 " indices to act on (e.g. 0,2) (optional,"
570 " defaults to all disks)")
572 OS_SIZE_OPT = cli_option("-s", "--os-size", dest="sd_size",
573 help="Enforces a single-disk configuration using the"
574 " given disk size, in MiB unless a suffix is used",
575 default=None, type="unit", metavar="<size>")
577 IGNORE_CONSIST_OPT = cli_option("--ignore-consistency",
578 dest="ignore_consistency",
579 action="store_true", default=False,
580 help="Ignore the consistency of the disks on"
583 NONLIVE_OPT = cli_option("--non-live", dest="live",
584 default=True, action="store_false",
585 help="Do a non-live migration (this usually means"
586 " freeze the instance, save the state, transfer and"
587 " only then resume running on the secondary node)")
589 NODE_PLACEMENT_OPT = cli_option("-n", "--node", dest="node",
590 help="Target node and optional secondary node",
591 metavar="<pnode>[:<snode>]",
592 completion_suggest=OPT_COMPL_INST_ADD_NODES)
594 NODE_LIST_OPT = cli_option("-n", "--node", dest="nodes", default=[],
595 action="append", metavar="<node>",
596 help="Use only this node (can be used multiple"
597 " times, if not given defaults to all nodes)",
598 completion_suggest=OPT_COMPL_ONE_NODE)
600 SINGLE_NODE_OPT = cli_option("-n", "--node", dest="node", help="Target node",
602 completion_suggest=OPT_COMPL_ONE_NODE)
604 NOSTART_OPT = cli_option("--no-start", dest="start", default=True,
605 action="store_false",
606 help="Don't start the instance after creation")
608 SHOWCMD_OPT = cli_option("--show-cmd", dest="show_command",
609 action="store_true", default=False,
610 help="Show command instead of executing it")
612 CLEANUP_OPT = cli_option("--cleanup", dest="cleanup",
613 default=False, action="store_true",
614 help="Instead of performing the migration, try to"
615 " recover from a failed cleanup. This is safe"
616 " to run even if the instance is healthy, but it"
617 " will create extra replication traffic and "
618 " disrupt briefly the replication (like during the"
621 STATIC_OPT = cli_option("-s", "--static", dest="static",
622 action="store_true", default=False,
623 help="Only show configuration data, not runtime data")
625 ALL_OPT = cli_option("--all", dest="show_all",
626 default=False, action="store_true",
627 help="Show info on all instances on the cluster."
628 " This can take a long time to run, use wisely")
630 SELECT_OS_OPT = cli_option("--select-os", dest="select_os",
631 action="store_true", default=False,
632 help="Interactive OS reinstall, lists available"
633 " OS templates for selection")
635 IGNORE_FAILURES_OPT = cli_option("--ignore-failures", dest="ignore_failures",
636 action="store_true", default=False,
637 help="Remove the instance from the cluster"
638 " configuration even if there are failures"
639 " during the removal process")
641 NEW_SECONDARY_OPT = cli_option("-n", "--new-secondary", dest="dst_node",
642 help="Specifies the new secondary node",
643 metavar="NODE", default=None,
644 completion_suggest=OPT_COMPL_ONE_NODE)
646 ON_PRIMARY_OPT = cli_option("-p", "--on-primary", dest="on_primary",
647 default=False, action="store_true",
648 help="Replace the disk(s) on the primary"
649 " node (only for the drbd template)")
652 def _ParseArgs(argv, commands, aliases):
653 """Parser for the command line arguments.
655 This function parses the arguments and returns the function which
656 must be executed together with its (modified) arguments.
658 @param argv: the command line
659 @param commands: dictionary with special contents, see the design
660 doc for cmdline handling
661 @param aliases: dictionary with command aliases {'alias': 'target, ...}
667 binary = argv[0].split("/")[-1]
669 if len(argv) > 1 and argv[1] == "--version":
670 ToStdout("%s (ganeti) %s", binary, constants.RELEASE_VERSION)
671 # Quit right away. That way we don't have to care about this special
672 # argument. optparse.py does it the same.
675 if len(argv) < 2 or not (argv[1] in commands or
677 # let's do a nice thing
678 sortedcmds = commands.keys()
681 ToStdout("Usage: %s {command} [options...] [argument...]", binary)
682 ToStdout("%s <command> --help to see details, or man %s", binary, binary)
685 # compute the max line length for cmd + usage
686 mlen = max([len(" %s" % cmd) for cmd in commands])
687 mlen = min(60, mlen) # should not get here...
689 # and format a nice command list
690 ToStdout("Commands:")
691 for cmd in sortedcmds:
692 cmdstr = " %s" % (cmd,)
693 help_text = commands[cmd][4]
694 help_lines = textwrap.wrap(help_text, 79 - 3 - mlen)
695 ToStdout("%-*s - %s", mlen, cmdstr, help_lines.pop(0))
696 for line in help_lines:
697 ToStdout("%-*s %s", mlen, "", line)
701 return None, None, None
703 # get command, unalias it, and look it up in commands
707 raise errors.ProgrammerError("Alias '%s' overrides an existing"
710 if aliases[cmd] not in commands:
711 raise errors.ProgrammerError("Alias '%s' maps to non-existing"
712 " command '%s'" % (cmd, aliases[cmd]))
716 func, args_def, parser_opts, usage, description = commands[cmd]
717 parser = OptionParser(option_list=parser_opts + [_DRY_RUN_OPT],
718 description=description,
719 formatter=TitledHelpFormatter(),
720 usage="%%prog %s %s" % (cmd, usage))
721 parser.disable_interspersed_args()
722 options, args = parser.parse_args()
724 if not _CheckArguments(cmd, args_def, args):
725 return None, None, None
727 return func, options, args
730 def _CheckArguments(cmd, args_def, args):
731 """Verifies the arguments using the argument definition.
735 1. Abort with error if values specified by user but none expected.
737 1. For each argument in definition
739 1. Keep running count of minimum number of values (min_count)
740 1. Keep running count of maximum number of values (max_count)
741 1. If it has an unlimited number of values
743 1. Abort with error if it's not the last argument in the definition
745 1. If last argument has limited number of values
747 1. Abort with error if number of values doesn't match or is too large
749 1. Abort with error if user didn't pass enough values (min_count)
752 if args and not args_def:
753 ToStderr("Error: Command %s expects no arguments", cmd)
760 last_idx = len(args_def) - 1
762 for idx, arg in enumerate(args_def):
763 if min_count is None:
765 elif arg.min is not None:
768 if max_count is None:
770 elif arg.max is not None:
774 check_max = (arg.max is not None)
776 elif arg.max is None:
777 raise errors.ProgrammerError("Only the last argument can have max=None")
780 # Command with exact number of arguments
781 if (min_count is not None and max_count is not None and
782 min_count == max_count and len(args) != min_count):
783 ToStderr("Error: Command %s expects %d argument(s)", cmd, min_count)
786 # Command with limited number of arguments
787 if max_count is not None and len(args) > max_count:
788 ToStderr("Error: Command %s expects only %d argument(s)",
792 # Command with some required arguments
793 if min_count is not None and len(args) < min_count:
794 ToStderr("Error: Command %s expects at least %d argument(s)",
801 def SplitNodeOption(value):
802 """Splits the value of a --node option.
805 if value and ':' in value:
806 return value.split(':', 1)
812 def wrapper(*args, **kwargs):
815 return fn(*args, **kwargs)
821 def AskUser(text, choices=None):
822 """Ask the user a question.
824 @param text: the question to ask
826 @param choices: list with elements tuples (input_char, return_value,
827 description); if not given, it will default to: [('y', True,
828 'Perform the operation'), ('n', False, 'Do no do the operation')];
829 note that the '?' char is reserved for help
831 @return: one of the return values from the choices list; if input is
832 not possible (i.e. not running with a tty, we return the last
837 choices = [('y', True, 'Perform the operation'),
838 ('n', False, 'Do not perform the operation')]
839 if not choices or not isinstance(choices, list):
840 raise errors.ProgrammerError("Invalid choices argument to AskUser")
841 for entry in choices:
842 if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
843 raise errors.ProgrammerError("Invalid choices element to AskUser")
845 answer = choices[-1][1]
847 for line in text.splitlines():
848 new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
849 text = "\n".join(new_text)
851 f = file("/dev/tty", "a+")
855 chars = [entry[0] for entry in choices]
856 chars[-1] = "[%s]" % chars[-1]
858 maps = dict([(entry[0], entry[1]) for entry in choices])
862 f.write("/".join(chars))
864 line = f.readline(2).strip().lower()
869 for entry in choices:
870 f.write(" %s - %s\n" % (entry[0], entry[2]))
878 class JobSubmittedException(Exception):
879 """Job was submitted, client should exit.
881 This exception has one argument, the ID of the job that was
882 submitted. The handler should print this ID.
884 This is not an error, just a structured way to exit from clients.
889 def SendJob(ops, cl=None):
890 """Function to submit an opcode without waiting for the results.
893 @param ops: list of opcodes
894 @type cl: luxi.Client
895 @param cl: the luxi client to use for communicating with the master;
896 if None, a new client will be created
902 job_id = cl.SubmitJob(ops)
907 def PollJob(job_id, cl=None, feedback_fn=None):
908 """Function to poll for the result of a job.
910 @type job_id: job identified
911 @param job_id: the job to poll for results
912 @type cl: luxi.Client
913 @param cl: the luxi client to use for communicating with the master;
914 if None, a new client will be created
921 prev_logmsg_serial = None
924 result = cl.WaitForJobChange(job_id, ["status"], prev_job_info,
927 # job not found, go away!
928 raise errors.JobLost("Job with id %s lost" % job_id)
930 # Split result, a tuple of (field values, log entries)
931 (job_info, log_entries) = result
932 (status, ) = job_info
935 for log_entry in log_entries:
936 (serial, timestamp, _, message) = log_entry
937 if callable(feedback_fn):
938 feedback_fn(log_entry[1:])
940 encoded = utils.SafeEncode(message)
941 ToStdout("%s %s", time.ctime(utils.MergeTime(timestamp)), encoded)
942 prev_logmsg_serial = max(prev_logmsg_serial, serial)
944 # TODO: Handle canceled and archived jobs
945 elif status in (constants.JOB_STATUS_SUCCESS,
946 constants.JOB_STATUS_ERROR,
947 constants.JOB_STATUS_CANCELING,
948 constants.JOB_STATUS_CANCELED):
951 prev_job_info = job_info
953 jobs = cl.QueryJobs([job_id], ["status", "opstatus", "opresult"])
955 raise errors.JobLost("Job with id %s lost" % job_id)
957 status, opstatus, result = jobs[0]
958 if status == constants.JOB_STATUS_SUCCESS:
960 elif status in (constants.JOB_STATUS_CANCELING,
961 constants.JOB_STATUS_CANCELED):
962 raise errors.OpExecError("Job was canceled")
965 for idx, (status, msg) in enumerate(zip(opstatus, result)):
966 if status == constants.OP_STATUS_SUCCESS:
968 elif status == constants.OP_STATUS_ERROR:
969 errors.MaybeRaise(msg)
971 raise errors.OpExecError("partial failure (opcode %d): %s" %
974 raise errors.OpExecError(str(msg))
975 # default failure mode
976 raise errors.OpExecError(result)
979 def SubmitOpCode(op, cl=None, feedback_fn=None):
980 """Legacy function to submit an opcode.
982 This is just a simple wrapper over the construction of the processor
983 instance. It should be extended to better handle feedback and
984 interaction functions.
990 job_id = SendJob([op], cl)
992 op_results = PollJob(job_id, cl=cl, feedback_fn=feedback_fn)
997 def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
998 """Wrapper around SubmitOpCode or SendJob.
1000 This function will decide, based on the 'opts' parameter, whether to
1001 submit and wait for the result of the opcode (and return it), or
1002 whether to just send the job and print its identifier. It is used in
1003 order to simplify the implementation of the '--submit' option.
1005 It will also add the dry-run parameter from the options passed, if true.
1008 if opts and opts.dry_run:
1009 op.dry_run = opts.dry_run
1010 if opts and opts.submit_only:
1011 job_id = SendJob([op], cl=cl)
1012 raise JobSubmittedException(job_id)
1014 return SubmitOpCode(op, cl=cl, feedback_fn=feedback_fn)
1018 # TODO: Cache object?
1020 client = luxi.Client()
1021 except luxi.NoMasterError:
1022 master, myself = ssconf.GetMasterAndMyself()
1023 if master != myself:
1024 raise errors.OpPrereqError("This is not the master node, please connect"
1025 " to node '%s' and rerun the command" %
1032 def FormatError(err):
1033 """Return a formatted error message for a given error.
1035 This function takes an exception instance and returns a tuple
1036 consisting of two values: first, the recommended exit code, and
1037 second, a string describing the error message (not
1038 newline-terminated).
1044 if isinstance(err, errors.ConfigurationError):
1045 txt = "Corrupt configuration file: %s" % msg
1047 obuf.write(txt + "\n")
1048 obuf.write("Aborting.")
1050 elif isinstance(err, errors.HooksAbort):
1051 obuf.write("Failure: hooks execution failed:\n")
1052 for node, script, out in err.args[0]:
1054 obuf.write(" node: %s, script: %s, output: %s\n" %
1055 (node, script, out))
1057 obuf.write(" node: %s, script: %s (no output)\n" %
1059 elif isinstance(err, errors.HooksFailure):
1060 obuf.write("Failure: hooks general failure: %s" % msg)
1061 elif isinstance(err, errors.ResolverError):
1062 this_host = utils.HostInfo.SysName()
1063 if err.args[0] == this_host:
1064 msg = "Failure: can't resolve my own hostname ('%s')"
1066 msg = "Failure: can't resolve hostname '%s'"
1067 obuf.write(msg % err.args[0])
1068 elif isinstance(err, errors.OpPrereqError):
1069 obuf.write("Failure: prerequisites not met for this"
1070 " operation:\n%s" % msg)
1071 elif isinstance(err, errors.OpExecError):
1072 obuf.write("Failure: command execution error:\n%s" % msg)
1073 elif isinstance(err, errors.TagError):
1074 obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
1075 elif isinstance(err, errors.JobQueueDrainError):
1076 obuf.write("Failure: the job queue is marked for drain and doesn't"
1077 " accept new requests\n")
1078 elif isinstance(err, errors.JobQueueFull):
1079 obuf.write("Failure: the job queue is full and doesn't accept new"
1080 " job submissions until old jobs are archived\n")
1081 elif isinstance(err, errors.TypeEnforcementError):
1082 obuf.write("Parameter Error: %s" % msg)
1083 elif isinstance(err, errors.ParameterError):
1084 obuf.write("Failure: unknown/wrong parameter name '%s'" % msg)
1085 elif isinstance(err, errors.GenericError):
1086 obuf.write("Unhandled Ganeti error: %s" % msg)
1087 elif isinstance(err, luxi.NoMasterError):
1088 obuf.write("Cannot communicate with the master daemon.\nIs it running"
1089 " and listening for connections?")
1090 elif isinstance(err, luxi.TimeoutError):
1091 obuf.write("Timeout while talking to the master daemon. Error:\n"
1093 elif isinstance(err, luxi.ProtocolError):
1094 obuf.write("Unhandled protocol error while talking to the master daemon:\n"
1096 elif isinstance(err, JobSubmittedException):
1097 obuf.write("JobID: %s\n" % err.args[0])
1100 obuf.write("Unhandled exception: %s" % msg)
1101 return retcode, obuf.getvalue().rstrip('\n')
1104 def GenericMain(commands, override=None, aliases=None):
1105 """Generic main function for all the gnt-* commands.
1108 - commands: a dictionary with a special structure, see the design doc
1109 for command line handling.
1110 - override: if not None, we expect a dictionary with keys that will
1111 override command line options; this can be used to pass
1112 options from the scripts to generic functions
1113 - aliases: dictionary with command aliases {'alias': 'target, ...}
1116 # save the program name and the entire command line for later logging
1118 binary = os.path.basename(sys.argv[0]) or sys.argv[0]
1119 if len(sys.argv) >= 2:
1120 binary += " " + sys.argv[1]
1121 old_cmdline = " ".join(sys.argv[2:])
1125 binary = "<unknown program>"
1132 func, options, args = _ParseArgs(sys.argv, commands, aliases)
1133 except errors.ParameterError, err:
1134 result, err_msg = FormatError(err)
1138 if func is None: # parse error
1141 if override is not None:
1142 for key, val in override.iteritems():
1143 setattr(options, key, val)
1145 utils.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
1146 stderr_logging=True, program=binary)
1149 logging.info("run with arguments '%s'", old_cmdline)
1151 logging.info("run with no arguments")
1154 result = func(options, args)
1155 except (errors.GenericError, luxi.ProtocolError,
1156 JobSubmittedException), err:
1157 result, err_msg = FormatError(err)
1158 logging.exception("Error during command processing")
1164 def GenerateTable(headers, fields, separator, data,
1165 numfields=None, unitfields=None,
1167 """Prints a table with headers and different fields.
1170 @param headers: dictionary mapping field names to headers for
1173 @param fields: the field names corresponding to each row in
1175 @param separator: the separator to be used; if this is None,
1176 the default 'smart' algorithm is used which computes optimal
1177 field width, otherwise just the separator is used between
1180 @param data: a list of lists, each sublist being one row to be output
1181 @type numfields: list
1182 @param numfields: a list with the fields that hold numeric
1183 values and thus should be right-aligned
1184 @type unitfields: list
1185 @param unitfields: a list with the fields that hold numeric
1186 values that should be formatted with the units field
1187 @type units: string or None
1188 @param units: the units we should use for formatting, or None for
1189 automatic choice (human-readable for non-separator usage, otherwise
1190 megabytes); this is a one-letter string
1199 if numfields is None:
1201 if unitfields is None:
1204 numfields = utils.FieldSet(*numfields)
1205 unitfields = utils.FieldSet(*unitfields)
1208 for field in fields:
1209 if headers and field not in headers:
1210 # TODO: handle better unknown fields (either revert to old
1211 # style of raising exception, or deal more intelligently with
1213 headers[field] = field
1214 if separator is not None:
1215 format_fields.append("%s")
1216 elif numfields.Matches(field):
1217 format_fields.append("%*s")
1219 format_fields.append("%-*s")
1221 if separator is None:
1222 mlens = [0 for name in fields]
1223 format = ' '.join(format_fields)
1225 format = separator.replace("%", "%%").join(format_fields)
1230 for idx, val in enumerate(row):
1231 if unitfields.Matches(fields[idx]):
1237 val = row[idx] = utils.FormatUnit(val, units)
1238 val = row[idx] = str(val)
1239 if separator is None:
1240 mlens[idx] = max(mlens[idx], len(val))
1245 for idx, name in enumerate(fields):
1247 if separator is None:
1248 mlens[idx] = max(mlens[idx], len(hdr))
1249 args.append(mlens[idx])
1251 result.append(format % tuple(args))
1256 line = ['-' for _ in fields]
1257 for idx in xrange(len(fields)):
1258 if separator is None:
1259 args.append(mlens[idx])
1260 args.append(line[idx])
1261 result.append(format % tuple(args))
1266 def FormatTimestamp(ts):
1267 """Formats a given timestamp.
1270 @param ts: a timeval-type timestamp, a tuple of seconds and microseconds
1273 @return: a string with the formatted timestamp
1276 if not isinstance (ts, (tuple, list)) or len(ts) != 2:
1279 return time.strftime("%F %T", time.localtime(sec)) + ".%06d" % usec
1282 def ParseTimespec(value):
1283 """Parse a time specification.
1285 The following suffixed will be recognized:
1293 Without any suffix, the value will be taken to be in seconds.
1298 raise errors.OpPrereqError("Empty time specification passed")
1306 if value[-1] not in suffix_map:
1310 raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1312 multiplier = suffix_map[value[-1]]
1314 if not value: # no data left after stripping the suffix
1315 raise errors.OpPrereqError("Invalid time specification (only"
1318 value = int(value) * multiplier
1320 raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1324 def GetOnlineNodes(nodes, cl=None, nowarn=False):
1325 """Returns the names of online nodes.
1327 This function will also log a warning on stderr with the names of
1330 @param nodes: if not empty, use only this subset of nodes (minus the
1332 @param cl: if not None, luxi client to use
1333 @type nowarn: boolean
1334 @param nowarn: by default, this function will output a note with the
1335 offline nodes that are skipped; if this parameter is True the
1336 note is not displayed
1342 result = cl.QueryNodes(names=nodes, fields=["name", "offline"],
1344 offline = [row[0] for row in result if row[1]]
1345 if offline and not nowarn:
1346 ToStderr("Note: skipping offline node(s): %s" % ", ".join(offline))
1347 return [row[0] for row in result if not row[1]]
1350 def _ToStream(stream, txt, *args):
1351 """Write a message to a stream, bypassing the logging system
1353 @type stream: file object
1354 @param stream: the file to which we should write
1356 @param txt: the message
1361 stream.write(txt % args)
1368 def ToStdout(txt, *args):
1369 """Write a message to stdout only, bypassing the logging system
1371 This is just a wrapper over _ToStream.
1374 @param txt: the message
1377 _ToStream(sys.stdout, txt, *args)
1380 def ToStderr(txt, *args):
1381 """Write a message to stderr only, bypassing the logging system
1383 This is just a wrapper over _ToStream.
1386 @param txt: the message
1389 _ToStream(sys.stderr, txt, *args)
1392 class JobExecutor(object):
1393 """Class which manages the submission and execution of multiple jobs.
1395 Note that instances of this class should not be reused between
1399 def __init__(self, cl=None, verbose=True):
1404 self.verbose = verbose
1407 def QueueJob(self, name, *ops):
1408 """Record a job for later submit.
1411 @param name: a description of the job, will be used in WaitJobSet
1413 self.queue.append((name, ops))
1415 def SubmitPending(self):
1416 """Submit all pending jobs.
1419 results = self.cl.SubmitManyJobs([row[1] for row in self.queue])
1420 for ((status, data), (name, _)) in zip(results, self.queue):
1421 self.jobs.append((status, data, name))
1423 def GetResults(self):
1424 """Wait for and return the results of all jobs.
1427 @return: list of tuples (success, job results), in the same order
1428 as the submitted jobs; if a job has failed, instead of the result
1429 there will be the error message
1433 self.SubmitPending()
1436 ok_jobs = [row[1] for row in self.jobs if row[0]]
1438 ToStdout("Submitted jobs %s", ", ".join(ok_jobs))
1439 for submit_status, jid, name in self.jobs:
1440 if not submit_status:
1441 ToStderr("Failed to submit job for %s: %s", name, jid)
1442 results.append((False, jid))
1445 ToStdout("Waiting for job %s for %s...", jid, name)
1447 job_result = PollJob(jid, cl=self.cl)
1449 except (errors.GenericError, luxi.ProtocolError), err:
1450 _, job_result = FormatError(err)
1452 # the error message will always be shown, verbose or not
1453 ToStderr("Job %s for %s has failed: %s", jid, name, job_result)
1455 results.append((success, job_result))
1458 def WaitOrShow(self, wait):
1459 """Wait for job results or only print the job IDs.
1462 @param wait: whether to wait or not
1466 return self.GetResults()
1469 self.SubmitPending()
1470 for status, result, name in self.jobs:
1472 ToStdout("%s: %s", result, name)
1474 ToStderr("Failure for %s: %s", name, result)