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"""
29 from cStringIO import StringIO
31 from ganeti import utils
32 from ganeti import logger
33 from ganeti import errors
34 from ganeti import mcpu
35 from ganeti import constants
36 from ganeti import opcodes
38 from optparse import (OptionParser, make_option, TitledHelpFormatter,
39 Option, OptionValueError, SUPPRESS_HELP)
41 __all__ = ["DEBUG_OPT", "NOHDR_OPT", "SEP_OPT", "GenericMain", "SubmitOpCode",
42 "cli_option", "GenerateTable", "AskUser",
43 "ARGS_NONE", "ARGS_FIXED", "ARGS_ATLEAST", "ARGS_ANY", "ARGS_ONE",
44 "USEUNITS_OPT", "FIELDS_OPT", "FORCE_OPT",
45 "ListTags", "AddTags", "RemoveTags", "TAG_SRC_OPT",
46 "FormatError", "SplitNodeOption"
50 def _ExtractTagsObject(opts, args):
51 """Extract the tag type object.
53 Note that this function will modify its args parameter.
56 if not hasattr(opts, "tag_type"):
57 raise errors.ProgrammerError("tag_type not passed to _ExtractTagsObject")
59 if kind == constants.TAG_CLUSTER:
61 elif kind == constants.TAG_NODE or kind == constants.TAG_INSTANCE:
63 raise errors.OpPrereqError("no arguments passed to the command")
67 raise errors.ProgrammerError("Unhandled tag type '%s'" % kind)
71 def _ExtendTags(opts, args):
72 """Extend the args if a source file has been given.
74 This function will extend the tags with the contents of the file
75 passed in the 'tags_source' attribute of the opts parameter. A file
76 named '-' will be replaced by stdin.
79 fname = opts.tags_source
85 new_fh = open(fname, "r")
88 # we don't use the nice 'new_data = [line.strip() for line in fh]'
89 # because of python bug 1633941
91 line = new_fh.readline()
94 new_data.append(line.strip())
100 def ListTags(opts, args):
101 """List the tags on a given object.
103 This is a generic implementation that knows how to deal with all
104 three cases of tag objects (cluster, node, instance). The opts
105 argument is expected to contain a tag_type field denoting what
106 object type we work on.
109 kind, name = _ExtractTagsObject(opts, args)
110 op = opcodes.OpGetTags(kind=kind, name=name)
111 result = SubmitOpCode(op)
112 result = list(result)
118 def AddTags(opts, args):
119 """Add tags on a given object.
121 This is a generic implementation that knows how to deal with all
122 three cases of tag objects (cluster, node, instance). The opts
123 argument is expected to contain a tag_type field denoting what
124 object type we work on.
127 kind, name = _ExtractTagsObject(opts, args)
128 _ExtendTags(opts, args)
130 raise errors.OpPrereqError("No tags to be added")
131 op = opcodes.OpAddTags(kind=kind, name=name, tags=args)
135 def RemoveTags(opts, args):
136 """Remove tags from a given object.
138 This is a generic implementation that knows how to deal with all
139 three cases of tag objects (cluster, node, instance). The opts
140 argument is expected to contain a tag_type field denoting what
141 object type we work on.
144 kind, name = _ExtractTagsObject(opts, args)
145 _ExtendTags(opts, args)
147 raise errors.OpPrereqError("No tags to be removed")
148 op = opcodes.OpDelTags(kind=kind, name=name, tags=args)
152 DEBUG_OPT = make_option("-d", "--debug", default=False,
154 help="Turn debugging on")
156 NOHDR_OPT = make_option("--no-headers", default=False,
157 action="store_true", dest="no_headers",
158 help="Don't display column headers")
160 SEP_OPT = make_option("--separator", default=None,
161 action="store", dest="separator",
162 help="Separator between output fields"
163 " (defaults to one space)")
165 USEUNITS_OPT = make_option("--human-readable", default=False,
166 action="store_true", dest="human_readable",
167 help="Print sizes in human readable format")
169 FIELDS_OPT = make_option("-o", "--output", dest="output", action="store",
170 type="string", help="Comma separated list of"
174 FORCE_OPT = make_option("-f", "--force", dest="force", action="store_true",
175 default=False, help="Force the operation")
177 _LOCK_OPT = make_option("--lock-retries", default=None,
178 type="int", help=SUPPRESS_HELP)
180 _LOCK_NOAUTOCLEAN = make_option("--lock-noautoclean", default=False,
181 action="store_true", help=SUPPRESS_HELP)
183 TAG_SRC_OPT = make_option("--from", dest="tags_source",
184 default=None, help="File with tag names")
188 """Macro-like function denoting a fixed number of arguments"""
192 def ARGS_ATLEAST(val):
193 """Macro-like function denoting a minimum number of arguments"""
198 ARGS_ONE = ARGS_FIXED(1)
199 ARGS_ANY = ARGS_ATLEAST(0)
202 def check_unit(option, opt, value):
203 """OptParsers custom converter for units.
207 return utils.ParseUnit(value)
208 except errors.UnitParseError, err:
209 raise OptionValueError("option %s: %s" % (opt, err))
212 class CliOption(Option):
213 """Custom option class for optparse.
216 TYPES = Option.TYPES + ("unit",)
217 TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER)
218 TYPE_CHECKER["unit"] = check_unit
221 # optparse.py sets make_option, so we do it for our own option class, too
222 cli_option = CliOption
225 def _ParseArgs(argv, commands, aliases):
226 """Parses the command line and return the function which must be
227 executed together with its arguments
230 argv: the command line
232 commands: dictionary with special contents, see the design doc for
234 aliases: dictionary with command aliases {'alias': 'target, ...}
240 binary = argv[0].split("/")[-1]
242 if len(argv) > 1 and argv[1] == "--version":
243 print "%s (ganeti) %s" % (binary, constants.RELEASE_VERSION)
244 # Quit right away. That way we don't have to care about this special
245 # argument. optparse.py does it the same.
248 if len(argv) < 2 or not (argv[1] in commands or
250 # let's do a nice thing
251 sortedcmds = commands.keys()
253 print ("Usage: %(bin)s {command} [options...] [argument...]"
254 "\n%(bin)s <command> --help to see details, or"
255 " man %(bin)s\n" % {"bin": binary})
256 # compute the max line length for cmd + usage
257 mlen = max([len(" %s %s" % (cmd, commands[cmd][3])) for cmd in commands])
258 mlen = min(60, mlen) # should not get here...
259 # and format a nice command list
261 for cmd in sortedcmds:
262 cmdstr = " %s %s" % (cmd, commands[cmd][3])
263 help_text = commands[cmd][4]
264 help_lines = textwrap.wrap(help_text, 79-3-mlen)
265 print "%-*s - %s" % (mlen, cmdstr,
267 for line in help_lines:
268 print "%-*s %s" % (mlen, "", line)
270 return None, None, None
272 # get command, unalias it, and look it up in commands
276 raise errors.ProgrammerError("Alias '%s' overrides an existing"
279 if aliases[cmd] not in commands:
280 raise errors.ProgrammerError("Alias '%s' maps to non-existing"
281 " command '%s'" % (cmd, aliases[cmd]))
285 func, nargs, parser_opts, usage, description = commands[cmd]
286 parser_opts.append(_LOCK_OPT)
287 parser_opts.append(_LOCK_NOAUTOCLEAN)
288 parser = OptionParser(option_list=parser_opts,
289 description=description,
290 formatter=TitledHelpFormatter(),
291 usage="%%prog %s %s" % (cmd, usage))
292 parser.disable_interspersed_args()
293 options, args = parser.parse_args()
296 print >> sys.stderr, ("Error: Command %s expects no arguments" % cmd)
297 return None, None, None
298 elif nargs < 0 and len(args) != -nargs:
299 print >> sys.stderr, ("Error: Command %s expects %d argument(s)" %
301 return None, None, None
302 elif nargs >= 0 and len(args) < nargs:
303 print >> sys.stderr, ("Error: Command %s expects at least %d argument(s)" %
305 return None, None, None
307 return func, options, args
310 def SplitNodeOption(value):
311 """Splits the value of a --node option.
314 if value and ':' in value:
315 return value.split(':', 1)
320 def AskUser(text, choices=None):
321 """Ask the user a question.
324 text - the question to ask.
326 choices - list with elements tuples (input_char, return_value,
327 description); if not given, it will default to: [('y', True,
328 'Perform the operation'), ('n', False, 'Do no do the operation')];
329 note that the '?' char is reserved for help
331 Returns: one of the return values from the choices list; if input is
332 not possible (i.e. not running with a tty, we return the last entry
337 choices = [('y', True, 'Perform the operation'),
338 ('n', False, 'Do not perform the operation')]
339 if not choices or not isinstance(choices, list):
340 raise errors.ProgrammerError("Invalid choiches argument to AskUser")
341 for entry in choices:
342 if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
343 raise errors.ProgrammerError("Invalid choiches element to AskUser")
345 answer = choices[-1][1]
347 for line in text.splitlines():
348 new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
349 text = "\n".join(new_text)
351 f = file("/dev/tty", "a+")
355 chars = [entry[0] for entry in choices]
356 chars[-1] = "[%s]" % chars[-1]
358 maps = dict([(entry[0], entry[1]) for entry in choices])
362 f.write("/".join(chars))
364 line = f.readline(2).strip().lower()
369 for entry in choices:
370 f.write(" %s - %s\n" % (entry[0], entry[2]))
378 def SubmitOpCode(op, proc=None, feedback_fn=None):
379 """Function to submit an opcode.
381 This is just a simple wrapper over the construction of the processor
382 instance. It should be extended to better handle feedback and
383 interaction functions.
386 if feedback_fn is None:
387 feedback_fn = logger.ToStdout
389 proc = mcpu.Processor(feedback=feedback_fn)
390 return proc.ExecOpCode(op)
393 def FormatError(err):
394 """Return a formatted error message for a given error.
396 This function takes an exception instance and returns a tuple
397 consisting of two values: first, the recommended exit code, and
398 second, a string describing the error message (not
405 if isinstance(err, errors.ConfigurationError):
406 txt = "Corrupt configuration file: %s" % msg
408 obuf.write(txt + "\n")
409 obuf.write("Aborting.")
411 elif isinstance(err, errors.HooksAbort):
412 obuf.write("Failure: hooks execution failed:\n")
413 for node, script, out in err.args[0]:
415 obuf.write(" node: %s, script: %s, output: %s\n" %
418 obuf.write(" node: %s, script: %s (no output)\n" %
420 elif isinstance(err, errors.HooksFailure):
421 obuf.write("Failure: hooks general failure: %s" % msg)
422 elif isinstance(err, errors.ResolverError):
423 this_host = utils.HostInfo.SysName()
424 if err.args[0] == this_host:
425 msg = "Failure: can't resolve my own hostname ('%s')"
427 msg = "Failure: can't resolve hostname '%s'"
428 obuf.write(msg % err.args[0])
429 elif isinstance(err, errors.OpPrereqError):
430 obuf.write("Failure: prerequisites not met for this"
431 " operation:\n%s" % msg)
432 elif isinstance(err, errors.OpExecError):
433 obuf.write("Failure: command execution error:\n%s" % msg)
434 elif isinstance(err, errors.TagError):
435 obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
436 elif isinstance(err, errors.GenericError):
437 obuf.write("Unhandled Ganeti error: %s" % msg)
439 obuf.write("Unhandled exception: %s" % msg)
440 return retcode, obuf.getvalue().rstrip('\n')
443 def GenericMain(commands, override=None, aliases=None):
444 """Generic main function for all the gnt-* commands.
447 - commands: a dictionary with a special structure, see the design doc
448 for command line handling.
449 - override: if not None, we expect a dictionary with keys that will
450 override command line options; this can be used to pass
451 options from the scripts to generic functions
452 - aliases: dictionary with command aliases {'alias': 'target, ...}
455 # save the program name and the entire command line for later logging
457 binary = os.path.basename(sys.argv[0]) or sys.argv[0]
458 if len(sys.argv) >= 2:
459 binary += " " + sys.argv[1]
460 old_cmdline = " ".join(sys.argv[2:])
464 binary = "<unknown program>"
470 func, options, args = _ParseArgs(sys.argv, commands, aliases)
471 if func is None: # parse error
474 if override is not None:
475 for key, val in override.iteritems():
476 setattr(options, key, val)
478 logger.SetupLogging(debug=options.debug, program=binary)
480 utils.debug = options.debug
482 utils.Lock('cmd', max_retries=options.lock_retries, debug=options.debug,
483 autoclean=not options.lock_noautoclean)
484 except errors.LockError, err:
485 logger.ToStderr(str(err))
487 except KeyboardInterrupt:
488 logger.ToStderr("Aborting.")
492 logger.Info("run with arguments '%s'" % old_cmdline)
494 logger.Info("run with no arguments")
498 result = func(options, args)
499 except errors.GenericError, err:
500 result, err_msg = FormatError(err)
501 logger.ToStderr(err_msg)
509 def GenerateTable(headers, fields, separator, data,
510 numfields=None, unitfields=None):
511 """Prints a table with headers and different fields.
514 headers: Dict of header titles or None if no headers should be shown
515 fields: List of fields to show
516 separator: String used to separate fields or None for spaces
517 data: Data to be printed
518 numfields: List of fields to be aligned to right
519 unitfields: List of fields to be formatted as units
522 if numfields is None:
524 if unitfields is None:
529 if headers and field not in headers:
530 raise errors.ProgrammerError("Missing header description for field '%s'"
532 if separator is not None:
533 format_fields.append("%s")
534 elif field in numfields:
535 format_fields.append("%*s")
537 format_fields.append("%-*s")
539 if separator is None:
540 mlens = [0 for name in fields]
541 format = ' '.join(format_fields)
543 format = separator.replace("%", "%%").join(format_fields)
546 for idx, val in enumerate(row):
547 if fields[idx] in unitfields:
553 val = row[idx] = utils.FormatUnit(val)
554 val = row[idx] = str(val)
555 if separator is None:
556 mlens[idx] = max(mlens[idx], len(val))
561 for idx, name in enumerate(fields):
563 if separator is None:
564 mlens[idx] = max(mlens[idx], len(hdr))
565 args.append(mlens[idx])
567 result.append(format % tuple(args))
571 for idx in xrange(len(fields)):
572 if separator is None:
573 args.append(mlens[idx])
574 args.append(line[idx])
575 result.append(format % tuple(args))