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", "TAG_SRC_OPT",
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 _ExtendTags(opts, args):
70 """Extend the args if a source file has been given.
72 This function will extend the tags with the contents of the file
73 passed in the 'tags_source' attribute of the opts parameter. A file
74 named '-' will be replaced by stdin.
77 fname = opts.tags_source
83 new_fh = open(fname, "r")
86 # we don't use the nice 'new_data = [line.strip() for line in fh]'
87 # because of python bug 1633941
89 line = new_fh.readline()
92 new_data.append(line.strip())
98 def ListTags(opts, args):
99 """List the tags on a given object.
101 This is a generic implementation that knows how to deal with all
102 three cases of tag objects (cluster, node, instance). The opts
103 argument is expected to contain a tag_type field denoting what
104 object type we work on.
107 kind, name = _ExtractTagsObject(opts, args)
108 op = opcodes.OpGetTags(kind=kind, name=name)
109 result = SubmitOpCode(op)
110 result = list(result)
116 def AddTags(opts, args):
117 """Add tags on a given object.
119 This is a generic implementation that knows how to deal with all
120 three cases of tag objects (cluster, node, instance). The opts
121 argument is expected to contain a tag_type field denoting what
122 object type we work on.
125 kind, name = _ExtractTagsObject(opts, args)
126 _ExtendTags(opts, args)
128 raise errors.OpPrereqError("No tags to be added")
129 op = opcodes.OpAddTags(kind=kind, name=name, tags=args)
133 def RemoveTags(opts, args):
134 """Remove tags from a given object.
136 This is a generic implementation that knows how to deal with all
137 three cases of tag objects (cluster, node, instance). The opts
138 argument is expected to contain a tag_type field denoting what
139 object type we work on.
142 kind, name = _ExtractTagsObject(opts, args)
143 _ExtendTags(opts, args)
145 raise errors.OpPrereqError("No tags to be removed")
146 op = opcodes.OpDelTags(kind=kind, name=name, tags=args)
150 DEBUG_OPT = make_option("-d", "--debug", default=False,
152 help="Turn debugging on")
154 NOHDR_OPT = make_option("--no-headers", default=False,
155 action="store_true", dest="no_headers",
156 help="Don't display column headers")
158 SEP_OPT = make_option("--separator", default=None,
159 action="store", dest="separator",
160 help="Separator between output fields"
161 " (defaults to one space)")
163 USEUNITS_OPT = make_option("--human-readable", default=False,
164 action="store_true", dest="human_readable",
165 help="Print sizes in human readable format")
167 FIELDS_OPT = make_option("-o", "--output", dest="output", action="store",
168 type="string", help="Select output fields",
171 FORCE_OPT = make_option("-f", "--force", dest="force", action="store_true",
172 default=False, help="Force the operation")
174 _LOCK_OPT = make_option("--lock-retries", default=None,
175 type="int", help=SUPPRESS_HELP)
177 TAG_SRC_OPT = make_option("--from", dest="tags_source",
178 default=None, help="File with tag names")
181 """Macro-like function denoting a fixed number of arguments"""
185 def ARGS_ATLEAST(val):
186 """Macro-like function denoting a minimum number of arguments"""
191 ARGS_ONE = ARGS_FIXED(1)
192 ARGS_ANY = ARGS_ATLEAST(0)
195 def check_unit(option, opt, value):
197 return utils.ParseUnit(value)
198 except errors.UnitParseError, err:
199 raise OptionValueError("option %s: %s" % (opt, err))
202 class CliOption(Option):
203 TYPES = Option.TYPES + ("unit",)
204 TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER)
205 TYPE_CHECKER["unit"] = check_unit
208 # optparse.py sets make_option, so we do it for our own option class, too
209 cli_option = CliOption
212 def _ParseArgs(argv, commands):
213 """Parses the command line and return the function which must be
214 executed together with its arguments
217 argv: the command line
219 commands: dictionary with special contents, see the design doc for
226 binary = argv[0].split("/")[-1]
228 if len(argv) > 1 and argv[1] == "--version":
229 print "%s (ganeti) %s" % (binary, constants.RELEASE_VERSION)
230 # Quit right away. That way we don't have to care about this special
231 # argument. optparse.py does it the same.
234 if len(argv) < 2 or argv[1] not in commands.keys():
235 # let's do a nice thing
236 sortedcmds = commands.keys()
238 print ("Usage: %(bin)s {command} [options...] [argument...]"
239 "\n%(bin)s <command> --help to see details, or"
240 " man %(bin)s\n" % {"bin": binary})
241 # compute the max line length for cmd + usage
242 mlen = max([len(" %s %s" % (cmd, commands[cmd][3])) for cmd in commands])
243 mlen = min(60, mlen) # should not get here...
244 # and format a nice command list
246 for cmd in sortedcmds:
247 cmdstr = " %s %s" % (cmd, commands[cmd][3])
248 help_text = commands[cmd][4]
249 help_lines = textwrap.wrap(help_text, 79-3-mlen)
250 print "%-*s - %s" % (mlen, cmdstr,
252 for line in help_lines:
253 print "%-*s %s" % (mlen, "", line)
255 return None, None, None
257 func, nargs, parser_opts, usage, description = commands[cmd]
258 parser_opts.append(_LOCK_OPT)
259 parser = OptionParser(option_list=parser_opts,
260 description=description,
261 formatter=TitledHelpFormatter(),
262 usage="%%prog %s %s" % (cmd, usage))
263 parser.disable_interspersed_args()
264 options, args = parser.parse_args()
267 print >> sys.stderr, ("Error: Command %s expects no arguments" % cmd)
268 return None, None, None
269 elif nargs < 0 and len(args) != -nargs:
270 print >> sys.stderr, ("Error: Command %s expects %d argument(s)" %
272 return None, None, None
273 elif nargs >= 0 and len(args) < nargs:
274 print >> sys.stderr, ("Error: Command %s expects at least %d argument(s)" %
276 return None, None, None
278 return func, options, args
281 def AskUser(text, choices=None):
282 """Ask the user a question.
285 text - the question to ask.
287 choices - list with elements tuples (input_char, return_value,
288 description); if not given, it will default to: [('y', True,
289 'Perform the operation'), ('n', False, 'Do no do the operation')];
290 note that the '?' char is reserved for help
292 Returns: one of the return values from the choices list; if input is
293 not possible (i.e. not running with a tty, we return the last entry
298 choices = [('y', True, 'Perform the operation'),
299 ('n', False, 'Do not perform the operation')]
300 if not choices or not isinstance(choices, list):
301 raise errors.ProgrammerError("Invalid choiches argument to AskUser")
302 for entry in choices:
303 if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
304 raise errors.ProgrammerError("Invalid choiches element to AskUser")
306 answer = choices[-1][1]
308 for line in text.splitlines():
309 new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
310 text = "\n".join(new_text)
312 f = file("/dev/tty", "r+")
316 chars = [entry[0] for entry in choices]
317 chars[-1] = "[%s]" % chars[-1]
319 maps = dict([(entry[0], entry[1]) for entry in choices])
323 f.write("/".join(chars))
325 line = f.readline(2).strip().lower()
330 for entry in choices:
331 f.write(" %s - %s\n" % (entry[0], entry[2]))
339 def SubmitOpCode(op, proc=None, feedback_fn=None):
340 """Function to submit an opcode.
342 This is just a simple wrapper over the construction of the processor
343 instance. It should be extended to better handle feedback and
344 interaction functions.
348 proc = mcpu.Processor()
349 if feedback_fn is None:
350 feedback_fn = logger.ToStdout
351 return proc.ExecOpCode(op, feedback_fn)
354 def GenericMain(commands, override=None):
355 """Generic main function for all the gnt-* commands.
358 - commands: a dictionary with a special structure, see the design doc
359 for command line handling.
360 - override: if not None, we expect a dictionary with keys that will
361 override command line options; this can be used to pass
362 options from the scripts to generic functions
365 # save the program name and the entire command line for later logging
367 binary = os.path.basename(sys.argv[0]) or sys.argv[0]
368 if len(sys.argv) >= 2:
369 binary += " " + sys.argv[1]
370 old_cmdline = " ".join(sys.argv[2:])
374 binary = "<unknown program>"
377 func, options, args = _ParseArgs(sys.argv, commands)
378 if func is None: # parse error
381 if override is not None:
382 for key, val in override.iteritems():
383 setattr(options, key, val)
385 logger.SetupLogging(debug=options.debug, program=binary)
388 utils.Lock('cmd', max_retries=options.lock_retries, debug=options.debug)
389 except errors.LockError, err:
390 logger.ToStderr(str(err))
394 logger.Info("run with arguments '%s'" % old_cmdline)
396 logger.Info("run with no arguments")
400 result = func(options, args)
401 except errors.ConfigurationError, err:
402 logger.Error("Corrupt configuration file: %s" % err)
403 logger.ToStderr("Aborting.")
405 except errors.HooksAbort, err:
406 logger.ToStderr("Failure: hooks execution failed:")
407 for node, script, out in err.args[0]:
409 logger.ToStderr(" node: %s, script: %s, output: %s" %
412 logger.ToStderr(" node: %s, script: %s (no output)" %
415 except errors.HooksFailure, err:
416 logger.ToStderr("Failure: hooks general failure: %s" % str(err))
418 except errors.ResolverError, err:
419 this_host = utils.HostInfo.SysName()
420 if err.args[0] == this_host:
421 msg = "Failure: can't resolve my own hostname ('%s')"
423 msg = "Failure: can't resolve hostname '%s'"
424 logger.ToStderr(msg % err.args[0])
426 except errors.OpPrereqError, err:
427 logger.ToStderr("Failure: prerequisites not met for this"
428 " operation:\n%s" % str(err))
430 except errors.OpExecError, err:
431 logger.ToStderr("Failure: command execution error:\n%s" % str(err))
433 except errors.TagError, err:
434 logger.ToStderr("Failure: invalid tag(s) given:\n%s" % str(err))
443 def GenerateTable(headers, fields, separator, data,
444 numfields=None, unitfields=None):
445 """Prints a table with headers and different fields.
448 headers: Dict of header titles or None if no headers should be shown
449 fields: List of fields to show
450 separator: String used to separate fields or None for spaces
451 data: Data to be printed
452 numfields: List of fields to be aligned to right
453 unitfields: List of fields to be formatted as units
456 if numfields is None:
458 if unitfields is None:
463 if separator is not None:
464 format_fields.append("%s")
465 elif field in numfields:
466 format_fields.append("%*s")
468 format_fields.append("%-*s")
470 if separator is None:
471 mlens = [0 for name in fields]
472 format = ' '.join(format_fields)
474 format = separator.replace("%", "%%").join(format_fields)
477 for idx, val in enumerate(row):
478 if fields[idx] in unitfields:
484 val = row[idx] = utils.FormatUnit(val)
485 if separator is None:
486 mlens[idx] = max(mlens[idx], len(val))
491 for idx, name in enumerate(fields):
493 if separator is None:
494 mlens[idx] = max(mlens[idx], len(hdr))
495 args.append(mlens[idx])
497 result.append(format % tuple(args))
501 for idx in xrange(len(fields)):
502 if separator is None:
503 args.append(mlens[idx])
504 args.append(line[idx])
505 result.append(format % tuple(args))