Statistics
| Branch: | Tag: | Revision:

root / lib / cli.py @ 0a1e74d9

History | View | Annotate | Download (19.1 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 constants
36
from ganeti import opcodes
37
from ganeti import luxi
38
from ganeti import ssconf
39

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

    
43
__all__ = ["DEBUG_OPT", "NOHDR_OPT", "SEP_OPT", "GenericMain",
44
           "SubmitOpCode", "GetClient",
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
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" % cmd) 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" % (cmd,)
260
      help_text = commands[cmd][4]
261
      help_lines = textwrap.wrap(help_text, 79-3-mlen)
262
      print "%-*s - %s" % (mlen, cmdstr, help_lines.pop(0))
263
      for line in help_lines:
264
        print "%-*s   %s" % (mlen, "", line)
265
    print
266
    return None, None, None
267

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

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

    
279
    cmd = aliases[cmd]
280

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

    
301
  return func, options, args
302

    
303

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

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

    
313

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

317
  Args:
318
    text - the question to ask.
319

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

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

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

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

    
371

    
372
def SendJob(ops, cl=None):
373
  """Function to submit an opcode without waiting for the results.
374

375
  @type ops: list
376
  @param ops: list of opcodes
377
  @type cl: luxi.Client
378
  @param cl: the luxi client to use for communicating with the master;
379
             if None, a new client will be created
380

381
  """
382
  if cl is None:
383
    cl = GetClient()
384

    
385
  job_id = cl.SubmitJob(ops)
386

    
387
  return job_id
388

    
389

    
390
def PollJob(job_id, cl=None):
391
  """Function to poll for the result of a job.
392

393
  @type job_id: job identified
394
  @param job_id: the job to poll for results
395
  @type cl: luxi.Client
396
  @param cl: the luxi client to use for communicating with the master;
397
             if None, a new client will be created
398

399
  """
400
  if cl is None:
401
    cl = GetClient()
402

    
403
  lastmsg = None
404
  while True:
405
    jobs = cl.QueryJobs([job_id], ["status", "ticker"])
406
    if not jobs:
407
      # job not found, go away!
408
      raise errors.JobLost("Job with id %s lost" % job_id)
409

    
410
    # TODO: Handle canceled and archived jobs
411
    status = jobs[0][0]
412
    if status in (constants.JOB_STATUS_SUCCESS, constants.JOB_STATUS_ERROR):
413
      break
414
    msg = jobs[0][1]
415
    if msg is not None and msg != lastmsg:
416
      if callable(feedback_fn):
417
        feedback_fn(msg)
418
      else:
419
        print "%s %s" % (time.ctime(msg[0]), msg[2])
420
    lastmsg = msg
421
    time.sleep(1)
422

    
423
  jobs = cl.QueryJobs([job_id], ["status", "opresult"])
424
  if not jobs:
425
    raise errors.JobLost("Job with id %s lost" % job_id)
426

    
427
  status, result = jobs[0]
428
  if status == constants.JOB_STATUS_SUCCESS:
429
    return result[0]
430
  else:
431
    raise errors.OpExecError(result)
432

    
433

    
434
def SubmitOpCode(op, cl=None, feedback_fn=None):
435
  """Legacy function to submit an opcode.
436

437
  This is just a simple wrapper over the construction of the processor
438
  instance. It should be extended to better handle feedback and
439
  interaction functions.
440

441
  """
442
  if cl is None:
443
    cl = GetClient()
444

    
445
  job_id = SendJob([op], cl)
446

    
447
  return PollJob(job_id, cl)
448

    
449

    
450
def GetClient():
451
  # TODO: Cache object?
452
  try:
453
    client = luxi.Client()
454
  except luxi.NoMasterError:
455
    master, myself = ssconf.GetMasterAndMyself()
456
    if master != myself:
457
      raise errors.OpPrereqError("This is not the master node, please connect"
458
                                 " to node '%s' and rerun the command" %
459
                                 master)
460
    else:
461
      raise
462
  return client
463

    
464

    
465
def FormatError(err):
466
  """Return a formatted error message for a given error.
467

468
  This function takes an exception instance and returns a tuple
469
  consisting of two values: first, the recommended exit code, and
470
  second, a string describing the error message (not
471
  newline-terminated).
472

473
  """
474
  retcode = 1
475
  obuf = StringIO()
476
  msg = str(err)
477
  if isinstance(err, errors.ConfigurationError):
478
    txt = "Corrupt configuration file: %s" % msg
479
    logger.Error(txt)
480
    obuf.write(txt + "\n")
481
    obuf.write("Aborting.")
482
    retcode = 2
483
  elif isinstance(err, errors.HooksAbort):
484
    obuf.write("Failure: hooks execution failed:\n")
485
    for node, script, out in err.args[0]:
486
      if out:
487
        obuf.write("  node: %s, script: %s, output: %s\n" %
488
                   (node, script, out))
489
      else:
490
        obuf.write("  node: %s, script: %s (no output)\n" %
491
                   (node, script))
492
  elif isinstance(err, errors.HooksFailure):
493
    obuf.write("Failure: hooks general failure: %s" % msg)
494
  elif isinstance(err, errors.ResolverError):
495
    this_host = utils.HostInfo.SysName()
496
    if err.args[0] == this_host:
497
      msg = "Failure: can't resolve my own hostname ('%s')"
498
    else:
499
      msg = "Failure: can't resolve hostname '%s'"
500
    obuf.write(msg % err.args[0])
501
  elif isinstance(err, errors.OpPrereqError):
502
    obuf.write("Failure: prerequisites not met for this"
503
               " operation:\n%s" % msg)
504
  elif isinstance(err, errors.OpExecError):
505
    obuf.write("Failure: command execution error:\n%s" % msg)
506
  elif isinstance(err, errors.TagError):
507
    obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
508
  elif isinstance(err, errors.GenericError):
509
    obuf.write("Unhandled Ganeti error: %s" % msg)
510
  elif isinstance(err, luxi.NoMasterError):
511
    obuf.write("Cannot communicate with the master daemon.\nIs it running"
512
               " and listening on '%s'?" % err.args[0])
513
  elif isinstance(err, luxi.TimeoutError):
514
    obuf.write("Timeout while talking to the master daemon. Error:\n"
515
               "%s" % msg)
516
  elif isinstance(err, luxi.ProtocolError):
517
    obuf.write("Unhandled protocol error while talking to the master daemon:\n"
518
               "%s" % msg)
519
  else:
520
    obuf.write("Unhandled exception: %s" % msg)
521
  return retcode, obuf.getvalue().rstrip('\n')
522

    
523

    
524
def GenericMain(commands, override=None, aliases=None):
525
  """Generic main function for all the gnt-* commands.
526

527
  Arguments:
528
    - commands: a dictionary with a special structure, see the design doc
529
                for command line handling.
530
    - override: if not None, we expect a dictionary with keys that will
531
                override command line options; this can be used to pass
532
                options from the scripts to generic functions
533
    - aliases: dictionary with command aliases {'alias': 'target, ...}
534

535
  """
536
  # save the program name and the entire command line for later logging
537
  if sys.argv:
538
    binary = os.path.basename(sys.argv[0]) or sys.argv[0]
539
    if len(sys.argv) >= 2:
540
      binary += " " + sys.argv[1]
541
      old_cmdline = " ".join(sys.argv[2:])
542
    else:
543
      old_cmdline = ""
544
  else:
545
    binary = "<unknown program>"
546
    old_cmdline = ""
547

    
548
  if aliases is None:
549
    aliases = {}
550

    
551
  func, options, args = _ParseArgs(sys.argv, commands, aliases)
552
  if func is None: # parse error
553
    return 1
554

    
555
  if override is not None:
556
    for key, val in override.iteritems():
557
      setattr(options, key, val)
558

    
559
  logger.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
560
                      stderr_logging=True, program=binary)
561

    
562
  utils.debug = options.debug
563

    
564
  if old_cmdline:
565
    logger.Info("run with arguments '%s'" % old_cmdline)
566
  else:
567
    logger.Info("run with no arguments")
568

    
569
  try:
570
    result = func(options, args)
571
  except (errors.GenericError, luxi.ProtocolError), err:
572
    result, err_msg = FormatError(err)
573
    logger.ToStderr(err_msg)
574

    
575
  return result
576

    
577

    
578
def GenerateTable(headers, fields, separator, data,
579
                  numfields=None, unitfields=None):
580
  """Prints a table with headers and different fields.
581

582
  Args:
583
    headers: Dict of header titles or None if no headers should be shown
584
    fields: List of fields to show
585
    separator: String used to separate fields or None for spaces
586
    data: Data to be printed
587
    numfields: List of fields to be aligned to right
588
    unitfields: List of fields to be formatted as units
589

590
  """
591
  if numfields is None:
592
    numfields = []
593
  if unitfields is None:
594
    unitfields = []
595

    
596
  format_fields = []
597
  for field in fields:
598
    if headers and field not in headers:
599
      raise errors.ProgrammerError("Missing header description for field '%s'"
600
                                   % field)
601
    if separator is not None:
602
      format_fields.append("%s")
603
    elif field in numfields:
604
      format_fields.append("%*s")
605
    else:
606
      format_fields.append("%-*s")
607

    
608
  if separator is None:
609
    mlens = [0 for name in fields]
610
    format = ' '.join(format_fields)
611
  else:
612
    format = separator.replace("%", "%%").join(format_fields)
613

    
614
  for row in data:
615
    for idx, val in enumerate(row):
616
      if fields[idx] in unitfields:
617
        try:
618
          val = int(val)
619
        except ValueError:
620
          pass
621
        else:
622
          val = row[idx] = utils.FormatUnit(val)
623
      val = row[idx] = str(val)
624
      if separator is None:
625
        mlens[idx] = max(mlens[idx], len(val))
626

    
627
  result = []
628
  if headers:
629
    args = []
630
    for idx, name in enumerate(fields):
631
      hdr = headers[name]
632
      if separator is None:
633
        mlens[idx] = max(mlens[idx], len(hdr))
634
        args.append(mlens[idx])
635
      args.append(hdr)
636
    result.append(format % tuple(args))
637

    
638
  for line in data:
639
    args = []
640
    for idx in xrange(len(fields)):
641
      if separator is None:
642
        args.append(mlens[idx])
643
      args.append(line[idx])
644
    result.append(format % tuple(args))
645

    
646
  return result