burnin: add option to not remove instances
[ganeti-local] / tools / burnin
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 urllib2
32 import errno
33 from itertools import izip, islice, cycle
34 from cStringIO import StringIO
35
36 from ganeti import opcodes
37 from ganeti import mcpu
38 from ganeti import constants
39 from ganeti import cli
40 from ganeti import errors
41 from ganeti import utils
42
43
44 USAGE = ("\tburnin -o OS_NAME [options...] instance_name ...")
45
46
47 class InstanceDown(Exception):
48   """The checked instance was not up"""
49
50
51 def Usage():
52   """Shows program usage information and exits the program."""
53
54   print >> sys.stderr, "Usage:"
55   print >> sys.stderr, USAGE
56   sys.exit(2)
57
58
59 def Log(msg, indent=0):
60   """Simple function that prints out its argument.
61
62   """
63   headers = {
64     0: "- ",
65     1: "* ",
66     2: ""
67     }
68   sys.stdout.write("%*s%s%s\n" % (2*indent, "",
69                                    headers.get(indent, "  "), msg))
70   sys.stdout.flush()
71
72 def Err(msg, exit_code=1):
73   """Simple error logging that prints to stderr.
74
75   """
76   sys.stderr.write(msg + "\n")
77   sys.stderr.flush()
78   sys.exit(exit_code)
79
80 class Burner(object):
81   """Burner class."""
82
83   def __init__(self):
84     """Constructor."""
85     utils.SetupLogging(constants.LOG_BURNIN, debug=False, stderr_logging=True)
86     self._feed_buf = StringIO()
87     self.nodes = []
88     self.instances = []
89     self.to_rem = []
90     self.opts = None
91     self.ParseOptions()
92     self.cl = cli.GetClient()
93     self.GetState()
94
95   def ClearFeedbackBuf(self):
96     """Clear the feedback buffer."""
97     self._feed_buf.truncate(0)
98
99   def GetFeedbackBuf(self):
100     """Return the contents of the buffer."""
101     return self._feed_buf.getvalue()
102
103   def Feedback(self, msg):
104     """Acumulate feedback in our buffer."""
105     self._feed_buf.write("%s %s\n" % (time.ctime(utils.MergeTime(msg[0])),
106                                       msg[2]))
107     if self.opts.verbose:
108       Log(msg, indent=3)
109
110   def ExecOp(self, op):
111     """Execute an opcode and manage the exec buffer."""
112     self.ClearFeedbackBuf()
113     return cli.SubmitOpCode(op, feedback_fn=self.Feedback, cl=self.cl)
114
115   def ExecJobSet(self, jobs):
116     """Execute a set of jobs and return once all are done.
117
118     The method will return the list of results, if all jobs are
119     successfull. Otherwise, OpExecError will be raised from within
120     cli.py.
121
122     """
123     self.ClearFeedbackBuf()
124     job_ids = [cli.SendJob(job, cl=self.cl) for job in jobs]
125     Log("Submitted job IDs %s" % ", ".join(job_ids), indent=1)
126     results = []
127     for jid in job_ids:
128       Log("Waiting for job %s" % jid, indent=2)
129       results.append(cli.PollJob(jid, cl=self.cl, feedback_fn=self.Feedback))
130
131     return results
132
133   def ParseOptions(self):
134     """Parses the command line options.
135
136     In case of command line errors, it will show the usage and exit the
137     program.
138
139     """
140
141     parser = optparse.OptionParser(usage="\n%s" % USAGE,
142                                    version="%%prog (ganeti) %s" %
143                                    constants.RELEASE_VERSION,
144                                    option_class=cli.CliOption)
145
146     parser.add_option("-o", "--os", dest="os", default=None,
147                       help="OS to use during burnin",
148                       metavar="<OS>")
149     parser.add_option("--disk-size", dest="disk_size",
150                       help="Disk size (determines disk count)",
151                       default="128m", type="string", metavar="<size,size,...>")
152     parser.add_option("--disk-growth", dest="disk_growth", help="Disk growth",
153                       default="128m", type="string", metavar="<size,size,...>")
154     parser.add_option("--mem-size", dest="mem_size", help="Memory size",
155                       default=128, type="unit", metavar="<size>")
156     parser.add_option("-v", "--verbose",
157                       action="store_true", dest="verbose", default=False,
158                       help="print command execution messages to stdout")
159     parser.add_option("--no-replace1", dest="do_replace1",
160                       help="Skip disk replacement with the same secondary",
161                       action="store_false", default=True)
162     parser.add_option("--no-replace2", dest="do_replace2",
163                       help="Skip disk replacement with a different secondary",
164                       action="store_false", default=True)
165     parser.add_option("--no-failover", dest="do_failover",
166                       help="Skip instance failovers", action="store_false",
167                       default=True)
168     parser.add_option("--no-migrate", dest="do_migrate",
169                       help="Skip instance live migration",
170                       action="store_false", default=True)
171     parser.add_option("--no-importexport", dest="do_importexport",
172                       help="Skip instance export/import", action="store_false",
173                       default=True)
174     parser.add_option("--no-startstop", dest="do_startstop",
175                       help="Skip instance stop/start", action="store_false",
176                       default=True)
177     parser.add_option("--no-reinstall", dest="do_reinstall",
178                       help="Skip instance reinstall", action="store_false",
179                       default=True)
180     parser.add_option("--no-reboot", dest="do_reboot",
181                       help="Skip instance reboot", action="store_false",
182                       default=True)
183     parser.add_option("--no-activate-disks", dest="do_activate_disks",
184                       help="Skip disk activation/deactivation",
185                       action="store_false", default=True)
186     parser.add_option("--no-add-disks", dest="do_addremove_disks",
187                       help="Skip disk addition/removal",
188                       action="store_false", default=True)
189     parser.add_option("--no-add-nics", dest="do_addremove_nics",
190                       help="Skip NIC addition/removal",
191                       action="store_false", default=True)
192     parser.add_option("--no-nics", dest="nics",
193                       help="No network interfaces", action="store_const",
194                       const=[], default=[{}])
195     parser.add_option("--rename", dest="rename", default=None,
196                       help="Give one unused instance name which is taken"
197                            " to start the renaming sequence",
198                       metavar="<instance_name>")
199     parser.add_option("-t", "--disk-template", dest="disk_template",
200                       choices=("diskless", "file", "plain", "drbd"),
201                       default="drbd",
202                       help="Disk template (diskless, file, plain or drbd)"
203                             " [drbd]")
204     parser.add_option("-n", "--nodes", dest="nodes", default="",
205                       help="Comma separated list of nodes to perform"
206                       " the burnin on (defaults to all nodes)")
207     parser.add_option("--iallocator", dest="iallocator",
208                       default=None, type="string",
209                       help="Perform the allocation using an iallocator"
210                       " instead of fixed node spread (node restrictions no"
211                       " longer apply, therefore -n/--nodes must not be used")
212     parser.add_option("-p", "--parallel", default=False, action="store_true",
213                       dest="parallel",
214                       help="Enable parallelization of some operations in"
215                       " order to speed burnin or to test granular locking")
216     parser.add_option("--net-timeout", default=15, type="int",
217                       dest="net_timeout",
218                       help="The instance check network timeout in seconds"
219                       " (defaults to 15 seconds)")
220     parser.add_option("-C", "--http-check", default=False, action="store_true",
221                       dest="http_check",
222                       help="Enable checking of instance status via http,"
223                       " looking for /hostname.txt that should contain the"
224                       " name of the instance")
225     parser.add_option("-K", "--keep-instances", default=False,
226                       action="store_true",
227                       dest="keep_instances",
228                       help="Leave instances on the cluster after burnin,"
229                       " for investigation in case of errors or simply"
230                       " to use them")
231
232
233     options, args = parser.parse_args()
234     if len(args) < 1 or options.os is None:
235       Usage()
236
237     supported_disk_templates = (constants.DT_DISKLESS,
238                                 constants.DT_FILE,
239                                 constants.DT_PLAIN,
240                                 constants.DT_DRBD8)
241     if options.disk_template not in supported_disk_templates:
242       Err("Unknown disk template '%s'" % options.disk_template)
243
244     if options.disk_template == constants.DT_DISKLESS:
245       disk_size = disk_growth = []
246       options.do_addremove_disks = False
247     else:
248       disk_size = [utils.ParseUnit(v) for v in options.disk_size.split(",")]
249       disk_growth = [utils.ParseUnit(v)
250                      for v in options.disk_growth.split(",")]
251       if len(disk_growth) != len(disk_size):
252         Err("Wrong disk sizes/growth combination")
253     if ((disk_size and options.disk_template == constants.DT_DISKLESS) or
254         (not disk_size and options.disk_template != constants.DT_DISKLESS)):
255       Err("Wrong disk count/disk template combination")
256
257     self.disk_size = disk_size
258     self.disk_growth = disk_growth
259     self.disk_count = len(disk_size)
260
261     if options.nodes and options.iallocator:
262       Err("Give either the nodes option or the iallocator option, not both")
263
264     self.opts = options
265     self.instances = args
266     self.bep = {
267       constants.BE_MEMORY: options.mem_size,
268       constants.BE_VCPUS: 1,
269       }
270     self.hvp = {}
271
272     socket.setdefaulttimeout(options.net_timeout)
273
274   def GetState(self):
275     """Read the cluster state from the config."""
276     if self.opts.nodes:
277       names = self.opts.nodes.split(",")
278     else:
279       names = []
280     try:
281       op = opcodes.OpQueryNodes(output_fields=["name", "offline"], names=names)
282       result = self.ExecOp(op)
283     except errors.GenericError, err:
284       err_code, msg = cli.FormatError(err)
285       Err(msg, exit_code=err_code)
286     self.nodes = [data[0] for data in result if not data[1]]
287
288     result = self.ExecOp(opcodes.OpDiagnoseOS(output_fields=["name", "valid"],
289                                               names=[]))
290
291     if not result:
292       Err("Can't get the OS list")
293
294     # filter non-valid OS-es
295     os_set = [val[0] for val in result if val[1]]
296
297     if self.opts.os not in os_set:
298       Err("OS '%s' not found" % self.opts.os)
299
300   def CreateInstances(self):
301     """Create the given instances.
302
303     """
304     self.to_rem = []
305     mytor = izip(cycle(self.nodes),
306                  islice(cycle(self.nodes), 1, None),
307                  self.instances)
308     jobset = []
309
310     Log("Creating instances")
311     for pnode, snode, instance in mytor:
312       Log("instance %s" % instance, indent=1)
313       if self.opts.iallocator:
314         pnode = snode = None
315         msg = "with iallocator %s" % self.opts.iallocator
316       elif self.opts.disk_template not in constants.DTS_NET_MIRROR:
317         snode = None
318         msg = "on %s" % pnode
319       else:
320         msg = "on %s, %s" % (pnode, snode)
321
322       Log(msg, indent=2)
323
324       op = opcodes.OpCreateInstance(instance_name=instance,
325                                     disks = [ {"size": size}
326                                               for size in self.disk_size],
327                                     disk_template=self.opts.disk_template,
328                                     nics=self.opts.nics,
329                                     mode=constants.INSTANCE_CREATE,
330                                     os_type=self.opts.os,
331                                     pnode=pnode,
332                                     snode=snode,
333                                     start=True,
334                                     ip_check=True,
335                                     wait_for_sync=True,
336                                     file_driver="loop",
337                                     file_storage_dir=None,
338                                     iallocator=self.opts.iallocator,
339                                     beparams=self.bep,
340                                     hvparams=self.hvp,
341                                     )
342
343       if self.opts.parallel:
344         jobset.append([op])
345         # FIXME: here we should not append to to_rem uncoditionally,
346         # but only when the job is successful
347         self.to_rem.append(instance)
348       else:
349         self.ExecOp(op)
350         self.to_rem.append(instance)
351     if self.opts.parallel:
352       self.ExecJobSet(jobset)
353
354     for instance in self.instances:
355       self._CheckInstanceAlive(instance)
356
357   def GrowDisks(self):
358     """Grow both the os and the swap disks by the requested amount, if any."""
359     Log("Growing disks")
360     for instance in self.instances:
361       Log("instance %s" % instance, indent=1)
362       for idx, growth in enumerate(self.disk_growth):
363         if growth > 0:
364           op = opcodes.OpGrowDisk(instance_name=instance, disk=idx,
365                                   amount=growth, wait_for_sync=True)
366           Log("increase disk/%s by %s MB" % (idx, growth), indent=2)
367           self.ExecOp(op)
368
369   def ReplaceDisks1D8(self):
370     """Replace disks on primary and secondary for drbd8."""
371     Log("Replacing disks on the same nodes")
372     for instance in self.instances:
373       Log("instance %s" % instance, indent=1)
374       for mode in constants.REPLACE_DISK_SEC, constants.REPLACE_DISK_PRI:
375         op = opcodes.OpReplaceDisks(instance_name=instance,
376                                     mode=mode,
377                                     disks=[i for i in range(self.disk_count)])
378         Log("run %s" % mode, indent=2)
379         self.ExecOp(op)
380
381   def ReplaceDisks2(self):
382     """Replace secondary node."""
383     Log("Changing the secondary node")
384     mode = constants.REPLACE_DISK_CHG
385
386     mytor = izip(islice(cycle(self.nodes), 2, None),
387                  self.instances)
388     for tnode, instance in mytor:
389       Log("instance %s" % instance, indent=1)
390       if self.opts.iallocator:
391         tnode = None
392         msg = "with iallocator %s" % self.opts.iallocator
393       else:
394         msg = tnode
395       op = opcodes.OpReplaceDisks(instance_name=instance,
396                                   mode=mode,
397                                   remote_node=tnode,
398                                   iallocator=self.opts.iallocator,
399                                   disks=[i for i in range(self.disk_count)])
400       Log("run %s %s" % (mode, msg), indent=2)
401       self.ExecOp(op)
402
403   def Failover(self):
404     """Failover the instances."""
405     Log("Failing over instances")
406     for instance in self.instances:
407       Log("instance %s" % instance, indent=1)
408       op = opcodes.OpFailoverInstance(instance_name=instance,
409                                       ignore_consistency=False)
410
411       self.ExecOp(op)
412     for instance in self.instances:
413       self._CheckInstanceAlive(instance)
414
415   def Migrate(self):
416     """Migrate the instances."""
417     Log("Migrating instances")
418     for instance in self.instances:
419       Log("instance %s" % instance, indent=1)
420       op = opcodes.OpMigrateInstance(instance_name=instance, live=True,
421                                      cleanup=False)
422
423       Log("migration", indent=2)
424       self.ExecOp(op)
425       op = opcodes.OpMigrateInstance(instance_name=instance, live=True,
426                                      cleanup=True)
427       Log("migration cleanup", indent=2)
428       self.ExecOp(op)
429
430   def ImportExport(self):
431     """Export the instance, delete it, and import it back.
432
433     """
434     Log("Exporting and re-importing instances")
435     mytor = izip(cycle(self.nodes),
436                  islice(cycle(self.nodes), 1, None),
437                  islice(cycle(self.nodes), 2, None),
438                  self.instances)
439
440     for pnode, snode, enode, instance in mytor:
441       Log("instance %s" % instance, indent=1)
442       if self.opts.iallocator:
443         pnode = snode = None
444         import_log_msg = ("import from %s"
445                           " with iallocator %s" %
446                           (enode, self.opts.iallocator))
447       elif self.opts.disk_template not in constants.DTS_NET_MIRROR:
448         snode = None
449         import_log_msg = ("import from %s to %s" %
450                           (enode, pnode))
451       else:
452         import_log_msg = ("import from %s to %s, %s" %
453                           (enode, pnode, snode))
454
455       exp_op = opcodes.OpExportInstance(instance_name=instance,
456                                            target_node=enode,
457                                            shutdown=True)
458       rem_op = opcodes.OpRemoveInstance(instance_name=instance,
459                                         ignore_failures=True)
460       nam_op = opcodes.OpQueryInstances(output_fields=["name"],
461                                            names=[instance])
462       full_name = self.ExecOp(nam_op)[0][0]
463       imp_dir = os.path.join(constants.EXPORT_DIR, full_name)
464       imp_op = opcodes.OpCreateInstance(instance_name=instance,
465                                         disks = [ {"size": size}
466                                                   for size in self.disk_size],
467                                         disk_template=self.opts.disk_template,
468                                         nics=self.opts.nics,
469                                         mode=constants.INSTANCE_IMPORT,
470                                         src_node=enode,
471                                         src_path=imp_dir,
472                                         pnode=pnode,
473                                         snode=snode,
474                                         start=True,
475                                         ip_check=True,
476                                         wait_for_sync=True,
477                                         file_storage_dir=None,
478                                         file_driver="loop",
479                                         iallocator=self.opts.iallocator,
480                                         beparams=self.bep,
481                                         hvparams=self.hvp,
482                                         )
483
484       erem_op = opcodes.OpRemoveExport(instance_name=instance)
485
486       Log("export to node %s" % enode, indent=2)
487       self.ExecOp(exp_op)
488       Log("remove instance", indent=2)
489       self.ExecOp(rem_op)
490       self.to_rem.remove(instance)
491       Log(import_log_msg, indent=2)
492       self.ExecOp(imp_op)
493       Log("remove export", indent=2)
494       self.ExecOp(erem_op)
495
496       self.to_rem.append(instance)
497
498     for instance in self.instances:
499       self._CheckInstanceAlive(instance)
500
501   def StopInstance(self, instance):
502     """Stop given instance."""
503     op = opcodes.OpShutdownInstance(instance_name=instance)
504     Log("shutdown", indent=2)
505     self.ExecOp(op)
506
507   def StartInstance(self, instance):
508     """Start given instance."""
509     op = opcodes.OpStartupInstance(instance_name=instance, force=False)
510     Log("startup", indent=2)
511     self.ExecOp(op)
512
513   def RenameInstance(self, instance, instance_new):
514     """Rename instance."""
515     op = opcodes.OpRenameInstance(instance_name=instance,
516                                   new_name=instance_new)
517     Log("rename to %s" % instance_new, indent=2)
518     self.ExecOp(op)
519
520   def StopStart(self):
521     """Stop/start the instances."""
522     Log("Stopping and starting instances")
523     for instance in self.instances:
524       Log("instance %s" % instance, indent=1)
525       self.StopInstance(instance)
526       self.StartInstance(instance)
527
528     for instance in self.instances:
529       self._CheckInstanceAlive(instance)
530
531   def Remove(self):
532     """Remove the instances."""
533     Log("Removing instances")
534     for instance in self.to_rem:
535       Log("instance %s" % instance, indent=1)
536       op = opcodes.OpRemoveInstance(instance_name=instance,
537                                     ignore_failures=True)
538       self.ExecOp(op)
539
540   def Rename(self):
541     """Rename the instances."""
542     Log("Renaming instances")
543     rename = self.opts.rename
544     for instance in self.instances:
545       Log("instance %s" % instance, indent=1)
546       self.StopInstance(instance)
547       self.RenameInstance(instance, rename)
548       self.StartInstance(rename)
549       self._CheckInstanceAlive(rename)
550       self.StopInstance(rename)
551       self.RenameInstance(rename, instance)
552       self.StartInstance(instance)
553
554     for instance in self.instances:
555       self._CheckInstanceAlive(instance)
556
557   def Reinstall(self):
558     """Reinstall the instances."""
559     Log("Reinstalling instances")
560     for instance in self.instances:
561       Log("instance %s" % instance, indent=1)
562       self.StopInstance(instance)
563       op = opcodes.OpReinstallInstance(instance_name=instance)
564       Log("reinstall without passing the OS", indent=2)
565       self.ExecOp(op)
566       op = opcodes.OpReinstallInstance(instance_name=instance,
567                                        os_type=self.opts.os)
568       Log("reinstall specifying the OS", indent=2)
569       self.ExecOp(op)
570       self.StartInstance(instance)
571     for instance in self.instances:
572       self._CheckInstanceAlive(instance)
573
574   def Reboot(self):
575     """Reboot the instances."""
576     Log("Rebooting instances")
577     for instance in self.instances:
578       Log("instance %s" % instance, indent=1)
579       for reboot_type in constants.REBOOT_TYPES:
580         op = opcodes.OpRebootInstance(instance_name=instance,
581                                       reboot_type=reboot_type,
582                                       ignore_secondaries=False)
583         Log("reboot with type '%s'" % reboot_type, indent=2)
584         self.ExecOp(op)
585         self._CheckInstanceAlive(instance)
586
587   def ActivateDisks(self):
588     """Activate and deactivate disks of the instances."""
589     Log("Activating/deactivating disks")
590     for instance in self.instances:
591       Log("instance %s" % instance, indent=1)
592       op_act = opcodes.OpActivateInstanceDisks(instance_name=instance)
593       op_deact = opcodes.OpDeactivateInstanceDisks(instance_name=instance)
594       Log("activate disks when online", indent=2)
595       self.ExecOp(op_act)
596       self.StopInstance(instance)
597       Log("activate disks when offline", indent=2)
598       self.ExecOp(op_act)
599       Log("deactivate disks (when offline)", indent=2)
600       self.ExecOp(op_deact)
601       self.StartInstance(instance)
602     for instance in self.instances:
603       self._CheckInstanceAlive(instance)
604
605   def AddRemoveDisks(self):
606     """Add and remove an extra disk for the instances."""
607     Log("Adding and removing disks")
608     for instance in self.instances:
609       Log("instance %s" % instance, indent=1)
610       op_add = opcodes.OpSetInstanceParams(\
611         instance_name=instance,
612         disks=[(constants.DDM_ADD, {"size": self.disk_size[0]})])
613       op_rem = opcodes.OpSetInstanceParams(\
614         instance_name=instance, disks=[(constants.DDM_REMOVE, {})])
615       Log("adding a disk", indent=2)
616       self.ExecOp(op_add)
617       self.StopInstance(instance)
618       Log("removing last disk", indent=2)
619       self.ExecOp(op_rem)
620       self.StartInstance(instance)
621     for instance in self.instances:
622       self._CheckInstanceAlive(instance)
623
624   def AddRemoveNICs(self):
625     """Add and remove an extra NIC for the instances."""
626     Log("Adding and removing NICs")
627     for instance in self.instances:
628       Log("instance %s" % instance, indent=1)
629       op_add = opcodes.OpSetInstanceParams(\
630         instance_name=instance, nics=[(constants.DDM_ADD, {})])
631       op_rem = opcodes.OpSetInstanceParams(\
632         instance_name=instance, nics=[(constants.DDM_REMOVE, {})])
633       Log("adding a NIC", indent=2)
634       self.ExecOp(op_add)
635       Log("removing last NIC", indent=2)
636       self.ExecOp(op_rem)
637
638   def _CheckInstanceAlive(self, instance):
639     """Check if an instance is alive by doing http checks.
640
641     This will try to retrieve the url on the instance /hostname.txt
642     and check that it contains the hostname of the instance. In case
643     we get ECONNREFUSED, we retry up to the net timeout seconds, for
644     any other error we abort.
645
646     """
647     if not self.opts.http_check:
648       return
649     try:
650       for retries in range(self.opts.net_timeout):
651         try:
652           url = urllib2.urlopen("http://%s/hostname.txt" % instance)
653         except urllib2.URLError, err:
654           if err.args[0][0] == errno.ECONNREFUSED:
655             time.sleep(1)
656             continue
657           raise
658     except urllib2.URLError, err:
659       raise InstanceDown(instance, str(err))
660     hostname = url.read().strip()
661     if hostname != instance:
662       raise InstanceDown(instance, ("Hostname mismatch, expected %s, got %s" %
663                                     (instance, hostname)))
664
665   def BurninCluster(self):
666     """Test a cluster intensively.
667
668     This will create instances and then start/stop/failover them.
669     It is safe for existing instances but could impact performance.
670
671     """
672
673     opts = self.opts
674
675     Log("Testing global parameters")
676
677     if (len(self.nodes) == 1 and
678         opts.disk_template not in (constants.DT_DISKLESS, constants.DT_PLAIN,
679                                    constants.DT_FILE)):
680       Err("When one node is available/selected the disk template must"
681           " be 'diskless', 'file' or 'plain'")
682
683     has_err = True
684     try:
685       self.CreateInstances()
686       if opts.do_replace1 and opts.disk_template in constants.DTS_NET_MIRROR:
687         self.ReplaceDisks1D8()
688       if (opts.do_replace2 and len(self.nodes) > 2 and
689           opts.disk_template in constants.DTS_NET_MIRROR) :
690         self.ReplaceDisks2()
691
692       if opts.disk_template != constants.DT_DISKLESS:
693         self.GrowDisks()
694
695       if opts.do_failover and opts.disk_template in constants.DTS_NET_MIRROR:
696         self.Failover()
697
698       if opts.do_migrate and opts.disk_template == constants.DT_DRBD8:
699         self.Migrate()
700
701       if (opts.do_importexport and
702           opts.disk_template not in (constants.DT_DISKLESS,
703                                      constants.DT_FILE)):
704         self.ImportExport()
705
706       if opts.do_reinstall:
707         self.Reinstall()
708
709       if opts.do_reboot:
710         self.Reboot()
711
712       if opts.do_addremove_disks:
713         self.AddRemoveDisks()
714
715       if opts.do_addremove_nics:
716         self.AddRemoveNICs()
717
718       if opts.do_activate_disks:
719         self.ActivateDisks()
720
721       if opts.rename:
722         self.Rename()
723
724       if opts.do_startstop:
725         self.StopStart()
726
727       has_err = False
728     finally:
729       if has_err:
730         Log("Error detected: opcode buffer follows:\n\n")
731         Log(self.GetFeedbackBuf())
732         Log("\n\n")
733       if not self.opts.keep_instances:
734         self.Remove()
735
736     return 0
737
738
739 def main():
740   """Main function"""
741
742   burner = Burner()
743   return burner.BurninCluster()
744
745
746 if __name__ == "__main__":
747   main()