Statistics
| Branch: | Tag: | Revision:

root / tools / burnin @ f9193417

History | View | Annotate | Download (12.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
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 self.proc.ExecOpCode(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("-v", "--verbose",
116
                      action="store_true", dest="verbose", default=False,
117
                      help="print command execution messages to stdout")
118
    parser.add_option("--no-replace1", dest="do_replace1",
119
                      help="Skip disk replacement with the same secondary",
120
                      action="store_false", default=True)
121
    parser.add_option("--no-replace2", dest="do_replace2",
122
                      help="Skip disk replacement with a different secondary",
123
                      action="store_false", default=True)
124
    parser.add_option("--no-failover", dest="do_failover",
125
                      help="Skip instance failovers", action="store_false",
126
                      default=True)
127
    parser.add_option("--no-importexport", dest="do_importexport",
128
                      help="Skip instance export/import", action="store_false",
129
                      default=True)
130
    parser.add_option("--no-startstop", dest="do_startstop",
131
                      help="Skip instance stop/start", action="store_false",
132
                      default=True)
133
    parser.add_option("-t", "--disk-template", dest="disk_template",
134
                      choices=("diskless", "plain", "drbd"),
135
                      default="drbd",
136
                      help="Disk template (diskless, plain or drbd) [drbd]")
137
    parser.add_option("-n", "--nodes", dest="nodes", default="",
138
                      help="Comma separated list of nodes to perform"
139
                      " the burnin on (defaults to all nodes)")
140

    
141
    options, args = parser.parse_args()
142
    if len(args) < 1 or options.os is None:
143
      Usage()
144

    
145
    supported_disk_templates = (constants.DT_DISKLESS,
146
                                constants.DT_PLAIN,
147
                                constants.DT_DRBD8)
148
    if options.disk_template not in supported_disk_templates:
149
      Log("Unknown disk template '%s'" % options.disk_template)
150
      sys.exit(1)
151

    
152
    self.opts = options
153
    self.instances = args
154

    
155
  def GetState(self):
156
    """Read the cluster state from the config."""
157
    if self.opts.nodes:
158
      names = self.opts.nodes.split(",")
159
    else:
160
      names = []
161
    try:
162
      op = opcodes.OpQueryNodes(output_fields=["name"], names=names)
163
      result = self.ExecOp(op)
164
    except errors.GenericError, err:
165
      err_code, msg = cli.FormatError(err)
166
      Log(msg)
167
      sys.exit(err_code)
168
    self.nodes = [data[0] for data in result]
169

    
170
    result = self.ExecOp(opcodes.OpDiagnoseOS())
171

    
172
    if not result:
173
      Log("Can't get the OS list")
174
      sys.exit(1)
175

    
176
    # filter non-valid OS-es
177
    oses = {}
178
    for node_name in result:
179
      oses[node_name] = [obj for obj in result[node_name] if obj]
180

    
181
    fnode = oses.keys()[0]
182
    os_set = set([os_inst.name for os_inst in oses[fnode]])
183
    del oses[fnode]
184
    for node in oses:
185
      os_set &= set([os_inst.name for os_inst in oses[node]])
186

    
187
    if self.opts.os not in os_set:
188
      Log("OS '%s' not found" % self.opts.os)
189
      sys.exit(1)
190

    
191
  def CreateInstances(self):
192
    """Create the given instances.
193

    
194
    """
195
    self.to_rem = []
196
    mytor = izip(cycle(self.nodes),
197
                 islice(cycle(self.nodes), 1, None),
198
                 self.instances)
199
    for pnode, snode, instance in mytor:
200
      op = opcodes.OpCreateInstance(instance_name=instance,
201
                                    mem_size=128,
202
                                    disk_size=self.opts.os_size,
203
                                    swap_size=self.opts.swap_size,
204
                                    disk_template=self.opts.disk_template,
205
                                    mode=constants.INSTANCE_CREATE,
206
                                    os_type=self.opts.os,
207
                                    pnode=pnode,
208
                                    snode=snode,
209
                                    vcpus=1,
210
                                    start=True,
211
                                    ip_check=True,
212
                                    wait_for_sync=True,
213
                                    mac="auto",
214
                                    kernel_path=None,
215
                                    initrd_path=None,
216
                                    hvm_boot_order=None)
217
      Log("- Add instance %s on nodes %s/%s" % (instance, pnode, snode))
218
      self.ExecOp(op)
219
      self.to_rem.append(instance)
220

    
221
  def ReplaceDisks1D8(self):
222
    """Replace disks on primary and secondary for drbd8."""
223
    for instance in self.instances:
224
      for mode in constants.REPLACE_DISK_SEC, constants.REPLACE_DISK_PRI:
225
        op = opcodes.OpReplaceDisks(instance_name=instance,
226
                                    mode=mode,
227
                                    disks=["sda", "sdb"])
228
        Log("- Replace disks (%s) for instance %s" % (mode, instance))
229
        self.ExecOp(op)
230

    
231
  def ReplaceDisks2(self):
232
    """Replace secondary node."""
233
    mode = constants.REPLACE_DISK_SEC
234

    
235
    mytor = izip(islice(cycle(self.nodes), 2, None),
236
                 self.instances)
237
    for tnode, instance in mytor:
238
      op = opcodes.OpReplaceDisks(instance_name=instance,
239
                                  mode=mode,
240
                                  remote_node=tnode,
241
                                  disks=["sda", "sdb"])
242
      Log("- Replace secondary (%s) for instance %s" % (mode, instance))
243
      self.ExecOp(op)
244

    
245
  def Failover(self):
246
    """Failover the instances."""
247

    
248
    for instance in self.instances:
249
      op = opcodes.OpFailoverInstance(instance_name=instance,
250
                                      ignore_consistency=False)
251

    
252
      Log("- Failover instance %s" % (instance))
253
      self.ExecOp(op)
254

    
255
  def ImportExport(self):
256
    """Export the instance, delete it, and import it back.
257

    
258
    """
259

    
260
    mytor = izip(cycle(self.nodes),
261
                 islice(cycle(self.nodes), 1, None),
262
                 islice(cycle(self.nodes), 2, None),
263
                 self.instances)
264

    
265
    for pnode, snode, enode, instance in mytor:
266
      exp_op = opcodes.OpExportInstance(instance_name=instance,
267
                                           target_node=enode,
268
                                           shutdown=True)
269
      rem_op = opcodes.OpRemoveInstance(instance_name=instance)
270
      nam_op = opcodes.OpQueryInstances(output_fields=["name"],
271
                                           names=[instance])
272
      full_name = self.ExecOp(nam_op)[0][0]
273
      imp_dir = os.path.join(constants.EXPORT_DIR, full_name)
274
      imp_op = opcodes.OpCreateInstance(instance_name=instance,
275
                                        mem_size=128,
276
                                        disk_size=self.opts.os_size,
277
                                        swap_size=self.opts.swap_size,
278
                                        disk_template=self.opts.disk_template,
279
                                        mode=constants.INSTANCE_IMPORT,
280
                                        src_node=enode,
281
                                        src_path=imp_dir,
282
                                        pnode=pnode,
283
                                        snode=snode,
284
                                        vcpus=1,
285
                                        start=True,
286
                                        ip_check=True,
287
                                        wait_for_sync=True,
288
                                        mac="auto")
289

    
290
      Log("- Export instance %s to node %s" % (instance, enode))
291
      self.ExecOp(exp_op)
292
      Log("- Remove instance %s" % (instance))
293
      self.ExecOp(rem_op)
294
      self.to_rem.remove(instance)
295
      Log("- Import instance %s from node %s to node %s" %
296
          (instance, enode, pnode))
297
      self.ExecOp(imp_op)
298
      self.to_rem.append(instance)
299

    
300
  def StopStart(self):
301
    """Stop/start the instances."""
302
    for instance in self.instances:
303
      op = opcodes.OpShutdownInstance(instance_name=instance)
304
      Log("- Shutdown instance %s" % instance)
305
      self.ExecOp(op)
306
      op = opcodes.OpStartupInstance(instance_name=instance, force=False)
307
      Log("- Start instance %s" % instance)
308
      self.ExecOp(op)
309

    
310
  def Remove(self):
311
    """Remove the instances."""
312
    for instance in self.to_rem:
313
      op = opcodes.OpRemoveInstance(instance_name=instance)
314
      Log("- Remove instance %s" % instance)
315
      self.ExecOp(op)
316

    
317
  def BurninCluster(self):
318
    """Test a cluster intensively.
319

    
320
    This will create instances and then start/stop/failover them.
321
    It is safe for existing instances but could impact performance.
322

    
323
    """
324

    
325
    opts = self.opts
326

    
327
    Log("- Testing global parameters")
328

    
329
    if (len(self.nodes) == 1 and
330
        opts.disk_template not in (constants.DT_DISKLESS, constants.DT_PLAIN)):
331
      Log("When one node is available/selected the disk template must"
332
               " be 'plain' or 'diskless'")
333
      sys.exit(1)
334

    
335
    has_err = True
336
    try:
337
      self.CreateInstances()
338
      if opts.do_replace1 and opts.disk_template in constants.DTS_NET_MIRROR:
339
        self.ReplaceDisks1D8()
340
      if (opts.do_replace2 and len(self.nodes) > 2 and
341
          opts.disk_template in constants.DTS_NET_MIRROR) :
342
        self.ReplaceDisks2()
343

    
344
      if opts.do_failover and opts.disk_template in constants.DTS_NET_MIRROR:
345
        self.Failover()
346

    
347
      if opts.do_importexport:
348
        self.ImportExport()
349

    
350
      if opts.do_startstop:
351
        self.StopStart()
352

    
353
      has_err = False
354
    finally:
355
      if has_err:
356
        Log("Error detected: opcode buffer follows:\n\n")
357
        Log(self.GetFeedbackBuf())
358
        Log("\n\n")
359
      self.Remove()
360

    
361
    return 0
362

    
363

    
364
def main():
365
  """Main function"""
366

    
367
  burner = Burner()
368
  try:
369
    utils.Lock('cmd', max_retries=15, debug=True)
370
  except errors.LockError, err:
371
    logger.ToStderr(str(err))
372
    return 1
373
  try:
374
    retval = burner.BurninCluster()
375
  finally:
376
    utils.Unlock('cmd')
377
    utils.LockCleanup()
378
  return retval
379

    
380

    
381
if __name__ == "__main__":
382
  main()