Change cli.OutputTable to cli.GenerateTable
[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
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
36 from optparse import (OptionParser, make_option, TitledHelpFormatter,
37                       Option, OptionValueError, SUPPRESS_HELP)
38
39 __all__ = ["DEBUG_OPT", "NOHDR_OPT", "SEP_OPT", "GenericMain", "SubmitOpCode",
40            "cli_option", "GenerateTable",
41            "ARGS_NONE", "ARGS_FIXED", "ARGS_ATLEAST", "ARGS_ANY", "ARGS_ONE",
42            "USEUNITS_OPT", "FIELDS_OPT", "FORCE_OPT"]
43
44 DEBUG_OPT = make_option("-d", "--debug", default=False,
45                         action="store_true",
46                         help="Turn debugging on")
47
48 NOHDR_OPT = make_option("--no-headers", default=False,
49                         action="store_true", dest="no_headers",
50                         help="Don't display column headers")
51
52 SEP_OPT = make_option("--separator", default=None,
53                       action="store", dest="separator",
54                       help="Separator between output fields"
55                       " (defaults to one space)")
56
57 USEUNITS_OPT = make_option("--human-readable", default=False,
58                            action="store_true", dest="human_readable",
59                            help="Print sizes in human readable format")
60
61 FIELDS_OPT = make_option("-o", "--output", dest="output", action="store",
62                          type="string", help="Select output fields",
63                          metavar="FIELDS")
64
65 FORCE_OPT = make_option("-f", "--force", dest="force", action="store_true",
66                         default=False, help="Force the operation")
67
68 _LOCK_OPT = make_option("--lock-retries", default=None,
69                         type="int", help=SUPPRESS_HELP)
70
71
72 def ARGS_FIXED(val):
73   """Macro-like function denoting a fixed number of arguments"""
74   return -val
75
76
77 def ARGS_ATLEAST(val):
78   """Macro-like function denoting a minimum number of arguments"""
79   return val
80
81
82 ARGS_NONE = None
83 ARGS_ONE = ARGS_FIXED(1)
84 ARGS_ANY = ARGS_ATLEAST(0)
85
86
87 def check_unit(option, opt, value):
88   try:
89     return utils.ParseUnit(value)
90   except errors.UnitParseError, err:
91     raise OptionValueError("option %s: %s" % (opt, err))
92
93
94 class CliOption(Option):
95   TYPES = Option.TYPES + ("unit",)
96   TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER)
97   TYPE_CHECKER["unit"] = check_unit
98
99
100 # optparse.py sets make_option, so we do it for our own option class, too
101 cli_option = CliOption
102
103
104 def _ParseArgs(argv, commands):
105   """Parses the command line and return the function which must be
106   executed together with its arguments
107
108   Arguments:
109     argv: the command line
110
111     commands: dictionary with special contents, see the design doc for
112     cmdline handling
113
114   """
115   if len(argv) == 0:
116     binary = "<command>"
117   else:
118     binary = argv[0].split("/")[-1]
119
120   if len(argv) > 1 and argv[1] == "--version":
121     print "%s (ganeti) %s" % (binary, constants.RELEASE_VERSION)
122     # Quit right away. That way we don't have to care about this special
123     # argument. optparse.py does it the same.
124     sys.exit(0)
125
126   if len(argv) < 2 or argv[1] not in commands.keys():
127     # let's do a nice thing
128     sortedcmds = commands.keys()
129     sortedcmds.sort()
130     print ("Usage: %(bin)s {command} [options...] [argument...]"
131            "\n%(bin)s <command> --help to see details, or"
132            " man %(bin)s\n" % {"bin": binary})
133     # compute the max line length for cmd + usage
134     mlen = max([len(" %s %s" % (cmd, commands[cmd][3])) for cmd in commands])
135     mlen = min(60, mlen) # should not get here...
136     # and format a nice command list
137     print "Commands:"
138     for cmd in sortedcmds:
139       cmdstr = " %s %s" % (cmd, commands[cmd][3])
140       help_text = commands[cmd][4]
141       help_lines = textwrap.wrap(help_text, 79-3-mlen)
142       print "%-*s - %s" % (mlen, cmdstr,
143                                           help_lines.pop(0))
144       for line in help_lines:
145         print "%-*s   %s" % (mlen, "", line)
146     print
147     return None, None, None
148   cmd = argv.pop(1)
149   func, nargs, parser_opts, usage, description = commands[cmd]
150   parser_opts.append(_LOCK_OPT)
151   parser = OptionParser(option_list=parser_opts,
152                         description=description,
153                         formatter=TitledHelpFormatter(),
154                         usage="%%prog %s %s" % (cmd, usage))
155   parser.disable_interspersed_args()
156   options, args = parser.parse_args()
157   if nargs is None:
158     if len(args) != 0:
159       print >> sys.stderr, ("Error: Command %s expects no arguments" % cmd)
160       return None, None, None
161   elif nargs < 0 and len(args) != -nargs:
162     print >> sys.stderr, ("Error: Command %s expects %d argument(s)" %
163                          (cmd, -nargs))
164     return None, None, None
165   elif nargs >= 0 and len(args) < nargs:
166     print >> sys.stderr, ("Error: Command %s expects at least %d argument(s)" %
167                          (cmd, nargs))
168     return None, None, None
169
170   return func, options, args
171
172
173 def _AskUser(text):
174   """Ask the user a yes/no question.
175
176   Args:
177     questionstring - the question to ask.
178
179   Returns:
180     True or False depending on answer (No for False is default).
181
182   """
183   try:
184     f = file("/dev/tty", "r+")
185   except IOError:
186     return False
187   answer = False
188   try:
189     f.write(textwrap.fill(text))
190     f.write('\n')
191     f.write("y/[n]: ")
192     line = f.readline(16).strip().lower()
193     answer = line in ('y', 'yes')
194   finally:
195     f.close()
196   return answer
197
198
199 def SubmitOpCode(op):
200   """Function to submit an opcode.
201
202   This is just a simple wrapper over the construction of the processor
203   instance. It should be extended to better handle feedback and
204   interaction functions.
205
206   """
207   proc = mcpu.Processor()
208   return proc.ExecOpCode(op, logger.ToStdout)
209
210
211 def GenericMain(commands):
212   """Generic main function for all the gnt-* commands.
213
214   Argument: a dictionary with a special structure, see the design doc
215   for command line handling.
216
217   """
218   # save the program name and the entire command line for later logging
219   if sys.argv:
220     binary = os.path.basename(sys.argv[0]) or sys.argv[0]
221     if len(sys.argv) >= 2:
222       binary += " " + sys.argv[1]
223       old_cmdline = " ".join(sys.argv[2:])
224     else:
225       old_cmdline = ""
226   else:
227     binary = "<unknown program>"
228     old_cmdline = ""
229
230   func, options, args = _ParseArgs(sys.argv, commands)
231   if func is None: # parse error
232     return 1
233
234   options._ask_user = _AskUser
235
236   logger.SetupLogging(debug=options.debug, program=binary)
237
238   try:
239     utils.Lock('cmd', max_retries=options.lock_retries, debug=options.debug)
240   except errors.LockError, err:
241     logger.ToStderr(str(err))
242     return 1
243
244   if old_cmdline:
245     logger.Info("run with arguments '%s'" % old_cmdline)
246   else:
247     logger.Info("run with no arguments")
248
249   try:
250     try:
251       result = func(options, args)
252     except errors.ConfigurationError, err:
253       logger.Error("Corrupt configuration file: %s" % err)
254       logger.ToStderr("Aborting.")
255       result = 2
256     except errors.HooksAbort, err:
257       logger.ToStderr("Failure: hooks execution failed:")
258       for node, script, out in err.args[0]:
259         if out:
260           logger.ToStderr("  node: %s, script: %s, output: %s" %
261                           (node, script, out))
262         else:
263           logger.ToStderr("  node: %s, script: %s (no output)" %
264                           (node, script))
265       result = 1
266     except errors.HooksFailure, err:
267       logger.ToStderr("Failure: hooks general failure: %s" % str(err))
268       result = 1
269     except errors.OpPrereqError, err:
270       logger.ToStderr("Failure: prerequisites not met for this"
271                       " operation:\n%s" % str(err))
272       result = 1
273     except errors.OpExecError, err:
274       logger.ToStderr("Failure: command execution error:\n%s" % str(err))
275       result = 1
276   finally:
277     utils.Unlock('cmd')
278     utils.LockCleanup()
279
280   return result
281
282
283 def GenerateTable(headers, fields, separator, data,
284                   numfields=None, unitfields=None):
285   """Prints a table with headers and different fields.
286
287   Args:
288     headers: Dict of header titles or None if no headers should be shown
289     fields: List of fields to show
290     separator: String used to separate fields or None for spaces
291     data: Data to be printed
292     numfields: List of fields to be aligned to right
293     unitfields: List of fields to be formatted as units
294
295   """
296   if numfields is None:
297     numfields = []
298   if unitfields is None:
299     unitfields = []
300
301   format_fields = []
302   for field in fields:
303     if separator is not None:
304       format_fields.append("%s")
305     elif field in numfields:
306       format_fields.append("%*s")
307     else:
308       format_fields.append("%-*s")
309
310   if separator is None:
311     mlens = [0 for name in fields]
312     format = ' '.join(format_fields)
313   else:
314     format = separator.replace("%", "%%").join(format_fields)
315
316   for row in data:
317     for idx, val in enumerate(row):
318       if fields[idx] in unitfields:
319         try:
320           val = int(val)
321         except ValueError:
322           pass
323         else:
324           val = row[idx] = utils.FormatUnit(val)
325       if separator is None:
326         mlens[idx] = max(mlens[idx], len(val))
327
328   result = []
329   if headers:
330     args = []
331     for idx, name in enumerate(fields):
332       hdr = headers[name]
333       if separator is None:
334         mlens[idx] = max(mlens[idx], len(hdr))
335         args.append(mlens[idx])
336       args.append(hdr)
337     result.append(format % tuple(args))
338
339   for line in data:
340     args = []
341     for idx in xrange(len(fields)):
342       if separator is None:
343         args.append(mlens[idx])
344       args.append(line[idx])
345     result.append(format % tuple(args))
346
347   return result