Statistics
| Branch: | Tag: | Revision:

root / tools / burnin @ 2a2060ff

History | View | Annotate | Download (15.2 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
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(program="ganeti/burnin", debug=False)
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
                                        ignore_failures=True)
296
      nam_op = opcodes.OpQueryInstances(output_fields=["name"],
297
                                           names=[instance])
298
      full_name = self.ExecOp(nam_op)[0][0]
299
      imp_dir = os.path.join(constants.EXPORT_DIR, full_name)
300
      imp_op = opcodes.OpCreateInstance(instance_name=instance,
301
                                        mem_size=128,
302
                                        disk_size=self.opts.os_size,
303
                                        swap_size=self.opts.swap_size,
304
                                        disk_template=self.opts.disk_template,
305
                                        mode=constants.INSTANCE_IMPORT,
306
                                        src_node=enode,
307
                                        src_path=imp_dir,
308
                                        pnode=pnode,
309
                                        snode=snode,
310
                                        vcpus=1,
311
                                        start=True,
312
                                        ip_check=True,
313
                                        wait_for_sync=True,
314
                                        mac="auto",
315
                                        file_storage_dir=None,
316
                                        file_driver=None)
317
      erem_op = opcodes.OpRemoveExport(instance_name=instance)
318

    
319
      Log("- Export instance %s to node %s" % (instance, enode))
320
      self.ExecOp(exp_op)
321
      Log("- Remove instance %s" % (instance))
322
      self.ExecOp(rem_op)
323
      self.to_rem.remove(instance)
324
      Log("- Import instance %s from node %s to node %s" %
325
          (instance, enode, pnode))
326
      self.ExecOp(imp_op)
327
      Log("- Remove export of instance %s" % (instance))
328
      self.ExecOp(erem_op)
329

    
330
      self.to_rem.append(instance)
331

    
332
  def StopInstance(self, instance):
333
    """Stop given instance."""
334
    op = opcodes.OpShutdownInstance(instance_name=instance)
335
    Log("- Shutdown instance %s" % instance)
336
    self.ExecOp(op)
337

    
338
  def StartInstance(self, instance):
339
    """Start given instance."""
340
    op = opcodes.OpStartupInstance(instance_name=instance, force=False)
341
    Log("- Start instance %s" % instance)
342
    self.ExecOp(op)
343

    
344
  def RenameInstance(self, instance, instance_new):
345
    """Rename instance."""
346
    op = opcodes.OpRenameInstance(instance_name=instance,
347
                                  new_name=instance_new)
348
    Log("- Rename instance %s to %s" % (instance, instance_new))
349
    self.ExecOp(op)
350

    
351
  def StopStart(self):
352
    """Stop/start the instances."""
353
    for instance in self.instances:
354
      self.StopInstance(instance)
355
      self.StartInstance(instance)
356

    
357
  def Remove(self):
358
    """Remove the instances."""
359
    for instance in self.to_rem:
360
      op = opcodes.OpRemoveInstance(instance_name=instance,
361
                                    ignore_failures=True)
362
      Log("- Remove instance %s" % instance)
363
      self.ExecOp(op)
364

    
365

    
366
  def Rename(self):
367
    """Rename the instances."""
368
    rename = self.opts.rename
369
    for instance in self.instances:
370
      self.StopInstance(instance)
371
      self.RenameInstance(instance, rename)
372
      self.StartInstance(rename)
373
      self.StopInstance(rename)
374
      self.RenameInstance(rename, instance)
375
      self.StartInstance(instance)
376

    
377
  def BurninCluster(self):
378
    """Test a cluster intensively.
379

    
380
    This will create instances and then start/stop/failover them.
381
    It is safe for existing instances but could impact performance.
382

    
383
    """
384

    
385
    opts = self.opts
386

    
387
    Log("- Testing global parameters")
388

    
389
    if (len(self.nodes) == 1 and
390
        opts.disk_template not in (constants.DT_DISKLESS, constants.DT_PLAIN,
391
                                   constants.DT_FILE)):
392
      Log("When one node is available/selected the disk template must"
393
          " be 'diskless', 'file' or 'plain'")
394
      sys.exit(1)
395

    
396
    has_err = True
397
    try:
398
      self.CreateInstances()
399
      if opts.do_replace1 and opts.disk_template in constants.DTS_NET_MIRROR:
400
        self.ReplaceDisks1D8()
401
      if (opts.do_replace2 and len(self.nodes) > 2 and
402
          opts.disk_template in constants.DTS_NET_MIRROR) :
403
        self.ReplaceDisks2()
404

    
405
      if opts.do_failover and opts.disk_template in constants.DTS_NET_MIRROR:
406
        self.Failover()
407

    
408
      if opts.do_importexport:
409
        self.ImportExport()
410

    
411
      if opts.do_startstop:
412
        self.StopStart()
413

    
414
      if opts.rename:
415
        self.Rename()
416

    
417
      has_err = False
418
    finally:
419
      if has_err:
420
        Log("Error detected: opcode buffer follows:\n\n")
421
        Log(self.GetFeedbackBuf())
422
        Log("\n\n")
423
      self.Remove()
424

    
425
    return 0
426

    
427

    
428
def main():
429
  """Main function"""
430

    
431
  burner = Burner()
432
  return burner.BurninCluster()
433

    
434

    
435
if __name__ == "__main__":
436
  main()