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: I034b4041414a797f65ede52db2963107f2ef7456changes/21/245921/39
parent
23063011b1
commit
c9091d0871
@ -0,0 +1,3 @@
|
||||
{
|
||||
"force_complete": null
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
{
|
||||
"os-migrateLive": {
|
||||
"host": "01c0cadef72d47e28a672a76060d492c",
|
||||
"block_migration": false,
|
||||
"disk_over_commit": false
|
||||
}
|
||||
}
|
@ -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,
|
||||
}
|
@ -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 []
|
@ -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)
|
@ -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())
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
features:
|
||||
- A new REST API to force live migration to complete has been added
|
||||
in microversion 2.22.
|
Loading…
Reference in new issue