diff --git a/tools/burnin b/tools/burnin index eed35559cb8364b14ef1ff6fd18b4c9805b42099..5a15b443975f4e38d4a08726cddebee48df0eb9f 100755 --- a/tools/burnin +++ b/tools/burnin @@ -1,12 +1,14 @@ #!/usr/bin/python # +"""Burnin program""" + import sys import optparse +from itertools import izip, islice, cycle from ganeti import opcodes from ganeti import mcpu -from ganeti import objects from ganeti import constants from ganeti import cli from ganeti import logger @@ -22,202 +24,262 @@ def Usage(): print >> sys.stderr, USAGE sys.exit(2) - def Feedback(msg): """Simple function that prints out its argument. """ print msg - -def ParseOptions(): - """Parses the command line options. - - In case of command line errors, it will show the usage and exit the - program. - - Returns: - (options, args), as returned by OptionParser.parse_args - """ - - 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="Do disk replacement with the same secondary", - action="store_false", default=True) - parser.add_option("--no-replace2", dest="do_replace2", - help="Do disk replacement with a different secondary", - action="store_false", default=True) - parser.add_option("--no-failover", dest="do_failover", - help="Do instance failovers", action="store_false", - default=True) - parser.add_option("-t", "--disk-template", dest="disk_template", - choices=("remote_raid1", "drbd8"), default="remote_raid1", - help="Template type for network mirroring (remote_raid1" - " or drbd8) [remote_raid1]") - parser.add_option("-n", "--nodes", dest="nodes", default="", - help="Comma separated list of nodes to perform the burnin" - " on (defaults to all nodes)") - - options, args = parser.parse_args() - if len(args) < 1 or options.os is None: - Usage() - - return options, args - - -def BurninCluster(opts, args): - """Test a cluster intensively. - - This will create instances and then start/stop/failover them. - It is safe for existing instances but could impact performance. - - """ - - logger.SetupLogging(debug=True, program="ganeti/burnin") - proc = mcpu.Processor(feedback=Feedback) - if opts.nodes: - names = opts.nodes.split(",") - else: - names = [] - try: - result = proc.ExecOpCode(opcodes.OpQueryNodes(output_fields=["name"], - names=names)) - except errors.GenericError, err: - err_code, msg = cli.FormatError(err) - Feedback(msg) - return err_code - nodelist = [data[0] for data in result] - - Feedback("- Testing global parameters") - - result = proc.ExecOpCode(opcodes.OpDiagnoseOS()) - - if not result: - Feedback("Can't get the OS list") - return 1 - - # filter non-valid OS-es - oses = {} - for node_name in result: - oses[node_name] = [obj for obj in result[node_name] - if isinstance(obj, objects.OS)] - - fnode = oses.keys()[0] - os_set = set([os_inst.name for os_inst in oses[fnode]]) - del oses[fnode] - for node in oses: - os_set &= set([os_inst.name for os_inst in oses[node]]) - - if opts.os not in os_set: - Feedback("OS '%s' not found" % opts.os) - return 1 - - to_remove = [] - if opts.disk_template == "remote_raid1": - disk_template = constants.DT_REMOTE_RAID1 - elif opts.disk_template == "drbd8": - disk_template = constants.DT_DRBD8 - else: - Feedback("Unknown disk template '%s'" % opts.disk_template) - return 1 - try: - idx = 0 - for instance_name in args: - next_idx = idx + 1 - if next_idx >= len(nodelist): - next_idx = 0 - pnode = nodelist[idx] - snode = nodelist[next_idx] - if len(nodelist) > 1: - tplate = disk_template - else: - tplate = constants.DT_PLAIN - - op = opcodes.OpCreateInstance(instance_name=instance_name, mem_size=128, - disk_size=opts.os_size, - swap_size=opts.swap_size, - disk_template=tplate, +class Burner(object): + """Burner class.""" + + def __init__(self): + """Constructor.""" + logger.SetupLogging(debug=True, program="ganeti/burnin") + self.proc = mcpu.Processor(feedback=Feedback) + self.nodes = [] + self.instances = [] + self.to_rem = [] + self.opts = None + self.ParseOptions() + self.GetState() + + 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) + parser.add_option("-t", "--disk-template", dest="disk_template", + choices=("remote_raid1", "drbd8"), + default="remote_raid1", + help="Template type for network mirroring (remote_raid1" + " or drbd8) [remote_raid1]") + parser.add_option("-n", "--nodes", dest="nodes", default="", + help="Comma separated list of nodes to perform" + " the burnin on (defaults to all nodes)") + + options, args = parser.parse_args() + if len(args) < 1 or options.os is None: + Usage() + + if options.disk_template == "plain": + disk_template = constants.DT_PLAIN + elif options.disk_template == "remote_raid1": + disk_template = constants.DT_REMOTE_RAID1 + elif options.disk_template == "drbd8": + disk_template = constants.DT_DRBD8 + else: + Feedback("Unknown disk template '%s'" % options.disk_template) + sys.exit(1) + + options.disk_template = disk_template + 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) + result = self.proc.ExecOpCode(op) + except errors.GenericError, err: + err_code, msg = cli.FormatError(err) + Feedback(msg) + sys.exit(err_code) + self.nodes = [data[0] for data in result] + + result = self.proc.ExecOpCode(opcodes.OpDiagnoseOS()) + + if not result: + Feedback("Can't get the OS list") + sys.exit(1) + + # filter non-valid OS-es + oses = {} + for node_name in result: + oses[node_name] = [obj for obj in result[node_name] if obj] + + fnode = oses.keys()[0] + os_set = set([os_inst.name for os_inst in oses[fnode]]) + del oses[fnode] + for node in oses: + os_set &= set([os_inst.name for os_inst in oses[node]]) + + if self.opts.os not in os_set: + Feedback("OS '%s' not found" % self.opts.os) + 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: + 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_CREATE, - os_type=opts.os, pnode=pnode, - snode=snode, vcpus=1, + os_type=self.opts.os, + pnode=pnode, + snode=snode, + vcpus=1, start=True, ip_check=True, wait_for_sync=True) - Feedback("- Add instance %s on node %s" % (instance_name, pnode)) - result = proc.ExecOpCode(op) - to_remove.append(instance_name) - idx = next_idx - - - if opts.do_replace1: - if len(nodelist) > 1: - # failover - for instance_name in args: - op = opcodes.OpReplaceDisks(instance_name=instance_name, - remote_node=None, - mode=constants.REPLACE_DISK_ALL, - disks=["sda", "sdb"]) - - Feedback("- Replace disks for instance %s" % (instance_name)) - result = proc.ExecOpCode(op) - else: - Feedback("- Can't run replace1, not enough nodes") - - if opts.do_failover: - if len(nodelist) > 1: - # failover - for instance_name in args: - op = opcodes.OpFailoverInstance(instance_name=instance_name, - ignore_consistency=True) - - Feedback("- Failover instance %s" % (instance_name)) - result = proc.ExecOpCode(op) - else: - Feedback("- Can't run failovers, not enough nodes") - - # stop / start - for instance_name in args: - op = opcodes.OpShutdownInstance(instance_name=instance_name) - Feedback("- Shutdown instance %s" % instance_name) - result = proc.ExecOpCode(op) - op = opcodes.OpStartupInstance(instance_name=instance_name, force=False) - Feedback("- Start instance %s" % instance_name) - result = proc.ExecOpCode(op) - - finally: - # remove - for instance_name in to_remove: - op = opcodes.OpRemoveInstance(instance_name=instance_name) - Feedback("- Remove instance %s" % instance_name) - result = proc.ExecOpCode(op) - - return 0 + Feedback("- Add instance %s on node %s" % (instance, pnode)) + self.proc.ExecOpCode(op) + self.to_rem.append(instance) + + def ReplaceDisks1R1(self): + """Replace disks with the same secondary for rr1.""" + # replace all, both disks + for instance in self.instances: + op = opcodes.OpReplaceDisks(instance_name=instance, + remote_node=None, + mode=constants.REPLACE_DISK_ALL, + disks=["sda", "sdb"]) + + Feedback("- Replace disks for instance %s" % (instance)) + self.proc.ExecOpCode(op) + + 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"]) + Feedback("- Replace disks (%s) for instance %s" % (mode, instance)) + self.proc.ExecOpCode(op) + + def ReplaceDisks2(self): + """Replace secondary node.""" + if self.opts.disk_template == constants.DT_REMOTE_RAID1: + mode = constants.REPLACE_DISK_ALL + else: + mode = constants.REPLACE_DISK_SEC + + 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"]) + Feedback("- Replace secondary (%s) for instance %s" % (mode, instance)) + self.proc.ExecOpCode(op) + + def Failover(self): + """Failover the instances.""" + + for instance in self.instances: + op = opcodes.OpFailoverInstance(instance_name=instance, + ignore_consistency=False) + + Feedback("- Failover instance %s" % (instance)) + self.proc.ExecOpCode(op) + + def StopStart(self): + """Stop/start the instances.""" + for instance in self.instances: + op = opcodes.OpShutdownInstance(instance_name=instance) + Feedback("- Shutdown instance %s" % instance) + self.proc.ExecOpCode(op) + op = opcodes.OpStartupInstance(instance_name=instance, force=False) + Feedback("- Start instance %s" % instance) + self.proc.ExecOpCode(op) + + def Remove(self): + """Remove the instances.""" + for instance in self.to_rem: + op = opcodes.OpRemoveInstance(instance_name=instance) + Feedback("- Remove instance %s" % instance) + self.proc.ExecOpCode(op) + + 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 + + Feedback("- Testing global parameters") + + if len(self.nodes) == 1 and opts.disk_template != constants.DT_PLAIN: + Feedback("When one node is available/selected the disk template must" + " be 'plain'") + sys.exit(1) + + try: + self.CreateInstances() + if opts.do_replace1 and opts.disk_template in constants.DTS_NET_MIRROR: + if opts.disk_template == constants.DT_REMOTE_RAID1: + self.ReplaceDisks1R1() + elif opts.disk_template == constants.DT_DRBD8: + self.ReplaceDisks1D8() + 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() + + self.StopStart() + + finally: + self.Remove() + + return 0 def main(): """Main function""" - opts, args = ParseOptions() + burner = Burner() try: utils.Lock('cmd', max_retries=15, debug=True) except errors.LockError, err: logger.ToStderr(str(err)) return 1 try: - retval = BurninCluster(opts, args) + retval = burner.BurninCluster() finally: utils.Unlock('cmd') utils.LockCleanup()