Statistics
| Branch: | Tag: | Revision:

root / lib / cli.py @ cdb08f44

History | View | Annotate | Download (17.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", "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="Comma separated list of"
171
                         " output fields",
172
                         metavar="FIELDS")
173

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

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

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

    
183

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

    
188

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

    
193

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

    
198

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

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

    
208

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

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

    
217

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

    
221

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

226
  Arguments:
227
    argv: the command line
228

229
    commands: dictionary with special contents, see the design doc for
230
    cmdline handling
231
    aliases: dictionary with command aliases {'alias': 'target, ...}
232

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

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

    
245
  if len(argv) < 2 or not (argv[1] in commands or
246
                           argv[1] in aliases):
247
    # let's do a nice thing
248
    sortedcmds = commands.keys()
249
    sortedcmds.sort()
250
    print ("Usage: %(bin)s {command} [options...] [argument...]"
251
           "\n%(bin)s <command> --help to see details, or"
252
           " man %(bin)s\n" % {"bin": binary})
253
    # compute the max line length for cmd + usage
254
    mlen = max([len(" %s %s" % (cmd, commands[cmd][3])) for cmd in commands])
255
    mlen = min(60, mlen) # should not get here...
256
    # and format a nice command list
257
    print "Commands:"
258
    for cmd in sortedcmds:
259
      cmdstr = " %s %s" % (cmd, commands[cmd][3])
260
      help_text = commands[cmd][4]
261
      help_lines = textwrap.wrap(help_text, 79-3-mlen)
262
      print "%-*s - %s" % (mlen, cmdstr,
263
                                          help_lines.pop(0))
264
      for line in help_lines:
265
        print "%-*s   %s" % (mlen, "", line)
266
    print
267
    return None, None, None
268

    
269
  # get command, unalias it, and look it up in commands
270
  cmd = argv.pop(1)
271
  if cmd in aliases:
272
    if cmd in commands:
273
      raise errors.ProgrammerError("Alias '%s' overrides an existing"
274
                                   " command" % cmd)
275

    
276
    if aliases[cmd] not in commands:
277
      raise errors.ProgrammerError("Alias '%s' maps to non-existing"
278
                                   " command '%s'" % (cmd, aliases[cmd]))
279

    
280
    cmd = aliases[cmd]
281

    
282
  func, nargs, parser_opts, usage, description = commands[cmd]
283
  parser_opts.append(_LOCK_OPT)
284
  parser = OptionParser(option_list=parser_opts,
285
                        description=description,
286
                        formatter=TitledHelpFormatter(),
287
                        usage="%%prog %s %s" % (cmd, usage))
288
  parser.disable_interspersed_args()
289
  options, args = parser.parse_args()
290
  if nargs is None:
291
    if len(args) != 0:
292
      print >> sys.stderr, ("Error: Command %s expects no arguments" % cmd)
293
      return None, None, None
294
  elif nargs < 0 and len(args) != -nargs:
295
    print >> sys.stderr, ("Error: Command %s expects %d argument(s)" %
296
                         (cmd, -nargs))
297
    return None, None, None
298
  elif nargs >= 0 and len(args) < nargs:
299
    print >> sys.stderr, ("Error: Command %s expects at least %d argument(s)" %
300
                         (cmd, nargs))
301
    return None, None, None
302

    
303
  return func, options, args
304

    
305

    
306
def SplitNodeOption(value):
307
  """Splits the value of a --node option.
308

309
  """
310
  if value and ':' in value:
311
    return value.split(':', 1)
312
  else:
313
    return (value, None)
314

    
315

    
316
def AskUser(text, choices=None):
317
  """Ask the user a question.
318

319
  Args:
320
    text - the question to ask.
321

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

327
  Returns: one of the return values from the choices list; if input is
328
  not possible (i.e. not running with a tty, we return the last entry
329
  from the list
330

331
  """
332
  if choices is None:
333
    choices = [('y', True, 'Perform the operation'),
334
               ('n', False, 'Do not perform the operation')]
335
  if not choices or not isinstance(choices, list):
336
    raise errors.ProgrammerError("Invalid choiches argument to AskUser")
337
  for entry in choices:
338
    if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
339
      raise errors.ProgrammerError("Invalid choiches element to AskUser")
340

    
341
  answer = choices[-1][1]
342
  new_text = []
343
  for line in text.splitlines():
344
    new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
345
  text = "\n".join(new_text)
346
  try:
347
    f = file("/dev/tty", "a+")
348
  except IOError:
349
    return answer
350
  try:
351
    chars = [entry[0] for entry in choices]
352
    chars[-1] = "[%s]" % chars[-1]
353
    chars.append('?')
354
    maps = dict([(entry[0], entry[1]) for entry in choices])
355
    while True:
356
      f.write(text)
357
      f.write('\n')
358
      f.write("/".join(chars))
359
      f.write(": ")
360
      line = f.readline(2).strip().lower()
361
      if line in maps:
362
        answer = maps[line]
363
        break
364
      elif line == '?':
365
        for entry in choices:
366
          f.write(" %s - %s\n" % (entry[0], entry[2]))
367
        f.write("\n")
368
        continue
369
  finally:
370
    f.close()
371
  return answer
372

    
373

    
374
def SubmitOpCode(op, proc=None, feedback_fn=None):
375
  """Function to submit an opcode.
376

377
  This is just a simple wrapper over the construction of the processor
378
  instance. It should be extended to better handle feedback and
379
  interaction functions.
380

381
  """
382
  if feedback_fn is None:
383
    feedback_fn = logger.ToStdout
384
  if proc is None:
385
    proc = mcpu.Processor(feedback=feedback_fn)
386
  return proc.ExecOpCode(op)
387

    
388

    
389
def FormatError(err):
390
  """Return a formatted error message for a given error.
391

392
  This function takes an exception instance and returns a tuple
393
  consisting of two values: first, the recommended exit code, and
394
  second, a string describing the error message (not
395
  newline-terminated).
396

397
  """
398
  retcode = 1
399
  obuf = StringIO()
400
  msg = str(err)
401
  if isinstance(err, errors.ConfigurationError):
402
    txt = "Corrupt configuration file: %s" % msg
403
    logger.Error(txt)
404
    obuf.write(txt + "\n")
405
    obuf.write("Aborting.")
406
    retcode = 2
407
  elif isinstance(err, errors.HooksAbort):
408
    obuf.write("Failure: hooks execution failed:\n")
409
    for node, script, out in err.args[0]:
410
      if out:
411
        obuf.write("  node: %s, script: %s, output: %s\n" %
412
                   (node, script, out))
413
      else:
414
        obuf.write("  node: %s, script: %s (no output)\n" %
415
                   (node, script))
416
  elif isinstance(err, errors.HooksFailure):
417
    obuf.write("Failure: hooks general failure: %s" % msg)
418
  elif isinstance(err, errors.ResolverError):
419
    this_host = utils.HostInfo.SysName()
420
    if err.args[0] == this_host:
421
      msg = "Failure: can't resolve my own hostname ('%s')"
422
    else:
423
      msg = "Failure: can't resolve hostname '%s'"
424
    obuf.write(msg % err.args[0])
425
  elif isinstance(err, errors.OpPrereqError):
426
    obuf.write("Failure: prerequisites not met for this"
427
               " operation:\n%s" % msg)
428
  elif isinstance(err, errors.OpExecError):
429
    obuf.write("Failure: command execution error:\n%s" % msg)
430
  elif isinstance(err, errors.TagError):
431
    obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
432
  elif isinstance(err, errors.GenericError):
433
    obuf.write("Unhandled Ganeti error: %s" % msg)
434
  else:
435
    obuf.write("Unhandled exception: %s" % msg)
436
  return retcode, obuf.getvalue().rstrip('\n')
437

    
438

    
439
def GenericMain(commands, override=None, aliases=None):
440
  """Generic main function for all the gnt-* commands.
441

442
  Arguments:
443
    - commands: a dictionary with a special structure, see the design doc
444
                for command line handling.
445
    - override: if not None, we expect a dictionary with keys that will
446
                override command line options; this can be used to pass
447
                options from the scripts to generic functions
448
    - aliases: dictionary with command aliases {'alias': 'target, ...}
449

450
  """
451
  # save the program name and the entire command line for later logging
452
  if sys.argv:
453
    binary = os.path.basename(sys.argv[0]) or sys.argv[0]
454
    if len(sys.argv) >= 2:
455
      binary += " " + sys.argv[1]
456
      old_cmdline = " ".join(sys.argv[2:])
457
    else:
458
      old_cmdline = ""
459
  else:
460
    binary = "<unknown program>"
461
    old_cmdline = ""
462

    
463
  if aliases is None:
464
    aliases = {}
465

    
466
  func, options, args = _ParseArgs(sys.argv, commands, aliases)
467
  if func is None: # parse error
468
    return 1
469

    
470
  if override is not None:
471
    for key, val in override.iteritems():
472
      setattr(options, key, val)
473

    
474
  logger.SetupLogging(debug=options.debug, program=binary)
475

    
476
  utils.debug = options.debug
477
  try:
478
    utils.Lock('cmd', max_retries=options.lock_retries, debug=options.debug)
479
  except errors.LockError, err:
480
    logger.ToStderr(str(err))
481
    return 1
482
  except KeyboardInterrupt:
483
    logger.ToStderr("Aborting.")
484
    return 1
485

    
486
  if old_cmdline:
487
    logger.Info("run with arguments '%s'" % old_cmdline)
488
  else:
489
    logger.Info("run with no arguments")
490

    
491
  try:
492
    try:
493
      result = func(options, args)
494
    except errors.GenericError, err:
495
      result, err_msg = FormatError(err)
496
      logger.ToStderr(err_msg)
497
  finally:
498
    utils.Unlock('cmd')
499
    utils.LockCleanup()
500

    
501
  return result
502

    
503

    
504
def GenerateTable(headers, fields, separator, data,
505
                  numfields=None, unitfields=None):
506
  """Prints a table with headers and different fields.
507

508
  Args:
509
    headers: Dict of header titles or None if no headers should be shown
510
    fields: List of fields to show
511
    separator: String used to separate fields or None for spaces
512
    data: Data to be printed
513
    numfields: List of fields to be aligned to right
514
    unitfields: List of fields to be formatted as units
515

516
  """
517
  if numfields is None:
518
    numfields = []
519
  if unitfields is None:
520
    unitfields = []
521

    
522
  format_fields = []
523
  for field in fields:
524
    if headers and field not in headers:
525
      raise errors.ProgrammerError("Missing header description for field '%s'"
526
                                   % field)
527
    if separator is not None:
528
      format_fields.append("%s")
529
    elif field in numfields:
530
      format_fields.append("%*s")
531
    else:
532
      format_fields.append("%-*s")
533

    
534
  if separator is None:
535
    mlens = [0 for name in fields]
536
    format = ' '.join(format_fields)
537
  else:
538
    format = separator.replace("%", "%%").join(format_fields)
539

    
540
  for row in data:
541
    for idx, val in enumerate(row):
542
      if fields[idx] in unitfields:
543
        try:
544
          val = int(val)
545
        except ValueError:
546
          pass
547
        else:
548
          val = row[idx] = utils.FormatUnit(val)
549
      val = row[idx] = str(val)
550
      if separator is None:
551
        mlens[idx] = max(mlens[idx], len(val))
552

    
553
  result = []
554
  if headers:
555
    args = []
556
    for idx, name in enumerate(fields):
557
      hdr = headers[name]
558
      if separator is None:
559
        mlens[idx] = max(mlens[idx], len(hdr))
560
        args.append(mlens[idx])
561
      args.append(hdr)
562
    result.append(format % tuple(args))
563

    
564
  for line in data:
565
    args = []
566
    for idx in xrange(len(fields)):
567
      if separator is None:
568
        args.append(mlens[idx])
569
      args.append(line[idx])
570
    result.append(format % tuple(args))
571

    
572
  return result