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="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")
184 """Macro-like function denoting a fixed number of arguments"""
188 def ARGS_ATLEAST(val):
189 """Macro-like function denoting a minimum number of arguments"""
194 ARGS_ONE = ARGS_FIXED(1)
195 ARGS_ANY = ARGS_ATLEAST(0)
198 def check_unit(option, opt, value):
199 """OptParsers custom converter for units.
203 return utils.ParseUnit(value)
204 except errors.UnitParseError, err:
205 raise OptionValueError("option %s: %s" % (opt, err))
208 class CliOption(Option):
209 """Custom option class for optparse.
212 TYPES = Option.TYPES + ("unit",)
213 TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER)
214 TYPE_CHECKER["unit"] = check_unit
217 # optparse.py sets make_option, so we do it for our own option class, too
218 cli_option = CliOption
221 def _ParseArgs(argv, commands, aliases):
222 """Parses the command line and return the function which must be
223 executed together with its arguments
226 argv: the command line
228 commands: dictionary with special contents, see the design doc for
230 aliases: dictionary with command aliases {'alias': 'target, ...}
236 binary = argv[0].split("/")[-1]
238 if len(argv) > 1 and argv[1] == "--version":
239 print "%s (ganeti) %s" % (binary, constants.RELEASE_VERSION)
240 # Quit right away. That way we don't have to care about this special
241 # argument. optparse.py does it the same.
244 if len(argv) < 2 or not (argv[1] in commands or
246 # let's do a nice thing
247 sortedcmds = commands.keys()
249 print ("Usage: %(bin)s {command} [options...] [argument...]"
250 "\n%(bin)s <command> --help to see details, or"
251 " man %(bin)s\n" % {"bin": binary})
252 # compute the max line length for cmd + usage
253 mlen = max([len(" %s %s" % (cmd, commands[cmd][3])) for cmd in commands])
254 mlen = min(60, mlen) # should not get here...
255 # and format a nice command list
257 for cmd in sortedcmds:
258 cmdstr = " %s %s" % (cmd, commands[cmd][3])
259 help_text = commands[cmd][4]
260 help_lines = textwrap.wrap(help_text, 79-3-mlen)
261 print "%-*s - %s" % (mlen, cmdstr,
263 for line in help_lines:
264 print "%-*s %s" % (mlen, "", line)
266 return None, None, None
268 # get command, unalias it, and look it up in commands
272 raise errors.ProgrammerError("Alias '%s' overrides an existing"
275 if aliases[cmd] not in commands:
276 raise errors.ProgrammerError("Alias '%s' maps to non-existing"
277 " command '%s'" % (cmd, aliases[cmd]))
281 func, nargs, parser_opts, usage, description = commands[cmd]
282 parser_opts.append(_LOCK_OPT)
283 parser = OptionParser(option_list=parser_opts,
284 description=description,
285 formatter=TitledHelpFormatter(),
286 usage="%%prog %s %s" % (cmd, usage))
287 parser.disable_interspersed_args()
288 options, args = parser.parse_args()
291 print >> sys.stderr, ("Error: Command %s expects no arguments" % cmd)
292 return None, None, None
293 elif nargs < 0 and len(args) != -nargs:
294 print >> sys.stderr, ("Error: Command %s expects %d argument(s)" %
296 return None, None, None
297 elif nargs >= 0 and len(args) < nargs:
298 print >> sys.stderr, ("Error: Command %s expects at least %d argument(s)" %
300 return None, None, None
302 return func, options, args
305 def SplitNodeOption(value):
306 """Splits the value of a --node option.
309 if value and ':' in value:
310 return value.split(':', 1)
315 def AskUser(text, choices=None):
316 """Ask the user a question.
319 text - the question to ask.
321 choices - list with elements tuples (input_char, return_value,
322 description); if not given, it will default to: [('y', True,
323 'Perform the operation'), ('n', False, 'Do no do the operation')];
324 note that the '?' char is reserved for help
326 Returns: one of the return values from the choices list; if input is
327 not possible (i.e. not running with a tty, we return the last entry
332 choices = [('y', True, 'Perform the operation'),
333 ('n', False, 'Do not perform the operation')]
334 if not choices or not isinstance(choices, list):
335 raise errors.ProgrammerError("Invalid choiches argument to AskUser")
336 for entry in choices:
337 if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
338 raise errors.ProgrammerError("Invalid choiches element to AskUser")
340 answer = choices[-1][1]
342 for line in text.splitlines():
343 new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
344 text = "\n".join(new_text)
346 f = file("/dev/tty", "a+")
350 chars = [entry[0] for entry in choices]
351 chars[-1] = "[%s]" % chars[-1]
353 maps = dict([(entry[0], entry[1]) for entry in choices])
357 f.write("/".join(chars))
359 line = f.readline(2).strip().lower()
364 for entry in choices:
365 f.write(" %s - %s\n" % (entry[0], entry[2]))
373 def SubmitOpCode(op, proc=None, feedback_fn=None):
374 """Function to submit an opcode.
376 This is just a simple wrapper over the construction of the processor
377 instance. It should be extended to better handle feedback and
378 interaction functions.
381 if feedback_fn is None:
382 feedback_fn = logger.ToStdout
384 proc = mcpu.Processor(feedback=feedback_fn)
385 return proc.ExecOpCode(op)
388 def FormatError(err):
389 """Return a formatted error message for a given error.
391 This function takes an exception instance and returns a tuple
392 consisting of two values: first, the recommended exit code, and
393 second, a string describing the error message (not
400 if isinstance(err, errors.ConfigurationError):
401 txt = "Corrupt configuration file: %s" % msg
403 obuf.write(txt + "\n")
404 obuf.write("Aborting.")
406 elif isinstance(err, errors.HooksAbort):
407 obuf.write("Failure: hooks execution failed:\n")
408 for node, script, out in err.args[0]:
410 obuf.write(" node: %s, script: %s, output: %s\n" %
413 obuf.write(" node: %s, script: %s (no output)\n" %
415 elif isinstance(err, errors.HooksFailure):
416 obuf.write("Failure: hooks general failure: %s" % msg)
417 elif isinstance(err, errors.ResolverError):
418 this_host = utils.HostInfo.SysName()
419 if err.args[0] == this_host:
420 msg = "Failure: can't resolve my own hostname ('%s')"
422 msg = "Failure: can't resolve hostname '%s'"
423 obuf.write(msg % err.args[0])
424 elif isinstance(err, errors.OpPrereqError):
425 obuf.write("Failure: prerequisites not met for this"
426 " operation:\n%s" % msg)
427 elif isinstance(err, errors.OpExecError):
428 obuf.write("Failure: command execution error:\n%s" % msg)
429 elif isinstance(err, errors.TagError):
430 obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
431 elif isinstance(err, errors.GenericError):
432 obuf.write("Unhandled Ganeti error: %s" % msg)
434 obuf.write("Unhandled exception: %s" % msg)
435 return retcode, obuf.getvalue().rstrip('\n')
438 def GenericMain(commands, override=None, aliases=None):
439 """Generic main function for all the gnt-* commands.
442 - commands: a dictionary with a special structure, see the design doc
443 for command line handling.
444 - override: if not None, we expect a dictionary with keys that will
445 override command line options; this can be used to pass
446 options from the scripts to generic functions
447 - aliases: dictionary with command aliases {'alias': 'target, ...}
450 # save the program name and the entire command line for later logging
452 binary = os.path.basename(sys.argv[0]) or sys.argv[0]
453 if len(sys.argv) >= 2:
454 binary += " " + sys.argv[1]
455 old_cmdline = " ".join(sys.argv[2:])
459 binary = "<unknown program>"
465 func, options, args = _ParseArgs(sys.argv, commands, aliases)
466 if func is None: # parse error
469 if override is not None:
470 for key, val in override.iteritems():
471 setattr(options, key, val)
473 logger.SetupLogging(debug=options.debug, program=binary)
475 utils.debug = options.debug
477 utils.Lock('cmd', max_retries=options.lock_retries, debug=options.debug)
478 except errors.LockError, err:
479 logger.ToStderr(str(err))
481 except KeyboardInterrupt:
482 logger.ToStderr("Aborting.")
486 logger.Info("run with arguments '%s'" % old_cmdline)
488 logger.Info("run with no arguments")
492 result = func(options, args)
493 except errors.GenericError, err:
494 result, err_msg = FormatError(err)
495 logger.ToStderr(err_msg)
503 def GenerateTable(headers, fields, separator, data,
504 numfields=None, unitfields=None):
505 """Prints a table with headers and different fields.
508 headers: Dict of header titles or None if no headers should be shown
509 fields: List of fields to show
510 separator: String used to separate fields or None for spaces
511 data: Data to be printed
512 numfields: List of fields to be aligned to right
513 unitfields: List of fields to be formatted as units
516 if numfields is None:
518 if unitfields is None:
523 if headers and field not in headers:
524 raise errors.ProgrammerError("Missing header description for field '%s'"
526 if separator is not None:
527 format_fields.append("%s")
528 elif field in numfields:
529 format_fields.append("%*s")
531 format_fields.append("%-*s")
533 if separator is None:
534 mlens = [0 for name in fields]
535 format = ' '.join(format_fields)
537 format = separator.replace("%", "%%").join(format_fields)
540 for idx, val in enumerate(row):
541 if fields[idx] in unitfields:
547 val = row[idx] = utils.FormatUnit(val)
548 val = row[idx] = str(val)
549 if separator is None:
550 mlens[idx] = max(mlens[idx], len(val))
555 for idx, name in enumerate(fields):
557 if separator is None:
558 mlens[idx] = max(mlens[idx], len(hdr))
559 args.append(mlens[idx])
561 result.append(format % tuple(args))
565 for idx in xrange(len(fields)):
566 if separator is None:
567 args.append(mlens[idx])
568 args.append(line[idx])
569 result.append(format % tuple(args))