Statistics
| Branch: | Tag: | Revision:

root / lib / cli.py @ e7e09483

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

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

    
149

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

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

    
159

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

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

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

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

    
174

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

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

181
  """
182

    
183

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

187
  """
188

    
189

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

193
  """
194

    
195

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

199
  """
200

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

204
  """
205

    
206

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

210
  """
211

    
212

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

216
  """
217

    
218

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

222
  """
223

    
224

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

    
231

    
232

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

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

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

    
253

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

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

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

    
282

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

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

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

    
300

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

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

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

    
317

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

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

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

    
334

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

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

    
344

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

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

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

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

    
381

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

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

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

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

    
409

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

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

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

    
418

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

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

    
437

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

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

    
455

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

    
459

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
701

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

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

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

    
714
ALLOCATABLE_OPT = cli_option("--allocatable", dest="allocatable",
715
                             choices=_YESNO, default=None, metavar=_YORNO,
716
                             help="Set the allocatable flag on a volume")
717

    
718

    
719
def _ParseArgs(argv, commands, aliases):
720
  """Parser for the command line arguments.
721

722
  This function parses the arguments and returns the function which
723
  must be executed together with its (modified) arguments.
724

725
  @param argv: the command line
726
  @param commands: dictionary with special contents, see the design
727
      doc for cmdline handling
728
  @param aliases: dictionary with command aliases {'alias': 'target, ...}
729

730
  """
731
  if len(argv) == 0:
732
    binary = "<command>"
733
  else:
734
    binary = argv[0].split("/")[-1]
735

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

    
742
  if len(argv) < 2 or not (argv[1] in commands or
743
                           argv[1] in aliases):
744
    # let's do a nice thing
745
    sortedcmds = commands.keys()
746
    sortedcmds.sort()
747

    
748
    ToStdout("Usage: %s {command} [options...] [argument...]", binary)
749
    ToStdout("%s <command> --help to see details, or man %s", binary, binary)
750
    ToStdout("")
751

    
752
    # compute the max line length for cmd + usage
753
    mlen = max([len(" %s" % cmd) for cmd in commands])
754
    mlen = min(60, mlen) # should not get here...
755

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

    
766
    ToStdout("")
767

    
768
    return None, None, None
769

    
770
  # get command, unalias it, and look it up in commands
771
  cmd = argv.pop(1)
772
  if cmd in aliases:
773
    if cmd in commands:
774
      raise errors.ProgrammerError("Alias '%s' overrides an existing"
775
                                   " command" % cmd)
776

    
777
    if aliases[cmd] not in commands:
778
      raise errors.ProgrammerError("Alias '%s' maps to non-existing"
779
                                   " command '%s'" % (cmd, aliases[cmd]))
780

    
781
    cmd = aliases[cmd]
782

    
783
  func, args_def, parser_opts, usage, description = commands[cmd]
784
  parser = OptionParser(option_list=parser_opts + [_DRY_RUN_OPT],
785
                        description=description,
786
                        formatter=TitledHelpFormatter(),
787
                        usage="%%prog %s %s" % (cmd, usage))
788
  parser.disable_interspersed_args()
789
  options, args = parser.parse_args()
790

    
791
  if not _CheckArguments(cmd, args_def, args):
792
    return None, None, None
793

    
794
  return func, options, args
795

    
796

    
797
def _CheckArguments(cmd, args_def, args):
798
  """Verifies the arguments using the argument definition.
799

800
  Algorithm:
801

802
    1. Abort with error if values specified by user but none expected.
803

804
    1. For each argument in definition
805

806
      1. Keep running count of minimum number of values (min_count)
807
      1. Keep running count of maximum number of values (max_count)
808
      1. If it has an unlimited number of values
809

810
        1. Abort with error if it's not the last argument in the definition
811

812
    1. If last argument has limited number of values
813

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

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

818
  """
819
  if args and not args_def:
820
    ToStderr("Error: Command %s expects no arguments", cmd)
821
    return False
822

    
823
  min_count = None
824
  max_count = None
825
  check_max = None
826

    
827
  last_idx = len(args_def) - 1
828

    
829
  for idx, arg in enumerate(args_def):
830
    if min_count is None:
831
      min_count = arg.min
832
    elif arg.min is not None:
833
      min_count += arg.min
834

    
835
    if max_count is None:
836
      max_count = arg.max
837
    elif arg.max is not None:
838
      max_count += arg.max
839

    
840
    if idx == last_idx:
841
      check_max = (arg.max is not None)
842

    
843
    elif arg.max is None:
844
      raise errors.ProgrammerError("Only the last argument can have max=None")
845

    
846
  if check_max:
847
    # Command with exact number of arguments
848
    if (min_count is not None and max_count is not None and
849
        min_count == max_count and len(args) != min_count):
850
      ToStderr("Error: Command %s expects %d argument(s)", cmd, min_count)
851
      return False
852

    
853
    # Command with limited number of arguments
854
    if max_count is not None and len(args) > max_count:
855
      ToStderr("Error: Command %s expects only %d argument(s)",
856
               cmd, max_count)
857
      return False
858

    
859
  # Command with some required arguments
860
  if min_count is not None and len(args) < min_count:
861
    ToStderr("Error: Command %s expects at least %d argument(s)",
862
             cmd, min_count)
863
    return False
864

    
865
  return True
866

    
867

    
868
def SplitNodeOption(value):
869
  """Splits the value of a --node option.
870

871
  """
872
  if value and ':' in value:
873
    return value.split(':', 1)
874
  else:
875
    return (value, None)
876

    
877

    
878
def UsesRPC(fn):
879
  def wrapper(*args, **kwargs):
880
    rpc.Init()
881
    try:
882
      return fn(*args, **kwargs)
883
    finally:
884
      rpc.Shutdown()
885
  return wrapper
886

    
887

    
888
def AskUser(text, choices=None):
889
  """Ask the user a question.
890

891
  @param text: the question to ask
892

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

898
  @return: one of the return values from the choices list; if input is
899
      not possible (i.e. not running with a tty, we return the last
900
      entry from the list
901

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

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

    
944

    
945
class JobSubmittedException(Exception):
946
  """Job was submitted, client should exit.
947

948
  This exception has one argument, the ID of the job that was
949
  submitted. The handler should print this ID.
950

951
  This is not an error, just a structured way to exit from clients.
952

953
  """
954

    
955

    
956
def SendJob(ops, cl=None):
957
  """Function to submit an opcode without waiting for the results.
958

959
  @type ops: list
960
  @param ops: list of opcodes
961
  @type cl: luxi.Client
962
  @param cl: the luxi client to use for communicating with the master;
963
             if None, a new client will be created
964

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

    
969
  job_id = cl.SubmitJob(ops)
970

    
971
  return job_id
972

    
973

    
974
def PollJob(job_id, cl=None, feedback_fn=None):
975
  """Function to poll for the result of a job.
976

977
  @type job_id: job identified
978
  @param job_id: the job to poll for results
979
  @type cl: luxi.Client
980
  @param cl: the luxi client to use for communicating with the master;
981
             if None, a new client will be created
982

983
  """
984
  if cl is None:
985
    cl = GetClient()
986

    
987
  prev_job_info = None
988
  prev_logmsg_serial = None
989

    
990
  while True:
991
    result = cl.WaitForJobChange(job_id, ["status"], prev_job_info,
992
                                 prev_logmsg_serial)
993
    if not result:
994
      # job not found, go away!
995
      raise errors.JobLost("Job with id %s lost" % job_id)
996

    
997
    # Split result, a tuple of (field values, log entries)
998
    (job_info, log_entries) = result
999
    (status, ) = job_info
1000

    
1001
    if log_entries:
1002
      for log_entry in log_entries:
1003
        (serial, timestamp, _, message) = log_entry
1004
        if callable(feedback_fn):
1005
          feedback_fn(log_entry[1:])
1006
        else:
1007
          encoded = utils.SafeEncode(message)
1008
          ToStdout("%s %s", time.ctime(utils.MergeTime(timestamp)), encoded)
1009
        prev_logmsg_serial = max(prev_logmsg_serial, serial)
1010

    
1011
    # TODO: Handle canceled and archived jobs
1012
    elif status in (constants.JOB_STATUS_SUCCESS,
1013
                    constants.JOB_STATUS_ERROR,
1014
                    constants.JOB_STATUS_CANCELING,
1015
                    constants.JOB_STATUS_CANCELED):
1016
      break
1017

    
1018
    prev_job_info = job_info
1019

    
1020
  jobs = cl.QueryJobs([job_id], ["status", "opstatus", "opresult"])
1021
  if not jobs:
1022
    raise errors.JobLost("Job with id %s lost" % job_id)
1023

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

    
1045

    
1046
def SubmitOpCode(op, cl=None, feedback_fn=None):
1047
  """Legacy function to submit an opcode.
1048

1049
  This is just a simple wrapper over the construction of the processor
1050
  instance. It should be extended to better handle feedback and
1051
  interaction functions.
1052

1053
  """
1054
  if cl is None:
1055
    cl = GetClient()
1056

    
1057
  job_id = SendJob([op], cl)
1058

    
1059
  op_results = PollJob(job_id, cl=cl, feedback_fn=feedback_fn)
1060

    
1061
  return op_results[0]
1062

    
1063

    
1064
def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
1065
  """Wrapper around SubmitOpCode or SendJob.
1066

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

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

1074
  """
1075
  if opts and opts.dry_run:
1076
    op.dry_run = opts.dry_run
1077
  if opts and opts.submit_only:
1078
    job_id = SendJob([op], cl=cl)
1079
    raise JobSubmittedException(job_id)
1080
  else:
1081
    return SubmitOpCode(op, cl=cl, feedback_fn=feedback_fn)
1082

    
1083

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

    
1098

    
1099
def FormatError(err):
1100
  """Return a formatted error message for a given error.
1101

1102
  This function takes an exception instance and returns a tuple
1103
  consisting of two values: first, the recommended exit code, and
1104
  second, a string describing the error message (not
1105
  newline-terminated).
1106

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

    
1170

    
1171
def GenericMain(commands, override=None, aliases=None):
1172
  """Generic main function for all the gnt-* commands.
1173

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

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

    
1195
  if aliases is None:
1196
    aliases = {}
1197

    
1198
  try:
1199
    func, options, args = _ParseArgs(sys.argv, commands, aliases)
1200
  except errors.ParameterError, err:
1201
    result, err_msg = FormatError(err)
1202
    ToStderr(err_msg)
1203
    return 1
1204

    
1205
  if func is None: # parse error
1206
    return 1
1207

    
1208
  if override is not None:
1209
    for key, val in override.iteritems():
1210
      setattr(options, key, val)
1211

    
1212
  utils.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
1213
                     stderr_logging=True, program=binary)
1214

    
1215
  if old_cmdline:
1216
    logging.info("run with arguments '%s'", old_cmdline)
1217
  else:
1218
    logging.info("run with no arguments")
1219

    
1220
  try:
1221
    result = func(options, args)
1222
  except (errors.GenericError, luxi.ProtocolError,
1223
          JobSubmittedException), err:
1224
    result, err_msg = FormatError(err)
1225
    logging.exception("Error during command processing")
1226
    ToStderr(err_msg)
1227

    
1228
  return result
1229

    
1230

    
1231
def GenerateTable(headers, fields, separator, data,
1232
                  numfields=None, unitfields=None,
1233
                  units=None):
1234
  """Prints a table with headers and different fields.
1235

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

1259
  """
1260
  if units is None:
1261
    if separator:
1262
      units = "m"
1263
    else:
1264
      units = "h"
1265

    
1266
  if numfields is None:
1267
    numfields = []
1268
  if unitfields is None:
1269
    unitfields = []
1270

    
1271
  numfields = utils.FieldSet(*numfields)
1272
  unitfields = utils.FieldSet(*unitfields)
1273

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

    
1288
  if separator is None:
1289
    mlens = [0 for name in fields]
1290
    format = ' '.join(format_fields)
1291
  else:
1292
    format = separator.replace("%", "%%").join(format_fields)
1293

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

    
1309
  result = []
1310
  if headers:
1311
    args = []
1312
    for idx, name in enumerate(fields):
1313
      hdr = headers[name]
1314
      if separator is None:
1315
        mlens[idx] = max(mlens[idx], len(hdr))
1316
        args.append(mlens[idx])
1317
      args.append(hdr)
1318
    result.append(format % tuple(args))
1319

    
1320
  for line in data:
1321
    args = []
1322
    if line is None:
1323
      line = ['-' for _ in fields]
1324
    for idx in xrange(len(fields)):
1325
      if separator is None:
1326
        args.append(mlens[idx])
1327
      args.append(line[idx])
1328
    result.append(format % tuple(args))
1329

    
1330
  return result
1331

    
1332

    
1333
def FormatTimestamp(ts):
1334
  """Formats a given timestamp.
1335

1336
  @type ts: timestamp
1337
  @param ts: a timeval-type timestamp, a tuple of seconds and microseconds
1338

1339
  @rtype: string
1340
  @return: a string with the formatted timestamp
1341

1342
  """
1343
  if not isinstance (ts, (tuple, list)) or len(ts) != 2:
1344
    return '?'
1345
  sec, usec = ts
1346
  return time.strftime("%F %T", time.localtime(sec)) + ".%06d" % usec
1347

    
1348

    
1349
def ParseTimespec(value):
1350
  """Parse a time specification.
1351

1352
  The following suffixed will be recognized:
1353

1354
    - s: seconds
1355
    - m: minutes
1356
    - h: hours
1357
    - d: day
1358
    - w: weeks
1359

1360
  Without any suffix, the value will be taken to be in seconds.
1361

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

    
1390

    
1391
def GetOnlineNodes(nodes, cl=None, nowarn=False):
1392
  """Returns the names of online nodes.
1393

1394
  This function will also log a warning on stderr with the names of
1395
  the online nodes.
1396

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

1405
  """
1406
  if cl is None:
1407
    cl = GetClient()
1408

    
1409
  result = cl.QueryNodes(names=nodes, fields=["name", "offline"],
1410
                         use_locking=False)
1411
  offline = [row[0] for row in result if row[1]]
1412
  if offline and not nowarn:
1413
    ToStderr("Note: skipping offline node(s): %s" % ", ".join(offline))
1414
  return [row[0] for row in result if not row[1]]
1415

    
1416

    
1417
def _ToStream(stream, txt, *args):
1418
  """Write a message to a stream, bypassing the logging system
1419

1420
  @type stream: file object
1421
  @param stream: the file to which we should write
1422
  @type txt: str
1423
  @param txt: the message
1424

1425
  """
1426
  if args:
1427
    args = tuple(args)
1428
    stream.write(txt % args)
1429
  else:
1430
    stream.write(txt)
1431
  stream.write('\n')
1432
  stream.flush()
1433

    
1434

    
1435
def ToStdout(txt, *args):
1436
  """Write a message to stdout only, bypassing the logging system
1437

1438
  This is just a wrapper over _ToStream.
1439

1440
  @type txt: str
1441
  @param txt: the message
1442

1443
  """
1444
  _ToStream(sys.stdout, txt, *args)
1445

    
1446

    
1447
def ToStderr(txt, *args):
1448
  """Write a message to stderr only, bypassing the logging system
1449

1450
  This is just a wrapper over _ToStream.
1451

1452
  @type txt: str
1453
  @param txt: the message
1454

1455
  """
1456
  _ToStream(sys.stderr, txt, *args)
1457

    
1458

    
1459
class JobExecutor(object):
1460
  """Class which manages the submission and execution of multiple jobs.
1461

1462
  Note that instances of this class should not be reused between
1463
  GetResults() calls.
1464

1465
  """
1466
  def __init__(self, cl=None, verbose=True):
1467
    self.queue = []
1468
    if cl is None:
1469
      cl = GetClient()
1470
    self.cl = cl
1471
    self.verbose = verbose
1472
    self.jobs = []
1473

    
1474
  def QueueJob(self, name, *ops):
1475
    """Record a job for later submit.
1476

1477
    @type name: string
1478
    @param name: a description of the job, will be used in WaitJobSet
1479
    """
1480
    self.queue.append((name, ops))
1481

    
1482
  def SubmitPending(self):
1483
    """Submit all pending jobs.
1484

1485
    """
1486
    results = self.cl.SubmitManyJobs([row[1] for row in self.queue])
1487
    for ((status, data), (name, _)) in zip(results, self.queue):
1488
      self.jobs.append((status, data, name))
1489

    
1490
  def GetResults(self):
1491
    """Wait for and return the results of all jobs.
1492

1493
    @rtype: list
1494
    @return: list of tuples (success, job results), in the same order
1495
        as the submitted jobs; if a job has failed, instead of the result
1496
        there will be the error message
1497

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

    
1522
      results.append((success, job_result))
1523
    return results
1524

    
1525
  def WaitOrShow(self, wait):
1526
    """Wait for job results or only print the job IDs.
1527

1528
    @type wait: boolean
1529
    @param wait: whether to wait or not
1530

1531
    """
1532
    if wait:
1533
      return self.GetResults()
1534
    else:
1535
      if not self.jobs:
1536
        self.SubmitPending()
1537
      for status, result, name in self.jobs:
1538
        if status:
1539
          ToStdout("%s: %s", result, name)
1540
        else:
1541
          ToStderr("Failure for %s: %s", name, result)