Statistics
| Branch: | Tag: | Revision:

root / lib / cli.py @ 5a9c3f46

History | View | Annotate | Download (28.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
from ganeti import rpc
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
           "UsesRPC",
56
           ]
57

    
58

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

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

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

    
79

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

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

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

    
108

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

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

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

    
126

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

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

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

    
143

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

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

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

    
160

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

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

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

    
174
USEUNITS_OPT = make_option("--units", default=None,
175
                           dest="units", choices=('h', 'm', 'g', 't'),
176
                           help="Specify units for output (one of hmgt)")
177

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

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

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

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

    
194

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

    
199

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

    
204

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

    
209

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

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

    
219

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

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

    
228

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

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

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

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

    
266

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

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

    
279

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

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

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

    
291

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

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

    
298

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

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

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

    
309

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

    
315

    
316
def _ParseArgs(argv, commands, aliases):
317
  """Parser for the command line arguments.
318

319
  This function parses the arguements and returns the function which
320
  must be executed together with its (modified) arguments.
321

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

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

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

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

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

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

    
373
    cmd = aliases[cmd]
374

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

    
395
  return func, options, args
396

    
397

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

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

    
407

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

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

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

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

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

    
428

    
429
def UsesRPC(fn):
430
  def wrapper(*args, **kwargs):
431
    rpc.Init()
432
    try:
433
      return fn(*args, **kwargs)
434
    finally:
435
      rpc.Shutdown()
436
  return wrapper
437

    
438

    
439
def AskUser(text, choices=None):
440
  """Ask the user a question.
441

442
  @param text: the question to ask
443

444
  @param choices: list with elements tuples (input_char, return_value,
445
      description); if not given, it will default to: [('y', True,
446
      'Perform the operation'), ('n', False, 'Do no do the operation')];
447
      note that the '?' char is reserved for help
448

449
  @return: one of the return values from the choices list; if input is
450
      not possible (i.e. not running with a tty, we return the last
451
      entry from the list
452

453
  """
454
  if choices is None:
455
    choices = [('y', True, 'Perform the operation'),
456
               ('n', False, 'Do not perform the operation')]
457
  if not choices or not isinstance(choices, list):
458
    raise errors.ProgrammerError("Invalid choiches argument to AskUser")
459
  for entry in choices:
460
    if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
461
      raise errors.ProgrammerError("Invalid choiches element to AskUser")
462

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

    
495

    
496
class JobSubmittedException(Exception):
497
  """Job was submitted, client should exit.
498

499
  This exception has one argument, the ID of the job that was
500
  submitted. The handler should print this ID.
501

502
  This is not an error, just a structured way to exit from clients.
503

504
  """
505

    
506

    
507
def SendJob(ops, cl=None):
508
  """Function to submit an opcode without waiting for the results.
509

510
  @type ops: list
511
  @param ops: list of opcodes
512
  @type cl: luxi.Client
513
  @param cl: the luxi client to use for communicating with the master;
514
             if None, a new client will be created
515

516
  """
517
  if cl is None:
518
    cl = GetClient()
519

    
520
  job_id = cl.SubmitJob(ops)
521

    
522
  return job_id
523

    
524

    
525
def PollJob(job_id, cl=None, feedback_fn=None):
526
  """Function to poll for the result of a job.
527

528
  @type job_id: job identified
529
  @param job_id: the job to poll for results
530
  @type cl: luxi.Client
531
  @param cl: the luxi client to use for communicating with the master;
532
             if None, a new client will be created
533

534
  """
535
  if cl is None:
536
    cl = GetClient()
537

    
538
  prev_job_info = None
539
  prev_logmsg_serial = None
540

    
541
  while True:
542
    result = cl.WaitForJobChange(job_id, ["status"], prev_job_info,
543
                                 prev_logmsg_serial)
544
    if not result:
545
      # job not found, go away!
546
      raise errors.JobLost("Job with id %s lost" % job_id)
547

    
548
    # Split result, a tuple of (field values, log entries)
549
    (job_info, log_entries) = result
550
    (status, ) = job_info
551

    
552
    if log_entries:
553
      for log_entry in log_entries:
554
        (serial, timestamp, _, message) = log_entry
555
        if callable(feedback_fn):
556
          feedback_fn(log_entry[1:])
557
        else:
558
          print "%s %s" % (time.ctime(utils.MergeTime(timestamp)), message)
559
        prev_logmsg_serial = max(prev_logmsg_serial, serial)
560

    
561
    # TODO: Handle canceled and archived jobs
562
    elif status in (constants.JOB_STATUS_SUCCESS,
563
                    constants.JOB_STATUS_ERROR,
564
                    constants.JOB_STATUS_CANCELING,
565
                    constants.JOB_STATUS_CANCELED):
566
      break
567

    
568
    prev_job_info = job_info
569

    
570
  jobs = cl.QueryJobs([job_id], ["status", "opstatus", "opresult"])
571
  if not jobs:
572
    raise errors.JobLost("Job with id %s lost" % job_id)
573

    
574
  status, opstatus, result = jobs[0]
575
  if status == constants.JOB_STATUS_SUCCESS:
576
    return result
577
  elif status in (constants.JOB_STATUS_CANCELING,
578
                  constants.JOB_STATUS_CANCELED):
579
    raise errors.OpExecError("Job was canceled")
580
  else:
581
    has_ok = False
582
    for idx, (status, msg) in enumerate(zip(opstatus, result)):
583
      if status == constants.OP_STATUS_SUCCESS:
584
        has_ok = True
585
      elif status == constants.OP_STATUS_ERROR:
586
        if has_ok:
587
          raise errors.OpExecError("partial failure (opcode %d): %s" %
588
                                   (idx, msg))
589
        else:
590
          raise errors.OpExecError(str(msg))
591
    # default failure mode
592
    raise errors.OpExecError(result)
593

    
594

    
595
def SubmitOpCode(op, cl=None, feedback_fn=None):
596
  """Legacy function to submit an opcode.
597

598
  This is just a simple wrapper over the construction of the processor
599
  instance. It should be extended to better handle feedback and
600
  interaction functions.
601

602
  """
603
  if cl is None:
604
    cl = GetClient()
605

    
606
  job_id = SendJob([op], cl)
607

    
608
  op_results = PollJob(job_id, cl=cl, feedback_fn=feedback_fn)
609

    
610
  return op_results[0]
611

    
612

    
613
def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
614
  """Wrapper around SubmitOpCode or SendJob.
615

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

621
  """
622
  if opts and opts.submit_only:
623
    job_id = SendJob([op], cl=cl)
624
    raise JobSubmittedException(job_id)
625
  else:
626
    return SubmitOpCode(op, cl=cl, feedback_fn=feedback_fn)
627

    
628

    
629
def GetClient():
630
  # TODO: Cache object?
631
  try:
632
    client = luxi.Client()
633
  except luxi.NoMasterError:
634
    master, myself = ssconf.GetMasterAndMyself()
635
    if master != myself:
636
      raise errors.OpPrereqError("This is not the master node, please connect"
637
                                 " to node '%s' and rerun the command" %
638
                                 master)
639
    else:
640
      raise
641
  return client
642

    
643

    
644
def FormatError(err):
645
  """Return a formatted error message for a given error.
646

647
  This function takes an exception instance and returns a tuple
648
  consisting of two values: first, the recommended exit code, and
649
  second, a string describing the error message (not
650
  newline-terminated).
651

652
  """
653
  retcode = 1
654
  obuf = StringIO()
655
  msg = str(err)
656
  if isinstance(err, errors.ConfigurationError):
657
    txt = "Corrupt configuration file: %s" % msg
658
    logging.error(txt)
659
    obuf.write(txt + "\n")
660
    obuf.write("Aborting.")
661
    retcode = 2
662
  elif isinstance(err, errors.HooksAbort):
663
    obuf.write("Failure: hooks execution failed:\n")
664
    for node, script, out in err.args[0]:
665
      if out:
666
        obuf.write("  node: %s, script: %s, output: %s\n" %
667
                   (node, script, out))
668
      else:
669
        obuf.write("  node: %s, script: %s (no output)\n" %
670
                   (node, script))
671
  elif isinstance(err, errors.HooksFailure):
672
    obuf.write("Failure: hooks general failure: %s" % msg)
673
  elif isinstance(err, errors.ResolverError):
674
    this_host = utils.HostInfo.SysName()
675
    if err.args[0] == this_host:
676
      msg = "Failure: can't resolve my own hostname ('%s')"
677
    else:
678
      msg = "Failure: can't resolve hostname '%s'"
679
    obuf.write(msg % err.args[0])
680
  elif isinstance(err, errors.OpPrereqError):
681
    obuf.write("Failure: prerequisites not met for this"
682
               " operation:\n%s" % msg)
683
  elif isinstance(err, errors.OpExecError):
684
    obuf.write("Failure: command execution error:\n%s" % msg)
685
  elif isinstance(err, errors.TagError):
686
    obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
687
  elif isinstance(err, errors.JobQueueDrainError):
688
    obuf.write("Failure: the job queue is marked for drain and doesn't"
689
               " accept new requests\n")
690
  elif isinstance(err, errors.GenericError):
691
    obuf.write("Unhandled Ganeti error: %s" % msg)
692
  elif isinstance(err, luxi.NoMasterError):
693
    obuf.write("Cannot communicate with the master daemon.\nIs it running"
694
               " and listening for connections?")
695
  elif isinstance(err, luxi.TimeoutError):
696
    obuf.write("Timeout while talking to the master daemon. Error:\n"
697
               "%s" % msg)
698
  elif isinstance(err, luxi.ProtocolError):
699
    obuf.write("Unhandled protocol error while talking to the master daemon:\n"
700
               "%s" % msg)
701
  elif isinstance(err, JobSubmittedException):
702
    obuf.write("JobID: %s\n" % err.args[0])
703
    retcode = 0
704
  else:
705
    obuf.write("Unhandled exception: %s" % msg)
706
  return retcode, obuf.getvalue().rstrip('\n')
707

    
708

    
709
def GenericMain(commands, override=None, aliases=None):
710
  """Generic main function for all the gnt-* commands.
711

712
  Arguments:
713
    - commands: a dictionary with a special structure, see the design doc
714
                for command line handling.
715
    - override: if not None, we expect a dictionary with keys that will
716
                override command line options; this can be used to pass
717
                options from the scripts to generic functions
718
    - aliases: dictionary with command aliases {'alias': 'target, ...}
719

720
  """
721
  # save the program name and the entire command line for later logging
722
  if sys.argv:
723
    binary = os.path.basename(sys.argv[0]) or sys.argv[0]
724
    if len(sys.argv) >= 2:
725
      binary += " " + sys.argv[1]
726
      old_cmdline = " ".join(sys.argv[2:])
727
    else:
728
      old_cmdline = ""
729
  else:
730
    binary = "<unknown program>"
731
    old_cmdline = ""
732

    
733
  if aliases is None:
734
    aliases = {}
735

    
736
  func, options, args = _ParseArgs(sys.argv, commands, aliases)
737
  if func is None: # parse error
738
    return 1
739

    
740
  if override is not None:
741
    for key, val in override.iteritems():
742
      setattr(options, key, val)
743

    
744
  utils.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
745
                     stderr_logging=True, program=binary)
746

    
747
  utils.debug = options.debug
748

    
749
  if old_cmdline:
750
    logging.info("run with arguments '%s'", old_cmdline)
751
  else:
752
    logging.info("run with no arguments")
753

    
754
  try:
755
    result = func(options, args)
756
  except (errors.GenericError, luxi.ProtocolError,
757
          JobSubmittedException), err:
758
    result, err_msg = FormatError(err)
759
    logging.exception("Error durring command processing")
760
    ToStderr(err_msg)
761

    
762
  return result
763

    
764

    
765
def GenerateTable(headers, fields, separator, data,
766
                  numfields=None, unitfields=None,
767
                  units=None):
768
  """Prints a table with headers and different fields.
769

770
  @type headers: dict
771
  @param headers: dictionary mapping field names to headers for
772
      the table
773
  @type fields: list
774
  @param fields: the field names corresponding to each row in
775
      the data field
776
  @param separator: the separator to be used; if this is None,
777
      the default 'smart' algorithm is used which computes optimal
778
      field width, otherwise just the separator is used between
779
      each field
780
  @type data: list
781
  @param data: a list of lists, each sublist being one row to be output
782
  @type numfields: list
783
  @param numfields: a list with the fields that hold numeric
784
      values and thus should be right-aligned
785
  @type unitfields: list
786
  @param unitfields: a list with the fields that hold numeric
787
      values that should be formatted with the units field
788
  @type units: string or None
789
  @param units: the units we should use for formatting, or None for
790
      automatic choice (human-readable for non-separator usage, otherwise
791
      megabytes); this is a one-letter string
792

793
  """
794
  if units is None:
795
    if separator:
796
      units = "m"
797
    else:
798
      units = "h"
799

    
800
  if numfields is None:
801
    numfields = []
802
  if unitfields is None:
803
    unitfields = []
804

    
805
  numfields = utils.FieldSet(*numfields)
806
  unitfields = utils.FieldSet(*unitfields)
807

    
808
  format_fields = []
809
  for field in fields:
810
    if headers and field not in headers:
811
      # FIXME: handle better unknown fields (either revert to old
812
      # style of raising exception, or deal more intelligently with
813
      # variable fields)
814
      headers[field] = field
815
    if separator is not None:
816
      format_fields.append("%s")
817
    elif numfields.Matches(field):
818
      format_fields.append("%*s")
819
    else:
820
      format_fields.append("%-*s")
821

    
822
  if separator is None:
823
    mlens = [0 for name in fields]
824
    format = ' '.join(format_fields)
825
  else:
826
    format = separator.replace("%", "%%").join(format_fields)
827

    
828
  for row in data:
829
    for idx, val in enumerate(row):
830
      if unitfields.Matches(fields[idx]):
831
        try:
832
          val = int(val)
833
        except ValueError:
834
          pass
835
        else:
836
          val = row[idx] = utils.FormatUnit(val, units)
837
      val = row[idx] = str(val)
838
      if separator is None:
839
        mlens[idx] = max(mlens[idx], len(val))
840

    
841
  result = []
842
  if headers:
843
    args = []
844
    for idx, name in enumerate(fields):
845
      hdr = headers[name]
846
      if separator is None:
847
        mlens[idx] = max(mlens[idx], len(hdr))
848
        args.append(mlens[idx])
849
      args.append(hdr)
850
    result.append(format % tuple(args))
851

    
852
  for line in data:
853
    args = []
854
    for idx in xrange(len(fields)):
855
      if separator is None:
856
        args.append(mlens[idx])
857
      args.append(line[idx])
858
    result.append(format % tuple(args))
859

    
860
  return result
861

    
862

    
863
def FormatTimestamp(ts):
864
  """Formats a given timestamp.
865

866
  @type ts: timestamp
867
  @param ts: a timeval-type timestamp, a tuple of seconds and microseconds
868

869
  @rtype: string
870
  @returns: a string with the formatted timestamp
871

872
  """
873
  if not isinstance (ts, (tuple, list)) or len(ts) != 2:
874
    return '?'
875
  sec, usec = ts
876
  return time.strftime("%F %T", time.localtime(sec)) + ".%06d" % usec
877

    
878

    
879
def ParseTimespec(value):
880
  """Parse a time specification.
881

882
  The following suffixed will be recognized:
883

884
    - s: seconds
885
    - m: minutes
886
    - h: hours
887
    - d: day
888
    - w: weeks
889

890
  Without any suffix, the value will be taken to be in seconds.
891

892
  """
893
  value = str(value)
894
  if not value:
895
    raise errors.OpPrereqError("Empty time specification passed")
896
  suffix_map = {
897
    's': 1,
898
    'm': 60,
899
    'h': 3600,
900
    'd': 86400,
901
    'w': 604800,
902
    }
903
  if value[-1] not in suffix_map:
904
    try:
905
      value = int(value)
906
    except ValueError:
907
      raise errors.OpPrereqError("Invalid time specification '%s'" % value)
908
  else:
909
    multiplier = suffix_map[value[-1]]
910
    value = value[:-1]
911
    if not value: # no data left after stripping the suffix
912
      raise errors.OpPrereqError("Invalid time specification (only"
913
                                 " suffix passed)")
914
    try:
915
      value = int(value) * multiplier
916
    except ValueError:
917
      raise errors.OpPrereqError("Invalid time specification '%s'" % value)
918
  return value
919

    
920

    
921
def _ToStream(stream, txt, *args):
922
  """Write a message to a stream, bypassing the logging system
923

924
  @type stream: file object
925
  @param stream: the file to which we should write
926
  @type txt: str
927
  @param txt: the message
928

929
  """
930
  if args:
931
    args = tuple(args)
932
    stream.write(txt % args)
933
  else:
934
    stream.write(txt)
935
  stream.write('\n')
936
  stream.flush()
937

    
938

    
939
def ToStdout(txt, *args):
940
  """Write a message to stdout only, bypassing the logging system
941

942
  This is just a wrapper over _ToStream.
943

944
  @type txt: str
945
  @param txt: the message
946

947
  """
948
  _ToStream(sys.stdout, txt, *args)
949

    
950

    
951
def ToStderr(txt, *args):
952
  """Write a message to stderr only, bypassing the logging system
953

954
  This is just a wrapper over _ToStream.
955

956
  @type txt: str
957
  @param txt: the message
958

959
  """
960
  _ToStream(sys.stderr, txt, *args)