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):
199 return utils.ParseUnit(value)
200 except errors.UnitParseError, err:
201 raise OptionValueError("option %s: %s" % (opt, err))
204 class CliOption(Option):
205 TYPES = Option.TYPES + ("unit",)
206 TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER)
207 TYPE_CHECKER["unit"] = check_unit
210 # optparse.py sets make_option, so we do it for our own option class, too
211 cli_option = CliOption
214 def _ParseArgs(argv, commands):
215 """Parses the command line and return the function which must be
216 executed together with its arguments
219 argv: the command line
221 commands: dictionary with special contents, see the design doc for
228 binary = argv[0].split("/")[-1]
230 if len(argv) > 1 and argv[1] == "--version":
231 print "%s (ganeti) %s" % (binary, constants.RELEASE_VERSION)
232 # Quit right away. That way we don't have to care about this special
233 # argument. optparse.py does it the same.
236 if len(argv) < 2 or argv[1] not in commands.keys():
237 # let's do a nice thing
238 sortedcmds = commands.keys()
240 print ("Usage: %(bin)s {command} [options...] [argument...]"
241 "\n%(bin)s <command> --help to see details, or"
242 " man %(bin)s\n" % {"bin": binary})
243 # compute the max line length for cmd + usage
244 mlen = max([len(" %s %s" % (cmd, commands[cmd][3])) for cmd in commands])
245 mlen = min(60, mlen) # should not get here...
246 # and format a nice command list
248 for cmd in sortedcmds:
249 cmdstr = " %s %s" % (cmd, commands[cmd][3])
250 help_text = commands[cmd][4]
251 help_lines = textwrap.wrap(help_text, 79-3-mlen)
252 print "%-*s - %s" % (mlen, cmdstr,
254 for line in help_lines:
255 print "%-*s %s" % (mlen, "", line)
257 return None, None, None
259 func, nargs, parser_opts, usage, description = commands[cmd]
260 parser_opts.append(_LOCK_OPT)
261 parser = OptionParser(option_list=parser_opts,
262 description=description,
263 formatter=TitledHelpFormatter(),
264 usage="%%prog %s %s" % (cmd, usage))
265 parser.disable_interspersed_args()
266 options, args = parser.parse_args()
269 print >> sys.stderr, ("Error: Command %s expects no arguments" % cmd)
270 return None, None, None
271 elif nargs < 0 and len(args) != -nargs:
272 print >> sys.stderr, ("Error: Command %s expects %d argument(s)" %
274 return None, None, None
275 elif nargs >= 0 and len(args) < nargs:
276 print >> sys.stderr, ("Error: Command %s expects at least %d argument(s)" %
278 return None, None, None
280 return func, options, args
283 def AskUser(text, choices=None):
284 """Ask the user a question.
287 text - the question to ask.
289 choices - list with elements tuples (input_char, return_value,
290 description); if not given, it will default to: [('y', True,
291 'Perform the operation'), ('n', False, 'Do no do the operation')];
292 note that the '?' char is reserved for help
294 Returns: one of the return values from the choices list; if input is
295 not possible (i.e. not running with a tty, we return the last entry
300 choices = [('y', True, 'Perform the operation'),
301 ('n', False, 'Do not perform the operation')]
302 if not choices or not isinstance(choices, list):
303 raise errors.ProgrammerError("Invalid choiches argument to AskUser")
304 for entry in choices:
305 if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
306 raise errors.ProgrammerError("Invalid choiches element to AskUser")
308 answer = choices[-1][1]
310 for line in text.splitlines():
311 new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
312 text = "\n".join(new_text)
314 f = file("/dev/tty", "r+")
318 chars = [entry[0] for entry in choices]
319 chars[-1] = "[%s]" % chars[-1]
321 maps = dict([(entry[0], entry[1]) for entry in choices])
325 f.write("/".join(chars))
327 line = f.readline(2).strip().lower()
332 for entry in choices:
333 f.write(" %s - %s\n" % (entry[0], entry[2]))
341 def SubmitOpCode(op, proc=None, feedback_fn=None):
342 """Function to submit an opcode.
344 This is just a simple wrapper over the construction of the processor
345 instance. It should be extended to better handle feedback and
346 interaction functions.
350 proc = mcpu.Processor()
351 if feedback_fn is None:
352 feedback_fn = logger.ToStdout
353 return proc.ExecOpCode(op, feedback_fn)
356 def FormatError(err):
357 """Return a formatted error message for a given error.
359 This function takes an exception instance and returns a tuple
360 consisting of two values: first, the recommended exit code, and
361 second, a string describing the error message (not
367 if isinstance(err, errors.ConfigurationError):
368 msg = "Corrupt configuration file: %s" % err
370 obuf.write(msg + "\n")
371 obuf.write("Aborting.")
373 elif isinstance(err, errors.HooksAbort):
374 obuf.write("Failure: hooks execution failed:\n")
375 for node, script, out in err.args[0]:
377 obuf.write(" node: %s, script: %s, output: %s\n" %
380 obuf.write(" node: %s, script: %s (no output)\n" %
382 elif isinstance(err, errors.HooksFailure):
383 obuf.write("Failure: hooks general failure: %s" % str(err))
384 elif isinstance(err, errors.ResolverError):
385 this_host = utils.HostInfo.SysName()
386 if err.args[0] == this_host:
387 msg = "Failure: can't resolve my own hostname ('%s')"
389 msg = "Failure: can't resolve hostname '%s'"
390 obuf.write(msg % err.args[0])
391 elif isinstance(err, errors.OpPrereqError):
392 obuf.write("Failure: prerequisites not met for this"
393 " operation:\n%s" % str(err))
394 elif isinstance(err, errors.OpExecError):
395 obuf.write("Failure: command execution error:\n%s" % str(err))
396 elif isinstance(err, errors.TagError):
397 obuf.write("Failure: invalid tag(s) given:\n%s" % str(err))
398 elif isinstance(err, errors.GenericError):
399 obuf.write("Unhandled Ganeti error: %s" % str(err))
401 obuf.write("Unhandled exception: %s" % str(err))
402 return retcode, obuf.getvalue().rstrip('\n')
405 def GenericMain(commands, override=None):
406 """Generic main function for all the gnt-* commands.
409 - commands: a dictionary with a special structure, see the design doc
410 for command line handling.
411 - override: if not None, we expect a dictionary with keys that will
412 override command line options; this can be used to pass
413 options from the scripts to generic functions
416 # save the program name and the entire command line for later logging
418 binary = os.path.basename(sys.argv[0]) or sys.argv[0]
419 if len(sys.argv) >= 2:
420 binary += " " + sys.argv[1]
421 old_cmdline = " ".join(sys.argv[2:])
425 binary = "<unknown program>"
428 func, options, args = _ParseArgs(sys.argv, commands)
429 if func is None: # parse error
432 if override is not None:
433 for key, val in override.iteritems():
434 setattr(options, key, val)
436 logger.SetupLogging(debug=options.debug, program=binary)
439 utils.Lock('cmd', max_retries=options.lock_retries, debug=options.debug)
440 except errors.LockError, err:
441 logger.ToStderr(str(err))
445 logger.Info("run with arguments '%s'" % old_cmdline)
447 logger.Info("run with no arguments")
451 result = func(options, args)
452 except errors.GenericError, err:
453 result, err_msg = FormatError(err)
454 logger.ToStderr(err_msg)
462 def GenerateTable(headers, fields, separator, data,
463 numfields=None, unitfields=None):
464 """Prints a table with headers and different fields.
467 headers: Dict of header titles or None if no headers should be shown
468 fields: List of fields to show
469 separator: String used to separate fields or None for spaces
470 data: Data to be printed
471 numfields: List of fields to be aligned to right
472 unitfields: List of fields to be formatted as units
475 if numfields is None:
477 if unitfields is None:
482 if separator is not None:
483 format_fields.append("%s")
484 elif field in numfields:
485 format_fields.append("%*s")
487 format_fields.append("%-*s")
489 if separator is None:
490 mlens = [0 for name in fields]
491 format = ' '.join(format_fields)
493 format = separator.replace("%", "%%").join(format_fields)
496 for idx, val in enumerate(row):
497 if fields[idx] in unitfields:
503 val = row[idx] = utils.FormatUnit(val)
504 if separator is None:
505 mlens[idx] = max(mlens[idx], len(val))
510 for idx, name in enumerate(fields):
512 if separator is None:
513 mlens[idx] = max(mlens[idx], len(hdr))
514 args.append(mlens[idx])
516 result.append(format % tuple(args))
520 for idx in xrange(len(fields)):
521 if separator is None:
522 args.append(mlens[idx])
523 args.append(line[idx])
524 result.append(format % tuple(args))