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 ganeti import utils
31 from ganeti import logger
32 from ganeti import errors
33 from ganeti import mcpu
34 from ganeti import constants
35 from ganeti import opcodes
37 from optparse import (OptionParser, make_option, TitledHelpFormatter,
38 Option, OptionValueError, SUPPRESS_HELP)
40 __all__ = ["DEBUG_OPT", "NOHDR_OPT", "SEP_OPT", "GenericMain", "SubmitOpCode",
41 "cli_option", "GenerateTable", "AskUser",
42 "ARGS_NONE", "ARGS_FIXED", "ARGS_ATLEAST", "ARGS_ANY", "ARGS_ONE",
43 "USEUNITS_OPT", "FIELDS_OPT", "FORCE_OPT",
44 "ListTags", "AddTags", "RemoveTags",
48 def _ExtractTagsObject(opts, args):
49 """Extract the tag type object.
51 Note that this function will modify its args parameter.
54 if not hasattr(opts, "tag_type"):
55 raise errors.ProgrammerError("tag_type not passed to _ExtractTagsObject")
57 if kind == constants.TAG_CLUSTER:
59 elif kind == constants.TAG_NODE or kind == constants.TAG_INSTANCE:
61 raise errors.OpPrereq("no arguments passed to the command")
65 raise errors.ProgrammerError("Unhandled tag type '%s'" % kind)
69 def ListTags(opts, args):
70 """List the tags on a given object.
72 This is a generic implementation that knows how to deal with all
73 three cases of tag objects (cluster, node, instance). The opts
74 argument is expected to contain a tag_type field denoting what
75 object type we work on.
78 kind, name = _ExtractTagsObject(opts, args)
79 op = opcodes.OpGetTags(kind=kind, name=name)
80 result = SubmitOpCode(op)
87 def AddTags(opts, args):
88 """Add tags on a given object.
90 This is a generic implementation that knows how to deal with all
91 three cases of tag objects (cluster, node, instance). The opts
92 argument is expected to contain a tag_type field denoting what
93 object type we work on.
96 kind, name = _ExtractTagsObject(opts, args)
98 raise errors.OpPrereqError("No tags to be added")
99 op = opcodes.OpAddTags(kind=kind, name=name, tags=args)
103 def RemoveTags(opts, args):
104 """Remove tags from 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)
114 raise errors.OpPrereqError("No tags to be removed")
115 op = opcodes.OpDelTags(kind=kind, name=name, tags=args)
119 DEBUG_OPT = make_option("-d", "--debug", default=False,
121 help="Turn debugging on")
123 NOHDR_OPT = make_option("--no-headers", default=False,
124 action="store_true", dest="no_headers",
125 help="Don't display column headers")
127 SEP_OPT = make_option("--separator", default=None,
128 action="store", dest="separator",
129 help="Separator between output fields"
130 " (defaults to one space)")
132 USEUNITS_OPT = make_option("--human-readable", default=False,
133 action="store_true", dest="human_readable",
134 help="Print sizes in human readable format")
136 FIELDS_OPT = make_option("-o", "--output", dest="output", action="store",
137 type="string", help="Select output fields",
140 FORCE_OPT = make_option("-f", "--force", dest="force", action="store_true",
141 default=False, help="Force the operation")
143 _LOCK_OPT = make_option("--lock-retries", default=None,
144 type="int", help=SUPPRESS_HELP)
147 """Macro-like function denoting a fixed number of arguments"""
151 def ARGS_ATLEAST(val):
152 """Macro-like function denoting a minimum number of arguments"""
157 ARGS_ONE = ARGS_FIXED(1)
158 ARGS_ANY = ARGS_ATLEAST(0)
161 def check_unit(option, opt, value):
163 return utils.ParseUnit(value)
164 except errors.UnitParseError, err:
165 raise OptionValueError("option %s: %s" % (opt, err))
168 class CliOption(Option):
169 TYPES = Option.TYPES + ("unit",)
170 TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER)
171 TYPE_CHECKER["unit"] = check_unit
174 # optparse.py sets make_option, so we do it for our own option class, too
175 cli_option = CliOption
178 def _ParseArgs(argv, commands):
179 """Parses the command line and return the function which must be
180 executed together with its arguments
183 argv: the command line
185 commands: dictionary with special contents, see the design doc for
192 binary = argv[0].split("/")[-1]
194 if len(argv) > 1 and argv[1] == "--version":
195 print "%s (ganeti) %s" % (binary, constants.RELEASE_VERSION)
196 # Quit right away. That way we don't have to care about this special
197 # argument. optparse.py does it the same.
200 if len(argv) < 2 or argv[1] not in commands.keys():
201 # let's do a nice thing
202 sortedcmds = commands.keys()
204 print ("Usage: %(bin)s {command} [options...] [argument...]"
205 "\n%(bin)s <command> --help to see details, or"
206 " man %(bin)s\n" % {"bin": binary})
207 # compute the max line length for cmd + usage
208 mlen = max([len(" %s %s" % (cmd, commands[cmd][3])) for cmd in commands])
209 mlen = min(60, mlen) # should not get here...
210 # and format a nice command list
212 for cmd in sortedcmds:
213 cmdstr = " %s %s" % (cmd, commands[cmd][3])
214 help_text = commands[cmd][4]
215 help_lines = textwrap.wrap(help_text, 79-3-mlen)
216 print "%-*s - %s" % (mlen, cmdstr,
218 for line in help_lines:
219 print "%-*s %s" % (mlen, "", line)
221 return None, None, None
223 func, nargs, parser_opts, usage, description = commands[cmd]
224 parser_opts.append(_LOCK_OPT)
225 parser = OptionParser(option_list=parser_opts,
226 description=description,
227 formatter=TitledHelpFormatter(),
228 usage="%%prog %s %s" % (cmd, usage))
229 parser.disable_interspersed_args()
230 options, args = parser.parse_args()
233 print >> sys.stderr, ("Error: Command %s expects no arguments" % cmd)
234 return None, None, None
235 elif nargs < 0 and len(args) != -nargs:
236 print >> sys.stderr, ("Error: Command %s expects %d argument(s)" %
238 return None, None, None
239 elif nargs >= 0 and len(args) < nargs:
240 print >> sys.stderr, ("Error: Command %s expects at least %d argument(s)" %
242 return None, None, None
244 return func, options, args
247 def AskUser(text, choices=None):
248 """Ask the user a question.
251 text - the question to ask.
253 choices - list with elements tuples (input_char, return_value,
254 description); if not given, it will default to: [('y', True,
255 'Perform the operation'), ('n', False, 'Do no do the operation')];
256 note that the '?' char is reserved for help
258 Returns: one of the return values from the choices list; if input is
259 not possible (i.e. not running with a tty, we return the last entry
264 choices = [('y', True, 'Perform the operation'),
265 ('n', False, 'Do not perform the operation')]
266 if not choices or not isinstance(choices, list):
267 raise errors.ProgrammerError("Invalid choiches argument to AskUser")
268 for entry in choices:
269 if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
270 raise errors.ProgrammerError("Invalid choiches element to AskUser")
272 answer = choices[-1][1]
274 for line in text.splitlines():
275 new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
276 text = "\n".join(new_text)
278 f = file("/dev/tty", "r+")
282 chars = [entry[0] for entry in choices]
283 chars[-1] = "[%s]" % chars[-1]
285 maps = dict([(entry[0], entry[1]) for entry in choices])
289 f.write("/".join(chars))
291 line = f.readline(2).strip().lower()
296 for entry in choices:
297 f.write(" %s - %s\n" % (entry[0], entry[2]))
305 def SubmitOpCode(op):
306 """Function to submit an opcode.
308 This is just a simple wrapper over the construction of the processor
309 instance. It should be extended to better handle feedback and
310 interaction functions.
313 proc = mcpu.Processor()
314 return proc.ExecOpCode(op, logger.ToStdout)
317 def GenericMain(commands, override=None):
318 """Generic main function for all the gnt-* commands.
321 - commands: a dictionary with a special structure, see the design doc
322 for command line handling.
323 - override: if not None, we expect a dictionary with keys that will
324 override command line options; this can be used to pass
325 options from the scripts to generic functions
328 # save the program name and the entire command line for later logging
330 binary = os.path.basename(sys.argv[0]) or sys.argv[0]
331 if len(sys.argv) >= 2:
332 binary += " " + sys.argv[1]
333 old_cmdline = " ".join(sys.argv[2:])
337 binary = "<unknown program>"
340 func, options, args = _ParseArgs(sys.argv, commands)
341 if func is None: # parse error
344 if override is not None:
345 for key, val in override.iteritems():
346 setattr(options, key, val)
348 logger.SetupLogging(debug=options.debug, program=binary)
351 utils.Lock('cmd', max_retries=options.lock_retries, debug=options.debug)
352 except errors.LockError, err:
353 logger.ToStderr(str(err))
357 logger.Info("run with arguments '%s'" % old_cmdline)
359 logger.Info("run with no arguments")
363 result = func(options, args)
364 except errors.ConfigurationError, err:
365 logger.Error("Corrupt configuration file: %s" % err)
366 logger.ToStderr("Aborting.")
368 except errors.HooksAbort, err:
369 logger.ToStderr("Failure: hooks execution failed:")
370 for node, script, out in err.args[0]:
372 logger.ToStderr(" node: %s, script: %s, output: %s" %
375 logger.ToStderr(" node: %s, script: %s (no output)" %
378 except errors.HooksFailure, err:
379 logger.ToStderr("Failure: hooks general failure: %s" % str(err))
381 except errors.ResolverError, err:
382 this_host = utils.HostInfo.SysName()
383 if err.args[0] == this_host:
384 msg = "Failure: can't resolve my own hostname ('%s')"
386 msg = "Failure: can't resolve hostname '%s'"
387 logger.ToStderr(msg % err.args[0])
389 except errors.OpPrereqError, err:
390 logger.ToStderr("Failure: prerequisites not met for this"
391 " operation:\n%s" % str(err))
393 except errors.OpExecError, err:
394 logger.ToStderr("Failure: command execution error:\n%s" % str(err))
403 def GenerateTable(headers, fields, separator, data,
404 numfields=None, unitfields=None):
405 """Prints a table with headers and different fields.
408 headers: Dict of header titles or None if no headers should be shown
409 fields: List of fields to show
410 separator: String used to separate fields or None for spaces
411 data: Data to be printed
412 numfields: List of fields to be aligned to right
413 unitfields: List of fields to be formatted as units
416 if numfields is None:
418 if unitfields is None:
423 if separator is not None:
424 format_fields.append("%s")
425 elif field in numfields:
426 format_fields.append("%*s")
428 format_fields.append("%-*s")
430 if separator is None:
431 mlens = [0 for name in fields]
432 format = ' '.join(format_fields)
434 format = separator.replace("%", "%%").join(format_fields)
437 for idx, val in enumerate(row):
438 if fields[idx] in unitfields:
444 val = row[idx] = utils.FormatUnit(val)
445 if separator is None:
446 mlens[idx] = max(mlens[idx], len(val))
451 for idx, name in enumerate(fields):
453 if separator is None:
454 mlens[idx] = max(mlens[idx], len(hdr))
455 args.append(mlens[idx])
457 result.append(format % tuple(args))
461 for idx in xrange(len(fields)):
462 if separator is None:
463 args.append(mlens[idx])
464 args.append(line[idx])
465 result.append(format % tuple(args))