Statistics
| Branch: | Tag: | Revision:

root / lib / cli.py @ 479636a3

History | View | Annotate | Download (30.9 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", "ToStderr", "ToStdout", "UsesRPC",
54
           "GetOnlineNodes", "JobExecutor",
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("--units", default=None,
174
                           dest="units", choices=('h', 'm', 'g', 't'),
175
                           help="Specify units for output (one of hmgt)")
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
  """Parser for the command line arguments.
317

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

321
  @param argv: the command line
322
  @param commands: dictionary with special contents, see the design
323
      doc for cmdline handling
324
  @param 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 UsesRPC(fn):
429
  def wrapper(*args, **kwargs):
430
    rpc.Init()
431
    try:
432
      return fn(*args, **kwargs)
433
    finally:
434
      rpc.Shutdown()
435
  return wrapper
436

    
437

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

441
  @param text: the question to ask
442

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

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

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

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

    
494

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

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

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

503
  """
504

    
505

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

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

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

    
519
  job_id = cl.SubmitJob(ops)
520

    
521
  return job_id
522

    
523

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

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

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

    
537
  prev_job_info = None
538
  prev_logmsg_serial = None
539

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

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

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

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

    
567
    prev_job_info = job_info
568

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

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

    
593

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

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

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

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

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

    
609
  return op_results[0]
610

    
611

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

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

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

    
627

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

    
642

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

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

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

    
710

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

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

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

    
735
  if aliases is None:
736
    aliases = {}
737

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

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

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

    
749
  utils.debug = options.debug
750

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

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

    
764
  return result
765

    
766

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

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

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

    
802
  if numfields is None:
803
    numfields = []
804
  if unitfields is None:
805
    unitfields = []
806

    
807
  numfields = utils.FieldSet(*numfields)
808
  unitfields = utils.FieldSet(*unitfields)
809

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

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

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

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

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

    
862
  return result
863

    
864

    
865
def FormatTimestamp(ts):
866
  """Formats a given timestamp.
867

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

871
  @rtype: string
872
  @returns: a string with the formatted timestamp
873

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

    
880

    
881
def ParseTimespec(value):
882
  """Parse a time specification.
883

884
  The following suffixed will be recognized:
885

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

892
  Without any suffix, the value will be taken to be in seconds.
893

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

    
922

    
923
def GetOnlineNodes(nodes, cl=None, nowarn=False):
924
  """Returns the names of online nodes.
925

926
  This function will also log a warning on stderr with the names of
927
  the online nodes.
928

929
  @param nodes: if not empty, use only this subset of nodes (minus the
930
      offline ones)
931
  @param cl: if not None, luxi client to use
932
  @type nowarn: boolean
933
  @param nowarn: by default, this function will output a note with the
934
      offline nodes that are skipped; if this parameter is True the
935
      note is not displayed
936

937
  """
938
  if cl is None:
939
    cl = GetClient()
940

    
941
  op = opcodes.OpQueryNodes(output_fields=["name", "offline"],
942
                            names=nodes)
943
  result = SubmitOpCode(op, cl=cl)
944
  offline = [row[0] for row in result if row[1]]
945
  if offline and not nowarn:
946
    ToStderr("Note: skipping offline node(s): %s" % ", ".join(offline))
947
  return [row[0] for row in result if not row[1]]
948

    
949

    
950
def _ToStream(stream, txt, *args):
951
  """Write a message to a stream, bypassing the logging system
952

953
  @type stream: file object
954
  @param stream: the file to which we should write
955
  @type txt: str
956
  @param txt: the message
957

958
  """
959
  if args:
960
    args = tuple(args)
961
    stream.write(txt % args)
962
  else:
963
    stream.write(txt)
964
  stream.write('\n')
965
  stream.flush()
966

    
967

    
968
def ToStdout(txt, *args):
969
  """Write a message to stdout only, bypassing the logging system
970

971
  This is just a wrapper over _ToStream.
972

973
  @type txt: str
974
  @param txt: the message
975

976
  """
977
  _ToStream(sys.stdout, txt, *args)
978

    
979

    
980
def ToStderr(txt, *args):
981
  """Write a message to stderr only, bypassing the logging system
982

983
  This is just a wrapper over _ToStream.
984

985
  @type txt: str
986
  @param txt: the message
987

988
  """
989
  _ToStream(sys.stderr, txt, *args)
990

    
991

    
992
class JobExecutor(object):
993
  """Class which manages the submission and execution of multiple jobs.
994

995
  Note that instances of this class should not be reused between
996
  GetResults() calls.
997

998
  """
999
  def __init__(self, cl=None, verbose=True):
1000
    self.queue = []
1001
    if cl is None:
1002
      cl = GetClient()
1003
    self.cl = cl
1004
    self.verbose = verbose
1005

    
1006
  def QueueJob(self, name, *ops):
1007
    """Submit a job for execution.
1008

1009
    @type name: string
1010
    @param name: a description of the job, will be used in WaitJobSet
1011
    """
1012
    job_id = SendJob(ops, cl=self.cl)
1013
    self.queue.append((job_id, name))
1014

    
1015
  def GetResults(self):
1016
    """Wait for and return the results of all jobs.
1017

1018
    @rtype: list
1019
    @return: list of tuples (success, job results), in the same order
1020
        as the submitted jobs; if a job has failed, instead of the result
1021
        there will be the error message
1022

1023
    """
1024
    results = []
1025
    if self.verbose:
1026
      ToStdout("Submitted jobs %s", ", ".join(row[0] for row in self.queue))
1027
    for jid, name in self.queue:
1028
      if self.verbose:
1029
        ToStdout("Waiting for job %s for %s...", jid, name)
1030
      try:
1031
        job_result = PollJob(jid, cl=self.cl)
1032
        success = True
1033
      except (errors.GenericError, luxi.ProtocolError), err:
1034
        _, job_result = FormatError(err)
1035
        success = False
1036
        # the error message will always be shown, verbose or not
1037
        ToStderr("Job %s for %s has failed: %s", jid, name, job_result)
1038

    
1039
      results.append((success, job_result))
1040
    return results
1041

    
1042
  def WaitOrShow(self, wait):
1043
    """Wait for job results or only print the job IDs.
1044

1045
    @type wait: boolean
1046
    @param wait: whether to wait or not
1047

1048
    """
1049
    if wait:
1050
      return self.GetResults()
1051
    else:
1052
      for jid, name in self.queue:
1053
        ToStdout("%s: %s", jid, name)