Statistics
| Branch: | Tag: | Revision:

root / lib / cli.py @ 03298ebe

History | View | Annotate | Download (32.5 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

    
45
__all__ = ["DEBUG_OPT", "NOHDR_OPT", "SEP_OPT", "GenericMain",
46
           "SubmitOpCode", "GetClient",
47
           "cli_option", "ikv_option", "keyval_option",
48
           "GenerateTable", "AskUser",
49
           "ARGS_NONE", "ARGS_FIXED", "ARGS_ATLEAST", "ARGS_ANY", "ARGS_ONE",
50
           "USEUNITS_OPT", "FIELDS_OPT", "FORCE_OPT", "SUBMIT_OPT",
51
           "ListTags", "AddTags", "RemoveTags", "TAG_SRC_OPT",
52
           "FormatError", "SplitNodeOption", "SubmitOrSend",
53
           "JobSubmittedException", "FormatTimestamp", "ParseTimespec",
54
           "ToStderr", "ToStdout", "UsesRPC",
55
           "GetOnlineNodes", "JobExecutor", "SYNC_OPT", "CONFIRM_OPT",
56
           ]
57

    
58
NO_PREFIX = "no_"
59
UN_PREFIX = "-"
60

    
61

    
62
def _ExtractTagsObject(opts, args):
63
  """Extract the tag type object.
64

65
  Note that this function will modify its args parameter.
66

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

    
82

    
83
def _ExtendTags(opts, args):
84
  """Extend the args if a source file has been given.
85

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

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

    
111

    
112
def ListTags(opts, args):
113
  """List the tags on a given object.
114

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

120
  """
121
  kind, name = _ExtractTagsObject(opts, args)
122
  op = opcodes.OpGetTags(kind=kind, name=name)
123
  result = SubmitOpCode(op)
124
  result = list(result)
125
  result.sort()
126
  for tag in result:
127
    ToStdout(tag)
128

    
129

    
130
def AddTags(opts, args):
131
  """Add tags on a given object.
132

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

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

    
146

    
147
def RemoveTags(opts, args):
148
  """Remove tags from a given object.
149

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

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

    
163

    
164
DEBUG_OPT = make_option("-d", "--debug", default=False,
165
                        action="store_true",
166
                        help="Turn debugging on")
167

    
168
NOHDR_OPT = make_option("--no-headers", default=False,
169
                        action="store_true", dest="no_headers",
170
                        help="Don't display column headers")
171

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

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

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

    
186
FORCE_OPT = make_option("-f", "--force", dest="force", action="store_true",
187
                        default=False, help="Force the operation")
188

    
189
CONFIRM_OPT = make_option("--yes", dest="confirm", action="store_true",
190
                          default=False, help="Do not require confirmation")
191

    
192
TAG_SRC_OPT = make_option("--from", dest="tags_source",
193
                          default=None, help="File with tag names")
194

    
195
SUBMIT_OPT = make_option("--submit", dest="submit_only",
196
                         default=False, action="store_true",
197
                         help="Submit the job and return the job ID, but"
198
                         " don't wait for the job to finish")
199

    
200
SYNC_OPT = make_option("--sync", dest="do_locking",
201
                       default=False, action="store_true",
202
                       help="Grab locks while doing the queries"
203
                       " in order to ensure more consistent results")
204

    
205
_DRY_RUN_OPT = make_option("--dry-run", default=False,
206
                          action="store_true",
207
                          help="Do not execute the operation, just run the"
208
                          " check steps and verify it it could be executed")
209

    
210

    
211
def ARGS_FIXED(val):
212
  """Macro-like function denoting a fixed number of arguments"""
213
  return -val
214

    
215

    
216
def ARGS_ATLEAST(val):
217
  """Macro-like function denoting a minimum number of arguments"""
218
  return val
219

    
220

    
221
ARGS_NONE = None
222
ARGS_ONE = ARGS_FIXED(1)
223
ARGS_ANY = ARGS_ATLEAST(0)
224

    
225

    
226
def check_unit(option, opt, value):
227
  """OptParsers custom converter for units.
228

229
  """
230
  try:
231
    return utils.ParseUnit(value)
232
  except errors.UnitParseError, err:
233
    raise OptionValueError("option %s: %s" % (opt, err))
234

    
235

    
236
class CliOption(Option):
237
  """Custom option class for optparse.
238

239
  """
240
  TYPES = Option.TYPES + ("unit",)
241
  TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER)
242
  TYPE_CHECKER["unit"] = check_unit
243

    
244

    
245
def _SplitKeyVal(opt, data):
246
  """Convert a KeyVal string into a dict.
247

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

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

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

    
281

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

285
  """
286
  if ":" not in value:
287
    ident, rest = value, ''
288
  else:
289
    ident, rest = value.split(":", 1)
290

    
291
  if ident.startswith(NO_PREFIX):
292
    if rest:
293
      msg = "Cannot pass options when removing parameter groups: %s" % value
294
      raise errors.ParameterError(msg)
295
    retval = (ident[len(NO_PREFIX):], False)
296
  elif ident.startswith(UN_PREFIX):
297
    if rest:
298
      msg = "Cannot pass options when removing parameter groups: %s" % value
299
      raise errors.ParameterError(msg)
300
    retval = (ident[len(UN_PREFIX):], None)
301
  else:
302
    kv_dict = _SplitKeyVal(opt, rest)
303
    retval = (ident, kv_dict)
304
  return retval
305

    
306

    
307
class IdentKeyValOption(Option):
308
  """Custom option class for ident:key=val,key=val options.
309

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

313
  """
314
  TYPES = Option.TYPES + ("identkeyval",)
315
  TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER)
316
  TYPE_CHECKER["identkeyval"] = check_ident_key_val
317

    
318

    
319
def check_key_val(option, opt, value):
320
  """Custom parser for the KeyVal option type.
321

322
  """
323
  return _SplitKeyVal(opt, value)
324

    
325

    
326
class KeyValOption(Option):
327
  """Custom option class for key=val,key=val options.
328

329
  This will store the parsed values as a dict {key: val}.
330

331
  """
332
  TYPES = Option.TYPES + ("keyval",)
333
  TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER)
334
  TYPE_CHECKER["keyval"] = check_key_val
335

    
336

    
337
# optparse.py sets make_option, so we do it for our own option class, too
338
cli_option = CliOption
339
ikv_option = IdentKeyValOption
340
keyval_option = KeyValOption
341

    
342

    
343
def _ParseArgs(argv, commands, aliases):
344
  """Parser for the command line arguments.
345

346
  This function parses the arguments and returns the function which
347
  must be executed together with its (modified) arguments.
348

349
  @param argv: the command line
350
  @param commands: dictionary with special contents, see the design
351
      doc for cmdline handling
352
  @param aliases: dictionary with command aliases {'alias': 'target, ...}
353

354
  """
355
  if len(argv) == 0:
356
    binary = "<command>"
357
  else:
358
    binary = argv[0].split("/")[-1]
359

    
360
  if len(argv) > 1 and argv[1] == "--version":
361
    ToStdout("%s (ganeti) %s", binary, constants.RELEASE_VERSION)
362
    # Quit right away. That way we don't have to care about this special
363
    # argument. optparse.py does it the same.
364
    sys.exit(0)
365

    
366
  if len(argv) < 2 or not (argv[1] in commands or
367
                           argv[1] in aliases):
368
    # let's do a nice thing
369
    sortedcmds = commands.keys()
370
    sortedcmds.sort()
371

    
372
    ToStdout("Usage: %s {command} [options...] [argument...]", binary)
373
    ToStdout("%s <command> --help to see details, or man %s", binary, binary)
374
    ToStdout("")
375

    
376
    # compute the max line length for cmd + usage
377
    mlen = max([len(" %s" % cmd) for cmd in commands])
378
    mlen = min(60, mlen) # should not get here...
379

    
380
    # and format a nice command list
381
    ToStdout("Commands:")
382
    for cmd in sortedcmds:
383
      cmdstr = " %s" % (cmd,)
384
      help_text = commands[cmd][4]
385
      help_lines = textwrap.wrap(help_text, 79 - 3 - mlen)
386
      ToStdout("%-*s - %s", mlen, cmdstr, help_lines.pop(0))
387
      for line in help_lines:
388
        ToStdout("%-*s   %s", mlen, "", line)
389

    
390
    ToStdout("")
391

    
392
    return None, None, None
393

    
394
  # get command, unalias it, and look it up in commands
395
  cmd = argv.pop(1)
396
  if cmd in aliases:
397
    if cmd in commands:
398
      raise errors.ProgrammerError("Alias '%s' overrides an existing"
399
                                   " command" % cmd)
400

    
401
    if aliases[cmd] not in commands:
402
      raise errors.ProgrammerError("Alias '%s' maps to non-existing"
403
                                   " command '%s'" % (cmd, aliases[cmd]))
404

    
405
    cmd = aliases[cmd]
406

    
407
  func, nargs, parser_opts, usage, description = commands[cmd]
408
  parser = OptionParser(option_list=parser_opts + [_DRY_RUN_OPT],
409
                        description=description,
410
                        formatter=TitledHelpFormatter(),
411
                        usage="%%prog %s %s" % (cmd, usage))
412
  parser.disable_interspersed_args()
413
  options, args = parser.parse_args()
414
  if nargs is None:
415
    if len(args) != 0:
416
      ToStderr("Error: Command %s expects no arguments", cmd)
417
      return None, None, None
418
  elif nargs < 0 and len(args) != -nargs:
419
    ToStderr("Error: Command %s expects %d argument(s)", cmd, -nargs)
420
    return None, None, None
421
  elif nargs >= 0 and len(args) < nargs:
422
    ToStderr("Error: Command %s expects at least %d argument(s)", cmd, nargs)
423
    return None, None, None
424

    
425
  return func, options, args
426

    
427

    
428
def SplitNodeOption(value):
429
  """Splits the value of a --node option.
430

431
  """
432
  if value and ':' in value:
433
    return value.split(':', 1)
434
  else:
435
    return (value, None)
436

    
437

    
438
def UsesRPC(fn):
439
  def wrapper(*args, **kwargs):
440
    rpc.Init()
441
    try:
442
      return fn(*args, **kwargs)
443
    finally:
444
      rpc.Shutdown()
445
  return wrapper
446

    
447

    
448
def AskUser(text, choices=None):
449
  """Ask the user a question.
450

451
  @param text: the question to ask
452

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

458
  @return: one of the return values from the choices list; if input is
459
      not possible (i.e. not running with a tty, we return the last
460
      entry from the list
461

462
  """
463
  if choices is None:
464
    choices = [('y', True, 'Perform the operation'),
465
               ('n', False, 'Do not perform the operation')]
466
  if not choices or not isinstance(choices, list):
467
    raise errors.ProgrammerError("Invalid choices argument to AskUser")
468
  for entry in choices:
469
    if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
470
      raise errors.ProgrammerError("Invalid choices element to AskUser")
471

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

    
504

    
505
class JobSubmittedException(Exception):
506
  """Job was submitted, client should exit.
507

508
  This exception has one argument, the ID of the job that was
509
  submitted. The handler should print this ID.
510

511
  This is not an error, just a structured way to exit from clients.
512

513
  """
514

    
515

    
516
def SendJob(ops, cl=None):
517
  """Function to submit an opcode without waiting for the results.
518

519
  @type ops: list
520
  @param ops: list of opcodes
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
  job_id = cl.SubmitJob(ops)
530

    
531
  return job_id
532

    
533

    
534
def PollJob(job_id, cl=None, feedback_fn=None):
535
  """Function to poll for the result of a job.
536

537
  @type job_id: job identified
538
  @param job_id: the job to poll for results
539
  @type cl: luxi.Client
540
  @param cl: the luxi client to use for communicating with the master;
541
             if None, a new client will be created
542

543
  """
544
  if cl is None:
545
    cl = GetClient()
546

    
547
  prev_job_info = None
548
  prev_logmsg_serial = None
549

    
550
  while True:
551
    result = cl.WaitForJobChange(job_id, ["status"], prev_job_info,
552
                                 prev_logmsg_serial)
553
    if not result:
554
      # job not found, go away!
555
      raise errors.JobLost("Job with id %s lost" % job_id)
556

    
557
    # Split result, a tuple of (field values, log entries)
558
    (job_info, log_entries) = result
559
    (status, ) = job_info
560

    
561
    if log_entries:
562
      for log_entry in log_entries:
563
        (serial, timestamp, _, message) = log_entry
564
        if callable(feedback_fn):
565
          feedback_fn(log_entry[1:])
566
        else:
567
          encoded = utils.SafeEncode(message)
568
          ToStdout("%s %s", time.ctime(utils.MergeTime(timestamp)), encoded)
569
        prev_logmsg_serial = max(prev_logmsg_serial, serial)
570

    
571
    # TODO: Handle canceled and archived jobs
572
    elif status in (constants.JOB_STATUS_SUCCESS,
573
                    constants.JOB_STATUS_ERROR,
574
                    constants.JOB_STATUS_CANCELING,
575
                    constants.JOB_STATUS_CANCELED):
576
      break
577

    
578
    prev_job_info = job_info
579

    
580
  jobs = cl.QueryJobs([job_id], ["status", "opstatus", "opresult"])
581
  if not jobs:
582
    raise errors.JobLost("Job with id %s lost" % job_id)
583

    
584
  status, opstatus, result = jobs[0]
585
  if status == constants.JOB_STATUS_SUCCESS:
586
    return result
587
  elif status in (constants.JOB_STATUS_CANCELING,
588
                  constants.JOB_STATUS_CANCELED):
589
    raise errors.OpExecError("Job was canceled")
590
  else:
591
    has_ok = False
592
    for idx, (status, msg) in enumerate(zip(opstatus, result)):
593
      if status == constants.OP_STATUS_SUCCESS:
594
        has_ok = True
595
      elif status == constants.OP_STATUS_ERROR:
596
        if has_ok:
597
          raise errors.OpExecError("partial failure (opcode %d): %s" %
598
                                   (idx, msg))
599
        else:
600
          raise errors.OpExecError(str(msg))
601
    # default failure mode
602
    raise errors.OpExecError(result)
603

    
604

    
605
def SubmitOpCode(op, cl=None, feedback_fn=None):
606
  """Legacy function to submit an opcode.
607

608
  This is just a simple wrapper over the construction of the processor
609
  instance. It should be extended to better handle feedback and
610
  interaction functions.
611

612
  """
613
  if cl is None:
614
    cl = GetClient()
615

    
616
  job_id = SendJob([op], cl)
617

    
618
  op_results = PollJob(job_id, cl=cl, feedback_fn=feedback_fn)
619

    
620
  return op_results[0]
621

    
622

    
623
def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
624
  """Wrapper around SubmitOpCode or SendJob.
625

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

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

633
  """
634
  if opts and opts.dry_run:
635
    op.dry_run = opts.dry_run
636
  if opts and opts.submit_only:
637
    job_id = SendJob([op], cl=cl)
638
    raise JobSubmittedException(job_id)
639
  else:
640
    return SubmitOpCode(op, cl=cl, feedback_fn=feedback_fn)
641

    
642

    
643
def GetClient():
644
  # TODO: Cache object?
645
  try:
646
    client = luxi.Client()
647
  except luxi.NoMasterError:
648
    master, myself = ssconf.GetMasterAndMyself()
649
    if master != myself:
650
      raise errors.OpPrereqError("This is not the master node, please connect"
651
                                 " to node '%s' and rerun the command" %
652
                                 master)
653
    else:
654
      raise
655
  return client
656

    
657

    
658
def FormatError(err):
659
  """Return a formatted error message for a given error.
660

661
  This function takes an exception instance and returns a tuple
662
  consisting of two values: first, the recommended exit code, and
663
  second, a string describing the error message (not
664
  newline-terminated).
665

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

    
729

    
730
def GenericMain(commands, override=None, aliases=None):
731
  """Generic main function for all the gnt-* commands.
732

733
  Arguments:
734
    - commands: a dictionary with a special structure, see the design doc
735
                for command line handling.
736
    - override: if not None, we expect a dictionary with keys that will
737
                override command line options; this can be used to pass
738
                options from the scripts to generic functions
739
    - aliases: dictionary with command aliases {'alias': 'target, ...}
740

741
  """
742
  # save the program name and the entire command line for later logging
743
  if sys.argv:
744
    binary = os.path.basename(sys.argv[0]) or sys.argv[0]
745
    if len(sys.argv) >= 2:
746
      binary += " " + sys.argv[1]
747
      old_cmdline = " ".join(sys.argv[2:])
748
    else:
749
      old_cmdline = ""
750
  else:
751
    binary = "<unknown program>"
752
    old_cmdline = ""
753

    
754
  if aliases is None:
755
    aliases = {}
756

    
757
  try:
758
    func, options, args = _ParseArgs(sys.argv, commands, aliases)
759
  except errors.ParameterError, err:
760
    result, err_msg = FormatError(err)
761
    ToStderr(err_msg)
762
    return 1
763

    
764
  if func is None: # parse error
765
    return 1
766

    
767
  if override is not None:
768
    for key, val in override.iteritems():
769
      setattr(options, key, val)
770

    
771
  utils.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
772
                     stderr_logging=True, program=binary)
773

    
774
  if old_cmdline:
775
    logging.info("run with arguments '%s'", old_cmdline)
776
  else:
777
    logging.info("run with no arguments")
778

    
779
  try:
780
    result = func(options, args)
781
  except (errors.GenericError, luxi.ProtocolError,
782
          JobSubmittedException), err:
783
    result, err_msg = FormatError(err)
784
    logging.exception("Error during command processing")
785
    ToStderr(err_msg)
786

    
787
  return result
788

    
789

    
790
def GenerateTable(headers, fields, separator, data,
791
                  numfields=None, unitfields=None,
792
                  units=None):
793
  """Prints a table with headers and different fields.
794

795
  @type headers: dict
796
  @param headers: dictionary mapping field names to headers for
797
      the table
798
  @type fields: list
799
  @param fields: the field names corresponding to each row in
800
      the data field
801
  @param separator: the separator to be used; if this is None,
802
      the default 'smart' algorithm is used which computes optimal
803
      field width, otherwise just the separator is used between
804
      each field
805
  @type data: list
806
  @param data: a list of lists, each sublist being one row to be output
807
  @type numfields: list
808
  @param numfields: a list with the fields that hold numeric
809
      values and thus should be right-aligned
810
  @type unitfields: list
811
  @param unitfields: a list with the fields that hold numeric
812
      values that should be formatted with the units field
813
  @type units: string or None
814
  @param units: the units we should use for formatting, or None for
815
      automatic choice (human-readable for non-separator usage, otherwise
816
      megabytes); this is a one-letter string
817

818
  """
819
  if units is None:
820
    if separator:
821
      units = "m"
822
    else:
823
      units = "h"
824

    
825
  if numfields is None:
826
    numfields = []
827
  if unitfields is None:
828
    unitfields = []
829

    
830
  numfields = utils.FieldSet(*numfields)
831
  unitfields = utils.FieldSet(*unitfields)
832

    
833
  format_fields = []
834
  for field in fields:
835
    if headers and field not in headers:
836
      # TODO: handle better unknown fields (either revert to old
837
      # style of raising exception, or deal more intelligently with
838
      # variable fields)
839
      headers[field] = field
840
    if separator is not None:
841
      format_fields.append("%s")
842
    elif numfields.Matches(field):
843
      format_fields.append("%*s")
844
    else:
845
      format_fields.append("%-*s")
846

    
847
  if separator is None:
848
    mlens = [0 for name in fields]
849
    format = ' '.join(format_fields)
850
  else:
851
    format = separator.replace("%", "%%").join(format_fields)
852

    
853
  for row in data:
854
    if row is None:
855
      continue
856
    for idx, val in enumerate(row):
857
      if unitfields.Matches(fields[idx]):
858
        try:
859
          val = int(val)
860
        except ValueError:
861
          pass
862
        else:
863
          val = row[idx] = utils.FormatUnit(val, units)
864
      val = row[idx] = str(val)
865
      if separator is None:
866
        mlens[idx] = max(mlens[idx], len(val))
867

    
868
  result = []
869
  if headers:
870
    args = []
871
    for idx, name in enumerate(fields):
872
      hdr = headers[name]
873
      if separator is None:
874
        mlens[idx] = max(mlens[idx], len(hdr))
875
        args.append(mlens[idx])
876
      args.append(hdr)
877
    result.append(format % tuple(args))
878

    
879
  for line in data:
880
    args = []
881
    if line is None:
882
      line = ['-' for _ in fields]
883
    for idx in xrange(len(fields)):
884
      if separator is None:
885
        args.append(mlens[idx])
886
      args.append(line[idx])
887
    result.append(format % tuple(args))
888

    
889
  return result
890

    
891

    
892
def FormatTimestamp(ts):
893
  """Formats a given timestamp.
894

895
  @type ts: timestamp
896
  @param ts: a timeval-type timestamp, a tuple of seconds and microseconds
897

898
  @rtype: string
899
  @return: a string with the formatted timestamp
900

901
  """
902
  if not isinstance (ts, (tuple, list)) or len(ts) != 2:
903
    return '?'
904
  sec, usec = ts
905
  return time.strftime("%F %T", time.localtime(sec)) + ".%06d" % usec
906

    
907

    
908
def ParseTimespec(value):
909
  """Parse a time specification.
910

911
  The following suffixed will be recognized:
912

913
    - s: seconds
914
    - m: minutes
915
    - h: hours
916
    - d: day
917
    - w: weeks
918

919
  Without any suffix, the value will be taken to be in seconds.
920

921
  """
922
  value = str(value)
923
  if not value:
924
    raise errors.OpPrereqError("Empty time specification passed")
925
  suffix_map = {
926
    's': 1,
927
    'm': 60,
928
    'h': 3600,
929
    'd': 86400,
930
    'w': 604800,
931
    }
932
  if value[-1] not in suffix_map:
933
    try:
934
      value = int(value)
935
    except ValueError:
936
      raise errors.OpPrereqError("Invalid time specification '%s'" % value)
937
  else:
938
    multiplier = suffix_map[value[-1]]
939
    value = value[:-1]
940
    if not value: # no data left after stripping the suffix
941
      raise errors.OpPrereqError("Invalid time specification (only"
942
                                 " suffix passed)")
943
    try:
944
      value = int(value) * multiplier
945
    except ValueError:
946
      raise errors.OpPrereqError("Invalid time specification '%s'" % value)
947
  return value
948

    
949

    
950
def GetOnlineNodes(nodes, cl=None, nowarn=False):
951
  """Returns the names of online nodes.
952

953
  This function will also log a warning on stderr with the names of
954
  the online nodes.
955

956
  @param nodes: if not empty, use only this subset of nodes (minus the
957
      offline ones)
958
  @param cl: if not None, luxi client to use
959
  @type nowarn: boolean
960
  @param nowarn: by default, this function will output a note with the
961
      offline nodes that are skipped; if this parameter is True the
962
      note is not displayed
963

964
  """
965
  if cl is None:
966
    cl = GetClient()
967

    
968
  result = cl.QueryNodes(names=nodes, fields=["name", "offline"],
969
                         use_locking=False)
970
  offline = [row[0] for row in result if row[1]]
971
  if offline and not nowarn:
972
    ToStderr("Note: skipping offline node(s): %s" % ", ".join(offline))
973
  return [row[0] for row in result if not row[1]]
974

    
975

    
976
def _ToStream(stream, txt, *args):
977
  """Write a message to a stream, bypassing the logging system
978

979
  @type stream: file object
980
  @param stream: the file to which we should write
981
  @type txt: str
982
  @param txt: the message
983

984
  """
985
  if args:
986
    args = tuple(args)
987
    stream.write(txt % args)
988
  else:
989
    stream.write(txt)
990
  stream.write('\n')
991
  stream.flush()
992

    
993

    
994
def ToStdout(txt, *args):
995
  """Write a message to stdout only, bypassing the logging system
996

997
  This is just a wrapper over _ToStream.
998

999
  @type txt: str
1000
  @param txt: the message
1001

1002
  """
1003
  _ToStream(sys.stdout, txt, *args)
1004

    
1005

    
1006
def ToStderr(txt, *args):
1007
  """Write a message to stderr only, bypassing the logging system
1008

1009
  This is just a wrapper over _ToStream.
1010

1011
  @type txt: str
1012
  @param txt: the message
1013

1014
  """
1015
  _ToStream(sys.stderr, txt, *args)
1016

    
1017

    
1018
class JobExecutor(object):
1019
  """Class which manages the submission and execution of multiple jobs.
1020

1021
  Note that instances of this class should not be reused between
1022
  GetResults() calls.
1023

1024
  """
1025
  def __init__(self, cl=None, verbose=True):
1026
    self.queue = []
1027
    if cl is None:
1028
      cl = GetClient()
1029
    self.cl = cl
1030
    self.verbose = verbose
1031
    self.jobs = []
1032

    
1033
  def QueueJob(self, name, *ops):
1034
    """Record a job for later submit.
1035

1036
    @type name: string
1037
    @param name: a description of the job, will be used in WaitJobSet
1038
    """
1039
    self.queue.append((name, ops))
1040

    
1041
  def SubmitPending(self):
1042
    """Submit all pending jobs.
1043

1044
    """
1045
    results = self.cl.SubmitManyJobs([row[1] for row in self.queue])
1046
    for ((status, data), (name, _)) in zip(results, self.queue):
1047
      self.jobs.append((status, data, name))
1048

    
1049
  def GetResults(self):
1050
    """Wait for and return the results of all jobs.
1051

1052
    @rtype: list
1053
    @return: list of tuples (success, job results), in the same order
1054
        as the submitted jobs; if a job has failed, instead of the result
1055
        there will be the error message
1056

1057
    """
1058
    if not self.jobs:
1059
      self.SubmitPending()
1060
    results = []
1061
    if self.verbose:
1062
      ok_jobs = [row[1] for row in self.jobs if row[0]]
1063
      if ok_jobs:
1064
        ToStdout("Submitted jobs %s", ", ".join(ok_jobs))
1065
    for submit_status, jid, name in self.jobs:
1066
      if not submit_status:
1067
        ToStderr("Failed to submit job for %s: %s", name, jid)
1068
        results.append((False, jid))
1069
        continue
1070
      if self.verbose:
1071
        ToStdout("Waiting for job %s for %s...", jid, name)
1072
      try:
1073
        job_result = PollJob(jid, cl=self.cl)
1074
        success = True
1075
      except (errors.GenericError, luxi.ProtocolError), err:
1076
        _, job_result = FormatError(err)
1077
        success = False
1078
        # the error message will always be shown, verbose or not
1079
        ToStderr("Job %s for %s has failed: %s", jid, name, job_result)
1080

    
1081
      results.append((success, job_result))
1082
    return results
1083

    
1084
  def WaitOrShow(self, wait):
1085
    """Wait for job results or only print the job IDs.
1086

1087
    @type wait: boolean
1088
    @param wait: whether to wait or not
1089

1090
    """
1091
    if wait:
1092
      return self.GetResults()
1093
    else:
1094
      if not self.jobs:
1095
        self.SubmitPending()
1096
      for status, result, name in self.jobs:
1097
        if status:
1098
          ToStdout("%s: %s", result, name)
1099
        else:
1100
          ToStderr("Failure for %s: %s", name, result)