Convert cli.SubmitOpCode to use the master
[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 from itertools import izip, islice, cycle
30 from cStringIO import StringIO
31
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
39
40
41 USAGE = ("\tburnin -o OS_NAME [options...] instance_name ...")
42
43
44 def Usage():
45   """Shows program usage information and exits the program."""
46
47   print >> sys.stderr, "Usage:"
48   print >> sys.stderr, USAGE
49   sys.exit(2)
50
51
52 def Log(msg):
53   """Simple function that prints out its argument.
54
55   """
56   print msg
57   sys.stdout.flush()
58
59
60 class Burner(object):
61   """Burner class."""
62
63   def __init__(self):
64     """Constructor."""
65     logger.SetupLogging(debug=False, program="ganeti/burnin")
66     self._feed_buf = StringIO()
67     self.proc = mcpu.Processor(feedback=self.Feedback)
68     self.nodes = []
69     self.instances = []
70     self.to_rem = []
71     self.opts = None
72     self.ParseOptions()
73     self.GetState()
74
75   def ClearFeedbackBuf(self):
76     """Clear the feedback buffer."""
77     self._feed_buf.truncate(0)
78
79   def GetFeedbackBuf(self):
80     """Return the contents of the buffer."""
81     return self._feed_buf.getvalue()
82
83   def Feedback(self, msg):
84     """Acumulate feedback in our buffer."""
85     self._feed_buf.write(msg)
86     self._feed_buf.write("\n")
87     if self.opts.verbose:
88       Log(msg)
89
90   def ExecOp(self, op):
91     """Execute an opcode and manage the exec buffer."""
92     self.ClearFeedbackBuf()
93     return cli.SubmitOpCode(op)
94
95   def ParseOptions(self):
96     """Parses the command line options.
97
98     In case of command line errors, it will show the usage and exit the
99     program.
100
101     """
102
103     parser = optparse.OptionParser(usage="\n%s" % USAGE,
104                                    version="%%prog (ganeti) %s" %
105                                    constants.RELEASE_VERSION,
106                                    option_class=cli.CliOption)
107
108     parser.add_option("-o", "--os", dest="os", default=None,
109                       help="OS to use during burnin",
110                       metavar="<OS>")
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("--mem-size", dest="mem_size", help="Memory size",
116                       default=128, type="unit", metavar="<size>")
117     parser.add_option("-v", "--verbose",
118                       action="store_true", dest="verbose", default=False,
119                       help="print command execution messages to stdout")
120     parser.add_option("--no-replace1", dest="do_replace1",
121                       help="Skip disk replacement with the same secondary",
122                       action="store_false", default=True)
123     parser.add_option("--no-replace2", dest="do_replace2",
124                       help="Skip disk replacement with a different secondary",
125                       action="store_false", default=True)
126     parser.add_option("--no-failover", dest="do_failover",
127                       help="Skip instance failovers", action="store_false",
128                       default=True)
129     parser.add_option("--no-importexport", dest="do_importexport",
130                       help="Skip instance export/import", action="store_false",
131                       default=True)
132     parser.add_option("--no-startstop", dest="do_startstop",
133                       help="Skip instance stop/start", action="store_false",
134                       default=True)
135     parser.add_option("--rename", dest="rename", default=None,
136                       help="Give one unused instance name which is taken"
137                            " to start the renaming sequence",
138                       metavar="<instance_name>")
139     parser.add_option("-t", "--disk-template", dest="disk_template",
140                       choices=("diskless", "file", "plain", "drbd"),
141                       default="drbd",
142                       help="Disk template (diskless, file, plain or drbd)"
143                             " [drbd]")
144     parser.add_option("-n", "--nodes", dest="nodes", default="",
145                       help="Comma separated list of nodes to perform"
146                       " the burnin on (defaults to all nodes)")
147     parser.add_option("--iallocator", dest="iallocator",
148                       default=None, type="string",
149                       help="Perform the allocation using an iallocator"
150                       " instead of fixed node spread (node restrictions no"
151                       " longer apply, therefore -n/--nodes must not be used")
152
153     options, args = parser.parse_args()
154     if len(args) < 1 or options.os is None:
155       Usage()
156
157     supported_disk_templates = (constants.DT_DISKLESS,
158                                 constants.DT_FILE,
159                                 constants.DT_PLAIN,
160                                 constants.DT_DRBD8)
161     if options.disk_template not in supported_disk_templates:
162       Log("Unknown disk template '%s'" % options.disk_template)
163       sys.exit(1)
164
165     if options.nodes and options.iallocator:
166       Log("Give either the nodes option or the iallocator option, not both")
167       sys.exit(1)
168
169     self.opts = options
170     self.instances = args
171
172   def GetState(self):
173     """Read the cluster state from the config."""
174     if self.opts.nodes:
175       names = self.opts.nodes.split(",")
176     else:
177       names = []
178     try:
179       op = opcodes.OpQueryNodes(output_fields=["name"], names=names)
180       result = self.ExecOp(op)
181     except errors.GenericError, err:
182       err_code, msg = cli.FormatError(err)
183       Log(msg)
184       sys.exit(err_code)
185     self.nodes = [data[0] for data in result]
186
187     result = self.ExecOp(opcodes.OpDiagnoseOS(output_fields=["name", "valid"],
188                                               names=[]))
189
190     if not result:
191       Log("Can't get the OS list")
192       sys.exit(1)
193
194     # filter non-valid OS-es
195     os_set = [val[0] for val in result if val[1]]
196
197     if self.opts.os not in os_set:
198       Log("OS '%s' not found" % self.opts.os)
199       sys.exit(1)
200
201   def CreateInstances(self):
202     """Create the given instances.
203
204     """
205     self.to_rem = []
206     mytor = izip(cycle(self.nodes),
207                  islice(cycle(self.nodes), 1, None),
208                  self.instances)
209     for pnode, snode, instance in mytor:
210       if self.opts.iallocator:
211         pnode = snode = None
212         Log("- Add instance %s (iallocator: %s)" %
213               (instance, self.opts.iallocator))
214       elif self.opts.disk_template not in constants.DTS_NET_MIRROR:
215         snode = None
216         Log("- Add instance %s on node %s" % (instance, pnode))
217       else:
218         Log("- Add instance %s on nodes %s/%s" % (instance, pnode, snode))
219
220       op = opcodes.OpCreateInstance(instance_name=instance,
221                                     mem_size=self.opts.mem_size,
222                                     disk_size=self.opts.os_size,
223                                     swap_size=self.opts.swap_size,
224                                     disk_template=self.opts.disk_template,
225                                     mode=constants.INSTANCE_CREATE,
226                                     os_type=self.opts.os,
227                                     pnode=pnode,
228                                     snode=snode,
229                                     vcpus=1,
230                                     start=True,
231                                     ip_check=True,
232                                     wait_for_sync=True,
233                                     mac="auto",
234                                     kernel_path=None,
235                                     initrd_path=None,
236                                     hvm_boot_order=None,
237                                     file_driver="loop",
238                                     file_storage_dir=None,
239                                     iallocator=self.opts.iallocator)
240       self.ExecOp(op)
241       self.to_rem.append(instance)
242
243   def ReplaceDisks1D8(self):
244     """Replace disks on primary and secondary for drbd8."""
245     for instance in self.instances:
246       for mode in constants.REPLACE_DISK_SEC, constants.REPLACE_DISK_PRI:
247         op = opcodes.OpReplaceDisks(instance_name=instance,
248                                     mode=mode,
249                                     disks=["sda", "sdb"])
250         Log("- Replace disks (%s) for instance %s" % (mode, instance))
251         self.ExecOp(op)
252
253   def ReplaceDisks2(self):
254     """Replace secondary node."""
255     mode = constants.REPLACE_DISK_SEC
256
257     mytor = izip(islice(cycle(self.nodes), 2, None),
258                  self.instances)
259     for tnode, instance in mytor:
260       if self.opts.iallocator:
261         tnode = None
262       op = opcodes.OpReplaceDisks(instance_name=instance,
263                                   mode=mode,
264                                   remote_node=tnode,
265                                   iallocator=self.opts.iallocator,
266                                   disks=["sda", "sdb"])
267       Log("- Replace secondary (%s) for instance %s" % (mode, instance))
268       self.ExecOp(op)
269
270   def Failover(self):
271     """Failover the instances."""
272
273     for instance in self.instances:
274       op = opcodes.OpFailoverInstance(instance_name=instance,
275                                       ignore_consistency=False)
276
277       Log("- Failover instance %s" % (instance))
278       self.ExecOp(op)
279
280   def ImportExport(self):
281     """Export the instance, delete it, and import it back.
282
283     """
284
285     mytor = izip(cycle(self.nodes),
286                  islice(cycle(self.nodes), 1, None),
287                  islice(cycle(self.nodes), 2, None),
288                  self.instances)
289
290     for pnode, snode, enode, instance in mytor:
291       exp_op = opcodes.OpExportInstance(instance_name=instance,
292                                            target_node=enode,
293                                            shutdown=True)
294       rem_op = opcodes.OpRemoveInstance(instance_name=instance)
295       nam_op = opcodes.OpQueryInstances(output_fields=["name"],
296                                            names=[instance])
297       full_name = self.ExecOp(nam_op)[0][0]
298       imp_dir = os.path.join(constants.EXPORT_DIR, full_name)
299       imp_op = opcodes.OpCreateInstance(instance_name=instance,
300                                         mem_size=128,
301                                         disk_size=self.opts.os_size,
302                                         swap_size=self.opts.swap_size,
303                                         disk_template=self.opts.disk_template,
304                                         mode=constants.INSTANCE_IMPORT,
305                                         src_node=enode,
306                                         src_path=imp_dir,
307                                         pnode=pnode,
308                                         snode=snode,
309                                         vcpus=1,
310                                         start=True,
311                                         ip_check=True,
312                                         wait_for_sync=True,
313                                         mac="auto",
314                                         file_storage_dir=None,
315                                         file_driver=None)
316       erem_op = opcodes.OpRemoveExport(instance_name=instance)
317
318       Log("- Export instance %s to node %s" % (instance, enode))
319       self.ExecOp(exp_op)
320       Log("- Remove instance %s" % (instance))
321       self.ExecOp(rem_op)
322       self.to_rem.remove(instance)
323       Log("- Import instance %s from node %s to node %s" %
324           (instance, enode, pnode))
325       self.ExecOp(imp_op)
326       Log("- Remove export of instance %s" % (instance))
327       self.ExecOp(erem_op)
328
329       self.to_rem.append(instance)
330
331   def StopInstance(self, instance):
332     """Stop given instance."""
333     op = opcodes.OpShutdownInstance(instance_name=instance)
334     Log("- Shutdown instance %s" % instance)
335     self.ExecOp(op)
336
337   def StartInstance(self, instance):
338     """Start given instance."""
339     op = opcodes.OpStartupInstance(instance_name=instance, force=False)
340     Log("- Start instance %s" % instance)
341     self.ExecOp(op)
342
343   def RenameInstance(self, instance, instance_new):
344     """Rename instance."""
345     op = opcodes.OpRenameInstance(instance_name=instance,
346                                   new_name=instance_new)
347     Log("- Rename instance %s to %s" % (instance, instance_new))
348     self.ExecOp(op)
349
350   def StopStart(self):
351     """Stop/start the instances."""
352     for instance in self.instances:
353       self.StopInstance(instance)
354       self.StartInstance(instance)
355
356   def Remove(self):
357     """Remove the instances."""
358     for instance in self.to_rem:
359       op = opcodes.OpRemoveInstance(instance_name=instance)
360       Log("- Remove instance %s" % instance)
361       self.ExecOp(op)
362
363
364   def Rename(self):
365     """Rename the instances."""
366     rename = self.opts.rename
367     for instance in self.instances:
368       self.StopInstance(instance)
369       self.RenameInstance(instance, rename)
370       self.StartInstance(rename)
371       self.StopInstance(rename)
372       self.RenameInstance(rename, instance)
373       self.StartInstance(instance)
374
375   def BurninCluster(self):
376     """Test a cluster intensively.
377
378     This will create instances and then start/stop/failover them.
379     It is safe for existing instances but could impact performance.
380
381     """
382
383     opts = self.opts
384
385     Log("- Testing global parameters")
386
387     if (len(self.nodes) == 1 and
388         opts.disk_template not in (constants.DT_DISKLESS, constants.DT_PLAIN,
389                                    constants.DT_FILE)):
390       Log("When one node is available/selected the disk template must"
391           " be 'diskless', 'file' or 'plain'")
392       sys.exit(1)
393
394     has_err = True
395     try:
396       self.CreateInstances()
397       if opts.do_replace1 and opts.disk_template in constants.DTS_NET_MIRROR:
398         self.ReplaceDisks1D8()
399       if (opts.do_replace2 and len(self.nodes) > 2 and
400           opts.disk_template in constants.DTS_NET_MIRROR) :
401         self.ReplaceDisks2()
402
403       if opts.do_failover and opts.disk_template in constants.DTS_NET_MIRROR:
404         self.Failover()
405
406       if opts.do_importexport:
407         self.ImportExport()
408
409       if opts.do_startstop:
410         self.StopStart()
411
412       if opts.rename:
413         self.Rename()
414
415       has_err = False
416     finally:
417       if has_err:
418         Log("Error detected: opcode buffer follows:\n\n")
419         Log(self.GetFeedbackBuf())
420         Log("\n\n")
421       self.Remove()
422
423     return 0
424
425
426 def main():
427   """Main function"""
428
429   burner = Burner()
430   try:
431     utils.Lock('cmd', max_retries=15, debug=True)
432   except errors.LockError, err:
433     logger.ToStderr(str(err))
434     return 1
435   try:
436     retval = burner.BurninCluster()
437   finally:
438     utils.Unlock('cmd')
439     utils.LockCleanup()
440   return retval
441
442
443 if __name__ == "__main__":
444   main()