Statistics
| Branch: | Tag: | Revision:

root / lib / cli.py @ 1a05d855

History | View | Annotate | Download (26.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
import logging
31
from cStringIO import StringIO
32

    
33
from ganeti import utils
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
           "ToStderr", "ToStdout",
54
           ]
55

    
56

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

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

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

    
77

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

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

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

    
106

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

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

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

    
124

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

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

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

    
141

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

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

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

    
158

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

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

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

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

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

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

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

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

    
192

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

    
197

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

    
202

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

    
207

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

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

    
217

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

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

    
226

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

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

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

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

    
264

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

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

    
277

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

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

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

    
289

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

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

    
296

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

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

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

    
307

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

    
313

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

318
  Arguments:
319
    argv: the command line
320

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

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

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

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

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

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

    
371
    cmd = aliases[cmd]
372

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

    
393
  return func, options, args
394

    
395

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

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

    
405

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

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

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

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

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

    
426

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

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

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

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

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

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

    
484

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

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

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

493
  """
494

    
495

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

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

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

    
509
  job_id = cl.SubmitJob(ops)
510

    
511
  return job_id
512

    
513

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

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

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

    
527
  prev_job_info = None
528
  prev_logmsg_serial = None
529

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

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

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

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

    
554
    prev_job_info = job_info
555

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

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

    
566

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

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

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

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

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

    
582
  return op_results[0]
583

    
584

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

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

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

    
600

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

    
615

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

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

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

    
680

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

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

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

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

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

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

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

    
719
  utils.debug = options.debug
720

    
721
  if old_cmdline:
722
    logging.info("run with arguments '%s'", old_cmdline)
723
  else:
724
    logging.info("run with no arguments")
725

    
726
  try:
727
    result = func(options, args)
728
  except (errors.GenericError, luxi.ProtocolError), err:
729
    result, err_msg = FormatError(err)
730
    logging.exception("Error durring command processing")
731
    ToStderr(err_msg)
732

    
733
  return result
734

    
735

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

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

748
  """
749
  if numfields is None:
750
    numfields = []
751
  if unitfields is None:
752
    unitfields = []
753

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

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

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

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

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

    
804
  return result
805

    
806

    
807
def FormatTimestamp(ts):
808
  """Formats a given timestamp.
809

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

813
  @rtype: string
814
  @returns: a string with the formatted timestamp
815

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

    
822

    
823
def ParseTimespec(value):
824
  """Parse a time specification.
825

826
  The following suffixed will be recognized:
827

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

834
  Without any suffix, the value will be taken to be in seconds.
835

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

    
864

    
865
def _ToStream(stream, txt, *args):
866
  """Write a message to a stream, bypassing the logging system
867

868
  @type stream: file object
869
  @param stream: the file to which we should write
870
  @type txt: str
871
  @param txt: the message
872

873
  """
874
  if args:
875
    args = tuple(args)
876
    stream.write(txt % args)
877
  else:
878
    stream.write(txt)
879
  stream.write('\n')
880
  stream.flush()
881

    
882

    
883
def ToStdout(txt, *args):
884
  """Write a message to stdout only, bypassing the logging system
885

886
  This is just a wrapper over _ToStream.
887

888
  @type txt: str
889
  @param txt: the message
890

891
  """
892
  _ToStream(sys.stdout, txt, *args)
893

    
894

    
895
def ToStderr(txt, *args):
896
  """Write a message to stderr only, bypassing the logging system
897

898
  This is just a wrapper over _ToStream.
899

900
  @type txt: str
901
  @param txt: the message
902

903
  """
904
  _ToStream(sys.stderr, txt, *args)