Statistics
| Branch: | Tag: | Revision:

root / lib / cli.py @ fe7b0351

History | View | Annotate | Download (10.2 kB)

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", "OutputTable",
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 OutputTable(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
  if headers:
329
    args = []
330
    for idx, name in enumerate(fields):
331
      hdr = headers[name]
332
      if separator is None:
333
        mlens[idx] = max(mlens[idx], len(hdr))
334
        args.append(mlens[idx])
335
      args.append(hdr)
336
    logger.ToStdout(format % tuple(args))
337

    
338
  for line in data:
339
    args = []
340
    for idx in xrange(len(fields)):
341
      if separator is None:
342
        args.append(mlens[idx])
343
      args.append(line[idx])
344
    logger.ToStdout(format % tuple(args))