Statistics
| Branch: | Tag: | Revision:

root / lib / cli.py @ 846baef9

History | View | Annotate | Download (14.2 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",
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 ListTags(opts, args):
70
  """List the tags on a given object.
71

72
  This is a generic implementation that knows how to deal with all
73
  three cases of tag objects (cluster, node, instance). The opts
74
  argument is expected to contain a tag_type field denoting what
75
  object type we work on.
76

77
  """
78
  kind, name = _ExtractTagsObject(opts, args)
79
  op = opcodes.OpGetTags(kind=kind, name=name)
80
  result = SubmitOpCode(op)
81
  result = list(result)
82
  result.sort()
83
  for tag in result:
84
    print tag
85

    
86

    
87
def AddTags(opts, args):
88
  """Add tags on a given object.
89

90
  This is a generic implementation that knows how to deal with all
91
  three cases of tag objects (cluster, node, instance). The opts
92
  argument is expected to contain a tag_type field denoting what
93
  object type we work on.
94

95
  """
96
  kind, name = _ExtractTagsObject(opts, args)
97
  if not args:
98
    raise errors.OpPrereqError("No tags to be added")
99
  op = opcodes.OpAddTags(kind=kind, name=name, tags=args)
100
  SubmitOpCode(op)
101

    
102

    
103
def RemoveTags(opts, args):
104
  """Remove tags from 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
  if not args:
114
    raise errors.OpPrereqError("No tags to be removed")
115
  op = opcodes.OpDelTags(kind=kind, name=name, tags=args)
116
  SubmitOpCode(op)
117

    
118

    
119
DEBUG_OPT = make_option("-d", "--debug", default=False,
120
                        action="store_true",
121
                        help="Turn debugging on")
122

    
123
NOHDR_OPT = make_option("--no-headers", default=False,
124
                        action="store_true", dest="no_headers",
125
                        help="Don't display column headers")
126

    
127
SEP_OPT = make_option("--separator", default=None,
128
                      action="store", dest="separator",
129
                      help="Separator between output fields"
130
                      " (defaults to one space)")
131

    
132
USEUNITS_OPT = make_option("--human-readable", default=False,
133
                           action="store_true", dest="human_readable",
134
                           help="Print sizes in human readable format")
135

    
136
FIELDS_OPT = make_option("-o", "--output", dest="output", action="store",
137
                         type="string", help="Select output fields",
138
                         metavar="FIELDS")
139

    
140
FORCE_OPT = make_option("-f", "--force", dest="force", action="store_true",
141
                        default=False, help="Force the operation")
142

    
143
_LOCK_OPT = make_option("--lock-retries", default=None,
144
                        type="int", help=SUPPRESS_HELP)
145

    
146
def ARGS_FIXED(val):
147
  """Macro-like function denoting a fixed number of arguments"""
148
  return -val
149

    
150

    
151
def ARGS_ATLEAST(val):
152
  """Macro-like function denoting a minimum number of arguments"""
153
  return val
154

    
155

    
156
ARGS_NONE = None
157
ARGS_ONE = ARGS_FIXED(1)
158
ARGS_ANY = ARGS_ATLEAST(0)
159

    
160

    
161
def check_unit(option, opt, value):
162
  try:
163
    return utils.ParseUnit(value)
164
  except errors.UnitParseError, err:
165
    raise OptionValueError("option %s: %s" % (opt, err))
166

    
167

    
168
class CliOption(Option):
169
  TYPES = Option.TYPES + ("unit",)
170
  TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER)
171
  TYPE_CHECKER["unit"] = check_unit
172

    
173

    
174
# optparse.py sets make_option, so we do it for our own option class, too
175
cli_option = CliOption
176

    
177

    
178
def _ParseArgs(argv, commands):
179
  """Parses the command line and return the function which must be
180
  executed together with its arguments
181

182
  Arguments:
183
    argv: the command line
184

185
    commands: dictionary with special contents, see the design doc for
186
    cmdline handling
187

188
  """
189
  if len(argv) == 0:
190
    binary = "<command>"
191
  else:
192
    binary = argv[0].split("/")[-1]
193

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

    
200
  if len(argv) < 2 or argv[1] not in commands.keys():
201
    # let's do a nice thing
202
    sortedcmds = commands.keys()
203
    sortedcmds.sort()
204
    print ("Usage: %(bin)s {command} [options...] [argument...]"
205
           "\n%(bin)s <command> --help to see details, or"
206
           " man %(bin)s\n" % {"bin": binary})
207
    # compute the max line length for cmd + usage
208
    mlen = max([len(" %s %s" % (cmd, commands[cmd][3])) for cmd in commands])
209
    mlen = min(60, mlen) # should not get here...
210
    # and format a nice command list
211
    print "Commands:"
212
    for cmd in sortedcmds:
213
      cmdstr = " %s %s" % (cmd, commands[cmd][3])
214
      help_text = commands[cmd][4]
215
      help_lines = textwrap.wrap(help_text, 79-3-mlen)
216
      print "%-*s - %s" % (mlen, cmdstr,
217
                                          help_lines.pop(0))
218
      for line in help_lines:
219
        print "%-*s   %s" % (mlen, "", line)
220
    print
221
    return None, None, None
222
  cmd = argv.pop(1)
223
  func, nargs, parser_opts, usage, description = commands[cmd]
224
  parser_opts.append(_LOCK_OPT)
225
  parser = OptionParser(option_list=parser_opts,
226
                        description=description,
227
                        formatter=TitledHelpFormatter(),
228
                        usage="%%prog %s %s" % (cmd, usage))
229
  parser.disable_interspersed_args()
230
  options, args = parser.parse_args()
231
  if nargs is None:
232
    if len(args) != 0:
233
      print >> sys.stderr, ("Error: Command %s expects no arguments" % cmd)
234
      return None, None, None
235
  elif nargs < 0 and len(args) != -nargs:
236
    print >> sys.stderr, ("Error: Command %s expects %d argument(s)" %
237
                         (cmd, -nargs))
238
    return None, None, None
239
  elif nargs >= 0 and len(args) < nargs:
240
    print >> sys.stderr, ("Error: Command %s expects at least %d argument(s)" %
241
                         (cmd, nargs))
242
    return None, None, None
243

    
244
  return func, options, args
245

    
246

    
247
def AskUser(text, choices=None):
248
  """Ask the user a question.
249

250
  Args:
251
    text - the question to ask.
252

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

258
  Returns: one of the return values from the choices list; if input is
259
  not possible (i.e. not running with a tty, we return the last entry
260
  from the list
261

262
  """
263
  if choices is None:
264
    choices = [('y', True, 'Perform the operation'),
265
               ('n', False, 'Do not perform the operation')]
266
  if not choices or not isinstance(choices, list):
267
    raise errors.ProgrammerError("Invalid choiches argument to AskUser")
268
  for entry in choices:
269
    if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
270
      raise errors.ProgrammerError("Invalid choiches element to AskUser")
271

    
272
  answer = choices[-1][1]
273
  new_text = []
274
  for line in text.splitlines():
275
    new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
276
  text = "\n".join(new_text)
277
  try:
278
    f = file("/dev/tty", "r+")
279
  except IOError:
280
    return answer
281
  try:
282
    chars = [entry[0] for entry in choices]
283
    chars[-1] = "[%s]" % chars[-1]
284
    chars.append('?')
285
    maps = dict([(entry[0], entry[1]) for entry in choices])
286
    while True:
287
      f.write(text)
288
      f.write('\n')
289
      f.write("/".join(chars))
290
      f.write(": ")
291
      line = f.readline(2).strip().lower()
292
      if line in maps:
293
        answer = maps[line]
294
        break
295
      elif line == '?':
296
        for entry in choices:
297
          f.write(" %s - %s\n" % (entry[0], entry[2]))
298
        f.write("\n")
299
        continue
300
  finally:
301
    f.close()
302
  return answer
303

    
304

    
305
def SubmitOpCode(op):
306
  """Function to submit an opcode.
307

308
  This is just a simple wrapper over the construction of the processor
309
  instance. It should be extended to better handle feedback and
310
  interaction functions.
311

312
  """
313
  proc = mcpu.Processor()
314
  return proc.ExecOpCode(op, logger.ToStdout)
315

    
316

    
317
def GenericMain(commands, override=None):
318
  """Generic main function for all the gnt-* commands.
319

320
  Arguments:
321
    - commands: a dictionary with a special structure, see the design doc
322
                for command line handling.
323
    - override: if not None, we expect a dictionary with keys that will
324
                override command line options; this can be used to pass
325
                options from the scripts to generic functions
326

327
  """
328
  # save the program name and the entire command line for later logging
329
  if sys.argv:
330
    binary = os.path.basename(sys.argv[0]) or sys.argv[0]
331
    if len(sys.argv) >= 2:
332
      binary += " " + sys.argv[1]
333
      old_cmdline = " ".join(sys.argv[2:])
334
    else:
335
      old_cmdline = ""
336
  else:
337
    binary = "<unknown program>"
338
    old_cmdline = ""
339

    
340
  func, options, args = _ParseArgs(sys.argv, commands)
341
  if func is None: # parse error
342
    return 1
343

    
344
  if override is not None:
345
    for key, val in override.iteritems():
346
      setattr(options, key, val)
347

    
348
  logger.SetupLogging(debug=options.debug, program=binary)
349

    
350
  try:
351
    utils.Lock('cmd', max_retries=options.lock_retries, debug=options.debug)
352
  except errors.LockError, err:
353
    logger.ToStderr(str(err))
354
    return 1
355

    
356
  if old_cmdline:
357
    logger.Info("run with arguments '%s'" % old_cmdline)
358
  else:
359
    logger.Info("run with no arguments")
360

    
361
  try:
362
    try:
363
      result = func(options, args)
364
    except errors.ConfigurationError, err:
365
      logger.Error("Corrupt configuration file: %s" % err)
366
      logger.ToStderr("Aborting.")
367
      result = 2
368
    except errors.HooksAbort, err:
369
      logger.ToStderr("Failure: hooks execution failed:")
370
      for node, script, out in err.args[0]:
371
        if out:
372
          logger.ToStderr("  node: %s, script: %s, output: %s" %
373
                          (node, script, out))
374
        else:
375
          logger.ToStderr("  node: %s, script: %s (no output)" %
376
                          (node, script))
377
      result = 1
378
    except errors.HooksFailure, err:
379
      logger.ToStderr("Failure: hooks general failure: %s" % str(err))
380
      result = 1
381
    except errors.ResolverError, err:
382
      this_host = utils.HostInfo.SysName()
383
      if err.args[0] == this_host:
384
        msg = "Failure: can't resolve my own hostname ('%s')"
385
      else:
386
        msg = "Failure: can't resolve hostname '%s'"
387
      logger.ToStderr(msg % err.args[0])
388
      result = 1
389
    except errors.OpPrereqError, err:
390
      logger.ToStderr("Failure: prerequisites not met for this"
391
                      " operation:\n%s" % str(err))
392
      result = 1
393
    except errors.OpExecError, err:
394
      logger.ToStderr("Failure: command execution error:\n%s" % str(err))
395
      result = 1
396
  finally:
397
    utils.Unlock('cmd')
398
    utils.LockCleanup()
399

    
400
  return result
401

    
402

    
403
def GenerateTable(headers, fields, separator, data,
404
                  numfields=None, unitfields=None):
405
  """Prints a table with headers and different fields.
406

407
  Args:
408
    headers: Dict of header titles or None if no headers should be shown
409
    fields: List of fields to show
410
    separator: String used to separate fields or None for spaces
411
    data: Data to be printed
412
    numfields: List of fields to be aligned to right
413
    unitfields: List of fields to be formatted as units
414

415
  """
416
  if numfields is None:
417
    numfields = []
418
  if unitfields is None:
419
    unitfields = []
420

    
421
  format_fields = []
422
  for field in fields:
423
    if separator is not None:
424
      format_fields.append("%s")
425
    elif field in numfields:
426
      format_fields.append("%*s")
427
    else:
428
      format_fields.append("%-*s")
429

    
430
  if separator is None:
431
    mlens = [0 for name in fields]
432
    format = ' '.join(format_fields)
433
  else:
434
    format = separator.replace("%", "%%").join(format_fields)
435

    
436
  for row in data:
437
    for idx, val in enumerate(row):
438
      if fields[idx] in unitfields:
439
        try:
440
          val = int(val)
441
        except ValueError:
442
          pass
443
        else:
444
          val = row[idx] = utils.FormatUnit(val)
445
      if separator is None:
446
        mlens[idx] = max(mlens[idx], len(val))
447

    
448
  result = []
449
  if headers:
450
    args = []
451
    for idx, name in enumerate(fields):
452
      hdr = headers[name]
453
      if separator is None:
454
        mlens[idx] = max(mlens[idx], len(hdr))
455
        args.append(mlens[idx])
456
      args.append(hdr)
457
    result.append(format % tuple(args))
458

    
459
  for line in data:
460
    args = []
461
    for idx in xrange(len(fields)):
462
      if separator is None:
463
        args.append(mlens[idx])
464
      args.append(line[idx])
465
    result.append(format % tuple(args))
466

    
467
  return result