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