Fix a wrong exception name
[ganeti-local] / lib / cli.py
1 #!/usr/bin/python
2 #
3
4 # Copyright (C) 2006, 2007 Google Inc.
5 #
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.
10 #
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.
15 #
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
19 # 02110-1301, USA.
20
21
22 """Module dealing with command line parsing"""
23
24
25 import sys
26 import textwrap
27 import os.path
28 import copy
29 from cStringIO import StringIO
30
31 from ganeti import utils
32 from ganeti import logger
33 from ganeti import errors
34 from ganeti import mcpu
35 from ganeti import constants
36 from ganeti import opcodes
37
38 from optparse import (OptionParser, make_option, TitledHelpFormatter,
39                       Option, OptionValueError, SUPPRESS_HELP)
40
41 __all__ = ["DEBUG_OPT", "NOHDR_OPT", "SEP_OPT", "GenericMain", "SubmitOpCode",
42            "cli_option", "GenerateTable", "AskUser",
43            "ARGS_NONE", "ARGS_FIXED", "ARGS_ATLEAST", "ARGS_ANY", "ARGS_ONE",
44            "USEUNITS_OPT", "FIELDS_OPT", "FORCE_OPT",
45            "ListTags", "AddTags", "RemoveTags", "TAG_SRC_OPT",
46            "FormatError",
47            ]
48
49
50 def _ExtractTagsObject(opts, args):
51   """Extract the tag type object.
52
53   Note that this function will modify its args parameter.
54
55   """
56   if not hasattr(opts, "tag_type"):
57     raise errors.ProgrammerError("tag_type not passed to _ExtractTagsObject")
58   kind = opts.tag_type
59   if kind == constants.TAG_CLUSTER:
60     retval = kind, kind
61   elif kind == constants.TAG_NODE or kind == constants.TAG_INSTANCE:
62     if not args:
63       raise errors.OpPrereqError("no arguments passed to the command")
64     name = args.pop(0)
65     retval = kind, name
66   else:
67     raise errors.ProgrammerError("Unhandled tag type '%s'" % kind)
68   return retval
69
70
71 def _ExtendTags(opts, args):
72   """Extend the args if a source file has been given.
73
74   This function will extend the tags with the contents of the file
75   passed in the 'tags_source' attribute of the opts parameter. A file
76   named '-' will be replaced by stdin.
77
78   """
79   fname = opts.tags_source
80   if fname is None:
81     return
82   if fname == "-":
83     new_fh = sys.stdin
84   else:
85     new_fh = open(fname, "r")
86   new_data = []
87   try:
88     # we don't use the nice 'new_data = [line.strip() for line in fh]'
89     # because of python bug 1633941
90     while True:
91       line = new_fh.readline()
92       if not line:
93         break
94       new_data.append(line.strip())
95   finally:
96     new_fh.close()
97   args.extend(new_data)
98
99
100 def ListTags(opts, args):
101   """List the tags on a given object.
102
103   This is a generic implementation that knows how to deal with all
104   three cases of tag objects (cluster, node, instance). The opts
105   argument is expected to contain a tag_type field denoting what
106   object type we work on.
107
108   """
109   kind, name = _ExtractTagsObject(opts, args)
110   op = opcodes.OpGetTags(kind=kind, name=name)
111   result = SubmitOpCode(op)
112   result = list(result)
113   result.sort()
114   for tag in result:
115     print tag
116
117
118 def AddTags(opts, args):
119   """Add tags on a given object.
120
121   This is a generic implementation that knows how to deal with all
122   three cases of tag objects (cluster, node, instance). The opts
123   argument is expected to contain a tag_type field denoting what
124   object type we work on.
125
126   """
127   kind, name = _ExtractTagsObject(opts, args)
128   _ExtendTags(opts, args)
129   if not args:
130     raise errors.OpPrereqError("No tags to be added")
131   op = opcodes.OpAddTags(kind=kind, name=name, tags=args)
132   SubmitOpCode(op)
133
134
135 def RemoveTags(opts, args):
136   """Remove tags from a given object.
137
138   This is a generic implementation that knows how to deal with all
139   three cases of tag objects (cluster, node, instance). The opts
140   argument is expected to contain a tag_type field denoting what
141   object type we work on.
142
143   """
144   kind, name = _ExtractTagsObject(opts, args)
145   _ExtendTags(opts, args)
146   if not args:
147     raise errors.OpPrereqError("No tags to be removed")
148   op = opcodes.OpDelTags(kind=kind, name=name, tags=args)
149   SubmitOpCode(op)
150
151
152 DEBUG_OPT = make_option("-d", "--debug", default=False,
153                         action="store_true",
154                         help="Turn debugging on")
155
156 NOHDR_OPT = make_option("--no-headers", default=False,
157                         action="store_true", dest="no_headers",
158                         help="Don't display column headers")
159
160 SEP_OPT = make_option("--separator", default=None,
161                       action="store", dest="separator",
162                       help="Separator between output fields"
163                       " (defaults to one space)")
164
165 USEUNITS_OPT = make_option("--human-readable", default=False,
166                            action="store_true", dest="human_readable",
167                            help="Print sizes in human readable format")
168
169 FIELDS_OPT = make_option("-o", "--output", dest="output", action="store",
170                          type="string", help="Select output fields",
171                          metavar="FIELDS")
172
173 FORCE_OPT = make_option("-f", "--force", dest="force", action="store_true",
174                         default=False, help="Force the operation")
175
176 _LOCK_OPT = make_option("--lock-retries", default=None,
177                         type="int", help=SUPPRESS_HELP)
178
179 TAG_SRC_OPT = make_option("--from", dest="tags_source",
180                           default=None, help="File with tag names")
181
182 def ARGS_FIXED(val):
183   """Macro-like function denoting a fixed number of arguments"""
184   return -val
185
186
187 def ARGS_ATLEAST(val):
188   """Macro-like function denoting a minimum number of arguments"""
189   return val
190
191
192 ARGS_NONE = None
193 ARGS_ONE = ARGS_FIXED(1)
194 ARGS_ANY = ARGS_ATLEAST(0)
195
196
197 def check_unit(option, opt, value):
198   try:
199     return utils.ParseUnit(value)
200   except errors.UnitParseError, err:
201     raise OptionValueError("option %s: %s" % (opt, err))
202
203
204 class CliOption(Option):
205   TYPES = Option.TYPES + ("unit",)
206   TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER)
207   TYPE_CHECKER["unit"] = check_unit
208
209
210 # optparse.py sets make_option, so we do it for our own option class, too
211 cli_option = CliOption
212
213
214 def _ParseArgs(argv, commands):
215   """Parses the command line and return the function which must be
216   executed together with its arguments
217
218   Arguments:
219     argv: the command line
220
221     commands: dictionary with special contents, see the design doc for
222     cmdline handling
223
224   """
225   if len(argv) == 0:
226     binary = "<command>"
227   else:
228     binary = argv[0].split("/")[-1]
229
230   if len(argv) > 1 and argv[1] == "--version":
231     print "%s (ganeti) %s" % (binary, constants.RELEASE_VERSION)
232     # Quit right away. That way we don't have to care about this special
233     # argument. optparse.py does it the same.
234     sys.exit(0)
235
236   if len(argv) < 2 or argv[1] not in commands.keys():
237     # let's do a nice thing
238     sortedcmds = commands.keys()
239     sortedcmds.sort()
240     print ("Usage: %(bin)s {command} [options...] [argument...]"
241            "\n%(bin)s <command> --help to see details, or"
242            " man %(bin)s\n" % {"bin": binary})
243     # compute the max line length for cmd + usage
244     mlen = max([len(" %s %s" % (cmd, commands[cmd][3])) for cmd in commands])
245     mlen = min(60, mlen) # should not get here...
246     # and format a nice command list
247     print "Commands:"
248     for cmd in sortedcmds:
249       cmdstr = " %s %s" % (cmd, commands[cmd][3])
250       help_text = commands[cmd][4]
251       help_lines = textwrap.wrap(help_text, 79-3-mlen)
252       print "%-*s - %s" % (mlen, cmdstr,
253                                           help_lines.pop(0))
254       for line in help_lines:
255         print "%-*s   %s" % (mlen, "", line)
256     print
257     return None, None, None
258   cmd = argv.pop(1)
259   func, nargs, parser_opts, usage, description = commands[cmd]
260   parser_opts.append(_LOCK_OPT)
261   parser = OptionParser(option_list=parser_opts,
262                         description=description,
263                         formatter=TitledHelpFormatter(),
264                         usage="%%prog %s %s" % (cmd, usage))
265   parser.disable_interspersed_args()
266   options, args = parser.parse_args()
267   if nargs is None:
268     if len(args) != 0:
269       print >> sys.stderr, ("Error: Command %s expects no arguments" % cmd)
270       return None, None, None
271   elif nargs < 0 and len(args) != -nargs:
272     print >> sys.stderr, ("Error: Command %s expects %d argument(s)" %
273                          (cmd, -nargs))
274     return None, None, None
275   elif nargs >= 0 and len(args) < nargs:
276     print >> sys.stderr, ("Error: Command %s expects at least %d argument(s)" %
277                          (cmd, nargs))
278     return None, None, None
279
280   return func, options, args
281
282
283 def AskUser(text, choices=None):
284   """Ask the user a question.
285
286   Args:
287     text - the question to ask.
288
289     choices - list with elements tuples (input_char, return_value,
290     description); if not given, it will default to: [('y', True,
291     'Perform the operation'), ('n', False, 'Do no do the operation')];
292     note that the '?' char is reserved for help
293
294   Returns: one of the return values from the choices list; if input is
295   not possible (i.e. not running with a tty, we return the last entry
296   from the list
297
298   """
299   if choices is None:
300     choices = [('y', True, 'Perform the operation'),
301                ('n', False, 'Do not perform the operation')]
302   if not choices or not isinstance(choices, list):
303     raise errors.ProgrammerError("Invalid choiches argument to AskUser")
304   for entry in choices:
305     if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
306       raise errors.ProgrammerError("Invalid choiches element to AskUser")
307
308   answer = choices[-1][1]
309   new_text = []
310   for line in text.splitlines():
311     new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
312   text = "\n".join(new_text)
313   try:
314     f = file("/dev/tty", "r+")
315   except IOError:
316     return answer
317   try:
318     chars = [entry[0] for entry in choices]
319     chars[-1] = "[%s]" % chars[-1]
320     chars.append('?')
321     maps = dict([(entry[0], entry[1]) for entry in choices])
322     while True:
323       f.write(text)
324       f.write('\n')
325       f.write("/".join(chars))
326       f.write(": ")
327       line = f.readline(2).strip().lower()
328       if line in maps:
329         answer = maps[line]
330         break
331       elif line == '?':
332         for entry in choices:
333           f.write(" %s - %s\n" % (entry[0], entry[2]))
334         f.write("\n")
335         continue
336   finally:
337     f.close()
338   return answer
339
340
341 def SubmitOpCode(op, proc=None, feedback_fn=None):
342   """Function to submit an opcode.
343
344   This is just a simple wrapper over the construction of the processor
345   instance. It should be extended to better handle feedback and
346   interaction functions.
347
348   """
349   if proc is None:
350     proc = mcpu.Processor()
351   if feedback_fn is None:
352     feedback_fn = logger.ToStdout
353   return proc.ExecOpCode(op, feedback_fn)
354
355
356 def FormatError(err):
357   """Return a formatted error message for a given error.
358
359   This function takes an exception instance and returns a tuple
360   consisting of two values: first, the recommended exit code, and
361   second, a string describing the error message (not
362   newline-terminated).
363
364   """
365   retcode = 1
366   obuf = StringIO()
367   if isinstance(err, errors.ConfigurationError):
368     msg = "Corrupt configuration file: %s" % err
369     logger.Error(msg)
370     obuf.write(msg + "\n")
371     obuf.write("Aborting.")
372     retcode = 2
373   elif isinstance(err, errors.HooksAbort):
374     obuf.write("Failure: hooks execution failed:\n")
375     for node, script, out in err.args[0]:
376       if out:
377         obuf.write("  node: %s, script: %s, output: %s\n" %
378                    (node, script, out))
379       else:
380         obuf.write("  node: %s, script: %s (no output)\n" %
381                    (node, script))
382   elif isinstance(err, errors.HooksFailure):
383     obuf.write("Failure: hooks general failure: %s" % str(err))
384   elif isinstance(err, errors.ResolverError):
385     this_host = utils.HostInfo.SysName()
386     if err.args[0] == this_host:
387       msg = "Failure: can't resolve my own hostname ('%s')"
388     else:
389       msg = "Failure: can't resolve hostname '%s'"
390     obuf.write(msg % err.args[0])
391   elif isinstance(err, errors.OpPrereqError):
392     obuf.write("Failure: prerequisites not met for this"
393                " operation:\n%s" % str(err))
394   elif isinstance(err, errors.OpExecError):
395     obuf.write("Failure: command execution error:\n%s" % str(err))
396   elif isinstance(err, errors.TagError):
397     obuf.write("Failure: invalid tag(s) given:\n%s" % str(err))
398   elif isinstance(err, errors.GenericError):
399     obuf.write("Unhandled Ganeti error: %s" % str(err))
400   else:
401     obuf.write("Unhandled exception: %s" % str(err))
402   return retcode, obuf.getvalue().rstrip('\n')
403
404
405 def GenericMain(commands, override=None):
406   """Generic main function for all the gnt-* commands.
407
408   Arguments:
409     - commands: a dictionary with a special structure, see the design doc
410                 for command line handling.
411     - override: if not None, we expect a dictionary with keys that will
412                 override command line options; this can be used to pass
413                 options from the scripts to generic functions
414
415   """
416   # save the program name and the entire command line for later logging
417   if sys.argv:
418     binary = os.path.basename(sys.argv[0]) or sys.argv[0]
419     if len(sys.argv) >= 2:
420       binary += " " + sys.argv[1]
421       old_cmdline = " ".join(sys.argv[2:])
422     else:
423       old_cmdline = ""
424   else:
425     binary = "<unknown program>"
426     old_cmdline = ""
427
428   func, options, args = _ParseArgs(sys.argv, commands)
429   if func is None: # parse error
430     return 1
431
432   if override is not None:
433     for key, val in override.iteritems():
434       setattr(options, key, val)
435
436   logger.SetupLogging(debug=options.debug, program=binary)
437
438   try:
439     utils.Lock('cmd', max_retries=options.lock_retries, debug=options.debug)
440   except errors.LockError, err:
441     logger.ToStderr(str(err))
442     return 1
443
444   if old_cmdline:
445     logger.Info("run with arguments '%s'" % old_cmdline)
446   else:
447     logger.Info("run with no arguments")
448
449   try:
450     try:
451       result = func(options, args)
452     except errors.GenericError, err:
453       result, err_msg = FormatError(err)
454       logger.ToStderr(err_msg)
455   finally:
456     utils.Unlock('cmd')
457     utils.LockCleanup()
458
459   return result
460
461
462 def GenerateTable(headers, fields, separator, data,
463                   numfields=None, unitfields=None):
464   """Prints a table with headers and different fields.
465
466   Args:
467     headers: Dict of header titles or None if no headers should be shown
468     fields: List of fields to show
469     separator: String used to separate fields or None for spaces
470     data: Data to be printed
471     numfields: List of fields to be aligned to right
472     unitfields: List of fields to be formatted as units
473
474   """
475   if numfields is None:
476     numfields = []
477   if unitfields is None:
478     unitfields = []
479
480   format_fields = []
481   for field in fields:
482     if separator is not None:
483       format_fields.append("%s")
484     elif field in numfields:
485       format_fields.append("%*s")
486     else:
487       format_fields.append("%-*s")
488
489   if separator is None:
490     mlens = [0 for name in fields]
491     format = ' '.join(format_fields)
492   else:
493     format = separator.replace("%", "%%").join(format_fields)
494
495   for row in data:
496     for idx, val in enumerate(row):
497       if fields[idx] in unitfields:
498         try:
499           val = int(val)
500         except ValueError:
501           pass
502         else:
503           val = row[idx] = utils.FormatUnit(val)
504       if separator is None:
505         mlens[idx] = max(mlens[idx], len(val))
506
507   result = []
508   if headers:
509     args = []
510     for idx, name in enumerate(fields):
511       hdr = headers[name]
512       if separator is None:
513         mlens[idx] = max(mlens[idx], len(hdr))
514         args.append(mlens[idx])
515       args.append(hdr)
516     result.append(format % tuple(args))
517
518   for line in data:
519     args = []
520     for idx in xrange(len(fields)):
521       if separator is None:
522         args.append(mlens[idx])
523       args.append(line[idx])
524     result.append(format % tuple(args))
525
526   return result