Statistics
| Branch: | Tag: | Revision:

root / lib / cli.py @ e00f7a05

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

    
136
NO_PREFIX = "no_"
137
UN_PREFIX = "-"
138

    
139

    
140
class _Argument:
141
  def __init__(self, min=0, max=None):
142
    self.min = min
143
    self.max = max
144

    
145
  def __repr__(self):
146
    return ("<%s min=%s max=%s>" %
147
            (self.__class__.__name__, self.min, self.max))
148

    
149

    
150
class ArgSuggest(_Argument):
151
  """Suggesting argument.
152

153
  Value can be any of the ones passed to the constructor.
154

155
  """
156
  def __init__(self, min=0, max=None, choices=None):
157
    _Argument.__init__(self, min=min, max=max)
158
    self.choices = choices
159

    
160
  def __repr__(self):
161
    return ("<%s min=%s max=%s choices=%r>" %
162
            (self.__class__.__name__, self.min, self.max, self.choices))
163

    
164

    
165
class ArgChoice(ArgSuggest):
166
  """Choice argument.
167

168
  Value can be any of the ones passed to the constructor. Like L{ArgSuggest},
169
  but value must be one of the choices.
170

171
  """
172

    
173

    
174
class ArgUnknown(_Argument):
175
  """Unknown argument to program (e.g. determined at runtime).
176

177
  """
178

    
179

    
180
class ArgInstance(_Argument):
181
  """Instances argument.
182

183
  """
184

    
185

    
186
class ArgNode(_Argument):
187
  """Node argument.
188

189
  """
190

    
191
class ArgJobId(_Argument):
192
  """Job ID argument.
193

194
  """
195

    
196

    
197
class ArgFile(_Argument):
198
  """File path argument.
199

200
  """
201

    
202

    
203
class ArgCommand(_Argument):
204
  """Command argument.
205

206
  """
207

    
208

    
209
class ArgHost(_Argument):
210
  """Host argument.
211

212
  """
213

    
214

    
215
ARGS_NONE = []
216
ARGS_MANY_INSTANCES = [ArgInstance()]
217
ARGS_MANY_NODES = [ArgNode()]
218
ARGS_ONE_INSTANCE = [ArgInstance(min=1, max=1)]
219
ARGS_ONE_NODE = [ArgNode(min=1, max=1)]
220

    
221

    
222

    
223
def _ExtractTagsObject(opts, args):
224
  """Extract the tag type object.
225

226
  Note that this function will modify its args parameter.
227

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

    
243

    
244
def _ExtendTags(opts, args):
245
  """Extend the args if a source file has been given.
246

247
  This function will extend the tags with the contents of the file
248
  passed in the 'tags_source' attribute of the opts parameter. A file
249
  named '-' will be replaced by stdin.
250

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

    
272

    
273
def ListTags(opts, args):
274
  """List the tags on a given object.
275

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

281
  """
282
  kind, name = _ExtractTagsObject(opts, args)
283
  op = opcodes.OpGetTags(kind=kind, name=name)
284
  result = SubmitOpCode(op)
285
  result = list(result)
286
  result.sort()
287
  for tag in result:
288
    ToStdout(tag)
289

    
290

    
291
def AddTags(opts, args):
292
  """Add tags on a given object.
293

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

299
  """
300
  kind, name = _ExtractTagsObject(opts, args)
301
  _ExtendTags(opts, args)
302
  if not args:
303
    raise errors.OpPrereqError("No tags to be added")
304
  op = opcodes.OpAddTags(kind=kind, name=name, tags=args)
305
  SubmitOpCode(op)
306

    
307

    
308
def RemoveTags(opts, args):
309
  """Remove tags from a given object.
310

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

316
  """
317
  kind, name = _ExtractTagsObject(opts, args)
318
  _ExtendTags(opts, args)
319
  if not args:
320
    raise errors.OpPrereqError("No tags to be removed")
321
  op = opcodes.OpDelTags(kind=kind, name=name, tags=args)
322
  SubmitOpCode(op)
323

    
324

    
325
def check_unit(option, opt, value):
326
  """OptParsers custom converter for units.
327

328
  """
329
  try:
330
    return utils.ParseUnit(value)
331
  except errors.UnitParseError, err:
332
    raise OptionValueError("option %s: %s" % (opt, err))
333

    
334

    
335
def _SplitKeyVal(opt, data):
336
  """Convert a KeyVal string into a dict.
337

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

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

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

    
371

    
372
def check_ident_key_val(option, opt, value):
373
  """Custom parser for ident:key=val,key=val options.
374

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

378
  """
379
  if ":" not in value:
380
    ident, rest = value, ''
381
  else:
382
    ident, rest = value.split(":", 1)
383

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

    
399

    
400
def check_key_val(option, opt, value):
401
  """Custom parser class for key=val,key=val options.
402

403
  This will store the parsed values as a dict {key: val}.
404

405
  """
406
  return _SplitKeyVal(opt, value)
407

    
408

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

    
418
OPT_COMPL_ALL = frozenset([
419
  OPT_COMPL_MANY_NODES,
420
  OPT_COMPL_ONE_NODE,
421
  OPT_COMPL_ONE_INSTANCE,
422
  OPT_COMPL_ONE_OS,
423
  OPT_COMPL_ONE_IALLOCATOR,
424
  OPT_COMPL_INST_ADD_NODES,
425
  ])
426

    
427

    
428
class CliOption(Option):
429
  """Custom option class for optparse.
430

431
  """
432
  ATTRS = Option.ATTRS + [
433
    "completion_suggest",
434
    ]
435
  TYPES = Option.TYPES + (
436
    "identkeyval",
437
    "keyval",
438
    "unit",
439
    )
440
  TYPE_CHECKER = Option.TYPE_CHECKER.copy()
441
  TYPE_CHECKER["identkeyval"] = check_ident_key_val
442
  TYPE_CHECKER["keyval"] = check_key_val
443
  TYPE_CHECKER["unit"] = check_unit
444

    
445

    
446
# optparse.py sets make_option, so we do it for our own option class, too
447
cli_option = CliOption
448

    
449

    
450
DEBUG_OPT = cli_option("-d", "--debug", default=False,
451
                       action="store_true",
452
                       help="Turn debugging on")
453

    
454
NOHDR_OPT = cli_option("--no-headers", default=False,
455
                       action="store_true", dest="no_headers",
456
                       help="Don't display column headers")
457

    
458
SEP_OPT = cli_option("--separator", default=None,
459
                     action="store", dest="separator",
460
                     help=("Separator between output fields"
461
                           " (defaults to one space)"))
462

    
463
USEUNITS_OPT = cli_option("--units", default=None,
464
                          dest="units", choices=('h', 'm', 'g', 't'),
465
                          help="Specify units for output (one of hmgt)")
466

    
467
FIELDS_OPT = cli_option("-o", "--output", dest="output", action="store",
468
                        type="string", metavar="FIELDS",
469
                        help="Comma separated list of output fields")
470

    
471
FORCE_OPT = cli_option("-f", "--force", dest="force", action="store_true",
472
                       default=False, help="Force the operation")
473

    
474
CONFIRM_OPT = cli_option("--yes", dest="confirm", action="store_true",
475
                         default=False, help="Do not require confirmation")
476

    
477
TAG_SRC_OPT = cli_option("--from", dest="tags_source",
478
                         default=None, help="File with tag names")
479

    
480
SUBMIT_OPT = cli_option("--submit", dest="submit_only",
481
                        default=False, action="store_true",
482
                        help=("Submit the job and return the job ID, but"
483
                              " don't wait for the job to finish"))
484

    
485
SYNC_OPT = cli_option("--sync", dest="do_locking",
486
                      default=False, action="store_true",
487
                      help=("Grab locks while doing the queries"
488
                            " in order to ensure more consistent results"))
489

    
490
_DRY_RUN_OPT = cli_option("--dry-run", default=False,
491
                          action="store_true",
492
                          help=("Do not execute the operation, just run the"
493
                                " check steps and verify it it could be"
494
                                " executed"))
495

    
496
VERBOSE_OPT = cli_option("-v", "--verbose", default=False,
497
                         action="store_true",
498
                         help="Increase the verbosity of the operation")
499

    
500
DEBUG_SIMERR_OPT = cli_option("--debug-simulate-errors", default=False,
501
                              action="store_true", dest="simulate_errors",
502
                              help="Debugging option that makes the operation"
503
                              " treat most runtime checks as failed")
504

    
505
NWSYNC_OPT = cli_option("--no-wait-for-sync", dest="wait_for_sync",
506
                        default=True, action="store_false",
507
                        help="Don't wait for sync (DANGEROUS!)")
508

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

    
515
NONICS_OPT = cli_option("--no-nics", default=False, action="store_true",
516
                        help="Do not create any network cards for"
517
                        " the instance")
518

    
519
FILESTORE_DIR_OPT = cli_option("--file-storage-dir", dest="file_storage_dir",
520
                               help="Relative path under default cluster-wide"
521
                               " file storage dir to store file-based disks",
522
                               default=None, metavar="<DIR>")
523

    
524
FILESTORE_DRIVER_OPT = cli_option("--file-driver", dest="file_driver",
525
                                  help="Driver to use for image files",
526
                                  default="loop", metavar="<DRIVER>",
527
                                  choices=list(constants.FILE_DRIVER))
528

    
529
IALLOCATOR_OPT = cli_option("-I", "--iallocator", metavar="<NAME>",
530
                            help="Select nodes for the instance automatically"
531
                            " using the <NAME> iallocator plugin",
532
                            default=None, type="string",
533
                            completion_suggest=OPT_COMPL_ONE_IALLOCATOR)
534

    
535
OS_OPT = cli_option("-o", "--os-type", dest="os", help="What OS to run",
536
                    metavar="<os>",
537
                    completion_suggest=OPT_COMPL_ONE_OS)
538

    
539
BACKEND_OPT = cli_option("-B", "--backend-parameters", dest="beparams",
540
                         type="keyval", default={},
541
                         help="Backend parameters")
542

    
543
HVOPTS_OPT =  cli_option("-H", "--hypervisor-parameters", type="keyval",
544
                         default={}, dest="hvparams",
545
                         help="Hypervisor parameters")
546

    
547
HYPERVISOR_OPT = cli_option("-H", "--hypervisor-parameters", dest="hypervisor",
548
                            help="Hypervisor and hypervisor options, in the"
549
                            " format hypervisor:option=value,option=value,...",
550
                            default=None, type="identkeyval")
551

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

    
557
NOIPCHECK_OPT = cli_option("--no-ip-check", dest="ip_check", default=True,
558
                           action="store_false",
559
                           help="Don't check that the instance's IP"
560
                           " is alive")
561

    
562
NET_OPT = cli_option("--net",
563
                     help="NIC parameters", default=[],
564
                     dest="nics", action="append", type="identkeyval")
565

    
566
DISK_OPT = cli_option("--disk", help="Disk parameters", default=[],
567
                      dest="disks", action="append", type="identkeyval")
568

    
569
DISKIDX_OPT = cli_option("--disks", dest="disks", default=None,
570
                         help="Comma-separated list of disks"
571
                         " indices to act on (e.g. 0,2) (optional,"
572
                         " defaults to all disks)")
573

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

    
579
IGNORE_CONSIST_OPT = cli_option("--ignore-consistency",
580
                                dest="ignore_consistency",
581
                                action="store_true", default=False,
582
                                help="Ignore the consistency of the disks on"
583
                                " the secondary")
584

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

    
591
NODE_PLACEMENT_OPT = cli_option("-n", "--node", dest="node",
592
                                help="Target node and optional secondary node",
593
                                metavar="<pnode>[:<snode>]",
594
                                completion_suggest=OPT_COMPL_INST_ADD_NODES)
595

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

    
602
SINGLE_NODE_OPT = cli_option("-n", "--node", dest="node", help="Target node",
603
                             metavar="<node>",
604
                             completion_suggest=OPT_COMPL_ONE_NODE)
605

    
606
NOSTART_OPT = cli_option("--no-start", dest="start", default=True,
607
                         action="store_false",
608
                         help="Don't start the instance after creation")
609

    
610
SHOWCMD_OPT = cli_option("--show-cmd", dest="show_command",
611
                         action="store_true", default=False,
612
                         help="Show command instead of executing it")
613

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

    
623
STATIC_OPT = cli_option("-s", "--static", dest="static",
624
                        action="store_true", default=False,
625
                        help="Only show configuration data, not runtime data")
626

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

    
632
SELECT_OS_OPT = cli_option("--select-os", dest="select_os",
633
                           action="store_true", default=False,
634
                           help="Interactive OS reinstall, lists available"
635
                           " OS templates for selection")
636

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

    
643
NEW_SECONDARY_OPT = cli_option("-n", "--new-secondary", dest="dst_node",
644
                               help="Specifies the new secondary node",
645
                               metavar="NODE", default=None,
646
                               completion_suggest=OPT_COMPL_ONE_NODE)
647

    
648
ON_PRIMARY_OPT = cli_option("-p", "--on-primary", dest="on_primary",
649
                            default=False, action="store_true",
650
                            help="Replace the disk(s) on the primary"
651
                            " node (only for the drbd template)")
652

    
653
ON_SECONDARY_OPT = cli_option("-s", "--on-secondary", dest="on_secondary",
654
                              default=False, action="store_true",
655
                              help="Replace the disk(s) on the secondary"
656
                              " node (only for the drbd template)")
657

    
658
AUTO_REPLACE_OPT = cli_option("-a", "--auto", dest="auto",
659
                              default=False, action="store_true",
660
                              help="Automatically replace faulty disks"
661
                              " (only for the drbd template)")
662

    
663

    
664
def _ParseArgs(argv, commands, aliases):
665
  """Parser for the command line arguments.
666

667
  This function parses the arguments and returns the function which
668
  must be executed together with its (modified) arguments.
669

670
  @param argv: the command line
671
  @param commands: dictionary with special contents, see the design
672
      doc for cmdline handling
673
  @param aliases: dictionary with command aliases {'alias': 'target, ...}
674

675
  """
676
  if len(argv) == 0:
677
    binary = "<command>"
678
  else:
679
    binary = argv[0].split("/")[-1]
680

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

    
687
  if len(argv) < 2 or not (argv[1] in commands or
688
                           argv[1] in aliases):
689
    # let's do a nice thing
690
    sortedcmds = commands.keys()
691
    sortedcmds.sort()
692

    
693
    ToStdout("Usage: %s {command} [options...] [argument...]", binary)
694
    ToStdout("%s <command> --help to see details, or man %s", binary, binary)
695
    ToStdout("")
696

    
697
    # compute the max line length for cmd + usage
698
    mlen = max([len(" %s" % cmd) for cmd in commands])
699
    mlen = min(60, mlen) # should not get here...
700

    
701
    # and format a nice command list
702
    ToStdout("Commands:")
703
    for cmd in sortedcmds:
704
      cmdstr = " %s" % (cmd,)
705
      help_text = commands[cmd][4]
706
      help_lines = textwrap.wrap(help_text, 79 - 3 - mlen)
707
      ToStdout("%-*s - %s", mlen, cmdstr, help_lines.pop(0))
708
      for line in help_lines:
709
        ToStdout("%-*s   %s", mlen, "", line)
710

    
711
    ToStdout("")
712

    
713
    return None, None, None
714

    
715
  # get command, unalias it, and look it up in commands
716
  cmd = argv.pop(1)
717
  if cmd in aliases:
718
    if cmd in commands:
719
      raise errors.ProgrammerError("Alias '%s' overrides an existing"
720
                                   " command" % cmd)
721

    
722
    if aliases[cmd] not in commands:
723
      raise errors.ProgrammerError("Alias '%s' maps to non-existing"
724
                                   " command '%s'" % (cmd, aliases[cmd]))
725

    
726
    cmd = aliases[cmd]
727

    
728
  func, args_def, parser_opts, usage, description = commands[cmd]
729
  parser = OptionParser(option_list=parser_opts + [_DRY_RUN_OPT],
730
                        description=description,
731
                        formatter=TitledHelpFormatter(),
732
                        usage="%%prog %s %s" % (cmd, usage))
733
  parser.disable_interspersed_args()
734
  options, args = parser.parse_args()
735

    
736
  if not _CheckArguments(cmd, args_def, args):
737
    return None, None, None
738

    
739
  return func, options, args
740

    
741

    
742
def _CheckArguments(cmd, args_def, args):
743
  """Verifies the arguments using the argument definition.
744

745
  Algorithm:
746

747
    1. Abort with error if values specified by user but none expected.
748

749
    1. For each argument in definition
750

751
      1. Keep running count of minimum number of values (min_count)
752
      1. Keep running count of maximum number of values (max_count)
753
      1. If it has an unlimited number of values
754

755
        1. Abort with error if it's not the last argument in the definition
756

757
    1. If last argument has limited number of values
758

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

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

763
  """
764
  if args and not args_def:
765
    ToStderr("Error: Command %s expects no arguments", cmd)
766
    return False
767

    
768
  min_count = None
769
  max_count = None
770
  check_max = None
771

    
772
  last_idx = len(args_def) - 1
773

    
774
  for idx, arg in enumerate(args_def):
775
    if min_count is None:
776
      min_count = arg.min
777
    elif arg.min is not None:
778
      min_count += arg.min
779

    
780
    if max_count is None:
781
      max_count = arg.max
782
    elif arg.max is not None:
783
      max_count += arg.max
784

    
785
    if idx == last_idx:
786
      check_max = (arg.max is not None)
787

    
788
    elif arg.max is None:
789
      raise errors.ProgrammerError("Only the last argument can have max=None")
790

    
791
  if check_max:
792
    # Command with exact number of arguments
793
    if (min_count is not None and max_count is not None and
794
        min_count == max_count and len(args) != min_count):
795
      ToStderr("Error: Command %s expects %d argument(s)", cmd, min_count)
796
      return False
797

    
798
    # Command with limited number of arguments
799
    if max_count is not None and len(args) > max_count:
800
      ToStderr("Error: Command %s expects only %d argument(s)",
801
               cmd, max_count)
802
      return False
803

    
804
  # Command with some required arguments
805
  if min_count is not None and len(args) < min_count:
806
    ToStderr("Error: Command %s expects at least %d argument(s)",
807
             cmd, min_count)
808
    return False
809

    
810
  return True
811

    
812

    
813
def SplitNodeOption(value):
814
  """Splits the value of a --node option.
815

816
  """
817
  if value and ':' in value:
818
    return value.split(':', 1)
819
  else:
820
    return (value, None)
821

    
822

    
823
def UsesRPC(fn):
824
  def wrapper(*args, **kwargs):
825
    rpc.Init()
826
    try:
827
      return fn(*args, **kwargs)
828
    finally:
829
      rpc.Shutdown()
830
  return wrapper
831

    
832

    
833
def AskUser(text, choices=None):
834
  """Ask the user a question.
835

836
  @param text: the question to ask
837

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

843
  @return: one of the return values from the choices list; if input is
844
      not possible (i.e. not running with a tty, we return the last
845
      entry from the list
846

847
  """
848
  if choices is None:
849
    choices = [('y', True, 'Perform the operation'),
850
               ('n', False, 'Do not perform the operation')]
851
  if not choices or not isinstance(choices, list):
852
    raise errors.ProgrammerError("Invalid choices argument to AskUser")
853
  for entry in choices:
854
    if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
855
      raise errors.ProgrammerError("Invalid choices element to AskUser")
856

    
857
  answer = choices[-1][1]
858
  new_text = []
859
  for line in text.splitlines():
860
    new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
861
  text = "\n".join(new_text)
862
  try:
863
    f = file("/dev/tty", "a+")
864
  except IOError:
865
    return answer
866
  try:
867
    chars = [entry[0] for entry in choices]
868
    chars[-1] = "[%s]" % chars[-1]
869
    chars.append('?')
870
    maps = dict([(entry[0], entry[1]) for entry in choices])
871
    while True:
872
      f.write(text)
873
      f.write('\n')
874
      f.write("/".join(chars))
875
      f.write(": ")
876
      line = f.readline(2).strip().lower()
877
      if line in maps:
878
        answer = maps[line]
879
        break
880
      elif line == '?':
881
        for entry in choices:
882
          f.write(" %s - %s\n" % (entry[0], entry[2]))
883
        f.write("\n")
884
        continue
885
  finally:
886
    f.close()
887
  return answer
888

    
889

    
890
class JobSubmittedException(Exception):
891
  """Job was submitted, client should exit.
892

893
  This exception has one argument, the ID of the job that was
894
  submitted. The handler should print this ID.
895

896
  This is not an error, just a structured way to exit from clients.
897

898
  """
899

    
900

    
901
def SendJob(ops, cl=None):
902
  """Function to submit an opcode without waiting for the results.
903

904
  @type ops: list
905
  @param ops: list of opcodes
906
  @type cl: luxi.Client
907
  @param cl: the luxi client to use for communicating with the master;
908
             if None, a new client will be created
909

910
  """
911
  if cl is None:
912
    cl = GetClient()
913

    
914
  job_id = cl.SubmitJob(ops)
915

    
916
  return job_id
917

    
918

    
919
def PollJob(job_id, cl=None, feedback_fn=None):
920
  """Function to poll for the result of a job.
921

922
  @type job_id: job identified
923
  @param job_id: the job to poll for results
924
  @type cl: luxi.Client
925
  @param cl: the luxi client to use for communicating with the master;
926
             if None, a new client will be created
927

928
  """
929
  if cl is None:
930
    cl = GetClient()
931

    
932
  prev_job_info = None
933
  prev_logmsg_serial = None
934

    
935
  while True:
936
    result = cl.WaitForJobChange(job_id, ["status"], prev_job_info,
937
                                 prev_logmsg_serial)
938
    if not result:
939
      # job not found, go away!
940
      raise errors.JobLost("Job with id %s lost" % job_id)
941

    
942
    # Split result, a tuple of (field values, log entries)
943
    (job_info, log_entries) = result
944
    (status, ) = job_info
945

    
946
    if log_entries:
947
      for log_entry in log_entries:
948
        (serial, timestamp, _, message) = log_entry
949
        if callable(feedback_fn):
950
          feedback_fn(log_entry[1:])
951
        else:
952
          encoded = utils.SafeEncode(message)
953
          ToStdout("%s %s", time.ctime(utils.MergeTime(timestamp)), encoded)
954
        prev_logmsg_serial = max(prev_logmsg_serial, serial)
955

    
956
    # TODO: Handle canceled and archived jobs
957
    elif status in (constants.JOB_STATUS_SUCCESS,
958
                    constants.JOB_STATUS_ERROR,
959
                    constants.JOB_STATUS_CANCELING,
960
                    constants.JOB_STATUS_CANCELED):
961
      break
962

    
963
    prev_job_info = job_info
964

    
965
  jobs = cl.QueryJobs([job_id], ["status", "opstatus", "opresult"])
966
  if not jobs:
967
    raise errors.JobLost("Job with id %s lost" % job_id)
968

    
969
  status, opstatus, result = jobs[0]
970
  if status == constants.JOB_STATUS_SUCCESS:
971
    return result
972
  elif status in (constants.JOB_STATUS_CANCELING,
973
                  constants.JOB_STATUS_CANCELED):
974
    raise errors.OpExecError("Job was canceled")
975
  else:
976
    has_ok = False
977
    for idx, (status, msg) in enumerate(zip(opstatus, result)):
978
      if status == constants.OP_STATUS_SUCCESS:
979
        has_ok = True
980
      elif status == constants.OP_STATUS_ERROR:
981
        errors.MaybeRaise(msg)
982
        if has_ok:
983
          raise errors.OpExecError("partial failure (opcode %d): %s" %
984
                                   (idx, msg))
985
        else:
986
          raise errors.OpExecError(str(msg))
987
    # default failure mode
988
    raise errors.OpExecError(result)
989

    
990

    
991
def SubmitOpCode(op, cl=None, feedback_fn=None):
992
  """Legacy function to submit an opcode.
993

994
  This is just a simple wrapper over the construction of the processor
995
  instance. It should be extended to better handle feedback and
996
  interaction functions.
997

998
  """
999
  if cl is None:
1000
    cl = GetClient()
1001

    
1002
  job_id = SendJob([op], cl)
1003

    
1004
  op_results = PollJob(job_id, cl=cl, feedback_fn=feedback_fn)
1005

    
1006
  return op_results[0]
1007

    
1008

    
1009
def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
1010
  """Wrapper around SubmitOpCode or SendJob.
1011

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

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

1019
  """
1020
  if opts and opts.dry_run:
1021
    op.dry_run = opts.dry_run
1022
  if opts and opts.submit_only:
1023
    job_id = SendJob([op], cl=cl)
1024
    raise JobSubmittedException(job_id)
1025
  else:
1026
    return SubmitOpCode(op, cl=cl, feedback_fn=feedback_fn)
1027

    
1028

    
1029
def GetClient():
1030
  # TODO: Cache object?
1031
  try:
1032
    client = luxi.Client()
1033
  except luxi.NoMasterError:
1034
    master, myself = ssconf.GetMasterAndMyself()
1035
    if master != myself:
1036
      raise errors.OpPrereqError("This is not the master node, please connect"
1037
                                 " to node '%s' and rerun the command" %
1038
                                 master)
1039
    else:
1040
      raise
1041
  return client
1042

    
1043

    
1044
def FormatError(err):
1045
  """Return a formatted error message for a given error.
1046

1047
  This function takes an exception instance and returns a tuple
1048
  consisting of two values: first, the recommended exit code, and
1049
  second, a string describing the error message (not
1050
  newline-terminated).
1051

1052
  """
1053
  retcode = 1
1054
  obuf = StringIO()
1055
  msg = str(err)
1056
  if isinstance(err, errors.ConfigurationError):
1057
    txt = "Corrupt configuration file: %s" % msg
1058
    logging.error(txt)
1059
    obuf.write(txt + "\n")
1060
    obuf.write("Aborting.")
1061
    retcode = 2
1062
  elif isinstance(err, errors.HooksAbort):
1063
    obuf.write("Failure: hooks execution failed:\n")
1064
    for node, script, out in err.args[0]:
1065
      if out:
1066
        obuf.write("  node: %s, script: %s, output: %s\n" %
1067
                   (node, script, out))
1068
      else:
1069
        obuf.write("  node: %s, script: %s (no output)\n" %
1070
                   (node, script))
1071
  elif isinstance(err, errors.HooksFailure):
1072
    obuf.write("Failure: hooks general failure: %s" % msg)
1073
  elif isinstance(err, errors.ResolverError):
1074
    this_host = utils.HostInfo.SysName()
1075
    if err.args[0] == this_host:
1076
      msg = "Failure: can't resolve my own hostname ('%s')"
1077
    else:
1078
      msg = "Failure: can't resolve hostname '%s'"
1079
    obuf.write(msg % err.args[0])
1080
  elif isinstance(err, errors.OpPrereqError):
1081
    obuf.write("Failure: prerequisites not met for this"
1082
               " operation:\n%s" % msg)
1083
  elif isinstance(err, errors.OpExecError):
1084
    obuf.write("Failure: command execution error:\n%s" % msg)
1085
  elif isinstance(err, errors.TagError):
1086
    obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
1087
  elif isinstance(err, errors.JobQueueDrainError):
1088
    obuf.write("Failure: the job queue is marked for drain and doesn't"
1089
               " accept new requests\n")
1090
  elif isinstance(err, errors.JobQueueFull):
1091
    obuf.write("Failure: the job queue is full and doesn't accept new"
1092
               " job submissions until old jobs are archived\n")
1093
  elif isinstance(err, errors.TypeEnforcementError):
1094
    obuf.write("Parameter Error: %s" % msg)
1095
  elif isinstance(err, errors.ParameterError):
1096
    obuf.write("Failure: unknown/wrong parameter name '%s'" % msg)
1097
  elif isinstance(err, errors.GenericError):
1098
    obuf.write("Unhandled Ganeti error: %s" % msg)
1099
  elif isinstance(err, luxi.NoMasterError):
1100
    obuf.write("Cannot communicate with the master daemon.\nIs it running"
1101
               " and listening for connections?")
1102
  elif isinstance(err, luxi.TimeoutError):
1103
    obuf.write("Timeout while talking to the master daemon. Error:\n"
1104
               "%s" % msg)
1105
  elif isinstance(err, luxi.ProtocolError):
1106
    obuf.write("Unhandled protocol error while talking to the master daemon:\n"
1107
               "%s" % msg)
1108
  elif isinstance(err, JobSubmittedException):
1109
    obuf.write("JobID: %s\n" % err.args[0])
1110
    retcode = 0
1111
  else:
1112
    obuf.write("Unhandled exception: %s" % msg)
1113
  return retcode, obuf.getvalue().rstrip('\n')
1114

    
1115

    
1116
def GenericMain(commands, override=None, aliases=None):
1117
  """Generic main function for all the gnt-* commands.
1118

1119
  Arguments:
1120
    - commands: a dictionary with a special structure, see the design doc
1121
                for command line handling.
1122
    - override: if not None, we expect a dictionary with keys that will
1123
                override command line options; this can be used to pass
1124
                options from the scripts to generic functions
1125
    - aliases: dictionary with command aliases {'alias': 'target, ...}
1126

1127
  """
1128
  # save the program name and the entire command line for later logging
1129
  if sys.argv:
1130
    binary = os.path.basename(sys.argv[0]) or sys.argv[0]
1131
    if len(sys.argv) >= 2:
1132
      binary += " " + sys.argv[1]
1133
      old_cmdline = " ".join(sys.argv[2:])
1134
    else:
1135
      old_cmdline = ""
1136
  else:
1137
    binary = "<unknown program>"
1138
    old_cmdline = ""
1139

    
1140
  if aliases is None:
1141
    aliases = {}
1142

    
1143
  try:
1144
    func, options, args = _ParseArgs(sys.argv, commands, aliases)
1145
  except errors.ParameterError, err:
1146
    result, err_msg = FormatError(err)
1147
    ToStderr(err_msg)
1148
    return 1
1149

    
1150
  if func is None: # parse error
1151
    return 1
1152

    
1153
  if override is not None:
1154
    for key, val in override.iteritems():
1155
      setattr(options, key, val)
1156

    
1157
  utils.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
1158
                     stderr_logging=True, program=binary)
1159

    
1160
  if old_cmdline:
1161
    logging.info("run with arguments '%s'", old_cmdline)
1162
  else:
1163
    logging.info("run with no arguments")
1164

    
1165
  try:
1166
    result = func(options, args)
1167
  except (errors.GenericError, luxi.ProtocolError,
1168
          JobSubmittedException), err:
1169
    result, err_msg = FormatError(err)
1170
    logging.exception("Error during command processing")
1171
    ToStderr(err_msg)
1172

    
1173
  return result
1174

    
1175

    
1176
def GenerateTable(headers, fields, separator, data,
1177
                  numfields=None, unitfields=None,
1178
                  units=None):
1179
  """Prints a table with headers and different fields.
1180

1181
  @type headers: dict
1182
  @param headers: dictionary mapping field names to headers for
1183
      the table
1184
  @type fields: list
1185
  @param fields: the field names corresponding to each row in
1186
      the data field
1187
  @param separator: the separator to be used; if this is None,
1188
      the default 'smart' algorithm is used which computes optimal
1189
      field width, otherwise just the separator is used between
1190
      each field
1191
  @type data: list
1192
  @param data: a list of lists, each sublist being one row to be output
1193
  @type numfields: list
1194
  @param numfields: a list with the fields that hold numeric
1195
      values and thus should be right-aligned
1196
  @type unitfields: list
1197
  @param unitfields: a list with the fields that hold numeric
1198
      values that should be formatted with the units field
1199
  @type units: string or None
1200
  @param units: the units we should use for formatting, or None for
1201
      automatic choice (human-readable for non-separator usage, otherwise
1202
      megabytes); this is a one-letter string
1203

1204
  """
1205
  if units is None:
1206
    if separator:
1207
      units = "m"
1208
    else:
1209
      units = "h"
1210

    
1211
  if numfields is None:
1212
    numfields = []
1213
  if unitfields is None:
1214
    unitfields = []
1215

    
1216
  numfields = utils.FieldSet(*numfields)
1217
  unitfields = utils.FieldSet(*unitfields)
1218

    
1219
  format_fields = []
1220
  for field in fields:
1221
    if headers and field not in headers:
1222
      # TODO: handle better unknown fields (either revert to old
1223
      # style of raising exception, or deal more intelligently with
1224
      # variable fields)
1225
      headers[field] = field
1226
    if separator is not None:
1227
      format_fields.append("%s")
1228
    elif numfields.Matches(field):
1229
      format_fields.append("%*s")
1230
    else:
1231
      format_fields.append("%-*s")
1232

    
1233
  if separator is None:
1234
    mlens = [0 for name in fields]
1235
    format = ' '.join(format_fields)
1236
  else:
1237
    format = separator.replace("%", "%%").join(format_fields)
1238

    
1239
  for row in data:
1240
    if row is None:
1241
      continue
1242
    for idx, val in enumerate(row):
1243
      if unitfields.Matches(fields[idx]):
1244
        try:
1245
          val = int(val)
1246
        except ValueError:
1247
          pass
1248
        else:
1249
          val = row[idx] = utils.FormatUnit(val, units)
1250
      val = row[idx] = str(val)
1251
      if separator is None:
1252
        mlens[idx] = max(mlens[idx], len(val))
1253

    
1254
  result = []
1255
  if headers:
1256
    args = []
1257
    for idx, name in enumerate(fields):
1258
      hdr = headers[name]
1259
      if separator is None:
1260
        mlens[idx] = max(mlens[idx], len(hdr))
1261
        args.append(mlens[idx])
1262
      args.append(hdr)
1263
    result.append(format % tuple(args))
1264

    
1265
  for line in data:
1266
    args = []
1267
    if line is None:
1268
      line = ['-' for _ in fields]
1269
    for idx in xrange(len(fields)):
1270
      if separator is None:
1271
        args.append(mlens[idx])
1272
      args.append(line[idx])
1273
    result.append(format % tuple(args))
1274

    
1275
  return result
1276

    
1277

    
1278
def FormatTimestamp(ts):
1279
  """Formats a given timestamp.
1280

1281
  @type ts: timestamp
1282
  @param ts: a timeval-type timestamp, a tuple of seconds and microseconds
1283

1284
  @rtype: string
1285
  @return: a string with the formatted timestamp
1286

1287
  """
1288
  if not isinstance (ts, (tuple, list)) or len(ts) != 2:
1289
    return '?'
1290
  sec, usec = ts
1291
  return time.strftime("%F %T", time.localtime(sec)) + ".%06d" % usec
1292

    
1293

    
1294
def ParseTimespec(value):
1295
  """Parse a time specification.
1296

1297
  The following suffixed will be recognized:
1298

1299
    - s: seconds
1300
    - m: minutes
1301
    - h: hours
1302
    - d: day
1303
    - w: weeks
1304

1305
  Without any suffix, the value will be taken to be in seconds.
1306

1307
  """
1308
  value = str(value)
1309
  if not value:
1310
    raise errors.OpPrereqError("Empty time specification passed")
1311
  suffix_map = {
1312
    's': 1,
1313
    'm': 60,
1314
    'h': 3600,
1315
    'd': 86400,
1316
    'w': 604800,
1317
    }
1318
  if value[-1] not in suffix_map:
1319
    try:
1320
      value = int(value)
1321
    except ValueError:
1322
      raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1323
  else:
1324
    multiplier = suffix_map[value[-1]]
1325
    value = value[:-1]
1326
    if not value: # no data left after stripping the suffix
1327
      raise errors.OpPrereqError("Invalid time specification (only"
1328
                                 " suffix passed)")
1329
    try:
1330
      value = int(value) * multiplier
1331
    except ValueError:
1332
      raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1333
  return value
1334

    
1335

    
1336
def GetOnlineNodes(nodes, cl=None, nowarn=False):
1337
  """Returns the names of online nodes.
1338

1339
  This function will also log a warning on stderr with the names of
1340
  the online nodes.
1341

1342
  @param nodes: if not empty, use only this subset of nodes (minus the
1343
      offline ones)
1344
  @param cl: if not None, luxi client to use
1345
  @type nowarn: boolean
1346
  @param nowarn: by default, this function will output a note with the
1347
      offline nodes that are skipped; if this parameter is True the
1348
      note is not displayed
1349

1350
  """
1351
  if cl is None:
1352
    cl = GetClient()
1353

    
1354
  result = cl.QueryNodes(names=nodes, fields=["name", "offline"],
1355
                         use_locking=False)
1356
  offline = [row[0] for row in result if row[1]]
1357
  if offline and not nowarn:
1358
    ToStderr("Note: skipping offline node(s): %s" % ", ".join(offline))
1359
  return [row[0] for row in result if not row[1]]
1360

    
1361

    
1362
def _ToStream(stream, txt, *args):
1363
  """Write a message to a stream, bypassing the logging system
1364

1365
  @type stream: file object
1366
  @param stream: the file to which we should write
1367
  @type txt: str
1368
  @param txt: the message
1369

1370
  """
1371
  if args:
1372
    args = tuple(args)
1373
    stream.write(txt % args)
1374
  else:
1375
    stream.write(txt)
1376
  stream.write('\n')
1377
  stream.flush()
1378

    
1379

    
1380
def ToStdout(txt, *args):
1381
  """Write a message to stdout only, bypassing the logging system
1382

1383
  This is just a wrapper over _ToStream.
1384

1385
  @type txt: str
1386
  @param txt: the message
1387

1388
  """
1389
  _ToStream(sys.stdout, txt, *args)
1390

    
1391

    
1392
def ToStderr(txt, *args):
1393
  """Write a message to stderr only, bypassing the logging system
1394

1395
  This is just a wrapper over _ToStream.
1396

1397
  @type txt: str
1398
  @param txt: the message
1399

1400
  """
1401
  _ToStream(sys.stderr, txt, *args)
1402

    
1403

    
1404
class JobExecutor(object):
1405
  """Class which manages the submission and execution of multiple jobs.
1406

1407
  Note that instances of this class should not be reused between
1408
  GetResults() calls.
1409

1410
  """
1411
  def __init__(self, cl=None, verbose=True):
1412
    self.queue = []
1413
    if cl is None:
1414
      cl = GetClient()
1415
    self.cl = cl
1416
    self.verbose = verbose
1417
    self.jobs = []
1418

    
1419
  def QueueJob(self, name, *ops):
1420
    """Record a job for later submit.
1421

1422
    @type name: string
1423
    @param name: a description of the job, will be used in WaitJobSet
1424
    """
1425
    self.queue.append((name, ops))
1426

    
1427
  def SubmitPending(self):
1428
    """Submit all pending jobs.
1429

1430
    """
1431
    results = self.cl.SubmitManyJobs([row[1] for row in self.queue])
1432
    for ((status, data), (name, _)) in zip(results, self.queue):
1433
      self.jobs.append((status, data, name))
1434

    
1435
  def GetResults(self):
1436
    """Wait for and return the results of all jobs.
1437

1438
    @rtype: list
1439
    @return: list of tuples (success, job results), in the same order
1440
        as the submitted jobs; if a job has failed, instead of the result
1441
        there will be the error message
1442

1443
    """
1444
    if not self.jobs:
1445
      self.SubmitPending()
1446
    results = []
1447
    if self.verbose:
1448
      ok_jobs = [row[1] for row in self.jobs if row[0]]
1449
      if ok_jobs:
1450
        ToStdout("Submitted jobs %s", ", ".join(ok_jobs))
1451
    for submit_status, jid, name in self.jobs:
1452
      if not submit_status:
1453
        ToStderr("Failed to submit job for %s: %s", name, jid)
1454
        results.append((False, jid))
1455
        continue
1456
      if self.verbose:
1457
        ToStdout("Waiting for job %s for %s...", jid, name)
1458
      try:
1459
        job_result = PollJob(jid, cl=self.cl)
1460
        success = True
1461
      except (errors.GenericError, luxi.ProtocolError), err:
1462
        _, job_result = FormatError(err)
1463
        success = False
1464
        # the error message will always be shown, verbose or not
1465
        ToStderr("Job %s for %s has failed: %s", jid, name, job_result)
1466

    
1467
      results.append((success, job_result))
1468
    return results
1469

    
1470
  def WaitOrShow(self, wait):
1471
    """Wait for job results or only print the job IDs.
1472

1473
    @type wait: boolean
1474
    @param wait: whether to wait or not
1475

1476
    """
1477
    if wait:
1478
      return self.GetResults()
1479
    else:
1480
      if not self.jobs:
1481
        self.SubmitPending()
1482
      for status, result, name in self.jobs:
1483
        if status:
1484
          ToStdout("%s: %s", result, name)
1485
        else:
1486
          ToStderr("Failure for %s: %s", name, result)