Statistics
| Branch: | Tag: | Revision:

root / lib / cli.py @ fcdde7f2

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

    
142
NO_PREFIX = "no_"
143
UN_PREFIX = "-"
144

    
145

    
146
class _Argument:
147
  def __init__(self, min=0, max=None):
148
    self.min = min
149
    self.max = max
150

    
151
  def __repr__(self):
152
    return ("<%s min=%s max=%s>" %
153
            (self.__class__.__name__, self.min, self.max))
154

    
155

    
156
class ArgSuggest(_Argument):
157
  """Suggesting argument.
158

159
  Value can be any of the ones passed to the constructor.
160

161
  """
162
  def __init__(self, min=0, max=None, choices=None):
163
    _Argument.__init__(self, min=min, max=max)
164
    self.choices = choices
165

    
166
  def __repr__(self):
167
    return ("<%s min=%s max=%s choices=%r>" %
168
            (self.__class__.__name__, self.min, self.max, self.choices))
169

    
170

    
171
class ArgChoice(ArgSuggest):
172
  """Choice argument.
173

174
  Value can be any of the ones passed to the constructor. Like L{ArgSuggest},
175
  but value must be one of the choices.
176

177
  """
178

    
179

    
180
class ArgUnknown(_Argument):
181
  """Unknown argument to program (e.g. determined at runtime).
182

183
  """
184

    
185

    
186
class ArgInstance(_Argument):
187
  """Instances argument.
188

189
  """
190

    
191

    
192
class ArgNode(_Argument):
193
  """Node argument.
194

195
  """
196

    
197
class ArgJobId(_Argument):
198
  """Job ID argument.
199

200
  """
201

    
202

    
203
class ArgFile(_Argument):
204
  """File path argument.
205

206
  """
207

    
208

    
209
class ArgCommand(_Argument):
210
  """Command argument.
211

212
  """
213

    
214

    
215
class ArgHost(_Argument):
216
  """Host argument.
217

218
  """
219

    
220

    
221
ARGS_NONE = []
222
ARGS_MANY_INSTANCES = [ArgInstance()]
223
ARGS_MANY_NODES = [ArgNode()]
224
ARGS_ONE_INSTANCE = [ArgInstance(min=1, max=1)]
225
ARGS_ONE_NODE = [ArgNode(min=1, max=1)]
226

    
227

    
228

    
229
def _ExtractTagsObject(opts, args):
230
  """Extract the tag type object.
231

232
  Note that this function will modify its args parameter.
233

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

    
249

    
250
def _ExtendTags(opts, args):
251
  """Extend the args if a source file has been given.
252

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

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

    
278

    
279
def ListTags(opts, args):
280
  """List the tags on a given object.
281

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

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

    
296

    
297
def AddTags(opts, args):
298
  """Add tags on a given object.
299

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

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

    
313

    
314
def RemoveTags(opts, args):
315
  """Remove tags from a given object.
316

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

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

    
330

    
331
def check_unit(option, opt, value):
332
  """OptParsers custom converter for units.
333

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

    
340

    
341
def _SplitKeyVal(opt, data):
342
  """Convert a KeyVal string into a dict.
343

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

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

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

    
377

    
378
def check_ident_key_val(option, opt, value):
379
  """Custom parser for ident:key=val,key=val options.
380

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

384
  """
385
  if ":" not in value:
386
    ident, rest = value, ''
387
  else:
388
    ident, rest = value.split(":", 1)
389

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

    
405

    
406
def check_key_val(option, opt, value):
407
  """Custom parser class for key=val,key=val options.
408

409
  This will store the parsed values as a dict {key: val}.
410

411
  """
412
  return _SplitKeyVal(opt, value)
413

    
414

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

    
424
OPT_COMPL_ALL = frozenset([
425
  OPT_COMPL_MANY_NODES,
426
  OPT_COMPL_ONE_NODE,
427
  OPT_COMPL_ONE_INSTANCE,
428
  OPT_COMPL_ONE_OS,
429
  OPT_COMPL_ONE_IALLOCATOR,
430
  OPT_COMPL_INST_ADD_NODES,
431
  ])
432

    
433

    
434
class CliOption(Option):
435
  """Custom option class for optparse.
436

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

    
451

    
452
# optparse.py sets make_option, so we do it for our own option class, too
453
cli_option = CliOption
454

    
455

    
456
DEBUG_OPT = cli_option("-d", "--debug", default=False,
457
                       action="store_true",
458
                       help="Turn debugging on")
459

    
460
NOHDR_OPT = cli_option("--no-headers", default=False,
461
                       action="store_true", dest="no_headers",
462
                       help="Don't display column headers")
463

    
464
SEP_OPT = cli_option("--separator", default=None,
465
                     action="store", dest="separator",
466
                     help=("Separator between output fields"
467
                           " (defaults to one space)"))
468

    
469
USEUNITS_OPT = cli_option("--units", default=None,
470
                          dest="units", choices=('h', 'm', 'g', 't'),
471
                          help="Specify units for output (one of hmgt)")
472

    
473
FIELDS_OPT = cli_option("-o", "--output", dest="output", action="store",
474
                        type="string", metavar="FIELDS",
475
                        help="Comma separated list of output fields")
476

    
477
FORCE_OPT = cli_option("-f", "--force", dest="force", action="store_true",
478
                       default=False, help="Force the operation")
479

    
480
CONFIRM_OPT = cli_option("--yes", dest="confirm", action="store_true",
481
                         default=False, help="Do not require confirmation")
482

    
483
TAG_SRC_OPT = cli_option("--from", dest="tags_source",
484
                         default=None, help="File with tag names")
485

    
486
SUBMIT_OPT = cli_option("--submit", dest="submit_only",
487
                        default=False, action="store_true",
488
                        help=("Submit the job and return the job ID, but"
489
                              " don't wait for the job to finish"))
490

    
491
SYNC_OPT = cli_option("--sync", dest="do_locking",
492
                      default=False, action="store_true",
493
                      help=("Grab locks while doing the queries"
494
                            " in order to ensure more consistent results"))
495

    
496
_DRY_RUN_OPT = cli_option("--dry-run", default=False,
497
                          action="store_true",
498
                          help=("Do not execute the operation, just run the"
499
                                " check steps and verify it it could be"
500
                                " executed"))
501

    
502
VERBOSE_OPT = cli_option("-v", "--verbose", default=False,
503
                         action="store_true",
504
                         help="Increase the verbosity of the operation")
505

    
506
DEBUG_SIMERR_OPT = cli_option("--debug-simulate-errors", default=False,
507
                              action="store_true", dest="simulate_errors",
508
                              help="Debugging option that makes the operation"
509
                              " treat most runtime checks as failed")
510

    
511
NWSYNC_OPT = cli_option("--no-wait-for-sync", dest="wait_for_sync",
512
                        default=True, action="store_false",
513
                        help="Don't wait for sync (DANGEROUS!)")
514

    
515
DISK_TEMPLATE_OPT = cli_option("-t", "--disk-template", dest="disk_template",
516
                               help="Custom disk setup (diskless, file,"
517
                               " plain or drbd)",
518
                               default=None, metavar="TEMPL",
519
                               choices=list(constants.DISK_TEMPLATES))
520

    
521
NONICS_OPT = cli_option("--no-nics", default=False, action="store_true",
522
                        help="Do not create any network cards for"
523
                        " the instance")
524

    
525
FILESTORE_DIR_OPT = cli_option("--file-storage-dir", dest="file_storage_dir",
526
                               help="Relative path under default cluster-wide"
527
                               " file storage dir to store file-based disks",
528
                               default=None, metavar="<DIR>")
529

    
530
FILESTORE_DRIVER_OPT = cli_option("--file-driver", dest="file_driver",
531
                                  help="Driver to use for image files",
532
                                  default="loop", metavar="<DRIVER>",
533
                                  choices=list(constants.FILE_DRIVER))
534

    
535
IALLOCATOR_OPT = cli_option("-I", "--iallocator", metavar="<NAME>",
536
                            help="Select nodes for the instance automatically"
537
                            " using the <NAME> iallocator plugin",
538
                            default=None, type="string",
539
                            completion_suggest=OPT_COMPL_ONE_IALLOCATOR)
540

    
541
OS_OPT = cli_option("-o", "--os-type", dest="os", help="What OS to run",
542
                    metavar="<os>",
543
                    completion_suggest=OPT_COMPL_ONE_OS)
544

    
545
BACKEND_OPT = cli_option("-B", "--backend-parameters", dest="beparams",
546
                         type="keyval", default={},
547
                         help="Backend parameters")
548

    
549
HVOPTS_OPT =  cli_option("-H", "--hypervisor-parameters", type="keyval",
550
                         default={}, dest="hvparams",
551
                         help="Hypervisor parameters")
552

    
553
HYPERVISOR_OPT = cli_option("-H", "--hypervisor-parameters", dest="hypervisor",
554
                            help="Hypervisor and hypervisor options, in the"
555
                            " format hypervisor:option=value,option=value,...",
556
                            default=None, type="identkeyval")
557

    
558
HVLIST_OPT = cli_option("-H", "--hypervisor-parameters", dest="hvparams",
559
                        help="Hypervisor and hypervisor options, in the"
560
                        " format hypervisor:option=value,option=value,...",
561
                        default=[], action="append", type="identkeyval")
562

    
563
NOIPCHECK_OPT = cli_option("--no-ip-check", dest="ip_check", default=True,
564
                           action="store_false",
565
                           help="Don't check that the instance's IP"
566
                           " is alive")
567

    
568
NET_OPT = cli_option("--net",
569
                     help="NIC parameters", default=[],
570
                     dest="nics", action="append", type="identkeyval")
571

    
572
DISK_OPT = cli_option("--disk", help="Disk parameters", default=[],
573
                      dest="disks", action="append", type="identkeyval")
574

    
575
DISKIDX_OPT = cli_option("--disks", dest="disks", default=None,
576
                         help="Comma-separated list of disks"
577
                         " indices to act on (e.g. 0,2) (optional,"
578
                         " defaults to all disks)")
579

    
580
OS_SIZE_OPT = cli_option("-s", "--os-size", dest="sd_size",
581
                         help="Enforces a single-disk configuration using the"
582
                         " given disk size, in MiB unless a suffix is used",
583
                         default=None, type="unit", metavar="<size>")
584

    
585
IGNORE_CONSIST_OPT = cli_option("--ignore-consistency",
586
                                dest="ignore_consistency",
587
                                action="store_true", default=False,
588
                                help="Ignore the consistency of the disks on"
589
                                " the secondary")
590

    
591
NONLIVE_OPT = cli_option("--non-live", dest="live",
592
                         default=True, action="store_false",
593
                         help="Do a non-live migration (this usually means"
594
                         " freeze the instance, save the state, transfer and"
595
                         " only then resume running on the secondary node)")
596

    
597
NODE_PLACEMENT_OPT = cli_option("-n", "--node", dest="node",
598
                                help="Target node and optional secondary node",
599
                                metavar="<pnode>[:<snode>]",
600
                                completion_suggest=OPT_COMPL_INST_ADD_NODES)
601

    
602
NODE_LIST_OPT = cli_option("-n", "--node", dest="nodes", default=[],
603
                           action="append", metavar="<node>",
604
                           help="Use only this node (can be used multiple"
605
                           " times, if not given defaults to all nodes)",
606
                           completion_suggest=OPT_COMPL_ONE_NODE)
607

    
608
SINGLE_NODE_OPT = cli_option("-n", "--node", dest="node", help="Target node",
609
                             metavar="<node>",
610
                             completion_suggest=OPT_COMPL_ONE_NODE)
611

    
612
NOSTART_OPT = cli_option("--no-start", dest="start", default=True,
613
                         action="store_false",
614
                         help="Don't start the instance after creation")
615

    
616
SHOWCMD_OPT = cli_option("--show-cmd", dest="show_command",
617
                         action="store_true", default=False,
618
                         help="Show command instead of executing it")
619

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

    
629
STATIC_OPT = cli_option("-s", "--static", dest="static",
630
                        action="store_true", default=False,
631
                        help="Only show configuration data, not runtime data")
632

    
633
ALL_OPT = cli_option("--all", dest="show_all",
634
                     default=False, action="store_true",
635
                     help="Show info on all instances on the cluster."
636
                     " This can take a long time to run, use wisely")
637

    
638
SELECT_OS_OPT = cli_option("--select-os", dest="select_os",
639
                           action="store_true", default=False,
640
                           help="Interactive OS reinstall, lists available"
641
                           " OS templates for selection")
642

    
643
IGNORE_FAILURES_OPT = cli_option("--ignore-failures", dest="ignore_failures",
644
                                 action="store_true", default=False,
645
                                 help="Remove the instance from the cluster"
646
                                 " configuration even if there are failures"
647
                                 " during the removal process")
648

    
649
NEW_SECONDARY_OPT = cli_option("-n", "--new-secondary", dest="dst_node",
650
                               help="Specifies the new secondary node",
651
                               metavar="NODE", default=None,
652
                               completion_suggest=OPT_COMPL_ONE_NODE)
653

    
654
ON_PRIMARY_OPT = cli_option("-p", "--on-primary", dest="on_primary",
655
                            default=False, action="store_true",
656
                            help="Replace the disk(s) on the primary"
657
                            " node (only for the drbd template)")
658

    
659
ON_SECONDARY_OPT = cli_option("-s", "--on-secondary", dest="on_secondary",
660
                              default=False, action="store_true",
661
                              help="Replace the disk(s) on the secondary"
662
                              " node (only for the drbd template)")
663

    
664
AUTO_REPLACE_OPT = cli_option("-a", "--auto", dest="auto",
665
                              default=False, action="store_true",
666
                              help="Automatically replace faulty disks"
667
                              " (only for the drbd template)")
668

    
669
IGNORE_SIZE_OPT = cli_option("--ignore-size", dest="ignore_size",
670
                             default=False, action="store_true",
671
                             help="Ignore current recorded size"
672
                             " (useful for forcing activation when"
673
                             " the recorded size is wrong)")
674

    
675
SRC_NODE_OPT = cli_option("--src-node", dest="src_node", help="Source node",
676
                          metavar="<node>",
677
                          completion_suggest=OPT_COMPL_ONE_NODE)
678

    
679
SRC_DIR_OPT = cli_option("--src-dir", dest="src_dir", help="Source directory",
680
                         metavar="<dir>")
681

    
682
SECONDARY_IP_OPT = cli_option("-s", "--secondary-ip", dest="secondary_ip",
683
                              help="Specify the secondary ip for the node",
684
                              metavar="ADDRESS", default=None)
685

    
686
READD_OPT = cli_option("--readd", dest="readd",
687
                       default=False, action="store_true",
688
                       help="Readd old node after replacing it")
689

    
690
NOSSH_KEYCHECK_OPT = cli_option("--no-ssh-key-check", dest="ssh_key_check",
691
                                default=True, action="store_false",
692
                                help="Disable SSH key fingerprint checking")
693

    
694

    
695
def _ParseArgs(argv, commands, aliases):
696
  """Parser for the command line arguments.
697

698
  This function parses the arguments and returns the function which
699
  must be executed together with its (modified) arguments.
700

701
  @param argv: the command line
702
  @param commands: dictionary with special contents, see the design
703
      doc for cmdline handling
704
  @param aliases: dictionary with command aliases {'alias': 'target, ...}
705

706
  """
707
  if len(argv) == 0:
708
    binary = "<command>"
709
  else:
710
    binary = argv[0].split("/")[-1]
711

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

    
718
  if len(argv) < 2 or not (argv[1] in commands or
719
                           argv[1] in aliases):
720
    # let's do a nice thing
721
    sortedcmds = commands.keys()
722
    sortedcmds.sort()
723

    
724
    ToStdout("Usage: %s {command} [options...] [argument...]", binary)
725
    ToStdout("%s <command> --help to see details, or man %s", binary, binary)
726
    ToStdout("")
727

    
728
    # compute the max line length for cmd + usage
729
    mlen = max([len(" %s" % cmd) for cmd in commands])
730
    mlen = min(60, mlen) # should not get here...
731

    
732
    # and format a nice command list
733
    ToStdout("Commands:")
734
    for cmd in sortedcmds:
735
      cmdstr = " %s" % (cmd,)
736
      help_text = commands[cmd][4]
737
      help_lines = textwrap.wrap(help_text, 79 - 3 - mlen)
738
      ToStdout("%-*s - %s", mlen, cmdstr, help_lines.pop(0))
739
      for line in help_lines:
740
        ToStdout("%-*s   %s", mlen, "", line)
741

    
742
    ToStdout("")
743

    
744
    return None, None, None
745

    
746
  # get command, unalias it, and look it up in commands
747
  cmd = argv.pop(1)
748
  if cmd in aliases:
749
    if cmd in commands:
750
      raise errors.ProgrammerError("Alias '%s' overrides an existing"
751
                                   " command" % cmd)
752

    
753
    if aliases[cmd] not in commands:
754
      raise errors.ProgrammerError("Alias '%s' maps to non-existing"
755
                                   " command '%s'" % (cmd, aliases[cmd]))
756

    
757
    cmd = aliases[cmd]
758

    
759
  func, args_def, parser_opts, usage, description = commands[cmd]
760
  parser = OptionParser(option_list=parser_opts + [_DRY_RUN_OPT],
761
                        description=description,
762
                        formatter=TitledHelpFormatter(),
763
                        usage="%%prog %s %s" % (cmd, usage))
764
  parser.disable_interspersed_args()
765
  options, args = parser.parse_args()
766

    
767
  if not _CheckArguments(cmd, args_def, args):
768
    return None, None, None
769

    
770
  return func, options, args
771

    
772

    
773
def _CheckArguments(cmd, args_def, args):
774
  """Verifies the arguments using the argument definition.
775

776
  Algorithm:
777

778
    1. Abort with error if values specified by user but none expected.
779

780
    1. For each argument in definition
781

782
      1. Keep running count of minimum number of values (min_count)
783
      1. Keep running count of maximum number of values (max_count)
784
      1. If it has an unlimited number of values
785

786
        1. Abort with error if it's not the last argument in the definition
787

788
    1. If last argument has limited number of values
789

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

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

794
  """
795
  if args and not args_def:
796
    ToStderr("Error: Command %s expects no arguments", cmd)
797
    return False
798

    
799
  min_count = None
800
  max_count = None
801
  check_max = None
802

    
803
  last_idx = len(args_def) - 1
804

    
805
  for idx, arg in enumerate(args_def):
806
    if min_count is None:
807
      min_count = arg.min
808
    elif arg.min is not None:
809
      min_count += arg.min
810

    
811
    if max_count is None:
812
      max_count = arg.max
813
    elif arg.max is not None:
814
      max_count += arg.max
815

    
816
    if idx == last_idx:
817
      check_max = (arg.max is not None)
818

    
819
    elif arg.max is None:
820
      raise errors.ProgrammerError("Only the last argument can have max=None")
821

    
822
  if check_max:
823
    # Command with exact number of arguments
824
    if (min_count is not None and max_count is not None and
825
        min_count == max_count and len(args) != min_count):
826
      ToStderr("Error: Command %s expects %d argument(s)", cmd, min_count)
827
      return False
828

    
829
    # Command with limited number of arguments
830
    if max_count is not None and len(args) > max_count:
831
      ToStderr("Error: Command %s expects only %d argument(s)",
832
               cmd, max_count)
833
      return False
834

    
835
  # Command with some required arguments
836
  if min_count is not None and len(args) < min_count:
837
    ToStderr("Error: Command %s expects at least %d argument(s)",
838
             cmd, min_count)
839
    return False
840

    
841
  return True
842

    
843

    
844
def SplitNodeOption(value):
845
  """Splits the value of a --node option.
846

847
  """
848
  if value and ':' in value:
849
    return value.split(':', 1)
850
  else:
851
    return (value, None)
852

    
853

    
854
def UsesRPC(fn):
855
  def wrapper(*args, **kwargs):
856
    rpc.Init()
857
    try:
858
      return fn(*args, **kwargs)
859
    finally:
860
      rpc.Shutdown()
861
  return wrapper
862

    
863

    
864
def AskUser(text, choices=None):
865
  """Ask the user a question.
866

867
  @param text: the question to ask
868

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

874
  @return: one of the return values from the choices list; if input is
875
      not possible (i.e. not running with a tty, we return the last
876
      entry from the list
877

878
  """
879
  if choices is None:
880
    choices = [('y', True, 'Perform the operation'),
881
               ('n', False, 'Do not perform the operation')]
882
  if not choices or not isinstance(choices, list):
883
    raise errors.ProgrammerError("Invalid choices argument to AskUser")
884
  for entry in choices:
885
    if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
886
      raise errors.ProgrammerError("Invalid choices element to AskUser")
887

    
888
  answer = choices[-1][1]
889
  new_text = []
890
  for line in text.splitlines():
891
    new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
892
  text = "\n".join(new_text)
893
  try:
894
    f = file("/dev/tty", "a+")
895
  except IOError:
896
    return answer
897
  try:
898
    chars = [entry[0] for entry in choices]
899
    chars[-1] = "[%s]" % chars[-1]
900
    chars.append('?')
901
    maps = dict([(entry[0], entry[1]) for entry in choices])
902
    while True:
903
      f.write(text)
904
      f.write('\n')
905
      f.write("/".join(chars))
906
      f.write(": ")
907
      line = f.readline(2).strip().lower()
908
      if line in maps:
909
        answer = maps[line]
910
        break
911
      elif line == '?':
912
        for entry in choices:
913
          f.write(" %s - %s\n" % (entry[0], entry[2]))
914
        f.write("\n")
915
        continue
916
  finally:
917
    f.close()
918
  return answer
919

    
920

    
921
class JobSubmittedException(Exception):
922
  """Job was submitted, client should exit.
923

924
  This exception has one argument, the ID of the job that was
925
  submitted. The handler should print this ID.
926

927
  This is not an error, just a structured way to exit from clients.
928

929
  """
930

    
931

    
932
def SendJob(ops, cl=None):
933
  """Function to submit an opcode without waiting for the results.
934

935
  @type ops: list
936
  @param ops: list of opcodes
937
  @type cl: luxi.Client
938
  @param cl: the luxi client to use for communicating with the master;
939
             if None, a new client will be created
940

941
  """
942
  if cl is None:
943
    cl = GetClient()
944

    
945
  job_id = cl.SubmitJob(ops)
946

    
947
  return job_id
948

    
949

    
950
def PollJob(job_id, cl=None, feedback_fn=None):
951
  """Function to poll for the result of a job.
952

953
  @type job_id: job identified
954
  @param job_id: the job to poll for results
955
  @type cl: luxi.Client
956
  @param cl: the luxi client to use for communicating with the master;
957
             if None, a new client will be created
958

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

    
963
  prev_job_info = None
964
  prev_logmsg_serial = None
965

    
966
  while True:
967
    result = cl.WaitForJobChange(job_id, ["status"], prev_job_info,
968
                                 prev_logmsg_serial)
969
    if not result:
970
      # job not found, go away!
971
      raise errors.JobLost("Job with id %s lost" % job_id)
972

    
973
    # Split result, a tuple of (field values, log entries)
974
    (job_info, log_entries) = result
975
    (status, ) = job_info
976

    
977
    if log_entries:
978
      for log_entry in log_entries:
979
        (serial, timestamp, _, message) = log_entry
980
        if callable(feedback_fn):
981
          feedback_fn(log_entry[1:])
982
        else:
983
          encoded = utils.SafeEncode(message)
984
          ToStdout("%s %s", time.ctime(utils.MergeTime(timestamp)), encoded)
985
        prev_logmsg_serial = max(prev_logmsg_serial, serial)
986

    
987
    # TODO: Handle canceled and archived jobs
988
    elif status in (constants.JOB_STATUS_SUCCESS,
989
                    constants.JOB_STATUS_ERROR,
990
                    constants.JOB_STATUS_CANCELING,
991
                    constants.JOB_STATUS_CANCELED):
992
      break
993

    
994
    prev_job_info = job_info
995

    
996
  jobs = cl.QueryJobs([job_id], ["status", "opstatus", "opresult"])
997
  if not jobs:
998
    raise errors.JobLost("Job with id %s lost" % job_id)
999

    
1000
  status, opstatus, result = jobs[0]
1001
  if status == constants.JOB_STATUS_SUCCESS:
1002
    return result
1003
  elif status in (constants.JOB_STATUS_CANCELING,
1004
                  constants.JOB_STATUS_CANCELED):
1005
    raise errors.OpExecError("Job was canceled")
1006
  else:
1007
    has_ok = False
1008
    for idx, (status, msg) in enumerate(zip(opstatus, result)):
1009
      if status == constants.OP_STATUS_SUCCESS:
1010
        has_ok = True
1011
      elif status == constants.OP_STATUS_ERROR:
1012
        errors.MaybeRaise(msg)
1013
        if has_ok:
1014
          raise errors.OpExecError("partial failure (opcode %d): %s" %
1015
                                   (idx, msg))
1016
        else:
1017
          raise errors.OpExecError(str(msg))
1018
    # default failure mode
1019
    raise errors.OpExecError(result)
1020

    
1021

    
1022
def SubmitOpCode(op, cl=None, feedback_fn=None):
1023
  """Legacy function to submit an opcode.
1024

1025
  This is just a simple wrapper over the construction of the processor
1026
  instance. It should be extended to better handle feedback and
1027
  interaction functions.
1028

1029
  """
1030
  if cl is None:
1031
    cl = GetClient()
1032

    
1033
  job_id = SendJob([op], cl)
1034

    
1035
  op_results = PollJob(job_id, cl=cl, feedback_fn=feedback_fn)
1036

    
1037
  return op_results[0]
1038

    
1039

    
1040
def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
1041
  """Wrapper around SubmitOpCode or SendJob.
1042

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

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

1050
  """
1051
  if opts and opts.dry_run:
1052
    op.dry_run = opts.dry_run
1053
  if opts and opts.submit_only:
1054
    job_id = SendJob([op], cl=cl)
1055
    raise JobSubmittedException(job_id)
1056
  else:
1057
    return SubmitOpCode(op, cl=cl, feedback_fn=feedback_fn)
1058

    
1059

    
1060
def GetClient():
1061
  # TODO: Cache object?
1062
  try:
1063
    client = luxi.Client()
1064
  except luxi.NoMasterError:
1065
    master, myself = ssconf.GetMasterAndMyself()
1066
    if master != myself:
1067
      raise errors.OpPrereqError("This is not the master node, please connect"
1068
                                 " to node '%s' and rerun the command" %
1069
                                 master)
1070
    else:
1071
      raise
1072
  return client
1073

    
1074

    
1075
def FormatError(err):
1076
  """Return a formatted error message for a given error.
1077

1078
  This function takes an exception instance and returns a tuple
1079
  consisting of two values: first, the recommended exit code, and
1080
  second, a string describing the error message (not
1081
  newline-terminated).
1082

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

    
1146

    
1147
def GenericMain(commands, override=None, aliases=None):
1148
  """Generic main function for all the gnt-* commands.
1149

1150
  Arguments:
1151
    - commands: a dictionary with a special structure, see the design doc
1152
                for command line handling.
1153
    - override: if not None, we expect a dictionary with keys that will
1154
                override command line options; this can be used to pass
1155
                options from the scripts to generic functions
1156
    - aliases: dictionary with command aliases {'alias': 'target, ...}
1157

1158
  """
1159
  # save the program name and the entire command line for later logging
1160
  if sys.argv:
1161
    binary = os.path.basename(sys.argv[0]) or sys.argv[0]
1162
    if len(sys.argv) >= 2:
1163
      binary += " " + sys.argv[1]
1164
      old_cmdline = " ".join(sys.argv[2:])
1165
    else:
1166
      old_cmdline = ""
1167
  else:
1168
    binary = "<unknown program>"
1169
    old_cmdline = ""
1170

    
1171
  if aliases is None:
1172
    aliases = {}
1173

    
1174
  try:
1175
    func, options, args = _ParseArgs(sys.argv, commands, aliases)
1176
  except errors.ParameterError, err:
1177
    result, err_msg = FormatError(err)
1178
    ToStderr(err_msg)
1179
    return 1
1180

    
1181
  if func is None: # parse error
1182
    return 1
1183

    
1184
  if override is not None:
1185
    for key, val in override.iteritems():
1186
      setattr(options, key, val)
1187

    
1188
  utils.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
1189
                     stderr_logging=True, program=binary)
1190

    
1191
  if old_cmdline:
1192
    logging.info("run with arguments '%s'", old_cmdline)
1193
  else:
1194
    logging.info("run with no arguments")
1195

    
1196
  try:
1197
    result = func(options, args)
1198
  except (errors.GenericError, luxi.ProtocolError,
1199
          JobSubmittedException), err:
1200
    result, err_msg = FormatError(err)
1201
    logging.exception("Error during command processing")
1202
    ToStderr(err_msg)
1203

    
1204
  return result
1205

    
1206

    
1207
def GenerateTable(headers, fields, separator, data,
1208
                  numfields=None, unitfields=None,
1209
                  units=None):
1210
  """Prints a table with headers and different fields.
1211

1212
  @type headers: dict
1213
  @param headers: dictionary mapping field names to headers for
1214
      the table
1215
  @type fields: list
1216
  @param fields: the field names corresponding to each row in
1217
      the data field
1218
  @param separator: the separator to be used; if this is None,
1219
      the default 'smart' algorithm is used which computes optimal
1220
      field width, otherwise just the separator is used between
1221
      each field
1222
  @type data: list
1223
  @param data: a list of lists, each sublist being one row to be output
1224
  @type numfields: list
1225
  @param numfields: a list with the fields that hold numeric
1226
      values and thus should be right-aligned
1227
  @type unitfields: list
1228
  @param unitfields: a list with the fields that hold numeric
1229
      values that should be formatted with the units field
1230
  @type units: string or None
1231
  @param units: the units we should use for formatting, or None for
1232
      automatic choice (human-readable for non-separator usage, otherwise
1233
      megabytes); this is a one-letter string
1234

1235
  """
1236
  if units is None:
1237
    if separator:
1238
      units = "m"
1239
    else:
1240
      units = "h"
1241

    
1242
  if numfields is None:
1243
    numfields = []
1244
  if unitfields is None:
1245
    unitfields = []
1246

    
1247
  numfields = utils.FieldSet(*numfields)
1248
  unitfields = utils.FieldSet(*unitfields)
1249

    
1250
  format_fields = []
1251
  for field in fields:
1252
    if headers and field not in headers:
1253
      # TODO: handle better unknown fields (either revert to old
1254
      # style of raising exception, or deal more intelligently with
1255
      # variable fields)
1256
      headers[field] = field
1257
    if separator is not None:
1258
      format_fields.append("%s")
1259
    elif numfields.Matches(field):
1260
      format_fields.append("%*s")
1261
    else:
1262
      format_fields.append("%-*s")
1263

    
1264
  if separator is None:
1265
    mlens = [0 for name in fields]
1266
    format = ' '.join(format_fields)
1267
  else:
1268
    format = separator.replace("%", "%%").join(format_fields)
1269

    
1270
  for row in data:
1271
    if row is None:
1272
      continue
1273
    for idx, val in enumerate(row):
1274
      if unitfields.Matches(fields[idx]):
1275
        try:
1276
          val = int(val)
1277
        except ValueError:
1278
          pass
1279
        else:
1280
          val = row[idx] = utils.FormatUnit(val, units)
1281
      val = row[idx] = str(val)
1282
      if separator is None:
1283
        mlens[idx] = max(mlens[idx], len(val))
1284

    
1285
  result = []
1286
  if headers:
1287
    args = []
1288
    for idx, name in enumerate(fields):
1289
      hdr = headers[name]
1290
      if separator is None:
1291
        mlens[idx] = max(mlens[idx], len(hdr))
1292
        args.append(mlens[idx])
1293
      args.append(hdr)
1294
    result.append(format % tuple(args))
1295

    
1296
  for line in data:
1297
    args = []
1298
    if line is None:
1299
      line = ['-' for _ in fields]
1300
    for idx in xrange(len(fields)):
1301
      if separator is None:
1302
        args.append(mlens[idx])
1303
      args.append(line[idx])
1304
    result.append(format % tuple(args))
1305

    
1306
  return result
1307

    
1308

    
1309
def FormatTimestamp(ts):
1310
  """Formats a given timestamp.
1311

1312
  @type ts: timestamp
1313
  @param ts: a timeval-type timestamp, a tuple of seconds and microseconds
1314

1315
  @rtype: string
1316
  @return: a string with the formatted timestamp
1317

1318
  """
1319
  if not isinstance (ts, (tuple, list)) or len(ts) != 2:
1320
    return '?'
1321
  sec, usec = ts
1322
  return time.strftime("%F %T", time.localtime(sec)) + ".%06d" % usec
1323

    
1324

    
1325
def ParseTimespec(value):
1326
  """Parse a time specification.
1327

1328
  The following suffixed will be recognized:
1329

1330
    - s: seconds
1331
    - m: minutes
1332
    - h: hours
1333
    - d: day
1334
    - w: weeks
1335

1336
  Without any suffix, the value will be taken to be in seconds.
1337

1338
  """
1339
  value = str(value)
1340
  if not value:
1341
    raise errors.OpPrereqError("Empty time specification passed")
1342
  suffix_map = {
1343
    's': 1,
1344
    'm': 60,
1345
    'h': 3600,
1346
    'd': 86400,
1347
    'w': 604800,
1348
    }
1349
  if value[-1] not in suffix_map:
1350
    try:
1351
      value = int(value)
1352
    except ValueError:
1353
      raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1354
  else:
1355
    multiplier = suffix_map[value[-1]]
1356
    value = value[:-1]
1357
    if not value: # no data left after stripping the suffix
1358
      raise errors.OpPrereqError("Invalid time specification (only"
1359
                                 " suffix passed)")
1360
    try:
1361
      value = int(value) * multiplier
1362
    except ValueError:
1363
      raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1364
  return value
1365

    
1366

    
1367
def GetOnlineNodes(nodes, cl=None, nowarn=False):
1368
  """Returns the names of online nodes.
1369

1370
  This function will also log a warning on stderr with the names of
1371
  the online nodes.
1372

1373
  @param nodes: if not empty, use only this subset of nodes (minus the
1374
      offline ones)
1375
  @param cl: if not None, luxi client to use
1376
  @type nowarn: boolean
1377
  @param nowarn: by default, this function will output a note with the
1378
      offline nodes that are skipped; if this parameter is True the
1379
      note is not displayed
1380

1381
  """
1382
  if cl is None:
1383
    cl = GetClient()
1384

    
1385
  result = cl.QueryNodes(names=nodes, fields=["name", "offline"],
1386
                         use_locking=False)
1387
  offline = [row[0] for row in result if row[1]]
1388
  if offline and not nowarn:
1389
    ToStderr("Note: skipping offline node(s): %s" % ", ".join(offline))
1390
  return [row[0] for row in result if not row[1]]
1391

    
1392

    
1393
def _ToStream(stream, txt, *args):
1394
  """Write a message to a stream, bypassing the logging system
1395

1396
  @type stream: file object
1397
  @param stream: the file to which we should write
1398
  @type txt: str
1399
  @param txt: the message
1400

1401
  """
1402
  if args:
1403
    args = tuple(args)
1404
    stream.write(txt % args)
1405
  else:
1406
    stream.write(txt)
1407
  stream.write('\n')
1408
  stream.flush()
1409

    
1410

    
1411
def ToStdout(txt, *args):
1412
  """Write a message to stdout only, bypassing the logging system
1413

1414
  This is just a wrapper over _ToStream.
1415

1416
  @type txt: str
1417
  @param txt: the message
1418

1419
  """
1420
  _ToStream(sys.stdout, txt, *args)
1421

    
1422

    
1423
def ToStderr(txt, *args):
1424
  """Write a message to stderr only, bypassing the logging system
1425

1426
  This is just a wrapper over _ToStream.
1427

1428
  @type txt: str
1429
  @param txt: the message
1430

1431
  """
1432
  _ToStream(sys.stderr, txt, *args)
1433

    
1434

    
1435
class JobExecutor(object):
1436
  """Class which manages the submission and execution of multiple jobs.
1437

1438
  Note that instances of this class should not be reused between
1439
  GetResults() calls.
1440

1441
  """
1442
  def __init__(self, cl=None, verbose=True):
1443
    self.queue = []
1444
    if cl is None:
1445
      cl = GetClient()
1446
    self.cl = cl
1447
    self.verbose = verbose
1448
    self.jobs = []
1449

    
1450
  def QueueJob(self, name, *ops):
1451
    """Record a job for later submit.
1452

1453
    @type name: string
1454
    @param name: a description of the job, will be used in WaitJobSet
1455
    """
1456
    self.queue.append((name, ops))
1457

    
1458
  def SubmitPending(self):
1459
    """Submit all pending jobs.
1460

1461
    """
1462
    results = self.cl.SubmitManyJobs([row[1] for row in self.queue])
1463
    for ((status, data), (name, _)) in zip(results, self.queue):
1464
      self.jobs.append((status, data, name))
1465

    
1466
  def GetResults(self):
1467
    """Wait for and return the results of all jobs.
1468

1469
    @rtype: list
1470
    @return: list of tuples (success, job results), in the same order
1471
        as the submitted jobs; if a job has failed, instead of the result
1472
        there will be the error message
1473

1474
    """
1475
    if not self.jobs:
1476
      self.SubmitPending()
1477
    results = []
1478
    if self.verbose:
1479
      ok_jobs = [row[1] for row in self.jobs if row[0]]
1480
      if ok_jobs:
1481
        ToStdout("Submitted jobs %s", ", ".join(ok_jobs))
1482
    for submit_status, jid, name in self.jobs:
1483
      if not submit_status:
1484
        ToStderr("Failed to submit job for %s: %s", name, jid)
1485
        results.append((False, jid))
1486
        continue
1487
      if self.verbose:
1488
        ToStdout("Waiting for job %s for %s...", jid, name)
1489
      try:
1490
        job_result = PollJob(jid, cl=self.cl)
1491
        success = True
1492
      except (errors.GenericError, luxi.ProtocolError), err:
1493
        _, job_result = FormatError(err)
1494
        success = False
1495
        # the error message will always be shown, verbose or not
1496
        ToStderr("Job %s for %s has failed: %s", jid, name, job_result)
1497

    
1498
      results.append((success, job_result))
1499
    return results
1500

    
1501
  def WaitOrShow(self, wait):
1502
    """Wait for job results or only print the job IDs.
1503

1504
    @type wait: boolean
1505
    @param wait: whether to wait or not
1506

1507
    """
1508
    if wait:
1509
      return self.GetResults()
1510
    else:
1511
      if not self.jobs:
1512
        self.SubmitPending()
1513
      for status, result, name in self.jobs:
1514
        if status:
1515
          ToStdout("%s: %s", result, name)
1516
        else:
1517
          ToStderr("Failure for %s: %s", name, result)