Statistics
| Branch: | Tag: | Revision:

root / lib / cli.py @ 64c65a2a

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
  for elem in data.split(","):
263
    if "=" in elem:
264
      key, val = elem.split("=", 1)
265
    else:
266
      if elem.startswith(NO_PREFIX):
267
        key, val = elem[len(NO_PREFIX):], False
268
      elif elem.startswith(UN_PREFIX):
269
        key, val = elem[len(UN_PREFIX):], None
270
      else:
271
        key, val = elem, True
272
    if key in kv_dict:
273
      raise errors.ParameterError("Duplicate key '%s' in option %s" %
274
                                  (key, opt))
275
    kv_dict[key] = val
276
  return kv_dict
277

    
278

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

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

    
291

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

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

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

    
303

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

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

    
310

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

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

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

    
321

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

    
327

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

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

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

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

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

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

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

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

    
385
    cmd = aliases[cmd]
386

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

    
407
  return func, options, args
408

    
409

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

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

    
419

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

    
429

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

433
  @param text: the question to ask
434

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

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

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

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

    
486

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

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

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

495
  """
496

    
497

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

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

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

    
511
  job_id = cl.SubmitJob(ops)
512

    
513
  return job_id
514

    
515

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

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

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

    
529
  prev_job_info = None
530
  prev_logmsg_serial = None
531

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

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

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

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

    
560
    prev_job_info = job_info
561

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

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

    
586

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

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

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

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

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

    
602
  return op_results[0]
603

    
604

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

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

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

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

    
624

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

    
639

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

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

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

    
709

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

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

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

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

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

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

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

    
748
  utils.debug = options.debug
749

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

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

    
763
  return result
764

    
765

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

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

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

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

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

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

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

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

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

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

    
865
  return result
866

    
867

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

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

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

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

    
883

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

887
  The following suffixed will be recognized:
888

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

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

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

    
925

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

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

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

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

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

    
951

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

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

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

    
969

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

973
  This is just a wrapper over _ToStream.
974

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

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

    
981

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

985
  This is just a wrapper over _ToStream.
986

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

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

    
993

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

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

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

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

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

    
1017

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

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

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

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

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

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

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

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

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