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",
|
"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"
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
|
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
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
|
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
|
||||||
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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)
|
||||||
|
@ -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),
|
||||||
|
@ -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")
|
||||||
|
@ -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'},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -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],
|
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):
|
||||||
|
@ -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()
|
||||||
|
@ -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,
|
||||||
|
@ -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": "",
|
||||||
|
@ -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",
|
||||||
|
@ -13039,6 +13039,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)
|
||||||
|
@ -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',
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -6411,6 +6411,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:
|
||||||
|
@ -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_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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user