Statistics
| Branch: | Tag: | Revision:

root / lib / cli.py @ 9f33ef86

History | View | Annotate | Download (15.3 kB)

1
#!/usr/bin/python
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

    
30
from ganeti import utils
31
from ganeti import logger
32
from ganeti import errors
33
from ganeti import mcpu
34
from ganeti import constants
35
from ganeti import opcodes
36

    
37
from optparse import (OptionParser, make_option, TitledHelpFormatter,
38
                      Option, OptionValueError, SUPPRESS_HELP)
39

    
40
__all__ = ["DEBUG_OPT", "NOHDR_OPT", "SEP_OPT", "GenericMain", "SubmitOpCode",
41
           "cli_option", "GenerateTable", "AskUser",
42
           "ARGS_NONE", "ARGS_FIXED", "ARGS_ATLEAST", "ARGS_ANY", "ARGS_ONE",
43
           "USEUNITS_OPT", "FIELDS_OPT", "FORCE_OPT",
44
           "ListTags", "AddTags", "RemoveTags", "TAG_SRC_OPT",
45
           ]
46

    
47

    
48
def _ExtractTagsObject(opts, args):
49
  """Extract the tag type object.
50

51
  Note that this function will modify its args parameter.
52

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

    
68

    
69
def _ExtendTags(opts, args):
70
  """Extend the args if a source file has been given.
71

72
  This function will extend the tags with the contents of the file
73
  passed in the 'tags_source' attribute of the opts parameter. A file
74
  named '-' will be replaced by stdin.
75

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

    
97

    
98
def ListTags(opts, args):
99
  """List the tags on a given object.
100

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

106
  """
107
  kind, name = _ExtractTagsObject(opts, args)
108
  op = opcodes.OpGetTags(kind=kind, name=name)
109
  result = SubmitOpCode(op)
110
  result = list(result)
111
  result.sort()
112
  for tag in result:
113
    print tag
114

    
115

    
116
def AddTags(opts, args):
117
  """Add tags on a given object.
118

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

124
  """
125
  kind, name = _ExtractTagsObject(opts, args)
126
  _ExtendTags(opts, args)
127
  if not args:
128
    raise errors.OpPrereqError("No tags to be added")
129
  op = opcodes.OpAddTags(kind=kind, name=name, tags=args)
130
  SubmitOpCode(op)
131

    
132

    
133
def RemoveTags(opts, args):
134
  """Remove tags from a given object.
135

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

141
  """
142
  kind, name = _ExtractTagsObject(opts, args)
143
  _ExtendTags(opts, args)
144
  if not args:
145
    raise errors.OpPrereqError("No tags to be removed")
146
  op = opcodes.OpDelTags(kind=kind, name=name, tags=args)
147
  SubmitOpCode(op)
148

    
149

    
150
DEBUG_OPT = make_option("-d", "--debug", default=False,
151
                        action="store_true",
152
                        help="Turn debugging on")
153

    
154
NOHDR_OPT = make_option("--no-headers", default=False,
155
                        action="store_true", dest="no_headers",
156
                        help="Don't display column headers")
157

    
158
SEP_OPT = make_option("--separator", default=None,
159
                      action="store", dest="separator",
160
                      help="Separator between output fields"
161
                      " (defaults to one space)")
162

    
163
USEUNITS_OPT = make_option("--human-readable", default=False,
164
                           action="store_true", dest="human_readable",
165
                           help="Print sizes in human readable format")
166

    
167
FIELDS_OPT = make_option("-o", "--output", dest="output", action="store",
168
                         type="string", help="Select output fields",
169
                         metavar="FIELDS")
170

    
171
FORCE_OPT = make_option("-f", "--force", dest="force", action="store_true",
172
                        default=False, help="Force the operation")
173

    
174
_LOCK_OPT = make_option("--lock-retries", default=None,
175
                        type="int", help=SUPPRESS_HELP)
176

    
177
TAG_SRC_OPT = make_option("--from", dest="tags_source",
178
                          default=None, help="File with tag names")
179

    
180
def ARGS_FIXED(val):
181
  """Macro-like function denoting a fixed number of arguments"""
182
  return -val
183

    
184

    
185
def ARGS_ATLEAST(val):
186
  """Macro-like function denoting a minimum number of arguments"""
187
  return val
188

    
189

    
190
ARGS_NONE = None
191
ARGS_ONE = ARGS_FIXED(1)
192
ARGS_ANY = ARGS_ATLEAST(0)
193

    
194

    
195
def check_unit(option, opt, value):
196
  try:
197
    return utils.ParseUnit(value)
198
  except errors.UnitParseError, err:
199
    raise OptionValueError("option %s: %s" % (opt, err))
200

    
201

    
202
class CliOption(Option):
203
  TYPES = Option.TYPES + ("unit",)
204
  TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER)
205
  TYPE_CHECKER["unit"] = check_unit
206

    
207

    
208
# optparse.py sets make_option, so we do it for our own option class, too
209
cli_option = CliOption
210

    
211

    
212
def _ParseArgs(argv, commands):
213
  """Parses the command line and return the function which must be
214
  executed together with its arguments
215

216
  Arguments:
217
    argv: the command line
218

219
    commands: dictionary with special contents, see the design doc for
220
    cmdline handling
221

222
  """
223
  if len(argv) == 0:
224
    binary = "<command>"
225
  else:
226
    binary = argv[0].split("/")[-1]
227

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

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

    
278
  return func, options, args
279

    
280

    
281
def AskUser(text, choices=None):
282
  """Ask the user a question.
283

284
  Args:
285
    text - the question to ask.
286

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

292
  Returns: one of the return values from the choices list; if input is
293
  not possible (i.e. not running with a tty, we return the last entry
294
  from the list
295

296
  """
297
  if choices is None:
298
    choices = [('y', True, 'Perform the operation'),
299
               ('n', False, 'Do not perform the operation')]
300
  if not choices or not isinstance(choices, list):
301
    raise errors.ProgrammerError("Invalid choiches argument to AskUser")
302
  for entry in choices:
303
    if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
304
      raise errors.ProgrammerError("Invalid choiches element to AskUser")
305

    
306
  answer = choices[-1][1]
307
  new_text = []
308
  for line in text.splitlines():
309
    new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
310
  text = "\n".join(new_text)
311
  try:
312
    f = file("/dev/tty", "r+")
313
  except IOError:
314
    return answer
315
  try:
316
    chars = [entry[0] for entry in choices]
317
    chars[-1] = "[%s]" % chars[-1]
318
    chars.append('?')
319
    maps = dict([(entry[0], entry[1]) for entry in choices])
320
    while True:
321
      f.write(text)
322
      f.write('\n')
323
      f.write("/".join(chars))
324
      f.write(": ")
325
      line = f.readline(2).strip().lower()
326
      if line in maps:
327
        answer = maps[line]
328
        break
329
      elif line == '?':
330
        for entry in choices:
331
          f.write(" %s - %s\n" % (entry[0], entry[2]))
332
        f.write("\n")
333
        continue
334
  finally:
335
    f.close()
336
  return answer
337

    
338

    
339
def SubmitOpCode(op, proc=None, feedback_fn=None):
340
  """Function to submit an opcode.
341

342
  This is just a simple wrapper over the construction of the processor
343
  instance. It should be extended to better handle feedback and
344
  interaction functions.
345

346
  """
347
  if proc is None:
348
    proc = mcpu.Processor()
349
  if feedback_fn is None:
350
    feedback_fn = logger.ToStdout
351
  return proc.ExecOpCode(op, feedback_fn)
352

    
353

    
354
def GenericMain(commands, override=None):
355
  """Generic main function for all the gnt-* commands.
356

357
  Arguments:
358
    - commands: a dictionary with a special structure, see the design doc
359
                for command line handling.
360
    - override: if not None, we expect a dictionary with keys that will
361
                override command line options; this can be used to pass
362
                options from the scripts to generic functions
363

364
  """
365
  # save the program name and the entire command line for later logging
366
  if sys.argv:
367
    binary = os.path.basename(sys.argv[0]) or sys.argv[0]
368
    if len(sys.argv) >= 2:
369
      binary += " " + sys.argv[1]
370
      old_cmdline = " ".join(sys.argv[2:])
371
    else:
372
      old_cmdline = ""
373
  else:
374
    binary = "<unknown program>"
375
    old_cmdline = ""
376

    
377
  func, options, args = _ParseArgs(sys.argv, commands)
378
  if func is None: # parse error
379
    return 1
380

    
381
  if override is not None:
382
    for key, val in override.iteritems():
383
      setattr(options, key, val)
384

    
385
  logger.SetupLogging(debug=options.debug, program=binary)
386

    
387
  try:
388
    utils.Lock('cmd', max_retries=options.lock_retries, debug=options.debug)
389
  except errors.LockError, err:
390
    logger.ToStderr(str(err))
391
    return 1
392

    
393
  if old_cmdline:
394
    logger.Info("run with arguments '%s'" % old_cmdline)
395
  else:
396
    logger.Info("run with no arguments")
397

    
398
  try:
399
    try:
400
      result = func(options, args)
401
    except errors.ConfigurationError, err:
402
      logger.Error("Corrupt configuration file: %s" % err)
403
      logger.ToStderr("Aborting.")
404
      result = 2
405
    except errors.HooksAbort, err:
406
      logger.ToStderr("Failure: hooks execution failed:")
407
      for node, script, out in err.args[0]:
408
        if out:
409
          logger.ToStderr("  node: %s, script: %s, output: %s" %
410
                          (node, script, out))
411
        else:
412
          logger.ToStderr("  node: %s, script: %s (no output)" %
413
                          (node, script))
414
      result = 1
415
    except errors.HooksFailure, err:
416
      logger.ToStderr("Failure: hooks general failure: %s" % str(err))
417
      result = 1
418
    except errors.ResolverError, err:
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
      logger.ToStderr(msg % err.args[0])
425
      result = 1
426
    except errors.OpPrereqError, err:
427
      logger.ToStderr("Failure: prerequisites not met for this"
428
                      " operation:\n%s" % str(err))
429
      result = 1
430
    except errors.OpExecError, err:
431
      logger.ToStderr("Failure: command execution error:\n%s" % str(err))
432
      result = 1
433
    except errors.TagError, err:
434
      logger.ToStderr("Failure: invalid tag(s) given:\n%s" % str(err))
435
      result = 1
436
  finally:
437
    utils.Unlock('cmd')
438
    utils.LockCleanup()
439

    
440
  return result
441

    
442

    
443
def GenerateTable(headers, fields, separator, data,
444
                  numfields=None, unitfields=None):
445
  """Prints a table with headers and different fields.
446

447
  Args:
448
    headers: Dict of header titles or None if no headers should be shown
449
    fields: List of fields to show
450
    separator: String used to separate fields or None for spaces
451
    data: Data to be printed
452
    numfields: List of fields to be aligned to right
453
    unitfields: List of fields to be formatted as units
454

455
  """
456
  if numfields is None:
457
    numfields = []
458
  if unitfields is None:
459
    unitfields = []
460

    
461
  format_fields = []
462
  for field in fields:
463
    if separator is not None:
464
      format_fields.append("%s")
465
    elif field in numfields:
466
      format_fields.append("%*s")
467
    else:
468
      format_fields.append("%-*s")
469

    
470
  if separator is None:
471
    mlens = [0 for name in fields]
472
    format = ' '.join(format_fields)
473
  else:
474
    format = separator.replace("%", "%%").join(format_fields)
475

    
476
  for row in data:
477
    for idx, val in enumerate(row):
478
      if fields[idx] in unitfields:
479
        try:
480
          val = int(val)
481
        except ValueError:
482
          pass
483
        else:
484
          val = row[idx] = utils.FormatUnit(val)
485
      if separator is None:
486
        mlens[idx] = max(mlens[idx], len(val))
487

    
488
  result = []
489
  if headers:
490
    args = []
491
    for idx, name in enumerate(fields):
492
      hdr = headers[name]
493
      if separator is None:
494
        mlens[idx] = max(mlens[idx], len(hdr))
495
        args.append(mlens[idx])
496
      args.append(hdr)
497
    result.append(format % tuple(args))
498

    
499
  for line in data:
500
    args = []
501
    for idx in xrange(len(fields)):
502
      if separator is None:
503
        args.append(mlens[idx])
504
      args.append(line[idx])
505
    result.append(format % tuple(args))
506

    
507
  return result