Statistics
| Branch: | Tag: | Revision:

root / tools / burnin @ e17188f4

History | View | Annotate | Download (15.8 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(utils.MergeTime(msg[0])),
86
                                      msg[2]))
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, feedback_fn=self.Feedback)
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

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

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

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

    
343
      self.to_rem.append(instance)
344

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

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

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

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

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

    
378

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

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

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

    
396
    """
397

    
398
    opts = self.opts
399

    
400
    Log("- Testing global parameters")
401

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

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

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

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

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

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

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

    
438
    return 0
439

    
440

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

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

    
447

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