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 constants
36 from ganeti import opcodes
37 from ganeti import luxi
39 from optparse import (OptionParser, make_option, TitledHelpFormatter,
40 Option, OptionValueError)
42 __all__ = ["DEBUG_OPT", "NOHDR_OPT", "SEP_OPT", "GenericMain",
43 "SubmitOpCode", "GetClient",
44 "cli_option", "GenerateTable", "AskUser",
45 "ARGS_NONE", "ARGS_FIXED", "ARGS_ATLEAST", "ARGS_ANY", "ARGS_ONE",
46 "USEUNITS_OPT", "FIELDS_OPT", "FORCE_OPT",
47 "ListTags", "AddTags", "RemoveTags", "TAG_SRC_OPT",
48 "FormatError", "SplitNodeOption"
52 def _ExtractTagsObject(opts, args):
53 """Extract the tag type object.
55 Note that this function will modify its args parameter.
58 if not hasattr(opts, "tag_type"):
59 raise errors.ProgrammerError("tag_type not passed to _ExtractTagsObject")
61 if kind == constants.TAG_CLUSTER:
63 elif kind == constants.TAG_NODE or kind == constants.TAG_INSTANCE:
65 raise errors.OpPrereqError("no arguments passed to the command")
69 raise errors.ProgrammerError("Unhandled tag type '%s'" % kind)
73 def _ExtendTags(opts, args):
74 """Extend the args if a source file has been given.
76 This function will extend the tags with the contents of the file
77 passed in the 'tags_source' attribute of the opts parameter. A file
78 named '-' will be replaced by stdin.
81 fname = opts.tags_source
87 new_fh = open(fname, "r")
90 # we don't use the nice 'new_data = [line.strip() for line in fh]'
91 # because of python bug 1633941
93 line = new_fh.readline()
96 new_data.append(line.strip())
102 def ListTags(opts, args):
103 """List the tags on a given object.
105 This is a generic implementation that knows how to deal with all
106 three cases of tag objects (cluster, node, instance). The opts
107 argument is expected to contain a tag_type field denoting what
108 object type we work on.
111 kind, name = _ExtractTagsObject(opts, args)
112 op = opcodes.OpGetTags(kind=kind, name=name)
113 result = SubmitOpCode(op)
114 result = list(result)
120 def AddTags(opts, args):
121 """Add tags on a given object.
123 This is a generic implementation that knows how to deal with all
124 three cases of tag objects (cluster, node, instance). The opts
125 argument is expected to contain a tag_type field denoting what
126 object type we work on.
129 kind, name = _ExtractTagsObject(opts, args)
130 _ExtendTags(opts, args)
132 raise errors.OpPrereqError("No tags to be added")
133 op = opcodes.OpAddTags(kind=kind, name=name, tags=args)
137 def RemoveTags(opts, args):
138 """Remove tags from a given object.
140 This is a generic implementation that knows how to deal with all
141 three cases of tag objects (cluster, node, instance). The opts
142 argument is expected to contain a tag_type field denoting what
143 object type we work on.
146 kind, name = _ExtractTagsObject(opts, args)
147 _ExtendTags(opts, args)
149 raise errors.OpPrereqError("No tags to be removed")
150 op = opcodes.OpDelTags(kind=kind, name=name, tags=args)
154 DEBUG_OPT = make_option("-d", "--debug", default=False,
156 help="Turn debugging on")
158 NOHDR_OPT = make_option("--no-headers", default=False,
159 action="store_true", dest="no_headers",
160 help="Don't display column headers")
162 SEP_OPT = make_option("--separator", default=None,
163 action="store", dest="separator",
164 help="Separator between output fields"
165 " (defaults to one space)")
167 USEUNITS_OPT = make_option("--human-readable", default=False,
168 action="store_true", dest="human_readable",
169 help="Print sizes in human readable format")
171 FIELDS_OPT = make_option("-o", "--output", dest="output", action="store",
172 type="string", help="Comma separated list of"
176 FORCE_OPT = make_option("-f", "--force", dest="force", action="store_true",
177 default=False, help="Force the operation")
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" % cmd) 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" % (cmd,)
259 help_text = commands[cmd][4]
260 help_lines = textwrap.wrap(help_text, 79-3-mlen)
261 print "%-*s - %s" % (mlen, cmdstr, help_lines.pop(0))
262 for line in help_lines:
263 print "%-*s %s" % (mlen, "", line)
265 return None, None, None
267 # get command, unalias it, and look it up in commands
271 raise errors.ProgrammerError("Alias '%s' overrides an existing"
274 if aliases[cmd] not in commands:
275 raise errors.ProgrammerError("Alias '%s' maps to non-existing"
276 " command '%s'" % (cmd, aliases[cmd]))
280 func, nargs, parser_opts, usage, description = commands[cmd]
281 parser = OptionParser(option_list=parser_opts,
282 description=description,
283 formatter=TitledHelpFormatter(),
284 usage="%%prog %s %s" % (cmd, usage))
285 parser.disable_interspersed_args()
286 options, args = parser.parse_args()
289 print >> sys.stderr, ("Error: Command %s expects no arguments" % cmd)
290 return None, None, None
291 elif nargs < 0 and len(args) != -nargs:
292 print >> sys.stderr, ("Error: Command %s expects %d argument(s)" %
294 return None, None, None
295 elif nargs >= 0 and len(args) < nargs:
296 print >> sys.stderr, ("Error: Command %s expects at least %d argument(s)" %
298 return None, None, None
300 return func, options, args
303 def SplitNodeOption(value):
304 """Splits the value of a --node option.
307 if value and ':' in value:
308 return value.split(':', 1)
313 def AskUser(text, choices=None):
314 """Ask the user a question.
317 text - the question to ask.
319 choices - list with elements tuples (input_char, return_value,
320 description); if not given, it will default to: [('y', True,
321 'Perform the operation'), ('n', False, 'Do no do the operation')];
322 note that the '?' char is reserved for help
324 Returns: one of the return values from the choices list; if input is
325 not possible (i.e. not running with a tty, we return the last entry
330 choices = [('y', True, 'Perform the operation'),
331 ('n', False, 'Do not perform the operation')]
332 if not choices or not isinstance(choices, list):
333 raise errors.ProgrammerError("Invalid choiches argument to AskUser")
334 for entry in choices:
335 if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
336 raise errors.ProgrammerError("Invalid choiches element to AskUser")
338 answer = choices[-1][1]
340 for line in text.splitlines():
341 new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
342 text = "\n".join(new_text)
344 f = file("/dev/tty", "a+")
348 chars = [entry[0] for entry in choices]
349 chars[-1] = "[%s]" % chars[-1]
351 maps = dict([(entry[0], entry[1]) for entry in choices])
355 f.write("/".join(chars))
357 line = f.readline(2).strip().lower()
362 for entry in choices:
363 f.write(" %s - %s\n" % (entry[0], entry[2]))
371 def SubmitOpCode(op, cl=None, feedback_fn=None):
372 """Legacy function to submit an opcode.
374 This is just a simple wrapper over the construction of the processor
375 instance. It should be extended to better handle feedback and
376 interaction functions.
382 job_id = cl.SubmitJob([op])
386 jobs = cl.QueryJobs([job_id], ["status", "ticker"])
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):
396 if msg is not None and msg != lastmsg:
397 if callable(feedback_fn):
400 print "%s %s" % (time.ctime(msg[0]), msg[2])
404 jobs = cl.QueryJobs([job_id], ["status", "opresult"])
406 raise errors.JobLost("Job with id %s lost" % job_id)
408 status, result = jobs[0]
409 if status == constants.JOB_STATUS_SUCCESS:
412 raise errors.OpExecError(result)
416 # TODO: Cache object?
420 def FormatError(err):
421 """Return a formatted error message for a given error.
423 This function takes an exception instance and returns a tuple
424 consisting of two values: first, the recommended exit code, and
425 second, a string describing the error message (not
432 if isinstance(err, errors.ConfigurationError):
433 txt = "Corrupt configuration file: %s" % msg
435 obuf.write(txt + "\n")
436 obuf.write("Aborting.")
438 elif isinstance(err, errors.HooksAbort):
439 obuf.write("Failure: hooks execution failed:\n")
440 for node, script, out in err.args[0]:
442 obuf.write(" node: %s, script: %s, output: %s\n" %
445 obuf.write(" node: %s, script: %s (no output)\n" %
447 elif isinstance(err, errors.HooksFailure):
448 obuf.write("Failure: hooks general failure: %s" % msg)
449 elif isinstance(err, errors.ResolverError):
450 this_host = utils.HostInfo.SysName()
451 if err.args[0] == this_host:
452 msg = "Failure: can't resolve my own hostname ('%s')"
454 msg = "Failure: can't resolve hostname '%s'"
455 obuf.write(msg % err.args[0])
456 elif isinstance(err, errors.OpPrereqError):
457 obuf.write("Failure: prerequisites not met for this"
458 " operation:\n%s" % msg)
459 elif isinstance(err, errors.OpExecError):
460 obuf.write("Failure: command execution error:\n%s" % msg)
461 elif isinstance(err, errors.TagError):
462 obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
463 elif isinstance(err, errors.GenericError):
464 obuf.write("Unhandled Ganeti error: %s" % msg)
465 elif isinstance(err, luxi.NoMasterError):
466 obuf.write("Cannot communicate with the master daemon.\nIs it running"
467 " and listening on '%s'?" % err.args[0])
468 elif isinstance(err, luxi.TimeoutError):
469 obuf.write("Timeout while talking to the master daemon. Error:\n"
471 elif isinstance(err, luxi.ProtocolError):
472 obuf.write("Unhandled protocol error while talking to the master daemon:\n"
475 obuf.write("Unhandled exception: %s" % msg)
476 return retcode, obuf.getvalue().rstrip('\n')
479 def GenericMain(commands, override=None, aliases=None):
480 """Generic main function for all the gnt-* commands.
483 - commands: a dictionary with a special structure, see the design doc
484 for command line handling.
485 - override: if not None, we expect a dictionary with keys that will
486 override command line options; this can be used to pass
487 options from the scripts to generic functions
488 - aliases: dictionary with command aliases {'alias': 'target, ...}
491 # save the program name and the entire command line for later logging
493 binary = os.path.basename(sys.argv[0]) or sys.argv[0]
494 if len(sys.argv) >= 2:
495 binary += " " + sys.argv[1]
496 old_cmdline = " ".join(sys.argv[2:])
500 binary = "<unknown program>"
506 func, options, args = _ParseArgs(sys.argv, commands, aliases)
507 if func is None: # parse error
510 if override is not None:
511 for key, val in override.iteritems():
512 setattr(options, key, val)
514 logger.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
515 stderr_logging=True, program=binary)
517 utils.debug = options.debug
520 logger.Info("run with arguments '%s'" % old_cmdline)
522 logger.Info("run with no arguments")
525 result = func(options, args)
526 except (errors.GenericError, luxi.ProtocolError), err:
527 result, err_msg = FormatError(err)
528 logger.ToStderr(err_msg)
533 def GenerateTable(headers, fields, separator, data,
534 numfields=None, unitfields=None):
535 """Prints a table with headers and different fields.
538 headers: Dict of header titles or None if no headers should be shown
539 fields: List of fields to show
540 separator: String used to separate fields or None for spaces
541 data: Data to be printed
542 numfields: List of fields to be aligned to right
543 unitfields: List of fields to be formatted as units
546 if numfields is None:
548 if unitfields is None:
553 if headers and field not in headers:
554 raise errors.ProgrammerError("Missing header description for field '%s'"
556 if separator is not None:
557 format_fields.append("%s")
558 elif field in numfields:
559 format_fields.append("%*s")
561 format_fields.append("%-*s")
563 if separator is None:
564 mlens = [0 for name in fields]
565 format = ' '.join(format_fields)
567 format = separator.replace("%", "%%").join(format_fields)
570 for idx, val in enumerate(row):
571 if fields[idx] in unitfields:
577 val = row[idx] = utils.FormatUnit(val)
578 val = row[idx] = str(val)
579 if separator is None:
580 mlens[idx] = max(mlens[idx], len(val))
585 for idx, name in enumerate(fields):
587 if separator is None:
588 mlens[idx] = max(mlens[idx], len(hdr))
589 args.append(mlens[idx])
591 result.append(format % tuple(args))
595 for idx in xrange(len(fields)):
596 if separator is None:
597 args.append(mlens[idx])
598 args.append(line[idx])
599 result.append(format % tuple(args))