Merge "Add new API to force live migration to complete"
This commit is contained in:
commit
137ddac8ef
3
doc/api_samples/server-migrations/force_complete.json
Normal file
3
doc/api_samples/server-migrations/force_complete.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"force_complete": null
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
{
|
||||
"os-migrateLive": {
|
||||
"host": "01c0cadef72d47e28a672a76060d492c",
|
||||
"block_migration": false,
|
||||
"disk_over_commit": false
|
||||
}
|
||||
}
|
@ -19,7 +19,7 @@
|
||||
}
|
||||
],
|
||||
"status": "CURRENT",
|
||||
"version": "2.21",
|
||||
"version": "2.22",
|
||||
"min_version": "2.1",
|
||||
"updated": "2013-07-23T11:33:21Z"
|
||||
}
|
||||
|
@ -22,7 +22,7 @@
|
||||
}
|
||||
],
|
||||
"status": "CURRENT",
|
||||
"version": "2.21",
|
||||
"version": "2.22",
|
||||
"min_version": "2.1",
|
||||
"updated": "2013-07-23T11:33:21Z"
|
||||
}
|
||||
|
@ -13,7 +13,7 @@
|
||||
"disabled_reason": null,
|
||||
"report_count": 1,
|
||||
"forced_down": false,
|
||||
"version": 6
|
||||
"version": 7
|
||||
}
|
||||
},
|
||||
"event_type": "service.update",
|
||||
|
@ -270,6 +270,8 @@
|
||||
"os_compute_api:servers:start": "rule:admin_or_owner",
|
||||
"os_compute_api:servers:stop": "rule:admin_or_owner",
|
||||
"os_compute_api:servers:trigger_crash_dump": "rule:admin_or_owner",
|
||||
"os_compute_api:servers:migrations:discoverable": "",
|
||||
"os_compute_api:servers:migrations:force_complete": "rule:admin_api",
|
||||
"os_compute_api:os-access-ips:discoverable": "",
|
||||
"os_compute_api:os-access-ips": "",
|
||||
"os_compute_api:os-admin-actions": "rule:admin_api",
|
||||
|
@ -64,7 +64,7 @@ REST_API_VERSION_HISTORY = """REST API Version History:
|
||||
* 2.20 - Add attach and detach volume operations for instances in shelved
|
||||
and shelved_offloaded state
|
||||
* 2.21 - Make os-instance-actions read deleted instances
|
||||
|
||||
* 2.22 - Add API to force live migration to complete
|
||||
"""
|
||||
|
||||
# The minimum and maximum versions of the API supported
|
||||
@ -73,7 +73,7 @@ REST_API_VERSION_HISTORY = """REST API Version History:
|
||||
# Note(cyeoh): This only applies for the v2.1 API once microversions
|
||||
# support is fully merged. It does not affect the V2 API.
|
||||
_MIN_API_VERSION = "2.1"
|
||||
_MAX_API_VERSION = "2.21"
|
||||
_MAX_API_VERSION = "2.22"
|
||||
DEFAULT_API_VERSION = _MIN_API_VERSION
|
||||
|
||||
|
||||
|
@ -82,7 +82,7 @@ v21_to_v2_extension_list_mapping = {
|
||||
v2_extension_suppress_list = ['servers', 'images', 'versions', 'flavors',
|
||||
'os-block-device-mapping-v1', 'os-consoles',
|
||||
'extensions', 'image-metadata', 'ips', 'limits',
|
||||
'server-metadata'
|
||||
'server-metadata', 'server-migrations'
|
||||
]
|
||||
|
||||
# v2.1 plugins which should appear under a different name in v2
|
||||
|
26
nova/api/openstack/compute/schemas/server_migrations.py
Normal file
26
nova/api/openstack/compute/schemas/server_migrations.py
Normal file
@ -0,0 +1,26 @@
|
||||
# Copyright 2016 OpenStack Foundation
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
|
||||
force_complete = {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'force_complete': {
|
||||
'type': 'null'
|
||||
}
|
||||
},
|
||||
'required': ['force_complete'],
|
||||
'additionalProperties': False,
|
||||
}
|
78
nova/api/openstack/compute/server_migrations.py
Normal file
78
nova/api/openstack/compute/server_migrations.py
Normal file
@ -0,0 +1,78 @@
|
||||
# Copyright 2016 OpenStack Foundation
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from webob import exc
|
||||
|
||||
from nova.api.openstack import common
|
||||
from nova.api.openstack.compute.schemas import server_migrations
|
||||
from nova.api.openstack import extensions
|
||||
from nova.api.openstack import wsgi
|
||||
from nova.api import validation
|
||||
from nova import compute
|
||||
from nova import exception
|
||||
|
||||
ALIAS = 'servers:migrations'
|
||||
authorize = extensions.os_compute_authorizer(ALIAS)
|
||||
|
||||
|
||||
class ServerMigrationsController(wsgi.Controller):
|
||||
"""The server migrations API controller for the OpenStack API."""
|
||||
|
||||
def __init__(self):
|
||||
self.compute_api = compute.API(skip_policy_check=True)
|
||||
super(ServerMigrationsController, self).__init__()
|
||||
|
||||
@wsgi.Controller.api_version("2.22")
|
||||
@wsgi.response(202)
|
||||
@extensions.expected_errors((400, 403, 404, 409))
|
||||
@wsgi.action('force_complete')
|
||||
@validation.schema(server_migrations.force_complete)
|
||||
def _force_complete(self, req, id, server_id, body):
|
||||
context = req.environ['nova.context']
|
||||
authorize(context, action='force_complete')
|
||||
|
||||
instance = common.get_instance(self.compute_api, context, server_id)
|
||||
try:
|
||||
self.compute_api.live_migrate_force_complete(context, instance, id)
|
||||
except exception.InstanceNotFound as e:
|
||||
raise exc.HTTPNotFound(explanation=e.format_message())
|
||||
except (exception.MigrationNotFoundByStatus,
|
||||
exception.InvalidMigrationState,
|
||||
exception.MigrationNotFoundForInstance) as e:
|
||||
raise exc.HTTPBadRequest(explanation=e.format_message())
|
||||
except exception.InstanceIsLocked as e:
|
||||
raise exc.HTTPConflict(explanation=e.format_message())
|
||||
except exception.InstanceInvalidState as state_error:
|
||||
common.raise_http_conflict_for_instance_invalid_state(
|
||||
state_error, 'force_complete', server_id)
|
||||
|
||||
|
||||
class ServerMigrations(extensions.V21APIExtensionBase):
|
||||
"""Server Migrations API."""
|
||||
name = "ServerMigrations"
|
||||
alias = 'server-migrations'
|
||||
version = 1
|
||||
|
||||
def get_resources(self):
|
||||
parent = {'member_name': 'server',
|
||||
'collection_name': 'servers'}
|
||||
member_actions = {'action': 'POST'}
|
||||
resources = [extensions.ResourceExtension(
|
||||
'migrations', ServerMigrationsController(),
|
||||
parent=parent, member_actions=member_actions)]
|
||||
return resources
|
||||
|
||||
def get_controller_extensions(self):
|
||||
return []
|
@ -181,5 +181,17 @@ user documentation.
|
||||
|
||||
2.21
|
||||
----
|
||||
|
||||
The ``os-instance-actions`` API now returns information from deleted
|
||||
instances.
|
||||
|
||||
2.22
|
||||
----
|
||||
|
||||
A new resource servers:migrations added. A new API to force live migration
|
||||
to complete added::
|
||||
|
||||
POST /servers/<uuid>/migrations/<id>/action
|
||||
{
|
||||
"force_complete": null
|
||||
}
|
||||
|
@ -3287,6 +3287,35 @@ class API(base.Base):
|
||||
host_name, block_migration=block_migration,
|
||||
disk_over_commit=disk_over_commit)
|
||||
|
||||
@check_instance_lock
|
||||
@check_instance_cell
|
||||
@check_instance_state(vm_state=[vm_states.ACTIVE],
|
||||
task_state=[task_states.MIGRATING])
|
||||
def live_migrate_force_complete(self, context, instance, migration_id):
|
||||
"""Force live migration to complete.
|
||||
|
||||
:param context: Security context
|
||||
:param instance: The instance that is being migrated
|
||||
:param migration_id: ID of ongoing migration
|
||||
|
||||
"""
|
||||
LOG.debug("Going to try to force live migration to complete",
|
||||
instance=instance)
|
||||
|
||||
# NOTE(pkoniszewski): Get migration object to check if there is ongoing
|
||||
# live migration for particular instance. Also pass migration id to
|
||||
# compute to double check and avoid possible race condition.
|
||||
migration = objects.Migration.get_by_id_and_instance(
|
||||
context, migration_id, instance.uuid)
|
||||
if migration.status != 'running':
|
||||
raise exception.InvalidMigrationState(migration_id=migration_id,
|
||||
instance_uuid=instance.uuid,
|
||||
state=migration.status,
|
||||
method='force complete')
|
||||
|
||||
self.compute_rpcapi.live_migration_force_complete(
|
||||
context, instance, migration.id)
|
||||
|
||||
@check_instance_state(vm_state=[vm_states.ACTIVE, vm_states.STOPPED,
|
||||
vm_states.ERROR])
|
||||
def evacuate(self, context, instance, host, on_shared_storage,
|
||||
|
@ -671,7 +671,7 @@ class ComputeVirtAPI(virtapi.VirtAPI):
|
||||
class ComputeManager(manager.Manager):
|
||||
"""Manages the running instances from creation to destruction."""
|
||||
|
||||
target = messaging.Target(version='4.8')
|
||||
target = messaging.Target(version='4.9')
|
||||
|
||||
# How long to wait in seconds before re-issuing a shutdown
|
||||
# signal to an instance during power off. The overall
|
||||
@ -5250,6 +5250,29 @@ class ComputeManager(manager.Manager):
|
||||
block_migration, migration,
|
||||
migrate_data)
|
||||
|
||||
@wrap_exception()
|
||||
@wrap_instance_fault
|
||||
def live_migration_force_complete(self, context, instance, migration_id):
|
||||
"""Force live migration to complete.
|
||||
|
||||
:param context: Security context
|
||||
:param instance: The instance that is being migrated
|
||||
:param migration_id: ID of ongoing migration
|
||||
|
||||
"""
|
||||
migration = objects.Migration.get_by_id(context, migration_id)
|
||||
if migration.status != 'running':
|
||||
raise exception.InvalidMigrationState(migration_id=migration_id,
|
||||
instance_uuid=instance.uuid,
|
||||
state=migration.status,
|
||||
method='force complete')
|
||||
|
||||
self._notify_about_instance_usage(
|
||||
context, instance, 'live.migration.force.complete.start')
|
||||
self.driver.live_migration_force_complete(instance)
|
||||
self._notify_about_instance_usage(
|
||||
context, instance, 'live.migration.force.complete.end')
|
||||
|
||||
def _live_migration_cleanup_flags(self, block_migration, migrate_data):
|
||||
"""Determine whether disks or instance path need to be cleaned up after
|
||||
live migration (at source on success, at destination on rollback)
|
||||
|
@ -325,6 +325,7 @@ class ComputeAPI(object):
|
||||
rollback_live_migration_at_destination, and
|
||||
pre_live_migration.
|
||||
* ... - Remove refresh_provider_fw_rules()
|
||||
* 4.9 - Add live_migration_force_complete()
|
||||
'''
|
||||
|
||||
VERSION_ALIASES = {
|
||||
@ -636,6 +637,13 @@ class ComputeAPI(object):
|
||||
dest=dest, block_migration=block_migration,
|
||||
migrate_data=migrate_data, **args)
|
||||
|
||||
def live_migration_force_complete(self, ctxt, instance, migration_id):
|
||||
version = '4.9'
|
||||
cctxt = self.client.prepare(server=_compute_host(None, instance),
|
||||
version=version)
|
||||
cctxt.cast(ctxt, 'live_migration_force_complete', instance=instance,
|
||||
migration_id=migration_id)
|
||||
|
||||
def pause_instance(self, ctxt, instance):
|
||||
version = '4.0'
|
||||
cctxt = self.client.prepare(server=_compute_host(None, instance),
|
||||
|
@ -1127,6 +1127,12 @@ class MigrationNotFoundForInstance(MigrationNotFound):
|
||||
"%(instance_id)s")
|
||||
|
||||
|
||||
class InvalidMigrationState(Invalid):
|
||||
msg_fmt = _("Migration %(migration_id)s state of instance "
|
||||
"%(instance_uuid)s is %(state)s. Cannot %(method)s while the "
|
||||
"migration is in this state.")
|
||||
|
||||
|
||||
class ConsoleLogOutputException(NovaException):
|
||||
msg_fmt = _("Console log output could not be retrieved for instance "
|
||||
"%(instance_id)s. Reason: %(reason)s")
|
||||
|
@ -29,7 +29,7 @@ LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# NOTE(danms): This is the global service version counter
|
||||
SERVICE_VERSION = 6
|
||||
SERVICE_VERSION = 7
|
||||
|
||||
|
||||
# NOTE(danms): This is our SERVICE_VERSION history. The idea is that any
|
||||
@ -65,6 +65,8 @@ SERVICE_VERSION_HISTORY = (
|
||||
{'compute_rpc': '4.7'},
|
||||
# Version 6: Compute RPC version 4.8
|
||||
{'compute_rpc': '4.8'},
|
||||
# Version 7: Add live_migration_force_complete in the compute_rpc
|
||||
{'compute_rpc': '4.9'},
|
||||
)
|
||||
|
||||
|
||||
|
@ -0,0 +1,3 @@
|
||||
{
|
||||
"force_complete": null
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
{
|
||||
"os-migrateLive": {
|
||||
"host": "%(hostname)s",
|
||||
"block_migration": false,
|
||||
"disk_over_commit": false
|
||||
}
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
# Copyright 2016 OpenStack Foundation
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import mock
|
||||
|
||||
from nova.conductor import manager as conductor_manager
|
||||
from nova import db
|
||||
from nova import objects
|
||||
from nova.tests.functional.api_sample_tests import test_servers
|
||||
|
||||
|
||||
class ServerMigrationsSampleJsonTest(test_servers.ServersSampleBase):
|
||||
extension_name = 'server-migrations'
|
||||
scenarios = [('v2_22', {'api_major_version': 'v2.1'})]
|
||||
extra_extensions_to_load = ["os-migrate-server", "os-access-ips"]
|
||||
|
||||
def setUp(self):
|
||||
"""setUp method for server usage."""
|
||||
super(ServerMigrationsSampleJsonTest, self).setUp()
|
||||
self.uuid = self._post_server()
|
||||
|
||||
@mock.patch.object(conductor_manager.ComputeTaskManager, '_live_migrate')
|
||||
@mock.patch.object(db, 'service_get_by_compute_host')
|
||||
@mock.patch.object(objects.Migration, 'get_by_id_and_instance')
|
||||
@mock.patch('nova.compute.manager.ComputeManager.'
|
||||
'live_migration_force_complete')
|
||||
def test_live_migrate_force_complete(self, live_migration_pause_instance,
|
||||
get_by_id_and_instance,
|
||||
service_get_by_compute_host,
|
||||
_live_migrate):
|
||||
migration = objects.Migration()
|
||||
migration.id = 1
|
||||
migration.status = 'running'
|
||||
get_by_id_and_instance.return_value = migration
|
||||
self._do_post('servers/%s/action' % self.uuid, 'live-migrate-server',
|
||||
{'hostname': self.compute.host})
|
||||
response = self._do_post('servers/%s/migrations/%s/action'
|
||||
% (self.uuid, '3'), 'force_complete',
|
||||
{}, api_version='2.22')
|
||||
self.assertEqual(202, response.status_code)
|
108
nova/tests/unit/api/openstack/compute/test_server_migrations.py
Normal file
108
nova/tests/unit/api/openstack/compute/test_server_migrations.py
Normal file
@ -0,0 +1,108 @@
|
||||
# Copyright 2016 OpenStack Foundation
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import mock
|
||||
import webob
|
||||
|
||||
from nova.api.openstack.compute import server_migrations
|
||||
from nova import exception
|
||||
from nova import test
|
||||
from nova.tests.unit.api.openstack import fakes
|
||||
|
||||
|
||||
class ServerMigrationsTestsV21(test.NoDBTestCase):
|
||||
wsgi_api_version = '2.22'
|
||||
|
||||
def setUp(self):
|
||||
super(ServerMigrationsTestsV21, self).setUp()
|
||||
self.req = fakes.HTTPRequest.blank('', version=self.wsgi_api_version)
|
||||
self.context = self.req.environ['nova.context']
|
||||
self.controller = server_migrations.ServerMigrationsController()
|
||||
self.compute_api = self.controller.compute_api
|
||||
|
||||
def test_force_complete_succeeded(self):
|
||||
@mock.patch.object(self.compute_api, 'live_migrate_force_complete')
|
||||
@mock.patch.object(self.compute_api, 'get')
|
||||
def _do_test(compute_api_get, live_migrate_force_complete):
|
||||
self.controller._force_complete(self.req, '1', '1',
|
||||
body={'force_complete': None})
|
||||
live_migrate_force_complete.assert_called_once_with(
|
||||
self.context, compute_api_get(), '1')
|
||||
_do_test()
|
||||
|
||||
def _test_force_complete_failed_with_exception(self, fake_exc,
|
||||
expected_exc):
|
||||
@mock.patch.object(self.compute_api, 'live_migrate_force_complete',
|
||||
side_effect=fake_exc)
|
||||
@mock.patch.object(self.compute_api, 'get')
|
||||
def _do_test(compute_api_get, live_migrate_force_complete):
|
||||
self.assertRaises(expected_exc,
|
||||
self.controller._force_complete,
|
||||
self.req, '1', '1',
|
||||
body={'force_complete': None})
|
||||
_do_test()
|
||||
|
||||
def test_force_complete_instance_not_migrating(self):
|
||||
self._test_force_complete_failed_with_exception(
|
||||
exception.InstanceInvalidState(instance_uuid='', state='',
|
||||
attr='', method=''),
|
||||
webob.exc.HTTPConflict)
|
||||
|
||||
def test_force_complete_migration_not_found(self):
|
||||
self._test_force_complete_failed_with_exception(
|
||||
exception.MigrationNotFoundByStatus(instance_id='', status=''),
|
||||
webob.exc.HTTPBadRequest)
|
||||
|
||||
def test_force_complete_instance_is_locked(self):
|
||||
self._test_force_complete_failed_with_exception(
|
||||
exception.InstanceIsLocked(instance_uuid=''),
|
||||
webob.exc.HTTPConflict)
|
||||
|
||||
def test_force_complete_invalid_migration_state(self):
|
||||
self._test_force_complete_failed_with_exception(
|
||||
exception.InvalidMigrationState(migration_id='', instance_uuid='',
|
||||
state='', method=''),
|
||||
webob.exc.HTTPBadRequest)
|
||||
|
||||
def test_force_complete_instance_not_found(self):
|
||||
self._test_force_complete_failed_with_exception(
|
||||
exception.InstanceNotFound(instance_id=''),
|
||||
webob.exc.HTTPNotFound)
|
||||
|
||||
def test_force_complete_unexpected_error(self):
|
||||
self._test_force_complete_failed_with_exception(
|
||||
exception.NovaException(), webob.exc.HTTPInternalServerError)
|
||||
|
||||
|
||||
class ServerMigrationsPolicyEnforcementV21(test.NoDBTestCase):
|
||||
wsgi_api_version = '2.22'
|
||||
|
||||
def setUp(self):
|
||||
super(ServerMigrationsPolicyEnforcementV21, self).setUp()
|
||||
self.controller = server_migrations.ServerMigrationsController()
|
||||
self.req = fakes.HTTPRequest.blank('', version=self.wsgi_api_version)
|
||||
|
||||
def test_migrate_live_policy_failed(self):
|
||||
rule_name = "os_compute_api:servers:migrations:force_complete"
|
||||
self.policy.set_rules({rule_name: "project:non_fake"})
|
||||
body_args = {'force_complete': None}
|
||||
exc = self.assertRaises(
|
||||
exception.PolicyNotAuthorized,
|
||||
self.controller._force_complete, self.req,
|
||||
fakes.FAKE_UUID, fakes.FAKE_UUID,
|
||||
body=body_args)
|
||||
self.assertEqual(
|
||||
"Policy doesn't allow %s to be performed." % rule_name,
|
||||
exc.format_message())
|
@ -3213,6 +3213,55 @@ class _ComputeAPIUnitTestMixIn(object):
|
||||
self.assertEqual(expect_statuses[instance.uuid],
|
||||
host_statuses[instance.uuid])
|
||||
|
||||
@mock.patch.object(objects.Migration, 'get_by_id_and_instance')
|
||||
def test_live_migrate_force_complete_succeeded(
|
||||
self, get_by_id_and_instance):
|
||||
|
||||
if self.cell_type == 'api':
|
||||
# cell api has not been implemented.
|
||||
return
|
||||
rpcapi = self.compute_api.compute_rpcapi
|
||||
|
||||
instance = self._create_instance_obj()
|
||||
instance.task_state = task_states.MIGRATING
|
||||
|
||||
migration = objects.Migration()
|
||||
migration.id = 0
|
||||
migration.status = 'running'
|
||||
get_by_id_and_instance.return_value = migration
|
||||
|
||||
with mock.patch.object(
|
||||
rpcapi, 'live_migration_force_complete') as lm_force_complete:
|
||||
self.compute_api.live_migrate_force_complete(
|
||||
self.context, instance, migration.id)
|
||||
|
||||
lm_force_complete.assert_called_once_with(self.context,
|
||||
instance,
|
||||
0)
|
||||
|
||||
@mock.patch.object(objects.Migration, 'get_by_id_and_instance')
|
||||
def test_live_migrate_force_complete_invalid_migration_state(
|
||||
self, get_by_id_and_instance):
|
||||
instance = self._create_instance_obj()
|
||||
instance.task_state = task_states.MIGRATING
|
||||
|
||||
migration = objects.Migration()
|
||||
migration.id = 0
|
||||
migration.status = 'error'
|
||||
get_by_id_and_instance.return_value = migration
|
||||
|
||||
self.assertRaises(exception.InvalidMigrationState,
|
||||
self.compute_api.live_migrate_force_complete,
|
||||
self.context, instance, migration.id)
|
||||
|
||||
def test_live_migrate_force_complete_invalid_vm_state(self):
|
||||
instance = self._create_instance_obj()
|
||||
instance.task_state = None
|
||||
|
||||
self.assertRaises(exception.InstanceInvalidState,
|
||||
self.compute_api.live_migrate_force_complete,
|
||||
self.context, instance, '1')
|
||||
|
||||
|
||||
class ComputeAPIUnitTestCase(_ComputeAPIUnitTestMixIn, test.NoDBTestCase):
|
||||
def setUp(self):
|
||||
|
@ -4356,3 +4356,50 @@ class ComputeManagerMigrationTestCase(test.NoDBTestCase):
|
||||
'foo', False, {})
|
||||
self.assertIsInstance(mock_lmcf.call_args_list[0][0][1],
|
||||
migrate_data_obj.LiveMigrateData)
|
||||
|
||||
def test_live_migration_force_complete_succeeded(self):
|
||||
|
||||
instance = objects.Instance(uuid=str(uuid.uuid4()))
|
||||
migration = objects.Migration()
|
||||
migration.status = 'running'
|
||||
migration.id = 0
|
||||
|
||||
@mock.patch.object(self.compute, '_notify_about_instance_usage')
|
||||
@mock.patch.object(objects.Migration, 'get_by_id',
|
||||
return_value=migration)
|
||||
@mock.patch.object(self.compute.driver,
|
||||
'live_migration_force_complete')
|
||||
def _do_test(force_complete, get_by_id, _notify_about_instance_usage):
|
||||
self.compute.live_migration_force_complete(
|
||||
self.context, instance, migration.id)
|
||||
|
||||
force_complete.assert_called_once_with(instance)
|
||||
|
||||
_notify_usage_calls = [
|
||||
mock.call(self.context, instance,
|
||||
'live.migration.force.complete.start'),
|
||||
mock.call(self.context, instance,
|
||||
'live.migration.force.complete.end')
|
||||
]
|
||||
|
||||
_notify_about_instance_usage.assert_has_calls(_notify_usage_calls)
|
||||
|
||||
_do_test()
|
||||
|
||||
@mock.patch.object(compute_utils, 'add_instance_fault_from_exc')
|
||||
def test_live_migration_pause_vm_invalid_migration_state(
|
||||
self, add_instance_fault_from_exc):
|
||||
|
||||
instance = objects.Instance(id=1234, uuid=str(uuid.uuid4()))
|
||||
migration = objects.Migration()
|
||||
migration.status = 'aborted'
|
||||
migration.id = 0
|
||||
|
||||
@mock.patch.object(objects.Migration, 'get_by_id',
|
||||
return_value=migration)
|
||||
def _do_test(get_by_id):
|
||||
self.assertRaises(exception.InvalidMigrationState,
|
||||
self.compute.live_migration_force_complete,
|
||||
self.context, instance, migration.id)
|
||||
|
||||
_do_test()
|
||||
|
@ -304,6 +304,11 @@ class ComputeRpcAPITestCase(test.NoDBTestCase):
|
||||
migration='migration',
|
||||
migrate_data={}, version='4.8')
|
||||
|
||||
def test_live_migration_force_complete(self):
|
||||
self._test_compute_api('live_migration_force_complete', 'cast',
|
||||
instance=self.fake_instance_obj,
|
||||
migration_id='1', version='4.9')
|
||||
|
||||
def test_post_live_migration_at_destination(self):
|
||||
self._test_compute_api('post_live_migration_at_destination', 'cast',
|
||||
instance=self.fake_instance_obj,
|
||||
|
@ -125,6 +125,7 @@ policy_data = """
|
||||
"os_compute_api:servers:start": "",
|
||||
"os_compute_api:servers:stop": "",
|
||||
"os_compute_api:servers:trigger_crash_dump": "",
|
||||
"os_compute_api:servers:migrations:force_complete": "",
|
||||
"os_compute_api:os-access-ips": "",
|
||||
"compute_extension:accounts": "",
|
||||
"compute_extension:admin_actions:pause": "",
|
||||
|
@ -300,6 +300,7 @@ class RealRolePolicyTestCase(test.NoDBTestCase):
|
||||
"os_compute_api:servers:detail:get_all_tenants",
|
||||
"os_compute_api:servers:index:get_all_tenants",
|
||||
"os_compute_api:servers:show:host_status",
|
||||
"os_compute_api:servers:migrations:force_complete",
|
||||
"network:attach_external_network",
|
||||
"os_compute_api:os-admin-actions",
|
||||
"os_compute_api:os-admin-actions:reset_network",
|
||||
@ -672,6 +673,7 @@ class RealRolePolicyTestCase(test.NoDBTestCase):
|
||||
"os_compute_api:os-server-usage:discoverable",
|
||||
"os_compute_api:os-server-groups",
|
||||
"os_compute_api:os-server-groups:discoverable",
|
||||
"os_compute_api:servers:migrations:discoverable",
|
||||
"os_compute_api:os-services:discoverable",
|
||||
"os_compute_api:server-metadata:discoverable",
|
||||
"os_compute_api:servers:discoverable",
|
||||
|
@ -13039,6 +13039,12 @@ class LibvirtConnTestCase(test.NoDBTestCase):
|
||||
lambda x: x,
|
||||
lambda x: x)
|
||||
|
||||
@mock.patch.object(libvirt_driver.LibvirtDriver, "pause")
|
||||
def test_live_migration_force_complete(self, pause):
|
||||
drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False)
|
||||
drvr.live_migration_force_complete(self.test_instance)
|
||||
pause.assert_called_once_with(self.test_instance)
|
||||
|
||||
@mock.patch('os.path.exists', return_value=True)
|
||||
@mock.patch('tempfile.mkstemp')
|
||||
@mock.patch('os.close', return_value=None)
|
||||
|
@ -661,6 +661,11 @@ class _VirtDriverTestCase(_FakeDriverBackendTestCase):
|
||||
self.connection.live_migration(self.ctxt, instance_ref, 'otherhost',
|
||||
lambda *a: None, lambda *a: None)
|
||||
|
||||
@catch_notimplementederror
|
||||
def test_live_migration_force_complete(self):
|
||||
instance_ref, network_info = self._get_running_instance()
|
||||
self.connection.live_migration_force_complete(instance_ref)
|
||||
|
||||
@catch_notimplementederror
|
||||
def _check_available_resource_fields(self, host_status):
|
||||
keys = ['vcpus', 'memory_mb', 'local_gb', 'vcpus_used',
|
||||
|
@ -860,6 +860,14 @@ class ComputeDriver(object):
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def live_migration_force_complete(self, instance):
|
||||
"""Force live migration to complete
|
||||
|
||||
:param instance: Instance being live migrated
|
||||
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def rollback_live_migration_at_destination(self, context, instance,
|
||||
network_info,
|
||||
block_device_info,
|
||||
|
@ -468,6 +468,9 @@ class FakeDriver(driver.ComputeDriver):
|
||||
migrate_data)
|
||||
return
|
||||
|
||||
def live_migration_force_complete(self, instance):
|
||||
return
|
||||
|
||||
def check_can_live_migrate_destination_cleanup(self, context,
|
||||
dest_check_data):
|
||||
return
|
||||
|
@ -6411,6 +6411,12 @@ class LibvirtDriver(driver.ComputeDriver):
|
||||
LOG.debug("Live migration monitoring is all done",
|
||||
instance=instance)
|
||||
|
||||
def live_migration_force_complete(self, instance):
|
||||
# NOTE(pkoniszewski): currently only pause during live migration is
|
||||
# supported to force live migration to complete, so just try to pause
|
||||
# the instance
|
||||
self.pause(instance)
|
||||
|
||||
def _try_fetch_image(self, context, path, image_id, instance,
|
||||
fallback_from_host=None):
|
||||
try:
|
||||
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
features:
|
||||
- A new REST API to force live migration to complete has been added
|
||||
in microversion 2.22.
|
@ -137,6 +137,7 @@ nova.api.v21.extensions =
|
||||
server_diagnostics = nova.api.openstack.compute.server_diagnostics:ServerDiagnostics
|
||||
server_external_events = nova.api.openstack.compute.server_external_events:ServerExternalEvents
|
||||
server_metadata = nova.api.openstack.compute.server_metadata:ServerMetadata
|
||||
server_migrations = nova.api.openstack.compute.server_migrations:ServerMigrations
|
||||
server_password = nova.api.openstack.compute.server_password:ServerPassword
|
||||
server_usage = nova.api.openstack.compute.server_usage:ServerUsage
|
||||
server_groups = nova.api.openstack.compute.server_groups:ServerGroups
|
||||
|
Loading…
Reference in New Issue
Block a user