Statistics
| Branch: | Tag: | Revision:

root / tools / burnin @ 59f187eb

History | View | Annotate | Download (15.7 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
from itertools import izip, islice, cycle
31
from cStringIO import StringIO
32

    
33
from ganeti import opcodes
34
from ganeti import mcpu
35
from ganeti import constants
36
from ganeti import cli
37
from ganeti import logger
38
from ganeti import errors
39
from ganeti import utils
40

    
41

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

    
44

    
45
def Usage():
46
  """Shows program usage information and exits the program."""
47

    
48
  print >> sys.stderr, "Usage:"
49
  print >> sys.stderr, USAGE
50
  sys.exit(2)
51

    
52

    
53
def Log(msg):
54
  """Simple function that prints out its argument.
55

    
56
  """
57
  print msg
58
  sys.stdout.flush()
59

    
60

    
61
class Burner(object):
62
  """Burner class."""
63

    
64
  def __init__(self):
65
    """Constructor."""
66
    logger.SetupLogging(constants.LOG_BURNIN, debug=False, stderr_logging=True)
67
    self._feed_buf = StringIO()
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("%s %s\n" % (time.ctime(msg[0]), msg[2]))
86
    if self.opts.verbose:
87
      Log(msg)
88

    
89
  def ExecOp(self, op):
90
    """Execute an opcode and manage the exec buffer."""
91
    self.ClearFeedbackBuf()
92
    return cli.SubmitOpCode(op, feedback_fn=self.Feedback)
93

    
94
  def ParseOptions(self):
95
    """Parses the command line options.
96

    
97
    In case of command line errors, it will show the usage and exit the
98
    program.
99

    
100
    """
101

    
102
    parser = optparse.OptionParser(usage="\n%s" % USAGE,
103
                                   version="%%prog (ganeti) %s" %
104
                                   constants.RELEASE_VERSION,
105
                                   option_class=cli.CliOption)
106

    
107
    parser.add_option("-o", "--os", dest="os", default=None,
108
                      help="OS to use during burnin",
109
                      metavar="<OS>")
110
    parser.add_option("--os-size", dest="os_size", help="Disk size",
111
                      default=4 * 1024, type="unit", metavar="<size>")
112
    parser.add_option("--swap-size", dest="swap_size", help="Swap size",
113
                      default=4 * 1024, type="unit", metavar="<size>")
114
    parser.add_option("--mem-size", dest="mem_size", help="Memory size",
115
                      default=128, type="unit", metavar="<size>")
116
    parser.add_option("-v", "--verbose",
117
                      action="store_true", dest="verbose", default=False,
118
                      help="print command execution messages to stdout")
119
    parser.add_option("--no-replace1", dest="do_replace1",
120
                      help="Skip disk replacement with the same secondary",
121
                      action="store_false", default=True)
122
    parser.add_option("--no-replace2", dest="do_replace2",
123
                      help="Skip disk replacement with a different secondary",
124
                      action="store_false", default=True)
125
    parser.add_option("--no-failover", dest="do_failover",
126
                      help="Skip instance failovers", action="store_false",
127
                      default=True)
128
    parser.add_option("--no-importexport", dest="do_importexport",
129
                      help="Skip instance export/import", action="store_false",
130
                      default=True)
131
    parser.add_option("--no-startstop", dest="do_startstop",
132
                      help="Skip instance stop/start", action="store_false",
133
                      default=True)
134
    parser.add_option("--rename", dest="rename", default=None,
135
                      help="Give one unused instance name which is taken"
136
                           " to start the renaming sequence",
137
                      metavar="<instance_name>")
138
    parser.add_option("-t", "--disk-template", dest="disk_template",
139
                      choices=("diskless", "file", "plain", "drbd"),
140
                      default="drbd",
141
                      help="Disk template (diskless, file, plain or drbd)"
142
                            " [drbd]")
143
    parser.add_option("-n", "--nodes", dest="nodes", default="",
144
                      help="Comma separated list of nodes to perform"
145
                      " the burnin on (defaults to all nodes)")
146
    parser.add_option("--iallocator", dest="iallocator",
147
                      default=None, type="string",
148
                      help="Perform the allocation using an iallocator"
149
                      " instead of fixed node spread (node restrictions no"
150
                      " longer apply, therefore -n/--nodes must not be used")
151

    
152
    options, args = parser.parse_args()
153
    if len(args) < 1 or options.os is None:
154
      Usage()
155

    
156
    supported_disk_templates = (constants.DT_DISKLESS,
157
                                constants.DT_FILE,
158
                                constants.DT_PLAIN,
159
                                constants.DT_DRBD8)
160
    if options.disk_template not in supported_disk_templates:
161
      Log("Unknown disk template '%s'" % options.disk_template)
162
      sys.exit(1)
163

    
164
    if options.nodes and options.iallocator:
165
      Log("Give either the nodes option or the iallocator option, not both")
166
      sys.exit(1)
167

    
168
    self.opts = options
169
    self.instances = args
170

    
171
  def GetState(self):
172
    """Read the cluster state from the config."""
173
    if self.opts.nodes:
174
      names = self.opts.nodes.split(",")
175
    else:
176
      names = []
177
    try:
178
      op = opcodes.OpQueryNodes(output_fields=["name"], names=names)
179
      result = self.ExecOp(op)
180
    except errors.GenericError, err:
181
      err_code, msg = cli.FormatError(err)
182
      Log(msg)
183
      sys.exit(err_code)
184
    self.nodes = [data[0] for data in result]
185

    
186
    result = self.ExecOp(opcodes.OpDiagnoseOS(output_fields=["name", "valid"],
187
                                              names=[]))
188

    
189
    if not result:
190
      Log("Can't get the OS list")
191
      sys.exit(1)
192

    
193
    # filter non-valid OS-es
194
    os_set = [val[0] for val in result if val[1]]
195

    
196
    if self.opts.os not in os_set:
197
      Log("OS '%s' not found" % self.opts.os)
198
      sys.exit(1)
199

    
200
  def CreateInstances(self):
201
    """Create the given instances.
202

    
203
    """
204
    self.to_rem = []
205
    mytor = izip(cycle(self.nodes),
206
                 islice(cycle(self.nodes), 1, None),
207
                 self.instances)
208
    for pnode, snode, instance in mytor:
209
      if self.opts.iallocator:
210
        pnode = snode = None
211
        Log("- Add instance %s (iallocator: %s)" %
212
              (instance, self.opts.iallocator))
213
      elif self.opts.disk_template not in constants.DTS_NET_MIRROR:
214
        snode = None
215
        Log("- Add instance %s on node %s" % (instance, pnode))
216
      else:
217
        Log("- Add instance %s on nodes %s/%s" % (instance, pnode, snode))
218

    
219
      op = opcodes.OpCreateInstance(instance_name=instance,
220
                                    mem_size=self.opts.mem_size,
221
                                    disk_size=self.opts.os_size,
222
                                    swap_size=self.opts.swap_size,
223
                                    disk_template=self.opts.disk_template,
224
                                    mode=constants.INSTANCE_CREATE,
225
                                    os_type=self.opts.os,
226
                                    pnode=pnode,
227
                                    snode=snode,
228
                                    vcpus=1,
229
                                    start=True,
230
                                    ip_check=True,
231
                                    wait_for_sync=True,
232
                                    mac="auto",
233
                                    kernel_path=None,
234
                                    initrd_path=None,
235
                                    hvm_boot_order=None,
236
                                    file_driver="loop",
237
                                    file_storage_dir=None,
238
                                    iallocator=self.opts.iallocator)
239
      self.ExecOp(op)
240
      self.to_rem.append(instance)
241

    
242
  def ReplaceDisks1D8(self):
243
    """Replace disks on primary and secondary for drbd8."""
244
    for instance in self.instances:
245
      for mode in constants.REPLACE_DISK_SEC, constants.REPLACE_DISK_PRI:
246
        op = opcodes.OpReplaceDisks(instance_name=instance,
247
                                    mode=mode,
248
                                    disks=["sda", "sdb"])
249
        Log("- Replace disks (%s) for instance %s" % (mode, instance))
250
        self.ExecOp(op)
251

    
252
  def ReplaceDisks2(self):
253
    """Replace secondary node."""
254
    mode = constants.REPLACE_DISK_SEC
255

    
256
    mytor = izip(islice(cycle(self.nodes), 2, None),
257
                 self.instances)
258
    for tnode, instance in mytor:
259
      if self.opts.iallocator:
260
        tnode = None
261
      op = opcodes.OpReplaceDisks(instance_name=instance,
262
                                  mode=mode,
263
                                  remote_node=tnode,
264
                                  iallocator=self.opts.iallocator,
265
                                  disks=["sda", "sdb"])
266
      Log("- Replace secondary (%s) for instance %s" % (mode, instance))
267
      self.ExecOp(op)
268

    
269
  def Failover(self):
270
    """Failover the instances."""
271

    
272
    for instance in self.instances:
273
      op = opcodes.OpFailoverInstance(instance_name=instance,
274
                                      ignore_consistency=False)
275

    
276
      Log("- Failover instance %s" % (instance))
277
      self.ExecOp(op)
278

    
279
  def ImportExport(self):
280
    """Export the instance, delete it, and import it back.
281

    
282
    """
283

    
284
    mytor = izip(cycle(self.nodes),
285
                 islice(cycle(self.nodes), 1, None),
286
                 islice(cycle(self.nodes), 2, None),
287
                 self.instances)
288

    
289
    for pnode, snode, enode, instance in mytor:
290

    
291
      if self.opts.iallocator:
292
        pnode = snode = None
293
        import_log_msg = ("- Import instance %s from node %s (iallocator: %s)" %
294
                          (instance, enode, self.opts.iallocator))
295
      elif self.opts.disk_template not in constants.DTS_NET_MIRROR:
296
        snode = None
297
        import_log_msg = ("- Import instance %s from node %s to node %s" %
298
                          (instance, enode, pnode))
299
      else:
300
        import_log_msg = ("- Import instance %s from node %s to nodes %s/%s" %
301
                          (instance, enode, pnode, snode))
302

    
303
      exp_op = opcodes.OpExportInstance(instance_name=instance,
304
                                           target_node=enode,
305
                                           shutdown=True)
306
      rem_op = opcodes.OpRemoveInstance(instance_name=instance,
307
                                        ignore_failures=True)
308
      nam_op = opcodes.OpQueryInstances(output_fields=["name"],
309
                                           names=[instance])
310
      full_name = self.ExecOp(nam_op)[0][0]
311
      imp_dir = os.path.join(constants.EXPORT_DIR, full_name)
312
      imp_op = opcodes.OpCreateInstance(instance_name=instance,
313
                                        mem_size=128,
314
                                        disk_size=self.opts.os_size,
315
                                        swap_size=self.opts.swap_size,
316
                                        disk_template=self.opts.disk_template,
317
                                        mode=constants.INSTANCE_IMPORT,
318
                                        src_node=enode,
319
                                        src_path=imp_dir,
320
                                        pnode=pnode,
321
                                        snode=snode,
322
                                        vcpus=1,
323
                                        start=True,
324
                                        ip_check=True,
325
                                        wait_for_sync=True,
326
                                        mac="auto",
327
                                        file_storage_dir=None,
328
                                        file_driver=None,
329
                                        iallocator=self.opts.iallocator)
330
      erem_op = opcodes.OpRemoveExport(instance_name=instance)
331

    
332
      Log("- Export instance %s to node %s" % (instance, enode))
333
      self.ExecOp(exp_op)
334
      Log("- Remove instance %s" % (instance))
335
      self.ExecOp(rem_op)
336
      self.to_rem.remove(instance)
337
      Log(import_log_msg)
338
      self.ExecOp(imp_op)
339
      Log("- Remove export of instance %s" % (instance))
340
      self.ExecOp(erem_op)
341

    
342
      self.to_rem.append(instance)
343

    
344
  def StopInstance(self, instance):
345
    """Stop given instance."""
346
    op = opcodes.OpShutdownInstance(instance_name=instance)
347
    Log("- Shutdown instance %s" % instance)
348
    self.ExecOp(op)
349

    
350
  def StartInstance(self, instance):
351
    """Start given instance."""
352
    op = opcodes.OpStartupInstance(instance_name=instance, force=False)
353
    Log("- Start instance %s" % instance)
354
    self.ExecOp(op)
355

    
356
  def RenameInstance(self, instance, instance_new):
357
    """Rename instance."""
358
    op = opcodes.OpRenameInstance(instance_name=instance,
359
                                  new_name=instance_new)
360
    Log("- Rename instance %s to %s" % (instance, instance_new))
361
    self.ExecOp(op)
362

    
363
  def StopStart(self):
364
    """Stop/start the instances."""
365
    for instance in self.instances:
366
      self.StopInstance(instance)
367
      self.StartInstance(instance)
368

    
369
  def Remove(self):
370
    """Remove the instances."""
371
    for instance in self.to_rem:
372
      op = opcodes.OpRemoveInstance(instance_name=instance,
373
                                    ignore_failures=True)
374
      Log("- Remove instance %s" % instance)
375
      self.ExecOp(op)
376

    
377

    
378
  def Rename(self):
379
    """Rename the instances."""
380
    rename = self.opts.rename
381
    for instance in self.instances:
382
      self.StopInstance(instance)
383
      self.RenameInstance(instance, rename)
384
      self.StartInstance(rename)
385
      self.StopInstance(rename)
386
      self.RenameInstance(rename, instance)
387
      self.StartInstance(instance)
388

    
389
  def BurninCluster(self):
390
    """Test a cluster intensively.
391

    
392
    This will create instances and then start/stop/failover them.
393
    It is safe for existing instances but could impact performance.
394

    
395
    """
396

    
397
    opts = self.opts
398

    
399
    Log("- Testing global parameters")
400

    
401
    if (len(self.nodes) == 1 and
402
        opts.disk_template not in (constants.DT_DISKLESS, constants.DT_PLAIN,
403
                                   constants.DT_FILE)):
404
      Log("When one node is available/selected the disk template must"
405
          " be 'diskless', 'file' or 'plain'")
406
      sys.exit(1)
407

    
408
    has_err = True
409
    try:
410
      self.CreateInstances()
411
      if opts.do_replace1 and opts.disk_template in constants.DTS_NET_MIRROR:
412
        self.ReplaceDisks1D8()
413
      if (opts.do_replace2 and len(self.nodes) > 2 and
414
          opts.disk_template in constants.DTS_NET_MIRROR) :
415
        self.ReplaceDisks2()
416

    
417
      if opts.do_failover and opts.disk_template in constants.DTS_NET_MIRROR:
418
        self.Failover()
419

    
420
      if opts.do_importexport:
421
        self.ImportExport()
422

    
423
      if opts.do_startstop:
424
        self.StopStart()
425

    
426
      if opts.rename:
427
        self.Rename()
428

    
429
      has_err = False
430
    finally:
431
      if has_err:
432
        Log("Error detected: opcode buffer follows:\n\n")
433
        Log(self.GetFeedbackBuf())
434
        Log("\n\n")
435
      self.Remove()
436

    
437
    return 0
438

    
439

    
440
def main():
441
  """Main function"""
442

    
443
  burner = Burner()
444
  return burner.BurninCluster()
445

    
446

    
447
if __name__ == "__main__":
448
  main()