Statistics
| Branch: | Tag: | Revision:

root / lib / cli.py @ 4f31882e

History | View | Annotate | Download (32 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", "CONFIRM_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
CONFIRM_OPT = make_option("--yes", dest="confirm", action="store_true",
186
                          default=False, help="Do not require confirmation")
187

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

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

    
196
SYNC_OPT = make_option("--sync", dest="do_locking",
197
                       default=False, action="store_true",
198
                       help="Grab locks while doing the queries"
199
                       " in order to ensure more consistent results")
200

    
201
_DRY_RUN_OPT = make_option("--dry-run", default=False,
202
                          action="store_true",
203
                          help="Do not execute the operation, just run the"
204
                          " check steps and verify it it could be executed")
205

    
206

    
207
def ARGS_FIXED(val):
208
  """Macro-like function denoting a fixed number of arguments"""
209
  return -val
210

    
211

    
212
def ARGS_ATLEAST(val):
213
  """Macro-like function denoting a minimum number of arguments"""
214
  return val
215

    
216

    
217
ARGS_NONE = None
218
ARGS_ONE = ARGS_FIXED(1)
219
ARGS_ANY = ARGS_ATLEAST(0)
220

    
221

    
222
def check_unit(option, opt, value):
223
  """OptParsers custom converter for units.
224

225
  """
226
  try:
227
    return utils.ParseUnit(value)
228
  except errors.UnitParseError, err:
229
    raise OptionValueError("option %s: %s" % (opt, err))
230

    
231

    
232
class CliOption(Option):
233
  """Custom option class for optparse.
234

235
  """
236
  TYPES = Option.TYPES + ("unit",)
237
  TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER)
238
  TYPE_CHECKER["unit"] = check_unit
239

    
240

    
241
def _SplitKeyVal(opt, data):
242
  """Convert a KeyVal string into a dict.
243

244
  This function will convert a key=val[,...] string into a dict. Empty
245
  values will be converted specially: keys which have the prefix 'no_'
246
  will have the value=False and the prefix stripped, the others will
247
  have value=True.
248

249
  @type opt: string
250
  @param opt: a string holding the option name for which we process the
251
      data, used in building error messages
252
  @type data: string
253
  @param data: a string of the format key=val,key=val,...
254
  @rtype: dict
255
  @return: {key=val, key=val}
256
  @raises errors.ParameterError: if there are duplicate keys
257

258
  """
259
  NO_PREFIX = "no_"
260
  UN_PREFIX = "-"
261
  kv_dict = {}
262
  if data:
263
    for elem in data.split(","):
264
      if "=" in elem:
265
        key, val = elem.split("=", 1)
266
      else:
267
        if elem.startswith(NO_PREFIX):
268
          key, val = elem[len(NO_PREFIX):], False
269
        elif elem.startswith(UN_PREFIX):
270
          key, val = elem[len(UN_PREFIX):], None
271
        else:
272
          key, val = elem, True
273
      if key in kv_dict:
274
        raise errors.ParameterError("Duplicate key '%s' in option %s" %
275
                                    (key, opt))
276
      kv_dict[key] = val
277
  return kv_dict
278

    
279

    
280
def check_ident_key_val(option, opt, value):
281
  """Custom parser for the IdentKeyVal option type.
282

283
  """
284
  if ":" not in value:
285
    retval =  (value, {})
286
  else:
287
    ident, rest = value.split(":", 1)
288
    kv_dict = _SplitKeyVal(opt, rest)
289
    retval = (ident, kv_dict)
290
  return retval
291

    
292

    
293
class IdentKeyValOption(Option):
294
  """Custom option class for ident:key=val,key=val options.
295

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

299
  """
300
  TYPES = Option.TYPES + ("identkeyval",)
301
  TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER)
302
  TYPE_CHECKER["identkeyval"] = check_ident_key_val
303

    
304

    
305
def check_key_val(option, opt, value):
306
  """Custom parser for the KeyVal option type.
307

308
  """
309
  return _SplitKeyVal(opt, value)
310

    
311

    
312
class KeyValOption(Option):
313
  """Custom option class for key=val,key=val options.
314

315
  This will store the parsed values as a dict {key: val}.
316

317
  """
318
  TYPES = Option.TYPES + ("keyval",)
319
  TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER)
320
  TYPE_CHECKER["keyval"] = check_key_val
321

    
322

    
323
# optparse.py sets make_option, so we do it for our own option class, too
324
cli_option = CliOption
325
ikv_option = IdentKeyValOption
326
keyval_option = KeyValOption
327

    
328

    
329
def _ParseArgs(argv, commands, aliases):
330
  """Parser for the command line arguments.
331

332
  This function parses the arguements and returns the function which
333
  must be executed together with its (modified) arguments.
334

335
  @param argv: the command line
336
  @param commands: dictionary with special contents, see the design
337
      doc for cmdline handling
338
  @param aliases: dictionary with command aliases {'alias': 'target, ...}
339

340
  """
341
  if len(argv) == 0:
342
    binary = "<command>"
343
  else:
344
    binary = argv[0].split("/")[-1]
345

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

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

    
375
  # get command, unalias it, and look it up in commands
376
  cmd = argv.pop(1)
377
  if cmd in aliases:
378
    if cmd in commands:
379
      raise errors.ProgrammerError("Alias '%s' overrides an existing"
380
                                   " command" % cmd)
381

    
382
    if aliases[cmd] not in commands:
383
      raise errors.ProgrammerError("Alias '%s' maps to non-existing"
384
                                   " command '%s'" % (cmd, aliases[cmd]))
385

    
386
    cmd = aliases[cmd]
387

    
388
  func, nargs, parser_opts, usage, description = commands[cmd]
389
  parser = OptionParser(option_list=parser_opts + [_DRY_RUN_OPT],
390
                        description=description,
391
                        formatter=TitledHelpFormatter(),
392
                        usage="%%prog %s %s" % (cmd, usage))
393
  parser.disable_interspersed_args()
394
  options, args = parser.parse_args()
395
  if nargs is None:
396
    if len(args) != 0:
397
      print >> sys.stderr, ("Error: Command %s expects no arguments" % cmd)
398
      return None, None, None
399
  elif nargs < 0 and len(args) != -nargs:
400
    print >> sys.stderr, ("Error: Command %s expects %d argument(s)" %
401
                         (cmd, -nargs))
402
    return None, None, None
403
  elif nargs >= 0 and len(args) < nargs:
404
    print >> sys.stderr, ("Error: Command %s expects at least %d argument(s)" %
405
                         (cmd, nargs))
406
    return None, None, None
407

    
408
  return func, options, args
409

    
410

    
411
def SplitNodeOption(value):
412
  """Splits the value of a --node option.
413

414
  """
415
  if value and ':' in value:
416
    return value.split(':', 1)
417
  else:
418
    return (value, None)
419

    
420

    
421
def UsesRPC(fn):
422
  def wrapper(*args, **kwargs):
423
    rpc.Init()
424
    try:
425
      return fn(*args, **kwargs)
426
    finally:
427
      rpc.Shutdown()
428
  return wrapper
429

    
430

    
431
def AskUser(text, choices=None):
432
  """Ask the user a question.
433

434
  @param text: the question to ask
435

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

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

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

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

    
487

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

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

494
  This is not an error, just a structured way to exit from clients.
495

496
  """
497

    
498

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

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

508
  """
509
  if cl is None:
510
    cl = GetClient()
511

    
512
  job_id = cl.SubmitJob(ops)
513

    
514
  return job_id
515

    
516

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

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

526
  """
527
  if cl is None:
528
    cl = GetClient()
529

    
530
  prev_job_info = None
531
  prev_logmsg_serial = None
532

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

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

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

    
554
    # TODO: Handle canceled and archived jobs
555
    elif status in (constants.JOB_STATUS_SUCCESS,
556
                    constants.JOB_STATUS_ERROR,
557
                    constants.JOB_STATUS_CANCELING,
558
                    constants.JOB_STATUS_CANCELED):
559
      break
560

    
561
    prev_job_info = job_info
562

    
563
  jobs = cl.QueryJobs([job_id], ["status", "opstatus", "opresult"])
564
  if not jobs:
565
    raise errors.JobLost("Job with id %s lost" % job_id)
566

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

    
587

    
588
def SubmitOpCode(op, cl=None, feedback_fn=None):
589
  """Legacy function to submit an opcode.
590

591
  This is just a simple wrapper over the construction of the processor
592
  instance. It should be extended to better handle feedback and
593
  interaction functions.
594

595
  """
596
  if cl is None:
597
    cl = GetClient()
598

    
599
  job_id = SendJob([op], cl)
600

    
601
  op_results = PollJob(job_id, cl=cl, feedback_fn=feedback_fn)
602

    
603
  return op_results[0]
604

    
605

    
606
def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
607
  """Wrapper around SubmitOpCode or SendJob.
608

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

614
  It will also add the dry-run parameter from the options passed, if true.
615

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

    
625

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

    
640

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

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

649
  """
650
  retcode = 1
651
  obuf = StringIO()
652
  msg = str(err)
653
  if isinstance(err, errors.ConfigurationError):
654
    txt = "Corrupt configuration file: %s" % msg
655
    logging.error(txt)
656
    obuf.write(txt + "\n")
657
    obuf.write("Aborting.")
658
    retcode = 2
659
  elif isinstance(err, errors.HooksAbort):
660
    obuf.write("Failure: hooks execution failed:\n")
661
    for node, script, out in err.args[0]:
662
      if out:
663
        obuf.write("  node: %s, script: %s, output: %s\n" %
664
                   (node, script, out))
665
      else:
666
        obuf.write("  node: %s, script: %s (no output)\n" %
667
                   (node, script))
668
  elif isinstance(err, errors.HooksFailure):
669
    obuf.write("Failure: hooks general failure: %s" % msg)
670
  elif isinstance(err, errors.ResolverError):
671
    this_host = utils.HostInfo.SysName()
672
    if err.args[0] == this_host:
673
      msg = "Failure: can't resolve my own hostname ('%s')"
674
    else:
675
      msg = "Failure: can't resolve hostname '%s'"
676
    obuf.write(msg % err.args[0])
677
  elif isinstance(err, errors.OpPrereqError):
678
    obuf.write("Failure: prerequisites not met for this"
679
               " operation:\n%s" % msg)
680
  elif isinstance(err, errors.OpExecError):
681
    obuf.write("Failure: command execution error:\n%s" % msg)
682
  elif isinstance(err, errors.TagError):
683
    obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
684
  elif isinstance(err, errors.JobQueueDrainError):
685
    obuf.write("Failure: the job queue is marked for drain and doesn't"
686
               " accept new requests\n")
687
  elif isinstance(err, errors.JobQueueFull):
688
    obuf.write("Failure: the job queue is full and doesn't accept new"
689
               " job submissions until old jobs are archived\n")
690
  elif isinstance(err, errors.TypeEnforcementError):
691
    obuf.write("Parameter Error: %s" % msg)
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
      # TODO: 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
    if row is None:
832
      continue
833
    for idx, val in enumerate(row):
834
      if unitfields.Matches(fields[idx]):
835
        try:
836
          val = int(val)
837
        except ValueError:
838
          pass
839
        else:
840
          val = row[idx] = utils.FormatUnit(val, units)
841
      val = row[idx] = str(val)
842
      if separator is None:
843
        mlens[idx] = max(mlens[idx], len(val))
844

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

    
856
  for line in data:
857
    args = []
858
    if line is None:
859
      line = ['-' for _ in fields]
860
    for idx in xrange(len(fields)):
861
      if separator is None:
862
        args.append(mlens[idx])
863
      args.append(line[idx])
864
    result.append(format % tuple(args))
865

    
866
  return result
867

    
868

    
869
def FormatTimestamp(ts):
870
  """Formats a given timestamp.
871

872
  @type ts: timestamp
873
  @param ts: a timeval-type timestamp, a tuple of seconds and microseconds
874

875
  @rtype: string
876
  @return: a string with the formatted timestamp
877

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

    
884

    
885
def ParseTimespec(value):
886
  """Parse a time specification.
887

888
  The following suffixed will be recognized:
889

890
    - s: seconds
891
    - m: minutes
892
    - h: hours
893
    - d: day
894
    - w: weeks
895

896
  Without any suffix, the value will be taken to be in seconds.
897

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

    
926

    
927
def GetOnlineNodes(nodes, cl=None, nowarn=False):
928
  """Returns the names of online nodes.
929

930
  This function will also log a warning on stderr with the names of
931
  the online nodes.
932

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

941
  """
942
  if cl is None:
943
    cl = GetClient()
944

    
945
  result = cl.QueryNodes(names=nodes, fields=["name", "offline"],
946
                         use_locking=False)
947
  offline = [row[0] for row in result if row[1]]
948
  if offline and not nowarn:
949
    ToStderr("Note: skipping offline node(s): %s" % ", ".join(offline))
950
  return [row[0] for row in result if not row[1]]
951

    
952

    
953
def _ToStream(stream, txt, *args):
954
  """Write a message to a stream, bypassing the logging system
955

956
  @type stream: file object
957
  @param stream: the file to which we should write
958
  @type txt: str
959
  @param txt: the message
960

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

    
970

    
971
def ToStdout(txt, *args):
972
  """Write a message to stdout only, bypassing the logging system
973

974
  This is just a wrapper over _ToStream.
975

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

979
  """
980
  _ToStream(sys.stdout, txt, *args)
981

    
982

    
983
def ToStderr(txt, *args):
984
  """Write a message to stderr only, bypassing the logging system
985

986
  This is just a wrapper over _ToStream.
987

988
  @type txt: str
989
  @param txt: the message
990

991
  """
992
  _ToStream(sys.stderr, txt, *args)
993

    
994

    
995
class JobExecutor(object):
996
  """Class which manages the submission and execution of multiple jobs.
997

998
  Note that instances of this class should not be reused between
999
  GetResults() calls.
1000

1001
  """
1002
  def __init__(self, cl=None, verbose=True):
1003
    self.queue = []
1004
    if cl is None:
1005
      cl = GetClient()
1006
    self.cl = cl
1007
    self.verbose = verbose
1008
    self.jobs = []
1009

    
1010
  def QueueJob(self, name, *ops):
1011
    """Record a job for later submit.
1012

1013
    @type name: string
1014
    @param name: a description of the job, will be used in WaitJobSet
1015
    """
1016
    self.queue.append((name, ops))
1017

    
1018

    
1019
  def SubmitPending(self):
1020
    """Submit all pending jobs.
1021

1022
    """
1023
    results = self.cl.SubmitManyJobs([row[1] for row in self.queue])
1024
    for ((status, data), (name, _)) in zip(results, self.queue):
1025
      self.jobs.append((status, data, name))
1026

    
1027
  def GetResults(self):
1028
    """Wait for and return the results of all jobs.
1029

1030
    @rtype: list
1031
    @return: list of tuples (success, job results), in the same order
1032
        as the submitted jobs; if a job has failed, instead of the result
1033
        there will be the error message
1034

1035
    """
1036
    if not self.jobs:
1037
      self.SubmitPending()
1038
    results = []
1039
    if self.verbose:
1040
      ok_jobs = [row[1] for row in self.jobs if row[0]]
1041
      if ok_jobs:
1042
        ToStdout("Submitted jobs %s", ", ".join(ok_jobs))
1043
    for submit_status, jid, name in self.jobs:
1044
      if not submit_status:
1045
        ToStderr("Failed to submit job for %s: %s", name, jid)
1046
        results.append((False, jid))
1047
        continue
1048
      if self.verbose:
1049
        ToStdout("Waiting for job %s for %s...", jid, name)
1050
      try:
1051
        job_result = PollJob(jid, cl=self.cl)
1052
        success = True
1053
      except (errors.GenericError, luxi.ProtocolError), err:
1054
        _, job_result = FormatError(err)
1055
        success = False
1056
        # the error message will always be shown, verbose or not
1057
        ToStderr("Job %s for %s has failed: %s", jid, name, job_result)
1058

    
1059
      results.append((success, job_result))
1060
    return results
1061

    
1062
  def WaitOrShow(self, wait):
1063
    """Wait for job results or only print the job IDs.
1064

1065
    @type wait: boolean
1066
    @param wait: whether to wait or not
1067

1068
    """
1069
    if wait:
1070
      return self.GetResults()
1071
    else:
1072
      if not self.jobs:
1073
        self.SubmitPending()
1074
      for status, result, name in self.jobs:
1075
        if status:
1076
          ToStdout("%s: %s", result, name)
1077
        else:
1078
          ToStderr("Failure for %s: %s", name, result)