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",
44 "SubmitOpCode", "SubmitJob", "SubmitQuery",
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 """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.
382 job = opcodes.Job(op_list=[op])
387 "fields": ["status"],
392 jdata = SubmitQuery(query)
394 # job not found, go away!
395 raise errors.JobLost("Job with id %s lost" % jid)
398 if status in (opcodes.Job.STATUS_SUCCESS, opcodes.Job.STATUS_FAIL):
402 query["fields"].extend(["op_list", "op_status", "op_result"])
403 jdata = SubmitQuery(query)
405 raise errors.JobLost("Job with id %s lost" % jid)
406 status, op_list, op_status, op_result = jdata[0]
407 if status != opcodes.Job.STATUS_SUCCESS:
408 raise errors.OpExecError(op_result[0])
412 def SubmitJob(job, cl=None):
415 return cl.SubmitJob(job)
418 def SubmitQuery(data, cl=None):
421 return cl.Query(data)
424 def FormatError(err):
425 """Return a formatted error message for a given error.
427 This function takes an exception instance and returns a tuple
428 consisting of two values: first, the recommended exit code, and
429 second, a string describing the error message (not
436 if isinstance(err, errors.ConfigurationError):
437 txt = "Corrupt configuration file: %s" % msg
439 obuf.write(txt + "\n")
440 obuf.write("Aborting.")
442 elif isinstance(err, errors.HooksAbort):
443 obuf.write("Failure: hooks execution failed:\n")
444 for node, script, out in err.args[0]:
446 obuf.write(" node: %s, script: %s, output: %s\n" %
449 obuf.write(" node: %s, script: %s (no output)\n" %
451 elif isinstance(err, errors.HooksFailure):
452 obuf.write("Failure: hooks general failure: %s" % msg)
453 elif isinstance(err, errors.ResolverError):
454 this_host = utils.HostInfo.SysName()
455 if err.args[0] == this_host:
456 msg = "Failure: can't resolve my own hostname ('%s')"
458 msg = "Failure: can't resolve hostname '%s'"
459 obuf.write(msg % err.args[0])
460 elif isinstance(err, errors.OpPrereqError):
461 obuf.write("Failure: prerequisites not met for this"
462 " operation:\n%s" % msg)
463 elif isinstance(err, errors.OpExecError):
464 obuf.write("Failure: command execution error:\n%s" % msg)
465 elif isinstance(err, errors.TagError):
466 obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
467 elif isinstance(err, errors.GenericError):
468 obuf.write("Unhandled Ganeti error: %s" % msg)
469 elif isinstance(err, luxi.NoMasterError):
470 obuf.write("Cannot communicate with the master daemon.\nIs it running"
471 " and listening on '%s'?" % err.args[0])
472 elif isinstance(err, luxi.TimeoutError):
473 obuf.write("Timeout while talking to the master daemon. Error:\n"
475 elif isinstance(err, luxi.ProtocolError):
476 obuf.write("Unhandled protocol error while talking to the master daemon:\n"
479 obuf.write("Unhandled exception: %s" % msg)
480 return retcode, obuf.getvalue().rstrip('\n')
483 def GenericMain(commands, override=None, aliases=None):
484 """Generic main function for all the gnt-* commands.
487 - commands: a dictionary with a special structure, see the design doc
488 for command line handling.
489 - override: if not None, we expect a dictionary with keys that will
490 override command line options; this can be used to pass
491 options from the scripts to generic functions
492 - aliases: dictionary with command aliases {'alias': 'target, ...}
495 # save the program name and the entire command line for later logging
497 binary = os.path.basename(sys.argv[0]) or sys.argv[0]
498 if len(sys.argv) >= 2:
499 binary += " " + sys.argv[1]
500 old_cmdline = " ".join(sys.argv[2:])
504 binary = "<unknown program>"
510 func, options, args = _ParseArgs(sys.argv, commands, aliases)
511 if func is None: # parse error
514 if override is not None:
515 for key, val in override.iteritems():
516 setattr(options, key, val)
518 logger.SetupLogging(program=binary, debug=options.debug)
520 utils.debug = options.debug
523 logger.Info("run with arguments '%s'" % old_cmdline)
525 logger.Info("run with no arguments")
528 result = func(options, args)
529 except (errors.GenericError, luxi.ProtocolError), err:
530 result, err_msg = FormatError(err)
531 logger.ToStderr(err_msg)
536 def GenerateTable(headers, fields, separator, data,
537 numfields=None, unitfields=None):
538 """Prints a table with headers and different fields.
541 headers: Dict of header titles or None if no headers should be shown
542 fields: List of fields to show
543 separator: String used to separate fields or None for spaces
544 data: Data to be printed
545 numfields: List of fields to be aligned to right
546 unitfields: List of fields to be formatted as units
549 if numfields is None:
551 if unitfields is None:
556 if headers and field not in headers:
557 raise errors.ProgrammerError("Missing header description for field '%s'"
559 if separator is not None:
560 format_fields.append("%s")
561 elif field in numfields:
562 format_fields.append("%*s")
564 format_fields.append("%-*s")
566 if separator is None:
567 mlens = [0 for name in fields]
568 format = ' '.join(format_fields)
570 format = separator.replace("%", "%%").join(format_fields)
573 for idx, val in enumerate(row):
574 if fields[idx] in unitfields:
580 val = row[idx] = utils.FormatUnit(val)
581 val = row[idx] = str(val)
582 if separator is None:
583 mlens[idx] = max(mlens[idx], len(val))
588 for idx, name in enumerate(fields):
590 if separator is None:
591 mlens[idx] = max(mlens[idx], len(hdr))
592 args.append(mlens[idx])
594 result.append(format % tuple(args))
598 for idx in xrange(len(fields)):
599 if separator is None:
600 args.append(mlens[idx])
601 args.append(line[idx])
602 result.append(format % tuple(args))