Statistics
| Branch: | Tag: | Revision:

root / lib / cli.py @ 65fe4693

History | View | Annotate | Download (16.2 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
  """OptParsers custom converter for units.
199

200
  """
201
  try:
202
    return utils.ParseUnit(value)
203
  except errors.UnitParseError, err:
204
    raise OptionValueError("option %s: %s" % (opt, err))
205

    
206

    
207
class CliOption(Option):
208
  """Custom option class for optparse.
209

210
  """
211
  TYPES = Option.TYPES + ("unit",)
212
  TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER)
213
  TYPE_CHECKER["unit"] = check_unit
214

    
215

    
216
# optparse.py sets make_option, so we do it for our own option class, too
217
cli_option = CliOption
218

    
219

    
220
def _ParseArgs(argv, commands):
221
  """Parses the command line and return the function which must be
222
  executed together with its arguments
223

224
  Arguments:
225
    argv: the command line
226

227
    commands: dictionary with special contents, see the design doc for
228
    cmdline handling
229

230
  """
231
  if len(argv) == 0:
232
    binary = "<command>"
233
  else:
234
    binary = argv[0].split("/")[-1]
235

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

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

    
286
  return func, options, args
287

    
288

    
289
def AskUser(text, choices=None):
290
  """Ask the user a question.
291

292
  Args:
293
    text - the question to ask.
294

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

300
  Returns: one of the return values from the choices list; if input is
301
  not possible (i.e. not running with a tty, we return the last entry
302
  from the list
303

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

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

    
346

    
347
def SubmitOpCode(op, proc=None, feedback_fn=None):
348
  """Function to submit an opcode.
349

350
  This is just a simple wrapper over the construction of the processor
351
  instance. It should be extended to better handle feedback and
352
  interaction functions.
353

354
  """
355
  if feedback_fn is None:
356
    feedback_fn = logger.ToStdout
357
  if proc is None:
358
    proc = mcpu.Processor(feedback=feedback_fn)
359
  return proc.ExecOpCode(op)
360

    
361

    
362
def FormatError(err):
363
  """Return a formatted error message for a given error.
364

365
  This function takes an exception instance and returns a tuple
366
  consisting of two values: first, the recommended exit code, and
367
  second, a string describing the error message (not
368
  newline-terminated).
369

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

    
410

    
411
def GenericMain(commands, override=None):
412
  """Generic main function for all the gnt-* commands.
413

414
  Arguments:
415
    - commands: a dictionary with a special structure, see the design doc
416
                for command line handling.
417
    - override: if not None, we expect a dictionary with keys that will
418
                override command line options; this can be used to pass
419
                options from the scripts to generic functions
420

421
  """
422
  # save the program name and the entire command line for later logging
423
  if sys.argv:
424
    binary = os.path.basename(sys.argv[0]) or sys.argv[0]
425
    if len(sys.argv) >= 2:
426
      binary += " " + sys.argv[1]
427
      old_cmdline = " ".join(sys.argv[2:])
428
    else:
429
      old_cmdline = ""
430
  else:
431
    binary = "<unknown program>"
432
    old_cmdline = ""
433

    
434
  func, options, args = _ParseArgs(sys.argv, commands)
435
  if func is None: # parse error
436
    return 1
437

    
438
  if override is not None:
439
    for key, val in override.iteritems():
440
      setattr(options, key, val)
441

    
442
  logger.SetupLogging(debug=options.debug, program=binary)
443

    
444
  try:
445
    utils.Lock('cmd', max_retries=options.lock_retries, debug=options.debug)
446
  except errors.LockError, err:
447
    logger.ToStderr(str(err))
448
    return 1
449

    
450
  if old_cmdline:
451
    logger.Info("run with arguments '%s'" % old_cmdline)
452
  else:
453
    logger.Info("run with no arguments")
454

    
455
  try:
456
    try:
457
      result = func(options, args)
458
    except errors.GenericError, err:
459
      result, err_msg = FormatError(err)
460
      logger.ToStderr(err_msg)
461
  finally:
462
    utils.Unlock('cmd')
463
    utils.LockCleanup()
464

    
465
  return result
466

    
467

    
468
def GenerateTable(headers, fields, separator, data,
469
                  numfields=None, unitfields=None):
470
  """Prints a table with headers and different fields.
471

472
  Args:
473
    headers: Dict of header titles or None if no headers should be shown
474
    fields: List of fields to show
475
    separator: String used to separate fields or None for spaces
476
    data: Data to be printed
477
    numfields: List of fields to be aligned to right
478
    unitfields: List of fields to be formatted as units
479

480
  """
481
  if numfields is None:
482
    numfields = []
483
  if unitfields is None:
484
    unitfields = []
485

    
486
  format_fields = []
487
  for field in fields:
488
    if headers and field not in headers:
489
      raise errors.ProgrammerError("Missing header description for field '%s'"
490
                                   % field)
491
    if separator is not None:
492
      format_fields.append("%s")
493
    elif field in numfields:
494
      format_fields.append("%*s")
495
    else:
496
      format_fields.append("%-*s")
497

    
498
  if separator is None:
499
    mlens = [0 for name in fields]
500
    format = ' '.join(format_fields)
501
  else:
502
    format = separator.replace("%", "%%").join(format_fields)
503

    
504
  for row in data:
505
    for idx, val in enumerate(row):
506
      if fields[idx] in unitfields:
507
        try:
508
          val = int(val)
509
        except ValueError:
510
          pass
511
        else:
512
          val = row[idx] = utils.FormatUnit(val)
513
      val = row[idx] = str(val)
514
      if separator is None:
515
        mlens[idx] = max(mlens[idx], len(val))
516

    
517
  result = []
518
  if headers:
519
    args = []
520
    for idx, name in enumerate(fields):
521
      hdr = headers[name]
522
      if separator is None:
523
        mlens[idx] = max(mlens[idx], len(hdr))
524
        args.append(mlens[idx])
525
      args.append(hdr)
526
    result.append(format % tuple(args))
527

    
528
  for line in data:
529
    args = []
530
    for idx in xrange(len(fields)):
531
      if separator is None:
532
        args.append(mlens[idx])
533
      args.append(line[idx])
534
    result.append(format % tuple(args))
535

    
536
  return result