diff --git a/Makefile.am b/Makefile.am index 89fbaca6fd99e9d93ace14ad8d0134bff8e8c99c..67f453bdcae82f7a88da209b140fa6b282346839 100644 --- a/Makefile.am +++ b/Makefile.am @@ -95,6 +95,7 @@ pkgpython_PYTHON = \ lib/bootstrap.py \ lib/cli.py \ lib/cmdlib.py \ + lib/compat.py \ lib/config.py \ lib/constants.py \ lib/daemon.py \ @@ -335,6 +336,7 @@ python_tests = \ test/ganeti.bdev_unittest.py \ test/ganeti.cli_unittest.py \ test/ganeti.cmdlib_unittest.py \ + test/ganeti.compat_unittest.py \ test/ganeti.confd.client_unittest.py \ test/ganeti.config_unittest.py \ test/ganeti.constants_unittest.py \ @@ -582,4 +584,6 @@ coverage: $(BUILT_SOURCES) $(python_tests) $(PLAIN_TESTS_ENVIRONMENT) $(abs_top_srcdir)/autotools/gen-coverage \ $(python_tests) +commit-check: distcheck lint apidoc + # vim: set noet : diff --git a/lib/bdev.py b/lib/bdev.py index 5783a211e6d5c1bdde72915d2d4fef8390a4eacd..5469283ccc3ff3ab224697010eef7f2bbc504088 100644 --- a/lib/bdev.py +++ b/lib/bdev.py @@ -32,6 +32,7 @@ from ganeti import utils from ganeti import errors from ganeti import constants from ganeti import objects +from ganeti import compat # Size of reads in _CanReadDevice @@ -388,7 +389,7 @@ class LogicalVolume(BlockDev): pvs_info.reverse() pvlist = [ pv[1] for pv in pvs_info ] - if utils.any(pvlist, lambda v: ":" in v): + if compat.any(pvlist, lambda v: ":" in v): _ThrowError("Some of your PVs have the invalid character ':' in their" " name, this is not supported - please filter them out" " in lvm.conf using either 'filter' or 'preferred_names'") @@ -462,7 +463,7 @@ class LogicalVolume(BlockDev): """ if (not cls._VALID_NAME_RE.match(name) or name in cls._INVALID_NAMES or - utils.any(cls._INVALID_SUBSTRINGS, lambda x: x in name)): + compat.any(cls._INVALID_SUBSTRINGS, lambda x: x in name)): _ThrowError("Invalid LVM name '%s'", name) def Remove(self): diff --git a/lib/cli.py b/lib/cli.py index 9a40c48c471ff410136ee393b062948a86a30a69..b7722e20955b93196ed782f19fc9602d1e6679d8 100644 --- a/lib/cli.py +++ b/lib/cli.py @@ -37,6 +37,7 @@ from ganeti import luxi from ganeti import ssconf from ganeti import rpc from ganeti import ssh +from ganeti import compat from optparse import (OptionParser, TitledHelpFormatter, Option, OptionValueError) @@ -2143,7 +2144,7 @@ class JobExecutor(object): ToStdout("Submitted jobs %s", utils.CommaJoin(ok_jobs)) # first, remove any non-submitted jobs - self.jobs, failures = utils.partition(self.jobs, lambda x: x[1]) + self.jobs, failures = compat.partition(self.jobs, lambda x: x[1]) for idx, _, jid, name in failures: ToStderr("Failed to submit job for %s: %s", name, jid) results.append((idx, False, jid)) diff --git a/lib/cmdlib.py b/lib/cmdlib.py index 2945c9ef0323d8527b2ce13c4a904bace6a7e143..506c49a5c0c109448585273d3787f252cc35996e 100644 --- a/lib/cmdlib.py +++ b/lib/cmdlib.py @@ -2255,8 +2255,11 @@ class LUSetClusterParams(LogicalUnit): """Check parameters """ - if not hasattr(self.op, "candidate_pool_size"): - self.op.candidate_pool_size = None + for attr in ["candidate_pool_size", + "uid_pool", "add_uids", "remove_uids"]: + if not hasattr(self.op, attr): + setattr(self.op, attr, None) + if self.op.candidate_pool_size is not None: try: self.op.candidate_pool_size = int(self.op.candidate_pool_size) diff --git a/lib/compat.py b/lib/compat.py new file mode 100644 index 0000000000000000000000000000000000000000..692ab246f9845ec7f48379049e2e835a8b325c2d --- /dev/null +++ b/lib/compat.py @@ -0,0 +1,88 @@ +# +# + +# Copyright (C) 2010 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. + + +"""Module containing backported language/library functionality. + +""" + +import itertools + +try: + import functools +except ImportError: + functools = None + + +def all(seq, pred=bool): # pylint: disable-msg=W0622 + """Returns True if pred(x) is True for every element in the iterable. + + Please note that this function provides a C{pred} parameter which isn't + available in the version included in Python 2.5 and above. + + """ + for _ in itertools.ifilterfalse(pred, seq): + return False + return True + + +def any(seq, pred=bool): # pylint: disable-msg=W0622 + """Returns True if pred(x) is True for at least one element in the iterable. + + Please note that this function provides a C{pred} parameter which isn't + available in the version included in Python 2.5 and above. + + """ + for _ in itertools.ifilter(pred, seq): + return True + return False + + +def partition(seq, pred=bool): # pylint: disable-msg=W0622 + """Partition a list in two, based on the given predicate. + + """ + return (list(itertools.ifilter(pred, seq)), + list(itertools.ifilterfalse(pred, seq))) + + +# Even though we're using Python's built-in "partial" function if available, +# this one is always defined for testing. +def _partial(func, *args, **keywords): # pylint: disable-msg=W0622 + """Decorator with partial application of arguments and keywords. + + This function was copied from Python's documentation. + + """ + def newfunc(*fargs, **fkeywords): + newkeywords = keywords.copy() + newkeywords.update(fkeywords) + return func(*(args + fargs), **newkeywords) # pylint: disable-msg=W0142 + + newfunc.func = func + newfunc.args = args + newfunc.keywords = keywords + return newfunc + + +if functools is None: + partial = _partial +else: + partial = functools.partial diff --git a/lib/confd/client.py b/lib/confd/client.py index 9bd9ef7fc692194b0e782bbf676a43519ba50b07..e2e0a13b80a46f86bfbe3bec8e6b3cffbdd8a892 100644 --- a/lib/confd/client.py +++ b/lib/confd/client.py @@ -61,6 +61,7 @@ from ganeti import daemon # contains AsyncUDPSocket from ganeti import errors from ganeti import confd from ganeti import ssconf +from ganeti import compat class ConfdAsyncUDPClient(daemon.AsyncUDPSocket): @@ -581,7 +582,7 @@ class ConfdCountingCallback: """Have all the registered queries received at least an answer? """ - return utils.all(self._answers.values()) + return compat.all(self._answers.values()) def _HandleExpire(self, up): # if we have no answer we have received none, before the expiration. diff --git a/lib/hypervisor/hv_base.py b/lib/hypervisor/hv_base.py index 58db39599a3165d22317ed0f64445c642cb1df76..1465da2401a570e4232604403e295acca1d4dc45 100644 --- a/lib/hypervisor/hv_base.py +++ b/lib/hypervisor/hv_base.py @@ -101,10 +101,14 @@ class BaseHypervisor(object): - a function to check for parameter validity on the remote node, in the L{ValidateParameters} function - an error message for the above function + @type CAN_MIGRATE: boolean + @cvar CAN_MIGRATE: whether this hypervisor can do migration (either + live or non-live) """ PARAMETERS = {} ANCILLARY_FILES = [] + CAN_MIGRATE = False def __init__(self): pass diff --git a/lib/hypervisor/hv_chroot.py b/lib/hypervisor/hv_chroot.py index 36b37a8eb1261f9e13c2133dc653d147cad7f7f4..c495c9e4dbb30dacd181b9ad7832338cadaf461b 100644 --- a/lib/hypervisor/hv_chroot.py +++ b/lib/hypervisor/hv_chroot.py @@ -212,6 +212,13 @@ class ChrootManager(hv_base.BaseHypervisor): raise HypervisorError("Can't stop the processes using the chroot") return + def CleanupInstance(self, instance_name): + """Cleanup after a stopped instance + + """ + if self._IsDirLive(root_dir): + raise HypervisorError("Processes are still using the chroot") + for mpath in self._GetMountSubdirs(root_dir): utils.RunCmd(["umount", mpath]) diff --git a/lib/hypervisor/hv_fake.py b/lib/hypervisor/hv_fake.py index cea05d61ef7c2cdd8e629915cdf4d5e3b26c5b27..a97bebe299c502406e692cc712108b041882d9c6 100644 --- a/lib/hypervisor/hv_fake.py +++ b/lib/hypervisor/hv_fake.py @@ -40,6 +40,8 @@ class FakeHypervisor(hv_base.BaseHypervisor): a real virtualisation software installed. """ + CAN_MIGRATE = True + _ROOT_DIR = constants.RUN_DIR + "/ganeti-fake-hypervisor" def __init__(self): diff --git a/lib/hypervisor/hv_kvm.py b/lib/hypervisor/hv_kvm.py index 6b7483338422d9a587162bf8a39568577fae23ef..13c09ddce547c8bfb9da5f096075a97d58641afb 100644 --- a/lib/hypervisor/hv_kvm.py +++ b/lib/hypervisor/hv_kvm.py @@ -44,6 +44,7 @@ from ganeti.hypervisor import hv_base class KVMHypervisor(hv_base.BaseHypervisor): """KVM hypervisor interface""" + CAN_MIGRATE = True _ROOT_DIR = constants.RUN_GANETI_DIR + "/kvm-hypervisor" _PIDS_DIR = _ROOT_DIR + "/pid" # contains live instances pids @@ -235,13 +236,12 @@ class KVMHypervisor(hv_base.BaseHypervisor): if os.path.exists(uid_file): try: uid = int(utils.ReadFile(uid_file)) + return uid except EnvironmentError: logging.warning("Can't read uid file", exc_info=True) - return None except (TypeError, ValueError): logging.warning("Can't parse uid file contents", exc_info=True) - return None - return uid + return None @classmethod def _RemoveInstanceRuntimeFiles(cls, pidfile, instance_name): @@ -425,7 +425,10 @@ class KVMHypervisor(hv_base.BaseHypervisor): # TODO: handle FD_LOOP and FD_BLKTAP (?) if boot_disk: kvm_cmd.extend(['-boot', 'c']) - boot_val = ',boot=on' + if disk_type != constants.HT_DISK_IDE: + boot_val = ',boot=on' + else: + boot_val = '' # We only boot from the first disk boot_disk = False else: @@ -440,9 +443,14 @@ class KVMHypervisor(hv_base.BaseHypervisor): options = ',format=raw,media=cdrom' if boot_cdrom: kvm_cmd.extend(['-boot', 'd']) - options = '%s,boot=on' % options + if disk_type != constants.HT_DISK_IDE: + options = '%s,boot=on' % options else: - options = '%s,if=virtio' % options + if disk_type == constants.HT_DISK_PARAVIRTUAL: + if_val = ',if=virtio' + else: + if_val = ',if=%s' % disk_type + options = '%s%s' % (options, if_val) drive_val = 'file=%s%s' % (iso_image, options) kvm_cmd.extend(['-drive', drive_val]) @@ -498,6 +506,10 @@ class KVMHypervisor(hv_base.BaseHypervisor): vnc_arg = 'unix:%s/%s.vnc' % (vnc_bind_address, instance.name) kvm_cmd.extend(['-vnc', vnc_arg]) + + # Also add a tablet USB device to act as a mouse + # This solves various mouse alignment issues + kvm_cmd.extend(['-usbdevice', 'tablet']) else: kvm_cmd.extend(['-nographic']) diff --git a/lib/hypervisor/hv_xen.py b/lib/hypervisor/hv_xen.py index 575e3b9404e990bd1de7589c1da3939ba8958897..a281e6a9b41d9434162a10847bfe010494b2e8f7 100644 --- a/lib/hypervisor/hv_xen.py +++ b/lib/hypervisor/hv_xen.py @@ -39,6 +39,7 @@ class XenHypervisor(hv_base.BaseHypervisor): all the functionality that is identical for both. """ + CAN_MIGRATE = True REBOOT_RETRY_COUNT = 60 REBOOT_RETRY_INTERVAL = 10 diff --git a/lib/locking.py b/lib/locking.py index aeafd3f7911db3830ff4b22c9d945fcd0185a067..3e25917c40f6e7388017e5a848f8f72933edb8ac 100644 --- a/lib/locking.py +++ b/lib/locking.py @@ -33,6 +33,7 @@ import errno from ganeti import errors from ganeti import utils +from ganeti import compat def ssynchronized(lock, shared=0): @@ -1216,7 +1217,7 @@ class GanetiLockManager: """ # This way of checking only works if LEVELS[i] = i, which we check for in # the test cases. - return utils.any((self._is_owned(l) for l in LEVELS[level + 1:])) + return compat.any((self._is_owned(l) for l in LEVELS[level + 1:])) def _BGL_owned(self): # pylint: disable-msg=C0103 """Check if the current thread owns the BGL. diff --git a/lib/rapi/client.py b/lib/rapi/client.py index 5a1b555a965735e19a2f93f52adaf419bed73815..b1f3d104538b8b1561544ecbb6762feb6fef916f 100644 --- a/lib/rapi/client.py +++ b/lib/rapi/client.py @@ -137,7 +137,7 @@ class GanetiRapiClient(object): @param query: query arguments to pass to urllib.urlencode @type prepend_version: bool @param prepend_version: whether to automatically fetch and prepend the - Ganeti version to the URL path + Ganeti RAPI version to the URL path @rtype: str @return: URL path @@ -171,7 +171,7 @@ class GanetiRapiClient(object): @param content: HTTP body content @type prepend_version: bool @param prepend_version: whether to automatically fetch and prepend the - Ganeti version to the URL path + Ganeti RAPI version to the URL path @rtype: str @return: JSON-Decoded response @@ -213,7 +213,7 @@ class GanetiRapiClient(object): return self._version def GetVersion(self): - """Gets the ganeti version running on the cluster. + """Gets the Remote API version running on the cluster. @rtype: int @return: Ganeti version @@ -266,7 +266,7 @@ class GanetiRapiClient(object): if dry_run: query.append(("dry-run", 1)) - self._SendRequest(HTTP_PUT, "/tags", query) + return self._SendRequest(HTTP_PUT, "/tags", query) def DeleteClusterTags(self, tags, dry_run=False): """Deletes tags from the cluster. @@ -339,12 +339,15 @@ class GanetiRapiClient(object): @type instance: str @param instance: the instance to delete + @rtype: int + @return: job id + """ query = [] if dry_run: query.append(("dry-run", 1)) - self._SendRequest(HTTP_DELETE, "/instances/%s" % instance, query) + return self._SendRequest(HTTP_DELETE, "/instances/%s" % instance, query) def GetInstanceTags(self, instance): """Gets tags for an instance. @@ -376,7 +379,7 @@ class GanetiRapiClient(object): if dry_run: query.append(("dry-run", 1)) - self._SendRequest(HTTP_PUT, "/instances/%s/tags" % instance, query) + return self._SendRequest(HTTP_PUT, "/instances/%s/tags" % instance, query) def DeleteInstanceTags(self, instance, tags, dry_run=False): """Deletes tags from an instance. diff --git a/lib/ssh.py b/lib/ssh.py index b3116d3a09aded5fda42473ead72a96bc915a091..ba4c3eb33ac9888e281e53165d4600b8f4cbd1f6 100644 --- a/lib/ssh.py +++ b/lib/ssh.py @@ -95,6 +95,7 @@ class SshRunner: "-oHashKnownHosts=no", "-oGlobalKnownHostsFile=%s" % constants.SSH_KNOWN_HOSTS_FILE, "-oUserKnownHostsFile=/dev/null", + "-oCheckHostIp=no", ] if use_cluster_key: diff --git a/lib/utils.py b/lib/utils.py index 48b427ac35be424deda322ab59092d1da58ee9bb..32ebd875b21dce356d98570cefb137ed799ae509 100644 --- a/lib/utils.py +++ b/lib/utils.py @@ -1781,20 +1781,6 @@ def FirstFree(seq, base=0): return None -def all(seq, pred=bool): # pylint: disable-msg=W0622 - "Returns True if pred(x) is True for every element in the iterable" - for _ in itertools.ifilterfalse(pred, seq): - return False - return True - - -def any(seq, pred=bool): # pylint: disable-msg=W0622 - "Returns True if pred(x) is True for at least one element in the iterable" - for _ in itertools.ifilter(pred, seq): - return True - return False - - def SingleWaitForFdCondition(fdobj, event, timeout): """Waits for a condition to occur on the socket. @@ -1886,12 +1872,6 @@ def WaitForFdCondition(fdobj, event, timeout): return result -def partition(seq, pred=bool): # # pylint: disable-msg=W0622 - "Partition a list in two, based on the given predicate" - return (list(itertools.ifilter(pred, seq)), - list(itertools.ifilterfalse(pred, seq))) - - def UniqueSequence(seq): """Returns a list with unique elements. diff --git a/scripts/gnt-cluster b/scripts/gnt-cluster index 8a3ba82c065d2f992aae4e2071dec0dd6460c1ed..4bde5b91f3affb1ce78b368e5499173d473c013c 100755 --- a/scripts/gnt-cluster +++ b/scripts/gnt-cluster @@ -40,6 +40,7 @@ from ganeti import bootstrap from ganeti import ssh from ganeti import objects from ganeti import uidpool +from ganeti import compat @UsesRPC @@ -423,7 +424,7 @@ def VerifyDisks(opts, args): if missing: for iname, ival in missing.iteritems(): - all_missing = utils.all(ival, lambda x: x[0] in bad_nodes) + all_missing = compat.all(ival, lambda x: x[0] in bad_nodes) if all_missing: ToStdout("Instance %s cannot be verified as it lives on" " broken nodes", iname) diff --git a/scripts/gnt-instance b/scripts/gnt-instance index 4f6ddc8040099385c673aaf56debd9ba9adc8016..3dfcd344a0cc059b03f97358cc54cceae6d39b96 100755 --- a/scripts/gnt-instance +++ b/scripts/gnt-instance @@ -1175,7 +1175,7 @@ def ShowInstanceConfig(opts, args): vnc_bind_address) buf.write(" - console connection: vnc to %s\n" % vnc_console_port) - for key in instance["hv_actual"]: + for key in sorted(instance["hv_actual"]): if key in instance["hv_instance"]: val = instance["hv_instance"][key] else: diff --git a/test/ganeti.compat_unittest.py b/test/ganeti.compat_unittest.py new file mode 100755 index 0000000000000000000000000000000000000000..19f82b7a8f26b8d225aea2c3dc8364afcc630719 --- /dev/null +++ b/test/ganeti.compat_unittest.py @@ -0,0 +1,62 @@ +#!/usr/bin/python +# + +# Copyright (C) 2010 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. + + +"""Script for unittesting the compat module""" + +import unittest + +from ganeti import compat + +import testutils + + +class TestPartial(testutils.GanetiTestCase): + def test(self): + # Test standard version + self._Test(compat.partial) + + # Test our version + self._Test(compat._partial) + + def _Test(self, fn): + def _TestFunc1(x, power=2): + return x ** power + + cubic = fn(_TestFunc1, power=3) + self.assertEqual(cubic(1), 1) + self.assertEqual(cubic(3), 27) + self.assertEqual(cubic(4), 64) + + def _TestFunc2(*args, **kwargs): + return (args, kwargs) + + self.assertEqualValues(fn(_TestFunc2, "Hello", "World")("Foo"), + (("Hello", "World", "Foo"), {})) + + self.assertEqualValues(fn(_TestFunc2, "Hello", xyz=123)("Foo"), + (("Hello", "Foo"), {"xyz": 123})) + + self.assertEqualValues(fn(_TestFunc2, xyz=123)("Foo", xyz=999), + (("Foo", ), {"xyz": 999,})) + + +if __name__ == "__main__": + testutils.GanetiTestProgram() diff --git a/test/ganeti.rapi.client_unittest.py b/test/ganeti.rapi.client_unittest.py index 66f5fbbd19600c82a1109ce46b72532353f92d12..d7ccf54e76f989fff6b341f63bd3cfda39e194b3 100755 --- a/test/ganeti.rapi.client_unittest.py +++ b/test/ganeti.rapi.client_unittest.py @@ -192,7 +192,9 @@ class GanetiRapiClientTests(unittest.TestCase): self.assertHandler(rlib2.R_2_tags) def testAddClusterTags(self): - self.client.AddClusterTags(["awesome"], dry_run=True) + self.rapi.AddResponse("1234") + self.assertEqual(1234, + self.client.AddClusterTags(["awesome"], dry_run=True)) self.assertHandler(rlib2.R_2_tags) self.assertDryRun() self.assertQuery("tag", ["awesome"]) @@ -227,7 +229,8 @@ class GanetiRapiClientTests(unittest.TestCase): self.assertDryRun() def testDeleteInstance(self): - self.client.DeleteInstance("instance", dry_run=True) + self.rapi.AddResponse("1234") + self.assertEqual(1234, self.client.DeleteInstance("instance", dry_run=True)) self.assertHandler(rlib2.R_2_instances_name) self.assertItems(["instance"]) self.assertDryRun() @@ -239,7 +242,9 @@ class GanetiRapiClientTests(unittest.TestCase): self.assertItems(["fooinstance"]) def testAddInstanceTags(self): - self.client.AddInstanceTags("fooinstance", ["awesome"], dry_run=True) + self.rapi.AddResponse("1234") + self.assertEqual(1234, + self.client.AddInstanceTags("fooinstance", ["awesome"], dry_run=True)) self.assertHandler(rlib2.R_2_instances_name_tags) self.assertItems(["fooinstance"]) self.assertDryRun() @@ -425,7 +430,9 @@ class GanetiRapiClientTests(unittest.TestCase): self.assertItems(["node-k"]) def testAddNodeTags(self): - self.client.AddNodeTags("node-v", ["awesome"], dry_run=True) + self.rapi.AddResponse("1234") + self.assertEqual(1234, + self.client.AddNodeTags("node-v", ["awesome"], dry_run=True)) self.assertHandler(rlib2.R_2_nodes_name_tags) self.assertItems(["node-v"]) self.assertDryRun() diff --git a/tools/burnin b/tools/burnin index a1dfc91880617beae752ddc6459eb72384dd2e9c..17e58500105a558eac84964b9af246346bb5ec15 100755 --- a/tools/burnin +++ b/tools/burnin @@ -36,6 +36,8 @@ from ganeti import constants from ganeti import cli from ganeti import errors from ganeti import utils +from ganeti import hypervisor +from ganeti import compat from ganeti.confd import client as confd_client @@ -111,6 +113,7 @@ OPTIONS = [ help="OS to use during burnin", metavar="<OS>", completion_suggest=cli.OPT_COMPL_ONE_OS), + cli.HYPERVISOR_OPT, cli.cli_option("--disk-size", dest="disk_size", help="Disk size (determines disk count)", default="128m", type="string", metavar="<size,size,...>", @@ -473,7 +476,11 @@ class Burner(object): constants.BE_MEMORY: options.mem_size, constants.BE_VCPUS: 1, } + + self.hypervisor = None self.hvp = {} + if options.hypervisor: + self.hypervisor, self.hvp = options.hypervisor socket.setdefaulttimeout(options.net_timeout) @@ -515,6 +522,9 @@ class Burner(object): default_nic_params = self.cluster_info["nicparams"][constants.PP_DEFAULT] self.cluster_default_nicparams = default_nic_params + if self.hypervisor is None: + self.hypervisor = self.cluster_info["default_hypervisor"] + self.hv_class = hypervisor.GetHypervisorClass(self.hypervisor) @_DoCheckInstances @_DoBatch(False) @@ -559,6 +569,7 @@ class Burner(object): iallocator=self.opts.iallocator, beparams=self.bep, hvparams=self.hvp, + hypervisor=self.hypervisor, ) remove_instance = lambda name: lambda: self.to_rem.append(name) self.ExecOrQueue(instance, [op], post_process=remove_instance(instance)) @@ -972,14 +983,20 @@ class Burner(object): self.BurnReplaceDisks2() if (opts.disk_template in constants.DTS_GROWABLE and - utils.any(self.disk_growth, lambda n: n > 0)): + compat.any(self.disk_growth, lambda n: n > 0)): self.BurnGrowDisks() if opts.do_failover and opts.disk_template in constants.DTS_NET_MIRROR: self.BurnFailover() - if opts.do_migrate and opts.disk_template == constants.DT_DRBD8: - self.BurnMigrate() + if opts.do_migrate: + if opts.disk_template != constants.DT_DRBD8: + Log("Skipping migration (disk template not DRBD8)") + elif not self.hv_class.CAN_MIGRATE: + Log("Skipping migration (hypervisor %s does not support it)", + self.hypervisor) + else: + self.BurnMigrate() if (opts.do_move and len(self.nodes) > 1 and opts.disk_template in [constants.DT_PLAIN, constants.DT_FILE]):