Enable cold migration with target host(2/2)

This function enables users to specify a target host
when cold migrating a VM instance.

This patch modifies the migration API.

APIImpact
    Add an optional parameter 'host' in cold migration action.

Change-Id: Iee356c4dd097c846b6ca8617ead6a061300c83f8
Implements: blueprint cold-migration-with-target-queens
This commit is contained in:
Takashi NATSUME 2016-12-09 14:03:32 +09:00 committed by Matt Riedemann
parent 05c6b54eec
commit d2ce4ca9ec
18 changed files with 328 additions and 23 deletions

View File

@ -2896,6 +2896,14 @@ host_migration:
in: body in: body
required: true required: true
type: string type: string
host_migration_2_56:
description: |
The host to which to migrate the server. If you specify ``null`` or
don't specify this parameter, the scheduler chooses a host.
in: body
required: false
type: string
min_version: 2.56
host_name_body: host_name_body:
description: | description: |
The name of the host. The name of the host.
@ -3897,10 +3905,12 @@ metadata_object:
type: object type: object
migrate: migrate:
description: | description: |
The action. The action to cold migrate a server.
This parameter can be ``null``.
Up to microversion 2.55, this parameter should be ``null``.
in: body in: body
required: true required: true
type: string type: object
migrate_dest_compute: migrate_dest_compute:
description: | description: |
The target compute for a migration. The target compute for a migration.

View File

@ -56,10 +56,15 @@ Migrate Server (migrate Action)
.. rest_method:: POST /servers/{server_id}/action .. rest_method:: POST /servers/{server_id}/action
Migrates a server to a host. The scheduler chooses the host. Migrates a server to a host.
Specify the ``migrate`` action in the request body. Specify the ``migrate`` action in the request body.
Up to microversion 2.55, the scheduler chooses the host.
Starting from microversion 2.56, the ``host`` parameter is available
to specify the destination host. If you specify ``null`` or don't specify
this parameter, the scheduler chooses a host.
Policy defaults enable only users with the administrative role to Policy defaults enable only users with the administrative role to
perform this operation. Cloud providers can change these permissions perform this operation. Cloud providers can change these permissions
through the ``policy.json`` file. through the ``policy.json`` file.
@ -76,12 +81,18 @@ Request
- server_id: server_id_path - server_id: server_id_path
- migrate: migrate - migrate: migrate
- host: host_migration_2_56
**Example Migrate Server (migrate Action)** **Example Migrate Server (migrate Action) (v2.1)**
.. literalinclude:: ../../doc/api_samples/os-migrate-server/migrate-server.json .. literalinclude:: ../../doc/api_samples/os-migrate-server/migrate-server.json
:language: javascript :language: javascript
**Example Migrate Server (migrate Action) (v2.56)**
.. literalinclude:: ../../doc/api_samples/os-migrate-server/v2.56/migrate-server.json
:language: javascript
Response Response
-------- --------

View File

@ -0,0 +1,3 @@
{
"migrate": null
}

View File

@ -0,0 +1,5 @@
{
"migrate": {
"host": "host1"
}
}

View File

@ -19,7 +19,7 @@
} }
], ],
"status": "CURRENT", "status": "CURRENT",
"version": "2.55", "version": "2.56",
"min_version": "2.1", "min_version": "2.1",
"updated": "2013-07-23T11:33:21Z" "updated": "2013-07-23T11:33:21Z"
} }

View File

@ -22,7 +22,7 @@
} }
], ],
"status": "CURRENT", "status": "CURRENT",
"version": "2.55", "version": "2.56",
"min_version": "2.1", "min_version": "2.1",
"updated": "2013-07-23T11:33:21Z" "updated": "2013-07-23T11:33:21Z"
} }

View File

@ -130,6 +130,9 @@ REST_API_VERSION_HISTORY = """REST API Version History:
and responses are also changed. and responses are also changed.
* 2.54 - Enable reset key pair while rebuilding instance. * 2.54 - Enable reset key pair while rebuilding instance.
* 2.55 - Added flavor.description to GET/POST/PUT flavors APIs. * 2.55 - Added flavor.description to GET/POST/PUT flavors APIs.
* 2.56 - Add a host parameter in migrate request body in order to
enable users to specify a target host in cold migration.
The target host is checked by the scheduler.
""" """
# The minimum and maximum versions of the API supported # The minimum and maximum versions of the API supported
@ -138,7 +141,7 @@ REST_API_VERSION_HISTORY = """REST API Version History:
# Note(cyeoh): This only applies for the v2.1 API once microversions # Note(cyeoh): This only applies for the v2.1 API once microversions
# support is fully merged. It does not affect the V2 API. # support is fully merged. It does not affect the V2 API.
_MIN_API_VERSION = "2.1" _MIN_API_VERSION = "2.1"
_MAX_API_VERSION = "2.55" _MAX_API_VERSION = "2.56"
DEFAULT_API_VERSION = _MIN_API_VERSION DEFAULT_API_VERSION = _MIN_API_VERSION
# Almost all proxy APIs which are related to network, images and baremetal # Almost all proxy APIs which are related to network, images and baremetal

View File

@ -40,24 +40,33 @@ class MigrateServerController(wsgi.Controller):
@wsgi.response(202) @wsgi.response(202)
@extensions.expected_errors((400, 403, 404, 409)) @extensions.expected_errors((400, 403, 404, 409))
@wsgi.action('migrate') @wsgi.action('migrate')
@validation.schema(migrate_server.migrate_v2_56, "2.56")
def _migrate(self, req, id, body): def _migrate(self, req, id, body):
"""Permit admins to migrate a server to a new host.""" """Permit admins to migrate a server to a new host."""
context = req.environ['nova.context'] context = req.environ['nova.context']
context.can(ms_policies.POLICY_ROOT % 'migrate') context.can(ms_policies.POLICY_ROOT % 'migrate')
host_name = None
if (api_version_request.is_supported(req, min_version='2.56') and
body['migrate'] is not None):
host_name = body['migrate'].get('host')
instance = common.get_instance(self.compute_api, context, id) instance = common.get_instance(self.compute_api, context, id)
try: try:
self.compute_api.resize(req.environ['nova.context'], instance) self.compute_api.resize(req.environ['nova.context'], instance,
host_name=host_name)
except (exception.TooManyInstances, exception.QuotaError) as e: except (exception.TooManyInstances, exception.QuotaError) as e:
raise exc.HTTPForbidden(explanation=e.format_message()) raise exc.HTTPForbidden(explanation=e.format_message())
except exception.InstanceIsLocked as e: except (exception.InstanceIsLocked,
exception.CannotMigrateWithTargetHost) as e:
raise exc.HTTPConflict(explanation=e.format_message()) raise exc.HTTPConflict(explanation=e.format_message())
except exception.InstanceInvalidState as state_error: except exception.InstanceInvalidState as state_error:
common.raise_http_conflict_for_instance_invalid_state(state_error, common.raise_http_conflict_for_instance_invalid_state(state_error,
'migrate', id) 'migrate', id)
except exception.InstanceNotFound as e: except exception.InstanceNotFound as e:
raise exc.HTTPNotFound(explanation=e.format_message()) raise exc.HTTPNotFound(explanation=e.format_message())
except exception.NoValidHost as e: except (exception.NoValidHost, exception.ComputeHostNotFound,
exception.CannotMigrateToSameHost) as e:
raise exc.HTTPBadRequest(explanation=e.format_message()) raise exc.HTTPBadRequest(explanation=e.format_message())
@wsgi.response(202) @wsgi.response(202)

View File

@ -701,3 +701,11 @@ Adds a ``description`` field to the flavor resource in the following APIs:
* ``PUT /flavors/{flavor_id}`` * ``PUT /flavors/{flavor_id}``
The embedded flavor description will not be included in server representations. The embedded flavor description will not be included in server representations.
2.56
----
Updates the POST request body for the ``migrate`` action to include the
the optional ``host`` string field defaulted to ``null``. If ``host`` is
set the migrate action verifies the provided host with the nova scheduler
and uses it as the destination for the migration.

View File

@ -20,6 +20,21 @@ from nova.api.validation import parameter_types
host = copy.deepcopy(parameter_types.hostname) host = copy.deepcopy(parameter_types.hostname)
host['type'] = ['string', 'null'] host['type'] = ['string', 'null']
migrate_v2_56 = {
'type': 'object',
'properties': {
'migrate': {
'type': ['object', 'null'],
'properties': {
'host': host,
},
'additionalProperties': False,
},
},
'required': ['migrate'],
'additionalProperties': False,
}
migrate_live = { migrate_live = {
'type': 'object', 'type': 'object',
'properties': { 'properties': {

View File

@ -0,0 +1,5 @@
{
"migrate": {
"host": %(hostname)s
}
}

View File

@ -141,3 +141,42 @@ class MigrateServerSamplesJsonTestV230(MigrateServerSamplesJsonTest):
{'hostname': hostname, {'hostname': hostname,
'force': 'False'}) 'force': 'False'})
self.assertEqual(400, response.status_code) self.assertEqual(400, response.status_code)
class MigrateServerSamplesJsonTestV256(test_servers.ServersSampleBase):
sample_dir = "os-migrate-server"
microversion = '2.56'
scenarios = [('v2_56', {'api_major_version': 'v2.1'})]
def setUp(self):
"""setUp Method for MigrateServer api samples extension
This method creates the server that will be used in each tests
"""
super(MigrateServerSamplesJsonTestV256, self).setUp()
self.uuid = self._post_server()
@mock.patch('nova.conductor.manager.ComputeTaskManager._cold_migrate')
def test_post_migrate(self, mock_cold_migrate):
response = self._do_post('servers/%s/action' % self.uuid,
'migrate-server',
{'hostname': 'null'})
self.assertEqual(202, response.status_code)
@mock.patch('nova.objects.ComputeNodeList.get_all_by_host',
return_value=[objects.ComputeNode(
host='target-host', hypervisor_hostname='target-node')])
@mock.patch('nova.conductor.manager.ComputeTaskManager._cold_migrate')
def test_post_migrate_with_host(self, mock_cold_migrate,
mock_get_all_by_host):
response = self._do_post('servers/%s/action' % self.uuid,
'migrate-server',
{'hostname': '"target-host"'})
self.assertEqual(202, response.status_code)
@mock.patch('nova.conductor.manager.ComputeTaskManager._cold_migrate')
def test_post_migrate_null(self, mock_cold_migrate):
# Check backward compatibility.
response = self._do_post('servers/%s/action' % self.uuid,
'migrate-server-null', {})
self.assertEqual(202, response.status_code)

View File

@ -137,11 +137,14 @@ class _IntegratedTestBase(test.TestCase):
def get_invalid_image(self): def get_invalid_image(self):
return uuids.fake return uuids.fake
def _build_minimal_create_server_request(self): def _build_minimal_create_server_request(self, image_uuid=None):
server = {} server = {}
# We now have a valid imageId # NOTE(takashin): In API version 2.36, image APIs were deprecated.
server[self._image_ref_parameter] = self.api.get_images()[0]['id'] # In API version 2.36 or greater, self.api.get_images() returns
# a 404 error. In that case, 'image_uuid' should be specified.
server[self._image_ref_parameter] = (image_uuid or
self.api.get_images()[0]['id'])
# Set a valid flavorId # Set a valid flavorId
flavor = self.api.get_flavors()[0] flavor = self.api.get_flavors()[0]

View File

@ -24,6 +24,7 @@ from oslo_utils import timeutils
from nova.compute import api as compute_api from nova.compute import api as compute_api
from nova.compute import instance_actions from nova.compute import instance_actions
from nova.compute import manager as compute_manager
from nova.compute import rpcapi from nova.compute import rpcapi
from nova import context from nova import context
from nova import db from nova import db
@ -2995,3 +2996,83 @@ class ServerSoftDeleteTests(ProviderUsageBaseTestCase):
# Now we want a real delete # Now we want a real delete
self.flags(reclaim_instance_interval=0) self.flags(reclaim_instance_interval=0)
self._delete_and_check_allocations(server) self._delete_and_check_allocations(server)
class ServerTestV256Common(ServersTestBase):
api_major_version = 'v2.1'
microversion = '2.56'
ADMIN_API = True
def _create_server(self):
server = self._build_minimal_create_server_request(
image_uuid='a2459075-d96c-40d5-893e-577ff92e721c')
server.update({'networks': 'auto'})
post = {'server': server}
response = self.api.api_post('/servers', post).body
return response['server']
class ServerTestV256SingleCellMultiHostTestCase(ServerTestV256Common):
"""Happy path test where we create a server on one host, migrate it to
another host of our choosing and ensure it lands there.
"""
def _setup_compute_service(self):
# Set up 3 compute services in the same cell
for host in ('host1', 'host2', 'host3'):
fake.set_nodes([host])
self.addCleanup(fake.restore_nodes)
self.start_service('compute', host=host)
@staticmethod
def _get_target_and_other_hosts(host):
target_other_hosts = {'host1': ['host2', 'host3'],
'host2': ['host3', 'host1'],
'host3': ['host1', 'host2']}
return target_other_hosts[host]
def test_migrate_server_to_host_in_same_cell(self):
server = self._create_server()
server = self._wait_for_state_change(server, 'BUILD')
source_host = server['OS-EXT-SRV-ATTR:host']
target_host = self._get_target_and_other_hosts(source_host)[0]
self.api.post_server_action(server['id'],
{'migrate': {'host': target_host}})
# Assert the server is now on the target host.
server = self.api.get_server(server['id'])
self.assertEqual(target_host, server['OS-EXT-SRV-ATTR:host'])
class ServerTestV256RescheduleTestCase(ServerTestV256Common):
def _setup_compute_service(self):
# Set up 3 compute services in the same cell
for host in ('host1', 'host2', 'host3'):
fake.set_nodes([host])
self.addCleanup(fake.restore_nodes)
self.start_service('compute', host=host)
@staticmethod
def _get_target_and_other_hosts(host):
target_other_hosts = {'host1': ['host2', 'host3'],
'host2': ['host3', 'host1'],
'host3': ['host1', 'host2']}
return target_other_hosts[host]
@mock.patch.object(compute_manager.ComputeManager, '_prep_resize',
side_effect=exception.MigrationError(
reason='Test Exception'))
def test_migrate_server_not_reschedule(self, mock_prep_resize):
server = self._create_server()
found_server = self._wait_for_state_change(server, 'BUILD')
target_host, other_host = self._get_target_and_other_hosts(
found_server['OS-EXT-SRV-ATTR:host'])
self.assertRaises(client.OpenStackApiException,
self.api.post_server_action,
server['id'],
{'migrate': {'host': target_host}})
self.assertEqual(1, mock_prep_resize.call_count)
found_server = self.api.get_server(server['id'])
# Check that rescheduling is not occurred.
self.assertNotEqual(other_host, found_server['OS-EXT-SRV-ATTR:host'])

View File

@ -230,7 +230,7 @@ class CommonTests(CommonMixin, test.NoDBTestCase):
body_map = body_map or {} body_map = body_map or {}
for action in actions: for action in actions:
self._test_non_existing_instance(action, self._test_non_existing_instance(action,
body_map=body_map) body_map=body_map.get(action))
# Re-mock this. # Re-mock this.
self.mox.StubOutWithMock(self.compute_api, 'get') self.mox.StubOutWithMock(self.compute_api, 'get')
@ -247,7 +247,7 @@ class CommonTests(CommonMixin, test.NoDBTestCase):
self.mox.StubOutWithMock(self.compute_api, self.mox.StubOutWithMock(self.compute_api,
method or action.replace('_', '')) method or action.replace('_', ''))
self._test_invalid_state(action, method=method, self._test_invalid_state(action, method=method,
body_map=body_map, body_map=body_map.get(action),
compute_api_args_map=args_map, compute_api_args_map=args_map,
exception_arg=exception_arg) exception_arg=exception_arg)
# Re-mock this. # Re-mock this.

View File

@ -13,6 +13,7 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import mock
from oslo_utils import uuidutils from oslo_utils import uuidutils
import six import six
import webob import webob
@ -21,9 +22,11 @@ from nova.api.openstack import api_version_request
from nova.api.openstack.compute import migrate_server as \ from nova.api.openstack.compute import migrate_server as \
migrate_server_v21 migrate_server_v21
from nova import exception from nova import exception
from nova import objects
from nova import test from nova import test
from nova.tests.unit.api.openstack.compute import admin_only_action_common from nova.tests.unit.api.openstack.compute import admin_only_action_common
from nova.tests.unit.api.openstack import fakes from nova.tests.unit.api.openstack import fakes
from nova.tests import uuidsentinel as uuids
class MigrateServerTestsV21(admin_only_action_common.CommonTests): class MigrateServerTestsV21(admin_only_action_common.CommonTests):
@ -34,6 +37,7 @@ class MigrateServerTestsV21(admin_only_action_common.CommonTests):
disk_over_commit = False disk_over_commit = False
force = None force = None
async = False async = False
host_name = None
def setUp(self): def setUp(self):
super(MigrateServerTestsV21, self).setUp() super(MigrateServerTestsV21, self).setUp()
@ -61,7 +65,8 @@ class MigrateServerTestsV21(admin_only_action_common.CommonTests):
body_map = {'_migrate_live': self._get_migration_body(host='hostname')} body_map = {'_migrate_live': self._get_migration_body(host='hostname')}
args_map = {'_migrate_live': ((False, self.disk_over_commit, args_map = {'_migrate_live': ((False, self.disk_over_commit,
'hostname', self.force, self.async), 'hostname', self.force, self.async),
{})} {}),
'_migrate': ((), {'host_name': self.host_name})}
self._test_actions(['_migrate', '_migrate_live'], body_map=body_map, self._test_actions(['_migrate', '_migrate_live'], body_map=body_map,
method_translations=method_translations, method_translations=method_translations,
args_map=args_map) args_map=args_map)
@ -72,23 +77,27 @@ class MigrateServerTestsV21(admin_only_action_common.CommonTests):
body_map = {'_migrate_live': self._get_migration_body(host=None)} body_map = {'_migrate_live': self._get_migration_body(host=None)}
args_map = {'_migrate_live': ((False, self.disk_over_commit, None, args_map = {'_migrate_live': ((False, self.disk_over_commit, None,
self.force, self.async), self.force, self.async),
{})} {}),
'_migrate': ((), {'host_name': None})}
self._test_actions(['_migrate', '_migrate_live'], body_map=body_map, self._test_actions(['_migrate', '_migrate_live'], body_map=body_map,
method_translations=method_translations, method_translations=method_translations,
args_map=args_map) args_map=args_map)
def test_migrate_with_non_existed_instance(self): def test_migrate_with_non_existed_instance(self):
body_map = self._get_migration_body(host='hostname') body_map = {'_migrate_live':
self._get_migration_body(host='hostname')}
self._test_actions_with_non_existed_instance( self._test_actions_with_non_existed_instance(
['_migrate', '_migrate_live'], body_map=body_map) ['_migrate', '_migrate_live'], body_map=body_map)
def test_migrate_raise_conflict_on_invalid_state(self): def test_migrate_raise_conflict_on_invalid_state(self):
method_translations = {'_migrate': 'resize', method_translations = {'_migrate': 'resize',
'_migrate_live': 'live_migrate'} '_migrate_live': 'live_migrate'}
body_map = self._get_migration_body(host='hostname') body_map = {'_migrate_live':
self._get_migration_body(host='hostname')}
args_map = {'_migrate_live': ((False, self.disk_over_commit, args_map = {'_migrate_live': ((False, self.disk_over_commit,
'hostname', self.force, self.async), 'hostname', self.force, self.async),
{})} {}),
'_migrate': ((), {'host_name': self.host_name})}
exception_arg = {'_migrate': 'migrate', exception_arg = {'_migrate': 'migrate',
'_migrate_live': 'os-migrateLive'} '_migrate_live': 'os-migrateLive'}
self._test_actions_raise_conflict_on_invalid_state( self._test_actions_raise_conflict_on_invalid_state(
@ -104,7 +113,8 @@ class MigrateServerTestsV21(admin_only_action_common.CommonTests):
self._get_migration_body(host='hostname')} self._get_migration_body(host='hostname')}
args_map = {'_migrate_live': ((False, self.disk_over_commit, args_map = {'_migrate_live': ((False, self.disk_over_commit,
'hostname', self.force, self.async), 'hostname', self.force, self.async),
{})} {}),
'_migrate': ((), {'host_name': self.host_name})}
self._test_actions_with_locked_instance( self._test_actions_with_locked_instance(
['_migrate', '_migrate_live'], body_map=body_map, ['_migrate', '_migrate_live'], body_map=body_map,
args_map=args_map, method_translations=method_translations) args_map=args_map, method_translations=method_translations)
@ -112,12 +122,14 @@ class MigrateServerTestsV21(admin_only_action_common.CommonTests):
def _test_migrate_exception(self, exc_info, expected_result): def _test_migrate_exception(self, exc_info, expected_result):
self.mox.StubOutWithMock(self.compute_api, 'resize') self.mox.StubOutWithMock(self.compute_api, 'resize')
instance = self._stub_instance_get() instance = self._stub_instance_get()
self.compute_api.resize(self.context, instance).AndRaise(exc_info) self.compute_api.resize(
self.context, instance,
host_name=self.host_name).AndRaise(exc_info)
self.mox.ReplayAll() self.mox.ReplayAll()
self.assertRaises(expected_result, self.assertRaises(expected_result,
self.controller._migrate, self.controller._migrate,
self.req, instance['uuid'], {'migrate': None}) self.req, instance['uuid'], body={'migrate': None})
def test_migrate_too_many_instances(self): def test_migrate_too_many_instances(self):
exc_info = exception.TooManyInstances(overs='', req='', used=0, exc_info = exception.TooManyInstances(overs='', req='', used=0,
@ -438,6 +450,100 @@ class MigrateServerTestsV234(MigrateServerTestsV230):
self.req, instance.uuid, body=body) self.req, instance.uuid, body=body)
class MigrateServerTestsV256(MigrateServerTestsV234):
host_name = 'fake-host'
method_translations = {'_migrate': 'resize'}
body_map = {'_migrate': {'migrate': {'host': host_name}}}
args_map = {'_migrate': ((), {'host_name': host_name})}
def setUp(self):
super(MigrateServerTestsV256, self).setUp()
self.req.api_version_request = api_version_request.APIVersionRequest(
'2.56')
def _test_migrate_validation_error(self, body):
self.assertRaises(self.validation_error,
self.controller._migrate,
self.req, fakes.FAKE_UUID, body=body)
def _test_migrate_exception(self, exc_info, expected_result):
@mock.patch.object(self.compute_api, 'get')
@mock.patch.object(self.compute_api, 'resize', side_effect=exc_info)
def _test(mock_resize, mock_get):
instance = objects.Instance(uuid=uuids.instance)
self.assertRaises(expected_result,
self.controller._migrate,
self.req, instance['uuid'],
body={'migrate': {'host': self.host_name}})
_test()
def test_migrate(self):
self._test_actions(['_migrate'], body_map=self.body_map,
method_translations=self.method_translations,
args_map=self.args_map)
def test_migrate_without_host(self):
# The request body is: '{"migrate": null}'
body_map = {'_migrate': {'migrate': None}}
args_map = {'_migrate': ((), {'host_name': None})}
self._test_actions(['_migrate'], body_map=body_map,
method_translations=self.method_translations,
args_map=args_map)
def test_migrate_none_hostname(self):
# The request body is: '{"migrate": {"host": null}}'
body_map = {'_migrate': {'migrate': {'host': None}}}
args_map = {'_migrate': ((), {'host_name': None})}
self._test_actions(['_migrate'], body_map=body_map,
method_translations=self.method_translations,
args_map=args_map)
def test_migrate_with_non_existed_instance(self):
self._test_actions_with_non_existed_instance(
['_migrate'], body_map=self.body_map)
def test_migrate_raise_conflict_on_invalid_state(self):
exception_arg = {'_migrate': 'migrate'}
self._test_actions_raise_conflict_on_invalid_state(
['_migrate'], body_map=self.body_map,
args_map=self.args_map,
method_translations=self.method_translations,
exception_args=exception_arg)
def test_actions_with_locked_instance(self):
self._test_actions_with_locked_instance(
['_migrate'], body_map=self.body_map,
args_map=self.args_map,
method_translations=self.method_translations)
def test_migrate_without_migrate_object(self):
self._test_migrate_validation_error({})
def test_migrate_invalid_migrate_object(self):
self._test_migrate_validation_error({'migrate': 'fake-host'})
def test_migrate_with_additional_property(self):
self._test_migrate_validation_error(
{'migrate': {'host': self.host_name,
'additional': 'foo'}})
def test_migrate_with_host_length_more_than_255(self):
self._test_migrate_validation_error(
{'migrate': {'host': 'a' * 256}})
def test_migrate_nonexistent_host(self):
exc_info = exception.ComputeHostNotFound(host='nonexistent_host')
self._test_migrate_exception(exc_info, webob.exc.HTTPBadRequest)
def test_migrate_no_request_spec(self):
exc_info = exception.CannotMigrateWithTargetHost()
self._test_migrate_exception(exc_info, webob.exc.HTTPConflict)
def test_migrate_to_same_host(self):
exc_info = exception.CannotMigrateToSameHost()
self._test_migrate_exception(exc_info, webob.exc.HTTPBadRequest)
class MigrateServerPolicyEnforcementV21(test.NoDBTestCase): class MigrateServerPolicyEnforcementV21(test.NoDBTestCase):
def setUp(self): def setUp(self):

View File

@ -0,0 +1,4 @@
---
features:
- When cold migrating a server, the ``host`` parameter is available
as of microversion 2.56. The target host is checked by the scheduler.