Statistics
| Branch: | Tag: | Revision:

root / tools / burnin @ a4af651e

History | View | Annotate | Download (15 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(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
  return burner.BurninCluster()
431

    
432

    
433
if __name__ == "__main__":
434
  main()