Statistics
| Branch: | Tag: | Revision:

root / lib / cli.py @ 686d7433

History | View | Annotate | Download (25.3 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", "ikv_option", "keyval_option",
46
           "GenerateTable", "AskUser",
47
           "ARGS_NONE", "ARGS_FIXED", "ARGS_ATLEAST", "ARGS_ANY", "ARGS_ONE",
48
           "USEUNITS_OPT", "FIELDS_OPT", "FORCE_OPT", "SUBMIT_OPT",
49
           "ListTags", "AddTags", "RemoveTags", "TAG_SRC_OPT",
50
           "FormatError", "SplitNodeOption", "SubmitOrSend",
51
           "JobSubmittedException", "FormatTimestamp", "ParseTimespec",
52
           "ValidateBeParams",
53
           ]
54

    
55

    
56
def _ExtractTagsObject(opts, args):
57
  """Extract the tag type object.
58

59
  Note that this function will modify its args parameter.
60

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

    
76

    
77
def _ExtendTags(opts, args):
78
  """Extend the args if a source file has been given.
79

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

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

    
105

    
106
def ListTags(opts, args):
107
  """List the tags on a given object.
108

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

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

    
123

    
124
def AddTags(opts, args):
125
  """Add tags on a given object.
126

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

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

    
140

    
141
def RemoveTags(opts, args):
142
  """Remove tags from a given object.
143

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

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

    
157

    
158
DEBUG_OPT = make_option("-d", "--debug", default=False,
159
                        action="store_true",
160
                        help="Turn debugging on")
161

    
162
NOHDR_OPT = make_option("--no-headers", default=False,
163
                        action="store_true", dest="no_headers",
164
                        help="Don't display column headers")
165

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

    
171
USEUNITS_OPT = make_option("--human-readable", default=False,
172
                           action="store_true", dest="human_readable",
173
                           help="Print sizes in human readable format")
174

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

    
180
FORCE_OPT = make_option("-f", "--force", dest="force", action="store_true",
181
                        default=False, help="Force the operation")
182

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

    
186
SUBMIT_OPT = make_option("--submit", dest="submit_only",
187
                         default=False, action="store_true",
188
                         help="Submit the job and return the job ID, but"
189
                         " don't wait for the job to finish")
190

    
191

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

    
196

    
197
def ARGS_ATLEAST(val):
198
  """Macro-like function denoting a minimum number of arguments"""
199
  return val
200

    
201

    
202
ARGS_NONE = None
203
ARGS_ONE = ARGS_FIXED(1)
204
ARGS_ANY = ARGS_ATLEAST(0)
205

    
206

    
207
def check_unit(option, opt, value):
208
  """OptParsers custom converter for units.
209

210
  """
211
  try:
212
    return utils.ParseUnit(value)
213
  except errors.UnitParseError, err:
214
    raise OptionValueError("option %s: %s" % (opt, err))
215

    
216

    
217
class CliOption(Option):
218
  """Custom option class for optparse.
219

220
  """
221
  TYPES = Option.TYPES + ("unit",)
222
  TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER)
223
  TYPE_CHECKER["unit"] = check_unit
224

    
225

    
226
def _SplitKeyVal(opt, data):
227
  """Convert a KeyVal string into a dict.
228

229
  This function will convert a key=val[,...] string into a dict. Empty
230
  values will be converted specially: keys which have the prefix 'no_'
231
  will have the value=False and the prefix stripped, the others will
232
  have value=True.
233

234
  @type opt: string
235
  @param opt: a string holding the option name for which we process the
236
      data, used in building error messages
237
  @type data: string
238
  @param data: a string of the format key=val,key=val,...
239
  @rtype: dict
240
  @return: {key=val, key=val}
241
  @raises errors.ParameterError: if there are duplicate keys
242

243
  """
244
  NO_PREFIX = "no_"
245
  UN_PREFIX = "-"
246
  kv_dict = {}
247
  for elem in data.split(","):
248
    if "=" in elem:
249
      key, val = elem.split("=", 1)
250
    else:
251
      if elem.startswith(NO_PREFIX):
252
        key, val = elem[len(NO_PREFIX):], False
253
      elif elem.startswith(UN_PREFIX):
254
        key, val = elem[len(UN_PREFIX):], None
255
      else:
256
        key, val = elem, True
257
    if key in kv_dict:
258
      raise errors.ParameterError("Duplicate key '%s' in option %s" %
259
                                  (key, opt))
260
    kv_dict[key] = val
261
  return kv_dict
262

    
263

    
264
def check_ident_key_val(option, opt, value):
265
  """Custom parser for the IdentKeyVal option type.
266

267
  """
268
  if ":" not in value:
269
    retval =  (value, {})
270
  else:
271
    ident, rest = value.split(":", 1)
272
    kv_dict = _SplitKeyVal(opt, rest)
273
    retval = (ident, kv_dict)
274
  return retval
275

    
276

    
277
class IdentKeyValOption(Option):
278
  """Custom option class for ident:key=val,key=val options.
279

280
  This will store the parsed values as a tuple (ident, {key: val}). As
281
  such, multiple uses of this option via action=append is possible.
282

283
  """
284
  TYPES = Option.TYPES + ("identkeyval",)
285
  TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER)
286
  TYPE_CHECKER["identkeyval"] = check_ident_key_val
287

    
288

    
289
def check_key_val(option, opt, value):
290
  """Custom parser for the KeyVal option type.
291

292
  """
293
  return _SplitKeyVal(opt, value)
294

    
295

    
296
class KeyValOption(Option):
297
  """Custom option class for key=val,key=val options.
298

299
  This will store the parsed values as a dict {key: val}.
300

301
  """
302
  TYPES = Option.TYPES + ("keyval",)
303
  TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER)
304
  TYPE_CHECKER["keyval"] = check_key_val
305

    
306

    
307
# optparse.py sets make_option, so we do it for our own option class, too
308
cli_option = CliOption
309
ikv_option = IdentKeyValOption
310
keyval_option = KeyValOption
311

    
312

    
313
def _ParseArgs(argv, commands, aliases):
314
  """Parses the command line and return the function which must be
315
  executed together with its arguments
316

317
  Arguments:
318
    argv: the command line
319

320
    commands: dictionary with special contents, see the design doc for
321
    cmdline handling
322
    aliases: dictionary with command aliases {'alias': 'target, ...}
323

324
  """
325
  if len(argv) == 0:
326
    binary = "<command>"
327
  else:
328
    binary = argv[0].split("/")[-1]
329

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

    
336
  if len(argv) < 2 or not (argv[1] in commands or
337
                           argv[1] in aliases):
338
    # let's do a nice thing
339
    sortedcmds = commands.keys()
340
    sortedcmds.sort()
341
    print ("Usage: %(bin)s {command} [options...] [argument...]"
342
           "\n%(bin)s <command> --help to see details, or"
343
           " man %(bin)s\n" % {"bin": binary})
344
    # compute the max line length for cmd + usage
345
    mlen = max([len(" %s" % cmd) for cmd in commands])
346
    mlen = min(60, mlen) # should not get here...
347
    # and format a nice command list
348
    print "Commands:"
349
    for cmd in sortedcmds:
350
      cmdstr = " %s" % (cmd,)
351
      help_text = commands[cmd][4]
352
      help_lines = textwrap.wrap(help_text, 79-3-mlen)
353
      print "%-*s - %s" % (mlen, cmdstr, help_lines.pop(0))
354
      for line in help_lines:
355
        print "%-*s   %s" % (mlen, "", line)
356
    print
357
    return None, None, None
358

    
359
  # get command, unalias it, and look it up in commands
360
  cmd = argv.pop(1)
361
  if cmd in aliases:
362
    if cmd in commands:
363
      raise errors.ProgrammerError("Alias '%s' overrides an existing"
364
                                   " command" % cmd)
365

    
366
    if aliases[cmd] not in commands:
367
      raise errors.ProgrammerError("Alias '%s' maps to non-existing"
368
                                   " command '%s'" % (cmd, aliases[cmd]))
369

    
370
    cmd = aliases[cmd]
371

    
372
  func, nargs, parser_opts, usage, description = commands[cmd]
373
  parser = OptionParser(option_list=parser_opts,
374
                        description=description,
375
                        formatter=TitledHelpFormatter(),
376
                        usage="%%prog %s %s" % (cmd, usage))
377
  parser.disable_interspersed_args()
378
  options, args = parser.parse_args()
379
  if nargs is None:
380
    if len(args) != 0:
381
      print >> sys.stderr, ("Error: Command %s expects no arguments" % cmd)
382
      return None, None, None
383
  elif nargs < 0 and len(args) != -nargs:
384
    print >> sys.stderr, ("Error: Command %s expects %d argument(s)" %
385
                         (cmd, -nargs))
386
    return None, None, None
387
  elif nargs >= 0 and len(args) < nargs:
388
    print >> sys.stderr, ("Error: Command %s expects at least %d argument(s)" %
389
                         (cmd, nargs))
390
    return None, None, None
391

    
392
  return func, options, args
393

    
394

    
395
def SplitNodeOption(value):
396
  """Splits the value of a --node option.
397

398
  """
399
  if value and ':' in value:
400
    return value.split(':', 1)
401
  else:
402
    return (value, None)
403

    
404

    
405
def ValidateBeParams(bep):
406
  """Parse and check the given beparams.
407

408
  The function will update in-place the given dictionary.
409

410
  @type bep: dict
411
  @param bep: input beparams
412
  @raise errors.ParameterError: if the input values are not OK
413
  @raise errors.UnitParseError: if the input values are not OK
414

415
  """
416
  if constants.BE_MEMORY in bep:
417
    bep[constants.BE_MEMORY] = utils.ParseUnit(bep[constants.BE_MEMORY])
418

    
419
  if constants.BE_VCPUS in bep:
420
    try:
421
      bep[constants.BE_VCPUS] = int(bep[constants.BE_VCPUS])
422
    except ValueError:
423
      raise errors.ParameterError("Invalid number of VCPUs")
424

    
425

    
426
def AskUser(text, choices=None):
427
  """Ask the user a question.
428

429
  Args:
430
    text - the question to ask.
431

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

437
  Returns: one of the return values from the choices list; if input is
438
  not possible (i.e. not running with a tty, we return the last entry
439
  from the list
440

441
  """
442
  if choices is None:
443
    choices = [('y', True, 'Perform the operation'),
444
               ('n', False, 'Do not perform the operation')]
445
  if not choices or not isinstance(choices, list):
446
    raise errors.ProgrammerError("Invalid choiches argument to AskUser")
447
  for entry in choices:
448
    if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
449
      raise errors.ProgrammerError("Invalid choiches element to AskUser")
450

    
451
  answer = choices[-1][1]
452
  new_text = []
453
  for line in text.splitlines():
454
    new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
455
  text = "\n".join(new_text)
456
  try:
457
    f = file("/dev/tty", "a+")
458
  except IOError:
459
    return answer
460
  try:
461
    chars = [entry[0] for entry in choices]
462
    chars[-1] = "[%s]" % chars[-1]
463
    chars.append('?')
464
    maps = dict([(entry[0], entry[1]) for entry in choices])
465
    while True:
466
      f.write(text)
467
      f.write('\n')
468
      f.write("/".join(chars))
469
      f.write(": ")
470
      line = f.readline(2).strip().lower()
471
      if line in maps:
472
        answer = maps[line]
473
        break
474
      elif line == '?':
475
        for entry in choices:
476
          f.write(" %s - %s\n" % (entry[0], entry[2]))
477
        f.write("\n")
478
        continue
479
  finally:
480
    f.close()
481
  return answer
482

    
483

    
484
class JobSubmittedException(Exception):
485
  """Job was submitted, client should exit.
486

487
  This exception has one argument, the ID of the job that was
488
  submitted. The handler should print this ID.
489

490
  This is not an error, just a structured way to exit from clients.
491

492
  """
493

    
494

    
495
def SendJob(ops, cl=None):
496
  """Function to submit an opcode without waiting for the results.
497

498
  @type ops: list
499
  @param ops: list of opcodes
500
  @type cl: luxi.Client
501
  @param cl: the luxi client to use for communicating with the master;
502
             if None, a new client will be created
503

504
  """
505
  if cl is None:
506
    cl = GetClient()
507

    
508
  job_id = cl.SubmitJob(ops)
509

    
510
  return job_id
511

    
512

    
513
def PollJob(job_id, cl=None, feedback_fn=None):
514
  """Function to poll for the result of a job.
515

516
  @type job_id: job identified
517
  @param job_id: the job to poll for results
518
  @type cl: luxi.Client
519
  @param cl: the luxi client to use for communicating with the master;
520
             if None, a new client will be created
521

522
  """
523
  if cl is None:
524
    cl = GetClient()
525

    
526
  prev_job_info = None
527
  prev_logmsg_serial = None
528

    
529
  while True:
530
    result = cl.WaitForJobChange(job_id, ["status"], prev_job_info,
531
                                 prev_logmsg_serial)
532
    if not result:
533
      # job not found, go away!
534
      raise errors.JobLost("Job with id %s lost" % job_id)
535

    
536
    # Split result, a tuple of (field values, log entries)
537
    (job_info, log_entries) = result
538
    (status, ) = job_info
539

    
540
    if log_entries:
541
      for log_entry in log_entries:
542
        (serial, timestamp, _, message) = log_entry
543
        if callable(feedback_fn):
544
          feedback_fn(log_entry[1:])
545
        else:
546
          print "%s %s" % (time.ctime(utils.MergeTime(timestamp)), message)
547
        prev_logmsg_serial = max(prev_logmsg_serial, serial)
548

    
549
    # TODO: Handle canceled and archived jobs
550
    elif status in (constants.JOB_STATUS_SUCCESS, constants.JOB_STATUS_ERROR):
551
      break
552

    
553
    prev_job_info = job_info
554

    
555
  jobs = cl.QueryJobs([job_id], ["status", "opresult"])
556
  if not jobs:
557
    raise errors.JobLost("Job with id %s lost" % job_id)
558

    
559
  status, result = jobs[0]
560
  if status == constants.JOB_STATUS_SUCCESS:
561
    return result
562
  else:
563
    raise errors.OpExecError(result)
564

    
565

    
566
def SubmitOpCode(op, cl=None, feedback_fn=None):
567
  """Legacy function to submit an opcode.
568

569
  This is just a simple wrapper over the construction of the processor
570
  instance. It should be extended to better handle feedback and
571
  interaction functions.
572

573
  """
574
  if cl is None:
575
    cl = GetClient()
576

    
577
  job_id = SendJob([op], cl)
578

    
579
  op_results = PollJob(job_id, cl=cl, feedback_fn=feedback_fn)
580

    
581
  return op_results[0]
582

    
583

    
584
def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
585
  """Wrapper around SubmitOpCode or SendJob.
586

587
  This function will decide, based on the 'opts' parameter, whether to
588
  submit and wait for the result of the opcode (and return it), or
589
  whether to just send the job and print its identifier. It is used in
590
  order to simplify the implementation of the '--submit' option.
591

592
  """
593
  if opts and opts.submit_only:
594
    job_id = SendJob([op], cl=cl)
595
    raise JobSubmittedException(job_id)
596
  else:
597
    return SubmitOpCode(op, cl=cl, feedback_fn=feedback_fn)
598

    
599

    
600
def GetClient():
601
  # TODO: Cache object?
602
  try:
603
    client = luxi.Client()
604
  except luxi.NoMasterError:
605
    master, myself = ssconf.GetMasterAndMyself()
606
    if master != myself:
607
      raise errors.OpPrereqError("This is not the master node, please connect"
608
                                 " to node '%s' and rerun the command" %
609
                                 master)
610
    else:
611
      raise
612
  return client
613

    
614

    
615
def FormatError(err):
616
  """Return a formatted error message for a given error.
617

618
  This function takes an exception instance and returns a tuple
619
  consisting of two values: first, the recommended exit code, and
620
  second, a string describing the error message (not
621
  newline-terminated).
622

623
  """
624
  retcode = 1
625
  obuf = StringIO()
626
  msg = str(err)
627
  if isinstance(err, errors.ConfigurationError):
628
    txt = "Corrupt configuration file: %s" % msg
629
    logger.Error(txt)
630
    obuf.write(txt + "\n")
631
    obuf.write("Aborting.")
632
    retcode = 2
633
  elif isinstance(err, errors.HooksAbort):
634
    obuf.write("Failure: hooks execution failed:\n")
635
    for node, script, out in err.args[0]:
636
      if out:
637
        obuf.write("  node: %s, script: %s, output: %s\n" %
638
                   (node, script, out))
639
      else:
640
        obuf.write("  node: %s, script: %s (no output)\n" %
641
                   (node, script))
642
  elif isinstance(err, errors.HooksFailure):
643
    obuf.write("Failure: hooks general failure: %s" % msg)
644
  elif isinstance(err, errors.ResolverError):
645
    this_host = utils.HostInfo.SysName()
646
    if err.args[0] == this_host:
647
      msg = "Failure: can't resolve my own hostname ('%s')"
648
    else:
649
      msg = "Failure: can't resolve hostname '%s'"
650
    obuf.write(msg % err.args[0])
651
  elif isinstance(err, errors.OpPrereqError):
652
    obuf.write("Failure: prerequisites not met for this"
653
               " operation:\n%s" % msg)
654
  elif isinstance(err, errors.OpExecError):
655
    obuf.write("Failure: command execution error:\n%s" % msg)
656
  elif isinstance(err, errors.TagError):
657
    obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
658
  elif isinstance(err, errors.JobQueueDrainError):
659
    obuf.write("Failure: the job queue is marked for drain and doesn't"
660
               " accept new requests\n")
661
  elif isinstance(err, errors.GenericError):
662
    obuf.write("Unhandled Ganeti error: %s" % msg)
663
  elif isinstance(err, luxi.NoMasterError):
664
    obuf.write("Cannot communicate with the master daemon.\nIs it running"
665
               " and listening for connections?")
666
  elif isinstance(err, luxi.TimeoutError):
667
    obuf.write("Timeout while talking to the master daemon. Error:\n"
668
               "%s" % msg)
669
  elif isinstance(err, luxi.ProtocolError):
670
    obuf.write("Unhandled protocol error while talking to the master daemon:\n"
671
               "%s" % msg)
672
  elif isinstance(err, JobSubmittedException):
673
    obuf.write("JobID: %s\n" % err.args[0])
674
    retcode = 0
675
  else:
676
    obuf.write("Unhandled exception: %s" % msg)
677
  return retcode, obuf.getvalue().rstrip('\n')
678

    
679

    
680
def GenericMain(commands, override=None, aliases=None):
681
  """Generic main function for all the gnt-* commands.
682

683
  Arguments:
684
    - commands: a dictionary with a special structure, see the design doc
685
                for command line handling.
686
    - override: if not None, we expect a dictionary with keys that will
687
                override command line options; this can be used to pass
688
                options from the scripts to generic functions
689
    - aliases: dictionary with command aliases {'alias': 'target, ...}
690

691
  """
692
  # save the program name and the entire command line for later logging
693
  if sys.argv:
694
    binary = os.path.basename(sys.argv[0]) or sys.argv[0]
695
    if len(sys.argv) >= 2:
696
      binary += " " + sys.argv[1]
697
      old_cmdline = " ".join(sys.argv[2:])
698
    else:
699
      old_cmdline = ""
700
  else:
701
    binary = "<unknown program>"
702
    old_cmdline = ""
703

    
704
  if aliases is None:
705
    aliases = {}
706

    
707
  func, options, args = _ParseArgs(sys.argv, commands, aliases)
708
  if func is None: # parse error
709
    return 1
710

    
711
  if override is not None:
712
    for key, val in override.iteritems():
713
      setattr(options, key, val)
714

    
715
  logger.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
716
                      stderr_logging=True, program=binary)
717

    
718
  utils.debug = options.debug
719

    
720
  if old_cmdline:
721
    logger.Info("run with arguments '%s'" % old_cmdline)
722
  else:
723
    logger.Info("run with no arguments")
724

    
725
  try:
726
    result = func(options, args)
727
  except (errors.GenericError, luxi.ProtocolError), err:
728
    result, err_msg = FormatError(err)
729
    logger.ToStderr(err_msg)
730

    
731
  return result
732

    
733

    
734
def GenerateTable(headers, fields, separator, data,
735
                  numfields=None, unitfields=None):
736
  """Prints a table with headers and different fields.
737

738
  Args:
739
    headers: Dict of header titles or None if no headers should be shown
740
    fields: List of fields to show
741
    separator: String used to separate fields or None for spaces
742
    data: Data to be printed
743
    numfields: List of fields to be aligned to right
744
    unitfields: List of fields to be formatted as units
745

746
  """
747
  if numfields is None:
748
    numfields = []
749
  if unitfields is None:
750
    unitfields = []
751

    
752
  format_fields = []
753
  for field in fields:
754
    if headers and field not in headers:
755
      raise errors.ProgrammerError("Missing header description for field '%s'"
756
                                   % field)
757
    if separator is not None:
758
      format_fields.append("%s")
759
    elif field in numfields:
760
      format_fields.append("%*s")
761
    else:
762
      format_fields.append("%-*s")
763

    
764
  if separator is None:
765
    mlens = [0 for name in fields]
766
    format = ' '.join(format_fields)
767
  else:
768
    format = separator.replace("%", "%%").join(format_fields)
769

    
770
  for row in data:
771
    for idx, val in enumerate(row):
772
      if fields[idx] in unitfields:
773
        try:
774
          val = int(val)
775
        except ValueError:
776
          pass
777
        else:
778
          val = row[idx] = utils.FormatUnit(val)
779
      val = row[idx] = str(val)
780
      if separator is None:
781
        mlens[idx] = max(mlens[idx], len(val))
782

    
783
  result = []
784
  if headers:
785
    args = []
786
    for idx, name in enumerate(fields):
787
      hdr = headers[name]
788
      if separator is None:
789
        mlens[idx] = max(mlens[idx], len(hdr))
790
        args.append(mlens[idx])
791
      args.append(hdr)
792
    result.append(format % tuple(args))
793

    
794
  for line in data:
795
    args = []
796
    for idx in xrange(len(fields)):
797
      if separator is None:
798
        args.append(mlens[idx])
799
      args.append(line[idx])
800
    result.append(format % tuple(args))
801

    
802
  return result
803

    
804

    
805
def FormatTimestamp(ts):
806
  """Formats a given timestamp.
807

808
  @type ts: timestamp
809
  @param ts: a timeval-type timestamp, a tuple of seconds and microseconds
810

811
  @rtype: string
812
  @returns: a string with the formatted timestamp
813

814
  """
815
  if not isinstance (ts, (tuple, list)) or len(ts) != 2:
816
    return '?'
817
  sec, usec = ts
818
  return time.strftime("%F %T", time.localtime(sec)) + ".%06d" % usec
819

    
820

    
821
def ParseTimespec(value):
822
  """Parse a time specification.
823

824
  The following suffixed will be recognized:
825

826
    - s: seconds
827
    - m: minutes
828
    - h: hours
829
    - d: day
830
    - w: weeks
831

832
  Without any suffix, the value will be taken to be in seconds.
833

834
  """
835
  value = str(value)
836
  if not value:
837
    raise errors.OpPrereqError("Empty time specification passed")
838
  suffix_map = {
839
    's': 1,
840
    'm': 60,
841
    'h': 3600,
842
    'd': 86400,
843
    'w': 604800,
844
    }
845
  if value[-1] not in suffix_map:
846
    try:
847
      value = int(value)
848
    except ValueError:
849
      raise errors.OpPrereqError("Invalid time specification '%s'" % value)
850
  else:
851
    multiplier = suffix_map[value[-1]]
852
    value = value[:-1]
853
    if not value: # no data left after stripping the suffix
854
      raise errors.OpPrereqError("Invalid time specification (only"
855
                                 " suffix passed)")
856
    try:
857
      value = int(value) * multiplier
858
    except ValueError:
859
      raise errors.OpPrereqError("Invalid time specification '%s'" % value)
860
  return value