Statistics
| Branch: | Tag: | Revision:

root / lib / cli.py @ e2e521d0

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

    
411

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

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

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

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

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

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

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

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

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

    
466
  return result
467

    
468

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

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

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

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

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

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

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

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

    
537
  return result