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",
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="Select output fields",
173 FORCE_OPT = make_option("-f", "--force", dest="force", action="store_true",
174 default=False, help="Force the operation")
176 _LOCK_OPT = make_option("--lock-retries", default=None,
177 type="int", help=SUPPRESS_HELP)
179 TAG_SRC_OPT = make_option("--from", dest="tags_source",
180 default=None, help="File with tag names")
183 """Macro-like function denoting a fixed number of arguments"""
187 def ARGS_ATLEAST(val):
188 """Macro-like function denoting a minimum number of arguments"""
193 ARGS_ONE = ARGS_FIXED(1)
194 ARGS_ANY = ARGS_ATLEAST(0)
197 def check_unit(option, opt, value):
198 """OptParsers custom converter for units.
202 return utils.ParseUnit(value)
203 except errors.UnitParseError, err:
204 raise OptionValueError("option %s: %s" % (opt, err))
207 class CliOption(Option):
208 """Custom option class for optparse.
211 TYPES = Option.TYPES + ("unit",)
212 TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER)
213 TYPE_CHECKER["unit"] = check_unit
216 # optparse.py sets make_option, so we do it for our own option class, too
217 cli_option = CliOption
220 def _ParseArgs(argv, commands):
221 """Parses the command line and return the function which must be
222 executed together with its arguments
225 argv: the command line
227 commands: dictionary with special contents, see the design doc for
234 binary = argv[0].split("/")[-1]
236 if len(argv) > 1 and argv[1] == "--version":
237 print "%s (ganeti) %s" % (binary, constants.RELEASE_VERSION)
238 # Quit right away. That way we don't have to care about this special
239 # argument. optparse.py does it the same.
242 if len(argv) < 2 or argv[1] not in commands.keys():
243 # let's do a nice thing
244 sortedcmds = commands.keys()
246 print ("Usage: %(bin)s {command} [options...] [argument...]"
247 "\n%(bin)s <command> --help to see details, or"
248 " man %(bin)s\n" % {"bin": binary})
249 # compute the max line length for cmd + usage
250 mlen = max([len(" %s %s" % (cmd, commands[cmd][3])) for cmd in commands])
251 mlen = min(60, mlen) # should not get here...
252 # and format a nice command list
254 for cmd in sortedcmds:
255 cmdstr = " %s %s" % (cmd, commands[cmd][3])
256 help_text = commands[cmd][4]
257 help_lines = textwrap.wrap(help_text, 79-3-mlen)
258 print "%-*s - %s" % (mlen, cmdstr,
260 for line in help_lines:
261 print "%-*s %s" % (mlen, "", line)
263 return None, None, None
265 func, nargs, parser_opts, usage, description = commands[cmd]
266 parser_opts.append(_LOCK_OPT)
267 parser = OptionParser(option_list=parser_opts,
268 description=description,
269 formatter=TitledHelpFormatter(),
270 usage="%%prog %s %s" % (cmd, usage))
271 parser.disable_interspersed_args()
272 options, args = parser.parse_args()
275 print >> sys.stderr, ("Error: Command %s expects no arguments" % cmd)
276 return None, None, None
277 elif nargs < 0 and len(args) != -nargs:
278 print >> sys.stderr, ("Error: Command %s expects %d argument(s)" %
280 return None, None, None
281 elif nargs >= 0 and len(args) < nargs:
282 print >> sys.stderr, ("Error: Command %s expects at least %d argument(s)" %
284 return None, None, None
286 return func, options, args
289 def AskUser(text, choices=None):
290 """Ask the user a question.
293 text - the question to ask.
295 choices - list with elements tuples (input_char, return_value,
296 description); if not given, it will default to: [('y', True,
297 'Perform the operation'), ('n', False, 'Do no do the operation')];
298 note that the '?' char is reserved for help
300 Returns: one of the return values from the choices list; if input is
301 not possible (i.e. not running with a tty, we return the last entry
306 choices = [('y', True, 'Perform the operation'),
307 ('n', False, 'Do not perform the operation')]
308 if not choices or not isinstance(choices, list):
309 raise errors.ProgrammerError("Invalid choiches argument to AskUser")
310 for entry in choices:
311 if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
312 raise errors.ProgrammerError("Invalid choiches element to AskUser")
314 answer = choices[-1][1]
316 for line in text.splitlines():
317 new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
318 text = "\n".join(new_text)
320 f = file("/dev/tty", "a+")
324 chars = [entry[0] for entry in choices]
325 chars[-1] = "[%s]" % chars[-1]
327 maps = dict([(entry[0], entry[1]) for entry in choices])
331 f.write("/".join(chars))
333 line = f.readline(2).strip().lower()
338 for entry in choices:
339 f.write(" %s - %s\n" % (entry[0], entry[2]))
347 def SubmitOpCode(op, proc=None, feedback_fn=None):
348 """Function to submit an opcode.
350 This is just a simple wrapper over the construction of the processor
351 instance. It should be extended to better handle feedback and
352 interaction functions.
355 if feedback_fn is None:
356 feedback_fn = logger.ToStdout
358 proc = mcpu.Processor(feedback=feedback_fn)
359 return proc.ExecOpCode(op)
362 def FormatError(err):
363 """Return a formatted error message for a given error.
365 This function takes an exception instance and returns a tuple
366 consisting of two values: first, the recommended exit code, and
367 second, a string describing the error message (not
374 if isinstance(err, errors.ConfigurationError):
375 txt = "Corrupt configuration file: %s" % msg
377 obuf.write(txt + "\n")
378 obuf.write("Aborting.")
380 elif isinstance(err, errors.HooksAbort):
381 obuf.write("Failure: hooks execution failed:\n")
382 for node, script, out in err.args[0]:
384 obuf.write(" node: %s, script: %s, output: %s\n" %
387 obuf.write(" node: %s, script: %s (no output)\n" %
389 elif isinstance(err, errors.HooksFailure):
390 obuf.write("Failure: hooks general failure: %s" % msg)
391 elif isinstance(err, errors.ResolverError):
392 this_host = utils.HostInfo.SysName()
393 if err.args[0] == this_host:
394 msg = "Failure: can't resolve my own hostname ('%s')"
396 msg = "Failure: can't resolve hostname '%s'"
397 obuf.write(msg % err.args[0])
398 elif isinstance(err, errors.OpPrereqError):
399 obuf.write("Failure: prerequisites not met for this"
400 " operation:\n%s" % msg)
401 elif isinstance(err, errors.OpExecError):
402 obuf.write("Failure: command execution error:\n%s" % msg)
403 elif isinstance(err, errors.TagError):
404 obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
405 elif isinstance(err, errors.GenericError):
406 obuf.write("Unhandled Ganeti error: %s" % msg)
408 obuf.write("Unhandled exception: %s" % msg)
409 return retcode, obuf.getvalue().rstrip('\n')
412 def GenericMain(commands, override=None):
413 """Generic main function for all the gnt-* commands.
416 - commands: a dictionary with a special structure, see the design doc
417 for command line handling.
418 - override: if not None, we expect a dictionary with keys that will
419 override command line options; this can be used to pass
420 options from the scripts to generic functions
423 # save the program name and the entire command line for later logging
425 binary = os.path.basename(sys.argv[0]) or sys.argv[0]
426 if len(sys.argv) >= 2:
427 binary += " " + sys.argv[1]
428 old_cmdline = " ".join(sys.argv[2:])
432 binary = "<unknown program>"
435 func, options, args = _ParseArgs(sys.argv, commands)
436 if func is None: # parse error
439 if override is not None:
440 for key, val in override.iteritems():
441 setattr(options, key, val)
443 logger.SetupLogging(debug=options.debug, program=binary)
446 utils.Lock('cmd', max_retries=options.lock_retries, debug=options.debug)
447 except errors.LockError, err:
448 logger.ToStderr(str(err))
452 logger.Info("run with arguments '%s'" % old_cmdline)
454 logger.Info("run with no arguments")
458 result = func(options, args)
459 except errors.GenericError, err:
460 result, err_msg = FormatError(err)
461 logger.ToStderr(err_msg)
469 def GenerateTable(headers, fields, separator, data,
470 numfields=None, unitfields=None):
471 """Prints a table with headers and different fields.
474 headers: Dict of header titles or None if no headers should be shown
475 fields: List of fields to show
476 separator: String used to separate fields or None for spaces
477 data: Data to be printed
478 numfields: List of fields to be aligned to right
479 unitfields: List of fields to be formatted as units
482 if numfields is None:
484 if unitfields is None:
489 if headers and field not in headers:
490 raise errors.ProgrammerError("Missing header description for field '%s'"
492 if separator is not None:
493 format_fields.append("%s")
494 elif field in numfields:
495 format_fields.append("%*s")
497 format_fields.append("%-*s")
499 if separator is None:
500 mlens = [0 for name in fields]
501 format = ' '.join(format_fields)
503 format = separator.replace("%", "%%").join(format_fields)
506 for idx, val in enumerate(row):
507 if fields[idx] in unitfields:
513 val = row[idx] = utils.FormatUnit(val)
514 val = row[idx] = str(val)
515 if separator is None:
516 mlens[idx] = max(mlens[idx], len(val))
521 for idx, name in enumerate(fields):
523 if separator is None:
524 mlens[idx] = max(mlens[idx], len(hdr))
525 args.append(mlens[idx])
527 result.append(format % tuple(args))
531 for idx in xrange(len(fields)):
532 if separator is None:
533 args.append(mlens[idx])
534 args.append(line[idx])
535 result.append(format % tuple(args))