Statistics
| Branch: | Tag: | Revision:

root / lib / cli.py @ 46fbdd04

History | View | Annotate | Download (26.2 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 logger
35
from ganeti import errors
36
from ganeti import constants
37
from ganeti import opcodes
38
from ganeti import luxi
39
from ganeti import ssconf
40

    
41
from optparse import (OptionParser, make_option, TitledHelpFormatter,
42
                      Option, OptionValueError)
43

    
44
__all__ = ["DEBUG_OPT", "NOHDR_OPT", "SEP_OPT", "GenericMain",
45
           "SubmitOpCode", "GetClient",
46
           "cli_option", "ikv_option", "keyval_option",
47
           "GenerateTable", "AskUser",
48
           "ARGS_NONE", "ARGS_FIXED", "ARGS_ATLEAST", "ARGS_ANY", "ARGS_ONE",
49
           "USEUNITS_OPT", "FIELDS_OPT", "FORCE_OPT", "SUBMIT_OPT",
50
           "ListTags", "AddTags", "RemoveTags", "TAG_SRC_OPT",
51
           "FormatError", "SplitNodeOption", "SubmitOrSend",
52
           "JobSubmittedException", "FormatTimestamp", "ParseTimespec",
53
           "ValidateBeParams",
54
           "ToStderr", "ToStdout",
55
           ]
56

    
57

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

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

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

    
78

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

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

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

    
107

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

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

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

    
125

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

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

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

    
142

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

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

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

    
159

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

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

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

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

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

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

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

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

    
193

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

    
198

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

    
203

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

    
208

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

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

    
218

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

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

    
227

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

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

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

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

    
265

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

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

    
278

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

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

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

    
290

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

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

    
297

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

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

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

    
308

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

    
314

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

319
  Arguments:
320
    argv: the command line
321

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

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

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

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

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

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

    
372
    cmd = aliases[cmd]
373

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

    
394
  return func, options, args
395

    
396

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

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

    
406

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

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

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

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

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

    
427

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

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

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

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

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

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

    
485

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

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

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

494
  """
495

    
496

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

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

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

    
510
  job_id = cl.SubmitJob(ops)
511

    
512
  return job_id
513

    
514

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

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

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

    
528
  prev_job_info = None
529
  prev_logmsg_serial = None
530

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

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

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

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

    
555
    prev_job_info = job_info
556

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

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

    
567

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

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

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

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

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

    
583
  return op_results[0]
584

    
585

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

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

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

    
601

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

    
616

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

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

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

    
681

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

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

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

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

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

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

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

    
720
  utils.debug = options.debug
721

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

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

    
734
  return result
735

    
736

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

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

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

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

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

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

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

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

    
805
  return result
806

    
807

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

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

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

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

    
823

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

827
  The following suffixed will be recognized:
828

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

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

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

    
865

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

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

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

    
883

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

887
  This is just a wrapper over _ToStream.
888

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

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

    
895

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

899
  This is just a wrapper over _ToStream.
900

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

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