Statistics
| Branch: | Tag: | Revision:

root / lib / cli.py @ 153d9724

History | View | Annotate | Download (16.1 kB)

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",
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="Select output fields",
171
                         metavar="FIELDS")
172

    
173
FORCE_OPT = make_option("-f", "--force", dest="force", action="store_true",
174
                        default=False, help="Force the operation")
175

    
176
_LOCK_OPT = make_option("--lock-retries", default=None,
177
                        type="int", help=SUPPRESS_HELP)
178

    
179
TAG_SRC_OPT = make_option("--from", dest="tags_source",
180
                          default=None, help="File with tag names")
181

    
182
def ARGS_FIXED(val):
183
  """Macro-like function denoting a fixed number of arguments"""
184
  return -val
185

    
186

    
187
def ARGS_ATLEAST(val):
188
  """Macro-like function denoting a minimum number of arguments"""
189
  return val
190

    
191

    
192
ARGS_NONE = None
193
ARGS_ONE = ARGS_FIXED(1)
194
ARGS_ANY = ARGS_ATLEAST(0)
195

    
196

    
197
def check_unit(option, opt, value):
198
  try:
199
    return utils.ParseUnit(value)
200
  except errors.UnitParseError, err:
201
    raise OptionValueError("option %s: %s" % (opt, err))
202

    
203

    
204
class CliOption(Option):
205
  TYPES = Option.TYPES + ("unit",)
206
  TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER)
207
  TYPE_CHECKER["unit"] = check_unit
208

    
209

    
210
# optparse.py sets make_option, so we do it for our own option class, too
211
cli_option = CliOption
212

    
213

    
214
def _ParseArgs(argv, commands):
215
  """Parses the command line and return the function which must be
216
  executed together with its arguments
217

218
  Arguments:
219
    argv: the command line
220

221
    commands: dictionary with special contents, see the design doc for
222
    cmdline handling
223

224
  """
225
  if len(argv) == 0:
226
    binary = "<command>"
227
  else:
228
    binary = argv[0].split("/")[-1]
229

    
230
  if len(argv) > 1 and argv[1] == "--version":
231
    print "%s (ganeti) %s" % (binary, constants.RELEASE_VERSION)
232
    # Quit right away. That way we don't have to care about this special
233
    # argument. optparse.py does it the same.
234
    sys.exit(0)
235

    
236
  if len(argv) < 2 or argv[1] not in commands.keys():
237
    # let's do a nice thing
238
    sortedcmds = commands.keys()
239
    sortedcmds.sort()
240
    print ("Usage: %(bin)s {command} [options...] [argument...]"
241
           "\n%(bin)s <command> --help to see details, or"
242
           " man %(bin)s\n" % {"bin": binary})
243
    # compute the max line length for cmd + usage
244
    mlen = max([len(" %s %s" % (cmd, commands[cmd][3])) for cmd in commands])
245
    mlen = min(60, mlen) # should not get here...
246
    # and format a nice command list
247
    print "Commands:"
248
    for cmd in sortedcmds:
249
      cmdstr = " %s %s" % (cmd, commands[cmd][3])
250
      help_text = commands[cmd][4]
251
      help_lines = textwrap.wrap(help_text, 79-3-mlen)
252
      print "%-*s - %s" % (mlen, cmdstr,
253
                                          help_lines.pop(0))
254
      for line in help_lines:
255
        print "%-*s   %s" % (mlen, "", line)
256
    print
257
    return None, None, None
258
  cmd = argv.pop(1)
259
  func, nargs, parser_opts, usage, description = commands[cmd]
260
  parser_opts.append(_LOCK_OPT)
261
  parser = OptionParser(option_list=parser_opts,
262
                        description=description,
263
                        formatter=TitledHelpFormatter(),
264
                        usage="%%prog %s %s" % (cmd, usage))
265
  parser.disable_interspersed_args()
266
  options, args = parser.parse_args()
267
  if nargs is None:
268
    if len(args) != 0:
269
      print >> sys.stderr, ("Error: Command %s expects no arguments" % cmd)
270
      return None, None, None
271
  elif nargs < 0 and len(args) != -nargs:
272
    print >> sys.stderr, ("Error: Command %s expects %d argument(s)" %
273
                         (cmd, -nargs))
274
    return None, None, None
275
  elif nargs >= 0 and len(args) < nargs:
276
    print >> sys.stderr, ("Error: Command %s expects at least %d argument(s)" %
277
                         (cmd, nargs))
278
    return None, None, None
279

    
280
  return func, options, args
281

    
282

    
283
def AskUser(text, choices=None):
284
  """Ask the user a question.
285

286
  Args:
287
    text - the question to ask.
288

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

294
  Returns: one of the return values from the choices list; if input is
295
  not possible (i.e. not running with a tty, we return the last entry
296
  from the list
297

298
  """
299
  if choices is None:
300
    choices = [('y', True, 'Perform the operation'),
301
               ('n', False, 'Do not perform the operation')]
302
  if not choices or not isinstance(choices, list):
303
    raise errors.ProgrammerError("Invalid choiches argument to AskUser")
304
  for entry in choices:
305
    if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
306
      raise errors.ProgrammerError("Invalid choiches element to AskUser")
307

    
308
  answer = choices[-1][1]
309
  new_text = []
310
  for line in text.splitlines():
311
    new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
312
  text = "\n".join(new_text)
313
  try:
314
    f = file("/dev/tty", "a+")
315
  except IOError:
316
    return answer
317
  try:
318
    chars = [entry[0] for entry in choices]
319
    chars[-1] = "[%s]" % chars[-1]
320
    chars.append('?')
321
    maps = dict([(entry[0], entry[1]) for entry in choices])
322
    while True:
323
      f.write(text)
324
      f.write('\n')
325
      f.write("/".join(chars))
326
      f.write(": ")
327
      line = f.readline(2).strip().lower()
328
      if line in maps:
329
        answer = maps[line]
330
        break
331
      elif line == '?':
332
        for entry in choices:
333
          f.write(" %s - %s\n" % (entry[0], entry[2]))
334
        f.write("\n")
335
        continue
336
  finally:
337
    f.close()
338
  return answer
339

    
340

    
341
def SubmitOpCode(op, proc=None, feedback_fn=None):
342
  """Function to submit an opcode.
343

344
  This is just a simple wrapper over the construction of the processor
345
  instance. It should be extended to better handle feedback and
346
  interaction functions.
347

348
  """
349
  if proc is None:
350
    proc = mcpu.Processor()
351
  if feedback_fn is None:
352
    feedback_fn = logger.ToStdout
353
  return proc.ExecOpCode(op, feedback_fn)
354

    
355

    
356
def FormatError(err):
357
  """Return a formatted error message for a given error.
358

359
  This function takes an exception instance and returns a tuple
360
  consisting of two values: first, the recommended exit code, and
361
  second, a string describing the error message (not
362
  newline-terminated).
363

364
  """
365
  retcode = 1
366
  obuf = StringIO()
367
  if isinstance(err, errors.ConfigurationError):
368
    msg = "Corrupt configuration file: %s" % err
369
    logger.Error(msg)
370
    obuf.write(msg + "\n")
371
    obuf.write("Aborting.")
372
    retcode = 2
373
  elif isinstance(err, errors.HooksAbort):
374
    obuf.write("Failure: hooks execution failed:\n")
375
    for node, script, out in err.args[0]:
376
      if out:
377
        obuf.write("  node: %s, script: %s, output: %s\n" %
378
                   (node, script, out))
379
      else:
380
        obuf.write("  node: %s, script: %s (no output)\n" %
381
                   (node, script))
382
  elif isinstance(err, errors.HooksFailure):
383
    obuf.write("Failure: hooks general failure: %s" % str(err))
384
  elif isinstance(err, errors.ResolverError):
385
    this_host = utils.HostInfo.SysName()
386
    if err.args[0] == this_host:
387
      msg = "Failure: can't resolve my own hostname ('%s')"
388
    else:
389
      msg = "Failure: can't resolve hostname '%s'"
390
    obuf.write(msg % err.args[0])
391
  elif isinstance(err, errors.OpPrereqError):
392
    obuf.write("Failure: prerequisites not met for this"
393
               " operation:\n%s" % str(err))
394
  elif isinstance(err, errors.OpExecError):
395
    obuf.write("Failure: command execution error:\n%s" % str(err))
396
  elif isinstance(err, errors.TagError):
397
    obuf.write("Failure: invalid tag(s) given:\n%s" % str(err))
398
  elif isinstance(err, errors.GenericError):
399
    obuf.write("Unhandled Ganeti error: %s" % str(err))
400
  else:
401
    obuf.write("Unhandled exception: %s" % str(err))
402
  return retcode, obuf.getvalue().rstrip('\n')
403

    
404

    
405
def GenericMain(commands, override=None):
406
  """Generic main function for all the gnt-* commands.
407

408
  Arguments:
409
    - commands: a dictionary with a special structure, see the design doc
410
                for command line handling.
411
    - override: if not None, we expect a dictionary with keys that will
412
                override command line options; this can be used to pass
413
                options from the scripts to generic functions
414

415
  """
416
  # save the program name and the entire command line for later logging
417
  if sys.argv:
418
    binary = os.path.basename(sys.argv[0]) or sys.argv[0]
419
    if len(sys.argv) >= 2:
420
      binary += " " + sys.argv[1]
421
      old_cmdline = " ".join(sys.argv[2:])
422
    else:
423
      old_cmdline = ""
424
  else:
425
    binary = "<unknown program>"
426
    old_cmdline = ""
427

    
428
  func, options, args = _ParseArgs(sys.argv, commands)
429
  if func is None: # parse error
430
    return 1
431

    
432
  if override is not None:
433
    for key, val in override.iteritems():
434
      setattr(options, key, val)
435

    
436
  logger.SetupLogging(debug=options.debug, program=binary)
437

    
438
  try:
439
    utils.Lock('cmd', max_retries=options.lock_retries, debug=options.debug)
440
  except errors.LockError, err:
441
    logger.ToStderr(str(err))
442
    return 1
443

    
444
  if old_cmdline:
445
    logger.Info("run with arguments '%s'" % old_cmdline)
446
  else:
447
    logger.Info("run with no arguments")
448

    
449
  try:
450
    try:
451
      result = func(options, args)
452
    except errors.GenericError, err:
453
      result, err_msg = FormatError(err)
454
      logger.ToStderr(err_msg)
455
  finally:
456
    utils.Unlock('cmd')
457
    utils.LockCleanup()
458

    
459
  return result
460

    
461

    
462
def GenerateTable(headers, fields, separator, data,
463
                  numfields=None, unitfields=None):
464
  """Prints a table with headers and different fields.
465

466
  Args:
467
    headers: Dict of header titles or None if no headers should be shown
468
    fields: List of fields to show
469
    separator: String used to separate fields or None for spaces
470
    data: Data to be printed
471
    numfields: List of fields to be aligned to right
472
    unitfields: List of fields to be formatted as units
473

474
  """
475
  if numfields is None:
476
    numfields = []
477
  if unitfields is None:
478
    unitfields = []
479

    
480
  format_fields = []
481
  for field in fields:
482
    if headers and field not in headers:
483
      raise errors.ProgrammerError("Missing header description for field '%s'"
484
                                   % field)
485
    if separator is not None:
486
      format_fields.append("%s")
487
    elif field in numfields:
488
      format_fields.append("%*s")
489
    else:
490
      format_fields.append("%-*s")
491

    
492
  if separator is None:
493
    mlens = [0 for name in fields]
494
    format = ' '.join(format_fields)
495
  else:
496
    format = separator.replace("%", "%%").join(format_fields)
497

    
498
  for row in data:
499
    for idx, val in enumerate(row):
500
      if fields[idx] in unitfields:
501
        try:
502
          val = int(val)
503
        except ValueError:
504
          pass
505
        else:
506
          val = row[idx] = utils.FormatUnit(val)
507
      val = row[idx] = str(val)
508
      if separator is None:
509
        mlens[idx] = max(mlens[idx], len(val))
510

    
511
  result = []
512
  if headers:
513
    args = []
514
    for idx, name in enumerate(fields):
515
      hdr = headers[name]
516
      if separator is None:
517
        mlens[idx] = max(mlens[idx], len(hdr))
518
        args.append(mlens[idx])
519
      args.append(hdr)
520
    result.append(format % tuple(args))
521

    
522
  for line in data:
523
    args = []
524
    for idx in xrange(len(fields)):
525
      if separator is None:
526
        args.append(mlens[idx])
527
      args.append(line[idx])
528
    result.append(format % tuple(args))
529

    
530
  return result