burnin 14.6 KB
Newer Older
Iustin Pop's avatar
Iustin Pop committed
1
2
3
#!/usr/bin/python
#

4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Copyright (C) 2006, 2007 Google Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.


"""Burnin program

"""
25

26
import os
Iustin Pop's avatar
Iustin Pop committed
27
28
import sys
import optparse
29
from itertools import izip, islice, cycle
Iustin Pop's avatar
Iustin Pop committed
30
from cStringIO import StringIO
Iustin Pop's avatar
Iustin Pop committed
31
32
33
34
35
36

from ganeti import opcodes
from ganeti import mcpu
from ganeti import constants
from ganeti import cli
from ganeti import logger
37
38
from ganeti import errors
from ganeti import utils
Iustin Pop's avatar
Iustin Pop committed
39

40

41
USAGE = ("\tburnin -o OS_NAME [options...] instance_name ...")
Iustin Pop's avatar
Iustin Pop committed
42

43

Iustin Pop's avatar
Iustin Pop committed
44
45
46
47
48
49
50
def Usage():
  """Shows program usage information and exits the program."""

  print >> sys.stderr, "Usage:"
  print >> sys.stderr, USAGE
  sys.exit(2)

51

Iustin Pop's avatar
Iustin Pop committed
52
def Log(msg):
53
54
55
56
  """Simple function that prints out its argument.

  """
  print msg
57
  sys.stdout.flush()
Iustin Pop's avatar
Iustin Pop committed
58

59

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

  def __init__(self):
    """Constructor."""
Iustin Pop's avatar
Iustin Pop committed
65
66
67
    logger.SetupLogging(debug=False, program="ganeti/burnin")
    self._feed_buf = StringIO()
    self.proc = mcpu.Processor(feedback=self.Feedback)
68
69
70
71
72
73
74
    self.nodes = []
    self.instances = []
    self.to_rem = []
    self.opts = None
    self.ParseOptions()
    self.GetState()

Iustin Pop's avatar
Iustin Pop committed
75
76
77
78
79
80
81
82
83
84
85
86
  def ClearFeedbackBuf(self):
    """Clear the feedback buffer."""
    self._feed_buf.truncate(0)

  def GetFeedbackBuf(self):
    """Return the contents of the buffer."""
    return self._feed_buf.getvalue()

  def Feedback(self, msg):
    """Acumulate feedback in our buffer."""
    self._feed_buf.write(msg)
    self._feed_buf.write("\n")
Iustin Pop's avatar
Iustin Pop committed
87
88
    if self.opts.verbose:
      Log(msg)
Iustin Pop's avatar
Iustin Pop committed
89
90
91
92
93
94

  def ExecOp(self, op):
    """Execute an opcode and manage the exec buffer."""
    self.ClearFeedbackBuf()
    return self.proc.ExecOpCode(op)

95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
  def ParseOptions(self):
    """Parses the command line options.

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

    """

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

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

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

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

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

167
168
169
170
171
172
173
174
175
176
177
    self.opts = options
    self.instances = args

  def GetState(self):
    """Read the cluster state from the config."""
    if self.opts.nodes:
      names = self.opts.nodes.split(",")
    else:
      names = []
    try:
      op = opcodes.OpQueryNodes(output_fields=["name"], names=names)
Iustin Pop's avatar
Iustin Pop committed
178
      result = self.ExecOp(op)
179
180
    except errors.GenericError, err:
      err_code, msg = cli.FormatError(err)
Iustin Pop's avatar
Iustin Pop committed
181
      Log(msg)
182
183
184
      sys.exit(err_code)
    self.nodes = [data[0] for data in result]

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

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

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

    if self.opts.os not in os_set:
Iustin Pop's avatar
Iustin Pop committed
196
      Log("OS '%s' not found" % self.opts.os)
197
198
199
200
201
202
203
204
205
206
207
      sys.exit(1)

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

    """
    self.to_rem = []
    mytor = izip(cycle(self.nodes),
                 islice(cycle(self.nodes), 1, None),
                 self.instances)
    for pnode, snode, instance in mytor:
208
209
      if self.opts.iallocator:
        pnode = snode = None
210
211
212
213
214
      op = opcodes.OpCreateInstance(instance_name=instance,
                                    mem_size=128,
                                    disk_size=self.opts.os_size,
                                    swap_size=self.opts.swap_size,
                                    disk_template=self.opts.disk_template,
Iustin Pop's avatar
Iustin Pop committed
215
                                    mode=constants.INSTANCE_CREATE,
216
217
218
219
                                    os_type=self.opts.os,
                                    pnode=pnode,
                                    snode=snode,
                                    vcpus=1,
Iustin Pop's avatar
Iustin Pop committed
220
                                    start=True,
221
                                    ip_check=True,
222
223
224
                                    wait_for_sync=True,
                                    mac="auto",
                                    kernel_path=None,
225
                                    initrd_path=None,
226
227
                                    hvm_boot_order=None,
                                    file_driver="loop",
228
229
                                    file_storage_dir=None,
                                    iallocator=self.opts.iallocator)
Iustin Pop's avatar
Iustin Pop committed
230
      Log("- Add instance %s on nodes %s/%s" % (instance, pnode, snode))
Iustin Pop's avatar
Iustin Pop committed
231
      self.ExecOp(op)
232
233
234
235
236
237
238
239
240
      self.to_rem.append(instance)

  def ReplaceDisks1D8(self):
    """Replace disks on primary and secondary for drbd8."""
    for instance in self.instances:
      for mode in constants.REPLACE_DISK_SEC, constants.REPLACE_DISK_PRI:
        op = opcodes.OpReplaceDisks(instance_name=instance,
                                    mode=mode,
                                    disks=["sda", "sdb"])
Iustin Pop's avatar
Iustin Pop committed
241
242
        Log("- Replace disks (%s) for instance %s" % (mode, instance))
        self.ExecOp(op)
243
244
245

  def ReplaceDisks2(self):
    """Replace secondary node."""
246
    mode = constants.REPLACE_DISK_SEC
247
248
249
250
251
252
253
254

    mytor = izip(islice(cycle(self.nodes), 2, None),
                 self.instances)
    for tnode, instance in mytor:
      op = opcodes.OpReplaceDisks(instance_name=instance,
                                  mode=mode,
                                  remote_node=tnode,
                                  disks=["sda", "sdb"])
Iustin Pop's avatar
Iustin Pop committed
255
256
      Log("- Replace secondary (%s) for instance %s" % (mode, instance))
      self.ExecOp(op)
257
258
259
260
261
262
263
264

  def Failover(self):
    """Failover the instances."""

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

Iustin Pop's avatar
Iustin Pop committed
265
266
      Log("- Failover instance %s" % (instance))
      self.ExecOp(op)
267

268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
  def ImportExport(self):
    """Export the instance, delete it, and import it back.

    """

    mytor = izip(cycle(self.nodes),
                 islice(cycle(self.nodes), 1, None),
                 islice(cycle(self.nodes), 2, None),
                 self.instances)

    for pnode, snode, enode, instance in mytor:
      exp_op = opcodes.OpExportInstance(instance_name=instance,
                                           target_node=enode,
                                           shutdown=True)
      rem_op = opcodes.OpRemoveInstance(instance_name=instance)
      nam_op = opcodes.OpQueryInstances(output_fields=["name"],
                                           names=[instance])
      full_name = self.ExecOp(nam_op)[0][0]
      imp_dir = os.path.join(constants.EXPORT_DIR, full_name)
      imp_op = opcodes.OpCreateInstance(instance_name=instance,
                                        mem_size=128,
                                        disk_size=self.opts.os_size,
                                        swap_size=self.opts.swap_size,
                                        disk_template=self.opts.disk_template,
                                        mode=constants.INSTANCE_IMPORT,
                                        src_node=enode,
                                        src_path=imp_dir,
                                        pnode=pnode,
                                        snode=snode,
                                        vcpus=1,
                                        start=True,
                                        ip_check=True,
                                        wait_for_sync=True,
                                        mac="auto")
Guido Trotter's avatar
Guido Trotter committed
302
      erem_op = opcodes.OpRemoveExport(instance_name=instance)
303
304
305
306
307
308
309
310
311

      Log("- Export instance %s to node %s" % (instance, enode))
      self.ExecOp(exp_op)
      Log("- Remove instance %s" % (instance))
      self.ExecOp(rem_op)
      self.to_rem.remove(instance)
      Log("- Import instance %s from node %s to node %s" %
          (instance, enode, pnode))
      self.ExecOp(imp_op)
Guido Trotter's avatar
Guido Trotter committed
312
313
314
      Log("- Remove export of instance %s" % (instance))
      self.ExecOp(erem_op)

315
316
      self.to_rem.append(instance)

317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
  def StopInstance(self, instance):
    """Stop given instance."""
    op = opcodes.OpShutdownInstance(instance_name=instance)
    Log("- Shutdown instance %s" % instance)
    self.ExecOp(op)

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

  def RenameInstance(self, instance, instance_new):
    """Rename instance."""
    op = opcodes.OpRenameInstance(instance_name=instance,
                                  new_name=instance_new)
    Log("- Rename instance %s to %s" % (instance, instance_new))
    self.ExecOp(op)

336
337
338
  def StopStart(self):
    """Stop/start the instances."""
    for instance in self.instances:
339
340
      self.StopInstance(instance)
      self.StartInstance(instance)
341
342
343
344
345

  def Remove(self):
    """Remove the instances."""
    for instance in self.to_rem:
      op = opcodes.OpRemoveInstance(instance_name=instance)
Iustin Pop's avatar
Iustin Pop committed
346
347
      Log("- Remove instance %s" % instance)
      self.ExecOp(op)
348

349
350
351
352
353
354
355
356
357
358
359
360

  def Rename(self):
    """Rename the instances."""
    rename = self.opts.rename
    for instance in self.instances:
      self.StopInstance(instance)
      self.RenameInstance(instance, rename)
      self.StartInstance(rename)
      self.StopInstance(rename)
      self.RenameInstance(rename, instance)
      self.StartInstance(instance)

361
362
363
364
365
366
367
368
369
370
  def BurninCluster(self):
    """Test a cluster intensively.

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

    """

    opts = self.opts

Iustin Pop's avatar
Iustin Pop committed
371
    Log("- Testing global parameters")
372

373
    if (len(self.nodes) == 1 and
374
375
        opts.disk_template not in (constants.DT_DISKLESS, constants.DT_PLAIN,
                                   constants.DT_FILE)):
Iustin Pop's avatar
Iustin Pop committed
376
      Log("When one node is available/selected the disk template must"
377
          " be 'diskless', 'file' or 'plain'")
378
379
      sys.exit(1)

Iustin Pop's avatar
Iustin Pop committed
380
    has_err = True
381
382
383
    try:
      self.CreateInstances()
      if opts.do_replace1 and opts.disk_template in constants.DTS_NET_MIRROR:
384
        self.ReplaceDisks1D8()
385
386
387
388
389
390
391
      if (opts.do_replace2 and len(self.nodes) > 2 and
          opts.disk_template in constants.DTS_NET_MIRROR) :
        self.ReplaceDisks2()

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

392
393
394
      if opts.do_importexport:
        self.ImportExport()

395
396
397
      if opts.do_startstop:
        self.StopStart()

398
399
400
      if opts.rename:
        self.Rename()

Iustin Pop's avatar
Iustin Pop committed
401
      has_err = False
402
    finally:
Iustin Pop's avatar
Iustin Pop committed
403
404
405
406
      if has_err:
        Log("Error detected: opcode buffer follows:\n\n")
        Log(self.GetFeedbackBuf())
        Log("\n\n")
407
408
409
      self.Remove()

    return 0
Iustin Pop's avatar
Iustin Pop committed
410

411

Iustin Pop's avatar
Iustin Pop committed
412
def main():
413
414
  """Main function"""

415
  burner = Burner()
416
417
418
419
420
421
  try:
    utils.Lock('cmd', max_retries=15, debug=True)
  except errors.LockError, err:
    logger.ToStderr(str(err))
    return 1
  try:
422
    retval = burner.BurninCluster()
423
424
425
426
  finally:
    utils.Unlock('cmd')
    utils.LockCleanup()
  return retval
Iustin Pop's avatar
Iustin Pop committed
427

428

Iustin Pop's avatar
Iustin Pop committed
429
if __name__ == "__main__":
430
  main()