Only send force parameter to live migration if supported

force isn't supported until microversion 2.30. Add a test for that
microversion and only send it if the server supports it.

Change-Id: Ic45e9cccac10d432162d27f9b42da4f4eb1ff167
Story: 2002752
Task: 22608
This commit is contained in:
Monty Taylor 2018-06-28 13:33:30 -05:00
parent 34ea72ce5b
commit ed9cd86573
No known key found for this signature in database
GPG Key ID: 7BAE94BC7141A594
5 changed files with 291 additions and 63 deletions

View File

@ -1167,15 +1167,31 @@ class Proxy(proxy.Proxy):
server = self._get_resource(_server.Server, server)
server.migrate(self)
def live_migrate_server(self, server, host=None, force=False):
"""Migrate a server from one host to target host
def live_migrate_server(
self, server, host=None, force=False, block_migration=None):
"""Live migrate a server from one host to target host
:param server: Either the ID of a server or a
:class:`~openstack.compute.v2.server.Server` instance.
:param host: The host to which to migrate the server
:param force: Force a live-migration by not verifying the provided
destination host by the scheduler.
:param server:
Either the ID of a server or a
:class:`~openstack.compute.v2.server.Server` instance.
:param str host:
The host to which to migrate the server. If the Nova service is
too old, the host parameter implies force=True which causes the
Nova scheduler to be bypassed. On such clouds, a ``ValueError``
will be thrown if ``host`` is given without ``force``.
:param bool force:
Force a live-migration by not verifying the provided destination
host by the scheduler. This is unsafe and not recommended.
:param block_migration:
Perform a block live migration to the destination host by the
scheduler. Can be 'auto', True or False. Some clouds are too old
to support 'auto', in which case a ValueError will be thrown. If
omitted, the value will be 'auto' on clouds that support it, and
False on clouds that do not.
:returns: None
"""
server = self._get_resource(_server.Server, server)
server.live_migrate(self, host, force)
server.live_migrate(
self, host,
force=force,
block_migration=block_migration)

View File

@ -171,7 +171,7 @@ class Server(resource.Resource, metadata.MetadataMixin):
return request
def _action(self, session, body):
def _action(self, session, body, microversion=None):
"""Preform server actions given the message body."""
# NOTE: This is using Server.base_path instead of self.base_path
# as both Server and ServerDetail instances can be acted on, but
@ -179,7 +179,7 @@ class Server(resource.Resource, metadata.MetadataMixin):
url = utils.urljoin(Server.base_path, self.id, 'action')
headers = {'Accept': ''}
return session.post(
url, json=body, headers=headers)
url, json=body, headers=headers, microversion=microversion)
def change_password(self, session, new_password):
"""Change the administrator password to the given password."""
@ -363,15 +363,80 @@ class Server(resource.Resource, metadata.MetadataMixin):
resp = self._action(session, body)
return resp.json()
def live_migrate(self, session, host, force):
def live_migrate(self, session, host, force, block_migration):
if utils.supports_microversion(session, '2.30'):
return self._live_migrate_30(
session, host,
force=force,
block_migration=block_migration)
elif utils.supports_microversion(session, '2.25'):
return self._live_migrate_25(
session, host,
force=force,
block_migration=block_migration)
else:
return self._live_migrate(
session, host,
force=force,
block_migration=block_migration)
def _live_migrate_30(self, session, host, force, block_migration):
microversion = '2.30'
body = {'host': None}
if block_migration is None:
block_migration = 'auto'
body['block_migration'] = block_migration
if host:
body['host'] = host
if force:
body['force'] = force
self._action(
session, {'os-migrateLive': body}, microversion=microversion)
def _live_migrate_25(self, session, host, force, block_migration):
microversion = '2.25'
body = {'host': None}
if block_migration is None:
block_migration = 'auto'
body['block_migration'] = block_migration
if host:
body['host'] = host
if not force:
raise ValueError(
"Live migration on this cloud implies 'force'"
" if the 'host' option has been given and it is not"
" possible to disable. It is recommended to not use 'host'"
" at all on this cloud as it is inherently unsafe, but if"
" it is unavoidable, please supply 'force=True' so that it"
" is clear you understand the risks.")
self._action(
session, {'os-migrateLive': body}, microversion=microversion)
def _live_migrate(self, session, host, force, block_migration):
microversion = None
# disk_over_commit is not exposed because post 2.25 it has been
# removed and no SDK user is depending on it today.
body = {
"os-migrateLive": {
"host": host,
"block_migration": "auto",
"force": force
}
'host': None,
'disk_over_commit': False,
}
self._action(session, body)
if block_migration == 'auto':
raise ValueError(
"Live migration on this cloud does not support 'auto' as"
" a parameter to block_migration, but only True and False.")
body['block_migration'] = block_migration or False
if host:
body['host'] = host
if not force:
raise ValueError(
"Live migration on this cloud implies 'force'"
" if the 'host' option has been given and it is not"
" possible to disable. It is recommended to not use 'host'"
" at all on this cloud as it is inherently unsafe, but if"
" it is unavoidable, please supply 'force=True' so that it"
" is clear you understand the risks.")
self._action(
session, {'os-migrateLive': body}, microversion=microversion)
class ServerDetail(Server):

View File

@ -526,5 +526,6 @@ class TestComputeProxy(test_proxy_base.TestProxyBase):
def test_live_migrate_server(self):
self._verify('openstack.compute.v2.server.Server.live_migrate',
self.proxy.live_migrate_server,
method_args=["value", "host1", "force"],
expected_args=["host1", "force"])
method_args=["value", "host1", False],
expected_args=["host1"],
expected_kwargs={'force': False, 'block_migration': None})

View File

@ -11,6 +11,7 @@
# under the License.
import mock
import six
from openstack.tests.unit import base
from openstack.compute.v2 import server
@ -193,7 +194,7 @@ class TestServer(base.TestCase):
body = {"changePassword": {"adminPass": "a"}}
headers = {'Accept': ''}
self.sess.post.assert_called_with(
url, json=body, headers=headers)
url, json=body, headers=headers, microversion=None)
def test_reboot(self):
sot = server.Server(**EXAMPLE)
@ -204,7 +205,7 @@ class TestServer(base.TestCase):
body = {"reboot": {"type": "HARD"}}
headers = {'Accept': ''}
self.sess.post.assert_called_with(
url, json=body, headers=headers)
url, json=body, headers=headers, microversion=None)
def test_force_delete(self):
sot = server.Server(**EXAMPLE)
@ -215,7 +216,7 @@ class TestServer(base.TestCase):
body = {'forceDelete': None}
headers = {'Accept': ''}
self.sess.post.assert_called_with(
url, json=body, headers=headers)
url, json=body, headers=headers, microversion=None)
def test_rebuild(self):
sot = server.Server(**EXAMPLE)
@ -246,7 +247,7 @@ class TestServer(base.TestCase):
}
headers = {'Accept': ''}
self.sess.post.assert_called_with(
url, json=body, headers=headers)
url, json=body, headers=headers, microversion=None)
def test_rebuild_minimal(self):
sot = server.Server(**EXAMPLE)
@ -270,7 +271,7 @@ class TestServer(base.TestCase):
}
headers = {'Accept': ''}
self.sess.post.assert_called_with(
url, json=body, headers=headers)
url, json=body, headers=headers, microversion=None)
def test_resize(self):
sot = server.Server(**EXAMPLE)
@ -281,7 +282,7 @@ class TestServer(base.TestCase):
body = {"resize": {"flavorRef": "2"}}
headers = {'Accept': ''}
self.sess.post.assert_called_with(
url, json=body, headers=headers)
url, json=body, headers=headers, microversion=None)
def test_confirm_resize(self):
sot = server.Server(**EXAMPLE)
@ -292,7 +293,7 @@ class TestServer(base.TestCase):
body = {"confirmResize": None}
headers = {'Accept': ''}
self.sess.post.assert_called_with(
url, json=body, headers=headers)
url, json=body, headers=headers, microversion=None)
def test_revert_resize(self):
sot = server.Server(**EXAMPLE)
@ -303,7 +304,7 @@ class TestServer(base.TestCase):
body = {"revertResize": None}
headers = {'Accept': ''}
self.sess.post.assert_called_with(
url, json=body, headers=headers)
url, json=body, headers=headers, microversion=None)
def test_create_image(self):
sot = server.Server(**EXAMPLE)
@ -316,7 +317,7 @@ class TestServer(base.TestCase):
body = {"createImage": {'name': name, 'metadata': metadata}}
headers = {'Accept': ''}
self.sess.post.assert_called_with(
url, json=body, headers=headers)
url, json=body, headers=headers, microversion=None)
def test_create_image_minimal(self):
sot = server.Server(**EXAMPLE)
@ -328,7 +329,7 @@ class TestServer(base.TestCase):
body = {"createImage": {'name': name}}
headers = {'Accept': ''}
self.sess.post.assert_called_with(
url, json=body, headers=headers)
url, json=body, headers=headers, microversion=None)
def test_add_security_group(self):
sot = server.Server(**EXAMPLE)
@ -339,7 +340,7 @@ class TestServer(base.TestCase):
body = {"addSecurityGroup": {"name": "group"}}
headers = {'Accept': ''}
self.sess.post.assert_called_with(
url, json=body, headers=headers)
url, json=body, headers=headers, microversion=None)
def test_remove_security_group(self):
sot = server.Server(**EXAMPLE)
@ -350,7 +351,7 @@ class TestServer(base.TestCase):
body = {"removeSecurityGroup": {"name": "group"}}
headers = {'Accept': ''}
self.sess.post.assert_called_with(
url, json=body, headers=headers)
url, json=body, headers=headers, microversion=None)
def test_reset_state(self):
sot = server.Server(**EXAMPLE)
@ -361,7 +362,7 @@ class TestServer(base.TestCase):
body = {"os-resetState": {"state": 'active'}}
headers = {'Accept': ''}
self.sess.post.assert_called_with(
url, json=body, headers=headers)
url, json=body, headers=headers, microversion=None)
def test_add_fixed_ip(self):
sot = server.Server(**EXAMPLE)
@ -373,7 +374,7 @@ class TestServer(base.TestCase):
body = {"addFixedIp": {"networkId": "NETWORK-ID"}}
headers = {'Accept': ''}
self.sess.post.assert_called_with(
url, json=body, headers=headers)
url, json=body, headers=headers, microversion=None)
def test_remove_fixed_ip(self):
sot = server.Server(**EXAMPLE)
@ -385,7 +386,7 @@ class TestServer(base.TestCase):
body = {"removeFixedIp": {"address": "ADDRESS"}}
headers = {'Accept': ''}
self.sess.post.assert_called_with(
url, json=body, headers=headers)
url, json=body, headers=headers, microversion=None)
def test_add_floating_ip(self):
sot = server.Server(**EXAMPLE)
@ -397,7 +398,7 @@ class TestServer(base.TestCase):
body = {"addFloatingIp": {"address": "FLOATING-IP"}}
headers = {'Accept': ''}
self.sess.post.assert_called_with(
url, json=body, headers=headers)
url, json=body, headers=headers, microversion=None)
def test_add_floating_ip_with_fixed_addr(self):
sot = server.Server(**EXAMPLE)
@ -410,7 +411,7 @@ class TestServer(base.TestCase):
"fixed_address": "FIXED-ADDR"}}
headers = {'Accept': ''}
self.sess.post.assert_called_with(
url, json=body, headers=headers)
url, json=body, headers=headers, microversion=None)
def test_remove_floating_ip(self):
sot = server.Server(**EXAMPLE)
@ -422,7 +423,7 @@ class TestServer(base.TestCase):
body = {"removeFloatingIp": {"address": "I-AM-FLOATING"}}
headers = {'Accept': ''}
self.sess.post.assert_called_with(
url, json=body, headers=headers)
url, json=body, headers=headers, microversion=None)
def test_backup(self):
sot = server.Server(**EXAMPLE)
@ -435,7 +436,7 @@ class TestServer(base.TestCase):
"rotation": 1}}
headers = {'Accept': ''}
self.sess.post.assert_called_with(
url, json=body, headers=headers)
url, json=body, headers=headers, microversion=None)
def test_pause(self):
sot = server.Server(**EXAMPLE)
@ -447,7 +448,7 @@ class TestServer(base.TestCase):
body = {"pause": None}
headers = {'Accept': ''}
self.sess.post.assert_called_with(
url, json=body, headers=headers)
url, json=body, headers=headers, microversion=None)
def test_unpause(self):
sot = server.Server(**EXAMPLE)
@ -459,7 +460,7 @@ class TestServer(base.TestCase):
body = {"unpause": None}
headers = {'Accept': ''}
self.sess.post.assert_called_with(
url, json=body, headers=headers)
url, json=body, headers=headers, microversion=None)
def test_suspend(self):
sot = server.Server(**EXAMPLE)
@ -471,7 +472,7 @@ class TestServer(base.TestCase):
body = {"suspend": None}
headers = {'Accept': ''}
self.sess.post.assert_called_with(
url, json=body, headers=headers)
url, json=body, headers=headers, microversion=None)
def test_resume(self):
sot = server.Server(**EXAMPLE)
@ -483,7 +484,7 @@ class TestServer(base.TestCase):
body = {"resume": None}
headers = {'Accept': ''}
self.sess.post.assert_called_with(
url, json=body, headers=headers)
url, json=body, headers=headers, microversion=None)
def test_lock(self):
sot = server.Server(**EXAMPLE)
@ -495,7 +496,7 @@ class TestServer(base.TestCase):
body = {"lock": None}
headers = {'Accept': ''}
self.sess.post.assert_called_with(
url, json=body, headers=headers)
url, json=body, headers=headers, microversion=None)
def test_unlock(self):
sot = server.Server(**EXAMPLE)
@ -507,7 +508,7 @@ class TestServer(base.TestCase):
body = {"unlock": None}
headers = {'Accept': ''}
self.sess.post.assert_called_with(
url, json=body, headers=headers)
url, json=body, headers=headers, microversion=None)
def test_rescue(self):
sot = server.Server(**EXAMPLE)
@ -519,7 +520,7 @@ class TestServer(base.TestCase):
body = {"rescue": {}}
headers = {'Accept': ''}
self.sess.post.assert_called_with(
url, json=body, headers=headers)
url, json=body, headers=headers, microversion=None)
def test_rescue_with_options(self):
sot = server.Server(**EXAMPLE)
@ -532,7 +533,7 @@ class TestServer(base.TestCase):
'rescue_image_ref': 'IMG-ID'}}
headers = {'Accept': ''}
self.sess.post.assert_called_with(
url, json=body, headers=headers)
url, json=body, headers=headers, microversion=None)
def test_unrescue(self):
sot = server.Server(**EXAMPLE)
@ -544,7 +545,7 @@ class TestServer(base.TestCase):
body = {"unrescue": None}
headers = {'Accept': ''}
self.sess.post.assert_called_with(
url, json=body, headers=headers)
url, json=body, headers=headers, microversion=None)
def test_evacuate(self):
sot = server.Server(**EXAMPLE)
@ -556,7 +557,7 @@ class TestServer(base.TestCase):
body = {"evacuate": {}}
headers = {'Accept': ''}
self.sess.post.assert_called_with(
url, json=body, headers=headers)
url, json=body, headers=headers, microversion=None)
def test_evacuate_with_options(self):
sot = server.Server(**EXAMPLE)
@ -570,7 +571,7 @@ class TestServer(base.TestCase):
'force': True}}
headers = {'Accept': ''}
self.sess.post.assert_called_with(
url, json=body, headers=headers)
url, json=body, headers=headers, microversion=None)
def test_start(self):
sot = server.Server(**EXAMPLE)
@ -582,7 +583,7 @@ class TestServer(base.TestCase):
body = {"os-start": None}
headers = {'Accept': ''}
self.sess.post.assert_called_with(
url, json=body, headers=headers)
url, json=body, headers=headers, microversion=None)
def test_stop(self):
sot = server.Server(**EXAMPLE)
@ -594,7 +595,7 @@ class TestServer(base.TestCase):
body = {"os-stop": None}
headers = {'Accept': ''}
self.sess.post.assert_called_with(
url, json=body, headers=headers)
url, json=body, headers=headers, microversion=None)
def test_shelve(self):
sot = server.Server(**EXAMPLE)
@ -606,7 +607,7 @@ class TestServer(base.TestCase):
body = {"shelve": None}
headers = {'Accept': ''}
self.sess.post.assert_called_with(
url, json=body, headers=headers)
url, json=body, headers=headers, microversion=None)
def test_unshelve(self):
sot = server.Server(**EXAMPLE)
@ -618,7 +619,7 @@ class TestServer(base.TestCase):
body = {"unshelve": None}
headers = {'Accept': ''}
self.sess.post.assert_called_with(
url, json=body, headers=headers)
url, json=body, headers=headers, microversion=None)
def test_migrate(self):
sot = server.Server(**EXAMPLE)
@ -631,7 +632,7 @@ class TestServer(base.TestCase):
headers = {'Accept': ''}
self.sess.post.assert_called_with(
url, json=body, headers=headers)
url, json=body, headers=headers, microversion=None)
def test_get_console_output(self):
sot = server.Server(**EXAMPLE)
@ -643,7 +644,7 @@ class TestServer(base.TestCase):
body = {'os-getConsoleOutput': {}}
headers = {'Accept': ''}
self.sess.post.assert_called_with(
url, json=body, headers=headers)
url, json=body, headers=headers, microversion=None)
res = sot.get_console_output(self.sess, length=1)
@ -653,23 +654,142 @@ class TestServer(base.TestCase):
headers = {'Accept': ''}
self.sess.post.assert_called_with(
url, json=body, headers=headers)
url, json=body, headers=headers, microversion=None)
def test_live_migrate(self):
def test_live_migrate_no_force(self):
sot = server.Server(**EXAMPLE)
res = sot.live_migrate(self.sess, host='HOST2', force=False)
class FakeEndpointData(object):
min_microversion = None
max_microversion = None
self.sess.get_endpoint_data.return_value = FakeEndpointData()
ex = self.assertRaises(
ValueError,
sot.live_migrate,
self.sess, host='HOST2', force=False, block_migration=False)
self.assertIn(
"Live migration on this cloud implies 'force'",
six.text_type(ex))
def test_live_migrate_no_microversion_force_true(self):
sot = server.Server(**EXAMPLE)
class FakeEndpointData(object):
min_microversion = None
max_microversion = None
self.sess.get_endpoint_data.return_value = FakeEndpointData()
res = sot.live_migrate(
self.sess, host='HOST2', force=True, block_migration=False)
self.assertIsNone(res)
url = 'servers/IDENTIFIER/action'
body = {
"os-migrateLive": {
"host": 'HOST2',
"block_migration": "auto",
"force": False
'os-migrateLive': {
'host': 'HOST2',
'disk_over_commit': False,
'block_migration': False
}
}
headers = {'Accept': ''}
self.sess.post.assert_called_with(
url, json=body, headers=headers)
url, json=body, headers=headers, microversion=None)
def test_live_migrate_25(self):
sot = server.Server(**EXAMPLE)
class FakeEndpointData(object):
min_microversion = '2.1'
max_microversion = '2.25'
self.sess.get_endpoint_data.return_value = FakeEndpointData()
res = sot.live_migrate(
self.sess, host='HOST2', force=True, block_migration=False)
self.assertIsNone(res)
url = 'servers/IDENTIFIER/action'
body = {
"os-migrateLive": {
'block_migration': False,
'host': 'HOST2',
}
}
headers = {'Accept': ''}
self.sess.post.assert_called_with(
url, json=body, headers=headers, microversion='2.25')
def test_live_migrate_25_default_block(self):
sot = server.Server(**EXAMPLE)
class FakeEndpointData(object):
min_microversion = '2.1'
max_microversion = '2.25'
self.sess.get_endpoint_data.return_value = FakeEndpointData()
res = sot.live_migrate(
self.sess, host='HOST2', force=True, block_migration=None)
self.assertIsNone(res)
url = 'servers/IDENTIFIER/action'
body = {
"os-migrateLive": {
'block_migration': 'auto',
'host': 'HOST2',
}
}
headers = {'Accept': ''}
self.sess.post.assert_called_with(
url, json=body, headers=headers, microversion='2.25')
def test_live_migrate_30(self):
sot = server.Server(**EXAMPLE)
class FakeEndpointData(object):
min_microversion = '2.1'
max_microversion = '2.30'
self.sess.get_endpoint_data.return_value = FakeEndpointData()
res = sot.live_migrate(
self.sess, host='HOST2', force=False, block_migration=False)
self.assertIsNone(res)
url = 'servers/IDENTIFIER/action'
body = {
'os-migrateLive': {
'block_migration': False,
'host': 'HOST2'
}
}
headers = {'Accept': ''}
self.sess.post.assert_called_with(
url, json=body, headers=headers, microversion='2.30')
def test_live_migrate_30_force(self):
sot = server.Server(**EXAMPLE)
class FakeEndpointData(object):
min_microversion = '2.1'
max_microversion = '2.30'
self.sess.get_endpoint_data.return_value = FakeEndpointData()
res = sot.live_migrate(
self.sess, host='HOST2', force=True, block_migration=None)
self.assertIsNone(res)
url = 'servers/IDENTIFIER/action'
body = {
'os-migrateLive': {
'block_migration': 'auto',
'host': 'HOST2',
'force': True,
}
}
headers = {'Accept': ''}
self.sess.post.assert_called_with(
url, json=body, headers=headers, microversion='2.30')

View File

@ -15,6 +15,7 @@ import string
import time
import deprecation
from keystoneauth1 import discover
from openstack import _log
from openstack import exceptions
@ -128,3 +129,28 @@ def get_string_format_keys(fmt_string, old_style=True):
if t[1] is not None:
keys.append(t[1])
return keys
def supports_microversion(adapter, microversion):
"""Determine if the given adapter supports the given microversion.
Checks the min and max microversion asserted by the service and checks
to make sure that ``min <= microversion <= max``.
:param adapter:
:class:`~keystoneauth1.adapter.Adapter` instance.
:param str microversion:
String containing the desired microversion.
:returns: True if the service supports the microversion.
:rtype: bool
"""
endpoint_data = adapter.get_endpoint_data()
if (endpoint_data.min_microversion
and endpoint_data.max_microversion
and discover.version_between(
endpoint_data.min_microversion,
endpoint_data.max_microversion,
microversion)):
return True
return False