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"""
30 from cStringIO import StringIO
32 from ganeti import utils
33 from ganeti import logger
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
40 from optparse import (OptionParser, make_option, TitledHelpFormatter,
41 Option, OptionValueError)
43 __all__ = ["DEBUG_OPT", "NOHDR_OPT", "SEP_OPT", "GenericMain",
44 "SubmitOpCode", "GetClient",
45 "cli_option", "ikv_option", "keyval_option",
46 "GenerateTable", "AskUser",
47 "ARGS_NONE", "ARGS_FIXED", "ARGS_ATLEAST", "ARGS_ANY", "ARGS_ONE",
48 "USEUNITS_OPT", "FIELDS_OPT", "FORCE_OPT", "SUBMIT_OPT",
49 "ListTags", "AddTags", "RemoveTags", "TAG_SRC_OPT",
50 "FormatError", "SplitNodeOption", "SubmitOrSend",
51 "JobSubmittedException", "FormatTimestamp", "ParseTimespec",
55 def _ExtractTagsObject(opts, args):
56 """Extract the tag type object.
58 Note that this function will modify its args parameter.
61 if not hasattr(opts, "tag_type"):
62 raise errors.ProgrammerError("tag_type not passed to _ExtractTagsObject")
64 if kind == constants.TAG_CLUSTER:
66 elif kind == constants.TAG_NODE or kind == constants.TAG_INSTANCE:
68 raise errors.OpPrereqError("no arguments passed to the command")
72 raise errors.ProgrammerError("Unhandled tag type '%s'" % kind)
76 def _ExtendTags(opts, args):
77 """Extend the args if a source file has been given.
79 This function will extend the tags with the contents of the file
80 passed in the 'tags_source' attribute of the opts parameter. A file
81 named '-' will be replaced by stdin.
84 fname = opts.tags_source
90 new_fh = open(fname, "r")
93 # we don't use the nice 'new_data = [line.strip() for line in fh]'
94 # because of python bug 1633941
96 line = new_fh.readline()
99 new_data.append(line.strip())
102 args.extend(new_data)
105 def ListTags(opts, args):
106 """List the tags on a given object.
108 This is a generic implementation that knows how to deal with all
109 three cases of tag objects (cluster, node, instance). The opts
110 argument is expected to contain a tag_type field denoting what
111 object type we work on.
114 kind, name = _ExtractTagsObject(opts, args)
115 op = opcodes.OpGetTags(kind=kind, name=name)
116 result = SubmitOpCode(op)
117 result = list(result)
123 def AddTags(opts, args):
124 """Add tags on a given object.
126 This is a generic implementation that knows how to deal with all
127 three cases of tag objects (cluster, node, instance). The opts
128 argument is expected to contain a tag_type field denoting what
129 object type we work on.
132 kind, name = _ExtractTagsObject(opts, args)
133 _ExtendTags(opts, args)
135 raise errors.OpPrereqError("No tags to be added")
136 op = opcodes.OpAddTags(kind=kind, name=name, tags=args)
140 def RemoveTags(opts, args):
141 """Remove tags from a given object.
143 This is a generic implementation that knows how to deal with all
144 three cases of tag objects (cluster, node, instance). The opts
145 argument is expected to contain a tag_type field denoting what
146 object type we work on.
149 kind, name = _ExtractTagsObject(opts, args)
150 _ExtendTags(opts, args)
152 raise errors.OpPrereqError("No tags to be removed")
153 op = opcodes.OpDelTags(kind=kind, name=name, tags=args)
157 DEBUG_OPT = make_option("-d", "--debug", default=False,
159 help="Turn debugging on")
161 NOHDR_OPT = make_option("--no-headers", default=False,
162 action="store_true", dest="no_headers",
163 help="Don't display column headers")
165 SEP_OPT = make_option("--separator", default=None,
166 action="store", dest="separator",
167 help="Separator between output fields"
168 " (defaults to one space)")
170 USEUNITS_OPT = make_option("--human-readable", default=False,
171 action="store_true", dest="human_readable",
172 help="Print sizes in human readable format")
174 FIELDS_OPT = make_option("-o", "--output", dest="output", action="store",
175 type="string", help="Comma separated list of"
179 FORCE_OPT = make_option("-f", "--force", dest="force", action="store_true",
180 default=False, help="Force the operation")
182 TAG_SRC_OPT = make_option("--from", dest="tags_source",
183 default=None, help="File with tag names")
185 SUBMIT_OPT = make_option("--submit", dest="submit_only",
186 default=False, action="store_true",
187 help="Submit the job and return the job ID, but"
188 " don't wait for the job to finish")
192 """Macro-like function denoting a fixed number of arguments"""
196 def ARGS_ATLEAST(val):
197 """Macro-like function denoting a minimum number of arguments"""
202 ARGS_ONE = ARGS_FIXED(1)
203 ARGS_ANY = ARGS_ATLEAST(0)
206 def check_unit(option, opt, value):
207 """OptParsers custom converter for units.
211 return utils.ParseUnit(value)
212 except errors.UnitParseError, err:
213 raise OptionValueError("option %s: %s" % (opt, err))
216 class CliOption(Option):
217 """Custom option class for optparse.
220 TYPES = Option.TYPES + ("unit",)
221 TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER)
222 TYPE_CHECKER["unit"] = check_unit
225 def _SplitKeyVal(opt, data):
226 """Convert a KeyVal string into a dict.
228 This function will convert a key=val[,...] string into a dict. Empty
229 values will be converted specially: keys which have the prefix 'no_'
230 will have the value=False and the prefix stripped, the others will
234 @param opt: a string holding the option name for which we process the
235 data, used in building error messages
237 @param data: a string of the format key=val,key=val,...
239 @return: {key=val, key=val}
240 @raises errors.ParameterError: if there are duplicate keys
245 for elem in data.split(","):
247 key, val = elem.split("=", 1)
249 if elem.startswith(NO_PREFIX):
250 key, val = elem[len(NO_PREFIX):], False
252 key, val = elem, True
254 raise errors.ParameterError("Duplicate key '%s' in option %s" %
260 def check_ident_key_val(option, opt, value):
261 """Custom parser for the IdentKeyVal option type.
267 ident, rest = value.split(":", 1)
268 kv_dict = _SplitKeyVal(opt, rest)
269 retval = (ident, kv_dict)
273 class IdentKeyValOption(Option):
274 """Custom option class for ident:key=val,key=val options.
276 This will store the parsed values as a tuple (ident, {key: val}). As
277 such, multiple uses of this option via action=append is possible.
280 TYPES = Option.TYPES + ("identkeyval",)
281 TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER)
282 TYPE_CHECKER["identkeyval"] = check_ident_key_val
285 def check_key_val(option, opt, value):
286 """Custom parser for the KeyVal option type.
289 return _SplitKeyVal(opt, value)
292 class KeyValOption(Option):
293 """Custom option class for key=val,key=val options.
295 This will store the parsed values as a dict {key: val}.
298 TYPES = Option.TYPES + ("keyval",)
299 TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER)
300 TYPE_CHECKER["keyval"] = check_key_val
303 # optparse.py sets make_option, so we do it for our own option class, too
304 cli_option = CliOption
305 ikv_option = IdentKeyValOption
306 keyval_option = KeyValOption
309 def _ParseArgs(argv, commands, aliases):
310 """Parses the command line and return the function which must be
311 executed together with its arguments
314 argv: the command line
316 commands: dictionary with special contents, see the design doc for
318 aliases: dictionary with command aliases {'alias': 'target, ...}
324 binary = argv[0].split("/")[-1]
326 if len(argv) > 1 and argv[1] == "--version":
327 print "%s (ganeti) %s" % (binary, constants.RELEASE_VERSION)
328 # Quit right away. That way we don't have to care about this special
329 # argument. optparse.py does it the same.
332 if len(argv) < 2 or not (argv[1] in commands or
334 # let's do a nice thing
335 sortedcmds = commands.keys()
337 print ("Usage: %(bin)s {command} [options...] [argument...]"
338 "\n%(bin)s <command> --help to see details, or"
339 " man %(bin)s\n" % {"bin": binary})
340 # compute the max line length for cmd + usage
341 mlen = max([len(" %s" % cmd) for cmd in commands])
342 mlen = min(60, mlen) # should not get here...
343 # and format a nice command list
345 for cmd in sortedcmds:
346 cmdstr = " %s" % (cmd,)
347 help_text = commands[cmd][4]
348 help_lines = textwrap.wrap(help_text, 79-3-mlen)
349 print "%-*s - %s" % (mlen, cmdstr, help_lines.pop(0))
350 for line in help_lines:
351 print "%-*s %s" % (mlen, "", line)
353 return None, None, None
355 # get command, unalias it, and look it up in commands
359 raise errors.ProgrammerError("Alias '%s' overrides an existing"
362 if aliases[cmd] not in commands:
363 raise errors.ProgrammerError("Alias '%s' maps to non-existing"
364 " command '%s'" % (cmd, aliases[cmd]))
368 func, nargs, parser_opts, usage, description = commands[cmd]
369 parser = OptionParser(option_list=parser_opts,
370 description=description,
371 formatter=TitledHelpFormatter(),
372 usage="%%prog %s %s" % (cmd, usage))
373 parser.disable_interspersed_args()
374 options, args = parser.parse_args()
377 print >> sys.stderr, ("Error: Command %s expects no arguments" % cmd)
378 return None, None, None
379 elif nargs < 0 and len(args) != -nargs:
380 print >> sys.stderr, ("Error: Command %s expects %d argument(s)" %
382 return None, None, None
383 elif nargs >= 0 and len(args) < nargs:
384 print >> sys.stderr, ("Error: Command %s expects at least %d argument(s)" %
386 return None, None, None
388 return func, options, args
391 def SplitNodeOption(value):
392 """Splits the value of a --node option.
395 if value and ':' in value:
396 return value.split(':', 1)
401 def AskUser(text, choices=None):
402 """Ask the user a question.
405 text - the question to ask.
407 choices - list with elements tuples (input_char, return_value,
408 description); if not given, it will default to: [('y', True,
409 'Perform the operation'), ('n', False, 'Do no do the operation')];
410 note that the '?' char is reserved for help
412 Returns: one of the return values from the choices list; if input is
413 not possible (i.e. not running with a tty, we return the last entry
418 choices = [('y', True, 'Perform the operation'),
419 ('n', False, 'Do not perform the operation')]
420 if not choices or not isinstance(choices, list):
421 raise errors.ProgrammerError("Invalid choiches argument to AskUser")
422 for entry in choices:
423 if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
424 raise errors.ProgrammerError("Invalid choiches element to AskUser")
426 answer = choices[-1][1]
428 for line in text.splitlines():
429 new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
430 text = "\n".join(new_text)
432 f = file("/dev/tty", "a+")
436 chars = [entry[0] for entry in choices]
437 chars[-1] = "[%s]" % chars[-1]
439 maps = dict([(entry[0], entry[1]) for entry in choices])
443 f.write("/".join(chars))
445 line = f.readline(2).strip().lower()
450 for entry in choices:
451 f.write(" %s - %s\n" % (entry[0], entry[2]))
459 class JobSubmittedException(Exception):
460 """Job was submitted, client should exit.
462 This exception has one argument, the ID of the job that was
463 submitted. The handler should print this ID.
465 This is not an error, just a structured way to exit from clients.
470 def SendJob(ops, cl=None):
471 """Function to submit an opcode without waiting for the results.
474 @param ops: list of opcodes
475 @type cl: luxi.Client
476 @param cl: the luxi client to use for communicating with the master;
477 if None, a new client will be created
483 job_id = cl.SubmitJob(ops)
488 def PollJob(job_id, cl=None, feedback_fn=None):
489 """Function to poll for the result of a job.
491 @type job_id: job identified
492 @param job_id: the job to poll for results
493 @type cl: luxi.Client
494 @param cl: the luxi client to use for communicating with the master;
495 if None, a new client will be created
502 prev_logmsg_serial = None
505 result = cl.WaitForJobChange(job_id, ["status"], prev_job_info,
508 # job not found, go away!
509 raise errors.JobLost("Job with id %s lost" % job_id)
511 # Split result, a tuple of (field values, log entries)
512 (job_info, log_entries) = result
513 (status, ) = job_info
516 for log_entry in log_entries:
517 (serial, timestamp, _, message) = log_entry
518 if callable(feedback_fn):
519 feedback_fn(log_entry[1:])
521 print "%s %s" % (time.ctime(utils.MergeTime(timestamp)), message)
522 prev_logmsg_serial = max(prev_logmsg_serial, serial)
524 # TODO: Handle canceled and archived jobs
525 elif status in (constants.JOB_STATUS_SUCCESS, constants.JOB_STATUS_ERROR):
528 prev_job_info = job_info
530 jobs = cl.QueryJobs([job_id], ["status", "opresult"])
532 raise errors.JobLost("Job with id %s lost" % job_id)
534 status, result = jobs[0]
535 if status == constants.JOB_STATUS_SUCCESS:
538 raise errors.OpExecError(result)
541 def SubmitOpCode(op, cl=None, feedback_fn=None):
542 """Legacy function to submit an opcode.
544 This is just a simple wrapper over the construction of the processor
545 instance. It should be extended to better handle feedback and
546 interaction functions.
552 job_id = SendJob([op], cl)
554 op_results = PollJob(job_id, cl=cl, feedback_fn=feedback_fn)
559 def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
560 """Wrapper around SubmitOpCode or SendJob.
562 This function will decide, based on the 'opts' parameter, whether to
563 submit and wait for the result of the opcode (and return it), or
564 whether to just send the job and print its identifier. It is used in
565 order to simplify the implementation of the '--submit' option.
568 if opts and opts.submit_only:
569 job_id = SendJob([op], cl=cl)
570 raise JobSubmittedException(job_id)
572 return SubmitOpCode(op, cl=cl, feedback_fn=feedback_fn)
576 # TODO: Cache object?
578 client = luxi.Client()
579 except luxi.NoMasterError:
580 master, myself = ssconf.GetMasterAndMyself()
582 raise errors.OpPrereqError("This is not the master node, please connect"
583 " to node '%s' and rerun the command" %
590 def FormatError(err):
591 """Return a formatted error message for a given error.
593 This function takes an exception instance and returns a tuple
594 consisting of two values: first, the recommended exit code, and
595 second, a string describing the error message (not
602 if isinstance(err, errors.ConfigurationError):
603 txt = "Corrupt configuration file: %s" % msg
605 obuf.write(txt + "\n")
606 obuf.write("Aborting.")
608 elif isinstance(err, errors.HooksAbort):
609 obuf.write("Failure: hooks execution failed:\n")
610 for node, script, out in err.args[0]:
612 obuf.write(" node: %s, script: %s, output: %s\n" %
615 obuf.write(" node: %s, script: %s (no output)\n" %
617 elif isinstance(err, errors.HooksFailure):
618 obuf.write("Failure: hooks general failure: %s" % msg)
619 elif isinstance(err, errors.ResolverError):
620 this_host = utils.HostInfo.SysName()
621 if err.args[0] == this_host:
622 msg = "Failure: can't resolve my own hostname ('%s')"
624 msg = "Failure: can't resolve hostname '%s'"
625 obuf.write(msg % err.args[0])
626 elif isinstance(err, errors.OpPrereqError):
627 obuf.write("Failure: prerequisites not met for this"
628 " operation:\n%s" % msg)
629 elif isinstance(err, errors.OpExecError):
630 obuf.write("Failure: command execution error:\n%s" % msg)
631 elif isinstance(err, errors.TagError):
632 obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
633 elif isinstance(err, errors.GenericError):
634 obuf.write("Unhandled Ganeti error: %s" % msg)
635 elif isinstance(err, luxi.NoMasterError):
636 obuf.write("Cannot communicate with the master daemon.\nIs it running"
637 " and listening for connections?")
638 elif isinstance(err, luxi.TimeoutError):
639 obuf.write("Timeout while talking to the master daemon. Error:\n"
641 elif isinstance(err, luxi.ProtocolError):
642 obuf.write("Unhandled protocol error while talking to the master daemon:\n"
644 elif isinstance(err, JobSubmittedException):
645 obuf.write("JobID: %s\n" % err.args[0])
648 obuf.write("Unhandled exception: %s" % msg)
649 return retcode, obuf.getvalue().rstrip('\n')
652 def GenericMain(commands, override=None, aliases=None):
653 """Generic main function for all the gnt-* commands.
656 - commands: a dictionary with a special structure, see the design doc
657 for command line handling.
658 - override: if not None, we expect a dictionary with keys that will
659 override command line options; this can be used to pass
660 options from the scripts to generic functions
661 - aliases: dictionary with command aliases {'alias': 'target, ...}
664 # save the program name and the entire command line for later logging
666 binary = os.path.basename(sys.argv[0]) or sys.argv[0]
667 if len(sys.argv) >= 2:
668 binary += " " + sys.argv[1]
669 old_cmdline = " ".join(sys.argv[2:])
673 binary = "<unknown program>"
679 func, options, args = _ParseArgs(sys.argv, commands, aliases)
680 if func is None: # parse error
683 if override is not None:
684 for key, val in override.iteritems():
685 setattr(options, key, val)
687 logger.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
688 stderr_logging=True, program=binary)
690 utils.debug = options.debug
693 logger.Info("run with arguments '%s'" % old_cmdline)
695 logger.Info("run with no arguments")
698 result = func(options, args)
699 except (errors.GenericError, luxi.ProtocolError), err:
700 result, err_msg = FormatError(err)
701 logger.ToStderr(err_msg)
706 def GenerateTable(headers, fields, separator, data,
707 numfields=None, unitfields=None):
708 """Prints a table with headers and different fields.
711 headers: Dict of header titles or None if no headers should be shown
712 fields: List of fields to show
713 separator: String used to separate fields or None for spaces
714 data: Data to be printed
715 numfields: List of fields to be aligned to right
716 unitfields: List of fields to be formatted as units
719 if numfields is None:
721 if unitfields is None:
726 if headers and field not in headers:
727 raise errors.ProgrammerError("Missing header description for field '%s'"
729 if separator is not None:
730 format_fields.append("%s")
731 elif field in numfields:
732 format_fields.append("%*s")
734 format_fields.append("%-*s")
736 if separator is None:
737 mlens = [0 for name in fields]
738 format = ' '.join(format_fields)
740 format = separator.replace("%", "%%").join(format_fields)
743 for idx, val in enumerate(row):
744 if fields[idx] in unitfields:
750 val = row[idx] = utils.FormatUnit(val)
751 val = row[idx] = str(val)
752 if separator is None:
753 mlens[idx] = max(mlens[idx], len(val))
758 for idx, name in enumerate(fields):
760 if separator is None:
761 mlens[idx] = max(mlens[idx], len(hdr))
762 args.append(mlens[idx])
764 result.append(format % tuple(args))
768 for idx in xrange(len(fields)):
769 if separator is None:
770 args.append(mlens[idx])
771 args.append(line[idx])
772 result.append(format % tuple(args))
777 def FormatTimestamp(ts):
778 """Formats a given timestamp.
781 @param ts: a timeval-type timestamp, a tuple of seconds and microseconds
784 @returns: a string with the formatted timestamp
787 if not isinstance (ts, (tuple, list)) or len(ts) != 2:
790 return time.strftime("%F %T", time.localtime(sec)) + ".%06d" % usec
793 def ParseTimespec(value):
794 """Parse a time specification.
796 The following suffixed will be recognized:
804 Without any suffix, the value will be taken to be in seconds.
809 raise errors.OpPrereqError("Empty time specification passed")
817 if value[-1] not in suffix_map:
821 raise errors.OpPrereqError("Invalid time specification '%s'" % value)
823 multiplier = suffix_map[value[-1]]
825 if not value: # no data left after stripping the suffix
826 raise errors.OpPrereqError("Invalid time specification (only"
829 value = int(value) * multiplier
831 raise errors.OpPrereqError("Invalid time specification '%s'" % value)