Add new API to force live migration to complete

This change adds manual knob to force ongoing live migration to
complete. It is implemented as a new server-migrations API.

DocImpact
ApiImpact

Implements: blueprint pause-vm-during-live-migration
Change-Id: I034b4041414a797f65ede52db2963107f2ef7456
This commit is contained in:
Pawel Koniszewski 2016-02-08 08:59:52 +01:00
parent 23063011b1
commit c9091d0871
32 changed files with 511 additions and 8 deletions

View File

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

View File

@ -0,0 +1,7 @@
{
"os-migrateLive": {
"host": "01c0cadef72d47e28a672a76060d492c",
"block_migration": false,
"disk_over_commit": false
}
}

View File

@ -19,7 +19,7 @@
} }
], ],
"status": "CURRENT", "status": "CURRENT",
"version": "2.21", "version": "2.22",
"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.21", "version": "2.22",
"min_version": "2.1", "min_version": "2.1",
"updated": "2013-07-23T11:33:21Z" "updated": "2013-07-23T11:33:21Z"
} }

View File

@ -13,7 +13,7 @@
"disabled_reason": null, "disabled_reason": null,
"report_count": 1, "report_count": 1,
"forced_down": false, "forced_down": false,
"version": 6 "version": 7
} }
}, },
"event_type": "service.update", "event_type": "service.update",

View File

@ -270,6 +270,8 @@
"os_compute_api:servers:start": "rule:admin_or_owner", "os_compute_api:servers:start": "rule:admin_or_owner",
"os_compute_api:servers:stop": "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: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:discoverable": "",
"os_compute_api:os-access-ips": "", "os_compute_api:os-access-ips": "",
"os_compute_api:os-admin-actions": "rule:admin_api", "os_compute_api:os-admin-actions": "rule:admin_api",

View File

@ -64,7 +64,7 @@ REST_API_VERSION_HISTORY = """REST API Version History:
* 2.20 - Add attach and detach volume operations for instances in shelved * 2.20 - Add attach and detach volume operations for instances in shelved
and shelved_offloaded state and shelved_offloaded state
* 2.21 - Make os-instance-actions read deleted instances * 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 # 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 # 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.21" _MAX_API_VERSION = "2.22"
DEFAULT_API_VERSION = _MIN_API_VERSION DEFAULT_API_VERSION = _MIN_API_VERSION

View File

@ -82,7 +82,7 @@ v21_to_v2_extension_list_mapping = {
v2_extension_suppress_list = ['servers', 'images', 'versions', 'flavors', v2_extension_suppress_list = ['servers', 'images', 'versions', 'flavors',
'os-block-device-mapping-v1', 'os-consoles', 'os-block-device-mapping-v1', 'os-consoles',
'extensions', 'image-metadata', 'ips', 'limits', 'extensions', 'image-metadata', 'ips', 'limits',
'server-metadata' 'server-metadata', 'server-migrations'
] ]
# v2.1 plugins which should appear under a different name in v2 # v2.1 plugins which should appear under a different name in v2

View 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,
}

View 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 []

View File

@ -181,5 +181,17 @@ user documentation.
2.21 2.21
---- ----
The ``os-instance-actions`` API now returns information from deleted The ``os-instance-actions`` API now returns information from deleted
instances. 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
}

View File

@ -3287,6 +3287,35 @@ class API(base.Base):
host_name, block_migration=block_migration, host_name, block_migration=block_migration,
disk_over_commit=disk_over_commit) 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, @check_instance_state(vm_state=[vm_states.ACTIVE, vm_states.STOPPED,
vm_states.ERROR]) vm_states.ERROR])
def evacuate(self, context, instance, host, on_shared_storage, def evacuate(self, context, instance, host, on_shared_storage,

View File

@ -671,7 +671,7 @@ class ComputeVirtAPI(virtapi.VirtAPI):
class ComputeManager(manager.Manager): class ComputeManager(manager.Manager):
"""Manages the running instances from creation to destruction.""" """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 # How long to wait in seconds before re-issuing a shutdown
# signal to an instance during power off. The overall # signal to an instance during power off. The overall
@ -5250,6 +5250,29 @@ class ComputeManager(manager.Manager):
block_migration, migration, block_migration, migration,
migrate_data) 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): def _live_migration_cleanup_flags(self, block_migration, migrate_data):
"""Determine whether disks or instance path need to be cleaned up after """Determine whether disks or instance path need to be cleaned up after
live migration (at source on success, at destination on rollback) live migration (at source on success, at destination on rollback)

View File

@ -325,6 +325,7 @@ class ComputeAPI(object):
rollback_live_migration_at_destination, and rollback_live_migration_at_destination, and
pre_live_migration. pre_live_migration.
* ... - Remove refresh_provider_fw_rules() * ... - Remove refresh_provider_fw_rules()
* 4.9 - Add live_migration_force_complete()
''' '''
VERSION_ALIASES = { VERSION_ALIASES = {
@ -636,6 +637,13 @@ class ComputeAPI(object):
dest=dest, block_migration=block_migration, dest=dest, block_migration=block_migration,
migrate_data=migrate_data, **args) 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): def pause_instance(self, ctxt, instance):
version = '4.0' version = '4.0'
cctxt = self.client.prepare(server=_compute_host(None, instance), cctxt = self.client.prepare(server=_compute_host(None, instance),

View File

@ -1127,6 +1127,12 @@ class MigrationNotFoundForInstance(MigrationNotFound):
"%(instance_id)s") "%(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): class ConsoleLogOutputException(NovaException):
msg_fmt = _("Console log output could not be retrieved for instance " msg_fmt = _("Console log output could not be retrieved for instance "
"%(instance_id)s. Reason: %(reason)s") "%(instance_id)s. Reason: %(reason)s")

View File

@ -29,7 +29,7 @@ LOG = logging.getLogger(__name__)
# NOTE(danms): This is the global service version counter # 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 # NOTE(danms): This is our SERVICE_VERSION history. The idea is that any
@ -65,6 +65,8 @@ SERVICE_VERSION_HISTORY = (
{'compute_rpc': '4.7'}, {'compute_rpc': '4.7'},
# Version 6: Compute RPC version 4.8 # Version 6: Compute RPC version 4.8
{'compute_rpc': '4.8'}, {'compute_rpc': '4.8'},
# Version 7: Add live_migration_force_complete in the compute_rpc
{'compute_rpc': '4.9'},
) )

View File

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

View File

@ -0,0 +1,7 @@
{
"os-migrateLive": {
"host": "%(hostname)s",
"block_migration": false,
"disk_over_commit": false
}
}

View File

@ -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)

View 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())

View File

@ -3212,6 +3212,55 @@ class _ComputeAPIUnitTestMixIn(object):
self.assertEqual(expect_statuses[instance.uuid], self.assertEqual(expect_statuses[instance.uuid],
host_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): class ComputeAPIUnitTestCase(_ComputeAPIUnitTestMixIn, test.NoDBTestCase):
def setUp(self): def setUp(self):

View File

@ -4356,3 +4356,50 @@ class ComputeManagerMigrationTestCase(test.NoDBTestCase):
'foo', False, {}) 'foo', False, {})
self.assertIsInstance(mock_lmcf.call_args_list[0][0][1], self.assertIsInstance(mock_lmcf.call_args_list[0][0][1],
migrate_data_obj.LiveMigrateData) 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()

View File

@ -304,6 +304,11 @@ class ComputeRpcAPITestCase(test.NoDBTestCase):
migration='migration', migration='migration',
migrate_data={}, version='4.8') 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): def test_post_live_migration_at_destination(self):
self._test_compute_api('post_live_migration_at_destination', 'cast', self._test_compute_api('post_live_migration_at_destination', 'cast',
instance=self.fake_instance_obj, instance=self.fake_instance_obj,

View File

@ -125,6 +125,7 @@ policy_data = """
"os_compute_api:servers:start": "", "os_compute_api:servers:start": "",
"os_compute_api:servers:stop": "", "os_compute_api:servers:stop": "",
"os_compute_api:servers:trigger_crash_dump": "", "os_compute_api:servers:trigger_crash_dump": "",
"os_compute_api:servers:migrations:force_complete": "",
"os_compute_api:os-access-ips": "", "os_compute_api:os-access-ips": "",
"compute_extension:accounts": "", "compute_extension:accounts": "",
"compute_extension:admin_actions:pause": "", "compute_extension:admin_actions:pause": "",

View File

@ -300,6 +300,7 @@ class RealRolePolicyTestCase(test.NoDBTestCase):
"os_compute_api:servers:detail:get_all_tenants", "os_compute_api:servers:detail:get_all_tenants",
"os_compute_api:servers:index:get_all_tenants", "os_compute_api:servers:index:get_all_tenants",
"os_compute_api:servers:show:host_status", "os_compute_api:servers:show:host_status",
"os_compute_api:servers:migrations:force_complete",
"network:attach_external_network", "network:attach_external_network",
"os_compute_api:os-admin-actions", "os_compute_api:os-admin-actions",
"os_compute_api:os-admin-actions:reset_network", "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-usage:discoverable",
"os_compute_api:os-server-groups", "os_compute_api:os-server-groups",
"os_compute_api:os-server-groups:discoverable", "os_compute_api:os-server-groups:discoverable",
"os_compute_api:servers:migrations:discoverable",
"os_compute_api:os-services:discoverable", "os_compute_api:os-services:discoverable",
"os_compute_api:server-metadata:discoverable", "os_compute_api:server-metadata:discoverable",
"os_compute_api:servers:discoverable", "os_compute_api:servers:discoverable",

View File

@ -13015,6 +13015,12 @@ class LibvirtConnTestCase(test.NoDBTestCase):
lambda x: x, lambda x: x,
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('os.path.exists', return_value=True)
@mock.patch('tempfile.mkstemp') @mock.patch('tempfile.mkstemp')
@mock.patch('os.close', return_value=None) @mock.patch('os.close', return_value=None)

View File

@ -661,6 +661,11 @@ class _VirtDriverTestCase(_FakeDriverBackendTestCase):
self.connection.live_migration(self.ctxt, instance_ref, 'otherhost', self.connection.live_migration(self.ctxt, instance_ref, 'otherhost',
lambda *a: None, lambda *a: None) 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 @catch_notimplementederror
def _check_available_resource_fields(self, host_status): def _check_available_resource_fields(self, host_status):
keys = ['vcpus', 'memory_mb', 'local_gb', 'vcpus_used', keys = ['vcpus', 'memory_mb', 'local_gb', 'vcpus_used',

View File

@ -860,6 +860,14 @@ class ComputeDriver(object):
""" """
raise NotImplementedError() 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, def rollback_live_migration_at_destination(self, context, instance,
network_info, network_info,
block_device_info, block_device_info,

View File

@ -468,6 +468,9 @@ class FakeDriver(driver.ComputeDriver):
migrate_data) migrate_data)
return return
def live_migration_force_complete(self, instance):
return
def check_can_live_migrate_destination_cleanup(self, context, def check_can_live_migrate_destination_cleanup(self, context,
dest_check_data): dest_check_data):
return return

View File

@ -6413,6 +6413,12 @@ class LibvirtDriver(driver.ComputeDriver):
LOG.debug("Live migration monitoring is all done", LOG.debug("Live migration monitoring is all done",
instance=instance) 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, def _try_fetch_image(self, context, path, image_id, instance,
fallback_from_host=None): fallback_from_host=None):
try: try:

View File

@ -0,0 +1,4 @@
---
features:
- A new REST API to force live migration to complete has been added
in microversion 2.22.

View File

@ -137,6 +137,7 @@ nova.api.v21.extensions =
server_diagnostics = nova.api.openstack.compute.server_diagnostics:ServerDiagnostics server_diagnostics = nova.api.openstack.compute.server_diagnostics:ServerDiagnostics
server_external_events = nova.api.openstack.compute.server_external_events:ServerExternalEvents server_external_events = nova.api.openstack.compute.server_external_events:ServerExternalEvents
server_metadata = nova.api.openstack.compute.server_metadata:ServerMetadata 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_password = nova.api.openstack.compute.server_password:ServerPassword
server_usage = nova.api.openstack.compute.server_usage:ServerUsage server_usage = nova.api.openstack.compute.server_usage:ServerUsage
server_groups = nova.api.openstack.compute.server_groups:ServerGroups server_groups = nova.api.openstack.compute.server_groups:ServerGroups