Statistics
| Branch: | Tag: | Revision:

root / lib / cli.py @ 8d528b7c

History | View | Annotate | Download (17.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
from ganeti import luxi
38

    
39
from optparse import (OptionParser, make_option, TitledHelpFormatter,
40
                      Option, OptionValueError, SUPPRESS_HELP)
41

    
42
__all__ = ["DEBUG_OPT", "NOHDR_OPT", "SEP_OPT", "GenericMain",
43
           "SubmitOpCode", "SubmitJob", "SubmitQuery",
44
           "cli_option", "GenerateTable", "AskUser",
45
           "ARGS_NONE", "ARGS_FIXED", "ARGS_ATLEAST", "ARGS_ANY", "ARGS_ONE",
46
           "USEUNITS_OPT", "FIELDS_OPT", "FORCE_OPT",
47
           "ListTags", "AddTags", "RemoveTags", "TAG_SRC_OPT",
48
           "FormatError", "SplitNodeOption"
49
           ]
50

    
51

    
52
def _ExtractTagsObject(opts, args):
53
  """Extract the tag type object.
54

55
  Note that this function will modify its args parameter.
56

57
  """
58
  if not hasattr(opts, "tag_type"):
59
    raise errors.ProgrammerError("tag_type not passed to _ExtractTagsObject")
60
  kind = opts.tag_type
61
  if kind == constants.TAG_CLUSTER:
62
    retval = kind, kind
63
  elif kind == constants.TAG_NODE or kind == constants.TAG_INSTANCE:
64
    if not args:
65
      raise errors.OpPrereqError("no arguments passed to the command")
66
    name = args.pop(0)
67
    retval = kind, name
68
  else:
69
    raise errors.ProgrammerError("Unhandled tag type '%s'" % kind)
70
  return retval
71

    
72

    
73
def _ExtendTags(opts, args):
74
  """Extend the args if a source file has been given.
75

76
  This function will extend the tags with the contents of the file
77
  passed in the 'tags_source' attribute of the opts parameter. A file
78
  named '-' will be replaced by stdin.
79

80
  """
81
  fname = opts.tags_source
82
  if fname is None:
83
    return
84
  if fname == "-":
85
    new_fh = sys.stdin
86
  else:
87
    new_fh = open(fname, "r")
88
  new_data = []
89
  try:
90
    # we don't use the nice 'new_data = [line.strip() for line in fh]'
91
    # because of python bug 1633941
92
    while True:
93
      line = new_fh.readline()
94
      if not line:
95
        break
96
      new_data.append(line.strip())
97
  finally:
98
    new_fh.close()
99
  args.extend(new_data)
100

    
101

    
102
def ListTags(opts, args):
103
  """List the tags on a given object.
104

105
  This is a generic implementation that knows how to deal with all
106
  three cases of tag objects (cluster, node, instance). The opts
107
  argument is expected to contain a tag_type field denoting what
108
  object type we work on.
109

110
  """
111
  kind, name = _ExtractTagsObject(opts, args)
112
  op = opcodes.OpGetTags(kind=kind, name=name)
113
  result = SubmitOpCode(op)
114
  result = list(result)
115
  result.sort()
116
  for tag in result:
117
    print tag
118

    
119

    
120
def AddTags(opts, args):
121
  """Add tags on a given object.
122

123
  This is a generic implementation that knows how to deal with all
124
  three cases of tag objects (cluster, node, instance). The opts
125
  argument is expected to contain a tag_type field denoting what
126
  object type we work on.
127

128
  """
129
  kind, name = _ExtractTagsObject(opts, args)
130
  _ExtendTags(opts, args)
131
  if not args:
132
    raise errors.OpPrereqError("No tags to be added")
133
  op = opcodes.OpAddTags(kind=kind, name=name, tags=args)
134
  SubmitOpCode(op)
135

    
136

    
137
def RemoveTags(opts, args):
138
  """Remove tags from a given object.
139

140
  This is a generic implementation that knows how to deal with all
141
  three cases of tag objects (cluster, node, instance). The opts
142
  argument is expected to contain a tag_type field denoting what
143
  object type we work on.
144

145
  """
146
  kind, name = _ExtractTagsObject(opts, args)
147
  _ExtendTags(opts, args)
148
  if not args:
149
    raise errors.OpPrereqError("No tags to be removed")
150
  op = opcodes.OpDelTags(kind=kind, name=name, tags=args)
151
  SubmitOpCode(op)
152

    
153

    
154
DEBUG_OPT = make_option("-d", "--debug", default=False,
155
                        action="store_true",
156
                        help="Turn debugging on")
157

    
158
NOHDR_OPT = make_option("--no-headers", default=False,
159
                        action="store_true", dest="no_headers",
160
                        help="Don't display column headers")
161

    
162
SEP_OPT = make_option("--separator", default=None,
163
                      action="store", dest="separator",
164
                      help="Separator between output fields"
165
                      " (defaults to one space)")
166

    
167
USEUNITS_OPT = make_option("--human-readable", default=False,
168
                           action="store_true", dest="human_readable",
169
                           help="Print sizes in human readable format")
170

    
171
FIELDS_OPT = make_option("-o", "--output", dest="output", action="store",
172
                         type="string", help="Comma separated list of"
173
                         " output fields",
174
                         metavar="FIELDS")
175

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

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

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

    
185

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

    
190

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

    
195

    
196
ARGS_NONE = None
197
ARGS_ONE = ARGS_FIXED(1)
198
ARGS_ANY = ARGS_ATLEAST(0)
199

    
200

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

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

    
210

    
211
class CliOption(Option):
212
  """Custom option class for optparse.
213

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

    
219

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

    
223

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

228
  Arguments:
229
    argv: the command line
230

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

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

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

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

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

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

    
282
    cmd = aliases[cmd]
283

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

    
305
  return func, options, args
306

    
307

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

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

    
317

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

321
  Args:
322
    text - the question to ask.
323

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

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

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

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

    
375

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

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

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

    
390

    
391
def SubmitJob(job, cl=None):
392
  if cl is None:
393
    cl = luxi.Client()
394
  return cl.SubmitJob(job)
395

    
396

    
397
def SubmitQuery(data, cl=None):
398
  if cl is None:
399
    cl = luxi.Client()
400
  return cl.Query(data)
401

    
402

    
403
def FormatError(err):
404
  """Return a formatted error message for a given error.
405

406
  This function takes an exception instance and returns a tuple
407
  consisting of two values: first, the recommended exit code, and
408
  second, a string describing the error message (not
409
  newline-terminated).
410

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

    
452

    
453
def GenericMain(commands, override=None, aliases=None):
454
  """Generic main function for all the gnt-* commands.
455

456
  Arguments:
457
    - commands: a dictionary with a special structure, see the design doc
458
                for command line handling.
459
    - override: if not None, we expect a dictionary with keys that will
460
                override command line options; this can be used to pass
461
                options from the scripts to generic functions
462
    - aliases: dictionary with command aliases {'alias': 'target, ...}
463

464
  """
465
  # save the program name and the entire command line for later logging
466
  if sys.argv:
467
    binary = os.path.basename(sys.argv[0]) or sys.argv[0]
468
    if len(sys.argv) >= 2:
469
      binary += " " + sys.argv[1]
470
      old_cmdline = " ".join(sys.argv[2:])
471
    else:
472
      old_cmdline = ""
473
  else:
474
    binary = "<unknown program>"
475
    old_cmdline = ""
476

    
477
  if aliases is None:
478
    aliases = {}
479

    
480
  func, options, args = _ParseArgs(sys.argv, commands, aliases)
481
  if func is None: # parse error
482
    return 1
483

    
484
  if override is not None:
485
    for key, val in override.iteritems():
486
      setattr(options, key, val)
487

    
488
  logger.SetupLogging(debug=options.debug, program=binary)
489

    
490
  utils.debug = options.debug
491
  try:
492
    utils.Lock('cmd', max_retries=options.lock_retries, debug=options.debug)
493
  except errors.LockError, err:
494
    logger.ToStderr(str(err))
495
    return 1
496
  except KeyboardInterrupt:
497
    logger.ToStderr("Aborting.")
498
    return 1
499

    
500
  if old_cmdline:
501
    logger.Info("run with arguments '%s'" % old_cmdline)
502
  else:
503
    logger.Info("run with no arguments")
504

    
505
  try:
506
    try:
507
      result = func(options, args)
508
    except errors.GenericError, err:
509
      result, err_msg = FormatError(err)
510
      logger.ToStderr(err_msg)
511
  finally:
512
    utils.Unlock('cmd')
513
    utils.LockCleanup()
514

    
515
  return result
516

    
517

    
518
def GenerateTable(headers, fields, separator, data,
519
                  numfields=None, unitfields=None):
520
  """Prints a table with headers and different fields.
521

522
  Args:
523
    headers: Dict of header titles or None if no headers should be shown
524
    fields: List of fields to show
525
    separator: String used to separate fields or None for spaces
526
    data: Data to be printed
527
    numfields: List of fields to be aligned to right
528
    unitfields: List of fields to be formatted as units
529

530
  """
531
  if numfields is None:
532
    numfields = []
533
  if unitfields is None:
534
    unitfields = []
535

    
536
  format_fields = []
537
  for field in fields:
538
    if headers and field not in headers:
539
      raise errors.ProgrammerError("Missing header description for field '%s'"
540
                                   % field)
541
    if separator is not None:
542
      format_fields.append("%s")
543
    elif field in numfields:
544
      format_fields.append("%*s")
545
    else:
546
      format_fields.append("%-*s")
547

    
548
  if separator is None:
549
    mlens = [0 for name in fields]
550
    format = ' '.join(format_fields)
551
  else:
552
    format = separator.replace("%", "%%").join(format_fields)
553

    
554
  for row in data:
555
    for idx, val in enumerate(row):
556
      if fields[idx] in unitfields:
557
        try:
558
          val = int(val)
559
        except ValueError:
560
          pass
561
        else:
562
          val = row[idx] = utils.FormatUnit(val)
563
      val = row[idx] = str(val)
564
      if separator is None:
565
        mlens[idx] = max(mlens[idx], len(val))
566

    
567
  result = []
568
  if headers:
569
    args = []
570
    for idx, name in enumerate(fields):
571
      hdr = headers[name]
572
      if separator is None:
573
        mlens[idx] = max(mlens[idx], len(hdr))
574
        args.append(mlens[idx])
575
      args.append(hdr)
576
    result.append(format % tuple(args))
577

    
578
  for line in data:
579
    args = []
580
    for idx in xrange(len(fields)):
581
      if separator is None:
582
        args.append(mlens[idx])
583
      args.append(line[idx])
584
    result.append(format % tuple(args))
585

    
586
  return result