4 # Copyright (C) 2006, 2007 Google Inc.
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.
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.
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
29 from itertools import izip, islice, cycle
30 from cStringIO import StringIO
32 from ganeti import opcodes
33 from ganeti import mcpu
34 from ganeti import constants
35 from ganeti import cli
36 from ganeti import logger
37 from ganeti import errors
38 from ganeti import utils
41 USAGE = ("\tburnin -o OS_NAME [options...] instance_name ...")
45 """Shows program usage information and exits the program."""
47 print >> sys.stderr, "Usage:"
48 print >> sys.stderr, USAGE
53 """Simple function that prints out its argument.
65 logger.SetupLogging(debug=False, program="ganeti/burnin")
66 self._feed_buf = StringIO()
67 self.proc = mcpu.Processor(feedback=self.Feedback)
75 def ClearFeedbackBuf(self):
76 """Clear the feedback buffer."""
77 self._feed_buf.truncate(0)
79 def GetFeedbackBuf(self):
80 """Return the contents of the buffer."""
81 return self._feed_buf.getvalue()
83 def Feedback(self, msg):
84 """Acumulate feedback in our buffer."""
85 self._feed_buf.write(msg)
86 self._feed_buf.write("\n")
91 """Execute an opcode and manage the exec buffer."""
92 self.ClearFeedbackBuf()
93 return self.proc.ExecOpCode(op)
95 def ParseOptions(self):
96 """Parses the command line options.
98 In case of command line errors, it will show the usage and exit the
103 parser = optparse.OptionParser(usage="\n%s" % USAGE,
104 version="%%prog (ganeti) %s" %
105 constants.RELEASE_VERSION,
106 option_class=cli.CliOption)
108 parser.add_option("-o", "--os", dest="os", default=None,
109 help="OS to use during burnin",
111 parser.add_option("--os-size", dest="os_size", help="Disk size",
112 default=4 * 1024, type="unit", metavar="<size>")
113 parser.add_option("--swap-size", dest="swap_size", help="Swap size",
114 default=4 * 1024, type="unit", metavar="<size>")
115 parser.add_option("-v", "--verbose",
116 action="store_true", dest="verbose", default=False,
117 help="print command execution messages to stdout")
118 parser.add_option("--no-replace1", dest="do_replace1",
119 help="Skip disk replacement with the same secondary",
120 action="store_false", default=True)
121 parser.add_option("--no-replace2", dest="do_replace2",
122 help="Skip disk replacement with a different secondary",
123 action="store_false", default=True)
124 parser.add_option("--no-failover", dest="do_failover",
125 help="Skip instance failovers", action="store_false",
127 parser.add_option("--no-importexport", dest="do_importexport",
128 help="Skip instance export/import", action="store_false",
130 parser.add_option("--no-startstop", dest="do_startstop",
131 help="Skip instance stop/start", action="store_false",
133 parser.add_option("-t", "--disk-template", dest="disk_template",
134 choices=("diskless", "plain", "remote_raid1", "drbd"),
135 default="remote_raid1",
136 help="Template type for network mirroring (remote_raid1"
137 " or drbd) [remote_raid1]")
138 parser.add_option("-n", "--nodes", dest="nodes", default="",
139 help="Comma separated list of nodes to perform"
140 " the burnin on (defaults to all nodes)")
142 options, args = parser.parse_args()
143 if len(args) < 1 or options.os is None:
146 supported_disk_templates = (constants.DT_DISKLESS, constants.DT_PLAIN,
147 constants.DT_REMOTE_RAID1,
149 if options.disk_template not in supported_disk_templates:
150 Log("Unknown disk template '%s'" % options.disk_template)
154 self.instances = args
157 """Read the cluster state from the config."""
159 names = self.opts.nodes.split(",")
163 op = opcodes.OpQueryNodes(output_fields=["name"], names=names)
164 result = self.ExecOp(op)
165 except errors.GenericError, err:
166 err_code, msg = cli.FormatError(err)
169 self.nodes = [data[0] for data in result]
171 result = self.ExecOp(opcodes.OpDiagnoseOS())
174 Log("Can't get the OS list")
177 # filter non-valid OS-es
179 for node_name in result:
180 oses[node_name] = [obj for obj in result[node_name] if obj]
182 fnode = oses.keys()[0]
183 os_set = set([os_inst.name for os_inst in oses[fnode]])
186 os_set &= set([os_inst.name for os_inst in oses[node]])
188 if self.opts.os not in os_set:
189 Log("OS '%s' not found" % self.opts.os)
192 def CreateInstances(self):
193 """Create the given instances.
197 mytor = izip(cycle(self.nodes),
198 islice(cycle(self.nodes), 1, None),
200 for pnode, snode, instance in mytor:
201 op = opcodes.OpCreateInstance(instance_name=instance,
203 disk_size=self.opts.os_size,
204 swap_size=self.opts.swap_size,
205 disk_template=self.opts.disk_template,
206 mode=constants.INSTANCE_CREATE,
207 os_type=self.opts.os,
218 Log("- Add instance %s on nodes %s/%s" % (instance, pnode, snode))
220 self.to_rem.append(instance)
222 def ReplaceDisks1R1(self):
223 """Replace disks with the same secondary for rr1."""
224 # replace all, both disks
225 for instance in self.instances:
226 op = opcodes.OpReplaceDisks(instance_name=instance,
228 mode=constants.REPLACE_DISK_ALL,
229 disks=["sda", "sdb"])
231 Log("- Replace disks for instance %s" % (instance))
234 def ReplaceDisks1D8(self):
235 """Replace disks on primary and secondary for drbd8."""
236 for instance in self.instances:
237 for mode in constants.REPLACE_DISK_SEC, constants.REPLACE_DISK_PRI:
238 op = opcodes.OpReplaceDisks(instance_name=instance,
240 disks=["sda", "sdb"])
241 Log("- Replace disks (%s) for instance %s" % (mode, instance))
244 def ReplaceDisks2(self):
245 """Replace secondary node."""
246 if self.opts.disk_template == constants.DT_REMOTE_RAID1:
247 mode = constants.REPLACE_DISK_ALL
249 mode = constants.REPLACE_DISK_SEC
251 mytor = izip(islice(cycle(self.nodes), 2, None),
253 for tnode, instance in mytor:
254 op = opcodes.OpReplaceDisks(instance_name=instance,
257 disks=["sda", "sdb"])
258 Log("- Replace secondary (%s) for instance %s" % (mode, instance))
262 """Failover the instances."""
264 for instance in self.instances:
265 op = opcodes.OpFailoverInstance(instance_name=instance,
266 ignore_consistency=False)
268 Log("- Failover instance %s" % (instance))
271 def ImportExport(self):
272 """Export the instance, delete it, and import it back.
276 mytor = izip(cycle(self.nodes),
277 islice(cycle(self.nodes), 1, None),
278 islice(cycle(self.nodes), 2, None),
281 for pnode, snode, enode, instance in mytor:
282 exp_op = opcodes.OpExportInstance(instance_name=instance,
285 rem_op = opcodes.OpRemoveInstance(instance_name=instance)
286 nam_op = opcodes.OpQueryInstances(output_fields=["name"],
288 full_name = self.ExecOp(nam_op)[0][0]
289 imp_dir = os.path.join(constants.EXPORT_DIR, full_name)
290 imp_op = opcodes.OpCreateInstance(instance_name=instance,
292 disk_size=self.opts.os_size,
293 swap_size=self.opts.swap_size,
294 disk_template=self.opts.disk_template,
295 mode=constants.INSTANCE_IMPORT,
306 Log("- Export instance %s to node %s" % (instance, enode))
308 Log("- Remove instance %s" % (instance))
310 self.to_rem.remove(instance)
311 Log("- Import instance %s from node %s to node %s" %
312 (instance, enode, pnode))
314 self.to_rem.append(instance)
317 """Stop/start the instances."""
318 for instance in self.instances:
319 op = opcodes.OpShutdownInstance(instance_name=instance)
320 Log("- Shutdown instance %s" % instance)
322 op = opcodes.OpStartupInstance(instance_name=instance, force=False)
323 Log("- Start instance %s" % instance)
327 """Remove the instances."""
328 for instance in self.to_rem:
329 op = opcodes.OpRemoveInstance(instance_name=instance)
330 Log("- Remove instance %s" % instance)
333 def BurninCluster(self):
334 """Test a cluster intensively.
336 This will create instances and then start/stop/failover them.
337 It is safe for existing instances but could impact performance.
343 Log("- Testing global parameters")
345 if (len(self.nodes) == 1 and
346 opts.disk_template not in (constants.DT_DISKLESS, constants.DT_PLAIN)):
347 Log("When one node is available/selected the disk template must"
348 " be 'plain' or 'diskless'")
353 self.CreateInstances()
354 if opts.do_replace1 and opts.disk_template in constants.DTS_NET_MIRROR:
355 if opts.disk_template == constants.DT_REMOTE_RAID1:
356 self.ReplaceDisks1R1()
357 elif opts.disk_template == constants.DT_DRBD8:
358 self.ReplaceDisks1D8()
359 if (opts.do_replace2 and len(self.nodes) > 2 and
360 opts.disk_template in constants.DTS_NET_MIRROR) :
363 if opts.do_failover and opts.disk_template in constants.DTS_NET_MIRROR:
366 if opts.do_importexport:
369 if opts.do_startstop:
375 Log("Error detected: opcode buffer follows:\n\n")
376 Log(self.GetFeedbackBuf())
388 utils.Lock('cmd', max_retries=15, debug=True)
389 except errors.LockError, err:
390 logger.ToStderr(str(err))
393 retval = burner.BurninCluster()
400 if __name__ == "__main__":