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"""
30 from cStringIO import StringIO
32 from ganeti import utils
33 from ganeti import logger
34 from ganeti import errors
35 from ganeti import mcpu
36 from ganeti import constants
37 from ganeti import opcodes
38 from ganeti import luxi
40 from optparse import (OptionParser, make_option, TitledHelpFormatter,
41 Option, OptionValueError, SUPPRESS_HELP)
43 __all__ = ["DEBUG_OPT", "NOHDR_OPT", "SEP_OPT", "GenericMain",
45 "cli_option", "GenerateTable", "AskUser",
46 "ARGS_NONE", "ARGS_FIXED", "ARGS_ATLEAST", "ARGS_ANY", "ARGS_ONE",
47 "USEUNITS_OPT", "FIELDS_OPT", "FORCE_OPT",
48 "ListTags", "AddTags", "RemoveTags", "TAG_SRC_OPT",
49 "FormatError", "SplitNodeOption"
53 def _ExtractTagsObject(opts, args):
54 """Extract the tag type object.
56 Note that this function will modify its args parameter.
59 if not hasattr(opts, "tag_type"):
60 raise errors.ProgrammerError("tag_type not passed to _ExtractTagsObject")
62 if kind == constants.TAG_CLUSTER:
64 elif kind == constants.TAG_NODE or kind == constants.TAG_INSTANCE:
66 raise errors.OpPrereqError("no arguments passed to the command")
70 raise errors.ProgrammerError("Unhandled tag type '%s'" % kind)
74 def _ExtendTags(opts, args):
75 """Extend the args if a source file has been given.
77 This function will extend the tags with the contents of the file
78 passed in the 'tags_source' attribute of the opts parameter. A file
79 named '-' will be replaced by stdin.
82 fname = opts.tags_source
88 new_fh = open(fname, "r")
91 # we don't use the nice 'new_data = [line.strip() for line in fh]'
92 # because of python bug 1633941
94 line = new_fh.readline()
97 new_data.append(line.strip())
100 args.extend(new_data)
103 def ListTags(opts, args):
104 """List the tags on a given object.
106 This is a generic implementation that knows how to deal with all
107 three cases of tag objects (cluster, node, instance). The opts
108 argument is expected to contain a tag_type field denoting what
109 object type we work on.
112 kind, name = _ExtractTagsObject(opts, args)
113 op = opcodes.OpGetTags(kind=kind, name=name)
114 result = SubmitOpCode(op)
115 result = list(result)
121 def AddTags(opts, args):
122 """Add tags on a given object.
124 This is a generic implementation that knows how to deal with all
125 three cases of tag objects (cluster, node, instance). The opts
126 argument is expected to contain a tag_type field denoting what
127 object type we work on.
130 kind, name = _ExtractTagsObject(opts, args)
131 _ExtendTags(opts, args)
133 raise errors.OpPrereqError("No tags to be added")
134 op = opcodes.OpAddTags(kind=kind, name=name, tags=args)
138 def RemoveTags(opts, args):
139 """Remove tags from a given object.
141 This is a generic implementation that knows how to deal with all
142 three cases of tag objects (cluster, node, instance). The opts
143 argument is expected to contain a tag_type field denoting what
144 object type we work on.
147 kind, name = _ExtractTagsObject(opts, args)
148 _ExtendTags(opts, args)
150 raise errors.OpPrereqError("No tags to be removed")
151 op = opcodes.OpDelTags(kind=kind, name=name, tags=args)
155 DEBUG_OPT = make_option("-d", "--debug", default=False,
157 help="Turn debugging on")
159 NOHDR_OPT = make_option("--no-headers", default=False,
160 action="store_true", dest="no_headers",
161 help="Don't display column headers")
163 SEP_OPT = make_option("--separator", default=None,
164 action="store", dest="separator",
165 help="Separator between output fields"
166 " (defaults to one space)")
168 USEUNITS_OPT = make_option("--human-readable", default=False,
169 action="store_true", dest="human_readable",
170 help="Print sizes in human readable format")
172 FIELDS_OPT = make_option("-o", "--output", dest="output", action="store",
173 type="string", help="Comma separated list of"
177 FORCE_OPT = make_option("-f", "--force", dest="force", action="store_true",
178 default=False, help="Force the operation")
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" % cmd) 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" % (cmd,)
260 help_text = commands[cmd][4]
261 help_lines = textwrap.wrap(help_text, 79-3-mlen)
262 print "%-*s - %s" % (mlen, cmdstr, help_lines.pop(0))
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 = OptionParser(option_list=parser_opts,
283 description=description,
284 formatter=TitledHelpFormatter(),
285 usage="%%prog %s %s" % (cmd, usage))
286 parser.disable_interspersed_args()
287 options, args = parser.parse_args()
290 print >> sys.stderr, ("Error: Command %s expects no arguments" % cmd)
291 return None, None, None
292 elif nargs < 0 and len(args) != -nargs:
293 print >> sys.stderr, ("Error: Command %s expects %d argument(s)" %
295 return None, None, None
296 elif nargs >= 0 and len(args) < nargs:
297 print >> sys.stderr, ("Error: Command %s expects at least %d argument(s)" %
299 return None, None, None
301 return func, options, args
304 def SplitNodeOption(value):
305 """Splits the value of a --node option.
308 if value and ':' in value:
309 return value.split(':', 1)
314 def AskUser(text, choices=None):
315 """Ask the user a question.
318 text - the question to ask.
320 choices - list with elements tuples (input_char, return_value,
321 description); if not given, it will default to: [('y', True,
322 'Perform the operation'), ('n', False, 'Do no do the operation')];
323 note that the '?' char is reserved for help
325 Returns: one of the return values from the choices list; if input is
326 not possible (i.e. not running with a tty, we return the last entry
331 choices = [('y', True, 'Perform the operation'),
332 ('n', False, 'Do not perform the operation')]
333 if not choices or not isinstance(choices, list):
334 raise errors.ProgrammerError("Invalid choiches argument to AskUser")
335 for entry in choices:
336 if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
337 raise errors.ProgrammerError("Invalid choiches element to AskUser")
339 answer = choices[-1][1]
341 for line in text.splitlines():
342 new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
343 text = "\n".join(new_text)
345 f = file("/dev/tty", "a+")
349 chars = [entry[0] for entry in choices]
350 chars[-1] = "[%s]" % chars[-1]
352 maps = dict([(entry[0], entry[1]) for entry in choices])
356 f.write("/".join(chars))
358 line = f.readline(2).strip().lower()
363 for entry in choices:
364 f.write(" %s - %s\n" % (entry[0], entry[2]))
372 def SubmitOpCode(op, proc=None, feedback_fn=None):
373 """Legacy function to submit an opcode.
375 This is just a simple wrapper over the construction of the processor
376 instance. It should be extended to better handle feedback and
377 interaction functions.
380 # TODO: Fix feedback_fn situation.
383 job_id = cl.SubmitJob([op])
386 jobs = cl.QueryJobs([job_id], ["status"])
388 # job not found, go away!
389 raise errors.JobLost("Job with id %s lost" % job_id)
391 # TODO: Handle canceled and archived jobs
393 if status in (constants.JOB_STATUS_SUCCESS, constants.JOB_STATUS_ERROR):
397 jobs = cl.QueryJobs([job_id], ["status", "result"])
399 raise errors.JobLost("Job with id %s lost" % job_id)
401 status, result = jobs[0]
402 if status == constants.JOB_STATUS_SUCCESS:
405 raise errors.OpExecError(result)
408 def FormatError(err):
409 """Return a formatted error message for a given error.
411 This function takes an exception instance and returns a tuple
412 consisting of two values: first, the recommended exit code, and
413 second, a string describing the error message (not
420 if isinstance(err, errors.ConfigurationError):
421 txt = "Corrupt configuration file: %s" % msg
423 obuf.write(txt + "\n")
424 obuf.write("Aborting.")
426 elif isinstance(err, errors.HooksAbort):
427 obuf.write("Failure: hooks execution failed:\n")
428 for node, script, out in err.args[0]:
430 obuf.write(" node: %s, script: %s, output: %s\n" %
433 obuf.write(" node: %s, script: %s (no output)\n" %
435 elif isinstance(err, errors.HooksFailure):
436 obuf.write("Failure: hooks general failure: %s" % msg)
437 elif isinstance(err, errors.ResolverError):
438 this_host = utils.HostInfo.SysName()
439 if err.args[0] == this_host:
440 msg = "Failure: can't resolve my own hostname ('%s')"
442 msg = "Failure: can't resolve hostname '%s'"
443 obuf.write(msg % err.args[0])
444 elif isinstance(err, errors.OpPrereqError):
445 obuf.write("Failure: prerequisites not met for this"
446 " operation:\n%s" % msg)
447 elif isinstance(err, errors.OpExecError):
448 obuf.write("Failure: command execution error:\n%s" % msg)
449 elif isinstance(err, errors.TagError):
450 obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
451 elif isinstance(err, errors.GenericError):
452 obuf.write("Unhandled Ganeti error: %s" % msg)
453 elif isinstance(err, luxi.NoMasterError):
454 obuf.write("Cannot communicate with the master daemon.\nIs it running"
455 " and listening on '%s'?" % err.args[0])
456 elif isinstance(err, luxi.TimeoutError):
457 obuf.write("Timeout while talking to the master daemon. Error:\n"
459 elif isinstance(err, luxi.ProtocolError):
460 obuf.write("Unhandled protocol error while talking to the master daemon:\n"
463 obuf.write("Unhandled exception: %s" % msg)
464 return retcode, obuf.getvalue().rstrip('\n')
467 def GenericMain(commands, override=None, aliases=None):
468 """Generic main function for all the gnt-* commands.
471 - commands: a dictionary with a special structure, see the design doc
472 for command line handling.
473 - override: if not None, we expect a dictionary with keys that will
474 override command line options; this can be used to pass
475 options from the scripts to generic functions
476 - aliases: dictionary with command aliases {'alias': 'target, ...}
479 # save the program name and the entire command line for later logging
481 binary = os.path.basename(sys.argv[0]) or sys.argv[0]
482 if len(sys.argv) >= 2:
483 binary += " " + sys.argv[1]
484 old_cmdline = " ".join(sys.argv[2:])
488 binary = "<unknown program>"
494 func, options, args = _ParseArgs(sys.argv, commands, aliases)
495 if func is None: # parse error
498 if override is not None:
499 for key, val in override.iteritems():
500 setattr(options, key, val)
502 logger.SetupLogging(program=binary, debug=options.debug)
504 utils.debug = options.debug
507 logger.Info("run with arguments '%s'" % old_cmdline)
509 logger.Info("run with no arguments")
512 result = func(options, args)
513 except (errors.GenericError, luxi.ProtocolError), err:
514 result, err_msg = FormatError(err)
515 logger.ToStderr(err_msg)
520 def GenerateTable(headers, fields, separator, data,
521 numfields=None, unitfields=None):
522 """Prints a table with headers and different fields.
525 headers: Dict of header titles or None if no headers should be shown
526 fields: List of fields to show
527 separator: String used to separate fields or None for spaces
528 data: Data to be printed
529 numfields: List of fields to be aligned to right
530 unitfields: List of fields to be formatted as units
533 if numfields is None:
535 if unitfields is None:
540 if headers and field not in headers:
541 raise errors.ProgrammerError("Missing header description for field '%s'"
543 if separator is not None:
544 format_fields.append("%s")
545 elif field in numfields:
546 format_fields.append("%*s")
548 format_fields.append("%-*s")
550 if separator is None:
551 mlens = [0 for name in fields]
552 format = ' '.join(format_fields)
554 format = separator.replace("%", "%%").join(format_fields)
557 for idx, val in enumerate(row):
558 if fields[idx] in unitfields:
564 val = row[idx] = utils.FormatUnit(val)
565 val = row[idx] = str(val)
566 if separator is None:
567 mlens[idx] = max(mlens[idx], len(val))
572 for idx, name in enumerate(fields):
574 if separator is None:
575 mlens[idx] = max(mlens[idx], len(hdr))
576 args.append(mlens[idx])
578 result.append(format % tuple(args))
582 for idx in xrange(len(fields)):
583 if separator is None:
584 args.append(mlens[idx])
585 args.append(line[idx])
586 result.append(format % tuple(args))