Statistics
| Branch: | Tag: | Revision:

root / lib / cli.py @ 334d1483

History | View | Annotate | Download (12.1 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, override=None):
244
  """Generic main function for all the gnt-* commands.
245

246
  Arguments:
247
    - commands: a dictionary with a special structure, see the design doc
248
                for command line handling.
249
    - override: if not None, we expect a dictionary with keys that will
250
                override command line options; this can be used to pass
251
                options from the scripts to generic functions
252

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

    
266
  func, options, args = _ParseArgs(sys.argv, commands)
267
  if func is None: # parse error
268
    return 1
269

    
270
  if override is not None:
271
    for key, val in override.iteritems():
272
      setattr(options, key, val)
273

    
274
  logger.SetupLogging(debug=options.debug, program=binary)
275

    
276
  try:
277
    utils.Lock('cmd', max_retries=options.lock_retries, debug=options.debug)
278
  except errors.LockError, err:
279
    logger.ToStderr(str(err))
280
    return 1
281

    
282
  if old_cmdline:
283
    logger.Info("run with arguments '%s'" % old_cmdline)
284
  else:
285
    logger.Info("run with no arguments")
286

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

    
326
  return result
327

    
328

    
329
def GenerateTable(headers, fields, separator, data,
330
                  numfields=None, unitfields=None):
331
  """Prints a table with headers and different fields.
332

333
  Args:
334
    headers: Dict of header titles or None if no headers should be shown
335
    fields: List of fields to show
336
    separator: String used to separate fields or None for spaces
337
    data: Data to be printed
338
    numfields: List of fields to be aligned to right
339
    unitfields: List of fields to be formatted as units
340

341
  """
342
  if numfields is None:
343
    numfields = []
344
  if unitfields is None:
345
    unitfields = []
346

    
347
  format_fields = []
348
  for field in fields:
349
    if separator is not None:
350
      format_fields.append("%s")
351
    elif field in numfields:
352
      format_fields.append("%*s")
353
    else:
354
      format_fields.append("%-*s")
355

    
356
  if separator is None:
357
    mlens = [0 for name in fields]
358
    format = ' '.join(format_fields)
359
  else:
360
    format = separator.replace("%", "%%").join(format_fields)
361

    
362
  for row in data:
363
    for idx, val in enumerate(row):
364
      if fields[idx] in unitfields:
365
        try:
366
          val = int(val)
367
        except ValueError:
368
          pass
369
        else:
370
          val = row[idx] = utils.FormatUnit(val)
371
      if separator is None:
372
        mlens[idx] = max(mlens[idx], len(val))
373

    
374
  result = []
375
  if headers:
376
    args = []
377
    for idx, name in enumerate(fields):
378
      hdr = headers[name]
379
      if separator is None:
380
        mlens[idx] = max(mlens[idx], len(hdr))
381
        args.append(mlens[idx])
382
      args.append(hdr)
383
    result.append(format % tuple(args))
384

    
385
  for line in data:
386
    args = []
387
    for idx in xrange(len(fields)):
388
      if separator is None:
389
        args.append(mlens[idx])
390
      args.append(line[idx])
391
    result.append(format % tuple(args))
392

    
393
  return result