Statistics
| Branch: | Tag: | Revision:

root / lib / cli.py @ 771734c9

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

    
145
NO_PREFIX = "no_"
146
UN_PREFIX = "-"
147

    
148

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

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

    
158

    
159
class ArgSuggest(_Argument):
160
  """Suggesting argument.
161

162
  Value can be any of the ones passed to the constructor.
163

164
  """
165
  def __init__(self, min=0, max=None, choices=None):
166
    _Argument.__init__(self, min=min, max=max)
167
    self.choices = choices
168

    
169
  def __repr__(self):
170
    return ("<%s min=%s max=%s choices=%r>" %
171
            (self.__class__.__name__, self.min, self.max, self.choices))
172

    
173

    
174
class ArgChoice(ArgSuggest):
175
  """Choice argument.
176

177
  Value can be any of the ones passed to the constructor. Like L{ArgSuggest},
178
  but value must be one of the choices.
179

180
  """
181

    
182

    
183
class ArgUnknown(_Argument):
184
  """Unknown argument to program (e.g. determined at runtime).
185

186
  """
187

    
188

    
189
class ArgInstance(_Argument):
190
  """Instances argument.
191

192
  """
193

    
194

    
195
class ArgNode(_Argument):
196
  """Node argument.
197

198
  """
199

    
200
class ArgJobId(_Argument):
201
  """Job ID argument.
202

203
  """
204

    
205

    
206
class ArgFile(_Argument):
207
  """File path argument.
208

209
  """
210

    
211

    
212
class ArgCommand(_Argument):
213
  """Command argument.
214

215
  """
216

    
217

    
218
class ArgHost(_Argument):
219
  """Host argument.
220

221
  """
222

    
223

    
224
ARGS_NONE = []
225
ARGS_MANY_INSTANCES = [ArgInstance()]
226
ARGS_MANY_NODES = [ArgNode()]
227
ARGS_ONE_INSTANCE = [ArgInstance(min=1, max=1)]
228
ARGS_ONE_NODE = [ArgNode(min=1, max=1)]
229

    
230

    
231

    
232
def _ExtractTagsObject(opts, args):
233
  """Extract the tag type object.
234

235
  Note that this function will modify its args parameter.
236

237
  """
238
  if not hasattr(opts, "tag_type"):
239
    raise errors.ProgrammerError("tag_type not passed to _ExtractTagsObject")
240
  kind = opts.tag_type
241
  if kind == constants.TAG_CLUSTER:
242
    retval = kind, kind
243
  elif kind == constants.TAG_NODE or kind == constants.TAG_INSTANCE:
244
    if not args:
245
      raise errors.OpPrereqError("no arguments passed to the command")
246
    name = args.pop(0)
247
    retval = kind, name
248
  else:
249
    raise errors.ProgrammerError("Unhandled tag type '%s'" % kind)
250
  return retval
251

    
252

    
253
def _ExtendTags(opts, args):
254
  """Extend the args if a source file has been given.
255

256
  This function will extend the tags with the contents of the file
257
  passed in the 'tags_source' attribute of the opts parameter. A file
258
  named '-' will be replaced by stdin.
259

260
  """
261
  fname = opts.tags_source
262
  if fname is None:
263
    return
264
  if fname == "-":
265
    new_fh = sys.stdin
266
  else:
267
    new_fh = open(fname, "r")
268
  new_data = []
269
  try:
270
    # we don't use the nice 'new_data = [line.strip() for line in fh]'
271
    # because of python bug 1633941
272
    while True:
273
      line = new_fh.readline()
274
      if not line:
275
        break
276
      new_data.append(line.strip())
277
  finally:
278
    new_fh.close()
279
  args.extend(new_data)
280

    
281

    
282
def ListTags(opts, args):
283
  """List the tags on a given object.
284

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

290
  """
291
  kind, name = _ExtractTagsObject(opts, args)
292
  op = opcodes.OpGetTags(kind=kind, name=name)
293
  result = SubmitOpCode(op)
294
  result = list(result)
295
  result.sort()
296
  for tag in result:
297
    ToStdout(tag)
298

    
299

    
300
def AddTags(opts, args):
301
  """Add tags on a given object.
302

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

308
  """
309
  kind, name = _ExtractTagsObject(opts, args)
310
  _ExtendTags(opts, args)
311
  if not args:
312
    raise errors.OpPrereqError("No tags to be added")
313
  op = opcodes.OpAddTags(kind=kind, name=name, tags=args)
314
  SubmitOpCode(op)
315

    
316

    
317
def RemoveTags(opts, args):
318
  """Remove tags from a given object.
319

320
  This is a generic implementation that knows how to deal with all
321
  three cases of tag objects (cluster, node, instance). The opts
322
  argument is expected to contain a tag_type field denoting what
323
  object type we work on.
324

325
  """
326
  kind, name = _ExtractTagsObject(opts, args)
327
  _ExtendTags(opts, args)
328
  if not args:
329
    raise errors.OpPrereqError("No tags to be removed")
330
  op = opcodes.OpDelTags(kind=kind, name=name, tags=args)
331
  SubmitOpCode(op)
332

    
333

    
334
def check_unit(option, opt, value):
335
  """OptParsers custom converter for units.
336

337
  """
338
  try:
339
    return utils.ParseUnit(value)
340
  except errors.UnitParseError, err:
341
    raise OptionValueError("option %s: %s" % (opt, err))
342

    
343

    
344
def _SplitKeyVal(opt, data):
345
  """Convert a KeyVal string into a dict.
346

347
  This function will convert a key=val[,...] string into a dict. Empty
348
  values will be converted specially: keys which have the prefix 'no_'
349
  will have the value=False and the prefix stripped, the others will
350
  have value=True.
351

352
  @type opt: string
353
  @param opt: a string holding the option name for which we process the
354
      data, used in building error messages
355
  @type data: string
356
  @param data: a string of the format key=val,key=val,...
357
  @rtype: dict
358
  @return: {key=val, key=val}
359
  @raises errors.ParameterError: if there are duplicate keys
360

361
  """
362
  kv_dict = {}
363
  if data:
364
    for elem in data.split(","):
365
      if "=" in elem:
366
        key, val = elem.split("=", 1)
367
      else:
368
        if elem.startswith(NO_PREFIX):
369
          key, val = elem[len(NO_PREFIX):], False
370
        elif elem.startswith(UN_PREFIX):
371
          key, val = elem[len(UN_PREFIX):], None
372
        else:
373
          key, val = elem, True
374
      if key in kv_dict:
375
        raise errors.ParameterError("Duplicate key '%s' in option %s" %
376
                                    (key, opt))
377
      kv_dict[key] = val
378
  return kv_dict
379

    
380

    
381
def check_ident_key_val(option, opt, value):
382
  """Custom parser for ident:key=val,key=val options.
383

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

387
  """
388
  if ":" not in value:
389
    ident, rest = value, ''
390
  else:
391
    ident, rest = value.split(":", 1)
392

    
393
  if ident.startswith(NO_PREFIX):
394
    if rest:
395
      msg = "Cannot pass options when removing parameter groups: %s" % value
396
      raise errors.ParameterError(msg)
397
    retval = (ident[len(NO_PREFIX):], False)
398
  elif ident.startswith(UN_PREFIX):
399
    if rest:
400
      msg = "Cannot pass options when removing parameter groups: %s" % value
401
      raise errors.ParameterError(msg)
402
    retval = (ident[len(UN_PREFIX):], None)
403
  else:
404
    kv_dict = _SplitKeyVal(opt, rest)
405
    retval = (ident, kv_dict)
406
  return retval
407

    
408

    
409
def check_key_val(option, opt, value):
410
  """Custom parser class for key=val,key=val options.
411

412
  This will store the parsed values as a dict {key: val}.
413

414
  """
415
  return _SplitKeyVal(opt, value)
416

    
417

    
418
# completion_suggestion is normally a list. Using numeric values not evaluating
419
# to False for dynamic completion.
420
(OPT_COMPL_MANY_NODES,
421
 OPT_COMPL_ONE_NODE,
422
 OPT_COMPL_ONE_INSTANCE,
423
 OPT_COMPL_ONE_OS,
424
 OPT_COMPL_ONE_IALLOCATOR,
425
 OPT_COMPL_INST_ADD_NODES) = range(100, 106)
426

    
427
OPT_COMPL_ALL = frozenset([
428
  OPT_COMPL_MANY_NODES,
429
  OPT_COMPL_ONE_NODE,
430
  OPT_COMPL_ONE_INSTANCE,
431
  OPT_COMPL_ONE_OS,
432
  OPT_COMPL_ONE_IALLOCATOR,
433
  OPT_COMPL_INST_ADD_NODES,
434
  ])
435

    
436

    
437
class CliOption(Option):
438
  """Custom option class for optparse.
439

440
  """
441
  ATTRS = Option.ATTRS + [
442
    "completion_suggest",
443
    ]
444
  TYPES = Option.TYPES + (
445
    "identkeyval",
446
    "keyval",
447
    "unit",
448
    )
449
  TYPE_CHECKER = Option.TYPE_CHECKER.copy()
450
  TYPE_CHECKER["identkeyval"] = check_ident_key_val
451
  TYPE_CHECKER["keyval"] = check_key_val
452
  TYPE_CHECKER["unit"] = check_unit
453

    
454

    
455
# optparse.py sets make_option, so we do it for our own option class, too
456
cli_option = CliOption
457

    
458

    
459
_YESNO = ("yes", "no")
460
_YORNO = "yes|no"
461

    
462
DEBUG_OPT = cli_option("-d", "--debug", default=False,
463
                       action="store_true",
464
                       help="Turn debugging on")
465

    
466
NOHDR_OPT = cli_option("--no-headers", default=False,
467
                       action="store_true", dest="no_headers",
468
                       help="Don't display column headers")
469

    
470
SEP_OPT = cli_option("--separator", default=None,
471
                     action="store", dest="separator",
472
                     help=("Separator between output fields"
473
                           " (defaults to one space)"))
474

    
475
USEUNITS_OPT = cli_option("--units", default=None,
476
                          dest="units", choices=('h', 'm', 'g', 't'),
477
                          help="Specify units for output (one of hmgt)")
478

    
479
FIELDS_OPT = cli_option("-o", "--output", dest="output", action="store",
480
                        type="string", metavar="FIELDS",
481
                        help="Comma separated list of output fields")
482

    
483
FORCE_OPT = cli_option("-f", "--force", dest="force", action="store_true",
484
                       default=False, help="Force the operation")
485

    
486
CONFIRM_OPT = cli_option("--yes", dest="confirm", action="store_true",
487
                         default=False, help="Do not require confirmation")
488

    
489
TAG_SRC_OPT = cli_option("--from", dest="tags_source",
490
                         default=None, help="File with tag names")
491

    
492
SUBMIT_OPT = cli_option("--submit", dest="submit_only",
493
                        default=False, action="store_true",
494
                        help=("Submit the job and return the job ID, but"
495
                              " don't wait for the job to finish"))
496

    
497
SYNC_OPT = cli_option("--sync", dest="do_locking",
498
                      default=False, action="store_true",
499
                      help=("Grab locks while doing the queries"
500
                            " in order to ensure more consistent results"))
501

    
502
_DRY_RUN_OPT = cli_option("--dry-run", default=False,
503
                          action="store_true",
504
                          help=("Do not execute the operation, just run the"
505
                                " check steps and verify it it could be"
506
                                " executed"))
507

    
508
VERBOSE_OPT = cli_option("-v", "--verbose", default=False,
509
                         action="store_true",
510
                         help="Increase the verbosity of the operation")
511

    
512
DEBUG_SIMERR_OPT = cli_option("--debug-simulate-errors", default=False,
513
                              action="store_true", dest="simulate_errors",
514
                              help="Debugging option that makes the operation"
515
                              " treat most runtime checks as failed")
516

    
517
NWSYNC_OPT = cli_option("--no-wait-for-sync", dest="wait_for_sync",
518
                        default=True, action="store_false",
519
                        help="Don't wait for sync (DANGEROUS!)")
520

    
521
DISK_TEMPLATE_OPT = cli_option("-t", "--disk-template", dest="disk_template",
522
                               help="Custom disk setup (diskless, file,"
523
                               " plain or drbd)",
524
                               default=None, metavar="TEMPL",
525
                               choices=list(constants.DISK_TEMPLATES))
526

    
527
NONICS_OPT = cli_option("--no-nics", default=False, action="store_true",
528
                        help="Do not create any network cards for"
529
                        " the instance")
530

    
531
FILESTORE_DIR_OPT = cli_option("--file-storage-dir", dest="file_storage_dir",
532
                               help="Relative path under default cluster-wide"
533
                               " file storage dir to store file-based disks",
534
                               default=None, metavar="<DIR>")
535

    
536
FILESTORE_DRIVER_OPT = cli_option("--file-driver", dest="file_driver",
537
                                  help="Driver to use for image files",
538
                                  default="loop", metavar="<DRIVER>",
539
                                  choices=list(constants.FILE_DRIVER))
540

    
541
IALLOCATOR_OPT = cli_option("-I", "--iallocator", metavar="<NAME>",
542
                            help="Select nodes for the instance automatically"
543
                            " using the <NAME> iallocator plugin",
544
                            default=None, type="string",
545
                            completion_suggest=OPT_COMPL_ONE_IALLOCATOR)
546

    
547
OS_OPT = cli_option("-o", "--os-type", dest="os", help="What OS to run",
548
                    metavar="<os>",
549
                    completion_suggest=OPT_COMPL_ONE_OS)
550

    
551
BACKEND_OPT = cli_option("-B", "--backend-parameters", dest="beparams",
552
                         type="keyval", default={},
553
                         help="Backend parameters")
554

    
555
HVOPTS_OPT =  cli_option("-H", "--hypervisor-parameters", type="keyval",
556
                         default={}, dest="hvparams",
557
                         help="Hypervisor parameters")
558

    
559
HYPERVISOR_OPT = cli_option("-H", "--hypervisor-parameters", dest="hypervisor",
560
                            help="Hypervisor and hypervisor options, in the"
561
                            " format hypervisor:option=value,option=value,...",
562
                            default=None, type="identkeyval")
563

    
564
HVLIST_OPT = cli_option("-H", "--hypervisor-parameters", dest="hvparams",
565
                        help="Hypervisor and hypervisor options, in the"
566
                        " format hypervisor:option=value,option=value,...",
567
                        default=[], action="append", type="identkeyval")
568

    
569
NOIPCHECK_OPT = cli_option("--no-ip-check", dest="ip_check", default=True,
570
                           action="store_false",
571
                           help="Don't check that the instance's IP"
572
                           " is alive")
573

    
574
NET_OPT = cli_option("--net",
575
                     help="NIC parameters", default=[],
576
                     dest="nics", action="append", type="identkeyval")
577

    
578
DISK_OPT = cli_option("--disk", help="Disk parameters", default=[],
579
                      dest="disks", action="append", type="identkeyval")
580

    
581
DISKIDX_OPT = cli_option("--disks", dest="disks", default=None,
582
                         help="Comma-separated list of disks"
583
                         " indices to act on (e.g. 0,2) (optional,"
584
                         " defaults to all disks)")
585

    
586
OS_SIZE_OPT = cli_option("-s", "--os-size", dest="sd_size",
587
                         help="Enforces a single-disk configuration using the"
588
                         " given disk size, in MiB unless a suffix is used",
589
                         default=None, type="unit", metavar="<size>")
590

    
591
IGNORE_CONSIST_OPT = cli_option("--ignore-consistency",
592
                                dest="ignore_consistency",
593
                                action="store_true", default=False,
594
                                help="Ignore the consistency of the disks on"
595
                                " the secondary")
596

    
597
NONLIVE_OPT = cli_option("--non-live", dest="live",
598
                         default=True, action="store_false",
599
                         help="Do a non-live migration (this usually means"
600
                         " freeze the instance, save the state, transfer and"
601
                         " only then resume running on the secondary node)")
602

    
603
NODE_PLACEMENT_OPT = cli_option("-n", "--node", dest="node",
604
                                help="Target node and optional secondary node",
605
                                metavar="<pnode>[:<snode>]",
606
                                completion_suggest=OPT_COMPL_INST_ADD_NODES)
607

    
608
NODE_LIST_OPT = cli_option("-n", "--node", dest="nodes", default=[],
609
                           action="append", metavar="<node>",
610
                           help="Use only this node (can be used multiple"
611
                           " times, if not given defaults to all nodes)",
612
                           completion_suggest=OPT_COMPL_ONE_NODE)
613

    
614
SINGLE_NODE_OPT = cli_option("-n", "--node", dest="node", help="Target node",
615
                             metavar="<node>",
616
                             completion_suggest=OPT_COMPL_ONE_NODE)
617

    
618
NOSTART_OPT = cli_option("--no-start", dest="start", default=True,
619
                         action="store_false",
620
                         help="Don't start the instance after creation")
621

    
622
SHOWCMD_OPT = cli_option("--show-cmd", dest="show_command",
623
                         action="store_true", default=False,
624
                         help="Show command instead of executing it")
625

    
626
CLEANUP_OPT = cli_option("--cleanup", dest="cleanup",
627
                         default=False, action="store_true",
628
                         help="Instead of performing the migration, try to"
629
                         " recover from a failed cleanup. This is safe"
630
                         " to run even if the instance is healthy, but it"
631
                         " will create extra replication traffic and "
632
                         " disrupt briefly the replication (like during the"
633
                         " migration")
634

    
635
STATIC_OPT = cli_option("-s", "--static", dest="static",
636
                        action="store_true", default=False,
637
                        help="Only show configuration data, not runtime data")
638

    
639
ALL_OPT = cli_option("--all", dest="show_all",
640
                     default=False, action="store_true",
641
                     help="Show info on all instances on the cluster."
642
                     " This can take a long time to run, use wisely")
643

    
644
SELECT_OS_OPT = cli_option("--select-os", dest="select_os",
645
                           action="store_true", default=False,
646
                           help="Interactive OS reinstall, lists available"
647
                           " OS templates for selection")
648

    
649
IGNORE_FAILURES_OPT = cli_option("--ignore-failures", dest="ignore_failures",
650
                                 action="store_true", default=False,
651
                                 help="Remove the instance from the cluster"
652
                                 " configuration even if there are failures"
653
                                 " during the removal process")
654

    
655
NEW_SECONDARY_OPT = cli_option("-n", "--new-secondary", dest="dst_node",
656
                               help="Specifies the new secondary node",
657
                               metavar="NODE", default=None,
658
                               completion_suggest=OPT_COMPL_ONE_NODE)
659

    
660
ON_PRIMARY_OPT = cli_option("-p", "--on-primary", dest="on_primary",
661
                            default=False, action="store_true",
662
                            help="Replace the disk(s) on the primary"
663
                            " node (only for the drbd template)")
664

    
665
ON_SECONDARY_OPT = cli_option("-s", "--on-secondary", dest="on_secondary",
666
                              default=False, action="store_true",
667
                              help="Replace the disk(s) on the secondary"
668
                              " node (only for the drbd template)")
669

    
670
AUTO_REPLACE_OPT = cli_option("-a", "--auto", dest="auto",
671
                              default=False, action="store_true",
672
                              help="Automatically replace faulty disks"
673
                              " (only for the drbd template)")
674

    
675
IGNORE_SIZE_OPT = cli_option("--ignore-size", dest="ignore_size",
676
                             default=False, action="store_true",
677
                             help="Ignore current recorded size"
678
                             " (useful for forcing activation when"
679
                             " the recorded size is wrong)")
680

    
681
SRC_NODE_OPT = cli_option("--src-node", dest="src_node", help="Source node",
682
                          metavar="<node>",
683
                          completion_suggest=OPT_COMPL_ONE_NODE)
684

    
685
SRC_DIR_OPT = cli_option("--src-dir", dest="src_dir", help="Source directory",
686
                         metavar="<dir>")
687

    
688
SECONDARY_IP_OPT = cli_option("-s", "--secondary-ip", dest="secondary_ip",
689
                              help="Specify the secondary ip for the node",
690
                              metavar="ADDRESS", default=None)
691

    
692
READD_OPT = cli_option("--readd", dest="readd",
693
                       default=False, action="store_true",
694
                       help="Readd old node after replacing it")
695

    
696
NOSSH_KEYCHECK_OPT = cli_option("--no-ssh-key-check", dest="ssh_key_check",
697
                                default=True, action="store_false",
698
                                help="Disable SSH key fingerprint checking")
699

    
700

    
701
MC_OPT = cli_option("-C", "--master-candidate", dest="master_candidate",
702
                    choices=_YESNO, default=None, metavar=_YORNO,
703
                    help="Set the master_candidate flag on the node")
704

    
705
OFFLINE_OPT = cli_option("-O", "--offline", dest="offline", metavar=_YORNO,
706
                         choices=_YESNO, default=None,
707
                         help="Set the offline flag on the node")
708

    
709
DRAINED_OPT = cli_option("-D", "--drained", dest="drained", metavar=_YORNO,
710
                         choices=_YESNO, default=None,
711
                         help="Set the drained flag on the node")
712

    
713

    
714
def _ParseArgs(argv, commands, aliases):
715
  """Parser for the command line arguments.
716

717
  This function parses the arguments and returns the function which
718
  must be executed together with its (modified) arguments.
719

720
  @param argv: the command line
721
  @param commands: dictionary with special contents, see the design
722
      doc for cmdline handling
723
  @param aliases: dictionary with command aliases {'alias': 'target, ...}
724

725
  """
726
  if len(argv) == 0:
727
    binary = "<command>"
728
  else:
729
    binary = argv[0].split("/")[-1]
730

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

    
737
  if len(argv) < 2 or not (argv[1] in commands or
738
                           argv[1] in aliases):
739
    # let's do a nice thing
740
    sortedcmds = commands.keys()
741
    sortedcmds.sort()
742

    
743
    ToStdout("Usage: %s {command} [options...] [argument...]", binary)
744
    ToStdout("%s <command> --help to see details, or man %s", binary, binary)
745
    ToStdout("")
746

    
747
    # compute the max line length for cmd + usage
748
    mlen = max([len(" %s" % cmd) for cmd in commands])
749
    mlen = min(60, mlen) # should not get here...
750

    
751
    # and format a nice command list
752
    ToStdout("Commands:")
753
    for cmd in sortedcmds:
754
      cmdstr = " %s" % (cmd,)
755
      help_text = commands[cmd][4]
756
      help_lines = textwrap.wrap(help_text, 79 - 3 - mlen)
757
      ToStdout("%-*s - %s", mlen, cmdstr, help_lines.pop(0))
758
      for line in help_lines:
759
        ToStdout("%-*s   %s", mlen, "", line)
760

    
761
    ToStdout("")
762

    
763
    return None, None, None
764

    
765
  # get command, unalias it, and look it up in commands
766
  cmd = argv.pop(1)
767
  if cmd in aliases:
768
    if cmd in commands:
769
      raise errors.ProgrammerError("Alias '%s' overrides an existing"
770
                                   " command" % cmd)
771

    
772
    if aliases[cmd] not in commands:
773
      raise errors.ProgrammerError("Alias '%s' maps to non-existing"
774
                                   " command '%s'" % (cmd, aliases[cmd]))
775

    
776
    cmd = aliases[cmd]
777

    
778
  func, args_def, parser_opts, usage, description = commands[cmd]
779
  parser = OptionParser(option_list=parser_opts + [_DRY_RUN_OPT],
780
                        description=description,
781
                        formatter=TitledHelpFormatter(),
782
                        usage="%%prog %s %s" % (cmd, usage))
783
  parser.disable_interspersed_args()
784
  options, args = parser.parse_args()
785

    
786
  if not _CheckArguments(cmd, args_def, args):
787
    return None, None, None
788

    
789
  return func, options, args
790

    
791

    
792
def _CheckArguments(cmd, args_def, args):
793
  """Verifies the arguments using the argument definition.
794

795
  Algorithm:
796

797
    1. Abort with error if values specified by user but none expected.
798

799
    1. For each argument in definition
800

801
      1. Keep running count of minimum number of values (min_count)
802
      1. Keep running count of maximum number of values (max_count)
803
      1. If it has an unlimited number of values
804

805
        1. Abort with error if it's not the last argument in the definition
806

807
    1. If last argument has limited number of values
808

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

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

813
  """
814
  if args and not args_def:
815
    ToStderr("Error: Command %s expects no arguments", cmd)
816
    return False
817

    
818
  min_count = None
819
  max_count = None
820
  check_max = None
821

    
822
  last_idx = len(args_def) - 1
823

    
824
  for idx, arg in enumerate(args_def):
825
    if min_count is None:
826
      min_count = arg.min
827
    elif arg.min is not None:
828
      min_count += arg.min
829

    
830
    if max_count is None:
831
      max_count = arg.max
832
    elif arg.max is not None:
833
      max_count += arg.max
834

    
835
    if idx == last_idx:
836
      check_max = (arg.max is not None)
837

    
838
    elif arg.max is None:
839
      raise errors.ProgrammerError("Only the last argument can have max=None")
840

    
841
  if check_max:
842
    # Command with exact number of arguments
843
    if (min_count is not None and max_count is not None and
844
        min_count == max_count and len(args) != min_count):
845
      ToStderr("Error: Command %s expects %d argument(s)", cmd, min_count)
846
      return False
847

    
848
    # Command with limited number of arguments
849
    if max_count is not None and len(args) > max_count:
850
      ToStderr("Error: Command %s expects only %d argument(s)",
851
               cmd, max_count)
852
      return False
853

    
854
  # Command with some required arguments
855
  if min_count is not None and len(args) < min_count:
856
    ToStderr("Error: Command %s expects at least %d argument(s)",
857
             cmd, min_count)
858
    return False
859

    
860
  return True
861

    
862

    
863
def SplitNodeOption(value):
864
  """Splits the value of a --node option.
865

866
  """
867
  if value and ':' in value:
868
    return value.split(':', 1)
869
  else:
870
    return (value, None)
871

    
872

    
873
def UsesRPC(fn):
874
  def wrapper(*args, **kwargs):
875
    rpc.Init()
876
    try:
877
      return fn(*args, **kwargs)
878
    finally:
879
      rpc.Shutdown()
880
  return wrapper
881

    
882

    
883
def AskUser(text, choices=None):
884
  """Ask the user a question.
885

886
  @param text: the question to ask
887

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

893
  @return: one of the return values from the choices list; if input is
894
      not possible (i.e. not running with a tty, we return the last
895
      entry from the list
896

897
  """
898
  if choices is None:
899
    choices = [('y', True, 'Perform the operation'),
900
               ('n', False, 'Do not perform the operation')]
901
  if not choices or not isinstance(choices, list):
902
    raise errors.ProgrammerError("Invalid choices argument to AskUser")
903
  for entry in choices:
904
    if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
905
      raise errors.ProgrammerError("Invalid choices element to AskUser")
906

    
907
  answer = choices[-1][1]
908
  new_text = []
909
  for line in text.splitlines():
910
    new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
911
  text = "\n".join(new_text)
912
  try:
913
    f = file("/dev/tty", "a+")
914
  except IOError:
915
    return answer
916
  try:
917
    chars = [entry[0] for entry in choices]
918
    chars[-1] = "[%s]" % chars[-1]
919
    chars.append('?')
920
    maps = dict([(entry[0], entry[1]) for entry in choices])
921
    while True:
922
      f.write(text)
923
      f.write('\n')
924
      f.write("/".join(chars))
925
      f.write(": ")
926
      line = f.readline(2).strip().lower()
927
      if line in maps:
928
        answer = maps[line]
929
        break
930
      elif line == '?':
931
        for entry in choices:
932
          f.write(" %s - %s\n" % (entry[0], entry[2]))
933
        f.write("\n")
934
        continue
935
  finally:
936
    f.close()
937
  return answer
938

    
939

    
940
class JobSubmittedException(Exception):
941
  """Job was submitted, client should exit.
942

943
  This exception has one argument, the ID of the job that was
944
  submitted. The handler should print this ID.
945

946
  This is not an error, just a structured way to exit from clients.
947

948
  """
949

    
950

    
951
def SendJob(ops, cl=None):
952
  """Function to submit an opcode without waiting for the results.
953

954
  @type ops: list
955
  @param ops: list of opcodes
956
  @type cl: luxi.Client
957
  @param cl: the luxi client to use for communicating with the master;
958
             if None, a new client will be created
959

960
  """
961
  if cl is None:
962
    cl = GetClient()
963

    
964
  job_id = cl.SubmitJob(ops)
965

    
966
  return job_id
967

    
968

    
969
def PollJob(job_id, cl=None, feedback_fn=None):
970
  """Function to poll for the result of a job.
971

972
  @type job_id: job identified
973
  @param job_id: the job to poll for results
974
  @type cl: luxi.Client
975
  @param cl: the luxi client to use for communicating with the master;
976
             if None, a new client will be created
977

978
  """
979
  if cl is None:
980
    cl = GetClient()
981

    
982
  prev_job_info = None
983
  prev_logmsg_serial = None
984

    
985
  while True:
986
    result = cl.WaitForJobChange(job_id, ["status"], prev_job_info,
987
                                 prev_logmsg_serial)
988
    if not result:
989
      # job not found, go away!
990
      raise errors.JobLost("Job with id %s lost" % job_id)
991

    
992
    # Split result, a tuple of (field values, log entries)
993
    (job_info, log_entries) = result
994
    (status, ) = job_info
995

    
996
    if log_entries:
997
      for log_entry in log_entries:
998
        (serial, timestamp, _, message) = log_entry
999
        if callable(feedback_fn):
1000
          feedback_fn(log_entry[1:])
1001
        else:
1002
          encoded = utils.SafeEncode(message)
1003
          ToStdout("%s %s", time.ctime(utils.MergeTime(timestamp)), encoded)
1004
        prev_logmsg_serial = max(prev_logmsg_serial, serial)
1005

    
1006
    # TODO: Handle canceled and archived jobs
1007
    elif status in (constants.JOB_STATUS_SUCCESS,
1008
                    constants.JOB_STATUS_ERROR,
1009
                    constants.JOB_STATUS_CANCELING,
1010
                    constants.JOB_STATUS_CANCELED):
1011
      break
1012

    
1013
    prev_job_info = job_info
1014

    
1015
  jobs = cl.QueryJobs([job_id], ["status", "opstatus", "opresult"])
1016
  if not jobs:
1017
    raise errors.JobLost("Job with id %s lost" % job_id)
1018

    
1019
  status, opstatus, result = jobs[0]
1020
  if status == constants.JOB_STATUS_SUCCESS:
1021
    return result
1022
  elif status in (constants.JOB_STATUS_CANCELING,
1023
                  constants.JOB_STATUS_CANCELED):
1024
    raise errors.OpExecError("Job was canceled")
1025
  else:
1026
    has_ok = False
1027
    for idx, (status, msg) in enumerate(zip(opstatus, result)):
1028
      if status == constants.OP_STATUS_SUCCESS:
1029
        has_ok = True
1030
      elif status == constants.OP_STATUS_ERROR:
1031
        errors.MaybeRaise(msg)
1032
        if has_ok:
1033
          raise errors.OpExecError("partial failure (opcode %d): %s" %
1034
                                   (idx, msg))
1035
        else:
1036
          raise errors.OpExecError(str(msg))
1037
    # default failure mode
1038
    raise errors.OpExecError(result)
1039

    
1040

    
1041
def SubmitOpCode(op, cl=None, feedback_fn=None):
1042
  """Legacy function to submit an opcode.
1043

1044
  This is just a simple wrapper over the construction of the processor
1045
  instance. It should be extended to better handle feedback and
1046
  interaction functions.
1047

1048
  """
1049
  if cl is None:
1050
    cl = GetClient()
1051

    
1052
  job_id = SendJob([op], cl)
1053

    
1054
  op_results = PollJob(job_id, cl=cl, feedback_fn=feedback_fn)
1055

    
1056
  return op_results[0]
1057

    
1058

    
1059
def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
1060
  """Wrapper around SubmitOpCode or SendJob.
1061

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

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

1069
  """
1070
  if opts and opts.dry_run:
1071
    op.dry_run = opts.dry_run
1072
  if opts and opts.submit_only:
1073
    job_id = SendJob([op], cl=cl)
1074
    raise JobSubmittedException(job_id)
1075
  else:
1076
    return SubmitOpCode(op, cl=cl, feedback_fn=feedback_fn)
1077

    
1078

    
1079
def GetClient():
1080
  # TODO: Cache object?
1081
  try:
1082
    client = luxi.Client()
1083
  except luxi.NoMasterError:
1084
    master, myself = ssconf.GetMasterAndMyself()
1085
    if master != myself:
1086
      raise errors.OpPrereqError("This is not the master node, please connect"
1087
                                 " to node '%s' and rerun the command" %
1088
                                 master)
1089
    else:
1090
      raise
1091
  return client
1092

    
1093

    
1094
def FormatError(err):
1095
  """Return a formatted error message for a given error.
1096

1097
  This function takes an exception instance and returns a tuple
1098
  consisting of two values: first, the recommended exit code, and
1099
  second, a string describing the error message (not
1100
  newline-terminated).
1101

1102
  """
1103
  retcode = 1
1104
  obuf = StringIO()
1105
  msg = str(err)
1106
  if isinstance(err, errors.ConfigurationError):
1107
    txt = "Corrupt configuration file: %s" % msg
1108
    logging.error(txt)
1109
    obuf.write(txt + "\n")
1110
    obuf.write("Aborting.")
1111
    retcode = 2
1112
  elif isinstance(err, errors.HooksAbort):
1113
    obuf.write("Failure: hooks execution failed:\n")
1114
    for node, script, out in err.args[0]:
1115
      if out:
1116
        obuf.write("  node: %s, script: %s, output: %s\n" %
1117
                   (node, script, out))
1118
      else:
1119
        obuf.write("  node: %s, script: %s (no output)\n" %
1120
                   (node, script))
1121
  elif isinstance(err, errors.HooksFailure):
1122
    obuf.write("Failure: hooks general failure: %s" % msg)
1123
  elif isinstance(err, errors.ResolverError):
1124
    this_host = utils.HostInfo.SysName()
1125
    if err.args[0] == this_host:
1126
      msg = "Failure: can't resolve my own hostname ('%s')"
1127
    else:
1128
      msg = "Failure: can't resolve hostname '%s'"
1129
    obuf.write(msg % err.args[0])
1130
  elif isinstance(err, errors.OpPrereqError):
1131
    obuf.write("Failure: prerequisites not met for this"
1132
               " operation:\n%s" % msg)
1133
  elif isinstance(err, errors.OpExecError):
1134
    obuf.write("Failure: command execution error:\n%s" % msg)
1135
  elif isinstance(err, errors.TagError):
1136
    obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
1137
  elif isinstance(err, errors.JobQueueDrainError):
1138
    obuf.write("Failure: the job queue is marked for drain and doesn't"
1139
               " accept new requests\n")
1140
  elif isinstance(err, errors.JobQueueFull):
1141
    obuf.write("Failure: the job queue is full and doesn't accept new"
1142
               " job submissions until old jobs are archived\n")
1143
  elif isinstance(err, errors.TypeEnforcementError):
1144
    obuf.write("Parameter Error: %s" % msg)
1145
  elif isinstance(err, errors.ParameterError):
1146
    obuf.write("Failure: unknown/wrong parameter name '%s'" % msg)
1147
  elif isinstance(err, errors.GenericError):
1148
    obuf.write("Unhandled Ganeti error: %s" % msg)
1149
  elif isinstance(err, luxi.NoMasterError):
1150
    obuf.write("Cannot communicate with the master daemon.\nIs it running"
1151
               " and listening for connections?")
1152
  elif isinstance(err, luxi.TimeoutError):
1153
    obuf.write("Timeout while talking to the master daemon. Error:\n"
1154
               "%s" % msg)
1155
  elif isinstance(err, luxi.ProtocolError):
1156
    obuf.write("Unhandled protocol error while talking to the master daemon:\n"
1157
               "%s" % msg)
1158
  elif isinstance(err, JobSubmittedException):
1159
    obuf.write("JobID: %s\n" % err.args[0])
1160
    retcode = 0
1161
  else:
1162
    obuf.write("Unhandled exception: %s" % msg)
1163
  return retcode, obuf.getvalue().rstrip('\n')
1164

    
1165

    
1166
def GenericMain(commands, override=None, aliases=None):
1167
  """Generic main function for all the gnt-* commands.
1168

1169
  Arguments:
1170
    - commands: a dictionary with a special structure, see the design doc
1171
                for command line handling.
1172
    - override: if not None, we expect a dictionary with keys that will
1173
                override command line options; this can be used to pass
1174
                options from the scripts to generic functions
1175
    - aliases: dictionary with command aliases {'alias': 'target, ...}
1176

1177
  """
1178
  # save the program name and the entire command line for later logging
1179
  if sys.argv:
1180
    binary = os.path.basename(sys.argv[0]) or sys.argv[0]
1181
    if len(sys.argv) >= 2:
1182
      binary += " " + sys.argv[1]
1183
      old_cmdline = " ".join(sys.argv[2:])
1184
    else:
1185
      old_cmdline = ""
1186
  else:
1187
    binary = "<unknown program>"
1188
    old_cmdline = ""
1189

    
1190
  if aliases is None:
1191
    aliases = {}
1192

    
1193
  try:
1194
    func, options, args = _ParseArgs(sys.argv, commands, aliases)
1195
  except errors.ParameterError, err:
1196
    result, err_msg = FormatError(err)
1197
    ToStderr(err_msg)
1198
    return 1
1199

    
1200
  if func is None: # parse error
1201
    return 1
1202

    
1203
  if override is not None:
1204
    for key, val in override.iteritems():
1205
      setattr(options, key, val)
1206

    
1207
  utils.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
1208
                     stderr_logging=True, program=binary)
1209

    
1210
  if old_cmdline:
1211
    logging.info("run with arguments '%s'", old_cmdline)
1212
  else:
1213
    logging.info("run with no arguments")
1214

    
1215
  try:
1216
    result = func(options, args)
1217
  except (errors.GenericError, luxi.ProtocolError,
1218
          JobSubmittedException), err:
1219
    result, err_msg = FormatError(err)
1220
    logging.exception("Error during command processing")
1221
    ToStderr(err_msg)
1222

    
1223
  return result
1224

    
1225

    
1226
def GenerateTable(headers, fields, separator, data,
1227
                  numfields=None, unitfields=None,
1228
                  units=None):
1229
  """Prints a table with headers and different fields.
1230

1231
  @type headers: dict
1232
  @param headers: dictionary mapping field names to headers for
1233
      the table
1234
  @type fields: list
1235
  @param fields: the field names corresponding to each row in
1236
      the data field
1237
  @param separator: the separator to be used; if this is None,
1238
      the default 'smart' algorithm is used which computes optimal
1239
      field width, otherwise just the separator is used between
1240
      each field
1241
  @type data: list
1242
  @param data: a list of lists, each sublist being one row to be output
1243
  @type numfields: list
1244
  @param numfields: a list with the fields that hold numeric
1245
      values and thus should be right-aligned
1246
  @type unitfields: list
1247
  @param unitfields: a list with the fields that hold numeric
1248
      values that should be formatted with the units field
1249
  @type units: string or None
1250
  @param units: the units we should use for formatting, or None for
1251
      automatic choice (human-readable for non-separator usage, otherwise
1252
      megabytes); this is a one-letter string
1253

1254
  """
1255
  if units is None:
1256
    if separator:
1257
      units = "m"
1258
    else:
1259
      units = "h"
1260

    
1261
  if numfields is None:
1262
    numfields = []
1263
  if unitfields is None:
1264
    unitfields = []
1265

    
1266
  numfields = utils.FieldSet(*numfields)
1267
  unitfields = utils.FieldSet(*unitfields)
1268

    
1269
  format_fields = []
1270
  for field in fields:
1271
    if headers and field not in headers:
1272
      # TODO: handle better unknown fields (either revert to old
1273
      # style of raising exception, or deal more intelligently with
1274
      # variable fields)
1275
      headers[field] = field
1276
    if separator is not None:
1277
      format_fields.append("%s")
1278
    elif numfields.Matches(field):
1279
      format_fields.append("%*s")
1280
    else:
1281
      format_fields.append("%-*s")
1282

    
1283
  if separator is None:
1284
    mlens = [0 for name in fields]
1285
    format = ' '.join(format_fields)
1286
  else:
1287
    format = separator.replace("%", "%%").join(format_fields)
1288

    
1289
  for row in data:
1290
    if row is None:
1291
      continue
1292
    for idx, val in enumerate(row):
1293
      if unitfields.Matches(fields[idx]):
1294
        try:
1295
          val = int(val)
1296
        except ValueError:
1297
          pass
1298
        else:
1299
          val = row[idx] = utils.FormatUnit(val, units)
1300
      val = row[idx] = str(val)
1301
      if separator is None:
1302
        mlens[idx] = max(mlens[idx], len(val))
1303

    
1304
  result = []
1305
  if headers:
1306
    args = []
1307
    for idx, name in enumerate(fields):
1308
      hdr = headers[name]
1309
      if separator is None:
1310
        mlens[idx] = max(mlens[idx], len(hdr))
1311
        args.append(mlens[idx])
1312
      args.append(hdr)
1313
    result.append(format % tuple(args))
1314

    
1315
  for line in data:
1316
    args = []
1317
    if line is None:
1318
      line = ['-' for _ in fields]
1319
    for idx in xrange(len(fields)):
1320
      if separator is None:
1321
        args.append(mlens[idx])
1322
      args.append(line[idx])
1323
    result.append(format % tuple(args))
1324

    
1325
  return result
1326

    
1327

    
1328
def FormatTimestamp(ts):
1329
  """Formats a given timestamp.
1330

1331
  @type ts: timestamp
1332
  @param ts: a timeval-type timestamp, a tuple of seconds and microseconds
1333

1334
  @rtype: string
1335
  @return: a string with the formatted timestamp
1336

1337
  """
1338
  if not isinstance (ts, (tuple, list)) or len(ts) != 2:
1339
    return '?'
1340
  sec, usec = ts
1341
  return time.strftime("%F %T", time.localtime(sec)) + ".%06d" % usec
1342

    
1343

    
1344
def ParseTimespec(value):
1345
  """Parse a time specification.
1346

1347
  The following suffixed will be recognized:
1348

1349
    - s: seconds
1350
    - m: minutes
1351
    - h: hours
1352
    - d: day
1353
    - w: weeks
1354

1355
  Without any suffix, the value will be taken to be in seconds.
1356

1357
  """
1358
  value = str(value)
1359
  if not value:
1360
    raise errors.OpPrereqError("Empty time specification passed")
1361
  suffix_map = {
1362
    's': 1,
1363
    'm': 60,
1364
    'h': 3600,
1365
    'd': 86400,
1366
    'w': 604800,
1367
    }
1368
  if value[-1] not in suffix_map:
1369
    try:
1370
      value = int(value)
1371
    except ValueError:
1372
      raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1373
  else:
1374
    multiplier = suffix_map[value[-1]]
1375
    value = value[:-1]
1376
    if not value: # no data left after stripping the suffix
1377
      raise errors.OpPrereqError("Invalid time specification (only"
1378
                                 " suffix passed)")
1379
    try:
1380
      value = int(value) * multiplier
1381
    except ValueError:
1382
      raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1383
  return value
1384

    
1385

    
1386
def GetOnlineNodes(nodes, cl=None, nowarn=False):
1387
  """Returns the names of online nodes.
1388

1389
  This function will also log a warning on stderr with the names of
1390
  the online nodes.
1391

1392
  @param nodes: if not empty, use only this subset of nodes (minus the
1393
      offline ones)
1394
  @param cl: if not None, luxi client to use
1395
  @type nowarn: boolean
1396
  @param nowarn: by default, this function will output a note with the
1397
      offline nodes that are skipped; if this parameter is True the
1398
      note is not displayed
1399

1400
  """
1401
  if cl is None:
1402
    cl = GetClient()
1403

    
1404
  result = cl.QueryNodes(names=nodes, fields=["name", "offline"],
1405
                         use_locking=False)
1406
  offline = [row[0] for row in result if row[1]]
1407
  if offline and not nowarn:
1408
    ToStderr("Note: skipping offline node(s): %s" % ", ".join(offline))
1409
  return [row[0] for row in result if not row[1]]
1410

    
1411

    
1412
def _ToStream(stream, txt, *args):
1413
  """Write a message to a stream, bypassing the logging system
1414

1415
  @type stream: file object
1416
  @param stream: the file to which we should write
1417
  @type txt: str
1418
  @param txt: the message
1419

1420
  """
1421
  if args:
1422
    args = tuple(args)
1423
    stream.write(txt % args)
1424
  else:
1425
    stream.write(txt)
1426
  stream.write('\n')
1427
  stream.flush()
1428

    
1429

    
1430
def ToStdout(txt, *args):
1431
  """Write a message to stdout only, bypassing the logging system
1432

1433
  This is just a wrapper over _ToStream.
1434

1435
  @type txt: str
1436
  @param txt: the message
1437

1438
  """
1439
  _ToStream(sys.stdout, txt, *args)
1440

    
1441

    
1442
def ToStderr(txt, *args):
1443
  """Write a message to stderr only, bypassing the logging system
1444

1445
  This is just a wrapper over _ToStream.
1446

1447
  @type txt: str
1448
  @param txt: the message
1449

1450
  """
1451
  _ToStream(sys.stderr, txt, *args)
1452

    
1453

    
1454
class JobExecutor(object):
1455
  """Class which manages the submission and execution of multiple jobs.
1456

1457
  Note that instances of this class should not be reused between
1458
  GetResults() calls.
1459

1460
  """
1461
  def __init__(self, cl=None, verbose=True):
1462
    self.queue = []
1463
    if cl is None:
1464
      cl = GetClient()
1465
    self.cl = cl
1466
    self.verbose = verbose
1467
    self.jobs = []
1468

    
1469
  def QueueJob(self, name, *ops):
1470
    """Record a job for later submit.
1471

1472
    @type name: string
1473
    @param name: a description of the job, will be used in WaitJobSet
1474
    """
1475
    self.queue.append((name, ops))
1476

    
1477
  def SubmitPending(self):
1478
    """Submit all pending jobs.
1479

1480
    """
1481
    results = self.cl.SubmitManyJobs([row[1] for row in self.queue])
1482
    for ((status, data), (name, _)) in zip(results, self.queue):
1483
      self.jobs.append((status, data, name))
1484

    
1485
  def GetResults(self):
1486
    """Wait for and return the results of all jobs.
1487

1488
    @rtype: list
1489
    @return: list of tuples (success, job results), in the same order
1490
        as the submitted jobs; if a job has failed, instead of the result
1491
        there will be the error message
1492

1493
    """
1494
    if not self.jobs:
1495
      self.SubmitPending()
1496
    results = []
1497
    if self.verbose:
1498
      ok_jobs = [row[1] for row in self.jobs if row[0]]
1499
      if ok_jobs:
1500
        ToStdout("Submitted jobs %s", ", ".join(ok_jobs))
1501
    for submit_status, jid, name in self.jobs:
1502
      if not submit_status:
1503
        ToStderr("Failed to submit job for %s: %s", name, jid)
1504
        results.append((False, jid))
1505
        continue
1506
      if self.verbose:
1507
        ToStdout("Waiting for job %s for %s...", jid, name)
1508
      try:
1509
        job_result = PollJob(jid, cl=self.cl)
1510
        success = True
1511
      except (errors.GenericError, luxi.ProtocolError), err:
1512
        _, job_result = FormatError(err)
1513
        success = False
1514
        # the error message will always be shown, verbose or not
1515
        ToStderr("Job %s for %s has failed: %s", jid, name, job_result)
1516

    
1517
      results.append((success, job_result))
1518
    return results
1519

    
1520
  def WaitOrShow(self, wait):
1521
    """Wait for job results or only print the job IDs.
1522

1523
    @type wait: boolean
1524
    @param wait: whether to wait or not
1525

1526
    """
1527
    if wait:
1528
      return self.GetResults()
1529
    else:
1530
      if not self.jobs:
1531
        self.SubmitPending()
1532
      for status, result, name in self.jobs:
1533
        if status:
1534
          ToStdout("%s: %s", result, name)
1535
        else:
1536
          ToStderr("Failure for %s: %s", name, result)