Statistics
| Branch: | Tag: | Revision:

root / lib / cli.py @ 810c50b7

History | View | Annotate | Download (15.1 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):
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
  proc = mcpu.Processor()
348
  return proc.ExecOpCode(op, logger.ToStdout)
349

    
350

    
351
def GenericMain(commands, override=None):
352
  """Generic main function for all the gnt-* commands.
353

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

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

    
374
  func, options, args = _ParseArgs(sys.argv, commands)
375
  if func is None: # parse error
376
    return 1
377

    
378
  if override is not None:
379
    for key, val in override.iteritems():
380
      setattr(options, key, val)
381

    
382
  logger.SetupLogging(debug=options.debug, program=binary)
383

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

    
390
  if old_cmdline:
391
    logger.Info("run with arguments '%s'" % old_cmdline)
392
  else:
393
    logger.Info("run with no arguments")
394

    
395
  try:
396
    try:
397
      result = func(options, args)
398
    except errors.ConfigurationError, err:
399
      logger.Error("Corrupt configuration file: %s" % err)
400
      logger.ToStderr("Aborting.")
401
      result = 2
402
    except errors.HooksAbort, err:
403
      logger.ToStderr("Failure: hooks execution failed:")
404
      for node, script, out in err.args[0]:
405
        if out:
406
          logger.ToStderr("  node: %s, script: %s, output: %s" %
407
                          (node, script, out))
408
        else:
409
          logger.ToStderr("  node: %s, script: %s (no output)" %
410
                          (node, script))
411
      result = 1
412
    except errors.HooksFailure, err:
413
      logger.ToStderr("Failure: hooks general failure: %s" % str(err))
414
      result = 1
415
    except errors.ResolverError, err:
416
      this_host = utils.HostInfo.SysName()
417
      if err.args[0] == this_host:
418
        msg = "Failure: can't resolve my own hostname ('%s')"
419
      else:
420
        msg = "Failure: can't resolve hostname '%s'"
421
      logger.ToStderr(msg % err.args[0])
422
      result = 1
423
    except errors.OpPrereqError, err:
424
      logger.ToStderr("Failure: prerequisites not met for this"
425
                      " operation:\n%s" % str(err))
426
      result = 1
427
    except errors.OpExecError, err:
428
      logger.ToStderr("Failure: command execution error:\n%s" % str(err))
429
      result = 1
430
  finally:
431
    utils.Unlock('cmd')
432
    utils.LockCleanup()
433

    
434
  return result
435

    
436

    
437
def GenerateTable(headers, fields, separator, data,
438
                  numfields=None, unitfields=None):
439
  """Prints a table with headers and different fields.
440

441
  Args:
442
    headers: Dict of header titles or None if no headers should be shown
443
    fields: List of fields to show
444
    separator: String used to separate fields or None for spaces
445
    data: Data to be printed
446
    numfields: List of fields to be aligned to right
447
    unitfields: List of fields to be formatted as units
448

449
  """
450
  if numfields is None:
451
    numfields = []
452
  if unitfields is None:
453
    unitfields = []
454

    
455
  format_fields = []
456
  for field in fields:
457
    if separator is not None:
458
      format_fields.append("%s")
459
    elif field in numfields:
460
      format_fields.append("%*s")
461
    else:
462
      format_fields.append("%-*s")
463

    
464
  if separator is None:
465
    mlens = [0 for name in fields]
466
    format = ' '.join(format_fields)
467
  else:
468
    format = separator.replace("%", "%%").join(format_fields)
469

    
470
  for row in data:
471
    for idx, val in enumerate(row):
472
      if fields[idx] in unitfields:
473
        try:
474
          val = int(val)
475
        except ValueError:
476
          pass
477
        else:
478
          val = row[idx] = utils.FormatUnit(val)
479
      if separator is None:
480
        mlens[idx] = max(mlens[idx], len(val))
481

    
482
  result = []
483
  if headers:
484
    args = []
485
    for idx, name in enumerate(fields):
486
      hdr = headers[name]
487
      if separator is None:
488
        mlens[idx] = max(mlens[idx], len(hdr))
489
        args.append(mlens[idx])
490
      args.append(hdr)
491
    result.append(format % tuple(args))
492

    
493
  for line in data:
494
    args = []
495
    for idx in xrange(len(fields)):
496
      if separator is None:
497
        args.append(mlens[idx])
498
      args.append(line[idx])
499
    result.append(format % tuple(args))
500

    
501
  return result