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 TAG_SRC_OPT = make_option("--from", dest="tags_source",
181 default=None, help="File with tag names")
185 """Macro-like function denoting a fixed number of arguments"""
189 def ARGS_ATLEAST(val):
190 """Macro-like function denoting a minimum number of arguments"""
195 ARGS_ONE = ARGS_FIXED(1)
196 ARGS_ANY = ARGS_ATLEAST(0)
199 def check_unit(option, opt, value):
200 """OptParsers custom converter for units.
204 return utils.ParseUnit(value)
205 except errors.UnitParseError, err:
206 raise OptionValueError("option %s: %s" % (opt, err))
209 class CliOption(Option):
210 """Custom option class for optparse.
213 TYPES = Option.TYPES + ("unit",)
214 TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER)
215 TYPE_CHECKER["unit"] = check_unit
218 # optparse.py sets make_option, so we do it for our own option class, too
219 cli_option = CliOption
222 def _ParseArgs(argv, commands, aliases):
223 """Parses the command line and return the function which must be
224 executed together with its arguments
227 argv: the command line
229 commands: dictionary with special contents, see the design doc for
231 aliases: dictionary with command aliases {'alias': 'target, ...}
237 binary = argv[0].split("/")[-1]
239 if len(argv) > 1 and argv[1] == "--version":
240 print "%s (ganeti) %s" % (binary, constants.RELEASE_VERSION)
241 # Quit right away. That way we don't have to care about this special
242 # argument. optparse.py does it the same.
245 if len(argv) < 2 or not (argv[1] in commands or
247 # let's do a nice thing
248 sortedcmds = commands.keys()
250 print ("Usage: %(bin)s {command} [options...] [argument...]"
251 "\n%(bin)s <command> --help to see details, or"
252 " man %(bin)s\n" % {"bin": binary})
253 # compute the max line length for cmd + usage
254 mlen = max([len(" %s %s" % (cmd, commands[cmd][3])) for cmd in commands])
255 mlen = min(60, mlen) # should not get here...
256 # and format a nice command list
258 for cmd in sortedcmds:
259 cmdstr = " %s %s" % (cmd, commands[cmd][3])
260 help_text = commands[cmd][4]
261 help_lines = textwrap.wrap(help_text, 79-3-mlen)
262 print "%-*s - %s" % (mlen, cmdstr,
264 for line in help_lines:
265 print "%-*s %s" % (mlen, "", line)
267 return None, None, None
269 # get command, unalias it, and look it up in commands
273 raise errors.ProgrammerError("Alias '%s' overrides an existing"
276 if aliases[cmd] not in commands:
277 raise errors.ProgrammerError("Alias '%s' maps to non-existing"
278 " command '%s'" % (cmd, aliases[cmd]))
282 func, nargs, parser_opts, usage, description = commands[cmd]
283 parser_opts.append(_LOCK_OPT)
284 parser = OptionParser(option_list=parser_opts,
285 description=description,
286 formatter=TitledHelpFormatter(),
287 usage="%%prog %s %s" % (cmd, usage))
288 parser.disable_interspersed_args()
289 options, args = parser.parse_args()
292 print >> sys.stderr, ("Error: Command %s expects no arguments" % cmd)
293 return None, None, None
294 elif nargs < 0 and len(args) != -nargs:
295 print >> sys.stderr, ("Error: Command %s expects %d argument(s)" %
297 return None, None, None
298 elif nargs >= 0 and len(args) < nargs:
299 print >> sys.stderr, ("Error: Command %s expects at least %d argument(s)" %
301 return None, None, None
303 return func, options, args
306 def SplitNodeOption(value):
307 """Splits the value of a --node option.
310 if value and ':' in value:
311 return value.split(':', 1)
316 def AskUser(text, choices=None):
317 """Ask the user a question.
320 text - the question to ask.
322 choices - list with elements tuples (input_char, return_value,
323 description); if not given, it will default to: [('y', True,
324 'Perform the operation'), ('n', False, 'Do no do the operation')];
325 note that the '?' char is reserved for help
327 Returns: one of the return values from the choices list; if input is
328 not possible (i.e. not running with a tty, we return the last entry
333 choices = [('y', True, 'Perform the operation'),
334 ('n', False, 'Do not perform the operation')]
335 if not choices or not isinstance(choices, list):
336 raise errors.ProgrammerError("Invalid choiches argument to AskUser")
337 for entry in choices:
338 if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
339 raise errors.ProgrammerError("Invalid choiches element to AskUser")
341 answer = choices[-1][1]
343 for line in text.splitlines():
344 new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
345 text = "\n".join(new_text)
347 f = file("/dev/tty", "a+")
351 chars = [entry[0] for entry in choices]
352 chars[-1] = "[%s]" % chars[-1]
354 maps = dict([(entry[0], entry[1]) for entry in choices])
358 f.write("/".join(chars))
360 line = f.readline(2).strip().lower()
365 for entry in choices:
366 f.write(" %s - %s\n" % (entry[0], entry[2]))
374 def SubmitOpCode(op, proc=None, feedback_fn=None):
375 """Function to submit an opcode.
377 This is just a simple wrapper over the construction of the processor
378 instance. It should be extended to better handle feedback and
379 interaction functions.
382 if feedback_fn is None:
383 feedback_fn = logger.ToStdout
385 proc = mcpu.Processor(feedback=feedback_fn)
386 return proc.ExecOpCode(op)
389 def FormatError(err):
390 """Return a formatted error message for a given error.
392 This function takes an exception instance and returns a tuple
393 consisting of two values: first, the recommended exit code, and
394 second, a string describing the error message (not
401 if isinstance(err, errors.ConfigurationError):
402 txt = "Corrupt configuration file: %s" % msg
404 obuf.write(txt + "\n")
405 obuf.write("Aborting.")
407 elif isinstance(err, errors.HooksAbort):
408 obuf.write("Failure: hooks execution failed:\n")
409 for node, script, out in err.args[0]:
411 obuf.write(" node: %s, script: %s, output: %s\n" %
414 obuf.write(" node: %s, script: %s (no output)\n" %
416 elif isinstance(err, errors.HooksFailure):
417 obuf.write("Failure: hooks general failure: %s" % msg)
418 elif isinstance(err, errors.ResolverError):
419 this_host = utils.HostInfo.SysName()
420 if err.args[0] == this_host:
421 msg = "Failure: can't resolve my own hostname ('%s')"
423 msg = "Failure: can't resolve hostname '%s'"
424 obuf.write(msg % err.args[0])
425 elif isinstance(err, errors.OpPrereqError):
426 obuf.write("Failure: prerequisites not met for this"
427 " operation:\n%s" % msg)
428 elif isinstance(err, errors.OpExecError):
429 obuf.write("Failure: command execution error:\n%s" % msg)
430 elif isinstance(err, errors.TagError):
431 obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
432 elif isinstance(err, errors.GenericError):
433 obuf.write("Unhandled Ganeti error: %s" % msg)
435 obuf.write("Unhandled exception: %s" % msg)
436 return retcode, obuf.getvalue().rstrip('\n')
439 def GenericMain(commands, override=None, aliases=None):
440 """Generic main function for all the gnt-* commands.
443 - commands: a dictionary with a special structure, see the design doc
444 for command line handling.
445 - override: if not None, we expect a dictionary with keys that will
446 override command line options; this can be used to pass
447 options from the scripts to generic functions
448 - aliases: dictionary with command aliases {'alias': 'target, ...}
451 # save the program name and the entire command line for later logging
453 binary = os.path.basename(sys.argv[0]) or sys.argv[0]
454 if len(sys.argv) >= 2:
455 binary += " " + sys.argv[1]
456 old_cmdline = " ".join(sys.argv[2:])
460 binary = "<unknown program>"
466 func, options, args = _ParseArgs(sys.argv, commands, aliases)
467 if func is None: # parse error
470 if override is not None:
471 for key, val in override.iteritems():
472 setattr(options, key, val)
474 logger.SetupLogging(debug=options.debug, program=binary)
476 utils.debug = options.debug
478 utils.Lock('cmd', max_retries=options.lock_retries, debug=options.debug)
479 except errors.LockError, err:
480 logger.ToStderr(str(err))
482 except KeyboardInterrupt:
483 logger.ToStderr("Aborting.")
487 logger.Info("run with arguments '%s'" % old_cmdline)
489 logger.Info("run with no arguments")
493 result = func(options, args)
494 except errors.GenericError, err:
495 result, err_msg = FormatError(err)
496 logger.ToStderr(err_msg)
504 def GenerateTable(headers, fields, separator, data,
505 numfields=None, unitfields=None):
506 """Prints a table with headers and different fields.
509 headers: Dict of header titles or None if no headers should be shown
510 fields: List of fields to show
511 separator: String used to separate fields or None for spaces
512 data: Data to be printed
513 numfields: List of fields to be aligned to right
514 unitfields: List of fields to be formatted as units
517 if numfields is None:
519 if unitfields is None:
524 if headers and field not in headers:
525 raise errors.ProgrammerError("Missing header description for field '%s'"
527 if separator is not None:
528 format_fields.append("%s")
529 elif field in numfields:
530 format_fields.append("%*s")
532 format_fields.append("%-*s")
534 if separator is None:
535 mlens = [0 for name in fields]
536 format = ' '.join(format_fields)
538 format = separator.replace("%", "%%").join(format_fields)
541 for idx, val in enumerate(row):
542 if fields[idx] in unitfields:
548 val = row[idx] = utils.FormatUnit(val)
549 val = row[idx] = str(val)
550 if separator is None:
551 mlens[idx] = max(mlens[idx], len(val))
556 for idx, name in enumerate(fields):
558 if separator is None:
559 mlens[idx] = max(mlens[idx], len(hdr))
560 args.append(mlens[idx])
562 result.append(format % tuple(args))
566 for idx in xrange(len(fields)):
567 if separator is None:
568 args.append(mlens[idx])
569 args.append(line[idx])
570 result.append(format % tuple(args))