Statistics
| Branch: | Tag: | Revision:

root / lib / cli.py @ a604f165

History | View | Annotate | Download (25.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
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.GenericError):
659
    obuf.write("Unhandled Ganeti error: %s" % msg)
660
  elif isinstance(err, luxi.NoMasterError):
661
    obuf.write("Cannot communicate with the master daemon.\nIs it running"
662
               " and listening for connections?")
663
  elif isinstance(err, luxi.TimeoutError):
664
    obuf.write("Timeout while talking to the master daemon. Error:\n"
665
               "%s" % msg)
666
  elif isinstance(err, luxi.ProtocolError):
667
    obuf.write("Unhandled protocol error while talking to the master daemon:\n"
668
               "%s" % msg)
669
  elif isinstance(err, JobSubmittedException):
670
    obuf.write("JobID: %s\n" % err.args[0])
671
    retcode = 0
672
  else:
673
    obuf.write("Unhandled exception: %s" % msg)
674
  return retcode, obuf.getvalue().rstrip('\n')
675

    
676

    
677
def GenericMain(commands, override=None, aliases=None):
678
  """Generic main function for all the gnt-* commands.
679

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

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

    
701
  if aliases is None:
702
    aliases = {}
703

    
704
  func, options, args = _ParseArgs(sys.argv, commands, aliases)
705
  if func is None: # parse error
706
    return 1
707

    
708
  if override is not None:
709
    for key, val in override.iteritems():
710
      setattr(options, key, val)
711

    
712
  logger.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
713
                      stderr_logging=True, program=binary)
714

    
715
  utils.debug = options.debug
716

    
717
  if old_cmdline:
718
    logger.Info("run with arguments '%s'" % old_cmdline)
719
  else:
720
    logger.Info("run with no arguments")
721

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

    
728
  return result
729

    
730

    
731
def GenerateTable(headers, fields, separator, data,
732
                  numfields=None, unitfields=None):
733
  """Prints a table with headers and different fields.
734

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

743
  """
744
  if numfields is None:
745
    numfields = []
746
  if unitfields is None:
747
    unitfields = []
748

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

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

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

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

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

    
799
  return result
800

    
801

    
802
def FormatTimestamp(ts):
803
  """Formats a given timestamp.
804

805
  @type ts: timestamp
806
  @param ts: a timeval-type timestamp, a tuple of seconds and microseconds
807

808
  @rtype: string
809
  @returns: a string with the formatted timestamp
810

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

    
817

    
818
def ParseTimespec(value):
819
  """Parse a time specification.
820

821
  The following suffixed will be recognized:
822

823
    - s: seconds
824
    - m: minutes
825
    - h: hours
826
    - d: day
827
    - w: weeks
828

829
  Without any suffix, the value will be taken to be in seconds.
830

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