Statistics
| Branch: | Tag: | Revision:

root / lib / cli.py @ c1ce76bb

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
           "ToStderr", "ToStdout", "UsesRPC",
54
           "GetOnlineNodes", "JobExecutor", "SYNC_OPT",
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
SYNC_OPT = make_option("--sync", dest="do_locking",
194
                       default=False, action="store_true",
195
                       help="Grab locks while doing the queries"
196
                       " in order to ensure more consistent results")
197

    
198

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

    
203

    
204
def ARGS_ATLEAST(val):
205
  """Macro-like function denoting a minimum number of arguments"""
206
  return val
207

    
208

    
209
ARGS_NONE = None
210
ARGS_ONE = ARGS_FIXED(1)
211
ARGS_ANY = ARGS_ATLEAST(0)
212

    
213

    
214
def check_unit(option, opt, value):
215
  """OptParsers custom converter for units.
216

217
  """
218
  try:
219
    return utils.ParseUnit(value)
220
  except errors.UnitParseError, err:
221
    raise OptionValueError("option %s: %s" % (opt, err))
222

    
223

    
224
class CliOption(Option):
225
  """Custom option class for optparse.
226

227
  """
228
  TYPES = Option.TYPES + ("unit",)
229
  TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER)
230
  TYPE_CHECKER["unit"] = check_unit
231

    
232

    
233
def _SplitKeyVal(opt, data):
234
  """Convert a KeyVal string into a dict.
235

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

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

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

    
270

    
271
def check_ident_key_val(option, opt, value):
272
  """Custom parser for the IdentKeyVal option type.
273

274
  """
275
  if ":" not in value:
276
    retval =  (value, {})
277
  else:
278
    ident, rest = value.split(":", 1)
279
    kv_dict = _SplitKeyVal(opt, rest)
280
    retval = (ident, kv_dict)
281
  return retval
282

    
283

    
284
class IdentKeyValOption(Option):
285
  """Custom option class for ident:key=val,key=val options.
286

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

290
  """
291
  TYPES = Option.TYPES + ("identkeyval",)
292
  TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER)
293
  TYPE_CHECKER["identkeyval"] = check_ident_key_val
294

    
295

    
296
def check_key_val(option, opt, value):
297
  """Custom parser for the KeyVal option type.
298

299
  """
300
  return _SplitKeyVal(opt, value)
301

    
302

    
303
class KeyValOption(Option):
304
  """Custom option class for key=val,key=val options.
305

306
  This will store the parsed values as a dict {key: val}.
307

308
  """
309
  TYPES = Option.TYPES + ("keyval",)
310
  TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER)
311
  TYPE_CHECKER["keyval"] = check_key_val
312

    
313

    
314
# optparse.py sets make_option, so we do it for our own option class, too
315
cli_option = CliOption
316
ikv_option = IdentKeyValOption
317
keyval_option = KeyValOption
318

    
319

    
320
def _ParseArgs(argv, commands, aliases):
321
  """Parser for the command line arguments.
322

323
  This function parses the arguements and returns the function which
324
  must be executed together with its (modified) arguments.
325

326
  @param argv: the command line
327
  @param commands: dictionary with special contents, see the design
328
      doc for cmdline handling
329
  @param aliases: dictionary with command aliases {'alias': 'target, ...}
330

331
  """
332
  if len(argv) == 0:
333
    binary = "<command>"
334
  else:
335
    binary = argv[0].split("/")[-1]
336

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

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

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

    
373
    if aliases[cmd] not in commands:
374
      raise errors.ProgrammerError("Alias '%s' maps to non-existing"
375
                                   " command '%s'" % (cmd, aliases[cmd]))
376

    
377
    cmd = aliases[cmd]
378

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

    
399
  return func, options, args
400

    
401

    
402
def SplitNodeOption(value):
403
  """Splits the value of a --node option.
404

405
  """
406
  if value and ':' in value:
407
    return value.split(':', 1)
408
  else:
409
    return (value, None)
410

    
411

    
412
def UsesRPC(fn):
413
  def wrapper(*args, **kwargs):
414
    rpc.Init()
415
    try:
416
      return fn(*args, **kwargs)
417
    finally:
418
      rpc.Shutdown()
419
  return wrapper
420

    
421

    
422
def AskUser(text, choices=None):
423
  """Ask the user a question.
424

425
  @param text: the question to ask
426

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

432
  @return: one of the return values from the choices list; if input is
433
      not possible (i.e. not running with a tty, we return the last
434
      entry from the list
435

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

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

    
478

    
479
class JobSubmittedException(Exception):
480
  """Job was submitted, client should exit.
481

482
  This exception has one argument, the ID of the job that was
483
  submitted. The handler should print this ID.
484

485
  This is not an error, just a structured way to exit from clients.
486

487
  """
488

    
489

    
490
def SendJob(ops, cl=None):
491
  """Function to submit an opcode without waiting for the results.
492

493
  @type ops: list
494
  @param ops: list of opcodes
495
  @type cl: luxi.Client
496
  @param cl: the luxi client to use for communicating with the master;
497
             if None, a new client will be created
498

499
  """
500
  if cl is None:
501
    cl = GetClient()
502

    
503
  job_id = cl.SubmitJob(ops)
504

    
505
  return job_id
506

    
507

    
508
def PollJob(job_id, cl=None, feedback_fn=None):
509
  """Function to poll for the result of a job.
510

511
  @type job_id: job identified
512
  @param job_id: the job to poll for results
513
  @type cl: luxi.Client
514
  @param cl: the luxi client to use for communicating with the master;
515
             if None, a new client will be created
516

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

    
521
  prev_job_info = None
522
  prev_logmsg_serial = None
523

    
524
  while True:
525
    result = cl.WaitForJobChange(job_id, ["status"], prev_job_info,
526
                                 prev_logmsg_serial)
527
    if not result:
528
      # job not found, go away!
529
      raise errors.JobLost("Job with id %s lost" % job_id)
530

    
531
    # Split result, a tuple of (field values, log entries)
532
    (job_info, log_entries) = result
533
    (status, ) = job_info
534

    
535
    if log_entries:
536
      for log_entry in log_entries:
537
        (serial, timestamp, _, message) = log_entry
538
        if callable(feedback_fn):
539
          feedback_fn(log_entry[1:])
540
        else:
541
          encoded = utils.SafeEncode(message)
542
          print "%s %s" % (time.ctime(utils.MergeTime(timestamp)), encoded)
543
        prev_logmsg_serial = max(prev_logmsg_serial, serial)
544

    
545
    # TODO: Handle canceled and archived jobs
546
    elif status in (constants.JOB_STATUS_SUCCESS,
547
                    constants.JOB_STATUS_ERROR,
548
                    constants.JOB_STATUS_CANCELING,
549
                    constants.JOB_STATUS_CANCELED):
550
      break
551

    
552
    prev_job_info = job_info
553

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

    
558
  status, opstatus, result = jobs[0]
559
  if status == constants.JOB_STATUS_SUCCESS:
560
    return result
561
  elif status in (constants.JOB_STATUS_CANCELING,
562
                  constants.JOB_STATUS_CANCELED):
563
    raise errors.OpExecError("Job was canceled")
564
  else:
565
    has_ok = False
566
    for idx, (status, msg) in enumerate(zip(opstatus, result)):
567
      if status == constants.OP_STATUS_SUCCESS:
568
        has_ok = True
569
      elif status == constants.OP_STATUS_ERROR:
570
        if has_ok:
571
          raise errors.OpExecError("partial failure (opcode %d): %s" %
572
                                   (idx, msg))
573
        else:
574
          raise errors.OpExecError(str(msg))
575
    # default failure mode
576
    raise errors.OpExecError(result)
577

    
578

    
579
def SubmitOpCode(op, cl=None, feedback_fn=None):
580
  """Legacy function to submit an opcode.
581

582
  This is just a simple wrapper over the construction of the processor
583
  instance. It should be extended to better handle feedback and
584
  interaction functions.
585

586
  """
587
  if cl is None:
588
    cl = GetClient()
589

    
590
  job_id = SendJob([op], cl)
591

    
592
  op_results = PollJob(job_id, cl=cl, feedback_fn=feedback_fn)
593

    
594
  return op_results[0]
595

    
596

    
597
def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
598
  """Wrapper around SubmitOpCode or SendJob.
599

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

605
  """
606
  if opts and opts.submit_only:
607
    job_id = SendJob([op], cl=cl)
608
    raise JobSubmittedException(job_id)
609
  else:
610
    return SubmitOpCode(op, cl=cl, feedback_fn=feedback_fn)
611

    
612

    
613
def GetClient():
614
  # TODO: Cache object?
615
  try:
616
    client = luxi.Client()
617
  except luxi.NoMasterError:
618
    master, myself = ssconf.GetMasterAndMyself()
619
    if master != myself:
620
      raise errors.OpPrereqError("This is not the master node, please connect"
621
                                 " to node '%s' and rerun the command" %
622
                                 master)
623
    else:
624
      raise
625
  return client
626

    
627

    
628
def FormatError(err):
629
  """Return a formatted error message for a given error.
630

631
  This function takes an exception instance and returns a tuple
632
  consisting of two values: first, the recommended exit code, and
633
  second, a string describing the error message (not
634
  newline-terminated).
635

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

    
699

    
700
def GenericMain(commands, override=None, aliases=None):
701
  """Generic main function for all the gnt-* commands.
702

703
  Arguments:
704
    - commands: a dictionary with a special structure, see the design doc
705
                for command line handling.
706
    - override: if not None, we expect a dictionary with keys that will
707
                override command line options; this can be used to pass
708
                options from the scripts to generic functions
709
    - aliases: dictionary with command aliases {'alias': 'target, ...}
710

711
  """
712
  # save the program name and the entire command line for later logging
713
  if sys.argv:
714
    binary = os.path.basename(sys.argv[0]) or sys.argv[0]
715
    if len(sys.argv) >= 2:
716
      binary += " " + sys.argv[1]
717
      old_cmdline = " ".join(sys.argv[2:])
718
    else:
719
      old_cmdline = ""
720
  else:
721
    binary = "<unknown program>"
722
    old_cmdline = ""
723

    
724
  if aliases is None:
725
    aliases = {}
726

    
727
  func, options, args = _ParseArgs(sys.argv, commands, aliases)
728
  if func is None: # parse error
729
    return 1
730

    
731
  if override is not None:
732
    for key, val in override.iteritems():
733
      setattr(options, key, val)
734

    
735
  utils.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
736
                     stderr_logging=True, program=binary)
737

    
738
  utils.debug = options.debug
739

    
740
  if old_cmdline:
741
    logging.info("run with arguments '%s'", old_cmdline)
742
  else:
743
    logging.info("run with no arguments")
744

    
745
  try:
746
    result = func(options, args)
747
  except (errors.GenericError, luxi.ProtocolError,
748
          JobSubmittedException), err:
749
    result, err_msg = FormatError(err)
750
    logging.exception("Error durring command processing")
751
    ToStderr(err_msg)
752

    
753
  return result
754

    
755

    
756
def GenerateTable(headers, fields, separator, data,
757
                  numfields=None, unitfields=None,
758
                  units=None):
759
  """Prints a table with headers and different fields.
760

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

784
  """
785
  if units is None:
786
    if separator:
787
      units = "m"
788
    else:
789
      units = "h"
790

    
791
  if numfields is None:
792
    numfields = []
793
  if unitfields is None:
794
    unitfields = []
795

    
796
  numfields = utils.FieldSet(*numfields)
797
  unitfields = utils.FieldSet(*unitfields)
798

    
799
  format_fields = []
800
  for field in fields:
801
    if headers and field not in headers:
802
      # TODO: handle better unknown fields (either revert to old
803
      # style of raising exception, or deal more intelligently with
804
      # variable fields)
805
      headers[field] = field
806
    if separator is not None:
807
      format_fields.append("%s")
808
    elif numfields.Matches(field):
809
      format_fields.append("%*s")
810
    else:
811
      format_fields.append("%-*s")
812

    
813
  if separator is None:
814
    mlens = [0 for name in fields]
815
    format = ' '.join(format_fields)
816
  else:
817
    format = separator.replace("%", "%%").join(format_fields)
818

    
819
  for row in data:
820
    if row is None:
821
      continue
822
    for idx, val in enumerate(row):
823
      if unitfields.Matches(fields[idx]):
824
        try:
825
          val = int(val)
826
        except ValueError:
827
          pass
828
        else:
829
          val = row[idx] = utils.FormatUnit(val, units)
830
      val = row[idx] = str(val)
831
      if separator is None:
832
        mlens[idx] = max(mlens[idx], len(val))
833

    
834
  result = []
835
  if headers:
836
    args = []
837
    for idx, name in enumerate(fields):
838
      hdr = headers[name]
839
      if separator is None:
840
        mlens[idx] = max(mlens[idx], len(hdr))
841
        args.append(mlens[idx])
842
      args.append(hdr)
843
    result.append(format % tuple(args))
844

    
845
  for line in data:
846
    args = []
847
    if line is None:
848
      line = ['-' for _ in fields]
849
    for idx in xrange(len(fields)):
850
      if separator is None:
851
        args.append(mlens[idx])
852
      args.append(line[idx])
853
    result.append(format % tuple(args))
854

    
855
  return result
856

    
857

    
858
def FormatTimestamp(ts):
859
  """Formats a given timestamp.
860

861
  @type ts: timestamp
862
  @param ts: a timeval-type timestamp, a tuple of seconds and microseconds
863

864
  @rtype: string
865
  @return: a string with the formatted timestamp
866

867
  """
868
  if not isinstance (ts, (tuple, list)) or len(ts) != 2:
869
    return '?'
870
  sec, usec = ts
871
  return time.strftime("%F %T", time.localtime(sec)) + ".%06d" % usec
872

    
873

    
874
def ParseTimespec(value):
875
  """Parse a time specification.
876

877
  The following suffixed will be recognized:
878

879
    - s: seconds
880
    - m: minutes
881
    - h: hours
882
    - d: day
883
    - w: weeks
884

885
  Without any suffix, the value will be taken to be in seconds.
886

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

    
915

    
916
def GetOnlineNodes(nodes, cl=None, nowarn=False):
917
  """Returns the names of online nodes.
918

919
  This function will also log a warning on stderr with the names of
920
  the online nodes.
921

922
  @param nodes: if not empty, use only this subset of nodes (minus the
923
      offline ones)
924
  @param cl: if not None, luxi client to use
925
  @type nowarn: boolean
926
  @param nowarn: by default, this function will output a note with the
927
      offline nodes that are skipped; if this parameter is True the
928
      note is not displayed
929

930
  """
931
  if cl is None:
932
    cl = GetClient()
933

    
934
  result = cl.QueryNodes(names=nodes, fields=["name", "offline"],
935
                         use_locking=False)
936
  offline = [row[0] for row in result if row[1]]
937
  if offline and not nowarn:
938
    ToStderr("Note: skipping offline node(s): %s" % ", ".join(offline))
939
  return [row[0] for row in result if not row[1]]
940

    
941

    
942
def _ToStream(stream, txt, *args):
943
  """Write a message to a stream, bypassing the logging system
944

945
  @type stream: file object
946
  @param stream: the file to which we should write
947
  @type txt: str
948
  @param txt: the message
949

950
  """
951
  if args:
952
    args = tuple(args)
953
    stream.write(txt % args)
954
  else:
955
    stream.write(txt)
956
  stream.write('\n')
957
  stream.flush()
958

    
959

    
960
def ToStdout(txt, *args):
961
  """Write a message to stdout only, bypassing the logging system
962

963
  This is just a wrapper over _ToStream.
964

965
  @type txt: str
966
  @param txt: the message
967

968
  """
969
  _ToStream(sys.stdout, txt, *args)
970

    
971

    
972
def ToStderr(txt, *args):
973
  """Write a message to stderr only, bypassing the logging system
974

975
  This is just a wrapper over _ToStream.
976

977
  @type txt: str
978
  @param txt: the message
979

980
  """
981
  _ToStream(sys.stderr, txt, *args)
982

    
983

    
984
class JobExecutor(object):
985
  """Class which manages the submission and execution of multiple jobs.
986

987
  Note that instances of this class should not be reused between
988
  GetResults() calls.
989

990
  """
991
  def __init__(self, cl=None, verbose=True):
992
    self.queue = []
993
    if cl is None:
994
      cl = GetClient()
995
    self.cl = cl
996
    self.verbose = verbose
997

    
998
  def QueueJob(self, name, *ops):
999
    """Submit a job for execution.
1000

1001
    @type name: string
1002
    @param name: a description of the job, will be used in WaitJobSet
1003
    """
1004
    job_id = SendJob(ops, cl=self.cl)
1005
    self.queue.append((job_id, name))
1006

    
1007
  def GetResults(self):
1008
    """Wait for and return the results of all jobs.
1009

1010
    @rtype: list
1011
    @return: list of tuples (success, job results), in the same order
1012
        as the submitted jobs; if a job has failed, instead of the result
1013
        there will be the error message
1014

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

    
1031
      results.append((success, job_result))
1032
    return results
1033

    
1034
  def WaitOrShow(self, wait):
1035
    """Wait for job results or only print the job IDs.
1036

1037
    @type wait: boolean
1038
    @param wait: whether to wait or not
1039

1040
    """
1041
    if wait:
1042
      return self.GetResults()
1043
    else:
1044
      for jid, name in self.queue:
1045
        ToStdout("%s: %s", jid, name)