Statistics
| Branch: | Tag: | Revision:

root / lib / cli.py @ 2d5e7ae1

History | View | Annotate | Download (43.3 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
  "FORCE_OPT",
65
  "NET_OPT",
66
  "NODE_LIST_OPT",
67
  "NODE_PLACEMENT_OPT",
68
  "NOHDR_OPT",
69
  "NOIPCHECK_OPT",
70
  "NONICS_OPT",
71
  "NONLIVE_OPT",
72
  "NOSTART_OPT",
73
  "NWSYNC_OPT",
74
  "OS_OPT",
75
  "OS_SIZE_OPT",
76
  "SEP_OPT",
77
  "SHOWCMD_OPT",
78
  "SINGLE_NODE_OPT",
79
  "SUBMIT_OPT",
80
  "STATIC_OPT",
81
  "SYNC_OPT",
82
  "TAG_SRC_OPT",
83
  "USEUNITS_OPT",
84
  "VERBOSE_OPT",
85
  # Generic functions for CLI programs
86
  "GenericMain",
87
  "GetClient",
88
  "GetOnlineNodes",
89
  "JobExecutor",
90
  "JobSubmittedException",
91
  "ParseTimespec",
92
  "SubmitOpCode",
93
  "SubmitOrSend",
94
  "UsesRPC",
95
  # Formatting functions
96
  "ToStderr", "ToStdout",
97
  "FormatError",
98
  "GenerateTable",
99
  "AskUser",
100
  "FormatTimestamp",
101
  # Tags functions
102
  "ListTags",
103
  "AddTags",
104
  "RemoveTags",
105
  # command line options support infrastructure
106
  "ARGS_MANY_INSTANCES",
107
  "ARGS_MANY_NODES",
108
  "ARGS_NONE",
109
  "ARGS_ONE_INSTANCE",
110
  "ARGS_ONE_NODE",
111
  "ArgChoice",
112
  "ArgCommand",
113
  "ArgFile",
114
  "ArgHost",
115
  "ArgInstance",
116
  "ArgJobId",
117
  "ArgNode",
118
  "ArgSuggest",
119
  "ArgUnknown",
120
  "OPT_COMPL_INST_ADD_NODES",
121
  "OPT_COMPL_MANY_NODES",
122
  "OPT_COMPL_ONE_IALLOCATOR",
123
  "OPT_COMPL_ONE_INSTANCE",
124
  "OPT_COMPL_ONE_NODE",
125
  "OPT_COMPL_ONE_OS",
126
  "cli_option",
127
  "SplitNodeOption",
128
  ]
129

    
130
NO_PREFIX = "no_"
131
UN_PREFIX = "-"
132

    
133

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

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

    
143

    
144
class ArgSuggest(_Argument):
145
  """Suggesting argument.
146

147
  Value can be any of the ones passed to the constructor.
148

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

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

    
158

    
159
class ArgChoice(ArgSuggest):
160
  """Choice argument.
161

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

165
  """
166

    
167

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

171
  """
172

    
173

    
174
class ArgInstance(_Argument):
175
  """Instances argument.
176

177
  """
178

    
179

    
180
class ArgNode(_Argument):
181
  """Node argument.
182

183
  """
184

    
185
class ArgJobId(_Argument):
186
  """Job ID argument.
187

188
  """
189

    
190

    
191
class ArgFile(_Argument):
192
  """File path argument.
193

194
  """
195

    
196

    
197
class ArgCommand(_Argument):
198
  """Command argument.
199

200
  """
201

    
202

    
203
class ArgHost(_Argument):
204
  """Host argument.
205

206
  """
207

    
208

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

    
215

    
216

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

220
  Note that this function will modify its args parameter.
221

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

    
237

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

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

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

    
266

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

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

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

    
284

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

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

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

    
301

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

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

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

    
318

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

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

    
328

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

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

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

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

    
365

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

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

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

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

    
393

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

397
  This will store the parsed values as a dict {key: val}.
398

399
  """
400
  return _SplitKeyVal(opt, value)
401

    
402

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

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

    
421

    
422
class CliOption(Option):
423
  """Custom option class for optparse.
424

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

    
439

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

    
443

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
626

    
627

    
628
def _ParseArgs(argv, commands, aliases):
629
  """Parser for the command line arguments.
630

631
  This function parses the arguments and returns the function which
632
  must be executed together with its (modified) arguments.
633

634
  @param argv: the command line
635
  @param commands: dictionary with special contents, see the design
636
      doc for cmdline handling
637
  @param aliases: dictionary with command aliases {'alias': 'target, ...}
638

639
  """
640
  if len(argv) == 0:
641
    binary = "<command>"
642
  else:
643
    binary = argv[0].split("/")[-1]
644

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

    
651
  if len(argv) < 2 or not (argv[1] in commands or
652
                           argv[1] in aliases):
653
    # let's do a nice thing
654
    sortedcmds = commands.keys()
655
    sortedcmds.sort()
656

    
657
    ToStdout("Usage: %s {command} [options...] [argument...]", binary)
658
    ToStdout("%s <command> --help to see details, or man %s", binary, binary)
659
    ToStdout("")
660

    
661
    # compute the max line length for cmd + usage
662
    mlen = max([len(" %s" % cmd) for cmd in commands])
663
    mlen = min(60, mlen) # should not get here...
664

    
665
    # and format a nice command list
666
    ToStdout("Commands:")
667
    for cmd in sortedcmds:
668
      cmdstr = " %s" % (cmd,)
669
      help_text = commands[cmd][4]
670
      help_lines = textwrap.wrap(help_text, 79 - 3 - mlen)
671
      ToStdout("%-*s - %s", mlen, cmdstr, help_lines.pop(0))
672
      for line in help_lines:
673
        ToStdout("%-*s   %s", mlen, "", line)
674

    
675
    ToStdout("")
676

    
677
    return None, None, None
678

    
679
  # get command, unalias it, and look it up in commands
680
  cmd = argv.pop(1)
681
  if cmd in aliases:
682
    if cmd in commands:
683
      raise errors.ProgrammerError("Alias '%s' overrides an existing"
684
                                   " command" % cmd)
685

    
686
    if aliases[cmd] not in commands:
687
      raise errors.ProgrammerError("Alias '%s' maps to non-existing"
688
                                   " command '%s'" % (cmd, aliases[cmd]))
689

    
690
    cmd = aliases[cmd]
691

    
692
  func, args_def, parser_opts, usage, description = commands[cmd]
693
  parser = OptionParser(option_list=parser_opts + [_DRY_RUN_OPT],
694
                        description=description,
695
                        formatter=TitledHelpFormatter(),
696
                        usage="%%prog %s %s" % (cmd, usage))
697
  parser.disable_interspersed_args()
698
  options, args = parser.parse_args()
699

    
700
  if not _CheckArguments(cmd, args_def, args):
701
    return None, None, None
702

    
703
  return func, options, args
704

    
705

    
706
def _CheckArguments(cmd, args_def, args):
707
  """Verifies the arguments using the argument definition.
708

709
  Algorithm:
710

711
    1. Abort with error if values specified by user but none expected.
712

713
    1. For each argument in definition
714

715
      1. Keep running count of minimum number of values (min_count)
716
      1. Keep running count of maximum number of values (max_count)
717
      1. If it has an unlimited number of values
718

719
        1. Abort with error if it's not the last argument in the definition
720

721
    1. If last argument has limited number of values
722

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

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

727
  """
728
  if args and not args_def:
729
    ToStderr("Error: Command %s expects no arguments", cmd)
730
    return False
731

    
732
  min_count = None
733
  max_count = None
734
  check_max = None
735

    
736
  last_idx = len(args_def) - 1
737

    
738
  for idx, arg in enumerate(args_def):
739
    if min_count is None:
740
      min_count = arg.min
741
    elif arg.min is not None:
742
      min_count += arg.min
743

    
744
    if max_count is None:
745
      max_count = arg.max
746
    elif arg.max is not None:
747
      max_count += arg.max
748

    
749
    if idx == last_idx:
750
      check_max = (arg.max is not None)
751

    
752
    elif arg.max is None:
753
      raise errors.ProgrammerError("Only the last argument can have max=None")
754

    
755
  if check_max:
756
    # Command with exact number of arguments
757
    if (min_count is not None and max_count is not None and
758
        min_count == max_count and len(args) != min_count):
759
      ToStderr("Error: Command %s expects %d argument(s)", cmd, min_count)
760
      return False
761

    
762
    # Command with limited number of arguments
763
    if max_count is not None and len(args) > max_count:
764
      ToStderr("Error: Command %s expects only %d argument(s)",
765
               cmd, max_count)
766
      return False
767

    
768
  # Command with some required arguments
769
  if min_count is not None and len(args) < min_count:
770
    ToStderr("Error: Command %s expects at least %d argument(s)",
771
             cmd, min_count)
772
    return False
773

    
774
  return True
775

    
776

    
777
def SplitNodeOption(value):
778
  """Splits the value of a --node option.
779

780
  """
781
  if value and ':' in value:
782
    return value.split(':', 1)
783
  else:
784
    return (value, None)
785

    
786

    
787
def UsesRPC(fn):
788
  def wrapper(*args, **kwargs):
789
    rpc.Init()
790
    try:
791
      return fn(*args, **kwargs)
792
    finally:
793
      rpc.Shutdown()
794
  return wrapper
795

    
796

    
797
def AskUser(text, choices=None):
798
  """Ask the user a question.
799

800
  @param text: the question to ask
801

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

807
  @return: one of the return values from the choices list; if input is
808
      not possible (i.e. not running with a tty, we return the last
809
      entry from the list
810

811
  """
812
  if choices is None:
813
    choices = [('y', True, 'Perform the operation'),
814
               ('n', False, 'Do not perform the operation')]
815
  if not choices or not isinstance(choices, list):
816
    raise errors.ProgrammerError("Invalid choices argument to AskUser")
817
  for entry in choices:
818
    if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
819
      raise errors.ProgrammerError("Invalid choices element to AskUser")
820

    
821
  answer = choices[-1][1]
822
  new_text = []
823
  for line in text.splitlines():
824
    new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
825
  text = "\n".join(new_text)
826
  try:
827
    f = file("/dev/tty", "a+")
828
  except IOError:
829
    return answer
830
  try:
831
    chars = [entry[0] for entry in choices]
832
    chars[-1] = "[%s]" % chars[-1]
833
    chars.append('?')
834
    maps = dict([(entry[0], entry[1]) for entry in choices])
835
    while True:
836
      f.write(text)
837
      f.write('\n')
838
      f.write("/".join(chars))
839
      f.write(": ")
840
      line = f.readline(2).strip().lower()
841
      if line in maps:
842
        answer = maps[line]
843
        break
844
      elif line == '?':
845
        for entry in choices:
846
          f.write(" %s - %s\n" % (entry[0], entry[2]))
847
        f.write("\n")
848
        continue
849
  finally:
850
    f.close()
851
  return answer
852

    
853

    
854
class JobSubmittedException(Exception):
855
  """Job was submitted, client should exit.
856

857
  This exception has one argument, the ID of the job that was
858
  submitted. The handler should print this ID.
859

860
  This is not an error, just a structured way to exit from clients.
861

862
  """
863

    
864

    
865
def SendJob(ops, cl=None):
866
  """Function to submit an opcode without waiting for the results.
867

868
  @type ops: list
869
  @param ops: list of opcodes
870
  @type cl: luxi.Client
871
  @param cl: the luxi client to use for communicating with the master;
872
             if None, a new client will be created
873

874
  """
875
  if cl is None:
876
    cl = GetClient()
877

    
878
  job_id = cl.SubmitJob(ops)
879

    
880
  return job_id
881

    
882

    
883
def PollJob(job_id, cl=None, feedback_fn=None):
884
  """Function to poll for the result of a job.
885

886
  @type job_id: job identified
887
  @param job_id: the job to poll for results
888
  @type cl: luxi.Client
889
  @param cl: the luxi client to use for communicating with the master;
890
             if None, a new client will be created
891

892
  """
893
  if cl is None:
894
    cl = GetClient()
895

    
896
  prev_job_info = None
897
  prev_logmsg_serial = None
898

    
899
  while True:
900
    result = cl.WaitForJobChange(job_id, ["status"], prev_job_info,
901
                                 prev_logmsg_serial)
902
    if not result:
903
      # job not found, go away!
904
      raise errors.JobLost("Job with id %s lost" % job_id)
905

    
906
    # Split result, a tuple of (field values, log entries)
907
    (job_info, log_entries) = result
908
    (status, ) = job_info
909

    
910
    if log_entries:
911
      for log_entry in log_entries:
912
        (serial, timestamp, _, message) = log_entry
913
        if callable(feedback_fn):
914
          feedback_fn(log_entry[1:])
915
        else:
916
          encoded = utils.SafeEncode(message)
917
          ToStdout("%s %s", time.ctime(utils.MergeTime(timestamp)), encoded)
918
        prev_logmsg_serial = max(prev_logmsg_serial, serial)
919

    
920
    # TODO: Handle canceled and archived jobs
921
    elif status in (constants.JOB_STATUS_SUCCESS,
922
                    constants.JOB_STATUS_ERROR,
923
                    constants.JOB_STATUS_CANCELING,
924
                    constants.JOB_STATUS_CANCELED):
925
      break
926

    
927
    prev_job_info = job_info
928

    
929
  jobs = cl.QueryJobs([job_id], ["status", "opstatus", "opresult"])
930
  if not jobs:
931
    raise errors.JobLost("Job with id %s lost" % job_id)
932

    
933
  status, opstatus, result = jobs[0]
934
  if status == constants.JOB_STATUS_SUCCESS:
935
    return result
936
  elif status in (constants.JOB_STATUS_CANCELING,
937
                  constants.JOB_STATUS_CANCELED):
938
    raise errors.OpExecError("Job was canceled")
939
  else:
940
    has_ok = False
941
    for idx, (status, msg) in enumerate(zip(opstatus, result)):
942
      if status == constants.OP_STATUS_SUCCESS:
943
        has_ok = True
944
      elif status == constants.OP_STATUS_ERROR:
945
        errors.MaybeRaise(msg)
946
        if has_ok:
947
          raise errors.OpExecError("partial failure (opcode %d): %s" %
948
                                   (idx, msg))
949
        else:
950
          raise errors.OpExecError(str(msg))
951
    # default failure mode
952
    raise errors.OpExecError(result)
953

    
954

    
955
def SubmitOpCode(op, cl=None, feedback_fn=None):
956
  """Legacy function to submit an opcode.
957

958
  This is just a simple wrapper over the construction of the processor
959
  instance. It should be extended to better handle feedback and
960
  interaction functions.
961

962
  """
963
  if cl is None:
964
    cl = GetClient()
965

    
966
  job_id = SendJob([op], cl)
967

    
968
  op_results = PollJob(job_id, cl=cl, feedback_fn=feedback_fn)
969

    
970
  return op_results[0]
971

    
972

    
973
def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
974
  """Wrapper around SubmitOpCode or SendJob.
975

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

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

983
  """
984
  if opts and opts.dry_run:
985
    op.dry_run = opts.dry_run
986
  if opts and opts.submit_only:
987
    job_id = SendJob([op], cl=cl)
988
    raise JobSubmittedException(job_id)
989
  else:
990
    return SubmitOpCode(op, cl=cl, feedback_fn=feedback_fn)
991

    
992

    
993
def GetClient():
994
  # TODO: Cache object?
995
  try:
996
    client = luxi.Client()
997
  except luxi.NoMasterError:
998
    master, myself = ssconf.GetMasterAndMyself()
999
    if master != myself:
1000
      raise errors.OpPrereqError("This is not the master node, please connect"
1001
                                 " to node '%s' and rerun the command" %
1002
                                 master)
1003
    else:
1004
      raise
1005
  return client
1006

    
1007

    
1008
def FormatError(err):
1009
  """Return a formatted error message for a given error.
1010

1011
  This function takes an exception instance and returns a tuple
1012
  consisting of two values: first, the recommended exit code, and
1013
  second, a string describing the error message (not
1014
  newline-terminated).
1015

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

    
1079

    
1080
def GenericMain(commands, override=None, aliases=None):
1081
  """Generic main function for all the gnt-* commands.
1082

1083
  Arguments:
1084
    - commands: a dictionary with a special structure, see the design doc
1085
                for command line handling.
1086
    - override: if not None, we expect a dictionary with keys that will
1087
                override command line options; this can be used to pass
1088
                options from the scripts to generic functions
1089
    - aliases: dictionary with command aliases {'alias': 'target, ...}
1090

1091
  """
1092
  # save the program name and the entire command line for later logging
1093
  if sys.argv:
1094
    binary = os.path.basename(sys.argv[0]) or sys.argv[0]
1095
    if len(sys.argv) >= 2:
1096
      binary += " " + sys.argv[1]
1097
      old_cmdline = " ".join(sys.argv[2:])
1098
    else:
1099
      old_cmdline = ""
1100
  else:
1101
    binary = "<unknown program>"
1102
    old_cmdline = ""
1103

    
1104
  if aliases is None:
1105
    aliases = {}
1106

    
1107
  try:
1108
    func, options, args = _ParseArgs(sys.argv, commands, aliases)
1109
  except errors.ParameterError, err:
1110
    result, err_msg = FormatError(err)
1111
    ToStderr(err_msg)
1112
    return 1
1113

    
1114
  if func is None: # parse error
1115
    return 1
1116

    
1117
  if override is not None:
1118
    for key, val in override.iteritems():
1119
      setattr(options, key, val)
1120

    
1121
  utils.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
1122
                     stderr_logging=True, program=binary)
1123

    
1124
  if old_cmdline:
1125
    logging.info("run with arguments '%s'", old_cmdline)
1126
  else:
1127
    logging.info("run with no arguments")
1128

    
1129
  try:
1130
    result = func(options, args)
1131
  except (errors.GenericError, luxi.ProtocolError,
1132
          JobSubmittedException), err:
1133
    result, err_msg = FormatError(err)
1134
    logging.exception("Error during command processing")
1135
    ToStderr(err_msg)
1136

    
1137
  return result
1138

    
1139

    
1140
def GenerateTable(headers, fields, separator, data,
1141
                  numfields=None, unitfields=None,
1142
                  units=None):
1143
  """Prints a table with headers and different fields.
1144

1145
  @type headers: dict
1146
  @param headers: dictionary mapping field names to headers for
1147
      the table
1148
  @type fields: list
1149
  @param fields: the field names corresponding to each row in
1150
      the data field
1151
  @param separator: the separator to be used; if this is None,
1152
      the default 'smart' algorithm is used which computes optimal
1153
      field width, otherwise just the separator is used between
1154
      each field
1155
  @type data: list
1156
  @param data: a list of lists, each sublist being one row to be output
1157
  @type numfields: list
1158
  @param numfields: a list with the fields that hold numeric
1159
      values and thus should be right-aligned
1160
  @type unitfields: list
1161
  @param unitfields: a list with the fields that hold numeric
1162
      values that should be formatted with the units field
1163
  @type units: string or None
1164
  @param units: the units we should use for formatting, or None for
1165
      automatic choice (human-readable for non-separator usage, otherwise
1166
      megabytes); this is a one-letter string
1167

1168
  """
1169
  if units is None:
1170
    if separator:
1171
      units = "m"
1172
    else:
1173
      units = "h"
1174

    
1175
  if numfields is None:
1176
    numfields = []
1177
  if unitfields is None:
1178
    unitfields = []
1179

    
1180
  numfields = utils.FieldSet(*numfields)
1181
  unitfields = utils.FieldSet(*unitfields)
1182

    
1183
  format_fields = []
1184
  for field in fields:
1185
    if headers and field not in headers:
1186
      # TODO: handle better unknown fields (either revert to old
1187
      # style of raising exception, or deal more intelligently with
1188
      # variable fields)
1189
      headers[field] = field
1190
    if separator is not None:
1191
      format_fields.append("%s")
1192
    elif numfields.Matches(field):
1193
      format_fields.append("%*s")
1194
    else:
1195
      format_fields.append("%-*s")
1196

    
1197
  if separator is None:
1198
    mlens = [0 for name in fields]
1199
    format = ' '.join(format_fields)
1200
  else:
1201
    format = separator.replace("%", "%%").join(format_fields)
1202

    
1203
  for row in data:
1204
    if row is None:
1205
      continue
1206
    for idx, val in enumerate(row):
1207
      if unitfields.Matches(fields[idx]):
1208
        try:
1209
          val = int(val)
1210
        except ValueError:
1211
          pass
1212
        else:
1213
          val = row[idx] = utils.FormatUnit(val, units)
1214
      val = row[idx] = str(val)
1215
      if separator is None:
1216
        mlens[idx] = max(mlens[idx], len(val))
1217

    
1218
  result = []
1219
  if headers:
1220
    args = []
1221
    for idx, name in enumerate(fields):
1222
      hdr = headers[name]
1223
      if separator is None:
1224
        mlens[idx] = max(mlens[idx], len(hdr))
1225
        args.append(mlens[idx])
1226
      args.append(hdr)
1227
    result.append(format % tuple(args))
1228

    
1229
  for line in data:
1230
    args = []
1231
    if line is None:
1232
      line = ['-' for _ in fields]
1233
    for idx in xrange(len(fields)):
1234
      if separator is None:
1235
        args.append(mlens[idx])
1236
      args.append(line[idx])
1237
    result.append(format % tuple(args))
1238

    
1239
  return result
1240

    
1241

    
1242
def FormatTimestamp(ts):
1243
  """Formats a given timestamp.
1244

1245
  @type ts: timestamp
1246
  @param ts: a timeval-type timestamp, a tuple of seconds and microseconds
1247

1248
  @rtype: string
1249
  @return: a string with the formatted timestamp
1250

1251
  """
1252
  if not isinstance (ts, (tuple, list)) or len(ts) != 2:
1253
    return '?'
1254
  sec, usec = ts
1255
  return time.strftime("%F %T", time.localtime(sec)) + ".%06d" % usec
1256

    
1257

    
1258
def ParseTimespec(value):
1259
  """Parse a time specification.
1260

1261
  The following suffixed will be recognized:
1262

1263
    - s: seconds
1264
    - m: minutes
1265
    - h: hours
1266
    - d: day
1267
    - w: weeks
1268

1269
  Without any suffix, the value will be taken to be in seconds.
1270

1271
  """
1272
  value = str(value)
1273
  if not value:
1274
    raise errors.OpPrereqError("Empty time specification passed")
1275
  suffix_map = {
1276
    's': 1,
1277
    'm': 60,
1278
    'h': 3600,
1279
    'd': 86400,
1280
    'w': 604800,
1281
    }
1282
  if value[-1] not in suffix_map:
1283
    try:
1284
      value = int(value)
1285
    except ValueError:
1286
      raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1287
  else:
1288
    multiplier = suffix_map[value[-1]]
1289
    value = value[:-1]
1290
    if not value: # no data left after stripping the suffix
1291
      raise errors.OpPrereqError("Invalid time specification (only"
1292
                                 " suffix passed)")
1293
    try:
1294
      value = int(value) * multiplier
1295
    except ValueError:
1296
      raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1297
  return value
1298

    
1299

    
1300
def GetOnlineNodes(nodes, cl=None, nowarn=False):
1301
  """Returns the names of online nodes.
1302

1303
  This function will also log a warning on stderr with the names of
1304
  the online nodes.
1305

1306
  @param nodes: if not empty, use only this subset of nodes (minus the
1307
      offline ones)
1308
  @param cl: if not None, luxi client to use
1309
  @type nowarn: boolean
1310
  @param nowarn: by default, this function will output a note with the
1311
      offline nodes that are skipped; if this parameter is True the
1312
      note is not displayed
1313

1314
  """
1315
  if cl is None:
1316
    cl = GetClient()
1317

    
1318
  result = cl.QueryNodes(names=nodes, fields=["name", "offline"],
1319
                         use_locking=False)
1320
  offline = [row[0] for row in result if row[1]]
1321
  if offline and not nowarn:
1322
    ToStderr("Note: skipping offline node(s): %s" % ", ".join(offline))
1323
  return [row[0] for row in result if not row[1]]
1324

    
1325

    
1326
def _ToStream(stream, txt, *args):
1327
  """Write a message to a stream, bypassing the logging system
1328

1329
  @type stream: file object
1330
  @param stream: the file to which we should write
1331
  @type txt: str
1332
  @param txt: the message
1333

1334
  """
1335
  if args:
1336
    args = tuple(args)
1337
    stream.write(txt % args)
1338
  else:
1339
    stream.write(txt)
1340
  stream.write('\n')
1341
  stream.flush()
1342

    
1343

    
1344
def ToStdout(txt, *args):
1345
  """Write a message to stdout only, bypassing the logging system
1346

1347
  This is just a wrapper over _ToStream.
1348

1349
  @type txt: str
1350
  @param txt: the message
1351

1352
  """
1353
  _ToStream(sys.stdout, txt, *args)
1354

    
1355

    
1356
def ToStderr(txt, *args):
1357
  """Write a message to stderr 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.stderr, txt, *args)
1366

    
1367

    
1368
class JobExecutor(object):
1369
  """Class which manages the submission and execution of multiple jobs.
1370

1371
  Note that instances of this class should not be reused between
1372
  GetResults() calls.
1373

1374
  """
1375
  def __init__(self, cl=None, verbose=True):
1376
    self.queue = []
1377
    if cl is None:
1378
      cl = GetClient()
1379
    self.cl = cl
1380
    self.verbose = verbose
1381
    self.jobs = []
1382

    
1383
  def QueueJob(self, name, *ops):
1384
    """Record a job for later submit.
1385

1386
    @type name: string
1387
    @param name: a description of the job, will be used in WaitJobSet
1388
    """
1389
    self.queue.append((name, ops))
1390

    
1391
  def SubmitPending(self):
1392
    """Submit all pending jobs.
1393

1394
    """
1395
    results = self.cl.SubmitManyJobs([row[1] for row in self.queue])
1396
    for ((status, data), (name, _)) in zip(results, self.queue):
1397
      self.jobs.append((status, data, name))
1398

    
1399
  def GetResults(self):
1400
    """Wait for and return the results of all jobs.
1401

1402
    @rtype: list
1403
    @return: list of tuples (success, job results), in the same order
1404
        as the submitted jobs; if a job has failed, instead of the result
1405
        there will be the error message
1406

1407
    """
1408
    if not self.jobs:
1409
      self.SubmitPending()
1410
    results = []
1411
    if self.verbose:
1412
      ok_jobs = [row[1] for row in self.jobs if row[0]]
1413
      if ok_jobs:
1414
        ToStdout("Submitted jobs %s", ", ".join(ok_jobs))
1415
    for submit_status, jid, name in self.jobs:
1416
      if not submit_status:
1417
        ToStderr("Failed to submit job for %s: %s", name, jid)
1418
        results.append((False, jid))
1419
        continue
1420
      if self.verbose:
1421
        ToStdout("Waiting for job %s for %s...", jid, name)
1422
      try:
1423
        job_result = PollJob(jid, cl=self.cl)
1424
        success = True
1425
      except (errors.GenericError, luxi.ProtocolError), err:
1426
        _, job_result = FormatError(err)
1427
        success = False
1428
        # the error message will always be shown, verbose or not
1429
        ToStderr("Job %s for %s has failed: %s", jid, name, job_result)
1430

    
1431
      results.append((success, job_result))
1432
    return results
1433

    
1434
  def WaitOrShow(self, wait):
1435
    """Wait for job results or only print the job IDs.
1436

1437
    @type wait: boolean
1438
    @param wait: whether to wait or not
1439

1440
    """
1441
    if wait:
1442
      return self.GetResults()
1443
    else:
1444
      if not self.jobs:
1445
        self.SubmitPending()
1446
      for status, result, name in self.jobs:
1447
        if status:
1448
          ToStdout("%s: %s", result, name)
1449
        else:
1450
          ToStderr("Failure for %s: %s", name, result)