Statistics
| Branch: | Tag: | Revision:

root / lib / cli.py @ b6e841a8

History | View | Annotate | Download (44 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, TitledHelpFormatter,
42
                      Option, OptionValueError)
43

    
44

    
45
__all__ = [
46
  # Command line options
47
  "ALL_OPT",
48
  "BACKEND_OPT",
49
  "CLEANUP_OPT",
50
  "CONFIRM_OPT",
51
  "DEBUG_OPT",
52
  "DEBUG_SIMERR_OPT",
53
  "DISKIDX_OPT",
54
  "DISK_OPT",
55
  "DISK_TEMPLATE_OPT",
56
  "FIELDS_OPT",
57
  "FILESTORE_DIR_OPT",
58
  "FILESTORE_DRIVER_OPT",
59
  "HVLIST_OPT",
60
  "HVOPTS_OPT",
61
  "HYPERVISOR_OPT",
62
  "IALLOCATOR_OPT",
63
  "IGNORE_CONSIST_OPT",
64
  "IGNORE_FAILURES_OPT",
65
  "FORCE_OPT",
66
  "NET_OPT",
67
  "NODE_LIST_OPT",
68
  "NODE_PLACEMENT_OPT",
69
  "NOHDR_OPT",
70
  "NOIPCHECK_OPT",
71
  "NONICS_OPT",
72
  "NONLIVE_OPT",
73
  "NOSTART_OPT",
74
  "NWSYNC_OPT",
75
  "OS_OPT",
76
  "OS_SIZE_OPT",
77
  "SELECT_OS_OPT",
78
  "SEP_OPT",
79
  "SHOWCMD_OPT",
80
  "SINGLE_NODE_OPT",
81
  "SUBMIT_OPT",
82
  "STATIC_OPT",
83
  "SYNC_OPT",
84
  "TAG_SRC_OPT",
85
  "USEUNITS_OPT",
86
  "VERBOSE_OPT",
87
  # Generic functions for CLI programs
88
  "GenericMain",
89
  "GetClient",
90
  "GetOnlineNodes",
91
  "JobExecutor",
92
  "JobSubmittedException",
93
  "ParseTimespec",
94
  "SubmitOpCode",
95
  "SubmitOrSend",
96
  "UsesRPC",
97
  # Formatting functions
98
  "ToStderr", "ToStdout",
99
  "FormatError",
100
  "GenerateTable",
101
  "AskUser",
102
  "FormatTimestamp",
103
  # Tags functions
104
  "ListTags",
105
  "AddTags",
106
  "RemoveTags",
107
  # command line options support infrastructure
108
  "ARGS_MANY_INSTANCES",
109
  "ARGS_MANY_NODES",
110
  "ARGS_NONE",
111
  "ARGS_ONE_INSTANCE",
112
  "ARGS_ONE_NODE",
113
  "ArgChoice",
114
  "ArgCommand",
115
  "ArgFile",
116
  "ArgHost",
117
  "ArgInstance",
118
  "ArgJobId",
119
  "ArgNode",
120
  "ArgSuggest",
121
  "ArgUnknown",
122
  "OPT_COMPL_INST_ADD_NODES",
123
  "OPT_COMPL_MANY_NODES",
124
  "OPT_COMPL_ONE_IALLOCATOR",
125
  "OPT_COMPL_ONE_INSTANCE",
126
  "OPT_COMPL_ONE_NODE",
127
  "OPT_COMPL_ONE_OS",
128
  "cli_option",
129
  "SplitNodeOption",
130
  ]
131

    
132
NO_PREFIX = "no_"
133
UN_PREFIX = "-"
134

    
135

    
136
class _Argument:
137
  def __init__(self, min=0, max=None):
138
    self.min = min
139
    self.max = max
140

    
141
  def __repr__(self):
142
    return ("<%s min=%s max=%s>" %
143
            (self.__class__.__name__, self.min, self.max))
144

    
145

    
146
class ArgSuggest(_Argument):
147
  """Suggesting argument.
148

149
  Value can be any of the ones passed to the constructor.
150

151
  """
152
  def __init__(self, min=0, max=None, choices=None):
153
    _Argument.__init__(self, min=min, max=max)
154
    self.choices = choices
155

    
156
  def __repr__(self):
157
    return ("<%s min=%s max=%s choices=%r>" %
158
            (self.__class__.__name__, self.min, self.max, self.choices))
159

    
160

    
161
class ArgChoice(ArgSuggest):
162
  """Choice argument.
163

164
  Value can be any of the ones passed to the constructor. Like L{ArgSuggest},
165
  but value must be one of the choices.
166

167
  """
168

    
169

    
170
class ArgUnknown(_Argument):
171
  """Unknown argument to program (e.g. determined at runtime).
172

173
  """
174

    
175

    
176
class ArgInstance(_Argument):
177
  """Instances argument.
178

179
  """
180

    
181

    
182
class ArgNode(_Argument):
183
  """Node argument.
184

185
  """
186

    
187
class ArgJobId(_Argument):
188
  """Job ID argument.
189

190
  """
191

    
192

    
193
class ArgFile(_Argument):
194
  """File path argument.
195

196
  """
197

    
198

    
199
class ArgCommand(_Argument):
200
  """Command argument.
201

202
  """
203

    
204

    
205
class ArgHost(_Argument):
206
  """Host argument.
207

208
  """
209

    
210

    
211
ARGS_NONE = []
212
ARGS_MANY_INSTANCES = [ArgInstance()]
213
ARGS_MANY_NODES = [ArgNode()]
214
ARGS_ONE_INSTANCE = [ArgInstance(min=1, max=1)]
215
ARGS_ONE_NODE = [ArgNode(min=1, max=1)]
216

    
217

    
218

    
219
def _ExtractTagsObject(opts, args):
220
  """Extract the tag type object.
221

222
  Note that this function will modify its args parameter.
223

224
  """
225
  if not hasattr(opts, "tag_type"):
226
    raise errors.ProgrammerError("tag_type not passed to _ExtractTagsObject")
227
  kind = opts.tag_type
228
  if kind == constants.TAG_CLUSTER:
229
    retval = kind, kind
230
  elif kind == constants.TAG_NODE or kind == constants.TAG_INSTANCE:
231
    if not args:
232
      raise errors.OpPrereqError("no arguments passed to the command")
233
    name = args.pop(0)
234
    retval = kind, name
235
  else:
236
    raise errors.ProgrammerError("Unhandled tag type '%s'" % kind)
237
  return retval
238

    
239

    
240
def _ExtendTags(opts, args):
241
  """Extend the args if a source file has been given.
242

243
  This function will extend the tags with the contents of the file
244
  passed in the 'tags_source' attribute of the opts parameter. A file
245
  named '-' will be replaced by stdin.
246

247
  """
248
  fname = opts.tags_source
249
  if fname is None:
250
    return
251
  if fname == "-":
252
    new_fh = sys.stdin
253
  else:
254
    new_fh = open(fname, "r")
255
  new_data = []
256
  try:
257
    # we don't use the nice 'new_data = [line.strip() for line in fh]'
258
    # because of python bug 1633941
259
    while True:
260
      line = new_fh.readline()
261
      if not line:
262
        break
263
      new_data.append(line.strip())
264
  finally:
265
    new_fh.close()
266
  args.extend(new_data)
267

    
268

    
269
def ListTags(opts, args):
270
  """List the tags on a given object.
271

272
  This is a generic implementation that knows how to deal with all
273
  three cases of tag objects (cluster, node, instance). The opts
274
  argument is expected to contain a tag_type field denoting what
275
  object type we work on.
276

277
  """
278
  kind, name = _ExtractTagsObject(opts, args)
279
  op = opcodes.OpGetTags(kind=kind, name=name)
280
  result = SubmitOpCode(op)
281
  result = list(result)
282
  result.sort()
283
  for tag in result:
284
    ToStdout(tag)
285

    
286

    
287
def AddTags(opts, args):
288
  """Add tags on a given object.
289

290
  This is a generic implementation that knows how to deal with all
291
  three cases of tag objects (cluster, node, instance). The opts
292
  argument is expected to contain a tag_type field denoting what
293
  object type we work on.
294

295
  """
296
  kind, name = _ExtractTagsObject(opts, args)
297
  _ExtendTags(opts, args)
298
  if not args:
299
    raise errors.OpPrereqError("No tags to be added")
300
  op = opcodes.OpAddTags(kind=kind, name=name, tags=args)
301
  SubmitOpCode(op)
302

    
303

    
304
def RemoveTags(opts, args):
305
  """Remove tags from a given object.
306

307
  This is a generic implementation that knows how to deal with all
308
  three cases of tag objects (cluster, node, instance). The opts
309
  argument is expected to contain a tag_type field denoting what
310
  object type we work on.
311

312
  """
313
  kind, name = _ExtractTagsObject(opts, args)
314
  _ExtendTags(opts, args)
315
  if not args:
316
    raise errors.OpPrereqError("No tags to be removed")
317
  op = opcodes.OpDelTags(kind=kind, name=name, tags=args)
318
  SubmitOpCode(op)
319

    
320

    
321
def check_unit(option, opt, value):
322
  """OptParsers custom converter for units.
323

324
  """
325
  try:
326
    return utils.ParseUnit(value)
327
  except errors.UnitParseError, err:
328
    raise OptionValueError("option %s: %s" % (opt, err))
329

    
330

    
331
def _SplitKeyVal(opt, data):
332
  """Convert a KeyVal string into a dict.
333

334
  This function will convert a key=val[,...] string into a dict. Empty
335
  values will be converted specially: keys which have the prefix 'no_'
336
  will have the value=False and the prefix stripped, the others will
337
  have value=True.
338

339
  @type opt: string
340
  @param opt: a string holding the option name for which we process the
341
      data, used in building error messages
342
  @type data: string
343
  @param data: a string of the format key=val,key=val,...
344
  @rtype: dict
345
  @return: {key=val, key=val}
346
  @raises errors.ParameterError: if there are duplicate keys
347

348
  """
349
  kv_dict = {}
350
  if data:
351
    for elem in data.split(","):
352
      if "=" in elem:
353
        key, val = elem.split("=", 1)
354
      else:
355
        if elem.startswith(NO_PREFIX):
356
          key, val = elem[len(NO_PREFIX):], False
357
        elif elem.startswith(UN_PREFIX):
358
          key, val = elem[len(UN_PREFIX):], None
359
        else:
360
          key, val = elem, True
361
      if key in kv_dict:
362
        raise errors.ParameterError("Duplicate key '%s' in option %s" %
363
                                    (key, opt))
364
      kv_dict[key] = val
365
  return kv_dict
366

    
367

    
368
def check_ident_key_val(option, opt, value):
369
  """Custom parser for ident:key=val,key=val options.
370

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

374
  """
375
  if ":" not in value:
376
    ident, rest = value, ''
377
  else:
378
    ident, rest = value.split(":", 1)
379

    
380
  if ident.startswith(NO_PREFIX):
381
    if rest:
382
      msg = "Cannot pass options when removing parameter groups: %s" % value
383
      raise errors.ParameterError(msg)
384
    retval = (ident[len(NO_PREFIX):], False)
385
  elif ident.startswith(UN_PREFIX):
386
    if rest:
387
      msg = "Cannot pass options when removing parameter groups: %s" % value
388
      raise errors.ParameterError(msg)
389
    retval = (ident[len(UN_PREFIX):], None)
390
  else:
391
    kv_dict = _SplitKeyVal(opt, rest)
392
    retval = (ident, kv_dict)
393
  return retval
394

    
395

    
396
def check_key_val(option, opt, value):
397
  """Custom parser class for key=val,key=val options.
398

399
  This will store the parsed values as a dict {key: val}.
400

401
  """
402
  return _SplitKeyVal(opt, value)
403

    
404

    
405
# completion_suggestion is normally a list. Using numeric values not evaluating
406
# to False for dynamic completion.
407
(OPT_COMPL_MANY_NODES,
408
 OPT_COMPL_ONE_NODE,
409
 OPT_COMPL_ONE_INSTANCE,
410
 OPT_COMPL_ONE_OS,
411
 OPT_COMPL_ONE_IALLOCATOR,
412
 OPT_COMPL_INST_ADD_NODES) = range(100, 106)
413

    
414
OPT_COMPL_ALL = frozenset([
415
  OPT_COMPL_MANY_NODES,
416
  OPT_COMPL_ONE_NODE,
417
  OPT_COMPL_ONE_INSTANCE,
418
  OPT_COMPL_ONE_OS,
419
  OPT_COMPL_ONE_IALLOCATOR,
420
  OPT_COMPL_INST_ADD_NODES,
421
  ])
422

    
423

    
424
class CliOption(Option):
425
  """Custom option class for optparse.
426

427
  """
428
  ATTRS = Option.ATTRS + [
429
    "completion_suggest",
430
    ]
431
  TYPES = Option.TYPES + (
432
    "identkeyval",
433
    "keyval",
434
    "unit",
435
    )
436
  TYPE_CHECKER = Option.TYPE_CHECKER.copy()
437
  TYPE_CHECKER["identkeyval"] = check_ident_key_val
438
  TYPE_CHECKER["keyval"] = check_key_val
439
  TYPE_CHECKER["unit"] = check_unit
440

    
441

    
442
# optparse.py sets make_option, so we do it for our own option class, too
443
cli_option = CliOption
444

    
445

    
446
DEBUG_OPT = cli_option("-d", "--debug", default=False,
447
                       action="store_true",
448
                       help="Turn debugging on")
449

    
450
NOHDR_OPT = cli_option("--no-headers", default=False,
451
                       action="store_true", dest="no_headers",
452
                       help="Don't display column headers")
453

    
454
SEP_OPT = cli_option("--separator", default=None,
455
                     action="store", dest="separator",
456
                     help=("Separator between output fields"
457
                           " (defaults to one space)"))
458

    
459
USEUNITS_OPT = cli_option("--units", default=None,
460
                          dest="units", choices=('h', 'm', 'g', 't'),
461
                          help="Specify units for output (one of hmgt)")
462

    
463
FIELDS_OPT = cli_option("-o", "--output", dest="output", action="store",
464
                        type="string", metavar="FIELDS",
465
                        help="Comma separated list of output fields")
466

    
467
FORCE_OPT = cli_option("-f", "--force", dest="force", action="store_true",
468
                       default=False, help="Force the operation")
469

    
470
CONFIRM_OPT = cli_option("--yes", dest="confirm", action="store_true",
471
                         default=False, help="Do not require confirmation")
472

    
473
TAG_SRC_OPT = cli_option("--from", dest="tags_source",
474
                         default=None, help="File with tag names")
475

    
476
SUBMIT_OPT = cli_option("--submit", dest="submit_only",
477
                        default=False, action="store_true",
478
                        help=("Submit the job and return the job ID, but"
479
                              " don't wait for the job to finish"))
480

    
481
SYNC_OPT = cli_option("--sync", dest="do_locking",
482
                      default=False, action="store_true",
483
                      help=("Grab locks while doing the queries"
484
                            " in order to ensure more consistent results"))
485

    
486
_DRY_RUN_OPT = cli_option("--dry-run", default=False,
487
                          action="store_true",
488
                          help=("Do not execute the operation, just run the"
489
                                " check steps and verify it it could be"
490
                                " executed"))
491

    
492
VERBOSE_OPT = cli_option("-v", "--verbose", default=False,
493
                         action="store_true",
494
                         help="Increase the verbosity of the operation")
495

    
496
DEBUG_SIMERR_OPT = cli_option("--debug-simulate-errors", default=False,
497
                              action="store_true", dest="simulate_errors",
498
                              help="Debugging option that makes the operation"
499
                              " treat most runtime checks as failed")
500

    
501
NWSYNC_OPT = cli_option("--no-wait-for-sync", dest="wait_for_sync",
502
                        default=True, action="store_false",
503
                        help="Don't wait for sync (DANGEROUS!)")
504

    
505
DISK_TEMPLATE_OPT = cli_option("-t", "--disk-template", dest="disk_template",
506
                               help="Custom disk setup (diskless, file,"
507
                               " plain or drbd)",
508
                               default=None, metavar="TEMPL",
509
                               choices=list(constants.DISK_TEMPLATES))
510

    
511
NONICS_OPT = cli_option("--no-nics", default=False, action="store_true",
512
                        help="Do not create any network cards for"
513
                        " the instance")
514

    
515
FILESTORE_DIR_OPT = cli_option("--file-storage-dir", dest="file_storage_dir",
516
                               help="Relative path under default cluster-wide"
517
                               " file storage dir to store file-based disks",
518
                               default=None, metavar="<DIR>")
519

    
520
FILESTORE_DRIVER_OPT = cli_option("--file-driver", dest="file_driver",
521
                                  help="Driver to use for image files",
522
                                  default="loop", metavar="<DRIVER>",
523
                                  choices=list(constants.FILE_DRIVER))
524

    
525
IALLOCATOR_OPT = cli_option("-I", "--iallocator", metavar="<NAME>",
526
                            help="Select nodes for the instance automatically"
527
                            " using the <NAME> iallocator plugin",
528
                            default=None, type="string",
529
                            completion_suggest=OPT_COMPL_ONE_IALLOCATOR)
530

    
531
OS_OPT = cli_option("-o", "--os-type", dest="os", help="What OS to run",
532
                    metavar="<os>",
533
                    completion_suggest=OPT_COMPL_ONE_OS)
534

    
535
BACKEND_OPT = cli_option("-B", "--backend-parameters", dest="beparams",
536
                         type="keyval", default={},
537
                         help="Backend parameters")
538

    
539
HVOPTS_OPT =  cli_option("-H", "--hypervisor-parameters", type="keyval",
540
                         default={}, dest="hvparams",
541
                         help="Hypervisor parameters")
542

    
543
HYPERVISOR_OPT = cli_option("-H", "--hypervisor-parameters", dest="hypervisor",
544
                            help="Hypervisor and hypervisor options, in the"
545
                            " format hypervisor:option=value,option=value,...",
546
                            default=None, type="identkeyval")
547

    
548
HVLIST_OPT = cli_option("-H", "--hypervisor-parameters", dest="hvparams",
549
                        help="Hypervisor and hypervisor options, in the"
550
                        " format hypervisor:option=value,option=value,...",
551
                        default=[], action="append", type="identkeyval")
552

    
553
NOIPCHECK_OPT = cli_option("--no-ip-check", dest="ip_check", default=True,
554
                           action="store_false",
555
                           help="Don't check that the instance's IP"
556
                           " is alive")
557

    
558
NET_OPT = cli_option("--net",
559
                     help="NIC parameters", default=[],
560
                     dest="nics", action="append", type="identkeyval")
561

    
562
DISK_OPT = cli_option("--disk", help="Disk parameters", default=[],
563
                      dest="disks", action="append", type="identkeyval")
564

    
565
DISKIDX_OPT = cli_option("--disks", dest="disks", default=None,
566
                         help="Comma-separated list of disks"
567
                         " indices to act on (e.g. 0,2) (optional,"
568
                         " defaults to all disks)")
569

    
570
OS_SIZE_OPT = cli_option("-s", "--os-size", dest="sd_size",
571
                         help="Enforces a single-disk configuration using the"
572
                         " given disk size, in MiB unless a suffix is used",
573
                         default=None, type="unit", metavar="<size>")
574

    
575
IGNORE_CONSIST_OPT = cli_option("--ignore-consistency",
576
                                dest="ignore_consistency",
577
                                action="store_true", default=False,
578
                                help="Ignore the consistency of the disks on"
579
                                " the secondary")
580

    
581
NONLIVE_OPT = cli_option("--non-live", dest="live",
582
                         default=True, action="store_false",
583
                         help="Do a non-live migration (this usually means"
584
                         " freeze the instance, save the state, transfer and"
585
                         " only then resume running on the secondary node)")
586

    
587
NODE_PLACEMENT_OPT = cli_option("-n", "--node", dest="node",
588
                                help="Target node and optional secondary node",
589
                                metavar="<pnode>[:<snode>]",
590
                                completion_suggest=OPT_COMPL_INST_ADD_NODES)
591

    
592
NODE_LIST_OPT = cli_option("-n", "--node", dest="nodes", default=[],
593
                           action="append", metavar="<node>",
594
                           help="Use only this node (can be used multiple"
595
                           " times, if not given defaults to all nodes)",
596
                           completion_suggest=OPT_COMPL_ONE_NODE)
597

    
598
SINGLE_NODE_OPT = cli_option("-n", "--node", dest="node", help="Target node",
599
                             metavar="<node>",
600
                             completion_suggest=OPT_COMPL_ONE_NODE)
601

    
602
NOSTART_OPT = cli_option("--no-start", dest="start", default=True,
603
                         action="store_false",
604
                         help="Don't start the instance after creation")
605

    
606
SHOWCMD_OPT = cli_option("--show-cmd", dest="show_command",
607
                         action="store_true", default=False,
608
                         help="Show command instead of executing it")
609

    
610
CLEANUP_OPT = cli_option("--cleanup", dest="cleanup",
611
                         default=False, action="store_true",
612
                         help="Instead of performing the migration, try to"
613
                         " recover from a failed cleanup. This is safe"
614
                         " to run even if the instance is healthy, but it"
615
                         " will create extra replication traffic and "
616
                         " disrupt briefly the replication (like during the"
617
                         " migration")
618

    
619
STATIC_OPT = cli_option("-s", "--static", dest="static",
620
                        action="store_true", default=False,
621
                        help="Only show configuration data, not runtime data")
622

    
623
ALL_OPT = cli_option("--all", dest="show_all",
624
                     default=False, action="store_true",
625
                     help="Show info on all instances on the cluster."
626
                     " This can take a long time to run, use wisely")
627

    
628
SELECT_OS_OPT = cli_option("--select-os", dest="select_os",
629
                           action="store_true", default=False,
630
                           help="Interactive OS reinstall, lists available"
631
                           " OS templates for selection")
632

    
633
IGNORE_FAILURES_OPT = cli_option("--ignore-failures", dest="ignore_failures",
634
                                 action="store_true", default=False,
635
                                 help="Remove the instance from the cluster"
636
                                 " configuration even if there are failures"
637
                                 " during the removal process")
638

    
639

    
640
def _ParseArgs(argv, commands, aliases):
641
  """Parser for the command line arguments.
642

643
  This function parses the arguments and returns the function which
644
  must be executed together with its (modified) arguments.
645

646
  @param argv: the command line
647
  @param commands: dictionary with special contents, see the design
648
      doc for cmdline handling
649
  @param aliases: dictionary with command aliases {'alias': 'target, ...}
650

651
  """
652
  if len(argv) == 0:
653
    binary = "<command>"
654
  else:
655
    binary = argv[0].split("/")[-1]
656

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

    
663
  if len(argv) < 2 or not (argv[1] in commands or
664
                           argv[1] in aliases):
665
    # let's do a nice thing
666
    sortedcmds = commands.keys()
667
    sortedcmds.sort()
668

    
669
    ToStdout("Usage: %s {command} [options...] [argument...]", binary)
670
    ToStdout("%s <command> --help to see details, or man %s", binary, binary)
671
    ToStdout("")
672

    
673
    # compute the max line length for cmd + usage
674
    mlen = max([len(" %s" % cmd) for cmd in commands])
675
    mlen = min(60, mlen) # should not get here...
676

    
677
    # and format a nice command list
678
    ToStdout("Commands:")
679
    for cmd in sortedcmds:
680
      cmdstr = " %s" % (cmd,)
681
      help_text = commands[cmd][4]
682
      help_lines = textwrap.wrap(help_text, 79 - 3 - mlen)
683
      ToStdout("%-*s - %s", mlen, cmdstr, help_lines.pop(0))
684
      for line in help_lines:
685
        ToStdout("%-*s   %s", mlen, "", line)
686

    
687
    ToStdout("")
688

    
689
    return None, None, None
690

    
691
  # get command, unalias it, and look it up in commands
692
  cmd = argv.pop(1)
693
  if cmd in aliases:
694
    if cmd in commands:
695
      raise errors.ProgrammerError("Alias '%s' overrides an existing"
696
                                   " command" % cmd)
697

    
698
    if aliases[cmd] not in commands:
699
      raise errors.ProgrammerError("Alias '%s' maps to non-existing"
700
                                   " command '%s'" % (cmd, aliases[cmd]))
701

    
702
    cmd = aliases[cmd]
703

    
704
  func, args_def, parser_opts, usage, description = commands[cmd]
705
  parser = OptionParser(option_list=parser_opts + [_DRY_RUN_OPT],
706
                        description=description,
707
                        formatter=TitledHelpFormatter(),
708
                        usage="%%prog %s %s" % (cmd, usage))
709
  parser.disable_interspersed_args()
710
  options, args = parser.parse_args()
711

    
712
  if not _CheckArguments(cmd, args_def, args):
713
    return None, None, None
714

    
715
  return func, options, args
716

    
717

    
718
def _CheckArguments(cmd, args_def, args):
719
  """Verifies the arguments using the argument definition.
720

721
  Algorithm:
722

723
    1. Abort with error if values specified by user but none expected.
724

725
    1. For each argument in definition
726

727
      1. Keep running count of minimum number of values (min_count)
728
      1. Keep running count of maximum number of values (max_count)
729
      1. If it has an unlimited number of values
730

731
        1. Abort with error if it's not the last argument in the definition
732

733
    1. If last argument has limited number of values
734

735
      1. Abort with error if number of values doesn't match or is too large
736

737
    1. Abort with error if user didn't pass enough values (min_count)
738

739
  """
740
  if args and not args_def:
741
    ToStderr("Error: Command %s expects no arguments", cmd)
742
    return False
743

    
744
  min_count = None
745
  max_count = None
746
  check_max = None
747

    
748
  last_idx = len(args_def) - 1
749

    
750
  for idx, arg in enumerate(args_def):
751
    if min_count is None:
752
      min_count = arg.min
753
    elif arg.min is not None:
754
      min_count += arg.min
755

    
756
    if max_count is None:
757
      max_count = arg.max
758
    elif arg.max is not None:
759
      max_count += arg.max
760

    
761
    if idx == last_idx:
762
      check_max = (arg.max is not None)
763

    
764
    elif arg.max is None:
765
      raise errors.ProgrammerError("Only the last argument can have max=None")
766

    
767
  if check_max:
768
    # Command with exact number of arguments
769
    if (min_count is not None and max_count is not None and
770
        min_count == max_count and len(args) != min_count):
771
      ToStderr("Error: Command %s expects %d argument(s)", cmd, min_count)
772
      return False
773

    
774
    # Command with limited number of arguments
775
    if max_count is not None and len(args) > max_count:
776
      ToStderr("Error: Command %s expects only %d argument(s)",
777
               cmd, max_count)
778
      return False
779

    
780
  # Command with some required arguments
781
  if min_count is not None and len(args) < min_count:
782
    ToStderr("Error: Command %s expects at least %d argument(s)",
783
             cmd, min_count)
784
    return False
785

    
786
  return True
787

    
788

    
789
def SplitNodeOption(value):
790
  """Splits the value of a --node option.
791

792
  """
793
  if value and ':' in value:
794
    return value.split(':', 1)
795
  else:
796
    return (value, None)
797

    
798

    
799
def UsesRPC(fn):
800
  def wrapper(*args, **kwargs):
801
    rpc.Init()
802
    try:
803
      return fn(*args, **kwargs)
804
    finally:
805
      rpc.Shutdown()
806
  return wrapper
807

    
808

    
809
def AskUser(text, choices=None):
810
  """Ask the user a question.
811

812
  @param text: the question to ask
813

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

819
  @return: one of the return values from the choices list; if input is
820
      not possible (i.e. not running with a tty, we return the last
821
      entry from the list
822

823
  """
824
  if choices is None:
825
    choices = [('y', True, 'Perform the operation'),
826
               ('n', False, 'Do not perform the operation')]
827
  if not choices or not isinstance(choices, list):
828
    raise errors.ProgrammerError("Invalid choices argument to AskUser")
829
  for entry in choices:
830
    if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
831
      raise errors.ProgrammerError("Invalid choices element to AskUser")
832

    
833
  answer = choices[-1][1]
834
  new_text = []
835
  for line in text.splitlines():
836
    new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
837
  text = "\n".join(new_text)
838
  try:
839
    f = file("/dev/tty", "a+")
840
  except IOError:
841
    return answer
842
  try:
843
    chars = [entry[0] for entry in choices]
844
    chars[-1] = "[%s]" % chars[-1]
845
    chars.append('?')
846
    maps = dict([(entry[0], entry[1]) for entry in choices])
847
    while True:
848
      f.write(text)
849
      f.write('\n')
850
      f.write("/".join(chars))
851
      f.write(": ")
852
      line = f.readline(2).strip().lower()
853
      if line in maps:
854
        answer = maps[line]
855
        break
856
      elif line == '?':
857
        for entry in choices:
858
          f.write(" %s - %s\n" % (entry[0], entry[2]))
859
        f.write("\n")
860
        continue
861
  finally:
862
    f.close()
863
  return answer
864

    
865

    
866
class JobSubmittedException(Exception):
867
  """Job was submitted, client should exit.
868

869
  This exception has one argument, the ID of the job that was
870
  submitted. The handler should print this ID.
871

872
  This is not an error, just a structured way to exit from clients.
873

874
  """
875

    
876

    
877
def SendJob(ops, cl=None):
878
  """Function to submit an opcode without waiting for the results.
879

880
  @type ops: list
881
  @param ops: list of opcodes
882
  @type cl: luxi.Client
883
  @param cl: the luxi client to use for communicating with the master;
884
             if None, a new client will be created
885

886
  """
887
  if cl is None:
888
    cl = GetClient()
889

    
890
  job_id = cl.SubmitJob(ops)
891

    
892
  return job_id
893

    
894

    
895
def PollJob(job_id, cl=None, feedback_fn=None):
896
  """Function to poll for the result of a job.
897

898
  @type job_id: job identified
899
  @param job_id: the job to poll for results
900
  @type cl: luxi.Client
901
  @param cl: the luxi client to use for communicating with the master;
902
             if None, a new client will be created
903

904
  """
905
  if cl is None:
906
    cl = GetClient()
907

    
908
  prev_job_info = None
909
  prev_logmsg_serial = None
910

    
911
  while True:
912
    result = cl.WaitForJobChange(job_id, ["status"], prev_job_info,
913
                                 prev_logmsg_serial)
914
    if not result:
915
      # job not found, go away!
916
      raise errors.JobLost("Job with id %s lost" % job_id)
917

    
918
    # Split result, a tuple of (field values, log entries)
919
    (job_info, log_entries) = result
920
    (status, ) = job_info
921

    
922
    if log_entries:
923
      for log_entry in log_entries:
924
        (serial, timestamp, _, message) = log_entry
925
        if callable(feedback_fn):
926
          feedback_fn(log_entry[1:])
927
        else:
928
          encoded = utils.SafeEncode(message)
929
          ToStdout("%s %s", time.ctime(utils.MergeTime(timestamp)), encoded)
930
        prev_logmsg_serial = max(prev_logmsg_serial, serial)
931

    
932
    # TODO: Handle canceled and archived jobs
933
    elif status in (constants.JOB_STATUS_SUCCESS,
934
                    constants.JOB_STATUS_ERROR,
935
                    constants.JOB_STATUS_CANCELING,
936
                    constants.JOB_STATUS_CANCELED):
937
      break
938

    
939
    prev_job_info = job_info
940

    
941
  jobs = cl.QueryJobs([job_id], ["status", "opstatus", "opresult"])
942
  if not jobs:
943
    raise errors.JobLost("Job with id %s lost" % job_id)
944

    
945
  status, opstatus, result = jobs[0]
946
  if status == constants.JOB_STATUS_SUCCESS:
947
    return result
948
  elif status in (constants.JOB_STATUS_CANCELING,
949
                  constants.JOB_STATUS_CANCELED):
950
    raise errors.OpExecError("Job was canceled")
951
  else:
952
    has_ok = False
953
    for idx, (status, msg) in enumerate(zip(opstatus, result)):
954
      if status == constants.OP_STATUS_SUCCESS:
955
        has_ok = True
956
      elif status == constants.OP_STATUS_ERROR:
957
        errors.MaybeRaise(msg)
958
        if has_ok:
959
          raise errors.OpExecError("partial failure (opcode %d): %s" %
960
                                   (idx, msg))
961
        else:
962
          raise errors.OpExecError(str(msg))
963
    # default failure mode
964
    raise errors.OpExecError(result)
965

    
966

    
967
def SubmitOpCode(op, cl=None, feedback_fn=None):
968
  """Legacy function to submit an opcode.
969

970
  This is just a simple wrapper over the construction of the processor
971
  instance. It should be extended to better handle feedback and
972
  interaction functions.
973

974
  """
975
  if cl is None:
976
    cl = GetClient()
977

    
978
  job_id = SendJob([op], cl)
979

    
980
  op_results = PollJob(job_id, cl=cl, feedback_fn=feedback_fn)
981

    
982
  return op_results[0]
983

    
984

    
985
def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
986
  """Wrapper around SubmitOpCode or SendJob.
987

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

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

995
  """
996
  if opts and opts.dry_run:
997
    op.dry_run = opts.dry_run
998
  if opts and opts.submit_only:
999
    job_id = SendJob([op], cl=cl)
1000
    raise JobSubmittedException(job_id)
1001
  else:
1002
    return SubmitOpCode(op, cl=cl, feedback_fn=feedback_fn)
1003

    
1004

    
1005
def GetClient():
1006
  # TODO: Cache object?
1007
  try:
1008
    client = luxi.Client()
1009
  except luxi.NoMasterError:
1010
    master, myself = ssconf.GetMasterAndMyself()
1011
    if master != myself:
1012
      raise errors.OpPrereqError("This is not the master node, please connect"
1013
                                 " to node '%s' and rerun the command" %
1014
                                 master)
1015
    else:
1016
      raise
1017
  return client
1018

    
1019

    
1020
def FormatError(err):
1021
  """Return a formatted error message for a given error.
1022

1023
  This function takes an exception instance and returns a tuple
1024
  consisting of two values: first, the recommended exit code, and
1025
  second, a string describing the error message (not
1026
  newline-terminated).
1027

1028
  """
1029
  retcode = 1
1030
  obuf = StringIO()
1031
  msg = str(err)
1032
  if isinstance(err, errors.ConfigurationError):
1033
    txt = "Corrupt configuration file: %s" % msg
1034
    logging.error(txt)
1035
    obuf.write(txt + "\n")
1036
    obuf.write("Aborting.")
1037
    retcode = 2
1038
  elif isinstance(err, errors.HooksAbort):
1039
    obuf.write("Failure: hooks execution failed:\n")
1040
    for node, script, out in err.args[0]:
1041
      if out:
1042
        obuf.write("  node: %s, script: %s, output: %s\n" %
1043
                   (node, script, out))
1044
      else:
1045
        obuf.write("  node: %s, script: %s (no output)\n" %
1046
                   (node, script))
1047
  elif isinstance(err, errors.HooksFailure):
1048
    obuf.write("Failure: hooks general failure: %s" % msg)
1049
  elif isinstance(err, errors.ResolverError):
1050
    this_host = utils.HostInfo.SysName()
1051
    if err.args[0] == this_host:
1052
      msg = "Failure: can't resolve my own hostname ('%s')"
1053
    else:
1054
      msg = "Failure: can't resolve hostname '%s'"
1055
    obuf.write(msg % err.args[0])
1056
  elif isinstance(err, errors.OpPrereqError):
1057
    obuf.write("Failure: prerequisites not met for this"
1058
               " operation:\n%s" % msg)
1059
  elif isinstance(err, errors.OpExecError):
1060
    obuf.write("Failure: command execution error:\n%s" % msg)
1061
  elif isinstance(err, errors.TagError):
1062
    obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
1063
  elif isinstance(err, errors.JobQueueDrainError):
1064
    obuf.write("Failure: the job queue is marked for drain and doesn't"
1065
               " accept new requests\n")
1066
  elif isinstance(err, errors.JobQueueFull):
1067
    obuf.write("Failure: the job queue is full and doesn't accept new"
1068
               " job submissions until old jobs are archived\n")
1069
  elif isinstance(err, errors.TypeEnforcementError):
1070
    obuf.write("Parameter Error: %s" % msg)
1071
  elif isinstance(err, errors.ParameterError):
1072
    obuf.write("Failure: unknown/wrong parameter name '%s'" % msg)
1073
  elif isinstance(err, errors.GenericError):
1074
    obuf.write("Unhandled Ganeti error: %s" % msg)
1075
  elif isinstance(err, luxi.NoMasterError):
1076
    obuf.write("Cannot communicate with the master daemon.\nIs it running"
1077
               " and listening for connections?")
1078
  elif isinstance(err, luxi.TimeoutError):
1079
    obuf.write("Timeout while talking to the master daemon. Error:\n"
1080
               "%s" % msg)
1081
  elif isinstance(err, luxi.ProtocolError):
1082
    obuf.write("Unhandled protocol error while talking to the master daemon:\n"
1083
               "%s" % msg)
1084
  elif isinstance(err, JobSubmittedException):
1085
    obuf.write("JobID: %s\n" % err.args[0])
1086
    retcode = 0
1087
  else:
1088
    obuf.write("Unhandled exception: %s" % msg)
1089
  return retcode, obuf.getvalue().rstrip('\n')
1090

    
1091

    
1092
def GenericMain(commands, override=None, aliases=None):
1093
  """Generic main function for all the gnt-* commands.
1094

1095
  Arguments:
1096
    - commands: a dictionary with a special structure, see the design doc
1097
                for command line handling.
1098
    - override: if not None, we expect a dictionary with keys that will
1099
                override command line options; this can be used to pass
1100
                options from the scripts to generic functions
1101
    - aliases: dictionary with command aliases {'alias': 'target, ...}
1102

1103
  """
1104
  # save the program name and the entire command line for later logging
1105
  if sys.argv:
1106
    binary = os.path.basename(sys.argv[0]) or sys.argv[0]
1107
    if len(sys.argv) >= 2:
1108
      binary += " " + sys.argv[1]
1109
      old_cmdline = " ".join(sys.argv[2:])
1110
    else:
1111
      old_cmdline = ""
1112
  else:
1113
    binary = "<unknown program>"
1114
    old_cmdline = ""
1115

    
1116
  if aliases is None:
1117
    aliases = {}
1118

    
1119
  try:
1120
    func, options, args = _ParseArgs(sys.argv, commands, aliases)
1121
  except errors.ParameterError, err:
1122
    result, err_msg = FormatError(err)
1123
    ToStderr(err_msg)
1124
    return 1
1125

    
1126
  if func is None: # parse error
1127
    return 1
1128

    
1129
  if override is not None:
1130
    for key, val in override.iteritems():
1131
      setattr(options, key, val)
1132

    
1133
  utils.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
1134
                     stderr_logging=True, program=binary)
1135

    
1136
  if old_cmdline:
1137
    logging.info("run with arguments '%s'", old_cmdline)
1138
  else:
1139
    logging.info("run with no arguments")
1140

    
1141
  try:
1142
    result = func(options, args)
1143
  except (errors.GenericError, luxi.ProtocolError,
1144
          JobSubmittedException), err:
1145
    result, err_msg = FormatError(err)
1146
    logging.exception("Error during command processing")
1147
    ToStderr(err_msg)
1148

    
1149
  return result
1150

    
1151

    
1152
def GenerateTable(headers, fields, separator, data,
1153
                  numfields=None, unitfields=None,
1154
                  units=None):
1155
  """Prints a table with headers and different fields.
1156

1157
  @type headers: dict
1158
  @param headers: dictionary mapping field names to headers for
1159
      the table
1160
  @type fields: list
1161
  @param fields: the field names corresponding to each row in
1162
      the data field
1163
  @param separator: the separator to be used; if this is None,
1164
      the default 'smart' algorithm is used which computes optimal
1165
      field width, otherwise just the separator is used between
1166
      each field
1167
  @type data: list
1168
  @param data: a list of lists, each sublist being one row to be output
1169
  @type numfields: list
1170
  @param numfields: a list with the fields that hold numeric
1171
      values and thus should be right-aligned
1172
  @type unitfields: list
1173
  @param unitfields: a list with the fields that hold numeric
1174
      values that should be formatted with the units field
1175
  @type units: string or None
1176
  @param units: the units we should use for formatting, or None for
1177
      automatic choice (human-readable for non-separator usage, otherwise
1178
      megabytes); this is a one-letter string
1179

1180
  """
1181
  if units is None:
1182
    if separator:
1183
      units = "m"
1184
    else:
1185
      units = "h"
1186

    
1187
  if numfields is None:
1188
    numfields = []
1189
  if unitfields is None:
1190
    unitfields = []
1191

    
1192
  numfields = utils.FieldSet(*numfields)
1193
  unitfields = utils.FieldSet(*unitfields)
1194

    
1195
  format_fields = []
1196
  for field in fields:
1197
    if headers and field not in headers:
1198
      # TODO: handle better unknown fields (either revert to old
1199
      # style of raising exception, or deal more intelligently with
1200
      # variable fields)
1201
      headers[field] = field
1202
    if separator is not None:
1203
      format_fields.append("%s")
1204
    elif numfields.Matches(field):
1205
      format_fields.append("%*s")
1206
    else:
1207
      format_fields.append("%-*s")
1208

    
1209
  if separator is None:
1210
    mlens = [0 for name in fields]
1211
    format = ' '.join(format_fields)
1212
  else:
1213
    format = separator.replace("%", "%%").join(format_fields)
1214

    
1215
  for row in data:
1216
    if row is None:
1217
      continue
1218
    for idx, val in enumerate(row):
1219
      if unitfields.Matches(fields[idx]):
1220
        try:
1221
          val = int(val)
1222
        except ValueError:
1223
          pass
1224
        else:
1225
          val = row[idx] = utils.FormatUnit(val, units)
1226
      val = row[idx] = str(val)
1227
      if separator is None:
1228
        mlens[idx] = max(mlens[idx], len(val))
1229

    
1230
  result = []
1231
  if headers:
1232
    args = []
1233
    for idx, name in enumerate(fields):
1234
      hdr = headers[name]
1235
      if separator is None:
1236
        mlens[idx] = max(mlens[idx], len(hdr))
1237
        args.append(mlens[idx])
1238
      args.append(hdr)
1239
    result.append(format % tuple(args))
1240

    
1241
  for line in data:
1242
    args = []
1243
    if line is None:
1244
      line = ['-' for _ in fields]
1245
    for idx in xrange(len(fields)):
1246
      if separator is None:
1247
        args.append(mlens[idx])
1248
      args.append(line[idx])
1249
    result.append(format % tuple(args))
1250

    
1251
  return result
1252

    
1253

    
1254
def FormatTimestamp(ts):
1255
  """Formats a given timestamp.
1256

1257
  @type ts: timestamp
1258
  @param ts: a timeval-type timestamp, a tuple of seconds and microseconds
1259

1260
  @rtype: string
1261
  @return: a string with the formatted timestamp
1262

1263
  """
1264
  if not isinstance (ts, (tuple, list)) or len(ts) != 2:
1265
    return '?'
1266
  sec, usec = ts
1267
  return time.strftime("%F %T", time.localtime(sec)) + ".%06d" % usec
1268

    
1269

    
1270
def ParseTimespec(value):
1271
  """Parse a time specification.
1272

1273
  The following suffixed will be recognized:
1274

1275
    - s: seconds
1276
    - m: minutes
1277
    - h: hours
1278
    - d: day
1279
    - w: weeks
1280

1281
  Without any suffix, the value will be taken to be in seconds.
1282

1283
  """
1284
  value = str(value)
1285
  if not value:
1286
    raise errors.OpPrereqError("Empty time specification passed")
1287
  suffix_map = {
1288
    's': 1,
1289
    'm': 60,
1290
    'h': 3600,
1291
    'd': 86400,
1292
    'w': 604800,
1293
    }
1294
  if value[-1] not in suffix_map:
1295
    try:
1296
      value = int(value)
1297
    except ValueError:
1298
      raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1299
  else:
1300
    multiplier = suffix_map[value[-1]]
1301
    value = value[:-1]
1302
    if not value: # no data left after stripping the suffix
1303
      raise errors.OpPrereqError("Invalid time specification (only"
1304
                                 " suffix passed)")
1305
    try:
1306
      value = int(value) * multiplier
1307
    except ValueError:
1308
      raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1309
  return value
1310

    
1311

    
1312
def GetOnlineNodes(nodes, cl=None, nowarn=False):
1313
  """Returns the names of online nodes.
1314

1315
  This function will also log a warning on stderr with the names of
1316
  the online nodes.
1317

1318
  @param nodes: if not empty, use only this subset of nodes (minus the
1319
      offline ones)
1320
  @param cl: if not None, luxi client to use
1321
  @type nowarn: boolean
1322
  @param nowarn: by default, this function will output a note with the
1323
      offline nodes that are skipped; if this parameter is True the
1324
      note is not displayed
1325

1326
  """
1327
  if cl is None:
1328
    cl = GetClient()
1329

    
1330
  result = cl.QueryNodes(names=nodes, fields=["name", "offline"],
1331
                         use_locking=False)
1332
  offline = [row[0] for row in result if row[1]]
1333
  if offline and not nowarn:
1334
    ToStderr("Note: skipping offline node(s): %s" % ", ".join(offline))
1335
  return [row[0] for row in result if not row[1]]
1336

    
1337

    
1338
def _ToStream(stream, txt, *args):
1339
  """Write a message to a stream, bypassing the logging system
1340

1341
  @type stream: file object
1342
  @param stream: the file to which we should write
1343
  @type txt: str
1344
  @param txt: the message
1345

1346
  """
1347
  if args:
1348
    args = tuple(args)
1349
    stream.write(txt % args)
1350
  else:
1351
    stream.write(txt)
1352
  stream.write('\n')
1353
  stream.flush()
1354

    
1355

    
1356
def ToStdout(txt, *args):
1357
  """Write a message to stdout only, bypassing the logging system
1358

1359
  This is just a wrapper over _ToStream.
1360

1361
  @type txt: str
1362
  @param txt: the message
1363

1364
  """
1365
  _ToStream(sys.stdout, txt, *args)
1366

    
1367

    
1368
def ToStderr(txt, *args):
1369
  """Write a message to stderr only, bypassing the logging system
1370

1371
  This is just a wrapper over _ToStream.
1372

1373
  @type txt: str
1374
  @param txt: the message
1375

1376
  """
1377
  _ToStream(sys.stderr, txt, *args)
1378

    
1379

    
1380
class JobExecutor(object):
1381
  """Class which manages the submission and execution of multiple jobs.
1382

1383
  Note that instances of this class should not be reused between
1384
  GetResults() calls.
1385

1386
  """
1387
  def __init__(self, cl=None, verbose=True):
1388
    self.queue = []
1389
    if cl is None:
1390
      cl = GetClient()
1391
    self.cl = cl
1392
    self.verbose = verbose
1393
    self.jobs = []
1394

    
1395
  def QueueJob(self, name, *ops):
1396
    """Record a job for later submit.
1397

1398
    @type name: string
1399
    @param name: a description of the job, will be used in WaitJobSet
1400
    """
1401
    self.queue.append((name, ops))
1402

    
1403
  def SubmitPending(self):
1404
    """Submit all pending jobs.
1405

1406
    """
1407
    results = self.cl.SubmitManyJobs([row[1] for row in self.queue])
1408
    for ((status, data), (name, _)) in zip(results, self.queue):
1409
      self.jobs.append((status, data, name))
1410

    
1411
  def GetResults(self):
1412
    """Wait for and return the results of all jobs.
1413

1414
    @rtype: list
1415
    @return: list of tuples (success, job results), in the same order
1416
        as the submitted jobs; if a job has failed, instead of the result
1417
        there will be the error message
1418

1419
    """
1420
    if not self.jobs:
1421
      self.SubmitPending()
1422
    results = []
1423
    if self.verbose:
1424
      ok_jobs = [row[1] for row in self.jobs if row[0]]
1425
      if ok_jobs:
1426
        ToStdout("Submitted jobs %s", ", ".join(ok_jobs))
1427
    for submit_status, jid, name in self.jobs:
1428
      if not submit_status:
1429
        ToStderr("Failed to submit job for %s: %s", name, jid)
1430
        results.append((False, jid))
1431
        continue
1432
      if self.verbose:
1433
        ToStdout("Waiting for job %s for %s...", jid, name)
1434
      try:
1435
        job_result = PollJob(jid, cl=self.cl)
1436
        success = True
1437
      except (errors.GenericError, luxi.ProtocolError), err:
1438
        _, job_result = FormatError(err)
1439
        success = False
1440
        # the error message will always be shown, verbose or not
1441
        ToStderr("Job %s for %s has failed: %s", jid, name, job_result)
1442

    
1443
      results.append((success, job_result))
1444
    return results
1445

    
1446
  def WaitOrShow(self, wait):
1447
    """Wait for job results or only print the job IDs.
1448

1449
    @type wait: boolean
1450
    @param wait: whether to wait or not
1451

1452
    """
1453
    if wait:
1454
      return self.GetResults()
1455
    else:
1456
      if not self.jobs:
1457
        self.SubmitPending()
1458
      for status, result, name in self.jobs:
1459
        if status:
1460
          ToStdout("%s: %s", result, name)
1461
        else:
1462
          ToStderr("Failure for %s: %s", name, result)