Statistics
| Branch: | Tag: | Revision:

root / lib / cli.py @ 685ee993

History | View | Annotate | Download (18.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
import time
30
from cStringIO import StringIO
31

    
32
from ganeti import utils
33
from ganeti import logger
34
from ganeti import errors
35
from ganeti import mcpu
36
from ganeti import constants
37
from ganeti import opcodes
38
from ganeti import luxi
39

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

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

    
52

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

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

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

    
73

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

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

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

    
102

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

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

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

    
120

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

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

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

    
137

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

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

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

    
154

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

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

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

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

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

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

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

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

    
186

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

    
191

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

    
196

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

    
201

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

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

    
211

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

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

    
220

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

    
224

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

229
  Arguments:
230
    argv: the command line
231

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

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

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

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

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

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

    
283
    cmd = aliases[cmd]
284

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

    
306
  return func, options, args
307

    
308

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

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

    
318

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

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

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

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

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

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

    
376

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

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

384
  """
385
  cl = luxi.Client()
386
  job = opcodes.Job(op_list=[op])
387
  jid = SubmitJob(job)
388

    
389
  query = {
390
    "object": "jobs",
391
    "fields": ["status"],
392
    "names": [jid],
393
    }
394

    
395
  while True:
396
    jdata = SubmitQuery(query)
397
    if not jdata:
398
      # job not found, go away!
399
      raise errors.JobLost("Job with id %s lost" % jid)
400

    
401
    status = jdata[0][0]
402
    if status in (opcodes.Job.STATUS_SUCCESS, opcodes.Job.STATUS_FAIL):
403
      break
404
    time.sleep(1)
405

    
406
  query["fields"].extend(["op_list", "op_status", "op_result"])
407
  jdata = SubmitQuery(query)
408
  if not jdata:
409
    raise errors.JobLost("Job with id %s lost" % jid)
410
  status, op_list, op_status, op_result = jdata[0]
411
  if status != opcodes.Job.STATUS_SUCCESS:
412
    raise errors.OpExecError(op_result[0])
413
  return op_result[0]
414

    
415
  if feedback_fn is None:
416
    feedback_fn = logger.ToStdout
417
  if proc is None:
418
    proc = mcpu.Processor(feedback=feedback_fn)
419
  return proc.ExecOpCode(op)
420

    
421

    
422
def SubmitJob(job, cl=None):
423
  if cl is None:
424
    cl = luxi.Client()
425
  return cl.SubmitJob(job)
426

    
427

    
428
def SubmitQuery(data, cl=None):
429
  if cl is None:
430
    cl = luxi.Client()
431
  return cl.Query(data)
432

    
433

    
434
def FormatError(err):
435
  """Return a formatted error message for a given error.
436

437
  This function takes an exception instance and returns a tuple
438
  consisting of two values: first, the recommended exit code, and
439
  second, a string describing the error message (not
440
  newline-terminated).
441

442
  """
443
  retcode = 1
444
  obuf = StringIO()
445
  msg = str(err)
446
  if isinstance(err, errors.ConfigurationError):
447
    txt = "Corrupt configuration file: %s" % msg
448
    logger.Error(txt)
449
    obuf.write(txt + "\n")
450
    obuf.write("Aborting.")
451
    retcode = 2
452
  elif isinstance(err, errors.HooksAbort):
453
    obuf.write("Failure: hooks execution failed:\n")
454
    for node, script, out in err.args[0]:
455
      if out:
456
        obuf.write("  node: %s, script: %s, output: %s\n" %
457
                   (node, script, out))
458
      else:
459
        obuf.write("  node: %s, script: %s (no output)\n" %
460
                   (node, script))
461
  elif isinstance(err, errors.HooksFailure):
462
    obuf.write("Failure: hooks general failure: %s" % msg)
463
  elif isinstance(err, errors.ResolverError):
464
    this_host = utils.HostInfo.SysName()
465
    if err.args[0] == this_host:
466
      msg = "Failure: can't resolve my own hostname ('%s')"
467
    else:
468
      msg = "Failure: can't resolve hostname '%s'"
469
    obuf.write(msg % err.args[0])
470
  elif isinstance(err, errors.OpPrereqError):
471
    obuf.write("Failure: prerequisites not met for this"
472
               " operation:\n%s" % msg)
473
  elif isinstance(err, errors.OpExecError):
474
    obuf.write("Failure: command execution error:\n%s" % msg)
475
  elif isinstance(err, errors.TagError):
476
    obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
477
  elif isinstance(err, errors.GenericError):
478
    obuf.write("Unhandled Ganeti error: %s" % msg)
479
  else:
480
    obuf.write("Unhandled exception: %s" % msg)
481
  return retcode, obuf.getvalue().rstrip('\n')
482

    
483

    
484
def GenericMain(commands, override=None, aliases=None):
485
  """Generic main function for all the gnt-* commands.
486

487
  Arguments:
488
    - commands: a dictionary with a special structure, see the design doc
489
                for command line handling.
490
    - override: if not None, we expect a dictionary with keys that will
491
                override command line options; this can be used to pass
492
                options from the scripts to generic functions
493
    - aliases: dictionary with command aliases {'alias': 'target, ...}
494

495
  """
496
  # save the program name and the entire command line for later logging
497
  if sys.argv:
498
    binary = os.path.basename(sys.argv[0]) or sys.argv[0]
499
    if len(sys.argv) >= 2:
500
      binary += " " + sys.argv[1]
501
      old_cmdline = " ".join(sys.argv[2:])
502
    else:
503
      old_cmdline = ""
504
  else:
505
    binary = "<unknown program>"
506
    old_cmdline = ""
507

    
508
  if aliases is None:
509
    aliases = {}
510

    
511
  func, options, args = _ParseArgs(sys.argv, commands, aliases)
512
  if func is None: # parse error
513
    return 1
514

    
515
  if override is not None:
516
    for key, val in override.iteritems():
517
      setattr(options, key, val)
518

    
519
  logger.SetupLogging(debug=options.debug, program=binary)
520

    
521
  utils.debug = options.debug
522
  try:
523
    utils.Lock('cmd', max_retries=options.lock_retries, debug=options.debug)
524
  except errors.LockError, err:
525
    logger.ToStderr(str(err))
526
    return 1
527
  except KeyboardInterrupt:
528
    logger.ToStderr("Aborting.")
529
    return 1
530

    
531
  if old_cmdline:
532
    logger.Info("run with arguments '%s'" % old_cmdline)
533
  else:
534
    logger.Info("run with no arguments")
535

    
536
  try:
537
    try:
538
      result = func(options, args)
539
    except errors.GenericError, err:
540
      result, err_msg = FormatError(err)
541
      logger.ToStderr(err_msg)
542
  finally:
543
    utils.Unlock('cmd')
544
    utils.LockCleanup()
545

    
546
  return result
547

    
548

    
549
def GenerateTable(headers, fields, separator, data,
550
                  numfields=None, unitfields=None):
551
  """Prints a table with headers and different fields.
552

553
  Args:
554
    headers: Dict of header titles or None if no headers should be shown
555
    fields: List of fields to show
556
    separator: String used to separate fields or None for spaces
557
    data: Data to be printed
558
    numfields: List of fields to be aligned to right
559
    unitfields: List of fields to be formatted as units
560

561
  """
562
  if numfields is None:
563
    numfields = []
564
  if unitfields is None:
565
    unitfields = []
566

    
567
  format_fields = []
568
  for field in fields:
569
    if headers and field not in headers:
570
      raise errors.ProgrammerError("Missing header description for field '%s'"
571
                                   % field)
572
    if separator is not None:
573
      format_fields.append("%s")
574
    elif field in numfields:
575
      format_fields.append("%*s")
576
    else:
577
      format_fields.append("%-*s")
578

    
579
  if separator is None:
580
    mlens = [0 for name in fields]
581
    format = ' '.join(format_fields)
582
  else:
583
    format = separator.replace("%", "%%").join(format_fields)
584

    
585
  for row in data:
586
    for idx, val in enumerate(row):
587
      if fields[idx] in unitfields:
588
        try:
589
          val = int(val)
590
        except ValueError:
591
          pass
592
        else:
593
          val = row[idx] = utils.FormatUnit(val)
594
      val = row[idx] = str(val)
595
      if separator is None:
596
        mlens[idx] = max(mlens[idx], len(val))
597

    
598
  result = []
599
  if headers:
600
    args = []
601
    for idx, name in enumerate(fields):
602
      hdr = headers[name]
603
      if separator is None:
604
        mlens[idx] = max(mlens[idx], len(hdr))
605
        args.append(mlens[idx])
606
      args.append(hdr)
607
    result.append(format % tuple(args))
608

    
609
  for line in data:
610
    args = []
611
    for idx in xrange(len(fields)):
612
      if separator is None:
613
        args.append(mlens[idx])
614
      args.append(line[idx])
615
    result.append(format % tuple(args))
616

    
617
  return result