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, make_option, TitledHelpFormatter,
42 Option, OptionValueError)
44 __all__ = ["DEBUG_OPT", "NOHDR_OPT", "SEP_OPT", "GenericMain",
45 "SubmitOpCode", "GetClient",
46 "cli_option", "ikv_option", "keyval_option",
47 "GenerateTable", "AskUser",
48 "ARGS_NONE", "ARGS_FIXED", "ARGS_ATLEAST", "ARGS_ANY", "ARGS_ONE",
49 "USEUNITS_OPT", "FIELDS_OPT", "FORCE_OPT", "SUBMIT_OPT",
50 "ListTags", "AddTags", "RemoveTags", "TAG_SRC_OPT",
51 "FormatError", "SplitNodeOption", "SubmitOrSend",
52 "JobSubmittedException", "FormatTimestamp", "ParseTimespec",
54 "ToStderr", "ToStdout",
59 def _ExtractTagsObject(opts, args):
60 """Extract the tag type object.
62 Note that this function will modify its args parameter.
65 if not hasattr(opts, "tag_type"):
66 raise errors.ProgrammerError("tag_type not passed to _ExtractTagsObject")
68 if kind == constants.TAG_CLUSTER:
70 elif kind == constants.TAG_NODE or kind == constants.TAG_INSTANCE:
72 raise errors.OpPrereqError("no arguments passed to the command")
76 raise errors.ProgrammerError("Unhandled tag type '%s'" % kind)
80 def _ExtendTags(opts, args):
81 """Extend the args if a source file has been given.
83 This function will extend the tags with the contents of the file
84 passed in the 'tags_source' attribute of the opts parameter. A file
85 named '-' will be replaced by stdin.
88 fname = opts.tags_source
94 new_fh = open(fname, "r")
97 # we don't use the nice 'new_data = [line.strip() for line in fh]'
98 # because of python bug 1633941
100 line = new_fh.readline()
103 new_data.append(line.strip())
106 args.extend(new_data)
109 def ListTags(opts, args):
110 """List the tags on a given object.
112 This is a generic implementation that knows how to deal with all
113 three cases of tag objects (cluster, node, instance). The opts
114 argument is expected to contain a tag_type field denoting what
115 object type we work on.
118 kind, name = _ExtractTagsObject(opts, args)
119 op = opcodes.OpGetTags(kind=kind, name=name)
120 result = SubmitOpCode(op)
121 result = list(result)
127 def AddTags(opts, args):
128 """Add tags on a given object.
130 This is a generic implementation that knows how to deal with all
131 three cases of tag objects (cluster, node, instance). The opts
132 argument is expected to contain a tag_type field denoting what
133 object type we work on.
136 kind, name = _ExtractTagsObject(opts, args)
137 _ExtendTags(opts, args)
139 raise errors.OpPrereqError("No tags to be added")
140 op = opcodes.OpAddTags(kind=kind, name=name, tags=args)
144 def RemoveTags(opts, args):
145 """Remove tags from a given object.
147 This is a generic implementation that knows how to deal with all
148 three cases of tag objects (cluster, node, instance). The opts
149 argument is expected to contain a tag_type field denoting what
150 object type we work on.
153 kind, name = _ExtractTagsObject(opts, args)
154 _ExtendTags(opts, args)
156 raise errors.OpPrereqError("No tags to be removed")
157 op = opcodes.OpDelTags(kind=kind, name=name, tags=args)
161 DEBUG_OPT = make_option("-d", "--debug", default=False,
163 help="Turn debugging on")
165 NOHDR_OPT = make_option("--no-headers", default=False,
166 action="store_true", dest="no_headers",
167 help="Don't display column headers")
169 SEP_OPT = make_option("--separator", default=None,
170 action="store", dest="separator",
171 help="Separator between output fields"
172 " (defaults to one space)")
174 USEUNITS_OPT = make_option("--units", default=None,
175 dest="units", choices=('h', 'm', 'g', 't'),
176 help="Specify units for output (one of hmgt)")
178 FIELDS_OPT = make_option("-o", "--output", dest="output", action="store",
179 type="string", help="Comma separated list of"
183 FORCE_OPT = make_option("-f", "--force", dest="force", action="store_true",
184 default=False, help="Force the operation")
186 TAG_SRC_OPT = make_option("--from", dest="tags_source",
187 default=None, help="File with tag names")
189 SUBMIT_OPT = make_option("--submit", dest="submit_only",
190 default=False, action="store_true",
191 help="Submit the job and return the job ID, but"
192 " don't wait for the job to finish")
196 """Macro-like function denoting a fixed number of arguments"""
200 def ARGS_ATLEAST(val):
201 """Macro-like function denoting a minimum number of arguments"""
206 ARGS_ONE = ARGS_FIXED(1)
207 ARGS_ANY = ARGS_ATLEAST(0)
210 def check_unit(option, opt, value):
211 """OptParsers custom converter for units.
215 return utils.ParseUnit(value)
216 except errors.UnitParseError, err:
217 raise OptionValueError("option %s: %s" % (opt, err))
220 class CliOption(Option):
221 """Custom option class for optparse.
224 TYPES = Option.TYPES + ("unit",)
225 TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER)
226 TYPE_CHECKER["unit"] = check_unit
229 def _SplitKeyVal(opt, data):
230 """Convert a KeyVal string into a dict.
232 This function will convert a key=val[,...] string into a dict. Empty
233 values will be converted specially: keys which have the prefix 'no_'
234 will have the value=False and the prefix stripped, the others will
238 @param opt: a string holding the option name for which we process the
239 data, used in building error messages
241 @param data: a string of the format key=val,key=val,...
243 @return: {key=val, key=val}
244 @raises errors.ParameterError: if there are duplicate keys
250 for elem in data.split(","):
252 key, val = elem.split("=", 1)
254 if elem.startswith(NO_PREFIX):
255 key, val = elem[len(NO_PREFIX):], False
256 elif elem.startswith(UN_PREFIX):
257 key, val = elem[len(UN_PREFIX):], None
259 key, val = elem, True
261 raise errors.ParameterError("Duplicate key '%s' in option %s" %
267 def check_ident_key_val(option, opt, value):
268 """Custom parser for the IdentKeyVal option type.
274 ident, rest = value.split(":", 1)
275 kv_dict = _SplitKeyVal(opt, rest)
276 retval = (ident, kv_dict)
280 class IdentKeyValOption(Option):
281 """Custom option class for ident:key=val,key=val options.
283 This will store the parsed values as a tuple (ident, {key: val}). As
284 such, multiple uses of this option via action=append is possible.
287 TYPES = Option.TYPES + ("identkeyval",)
288 TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER)
289 TYPE_CHECKER["identkeyval"] = check_ident_key_val
292 def check_key_val(option, opt, value):
293 """Custom parser for the KeyVal option type.
296 return _SplitKeyVal(opt, value)
299 class KeyValOption(Option):
300 """Custom option class for key=val,key=val options.
302 This will store the parsed values as a dict {key: val}.
305 TYPES = Option.TYPES + ("keyval",)
306 TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER)
307 TYPE_CHECKER["keyval"] = check_key_val
310 # optparse.py sets make_option, so we do it for our own option class, too
311 cli_option = CliOption
312 ikv_option = IdentKeyValOption
313 keyval_option = KeyValOption
316 def _ParseArgs(argv, commands, aliases):
317 """Parser for the command line arguments.
319 This function parses the arguements and returns the function which
320 must be executed together with its (modified) arguments.
322 @param argv: the command line
323 @param commands: dictionary with special contents, see the design
324 doc for cmdline handling
325 @param aliases: dictionary with command aliases {'alias': 'target, ...}
331 binary = argv[0].split("/")[-1]
333 if len(argv) > 1 and argv[1] == "--version":
334 print "%s (ganeti) %s" % (binary, constants.RELEASE_VERSION)
335 # Quit right away. That way we don't have to care about this special
336 # argument. optparse.py does it the same.
339 if len(argv) < 2 or not (argv[1] in commands or
341 # let's do a nice thing
342 sortedcmds = commands.keys()
344 print ("Usage: %(bin)s {command} [options...] [argument...]"
345 "\n%(bin)s <command> --help to see details, or"
346 " man %(bin)s\n" % {"bin": binary})
347 # compute the max line length for cmd + usage
348 mlen = max([len(" %s" % cmd) for cmd in commands])
349 mlen = min(60, mlen) # should not get here...
350 # and format a nice command list
352 for cmd in sortedcmds:
353 cmdstr = " %s" % (cmd,)
354 help_text = commands[cmd][4]
355 help_lines = textwrap.wrap(help_text, 79-3-mlen)
356 print "%-*s - %s" % (mlen, cmdstr, help_lines.pop(0))
357 for line in help_lines:
358 print "%-*s %s" % (mlen, "", line)
360 return None, None, None
362 # get command, unalias it, and look it up in commands
366 raise errors.ProgrammerError("Alias '%s' overrides an existing"
369 if aliases[cmd] not in commands:
370 raise errors.ProgrammerError("Alias '%s' maps to non-existing"
371 " command '%s'" % (cmd, aliases[cmd]))
375 func, nargs, parser_opts, usage, description = commands[cmd]
376 parser = OptionParser(option_list=parser_opts,
377 description=description,
378 formatter=TitledHelpFormatter(),
379 usage="%%prog %s %s" % (cmd, usage))
380 parser.disable_interspersed_args()
381 options, args = parser.parse_args()
384 print >> sys.stderr, ("Error: Command %s expects no arguments" % cmd)
385 return None, None, None
386 elif nargs < 0 and len(args) != -nargs:
387 print >> sys.stderr, ("Error: Command %s expects %d argument(s)" %
389 return None, None, None
390 elif nargs >= 0 and len(args) < nargs:
391 print >> sys.stderr, ("Error: Command %s expects at least %d argument(s)" %
393 return None, None, None
395 return func, options, args
398 def SplitNodeOption(value):
399 """Splits the value of a --node option.
402 if value and ':' in value:
403 return value.split(':', 1)
408 def ValidateBeParams(bep):
409 """Parse and check the given beparams.
411 The function will update in-place the given dictionary.
414 @param bep: input beparams
415 @raise errors.ParameterError: if the input values are not OK
416 @raise errors.UnitParseError: if the input values are not OK
419 if constants.BE_MEMORY in bep:
420 bep[constants.BE_MEMORY] = utils.ParseUnit(bep[constants.BE_MEMORY])
422 if constants.BE_VCPUS in bep:
424 bep[constants.BE_VCPUS] = int(bep[constants.BE_VCPUS])
426 raise errors.ParameterError("Invalid number of VCPUs")
430 def wrapper(*args, **kwargs):
433 return fn(*args, **kwargs)
439 def AskUser(text, choices=None):
440 """Ask the user a question.
442 @param text: the question to ask
444 @param choices: list with elements tuples (input_char, return_value,
445 description); if not given, it will default to: [('y', True,
446 'Perform the operation'), ('n', False, 'Do no do the operation')];
447 note that the '?' char is reserved for help
449 @return: one of the return values from the choices list; if input is
450 not possible (i.e. not running with a tty, we return the last
455 choices = [('y', True, 'Perform the operation'),
456 ('n', False, 'Do not perform the operation')]
457 if not choices or not isinstance(choices, list):
458 raise errors.ProgrammerError("Invalid choiches argument to AskUser")
459 for entry in choices:
460 if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
461 raise errors.ProgrammerError("Invalid choiches element to AskUser")
463 answer = choices[-1][1]
465 for line in text.splitlines():
466 new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
467 text = "\n".join(new_text)
469 f = file("/dev/tty", "a+")
473 chars = [entry[0] for entry in choices]
474 chars[-1] = "[%s]" % chars[-1]
476 maps = dict([(entry[0], entry[1]) for entry in choices])
480 f.write("/".join(chars))
482 line = f.readline(2).strip().lower()
487 for entry in choices:
488 f.write(" %s - %s\n" % (entry[0], entry[2]))
496 class JobSubmittedException(Exception):
497 """Job was submitted, client should exit.
499 This exception has one argument, the ID of the job that was
500 submitted. The handler should print this ID.
502 This is not an error, just a structured way to exit from clients.
507 def SendJob(ops, cl=None):
508 """Function to submit an opcode without waiting for the results.
511 @param ops: list of opcodes
512 @type cl: luxi.Client
513 @param cl: the luxi client to use for communicating with the master;
514 if None, a new client will be created
520 job_id = cl.SubmitJob(ops)
525 def PollJob(job_id, cl=None, feedback_fn=None):
526 """Function to poll for the result of a job.
528 @type job_id: job identified
529 @param job_id: the job to poll for results
530 @type cl: luxi.Client
531 @param cl: the luxi client to use for communicating with the master;
532 if None, a new client will be created
539 prev_logmsg_serial = None
542 result = cl.WaitForJobChange(job_id, ["status"], prev_job_info,
545 # job not found, go away!
546 raise errors.JobLost("Job with id %s lost" % job_id)
548 # Split result, a tuple of (field values, log entries)
549 (job_info, log_entries) = result
550 (status, ) = job_info
553 for log_entry in log_entries:
554 (serial, timestamp, _, message) = log_entry
555 if callable(feedback_fn):
556 feedback_fn(log_entry[1:])
558 print "%s %s" % (time.ctime(utils.MergeTime(timestamp)), message)
559 prev_logmsg_serial = max(prev_logmsg_serial, serial)
561 # TODO: Handle canceled and archived jobs
562 elif status in (constants.JOB_STATUS_SUCCESS,
563 constants.JOB_STATUS_ERROR,
564 constants.JOB_STATUS_CANCELING,
565 constants.JOB_STATUS_CANCELED):
568 prev_job_info = job_info
570 jobs = cl.QueryJobs([job_id], ["status", "opstatus", "opresult"])
572 raise errors.JobLost("Job with id %s lost" % job_id)
574 status, opstatus, result = jobs[0]
575 if status == constants.JOB_STATUS_SUCCESS:
577 elif status in (constants.JOB_STATUS_CANCELING,
578 constants.JOB_STATUS_CANCELED):
579 raise errors.OpExecError("Job was canceled")
582 for idx, (status, msg) in enumerate(zip(opstatus, result)):
583 if status == constants.OP_STATUS_SUCCESS:
585 elif status == constants.OP_STATUS_ERROR:
587 raise errors.OpExecError("partial failure (opcode %d): %s" %
590 raise errors.OpExecError(str(msg))
591 # default failure mode
592 raise errors.OpExecError(result)
595 def SubmitOpCode(op, cl=None, feedback_fn=None):
596 """Legacy function to submit an opcode.
598 This is just a simple wrapper over the construction of the processor
599 instance. It should be extended to better handle feedback and
600 interaction functions.
606 job_id = SendJob([op], cl)
608 op_results = PollJob(job_id, cl=cl, feedback_fn=feedback_fn)
613 def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
614 """Wrapper around SubmitOpCode or SendJob.
616 This function will decide, based on the 'opts' parameter, whether to
617 submit and wait for the result of the opcode (and return it), or
618 whether to just send the job and print its identifier. It is used in
619 order to simplify the implementation of the '--submit' option.
622 if opts and opts.submit_only:
623 job_id = SendJob([op], cl=cl)
624 raise JobSubmittedException(job_id)
626 return SubmitOpCode(op, cl=cl, feedback_fn=feedback_fn)
630 # TODO: Cache object?
632 client = luxi.Client()
633 except luxi.NoMasterError:
634 master, myself = ssconf.GetMasterAndMyself()
636 raise errors.OpPrereqError("This is not the master node, please connect"
637 " to node '%s' and rerun the command" %
644 def FormatError(err):
645 """Return a formatted error message for a given error.
647 This function takes an exception instance and returns a tuple
648 consisting of two values: first, the recommended exit code, and
649 second, a string describing the error message (not
656 if isinstance(err, errors.ConfigurationError):
657 txt = "Corrupt configuration file: %s" % msg
659 obuf.write(txt + "\n")
660 obuf.write("Aborting.")
662 elif isinstance(err, errors.HooksAbort):
663 obuf.write("Failure: hooks execution failed:\n")
664 for node, script, out in err.args[0]:
666 obuf.write(" node: %s, script: %s, output: %s\n" %
669 obuf.write(" node: %s, script: %s (no output)\n" %
671 elif isinstance(err, errors.HooksFailure):
672 obuf.write("Failure: hooks general failure: %s" % msg)
673 elif isinstance(err, errors.ResolverError):
674 this_host = utils.HostInfo.SysName()
675 if err.args[0] == this_host:
676 msg = "Failure: can't resolve my own hostname ('%s')"
678 msg = "Failure: can't resolve hostname '%s'"
679 obuf.write(msg % err.args[0])
680 elif isinstance(err, errors.OpPrereqError):
681 obuf.write("Failure: prerequisites not met for this"
682 " operation:\n%s" % msg)
683 elif isinstance(err, errors.OpExecError):
684 obuf.write("Failure: command execution error:\n%s" % msg)
685 elif isinstance(err, errors.TagError):
686 obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
687 elif isinstance(err, errors.JobQueueDrainError):
688 obuf.write("Failure: the job queue is marked for drain and doesn't"
689 " accept new requests\n")
690 elif isinstance(err, errors.JobQueueFull):
691 obuf.write("Failure: the job queue is full and doesn't accept new"
692 " job submissions until old jobs are archived\n")
693 elif isinstance(err, errors.GenericError):
694 obuf.write("Unhandled Ganeti error: %s" % msg)
695 elif isinstance(err, luxi.NoMasterError):
696 obuf.write("Cannot communicate with the master daemon.\nIs it running"
697 " and listening for connections?")
698 elif isinstance(err, luxi.TimeoutError):
699 obuf.write("Timeout while talking to the master daemon. Error:\n"
701 elif isinstance(err, luxi.ProtocolError):
702 obuf.write("Unhandled protocol error while talking to the master daemon:\n"
704 elif isinstance(err, JobSubmittedException):
705 obuf.write("JobID: %s\n" % err.args[0])
708 obuf.write("Unhandled exception: %s" % msg)
709 return retcode, obuf.getvalue().rstrip('\n')
712 def GenericMain(commands, override=None, aliases=None):
713 """Generic main function for all the gnt-* commands.
716 - commands: a dictionary with a special structure, see the design doc
717 for command line handling.
718 - override: if not None, we expect a dictionary with keys that will
719 override command line options; this can be used to pass
720 options from the scripts to generic functions
721 - aliases: dictionary with command aliases {'alias': 'target, ...}
724 # save the program name and the entire command line for later logging
726 binary = os.path.basename(sys.argv[0]) or sys.argv[0]
727 if len(sys.argv) >= 2:
728 binary += " " + sys.argv[1]
729 old_cmdline = " ".join(sys.argv[2:])
733 binary = "<unknown program>"
739 func, options, args = _ParseArgs(sys.argv, commands, aliases)
740 if func is None: # parse error
743 if override is not None:
744 for key, val in override.iteritems():
745 setattr(options, key, val)
747 utils.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
748 stderr_logging=True, program=binary)
750 utils.debug = options.debug
753 logging.info("run with arguments '%s'", old_cmdline)
755 logging.info("run with no arguments")
758 result = func(options, args)
759 except (errors.GenericError, luxi.ProtocolError,
760 JobSubmittedException), err:
761 result, err_msg = FormatError(err)
762 logging.exception("Error durring command processing")
768 def GenerateTable(headers, fields, separator, data,
769 numfields=None, unitfields=None,
771 """Prints a table with headers and different fields.
774 @param headers: dictionary mapping field names to headers for
777 @param fields: the field names corresponding to each row in
779 @param separator: the separator to be used; if this is None,
780 the default 'smart' algorithm is used which computes optimal
781 field width, otherwise just the separator is used between
784 @param data: a list of lists, each sublist being one row to be output
785 @type numfields: list
786 @param numfields: a list with the fields that hold numeric
787 values and thus should be right-aligned
788 @type unitfields: list
789 @param unitfields: a list with the fields that hold numeric
790 values that should be formatted with the units field
791 @type units: string or None
792 @param units: the units we should use for formatting, or None for
793 automatic choice (human-readable for non-separator usage, otherwise
794 megabytes); this is a one-letter string
803 if numfields is None:
805 if unitfields is None:
808 numfields = utils.FieldSet(*numfields)
809 unitfields = utils.FieldSet(*unitfields)
813 if headers and field not in headers:
814 # FIXME: handle better unknown fields (either revert to old
815 # style of raising exception, or deal more intelligently with
817 headers[field] = field
818 if separator is not None:
819 format_fields.append("%s")
820 elif numfields.Matches(field):
821 format_fields.append("%*s")
823 format_fields.append("%-*s")
825 if separator is None:
826 mlens = [0 for name in fields]
827 format = ' '.join(format_fields)
829 format = separator.replace("%", "%%").join(format_fields)
832 for idx, val in enumerate(row):
833 if unitfields.Matches(fields[idx]):
839 val = row[idx] = utils.FormatUnit(val, units)
840 val = row[idx] = str(val)
841 if separator is None:
842 mlens[idx] = max(mlens[idx], len(val))
847 for idx, name in enumerate(fields):
849 if separator is None:
850 mlens[idx] = max(mlens[idx], len(hdr))
851 args.append(mlens[idx])
853 result.append(format % tuple(args))
857 for idx in xrange(len(fields)):
858 if separator is None:
859 args.append(mlens[idx])
860 args.append(line[idx])
861 result.append(format % tuple(args))
866 def FormatTimestamp(ts):
867 """Formats a given timestamp.
870 @param ts: a timeval-type timestamp, a tuple of seconds and microseconds
873 @returns: a string with the formatted timestamp
876 if not isinstance (ts, (tuple, list)) or len(ts) != 2:
879 return time.strftime("%F %T", time.localtime(sec)) + ".%06d" % usec
882 def ParseTimespec(value):
883 """Parse a time specification.
885 The following suffixed will be recognized:
893 Without any suffix, the value will be taken to be in seconds.
898 raise errors.OpPrereqError("Empty time specification passed")
906 if value[-1] not in suffix_map:
910 raise errors.OpPrereqError("Invalid time specification '%s'" % value)
912 multiplier = suffix_map[value[-1]]
914 if not value: # no data left after stripping the suffix
915 raise errors.OpPrereqError("Invalid time specification (only"
918 value = int(value) * multiplier
920 raise errors.OpPrereqError("Invalid time specification '%s'" % value)
924 def _ToStream(stream, txt, *args):
925 """Write a message to a stream, bypassing the logging system
927 @type stream: file object
928 @param stream: the file to which we should write
930 @param txt: the message
935 stream.write(txt % args)
942 def ToStdout(txt, *args):
943 """Write a message to stdout only, bypassing the logging system
945 This is just a wrapper over _ToStream.
948 @param txt: the message
951 _ToStream(sys.stdout, txt, *args)
954 def ToStderr(txt, *args):
955 """Write a message to stderr only, bypassing the logging system
957 This is just a wrapper over _ToStream.
960 @param txt: the message
963 _ToStream(sys.stderr, txt, *args)