Statistics
| Branch: | Tag: | Revision:

root / tools / burnin @ a52ba89d

History | View | Annotate | Download (32.4 kB)

1
#!/usr/bin/python
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
"""Burnin program
23

    
24
"""
25

    
26
import os
27
import sys
28
import optparse
29
import time
30
import socket
31
import urllib
32
from itertools import izip, islice, cycle
33
from cStringIO import StringIO
34

    
35
from ganeti import opcodes
36
from ganeti import constants
37
from ganeti import cli
38
from ganeti import errors
39
from ganeti import utils
40

    
41

    
42
USAGE = ("\tburnin -o OS_NAME [options...] instance_name ...")
43

    
44
MAX_RETRIES = 3
45

    
46
class InstanceDown(Exception):
47
  """The checked instance was not up"""
48

    
49

    
50
class BurninFailure(Exception):
51
  """Failure detected during burning"""
52

    
53

    
54
def Usage():
55
  """Shows program usage information and exits the program."""
56

    
57
  print >> sys.stderr, "Usage:"
58
  print >> sys.stderr, USAGE
59
  sys.exit(2)
60

    
61

    
62
def Log(msg, indent=0):
63
  """Simple function that prints out its argument.
64

    
65
  """
66
  headers = {
67
    0: "- ",
68
    1: "* ",
69
    2: ""
70
    }
71
  sys.stdout.write("%*s%s%s\n" % (2*indent, "",
72
                                   headers.get(indent, "  "), msg))
73
  sys.stdout.flush()
74

    
75
def Err(msg, exit_code=1):
76
  """Simple error logging that prints to stderr.
77

    
78
  """
79
  sys.stderr.write(msg + "\n")
80
  sys.stderr.flush()
81
  sys.exit(exit_code)
82

    
83

    
84
class SimpleOpener(urllib.FancyURLopener):
85
  """A simple url opener"""
86

    
87
  def prompt_user_passwd(self, host, realm, clear_cache = 0):
88
    """No-interaction version of prompt_user_passwd."""
89
    return None, None
90

    
91
  def http_error_default(self, url, fp, errcode, errmsg, headers):
92
    """Custom error handling"""
93
    # make sure sockets are not left in CLOSE_WAIT, this is similar
94
    # but with a different exception to the BasicURLOpener class
95
    _ = fp.read() # throw away data
96
    fp.close()
97
    raise InstanceDown("HTTP error returned: code %s, msg %s" %
98
                       (errcode, errmsg))
99

    
100

    
101
OPTIONS = [
102
  cli.cli_option("-o", "--os", dest="os", default=None,
103
                 help="OS to use during burnin",
104
                 metavar="<OS>",
105
                 completion_suggest=cli.OPT_COMPL_ONE_OS),
106
  cli.cli_option("--disk-size", dest="disk_size",
107
                 help="Disk size (determines disk count)",
108
                 default="128m", type="string", metavar="<size,size,...>",
109
                 completion_suggest=("128M 512M 1G 4G 1G,256M"
110
                                     " 4G,1G,1G 10G").split()),
111
  cli.cli_option("--disk-growth", dest="disk_growth", help="Disk growth",
112
                 default="128m", type="string", metavar="<size,size,...>"),
113
  cli.cli_option("--mem-size", dest="mem_size", help="Memory size",
114
                 default=128, type="unit", metavar="<size>",
115
                 completion_suggest=("128M 256M 512M 1G 4G 8G"
116
                                     " 12G 16G").split()),
117
  cli.cli_option("-v", "--verbose",
118
                 action="store_true", dest="verbose", default=False,
119
                 help="print command execution messages to stdout"),
120
  cli.cli_option("--no-replace1", dest="do_replace1",
121
                 help="Skip disk replacement with the same secondary",
122
                 action="store_false", default=True),
123
  cli.cli_option("--no-replace2", dest="do_replace2",
124
                 help="Skip disk replacement with a different secondary",
125
                 action="store_false", default=True),
126
  cli.cli_option("--no-failover", dest="do_failover",
127
                 help="Skip instance failovers", action="store_false",
128
                 default=True),
129
  cli.cli_option("--no-migrate", dest="do_migrate",
130
                 help="Skip instance live migration",
131
                 action="store_false", default=True),
132
  cli.cli_option("--no-move", dest="do_move",
133
                 help="Skip instance moves", action="store_false",
134
                 default=True),
135
  cli.cli_option("--no-importexport", dest="do_importexport",
136
                 help="Skip instance export/import", action="store_false",
137
                 default=True),
138
  cli.cli_option("--no-startstop", dest="do_startstop",
139
                 help="Skip instance stop/start", action="store_false",
140
                 default=True),
141
  cli.cli_option("--no-reinstall", dest="do_reinstall",
142
                 help="Skip instance reinstall", action="store_false",
143
                 default=True),
144
  cli.cli_option("--no-reboot", dest="do_reboot",
145
                 help="Skip instance reboot", action="store_false",
146
                 default=True),
147
  cli.cli_option("--no-activate-disks", dest="do_activate_disks",
148
                 help="Skip disk activation/deactivation",
149
                 action="store_false", default=True),
150
  cli.cli_option("--no-add-disks", dest="do_addremove_disks",
151
                 help="Skip disk addition/removal",
152
                 action="store_false", default=True),
153
  cli.cli_option("--no-add-nics", dest="do_addremove_nics",
154
                 help="Skip NIC addition/removal",
155
                 action="store_false", default=True),
156
  cli.cli_option("--no-nics", dest="nics",
157
                 help="No network interfaces", action="store_const",
158
                 const=[], default=[{}]),
159
  cli.cli_option("--rename", dest="rename", default=None,
160
                 help=("Give one unused instance name which is taken"
161
                       " to start the renaming sequence"),
162
                 metavar="<instance_name>"),
163
  cli.cli_option("-t", "--disk-template", dest="disk_template",
164
                 choices=list(constants.DISK_TEMPLATES),
165
                 default=constants.DT_DRBD8,
166
                 help="Disk template (diskless, file, plain or drbd) [drbd]"),
167
  cli.cli_option("-n", "--nodes", dest="nodes", default="",
168
                 help=("Comma separated list of nodes to perform"
169
                       " the burnin on (defaults to all nodes)"),
170
                 completion_suggest=cli.OPT_COMPL_MANY_NODES),
171
  cli.cli_option("-I", "--iallocator", dest="iallocator",
172
                 default=None, type="string",
173
                 help=("Perform the allocation using an iallocator"
174
                       " instead of fixed node spread (node restrictions no"
175
                       " longer apply, therefore -n/--nodes must not be"
176
                       " used"),
177
                 completion_suggest=cli.OPT_COMPL_ONE_IALLOCATOR),
178
  cli.cli_option("-p", "--parallel", default=False, action="store_true",
179
                 dest="parallel",
180
                 help=("Enable parallelization of some operations in"
181
                       " order to speed burnin or to test granular locking")),
182
  cli.cli_option("--net-timeout", default=15, type="int",
183
                 dest="net_timeout",
184
                 help=("The instance check network timeout in seconds"
185
                       " (defaults to 15 seconds)"),
186
                 completion_suggest="15 60 300 900".split()),
187
  cli.cli_option("-C", "--http-check", default=False, action="store_true",
188
                 dest="http_check",
189
                 help=("Enable checking of instance status via http,"
190
                       " looking for /hostname.txt that should contain the"
191
                       " name of the instance")),
192
  cli.cli_option("-K", "--keep-instances", default=False,
193
                 action="store_true",
194
                 dest="keep_instances",
195
                 help=("Leave instances on the cluster after burnin,"
196
                       " for investigation in case of errors or simply"
197
                       " to use them")),
198
  ]
199

    
200
# Mainly used for bash completion
201
ARGUMENTS = [cli.ArgInstance(min=1)]
202

    
203

    
204
class Burner(object):
205
  """Burner class."""
206

    
207
  def __init__(self):
208
    """Constructor."""
209
    utils.SetupLogging(constants.LOG_BURNIN, debug=False, stderr_logging=True)
210
    self.url_opener = SimpleOpener()
211
    self._feed_buf = StringIO()
212
    self.nodes = []
213
    self.instances = []
214
    self.to_rem = []
215
    self.queued_ops = []
216
    self.opts = None
217
    self.queue_retry = False
218
    self.disk_count = self.disk_growth = self.disk_size = None
219
    self.hvp = self.bep = None
220
    self.ParseOptions()
221
    self.cl = cli.GetClient()
222
    self.GetState()
223

    
224
  def ClearFeedbackBuf(self):
225
    """Clear the feedback buffer."""
226
    self._feed_buf.truncate(0)
227

    
228
  def GetFeedbackBuf(self):
229
    """Return the contents of the buffer."""
230
    return self._feed_buf.getvalue()
231

    
232
  def Feedback(self, msg):
233
    """Acumulate feedback in our buffer."""
234
    self._feed_buf.write("%s %s\n" % (time.ctime(utils.MergeTime(msg[0])),
235
                                      msg[2]))
236
    if self.opts.verbose:
237
      Log(msg, indent=3)
238

    
239
  def MaybeRetry(self, retry_count, msg, fn, *args):
240
    """Possibly retry a given function execution.
241

    
242
    @type retry_count: int
243
    @param retry_count: retry counter:
244
        - 0: non-retryable action
245
        - 1: last retry for a retryable action
246
        - MAX_RETRIES: original try for a retryable action
247
    @type msg: str
248
    @param msg: the kind of the operation
249
    @type fn: callable
250
    @param fn: the function to be called
251

    
252
    """
253
    try:
254
      val = fn(*args)
255
      if retry_count > 0 and retry_count < MAX_RETRIES:
256
        Log("Idempotent %s succeeded after %d retries" %
257
            (msg, MAX_RETRIES - retry_count))
258
      return val
259
    except Exception, err:
260
      if retry_count == 0:
261
        Log("Non-idempotent %s failed, aborting" % (msg, ))
262
        raise
263
      elif retry_count == 1:
264
        Log("Idempotent %s repeated failure, aborting" % (msg, ))
265
        raise
266
      else:
267
        Log("Idempotent %s failed, retry #%d/%d: %s" %
268
            (msg, MAX_RETRIES - retry_count + 1, MAX_RETRIES, err))
269
        self.MaybeRetry(retry_count - 1, msg, fn, *args)
270

    
271
  def _ExecOp(self, *ops):
272
    """Execute one or more opcodes and manage the exec buffer.
273

    
274
    @result: if only opcode has been passed, we return its result;
275
        otherwise we return the list of results
276

    
277
    """
278
    job_id = cli.SendJob(ops, cl=self.cl)
279
    results = cli.PollJob(job_id, cl=self.cl, feedback_fn=self.Feedback)
280
    if len(ops) == 1:
281
      return results[0]
282
    else:
283
      return results
284

    
285
  def ExecOp(self, retry, *ops):
286
    """Execute one or more opcodes and manage the exec buffer.
287

    
288
    @result: if only opcode has been passed, we return its result;
289
        otherwise we return the list of results
290

    
291
    """
292
    if retry:
293
      rval = MAX_RETRIES
294
    else:
295
      rval = 0
296
    return self.MaybeRetry(rval, "opcode", self._ExecOp, *ops)
297

    
298
  def ExecOrQueue(self, name, *ops):
299
    """Execute an opcode and manage the exec buffer."""
300
    if self.opts.parallel:
301
      self.queued_ops.append((ops, name))
302
    else:
303
      return self.ExecOp(self.queue_retry, *ops)
304

    
305
  def StartBatch(self, retry):
306
    """Start a new batch of jobs.
307

    
308
    @param retry: whether this is a retryable batch
309

    
310
    """
311
    self.queued_ops = []
312
    self.queue_retry = retry
313

    
314
  def CommitQueue(self):
315
    """Execute all submitted opcodes in case of parallel burnin"""
316
    if not self.opts.parallel:
317
      return
318

    
319
    if self.queue_retry:
320
      rval = MAX_RETRIES
321
    else:
322
      rval = 0
323

    
324
    try:
325
      results = self.MaybeRetry(rval, "jobset", self.ExecJobSet,
326
                                self.queued_ops)
327
    finally:
328
      self.queued_ops = []
329
    return results
330

    
331
  def ExecJobSet(self, jobs):
332
    """Execute a set of jobs and return once all are done.
333

    
334
    The method will return the list of results, if all jobs are
335
    successful. Otherwise, OpExecError will be raised from within
336
    cli.py.
337

    
338
    """
339
    self.ClearFeedbackBuf()
340
    job_ids = [cli.SendJob(row[0], cl=self.cl) for row in jobs]
341
    Log("Submitted job ID(s) %s" % ", ".join(job_ids), indent=1)
342
    results = []
343
    for jid, (_, iname) in zip(job_ids, jobs):
344
      Log("waiting for job %s for %s" % (jid, iname), indent=2)
345
      try:
346
        results.append(cli.PollJob(jid, cl=self.cl, feedback_fn=self.Feedback))
347
      except Exception, err:
348
        Log("Job for %s failed: %s" % (iname, err))
349
    if len(results) != len(jobs):
350
      raise BurninFailure()
351
    return results
352

    
353
  def _DoCheckInstances(fn):
354
    """Decorator for checking instances.
355

    
356
    """
357
    def wrapper(self, *args, **kwargs):
358
      val = fn(self, *args, **kwargs)
359
      for instance in self.instances:
360
        self._CheckInstanceAlive(instance)
361
      return val
362

    
363
    return wrapper
364

    
365
  def _DoBatch(retry):
366
    """Decorator for possible batch operations.
367

    
368
    Must come after the _DoCheckInstances decorator (if any).
369

    
370
    @param retry: whether this is a retryable batch, will be
371
        passed to StartBatch
372

    
373
    """
374
    def wrap(fn):
375
      def batched(self, *args, **kwargs):
376
        self.StartBatch(retry)
377
        val = fn(self, *args, **kwargs)
378
        self.CommitQueue()
379
        return val
380
      return batched
381

    
382
    return wrap
383

    
384
  def ParseOptions(self):
385
    """Parses the command line options.
386

    
387
    In case of command line errors, it will show the usage and exit the
388
    program.
389

    
390
    """
391
    parser = optparse.OptionParser(usage="\n%s" % USAGE,
392
                                   version=("%%prog (ganeti) %s" %
393
                                            constants.RELEASE_VERSION),
394
                                   option_list=OPTIONS)
395

    
396
    options, args = parser.parse_args()
397
    if len(args) < 1 or options.os is None:
398
      Usage()
399

    
400
    supported_disk_templates = (constants.DT_DISKLESS,
401
                                constants.DT_FILE,
402
                                constants.DT_PLAIN,
403
                                constants.DT_DRBD8)
404
    if options.disk_template not in supported_disk_templates:
405
      Err("Unknown disk template '%s'" % options.disk_template)
406

    
407
    if options.disk_template == constants.DT_DISKLESS:
408
      disk_size = disk_growth = []
409
      options.do_addremove_disks = False
410
    else:
411
      disk_size = [utils.ParseUnit(v) for v in options.disk_size.split(",")]
412
      disk_growth = [utils.ParseUnit(v)
413
                     for v in options.disk_growth.split(",")]
414
      if len(disk_growth) != len(disk_size):
415
        Err("Wrong disk sizes/growth combination")
416
    if ((disk_size and options.disk_template == constants.DT_DISKLESS) or
417
        (not disk_size and options.disk_template != constants.DT_DISKLESS)):
418
      Err("Wrong disk count/disk template combination")
419

    
420
    self.disk_size = disk_size
421
    self.disk_growth = disk_growth
422
    self.disk_count = len(disk_size)
423

    
424
    if options.nodes and options.iallocator:
425
      Err("Give either the nodes option or the iallocator option, not both")
426

    
427
    self.opts = options
428
    self.instances = args
429
    self.bep = {
430
      constants.BE_MEMORY: options.mem_size,
431
      constants.BE_VCPUS: 1,
432
      }
433
    self.hvp = {}
434

    
435
    socket.setdefaulttimeout(options.net_timeout)
436

    
437
  def GetState(self):
438
    """Read the cluster state from the config."""
439
    if self.opts.nodes:
440
      names = self.opts.nodes.split(",")
441
    else:
442
      names = []
443
    try:
444
      op = opcodes.OpQueryNodes(output_fields=["name", "offline", "drained"],
445
                                names=names, use_locking=True)
446
      result = self.ExecOp(True, op)
447
    except errors.GenericError, err:
448
      err_code, msg = cli.FormatError(err)
449
      Err(msg, exit_code=err_code)
450
    self.nodes = [data[0] for data in result if not (data[1] or data[2])]
451

    
452
    op_diagos = opcodes.OpDiagnoseOS(output_fields=["name", "valid"], names=[])
453
    result = self.ExecOp(True, op_diagos)
454

    
455
    if not result:
456
      Err("Can't get the OS list")
457

    
458
    # filter non-valid OS-es
459
    os_set = [val[0] for val in result if val[1]]
460

    
461
    if self.opts.os not in os_set:
462
      Err("OS '%s' not found" % self.opts.os)
463

    
464
  @_DoCheckInstances
465
  @_DoBatch(False)
466
  def BurnCreateInstances(self):
467
    """Create the given instances.
468

    
469
    """
470
    self.to_rem = []
471
    mytor = izip(cycle(self.nodes),
472
                 islice(cycle(self.nodes), 1, None),
473
                 self.instances)
474

    
475
    Log("Creating instances")
476
    for pnode, snode, instance in mytor:
477
      Log("instance %s" % instance, indent=1)
478
      if self.opts.iallocator:
479
        pnode = snode = None
480
        msg = "with iallocator %s" % self.opts.iallocator
481
      elif self.opts.disk_template not in constants.DTS_NET_MIRROR:
482
        snode = None
483
        msg = "on %s" % pnode
484
      else:
485
        msg = "on %s, %s" % (pnode, snode)
486

    
487
      Log(msg, indent=2)
488

    
489
      op = opcodes.OpCreateInstance(instance_name=instance,
490
                                    disks = [ {"size": size}
491
                                              for size in self.disk_size],
492
                                    disk_template=self.opts.disk_template,
493
                                    nics=self.opts.nics,
494
                                    mode=constants.INSTANCE_CREATE,
495
                                    os_type=self.opts.os,
496
                                    pnode=pnode,
497
                                    snode=snode,
498
                                    start=True,
499
                                    ip_check=True,
500
                                    wait_for_sync=True,
501
                                    file_driver="loop",
502
                                    file_storage_dir=None,
503
                                    iallocator=self.opts.iallocator,
504
                                    beparams=self.bep,
505
                                    hvparams=self.hvp,
506
                                    )
507

    
508
      self.ExecOrQueue(instance, op)
509
      self.to_rem.append(instance)
510

    
511
  @_DoBatch(False)
512
  def BurnGrowDisks(self):
513
    """Grow both the os and the swap disks by the requested amount, if any."""
514
    Log("Growing disks")
515
    for instance in self.instances:
516
      Log("instance %s" % instance, indent=1)
517
      for idx, growth in enumerate(self.disk_growth):
518
        if growth > 0:
519
          op = opcodes.OpGrowDisk(instance_name=instance, disk=idx,
520
                                  amount=growth, wait_for_sync=True)
521
          Log("increase disk/%s by %s MB" % (idx, growth), indent=2)
522
          self.ExecOrQueue(instance, op)
523

    
524
  @_DoBatch(True)
525
  def BurnReplaceDisks1D8(self):
526
    """Replace disks on primary and secondary for drbd8."""
527
    Log("Replacing disks on the same nodes")
528
    for instance in self.instances:
529
      Log("instance %s" % instance, indent=1)
530
      ops = []
531
      for mode in constants.REPLACE_DISK_SEC, constants.REPLACE_DISK_PRI:
532
        op = opcodes.OpReplaceDisks(instance_name=instance,
533
                                    mode=mode,
534
                                    disks=[i for i in range(self.disk_count)])
535
        Log("run %s" % mode, indent=2)
536
        ops.append(op)
537
      self.ExecOrQueue(instance, *ops)
538

    
539
  @_DoBatch(True)
540
  def BurnReplaceDisks2(self):
541
    """Replace secondary node."""
542
    Log("Changing the secondary node")
543
    mode = constants.REPLACE_DISK_CHG
544

    
545
    mytor = izip(islice(cycle(self.nodes), 2, None),
546
                 self.instances)
547
    for tnode, instance in mytor:
548
      Log("instance %s" % instance, indent=1)
549
      if self.opts.iallocator:
550
        tnode = None
551
        msg = "with iallocator %s" % self.opts.iallocator
552
      else:
553
        msg = tnode
554
      op = opcodes.OpReplaceDisks(instance_name=instance,
555
                                  mode=mode,
556
                                  remote_node=tnode,
557
                                  iallocator=self.opts.iallocator,
558
                                  disks=[])
559
      Log("run %s %s" % (mode, msg), indent=2)
560
      self.ExecOrQueue(instance, op)
561

    
562
  @_DoCheckInstances
563
  @_DoBatch(False)
564
  def BurnFailover(self):
565
    """Failover the instances."""
566
    Log("Failing over instances")
567
    for instance in self.instances:
568
      Log("instance %s" % instance, indent=1)
569
      op = opcodes.OpFailoverInstance(instance_name=instance,
570
                                      ignore_consistency=False)
571
      self.ExecOrQueue(instance, op)
572

    
573
  @_DoCheckInstances
574
  @_DoBatch(False)
575
  def BurnMove(self):
576
    """Move the instances."""
577
    Log("Moving instances")
578
    mytor = izip(islice(cycle(self.nodes), 1, None),
579
                 self.instances)
580
    for tnode, instance in mytor:
581
      Log("instance %s" % instance, indent=1)
582
      op = opcodes.OpMoveInstance(instance_name=instance,
583
                                  target_node=tnode)
584
      self.ExecOrQueue(instance, op)
585

    
586
  @_DoBatch(False)
587
  def BurnMigrate(self):
588
    """Migrate the instances."""
589
    Log("Migrating instances")
590
    for instance in self.instances:
591
      Log("instance %s" % instance, indent=1)
592
      op1 = opcodes.OpMigrateInstance(instance_name=instance, live=True,
593
                                      cleanup=False)
594

    
595
      op2 = opcodes.OpMigrateInstance(instance_name=instance, live=True,
596
                                      cleanup=True)
597
      Log("migration and migration cleanup", indent=2)
598
      self.ExecOrQueue(instance, op1, op2)
599

    
600
  @_DoCheckInstances
601
  @_DoBatch(False)
602
  def BurnImportExport(self):
603
    """Export the instance, delete it, and import it back.
604

    
605
    """
606
    Log("Exporting and re-importing instances")
607
    mytor = izip(cycle(self.nodes),
608
                 islice(cycle(self.nodes), 1, None),
609
                 islice(cycle(self.nodes), 2, None),
610
                 self.instances)
611

    
612
    for pnode, snode, enode, instance in mytor:
613
      Log("instance %s" % instance, indent=1)
614
      # read the full name of the instance
615
      nam_op = opcodes.OpQueryInstances(output_fields=["name"],
616
                                        names=[instance], use_locking=True)
617
      full_name = self.ExecOp(False, nam_op)[0][0]
618

    
619
      if self.opts.iallocator:
620
        pnode = snode = None
621
        import_log_msg = ("import from %s"
622
                          " with iallocator %s" %
623
                          (enode, self.opts.iallocator))
624
      elif self.opts.disk_template not in constants.DTS_NET_MIRROR:
625
        snode = None
626
        import_log_msg = ("import from %s to %s" %
627
                          (enode, pnode))
628
      else:
629
        import_log_msg = ("import from %s to %s, %s" %
630
                          (enode, pnode, snode))
631

    
632
      exp_op = opcodes.OpExportInstance(instance_name=instance,
633
                                           target_node=enode,
634
                                           shutdown=True)
635
      rem_op = opcodes.OpRemoveInstance(instance_name=instance,
636
                                        ignore_failures=True)
637
      imp_dir = os.path.join(constants.EXPORT_DIR, full_name)
638
      imp_op = opcodes.OpCreateInstance(instance_name=instance,
639
                                        disks = [ {"size": size}
640
                                                  for size in self.disk_size],
641
                                        disk_template=self.opts.disk_template,
642
                                        nics=self.opts.nics,
643
                                        mode=constants.INSTANCE_IMPORT,
644
                                        src_node=enode,
645
                                        src_path=imp_dir,
646
                                        pnode=pnode,
647
                                        snode=snode,
648
                                        start=True,
649
                                        ip_check=True,
650
                                        wait_for_sync=True,
651
                                        file_storage_dir=None,
652
                                        file_driver="loop",
653
                                        iallocator=self.opts.iallocator,
654
                                        beparams=self.bep,
655
                                        hvparams=self.hvp,
656
                                        )
657

    
658
      erem_op = opcodes.OpRemoveExport(instance_name=instance)
659

    
660
      Log("export to node %s" % enode, indent=2)
661
      Log("remove instance", indent=2)
662
      Log(import_log_msg, indent=2)
663
      Log("remove export", indent=2)
664
      self.ExecOrQueue(instance, exp_op, rem_op, imp_op, erem_op)
665

    
666
  def StopInstanceOp(self, instance):
667
    """Stop given instance."""
668
    return opcodes.OpShutdownInstance(instance_name=instance)
669

    
670
  def StartInstanceOp(self, instance):
671
    """Start given instance."""
672
    return opcodes.OpStartupInstance(instance_name=instance, force=False)
673

    
674
  def RenameInstanceOp(self, instance, instance_new):
675
    """Rename instance."""
676
    return opcodes.OpRenameInstance(instance_name=instance,
677
                                    new_name=instance_new)
678

    
679
  @_DoCheckInstances
680
  @_DoBatch(True)
681
  def BurnStopStart(self):
682
    """Stop/start the instances."""
683
    Log("Stopping and starting instances")
684
    for instance in self.instances:
685
      Log("instance %s" % instance, indent=1)
686
      op1 = self.StopInstanceOp(instance)
687
      op2 = self.StartInstanceOp(instance)
688
      self.ExecOrQueue(instance, op1, op2)
689

    
690
  @_DoBatch(False)
691
  def BurnRemove(self):
692
    """Remove the instances."""
693
    Log("Removing instances")
694
    for instance in self.to_rem:
695
      Log("instance %s" % instance, indent=1)
696
      op = opcodes.OpRemoveInstance(instance_name=instance,
697
                                    ignore_failures=True)
698
      self.ExecOrQueue(instance, op)
699

    
700
  def BurnRename(self):
701
    """Rename the instances.
702

    
703
    Note that this function will not execute in parallel, since we
704
    only have one target for rename.
705

    
706
    """
707
    Log("Renaming instances")
708
    rename = self.opts.rename
709
    for instance in self.instances:
710
      Log("instance %s" % instance, indent=1)
711
      op_stop1 = self.StopInstanceOp(instance)
712
      op_stop2 = self.StopInstanceOp(rename)
713
      op_rename1 = self.RenameInstanceOp(instance, rename)
714
      op_rename2 = self.RenameInstanceOp(rename, instance)
715
      op_start1 = self.StartInstanceOp(rename)
716
      op_start2 = self.StartInstanceOp(instance)
717
      self.ExecOp(False, op_stop1, op_rename1, op_start1)
718
      self._CheckInstanceAlive(rename)
719
      self.ExecOp(False, op_stop2, op_rename2, op_start2)
720
      self._CheckInstanceAlive(instance)
721

    
722
  @_DoCheckInstances
723
  @_DoBatch(True)
724
  def BurnReinstall(self):
725
    """Reinstall the instances."""
726
    Log("Reinstalling instances")
727
    for instance in self.instances:
728
      Log("instance %s" % instance, indent=1)
729
      op1 = self.StopInstanceOp(instance)
730
      op2 = opcodes.OpReinstallInstance(instance_name=instance)
731
      Log("reinstall without passing the OS", indent=2)
732
      op3 = opcodes.OpReinstallInstance(instance_name=instance,
733
                                        os_type=self.opts.os)
734
      Log("reinstall specifying the OS", indent=2)
735
      op4 = self.StartInstanceOp(instance)
736
      self.ExecOrQueue(instance, op1, op2, op3, op4)
737

    
738
  @_DoCheckInstances
739
  @_DoBatch(True)
740
  def BurnReboot(self):
741
    """Reboot the instances."""
742
    Log("Rebooting instances")
743
    for instance in self.instances:
744
      Log("instance %s" % instance, indent=1)
745
      ops = []
746
      for reboot_type in constants.REBOOT_TYPES:
747
        op = opcodes.OpRebootInstance(instance_name=instance,
748
                                      reboot_type=reboot_type,
749
                                      ignore_secondaries=False)
750
        Log("reboot with type '%s'" % reboot_type, indent=2)
751
        ops.append(op)
752
      self.ExecOrQueue(instance, *ops)
753

    
754
  @_DoCheckInstances
755
  @_DoBatch(True)
756
  def BurnActivateDisks(self):
757
    """Activate and deactivate disks of the instances."""
758
    Log("Activating/deactivating disks")
759
    for instance in self.instances:
760
      Log("instance %s" % instance, indent=1)
761
      op_start = self.StartInstanceOp(instance)
762
      op_act = opcodes.OpActivateInstanceDisks(instance_name=instance)
763
      op_deact = opcodes.OpDeactivateInstanceDisks(instance_name=instance)
764
      op_stop = self.StopInstanceOp(instance)
765
      Log("activate disks when online", indent=2)
766
      Log("activate disks when offline", indent=2)
767
      Log("deactivate disks (when offline)", indent=2)
768
      self.ExecOrQueue(instance, op_act, op_stop, op_act, op_deact, op_start)
769

    
770
  @_DoCheckInstances
771
  @_DoBatch(False)
772
  def BurnAddRemoveDisks(self):
773
    """Add and remove an extra disk for the instances."""
774
    Log("Adding and removing disks")
775
    for instance in self.instances:
776
      Log("instance %s" % instance, indent=1)
777
      op_add = opcodes.OpSetInstanceParams(\
778
        instance_name=instance,
779
        disks=[(constants.DDM_ADD, {"size": self.disk_size[0]})])
780
      op_rem = opcodes.OpSetInstanceParams(\
781
        instance_name=instance, disks=[(constants.DDM_REMOVE, {})])
782
      op_stop = self.StopInstanceOp(instance)
783
      op_start = self.StartInstanceOp(instance)
784
      Log("adding a disk", indent=2)
785
      Log("removing last disk", indent=2)
786
      self.ExecOrQueue(instance, op_add, op_stop, op_rem, op_start)
787

    
788
  @_DoBatch(False)
789
  def BurnAddRemoveNICs(self):
790
    """Add and remove an extra NIC for the instances."""
791
    Log("Adding and removing NICs")
792
    for instance in self.instances:
793
      Log("instance %s" % instance, indent=1)
794
      op_add = opcodes.OpSetInstanceParams(\
795
        instance_name=instance, nics=[(constants.DDM_ADD, {})])
796
      op_rem = opcodes.OpSetInstanceParams(\
797
        instance_name=instance, nics=[(constants.DDM_REMOVE, {})])
798
      Log("adding a NIC", indent=2)
799
      Log("removing last NIC", indent=2)
800
      self.ExecOrQueue(instance, op_add, op_rem)
801

    
802
  def _CheckInstanceAlive(self, instance):
803
    """Check if an instance is alive by doing http checks.
804

    
805
    This will try to retrieve the url on the instance /hostname.txt
806
    and check that it contains the hostname of the instance. In case
807
    we get ECONNREFUSED, we retry up to the net timeout seconds, for
808
    any other error we abort.
809

    
810
    """
811
    if not self.opts.http_check:
812
      return
813
    end_time = time.time() + self.opts.net_timeout
814
    url = None
815
    while time.time() < end_time and url is None:
816
      try:
817
        url = self.url_opener.open("http://%s/hostname.txt" % instance)
818
      except IOError:
819
        # here we can have connection refused, no route to host, etc.
820
        time.sleep(1)
821
    if url is None:
822
      raise InstanceDown(instance, "Cannot contact instance")
823
    hostname = url.read().strip()
824
    url.close()
825
    if hostname != instance:
826
      raise InstanceDown(instance, ("Hostname mismatch, expected %s, got %s" %
827
                                    (instance, hostname)))
828

    
829
  def BurninCluster(self):
830
    """Test a cluster intensively.
831

    
832
    This will create instances and then start/stop/failover them.
833
    It is safe for existing instances but could impact performance.
834

    
835
    """
836

    
837
    opts = self.opts
838

    
839
    Log("Testing global parameters")
840

    
841
    if (len(self.nodes) == 1 and
842
        opts.disk_template not in (constants.DT_DISKLESS, constants.DT_PLAIN,
843
                                   constants.DT_FILE)):
844
      Err("When one node is available/selected the disk template must"
845
          " be 'diskless', 'file' or 'plain'")
846

    
847
    has_err = True
848
    try:
849
      self.BurnCreateInstances()
850
      if opts.do_replace1 and opts.disk_template in constants.DTS_NET_MIRROR:
851
        self.BurnReplaceDisks1D8()
852
      if (opts.do_replace2 and len(self.nodes) > 2 and
853
          opts.disk_template in constants.DTS_NET_MIRROR) :
854
        self.BurnReplaceDisks2()
855

    
856
      if (opts.disk_template != constants.DT_DISKLESS and
857
          utils.any(self.disk_growth, lambda n: n > 0)):
858
        self.BurnGrowDisks()
859

    
860
      if opts.do_failover and opts.disk_template in constants.DTS_NET_MIRROR:
861
        self.BurnFailover()
862

    
863
      if opts.do_migrate and opts.disk_template == constants.DT_DRBD8:
864
        self.BurnMigrate()
865

    
866
      if opts.do_move and opts.disk_template in [constants.DT_PLAIN,
867
                                                 constants.DT_FILE]:
868
        self.BurnMove()
869

    
870
      if (opts.do_importexport and
871
          opts.disk_template not in (constants.DT_DISKLESS,
872
                                     constants.DT_FILE)):
873
        self.BurnImportExport()
874

    
875
      if opts.do_reinstall:
876
        self.BurnReinstall()
877

    
878
      if opts.do_reboot:
879
        self.BurnReboot()
880

    
881
      if opts.do_addremove_disks:
882
        self.BurnAddRemoveDisks()
883

    
884
      if opts.do_addremove_nics:
885
        self.BurnAddRemoveNICs()
886

    
887
      if opts.do_activate_disks:
888
        self.BurnActivateDisks()
889

    
890
      if opts.rename:
891
        self.BurnRename()
892

    
893
      if opts.do_startstop:
894
        self.BurnStopStart()
895

    
896
      has_err = False
897
    finally:
898
      if has_err:
899
        Log("Error detected: opcode buffer follows:\n\n")
900
        Log(self.GetFeedbackBuf())
901
        Log("\n\n")
902
      if not self.opts.keep_instances:
903
        try:
904
          self.BurnRemove()
905
        except Exception, err:
906
          if has_err: # already detected errors, so errors in removal
907
                      # are quite expected
908
            Log("Note: error detected during instance remove: %s" % str(err))
909
          else: # non-expected error
910
            raise
911

    
912
    return 0
913

    
914

    
915
def main():
916
  """Main function"""
917

    
918
  burner = Burner()
919
  return burner.BurninCluster()
920

    
921

    
922
if __name__ == "__main__":
923
  main()