Statistics
| Branch: | Tag: | Revision:

root / lib / cli.py @ 6e06b36c

History | View | Annotate | Download (16.4 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", "SplitNodeOption"
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

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

    
187

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

    
192

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

    
197

    
198
def check_unit(option, opt, value):
199
  """OptParsers custom converter for units.
200

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

    
207

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

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

    
216

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

    
220

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

225
  Arguments:
226
    argv: the command line
227

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

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

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

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

    
287
  return func, options, args
288

    
289

    
290
def SplitNodeOption(value):
291
  """Splits the value of a --node option.
292

293
  """
294
  if value and ':' in value:
295
    return value.split(':', 1)
296
  else:
297
    return (value, None)
298

    
299

    
300
def AskUser(text, choices=None):
301
  """Ask the user a question.
302

303
  Args:
304
    text - the question to ask.
305

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

311
  Returns: one of the return values from the choices list; if input is
312
  not possible (i.e. not running with a tty, we return the last entry
313
  from the list
314

315
  """
316
  if choices is None:
317
    choices = [('y', True, 'Perform the operation'),
318
               ('n', False, 'Do not perform the operation')]
319
  if not choices or not isinstance(choices, list):
320
    raise errors.ProgrammerError("Invalid choiches argument to AskUser")
321
  for entry in choices:
322
    if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
323
      raise errors.ProgrammerError("Invalid choiches element to AskUser")
324

    
325
  answer = choices[-1][1]
326
  new_text = []
327
  for line in text.splitlines():
328
    new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
329
  text = "\n".join(new_text)
330
  try:
331
    f = file("/dev/tty", "a+")
332
  except IOError:
333
    return answer
334
  try:
335
    chars = [entry[0] for entry in choices]
336
    chars[-1] = "[%s]" % chars[-1]
337
    chars.append('?')
338
    maps = dict([(entry[0], entry[1]) for entry in choices])
339
    while True:
340
      f.write(text)
341
      f.write('\n')
342
      f.write("/".join(chars))
343
      f.write(": ")
344
      line = f.readline(2).strip().lower()
345
      if line in maps:
346
        answer = maps[line]
347
        break
348
      elif line == '?':
349
        for entry in choices:
350
          f.write(" %s - %s\n" % (entry[0], entry[2]))
351
        f.write("\n")
352
        continue
353
  finally:
354
    f.close()
355
  return answer
356

    
357

    
358
def SubmitOpCode(op, proc=None, feedback_fn=None):
359
  """Function to submit an opcode.
360

361
  This is just a simple wrapper over the construction of the processor
362
  instance. It should be extended to better handle feedback and
363
  interaction functions.
364

365
  """
366
  if feedback_fn is None:
367
    feedback_fn = logger.ToStdout
368
  if proc is None:
369
    proc = mcpu.Processor(feedback=feedback_fn)
370
  return proc.ExecOpCode(op)
371

    
372

    
373
def FormatError(err):
374
  """Return a formatted error message for a given error.
375

376
  This function takes an exception instance and returns a tuple
377
  consisting of two values: first, the recommended exit code, and
378
  second, a string describing the error message (not
379
  newline-terminated).
380

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

    
422

    
423
def GenericMain(commands, override=None):
424
  """Generic main function for all the gnt-* commands.
425

426
  Arguments:
427
    - commands: a dictionary with a special structure, see the design doc
428
                for command line handling.
429
    - override: if not None, we expect a dictionary with keys that will
430
                override command line options; this can be used to pass
431
                options from the scripts to generic functions
432

433
  """
434
  # save the program name and the entire command line for later logging
435
  if sys.argv:
436
    binary = os.path.basename(sys.argv[0]) or sys.argv[0]
437
    if len(sys.argv) >= 2:
438
      binary += " " + sys.argv[1]
439
      old_cmdline = " ".join(sys.argv[2:])
440
    else:
441
      old_cmdline = ""
442
  else:
443
    binary = "<unknown program>"
444
    old_cmdline = ""
445

    
446
  func, options, args = _ParseArgs(sys.argv, commands)
447
  if func is None: # parse error
448
    return 1
449

    
450
  if override is not None:
451
    for key, val in override.iteritems():
452
      setattr(options, key, val)
453

    
454
  logger.SetupLogging(debug=options.debug, program=binary)
455

    
456
  try:
457
    utils.Lock('cmd', max_retries=options.lock_retries, debug=options.debug)
458
  except errors.LockError, err:
459
    logger.ToStderr(str(err))
460
    return 1
461

    
462
  if old_cmdline:
463
    logger.Info("run with arguments '%s'" % old_cmdline)
464
  else:
465
    logger.Info("run with no arguments")
466

    
467
  try:
468
    try:
469
      result = func(options, args)
470
    except errors.GenericError, err:
471
      result, err_msg = FormatError(err)
472
      logger.ToStderr(err_msg)
473
  finally:
474
    utils.Unlock('cmd')
475
    utils.LockCleanup()
476

    
477
  return result
478

    
479

    
480
def GenerateTable(headers, fields, separator, data,
481
                  numfields=None, unitfields=None):
482
  """Prints a table with headers and different fields.
483

484
  Args:
485
    headers: Dict of header titles or None if no headers should be shown
486
    fields: List of fields to show
487
    separator: String used to separate fields or None for spaces
488
    data: Data to be printed
489
    numfields: List of fields to be aligned to right
490
    unitfields: List of fields to be formatted as units
491

492
  """
493
  if numfields is None:
494
    numfields = []
495
  if unitfields is None:
496
    unitfields = []
497

    
498
  format_fields = []
499
  for field in fields:
500
    if headers and field not in headers:
501
      raise errors.ProgrammerError("Missing header description for field '%s'"
502
                                   % field)
503
    if separator is not None:
504
      format_fields.append("%s")
505
    elif field in numfields:
506
      format_fields.append("%*s")
507
    else:
508
      format_fields.append("%-*s")
509

    
510
  if separator is None:
511
    mlens = [0 for name in fields]
512
    format = ' '.join(format_fields)
513
  else:
514
    format = separator.replace("%", "%%").join(format_fields)
515

    
516
  for row in data:
517
    for idx, val in enumerate(row):
518
      if fields[idx] in unitfields:
519
        try:
520
          val = int(val)
521
        except ValueError:
522
          pass
523
        else:
524
          val = row[idx] = utils.FormatUnit(val)
525
      val = row[idx] = str(val)
526
      if separator is None:
527
        mlens[idx] = max(mlens[idx], len(val))
528

    
529
  result = []
530
  if headers:
531
    args = []
532
    for idx, name in enumerate(fields):
533
      hdr = headers[name]
534
      if separator is None:
535
        mlens[idx] = max(mlens[idx], len(hdr))
536
        args.append(mlens[idx])
537
      args.append(hdr)
538
    result.append(format % tuple(args))
539

    
540
  for line in data:
541
    args = []
542
    for idx in xrange(len(fields)):
543
      if separator is None:
544
        args.append(mlens[idx])
545
      args.append(line[idx])
546
    result.append(format % tuple(args))
547

    
548
  return result