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", "GetClient",
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])
387 jobs = cl.QueryJobs([job_id], ["status", "ticker"])
389 # job not found, go away!
390 raise errors.JobLost("Job with id %s lost" % job_id)
392 # TODO: Handle canceled and archived jobs
394 if status in (constants.JOB_STATUS_SUCCESS, constants.JOB_STATUS_ERROR):
397 if msg is not None and msg != lastmsg:
398 print "%s %s" % (time.ctime(msg[0]), msg[2])
402 jobs = cl.QueryJobs([job_id], ["status", "opresult"])
404 raise errors.JobLost("Job with id %s lost" % job_id)
406 status, result = jobs[0]
407 if status == constants.JOB_STATUS_SUCCESS:
410 raise errors.OpExecError(result)
414 # TODO: Cache object?
418 def FormatError(err):
419 """Return a formatted error message for a given error.
421 This function takes an exception instance and returns a tuple
422 consisting of two values: first, the recommended exit code, and
423 second, a string describing the error message (not
430 if isinstance(err, errors.ConfigurationError):
431 txt = "Corrupt configuration file: %s" % msg
433 obuf.write(txt + "\n")
434 obuf.write("Aborting.")
436 elif isinstance(err, errors.HooksAbort):
437 obuf.write("Failure: hooks execution failed:\n")
438 for node, script, out in err.args[0]:
440 obuf.write(" node: %s, script: %s, output: %s\n" %
443 obuf.write(" node: %s, script: %s (no output)\n" %
445 elif isinstance(err, errors.HooksFailure):
446 obuf.write("Failure: hooks general failure: %s" % msg)
447 elif isinstance(err, errors.ResolverError):
448 this_host = utils.HostInfo.SysName()
449 if err.args[0] == this_host:
450 msg = "Failure: can't resolve my own hostname ('%s')"
452 msg = "Failure: can't resolve hostname '%s'"
453 obuf.write(msg % err.args[0])
454 elif isinstance(err, errors.OpPrereqError):
455 obuf.write("Failure: prerequisites not met for this"
456 " operation:\n%s" % msg)
457 elif isinstance(err, errors.OpExecError):
458 obuf.write("Failure: command execution error:\n%s" % msg)
459 elif isinstance(err, errors.TagError):
460 obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
461 elif isinstance(err, errors.GenericError):
462 obuf.write("Unhandled Ganeti error: %s" % msg)
463 elif isinstance(err, luxi.NoMasterError):
464 obuf.write("Cannot communicate with the master daemon.\nIs it running"
465 " and listening on '%s'?" % err.args[0])
466 elif isinstance(err, luxi.TimeoutError):
467 obuf.write("Timeout while talking to the master daemon. Error:\n"
469 elif isinstance(err, luxi.ProtocolError):
470 obuf.write("Unhandled protocol error while talking to the master daemon:\n"
473 obuf.write("Unhandled exception: %s" % msg)
474 return retcode, obuf.getvalue().rstrip('\n')
477 def GenericMain(commands, override=None, aliases=None):
478 """Generic main function for all the gnt-* commands.
481 - commands: a dictionary with a special structure, see the design doc
482 for command line handling.
483 - override: if not None, we expect a dictionary with keys that will
484 override command line options; this can be used to pass
485 options from the scripts to generic functions
486 - aliases: dictionary with command aliases {'alias': 'target, ...}
489 # save the program name and the entire command line for later logging
491 binary = os.path.basename(sys.argv[0]) or sys.argv[0]
492 if len(sys.argv) >= 2:
493 binary += " " + sys.argv[1]
494 old_cmdline = " ".join(sys.argv[2:])
498 binary = "<unknown program>"
504 func, options, args = _ParseArgs(sys.argv, commands, aliases)
505 if func is None: # parse error
508 if override is not None:
509 for key, val in override.iteritems():
510 setattr(options, key, val)
512 logger.SetupLogging(program=binary, debug=options.debug)
514 utils.debug = options.debug
517 logger.Info("run with arguments '%s'" % old_cmdline)
519 logger.Info("run with no arguments")
522 result = func(options, args)
523 except (errors.GenericError, luxi.ProtocolError), err:
524 result, err_msg = FormatError(err)
525 logger.ToStderr(err_msg)
530 def GenerateTable(headers, fields, separator, data,
531 numfields=None, unitfields=None):
532 """Prints a table with headers and different fields.
535 headers: Dict of header titles or None if no headers should be shown
536 fields: List of fields to show
537 separator: String used to separate fields or None for spaces
538 data: Data to be printed
539 numfields: List of fields to be aligned to right
540 unitfields: List of fields to be formatted as units
543 if numfields is None:
545 if unitfields is None:
550 if headers and field not in headers:
551 raise errors.ProgrammerError("Missing header description for field '%s'"
553 if separator is not None:
554 format_fields.append("%s")
555 elif field in numfields:
556 format_fields.append("%*s")
558 format_fields.append("%-*s")
560 if separator is None:
561 mlens = [0 for name in fields]
562 format = ' '.join(format_fields)
564 format = separator.replace("%", "%%").join(format_fields)
567 for idx, val in enumerate(row):
568 if fields[idx] in unitfields:
574 val = row[idx] = utils.FormatUnit(val)
575 val = row[idx] = str(val)
576 if separator is None:
577 mlens[idx] = max(mlens[idx], len(val))
582 for idx, name in enumerate(fields):
584 if separator is None:
585 mlens[idx] = max(mlens[idx], len(hdr))
586 args.append(mlens[idx])
588 result.append(format % tuple(args))
592 for idx in xrange(len(fields)):
593 if separator is None:
594 args.append(mlens[idx])
595 args.append(line[idx])
596 result.append(format % tuple(args))