Improve the documentation of query output fields
[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 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", "SplitNodeOption"
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="Comma separated list of"
171                          " output fields",
172                          metavar="FIELDS")
173
174 FORCE_OPT = make_option("-f", "--force", dest="force", action="store_true",
175                         default=False, help="Force the operation")
176
177 _LOCK_OPT = make_option("--lock-retries", default=None,
178                         type="int", help=SUPPRESS_HELP)
179
180 TAG_SRC_OPT = make_option("--from", dest="tags_source",
181                           default=None, help="File with tag names")
182
183
184 def ARGS_FIXED(val):
185   """Macro-like function denoting a fixed number of arguments"""
186   return -val
187
188
189 def ARGS_ATLEAST(val):
190   """Macro-like function denoting a minimum number of arguments"""
191   return val
192
193
194 ARGS_NONE = None
195 ARGS_ONE = ARGS_FIXED(1)
196 ARGS_ANY = ARGS_ATLEAST(0)
197
198
199 def check_unit(option, opt, value):
200   """OptParsers custom converter for units.
201
202   """
203   try:
204     return utils.ParseUnit(value)
205   except errors.UnitParseError, err:
206     raise OptionValueError("option %s: %s" % (opt, err))
207
208
209 class CliOption(Option):
210   """Custom option class for optparse.
211
212   """
213   TYPES = Option.TYPES + ("unit",)
214   TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER)
215   TYPE_CHECKER["unit"] = check_unit
216
217
218 # optparse.py sets make_option, so we do it for our own option class, too
219 cli_option = CliOption
220
221
222 def _ParseArgs(argv, commands, aliases):
223   """Parses the command line and return the function which must be
224   executed together with its arguments
225
226   Arguments:
227     argv: the command line
228
229     commands: dictionary with special contents, see the design doc for
230     cmdline handling
231     aliases: dictionary with command aliases {'alias': 'target, ...}
232
233   """
234   if len(argv) == 0:
235     binary = "<command>"
236   else:
237     binary = argv[0].split("/")[-1]
238
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.
243     sys.exit(0)
244
245   if len(argv) < 2 or not (argv[1] in commands or
246                            argv[1] in aliases):
247     # let's do a nice thing
248     sortedcmds = commands.keys()
249     sortedcmds.sort()
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
257     print "Commands:"
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,
263                                           help_lines.pop(0))
264       for line in help_lines:
265         print "%-*s   %s" % (mlen, "", line)
266     print
267     return None, None, None
268
269   # get command, unalias it, and look it up in commands
270   cmd = argv.pop(1)
271   if cmd in aliases:
272     if cmd in commands:
273       raise errors.ProgrammerError("Alias '%s' overrides an existing"
274                                    " command" % cmd)
275
276     if aliases[cmd] not in commands:
277       raise errors.ProgrammerError("Alias '%s' maps to non-existing"
278                                    " command '%s'" % (cmd, aliases[cmd]))
279
280     cmd = aliases[cmd]
281
282   func, nargs, parser_opts, usage, description = commands[cmd]
283   parser_opts.append(_LOCK_OPT)
284   parser = OptionParser(option_list=parser_opts,
285                         description=description,
286                         formatter=TitledHelpFormatter(),
287                         usage="%%prog %s %s" % (cmd, usage))
288   parser.disable_interspersed_args()
289   options, args = parser.parse_args()
290   if nargs is None:
291     if len(args) != 0:
292       print >> sys.stderr, ("Error: Command %s expects no arguments" % cmd)
293       return None, None, None
294   elif nargs < 0 and len(args) != -nargs:
295     print >> sys.stderr, ("Error: Command %s expects %d argument(s)" %
296                          (cmd, -nargs))
297     return None, None, None
298   elif nargs >= 0 and len(args) < nargs:
299     print >> sys.stderr, ("Error: Command %s expects at least %d argument(s)" %
300                          (cmd, nargs))
301     return None, None, None
302
303   return func, options, args
304
305
306 def SplitNodeOption(value):
307   """Splits the value of a --node option.
308
309   """
310   if value and ':' in value:
311     return value.split(':', 1)
312   else:
313     return (value, None)
314
315
316 def AskUser(text, choices=None):
317   """Ask the user a question.
318
319   Args:
320     text - the question to ask.
321
322     choices - list with elements tuples (input_char, return_value,
323     description); if not given, it will default to: [('y', True,
324     'Perform the operation'), ('n', False, 'Do no do the operation')];
325     note that the '?' char is reserved for help
326
327   Returns: one of the return values from the choices list; if input is
328   not possible (i.e. not running with a tty, we return the last entry
329   from the list
330
331   """
332   if choices is None:
333     choices = [('y', True, 'Perform the operation'),
334                ('n', False, 'Do not perform the operation')]
335   if not choices or not isinstance(choices, list):
336     raise errors.ProgrammerError("Invalid choiches argument to AskUser")
337   for entry in choices:
338     if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
339       raise errors.ProgrammerError("Invalid choiches element to AskUser")
340
341   answer = choices[-1][1]
342   new_text = []
343   for line in text.splitlines():
344     new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
345   text = "\n".join(new_text)
346   try:
347     f = file("/dev/tty", "a+")
348   except IOError:
349     return answer
350   try:
351     chars = [entry[0] for entry in choices]
352     chars[-1] = "[%s]" % chars[-1]
353     chars.append('?')
354     maps = dict([(entry[0], entry[1]) for entry in choices])
355     while True:
356       f.write(text)
357       f.write('\n')
358       f.write("/".join(chars))
359       f.write(": ")
360       line = f.readline(2).strip().lower()
361       if line in maps:
362         answer = maps[line]
363         break
364       elif line == '?':
365         for entry in choices:
366           f.write(" %s - %s\n" % (entry[0], entry[2]))
367         f.write("\n")
368         continue
369   finally:
370     f.close()
371   return answer
372
373
374 def SubmitOpCode(op, proc=None, feedback_fn=None):
375   """Function to submit an opcode.
376
377   This is just a simple wrapper over the construction of the processor
378   instance. It should be extended to better handle feedback and
379   interaction functions.
380
381   """
382   if feedback_fn is None:
383     feedback_fn = logger.ToStdout
384   if proc is None:
385     proc = mcpu.Processor(feedback=feedback_fn)
386   return proc.ExecOpCode(op)
387
388
389 def FormatError(err):
390   """Return a formatted error message for a given error.
391
392   This function takes an exception instance and returns a tuple
393   consisting of two values: first, the recommended exit code, and
394   second, a string describing the error message (not
395   newline-terminated).
396
397   """
398   retcode = 1
399   obuf = StringIO()
400   msg = str(err)
401   if isinstance(err, errors.ConfigurationError):
402     txt = "Corrupt configuration file: %s" % msg
403     logger.Error(txt)
404     obuf.write(txt + "\n")
405     obuf.write("Aborting.")
406     retcode = 2
407   elif isinstance(err, errors.HooksAbort):
408     obuf.write("Failure: hooks execution failed:\n")
409     for node, script, out in err.args[0]:
410       if out:
411         obuf.write("  node: %s, script: %s, output: %s\n" %
412                    (node, script, out))
413       else:
414         obuf.write("  node: %s, script: %s (no output)\n" %
415                    (node, script))
416   elif isinstance(err, errors.HooksFailure):
417     obuf.write("Failure: hooks general failure: %s" % msg)
418   elif isinstance(err, errors.ResolverError):
419     this_host = utils.HostInfo.SysName()
420     if err.args[0] == this_host:
421       msg = "Failure: can't resolve my own hostname ('%s')"
422     else:
423       msg = "Failure: can't resolve hostname '%s'"
424     obuf.write(msg % err.args[0])
425   elif isinstance(err, errors.OpPrereqError):
426     obuf.write("Failure: prerequisites not met for this"
427                " operation:\n%s" % msg)
428   elif isinstance(err, errors.OpExecError):
429     obuf.write("Failure: command execution error:\n%s" % msg)
430   elif isinstance(err, errors.TagError):
431     obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
432   elif isinstance(err, errors.GenericError):
433     obuf.write("Unhandled Ganeti error: %s" % msg)
434   else:
435     obuf.write("Unhandled exception: %s" % msg)
436   return retcode, obuf.getvalue().rstrip('\n')
437
438
439 def GenericMain(commands, override=None, aliases=None):
440   """Generic main function for all the gnt-* commands.
441
442   Arguments:
443     - commands: a dictionary with a special structure, see the design doc
444                 for command line handling.
445     - override: if not None, we expect a dictionary with keys that will
446                 override command line options; this can be used to pass
447                 options from the scripts to generic functions
448     - aliases: dictionary with command aliases {'alias': 'target, ...}
449
450   """
451   # save the program name and the entire command line for later logging
452   if sys.argv:
453     binary = os.path.basename(sys.argv[0]) or sys.argv[0]
454     if len(sys.argv) >= 2:
455       binary += " " + sys.argv[1]
456       old_cmdline = " ".join(sys.argv[2:])
457     else:
458       old_cmdline = ""
459   else:
460     binary = "<unknown program>"
461     old_cmdline = ""
462
463   if aliases is None:
464     aliases = {}
465
466   func, options, args = _ParseArgs(sys.argv, commands, aliases)
467   if func is None: # parse error
468     return 1
469
470   if override is not None:
471     for key, val in override.iteritems():
472       setattr(options, key, val)
473
474   logger.SetupLogging(debug=options.debug, program=binary)
475
476   utils.debug = options.debug
477   try:
478     utils.Lock('cmd', max_retries=options.lock_retries, debug=options.debug)
479   except errors.LockError, err:
480     logger.ToStderr(str(err))
481     return 1
482   except KeyboardInterrupt:
483     logger.ToStderr("Aborting.")
484     return 1
485
486   if old_cmdline:
487     logger.Info("run with arguments '%s'" % old_cmdline)
488   else:
489     logger.Info("run with no arguments")
490
491   try:
492     try:
493       result = func(options, args)
494     except errors.GenericError, err:
495       result, err_msg = FormatError(err)
496       logger.ToStderr(err_msg)
497   finally:
498     utils.Unlock('cmd')
499     utils.LockCleanup()
500
501   return result
502
503
504 def GenerateTable(headers, fields, separator, data,
505                   numfields=None, unitfields=None):
506   """Prints a table with headers and different fields.
507
508   Args:
509     headers: Dict of header titles or None if no headers should be shown
510     fields: List of fields to show
511     separator: String used to separate fields or None for spaces
512     data: Data to be printed
513     numfields: List of fields to be aligned to right
514     unitfields: List of fields to be formatted as units
515
516   """
517   if numfields is None:
518     numfields = []
519   if unitfields is None:
520     unitfields = []
521
522   format_fields = []
523   for field in fields:
524     if headers and field not in headers:
525       raise errors.ProgrammerError("Missing header description for field '%s'"
526                                    % field)
527     if separator is not None:
528       format_fields.append("%s")
529     elif field in numfields:
530       format_fields.append("%*s")
531     else:
532       format_fields.append("%-*s")
533
534   if separator is None:
535     mlens = [0 for name in fields]
536     format = ' '.join(format_fields)
537   else:
538     format = separator.replace("%", "%%").join(format_fields)
539
540   for row in data:
541     for idx, val in enumerate(row):
542       if fields[idx] in unitfields:
543         try:
544           val = int(val)
545         except ValueError:
546           pass
547         else:
548           val = row[idx] = utils.FormatUnit(val)
549       val = row[idx] = str(val)
550       if separator is None:
551         mlens[idx] = max(mlens[idx], len(val))
552
553   result = []
554   if headers:
555     args = []
556     for idx, name in enumerate(fields):
557       hdr = headers[name]
558       if separator is None:
559         mlens[idx] = max(mlens[idx], len(hdr))
560         args.append(mlens[idx])
561       args.append(hdr)
562     result.append(format % tuple(args))
563
564   for line in data:
565     args = []
566     for idx in xrange(len(fields)):
567       if separator is None:
568         args.append(mlens[idx])
569       args.append(line[idx])
570     result.append(format % tuple(args))
571
572   return result