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 %s" % (cmd, commands[cmd][3])) 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 %s" % (cmd, commands[cmd][3])
260 help_text = commands[cmd][4]
261 help_lines = textwrap.wrap(help_text, 79-3-mlen)
262 print "%-*s - %s" % (mlen, cmdstr,
264 for line in help_lines:
265 print "%-*s %s" % (mlen, "", line)
267 return None, None, None
269 # get command, unalias it, and look it up in commands
273 raise errors.ProgrammerError("Alias '%s' overrides an existing"
276 if aliases[cmd] not in commands:
277 raise errors.ProgrammerError("Alias '%s' maps to non-existing"
278 " command '%s'" % (cmd, aliases[cmd]))
282 func, nargs, parser_opts, usage, description = commands[cmd]
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.
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])
411 if feedback_fn is None:
412 feedback_fn = logger.ToStdout
414 proc = mcpu.Processor(feedback=feedback_fn)
415 return proc.ExecOpCode(op)
418 def SubmitJob(job, cl=None):
421 return cl.SubmitJob(job)
424 def SubmitQuery(data, cl=None):
427 return cl.Query(data)
430 def FormatError(err):
431 """Return a formatted error message for a given error.
433 This function takes an exception instance and returns a tuple
434 consisting of two values: first, the recommended exit code, and
435 second, a string describing the error message (not
442 if isinstance(err, errors.ConfigurationError):
443 txt = "Corrupt configuration file: %s" % msg
445 obuf.write(txt + "\n")
446 obuf.write("Aborting.")
448 elif isinstance(err, errors.HooksAbort):
449 obuf.write("Failure: hooks execution failed:\n")
450 for node, script, out in err.args[0]:
452 obuf.write(" node: %s, script: %s, output: %s\n" %
455 obuf.write(" node: %s, script: %s (no output)\n" %
457 elif isinstance(err, errors.HooksFailure):
458 obuf.write("Failure: hooks general failure: %s" % msg)
459 elif isinstance(err, errors.ResolverError):
460 this_host = utils.HostInfo.SysName()
461 if err.args[0] == this_host:
462 msg = "Failure: can't resolve my own hostname ('%s')"
464 msg = "Failure: can't resolve hostname '%s'"
465 obuf.write(msg % err.args[0])
466 elif isinstance(err, errors.OpPrereqError):
467 obuf.write("Failure: prerequisites not met for this"
468 " operation:\n%s" % msg)
469 elif isinstance(err, errors.OpExecError):
470 obuf.write("Failure: command execution error:\n%s" % msg)
471 elif isinstance(err, errors.TagError):
472 obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
473 elif isinstance(err, errors.GenericError):
474 obuf.write("Unhandled Ganeti error: %s" % msg)
476 obuf.write("Unhandled exception: %s" % msg)
477 return retcode, obuf.getvalue().rstrip('\n')
480 def GenericMain(commands, override=None, aliases=None):
481 """Generic main function for all the gnt-* commands.
484 - commands: a dictionary with a special structure, see the design doc
485 for command line handling.
486 - override: if not None, we expect a dictionary with keys that will
487 override command line options; this can be used to pass
488 options from the scripts to generic functions
489 - aliases: dictionary with command aliases {'alias': 'target, ...}
492 # save the program name and the entire command line for later logging
494 binary = os.path.basename(sys.argv[0]) or sys.argv[0]
495 if len(sys.argv) >= 2:
496 binary += " " + sys.argv[1]
497 old_cmdline = " ".join(sys.argv[2:])
501 binary = "<unknown program>"
507 func, options, args = _ParseArgs(sys.argv, commands, aliases)
508 if func is None: # parse error
511 if override is not None:
512 for key, val in override.iteritems():
513 setattr(options, key, val)
515 logger.SetupLogging(debug=options.debug, 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, 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))