Statistics
| Branch: | Tag: | Revision:

root / lib / cli.py @ 9ff7e35c

History | View | Annotate | Download (11.8 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", "GenerateTable", "AskUser",
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, choices=None):
174
  """Ask the user a question.
175

176
  Args:
177
    text - the question to ask.
178

179
    choices - list with elements tuples (input_char, return_value,
180
    description); if not given, it will default to: [('y', True,
181
    'Perform the operation'), ('n', False, 'Do no do the operation')];
182
    note that the '?' char is reserved for help
183

184
  Returns: one of the return values from the choices list; if input is
185
  not possible (i.e. not running with a tty, we return the last entry
186
  from the list
187

188
  """
189
  if choices is None:
190
    choices = [('y', True, 'Perform the operation'),
191
               ('n', False, 'Do not perform the operation')]
192
  if not choices or not isinstance(choices, list):
193
    raise errors.ProgrammerError("Invalid choiches argument to AskUser")
194
  for entry in choices:
195
    if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
196
      raise errors.ProgrammerError("Invalid choiches element to AskUser")
197

    
198
  answer = choices[-1][1]
199
  new_text = []
200
  for line in text.splitlines():
201
    new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
202
  text = "\n".join(new_text)
203
  try:
204
    f = file("/dev/tty", "r+")
205
  except IOError:
206
    return answer
207
  try:
208
    chars = [entry[0] for entry in choices]
209
    chars[-1] = "[%s]" % chars[-1]
210
    chars.append('?')
211
    maps = dict([(entry[0], entry[1]) for entry in choices])
212
    while True:
213
      f.write(text)
214
      f.write('\n')
215
      f.write("/".join(chars))
216
      f.write(": ")
217
      line = f.readline(2).strip().lower()
218
      if line in maps:
219
        answer = maps[line]
220
        break
221
      elif line == '?':
222
        for entry in choices:
223
          f.write(" %s - %s\n" % (entry[0], entry[2]))
224
        f.write("\n")
225
        continue
226
  finally:
227
    f.close()
228
  return answer
229

    
230

    
231
def SubmitOpCode(op):
232
  """Function to submit an opcode.
233

234
  This is just a simple wrapper over the construction of the processor
235
  instance. It should be extended to better handle feedback and
236
  interaction functions.
237

238
  """
239
  proc = mcpu.Processor()
240
  return proc.ExecOpCode(op, logger.ToStdout)
241

    
242

    
243
def GenericMain(commands):
244
  """Generic main function for all the gnt-* commands.
245

246
  Argument: a dictionary with a special structure, see the design doc
247
  for command line handling.
248

249
  """
250
  # save the program name and the entire command line for later logging
251
  if sys.argv:
252
    binary = os.path.basename(sys.argv[0]) or sys.argv[0]
253
    if len(sys.argv) >= 2:
254
      binary += " " + sys.argv[1]
255
      old_cmdline = " ".join(sys.argv[2:])
256
    else:
257
      old_cmdline = ""
258
  else:
259
    binary = "<unknown program>"
260
    old_cmdline = ""
261

    
262
  func, options, args = _ParseArgs(sys.argv, commands)
263
  if func is None: # parse error
264
    return 1
265

    
266
  logger.SetupLogging(debug=options.debug, program=binary)
267

    
268
  try:
269
    utils.Lock('cmd', max_retries=options.lock_retries, debug=options.debug)
270
  except errors.LockError, err:
271
    logger.ToStderr(str(err))
272
    return 1
273

    
274
  if old_cmdline:
275
    logger.Info("run with arguments '%s'" % old_cmdline)
276
  else:
277
    logger.Info("run with no arguments")
278

    
279
  try:
280
    try:
281
      result = func(options, args)
282
    except errors.ConfigurationError, err:
283
      logger.Error("Corrupt configuration file: %s" % err)
284
      logger.ToStderr("Aborting.")
285
      result = 2
286
    except errors.HooksAbort, err:
287
      logger.ToStderr("Failure: hooks execution failed:")
288
      for node, script, out in err.args[0]:
289
        if out:
290
          logger.ToStderr("  node: %s, script: %s, output: %s" %
291
                          (node, script, out))
292
        else:
293
          logger.ToStderr("  node: %s, script: %s (no output)" %
294
                          (node, script))
295
      result = 1
296
    except errors.HooksFailure, err:
297
      logger.ToStderr("Failure: hooks general failure: %s" % str(err))
298
      result = 1
299
    except errors.ResolverError, err:
300
      this_host = utils.HostInfo.SysName()
301
      if err.args[0] == this_host:
302
        msg = "Failure: can't resolve my own hostname ('%s')"
303
      else:
304
        msg = "Failure: can't resolve hostname '%s'"
305
      logger.ToStderr(msg % err.args[0])
306
      result = 1
307
    except errors.OpPrereqError, err:
308
      logger.ToStderr("Failure: prerequisites not met for this"
309
                      " operation:\n%s" % str(err))
310
      result = 1
311
    except errors.OpExecError, err:
312
      logger.ToStderr("Failure: command execution error:\n%s" % str(err))
313
      result = 1
314
  finally:
315
    utils.Unlock('cmd')
316
    utils.LockCleanup()
317

    
318
  return result
319

    
320

    
321
def GenerateTable(headers, fields, separator, data,
322
                  numfields=None, unitfields=None):
323
  """Prints a table with headers and different fields.
324

325
  Args:
326
    headers: Dict of header titles or None if no headers should be shown
327
    fields: List of fields to show
328
    separator: String used to separate fields or None for spaces
329
    data: Data to be printed
330
    numfields: List of fields to be aligned to right
331
    unitfields: List of fields to be formatted as units
332

333
  """
334
  if numfields is None:
335
    numfields = []
336
  if unitfields is None:
337
    unitfields = []
338

    
339
  format_fields = []
340
  for field in fields:
341
    if separator is not None:
342
      format_fields.append("%s")
343
    elif field in numfields:
344
      format_fields.append("%*s")
345
    else:
346
      format_fields.append("%-*s")
347

    
348
  if separator is None:
349
    mlens = [0 for name in fields]
350
    format = ' '.join(format_fields)
351
  else:
352
    format = separator.replace("%", "%%").join(format_fields)
353

    
354
  for row in data:
355
    for idx, val in enumerate(row):
356
      if fields[idx] in unitfields:
357
        try:
358
          val = int(val)
359
        except ValueError:
360
          pass
361
        else:
362
          val = row[idx] = utils.FormatUnit(val)
363
      if separator is None:
364
        mlens[idx] = max(mlens[idx], len(val))
365

    
366
  result = []
367
  if headers:
368
    args = []
369
    for idx, name in enumerate(fields):
370
      hdr = headers[name]
371
      if separator is None:
372
        mlens[idx] = max(mlens[idx], len(hdr))
373
        args.append(mlens[idx])
374
      args.append(hdr)
375
    result.append(format % tuple(args))
376

    
377
  for line in data:
378
    args = []
379
    for idx in xrange(len(fields)):
380
      if separator is None:
381
        args.append(mlens[idx])
382
      args.append(line[idx])
383
    result.append(format % tuple(args))
384

    
385
  return result