From 830ac0f0654dc989ff27ae9314bd6af4902daf9b Mon Sep 17 00:00:00 2001 From: Chris Yeoh Date: Sun, 1 Dec 2013 23:48:09 +1030 Subject: [PATCH] Adds migrate server extension for V3 API Moves the migrate/live-migrate server functionality out of admin_actions into its own extension. This part of the blueprint v3-admin-actions-split allows more selective enablement of features contained in the admin actions extension. Note the XML api samples are no longer generated because bp remove-v3-xml-api has been approved. Refactor removes some exception handling for migrate along with the relevant tests as those exceptions will never occur. Partially implements bp v3-api-admin-actions-split DocImpact: Adds os-migrate-server extension and moves migrate/live-migrate functionality out of os-admin-actions into this new extension. Change-Id: I6b29f501ad6bb9a6401b1c20cd419d9e05fe369b --- .../live-migrate-server.json} | 0 .../migrate-server.json} | 0 .../os-migrate-server/server-post-req.json | 10 + .../os-migrate-server/server-post-resp.json | 16 ++ etc/nova/policy.json | 5 +- .../compute/plugins/v3/admin_actions.py | 72 ------- .../compute/plugins/v3/migrate_server.py | 126 ++++++++++++ .../plugins/v3/admin_only_action_common.py | 34 +++- .../compute/plugins/v3/test_admin_actions.py | 161 +-------------- .../compute/plugins/v3/test_migrate_server.py | 186 ++++++++++++++++++ nova/tests/fake_policy.py | 4 +- .../live-migrate-server.json.tpl} | 0 .../migrate-server.json.tpl} | 0 .../server-post-req.json.tpl | 10 + .../server-post-resp.json.tpl | 16 ++ .../tests/integrated/v3/test_admin_actions.py | 37 ---- .../integrated/v3/test_migrate_server.py | 66 +++++++ setup.cfg | 1 + 18 files changed, 468 insertions(+), 276 deletions(-) rename doc/v3/api_samples/{os-admin-actions/admin-actions-live-migrate.json => os-migrate-server/live-migrate-server.json} (100%) rename doc/v3/api_samples/{os-admin-actions/admin-actions-migrate.json => os-migrate-server/migrate-server.json} (100%) create mode 100644 doc/v3/api_samples/os-migrate-server/server-post-req.json create mode 100644 doc/v3/api_samples/os-migrate-server/server-post-resp.json create mode 100644 nova/api/openstack/compute/plugins/v3/migrate_server.py create mode 100644 nova/tests/api/openstack/compute/plugins/v3/test_migrate_server.py rename nova/tests/integrated/v3/api_samples/{os-admin-actions/admin-actions-live-migrate.json.tpl => os-migrate-server/live-migrate-server.json.tpl} (100%) rename nova/tests/integrated/v3/api_samples/{os-admin-actions/admin-actions-migrate.json.tpl => os-migrate-server/migrate-server.json.tpl} (100%) create mode 100644 nova/tests/integrated/v3/api_samples/os-migrate-server/server-post-req.json.tpl create mode 100644 nova/tests/integrated/v3/api_samples/os-migrate-server/server-post-resp.json.tpl create mode 100644 nova/tests/integrated/v3/test_migrate_server.py diff --git a/doc/v3/api_samples/os-admin-actions/admin-actions-live-migrate.json b/doc/v3/api_samples/os-migrate-server/live-migrate-server.json similarity index 100% rename from doc/v3/api_samples/os-admin-actions/admin-actions-live-migrate.json rename to doc/v3/api_samples/os-migrate-server/live-migrate-server.json diff --git a/doc/v3/api_samples/os-admin-actions/admin-actions-migrate.json b/doc/v3/api_samples/os-migrate-server/migrate-server.json similarity index 100% rename from doc/v3/api_samples/os-admin-actions/admin-actions-migrate.json rename to doc/v3/api_samples/os-migrate-server/migrate-server.json diff --git a/doc/v3/api_samples/os-migrate-server/server-post-req.json b/doc/v3/api_samples/os-migrate-server/server-post-req.json new file mode 100644 index 000000000000..2eedab6147cf --- /dev/null +++ b/doc/v3/api_samples/os-migrate-server/server-post-req.json @@ -0,0 +1,10 @@ +{ + "server" : { + "name" : "new-server-test", + "image_ref" : "http://glance.openstack.example.com/images/70a599e0-31e7-49b7-b260-868f441e862b", + "flavor_ref" : "http://openstack.example.com/flavors/1", + "metadata" : { + "My Server Name" : "Apache1" + } + } +} diff --git a/doc/v3/api_samples/os-migrate-server/server-post-resp.json b/doc/v3/api_samples/os-migrate-server/server-post-resp.json new file mode 100644 index 000000000000..270cb8463484 --- /dev/null +++ b/doc/v3/api_samples/os-migrate-server/server-post-resp.json @@ -0,0 +1,16 @@ +{ + "server": { + "admin_password": "DM3QzjhGTzLB", + "id": "bebeec79-497e-4711-a311-d0d2e3dfc73b", + "links": [ + { + "href": "http://openstack.example.com/v3/servers/bebeec79-497e-4711-a311-d0d2e3dfc73b", + "rel": "self" + }, + { + "href": "http://openstack.example.com/servers/bebeec79-497e-4711-a311-d0d2e3dfc73b", + "rel": "bookmark" + } + ] + } +} diff --git a/etc/nova/policy.json b/etc/nova/policy.json index 378032b04e49..2d50b9d27377 100644 --- a/etc/nova/policy.json +++ b/etc/nova/policy.json @@ -42,9 +42,7 @@ "compute_extension:v3:os-admin-actions:reset_network": "rule:admin_api", "compute_extension:v3:os-admin-actions:inject_network_info": "rule:admin_api", "compute_extension:v3:os-admin-actions:create_backup": "rule:admin_or_owner", - "compute_extension:v3:os-admin-actions:migrate_live": "rule:admin_api", "compute_extension:v3:os-admin-actions:reset_state": "rule:admin_api", - "compute_extension:v3:os-admin-actions:migrate": "rule:admin_api", "compute_extension:v3:os-admin-password": "", "compute_extension:v3:os-admin-password:discoverable": "", "compute_extension:aggregates": "rule:admin_api", @@ -174,6 +172,9 @@ "compute_extension:v3:os-lock-server:discoverable": "", "compute_extension:v3:os-lock-server:lock": "rule:admin_or_owner", "compute_extension:v3:os-lock-server:unlock": "rule:admin_or_owner", + "compute_extension:v3:os-migrate-server:discoverable": "", + "compute_extension:v3:os-migrate-server:migrate": "rule:admin_api", + "compute_extension:v3:os-migrate-server:migrate_live": "rule:admin_api", "compute_extension:multinic": "", "compute_extension:v3:os-multinic": "", "compute_extension:v3:os-multinic:discoverable": "", diff --git a/nova/api/openstack/compute/plugins/v3/admin_actions.py b/nova/api/openstack/compute/plugins/v3/admin_actions.py index 617d969a162a..1dc51f20c9a6 100644 --- a/nova/api/openstack/compute/plugins/v3/admin_actions.py +++ b/nova/api/openstack/compute/plugins/v3/admin_actions.py @@ -25,7 +25,6 @@ from nova.compute import vm_states from nova import exception from nova.openstack.common.gettextutils import _ from nova.openstack.common import log as logging -from nova.openstack.common import strutils LOG = logging.getLogger(__name__) ALIAS = "os-admin-actions" @@ -44,34 +43,6 @@ class AdminActionsController(wsgi.Controller): super(AdminActionsController, self).__init__(*args, **kwargs) self.compute_api = compute.API() - @extensions.expected_errors((400, 404, 409, 413)) - @wsgi.action('migrate') - def _migrate(self, req, id, body): - """Permit admins to migrate a server to a new host.""" - context = req.environ['nova.context'] - authorize(context, 'migrate') - - instance = common.get_instance(self.compute_api, context, id, - want_objects=True) - try: - self.compute_api.resize(req.environ['nova.context'], instance) - except exception.QuotaError as error: - raise exc.HTTPRequestEntityTooLarge( - explanation=error.format_message(), - headers={'Retry-After': 0}) - 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, - 'migrate') - except exception.FlavorNotFound as e: - raise exc.HTTPNotFound(explanation=e.format_message()) - except exception.CannotResizeToSameFlavor as e: - raise exc.HTTPBadRequest(explanation=e.format_message()) - except exception.TooManyInstances as e: - raise exc.HTTPRequestEntityTooLarge(explanation=e.format_message()) - return webob.Response(status_int=202) - @extensions.expected_errors((404, 409)) @wsgi.action('reset_network') def _reset_network(self, req, id, body): @@ -168,49 +139,6 @@ class AdminActionsController(wsgi.Controller): return resp - @extensions.expected_errors((400, 404, 409)) - @wsgi.action('migrate_live') - def _migrate_live(self, req, id, body): - """Permit admins to (live) migrate a server to a new host.""" - context = req.environ["nova.context"] - authorize(context, 'migrate_live') - - try: - block_migration = body["migrate_live"]["block_migration"] - disk_over_commit = body["migrate_live"]["disk_over_commit"] - host = body["migrate_live"]["host"] - except (TypeError, KeyError): - msg = _("host, block_migration and disk_over_commit must " - "be specified for live migration.") - raise exc.HTTPBadRequest(explanation=msg) - - try: - block_migration = strutils.bool_from_string(block_migration, - strict=True) - disk_over_commit = strutils.bool_from_string(disk_over_commit, - strict=True) - except ValueError as err: - raise exc.HTTPBadRequest(explanation=str(err)) - - try: - instance = common.get_instance(self.compute_api, context, id, - want_objects=True) - self.compute_api.live_migrate(context, instance, block_migration, - disk_over_commit, host) - except (exception.ComputeServiceUnavailable, - exception.InvalidHypervisorType, - exception.UnableToMigrateToSelf, - exception.DestinationHypervisorTooOld, - exception.NoValidHost, - exception.InvalidLocalStorage, - exception.InvalidSharedStorage, - exception.MigrationPreCheckError) as ex: - raise exc.HTTPBadRequest(explanation=ex.format_message()) - except exception.InstanceInvalidState as state_error: - common.raise_http_conflict_for_instance_invalid_state(state_error, - 'migrate_live') - return webob.Response(status_int=202) - @extensions.expected_errors((400, 404)) @wsgi.action('reset_state') def _reset_state(self, req, id, body): diff --git a/nova/api/openstack/compute/plugins/v3/migrate_server.py b/nova/api/openstack/compute/plugins/v3/migrate_server.py new file mode 100644 index 000000000000..b82df9863c4e --- /dev/null +++ b/nova/api/openstack/compute/plugins/v3/migrate_server.py @@ -0,0 +1,126 @@ +# Copyright 2011 OpenStack Foundation +# Copyright 2013 IBM Corp. +# +# 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 webob +from webob import exc + +from nova.api.openstack import common +from nova.api.openstack import extensions +from nova.api.openstack import wsgi +from nova import compute +from nova import exception +from nova.openstack.common.gettextutils import _ +from nova.openstack.common import log as logging +from nova.openstack.common import strutils + +LOG = logging.getLogger(__name__) +ALIAS = "os-migrate-server" + + +def authorize(context, action_name): + action = 'v3:%s:%s' % (ALIAS, action_name) + extensions.extension_authorizer('compute', action)(context) + + +class MigrateServerController(wsgi.Controller): + def __init__(self, *args, **kwargs): + super(MigrateServerController, self).__init__(*args, **kwargs) + self.compute_api = compute.API() + + @extensions.expected_errors((400, 404, 409, 413)) + @wsgi.action('migrate') + def _migrate(self, req, id, body): + """Permit admins to migrate a server to a new host.""" + context = req.environ['nova.context'] + authorize(context, 'migrate') + + instance = common.get_instance(self.compute_api, context, id, + want_objects=True) + try: + self.compute_api.resize(req.environ['nova.context'], instance) + except exception.TooManyInstances as e: + raise exc.HTTPRequestEntityTooLarge(explanation=e.format_message()) + except exception.QuotaError as error: + raise exc.HTTPRequestEntityTooLarge( + explanation=error.format_message(), + headers={'Retry-After': 0}) + 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, + 'migrate') + except exception.InstanceNotFound as e: + raise exc.HTTPNotFound(explanation=e.format_message()) + return webob.Response(status_int=202) + + @extensions.expected_errors((400, 404, 409)) + @wsgi.action('migrate_live') + def _migrate_live(self, req, id, body): + """Permit admins to (live) migrate a server to a new host.""" + context = req.environ["nova.context"] + authorize(context, 'migrate_live') + + try: + block_migration = body["migrate_live"]["block_migration"] + disk_over_commit = body["migrate_live"]["disk_over_commit"] + host = body["migrate_live"]["host"] + except (TypeError, KeyError): + msg = _("host, block_migration and disk_over_commit must " + "be specified for live migration.") + raise exc.HTTPBadRequest(explanation=msg) + + try: + block_migration = strutils.bool_from_string(block_migration, + strict=True) + disk_over_commit = strutils.bool_from_string(disk_over_commit, + strict=True) + except ValueError as err: + raise exc.HTTPBadRequest(explanation=str(err)) + + try: + instance = common.get_instance(self.compute_api, context, id, + want_objects=True) + self.compute_api.live_migrate(context, instance, block_migration, + disk_over_commit, host) + except (exception.ComputeServiceUnavailable, + exception.InvalidHypervisorType, + exception.UnableToMigrateToSelf, + exception.DestinationHypervisorTooOld, + exception.NoValidHost, + exception.InvalidLocalStorage, + exception.InvalidSharedStorage, + exception.MigrationPreCheckError) as ex: + raise exc.HTTPBadRequest(explanation=ex.format_message()) + except exception.InstanceInvalidState as state_error: + common.raise_http_conflict_for_instance_invalid_state(state_error, + 'migrate_live') + return webob.Response(status_int=202) + + +class MigrateServer(extensions.V3APIExtensionBase): + """Enable migrate and live-migrate server actions.""" + + name = "MigrateServer" + alias = ALIAS + namespace = "http://docs.openstack.org/compute/ext/%s/api/v3" % ALIAS + version = 1 + + def get_controller_extensions(self): + controller = MigrateServerController() + extension = extensions.ControllerExtension(self, 'servers', controller) + return [extension] + + def get_resources(self): + return [] diff --git a/nova/tests/api/openstack/compute/plugins/v3/admin_only_action_common.py b/nova/tests/api/openstack/compute/plugins/v3/admin_only_action_common.py index 3946f964389e..39c12b639322 100644 --- a/nova/tests/api/openstack/compute/plugins/v3/admin_only_action_common.py +++ b/nova/tests/api/openstack/compute/plugins/v3/admin_only_action_common.py @@ -69,17 +69,21 @@ class CommonMixin(object): self.mox.VerifyAll() self.mox.UnsetStubs() - def _test_action(self, action, body=None, method=None): + def _test_action(self, action, body=None, method=None, + compute_api_args_map={}): if method is None: method = action instance = self._stub_instance_get() - getattr(self.compute_api, method)(self.context, instance) + + args, kwargs = compute_api_args_map.get(action, ((), {})) + getattr(self.compute_api, method)(self.context, instance, *args, + **kwargs) self.mox.ReplayAll() res = self._make_request('/servers/%s/action' % instance.uuid, - {action: None}) + {action: body}) self.assertEqual(202, res.status_int) # Do these here instead of tearDown because this method is called # more than once for the same test case @@ -116,18 +120,22 @@ class CommonMixin(object): self.mox.VerifyAll() self.mox.UnsetStubs() - def _test_locked_instance(self, action, method=None): + def _test_locked_instance(self, action, method=None, body=None, + compute_api_args_map={}): if method is None: method = action instance = self._stub_instance_get() - getattr(self.compute_api, method)(self.context, instance).AndRaise( + + args, kwargs = compute_api_args_map.get(action, ((), {})) + getattr(self.compute_api, method)(self.context, instance, *args, + **kwargs).AndRaise( exception.InstanceIsLocked(instance_uuid=instance.uuid)) self.mox.ReplayAll() res = self._make_request('/servers/%s/action' % instance.uuid, - {action: None}) + {action: body}) self.assertEqual(409, res.status_int) # Do these here instead of tearDown because this method is called # more than once for the same test case @@ -136,11 +144,14 @@ class CommonMixin(object): class CommonTests(CommonMixin, test.NoDBTestCase): - def _test_actions(self, actions, method_translations={}): + def _test_actions(self, actions, method_translations={}, body_map={}, + args_map={}): for action in actions: method = method_translations.get(action) + body = body_map.get(action) self.mox.StubOutWithMock(self.compute_api, method or action) - self._test_action(action, method=method) + self._test_action(action, method=method, body=body, + compute_api_args_map=args_map) # Re-mock this. self.mox.StubOutWithMock(self.compute_api, 'get') @@ -163,10 +174,13 @@ class CommonTests(CommonMixin, test.NoDBTestCase): self.mox.StubOutWithMock(self.compute_api, 'get') def _test_actions_with_locked_instance(self, actions, - method_translations={}): + method_translations={}, + body_map={}, args_map={}): for action in actions: method = method_translations.get(action) + body = body_map.get(action) self.mox.StubOutWithMock(self.compute_api, method or action) - self._test_locked_instance(action, method=method) + self._test_locked_instance(action, method=method, body=body, + compute_api_args_map=args_map) # Re-mock this. self.mox.StubOutWithMock(self.compute_api, 'get') diff --git a/nova/tests/api/openstack/compute/plugins/v3/test_admin_actions.py b/nova/tests/api/openstack/compute/plugins/v3/test_admin_actions.py index e4208497957a..246b7acc9269 100644 --- a/nova/tests/api/openstack/compute/plugins/v3/test_admin_actions.py +++ b/nova/tests/api/openstack/compute/plugins/v3/test_admin_actions.py @@ -156,42 +156,17 @@ class CommonMixin(object): class AdminActionsTest(CommonMixin, test.NoDBTestCase): def test_actions(self): - actions = ['migrate', 'reset_network', 'inject_network_info'] - method_translations = {'migrate': 'resize'} + actions = ['reset_network', 'inject_network_info'] for action in actions: - method = method_translations.get(action) - self.mox.StubOutWithMock(self.compute_api, method or action) - self._test_action(action, method=method) - # Re-mock this. - self.mox.StubOutWithMock(self.compute_api, 'get') - - def test_actions_raise_conflict_on_invalid_state(self): - actions = ['migrate', 'migrate_live'] - method_translations = {'migrate': 'resize', - 'migrate_live': 'live_migrate'} - - body_map = {'migrate_live': {'host': 'hostname', - 'block_migration': False, - 'disk_over_commit': False}} - args_map = {'migrate_live': ((False, False, 'hostname'), {})} - - for action in actions: - method = method_translations.get(action) - self.mox.StubOutWithMock(self.compute_api, method or action) - self._test_invalid_state(action, method=method, - body_map=body_map, - compute_api_args_map=args_map) + self.mox.StubOutWithMock(self.compute_api, action) + self._test_action(action) # Re-mock this. self.mox.StubOutWithMock(self.compute_api, 'get') def test_actions_with_non_existed_instance(self): - actions = ['migrate', 'reset_network', 'inject_network_info', - 'reset_state', 'migrate_live'] - body_map = {'reset_state': {'state': 'active'}, - 'migrate_live': {'host': 'hostname', - 'block_migration': False, - 'disk_over_commit': False}} + actions = ['reset_network', 'inject_network_info', 'reset_state'] + body_map = {'reset_state': {'state': 'active'}} for action in actions: self._test_non_existing_instance(action, body_map=body_map) @@ -199,134 +174,14 @@ class AdminActionsTest(CommonMixin, test.NoDBTestCase): self.mox.StubOutWithMock(self.compute_api, 'get') def test_actions_with_locked_instance(self): - actions = ['migrate', 'reset_network', 'inject_network_info'] - method_translations = {'migrate': 'resize'} + actions = ['reset_network', 'inject_network_info'] for action in actions: - method = method_translations.get(action) - self.mox.StubOutWithMock(self.compute_api, method or action) - self._test_locked_instance(action, method=method) + self.mox.StubOutWithMock(self.compute_api, action) + self._test_locked_instance(action) # Re-mock this. self.mox.StubOutWithMock(self.compute_api, 'get') - def _test_migrate_exception(self, exc_info, expected_result): - self.mox.StubOutWithMock(self.compute_api, 'resize') - instance = self._stub_instance_get() - self.compute_api.resize(self.context, instance).AndRaise(exc_info) - - self.mox.ReplayAll() - - res = self._make_request('/servers/%s/action' % instance['uuid'], - {'migrate': None}) - self.assertEqual(expected_result, res.status_int) - - def test_migrate_resize_to_same_flavor(self): - exc_info = exception.CannotResizeToSameFlavor() - self._test_migrate_exception(exc_info, 400) - - def test_migrate_too_many_instances(self): - exc_info = exception.TooManyInstances(overs='', req='', used=0, - allowed=0, resource='') - self._test_migrate_exception(exc_info, 413) - - def _test_migrate_live_succeeded(self, param): - self.mox.StubOutWithMock(self.compute_api, 'live_migrate') - instance = self._stub_instance_get() - self.compute_api.live_migrate(self.context, instance, False, - False, 'hostname') - - self.mox.ReplayAll() - - res = self._make_request('/servers/%s/action' % instance.uuid, - {'migrate_live': param}) - self.assertEqual(202, res.status_int) - - def test_migrate_live_enabled(self): - param = {'host': 'hostname', - 'block_migration': False, - 'disk_over_commit': False} - self._test_migrate_live_succeeded(param) - - def test_migrate_live_enabled_with_string_param(self): - param = {'host': 'hostname', - 'block_migration': "False", - 'disk_over_commit': "False"} - self._test_migrate_live_succeeded(param) - - def test_migrate_live_missing_dict_param(self): - res = self._make_request('/servers/FAKE/action', - {'migrate_live': {'dummy': 'hostname', - 'block_migration': False, - 'disk_over_commit': False}}) - self.assertEqual(400, res.status_int) - - def test_migrate_live_with_invalid_block_migration(self): - res = self._make_request('/servers/FAKE/action', - {'migrate_live': {'host': 'hostname', - 'block_migration': "foo", - 'disk_over_commit': False}}) - self.assertEqual(400, res.status_int) - - def test_migrate_live_with_invalid_disk_over_commit(self): - res = self._make_request('/servers/FAKE/action', - {'migrate_live': {'host': 'hostname', - 'block_migration': False, - 'disk_over_commit': "foo"}}) - self.assertEqual(400, res.status_int) - - def _test_migrate_live_failed_with_exception(self, fake_exc, - uuid=None): - self.mox.StubOutWithMock(self.compute_api, 'live_migrate') - - instance = self._stub_instance_get(uuid=uuid) - self.compute_api.live_migrate(self.context, instance, False, - False, 'hostname').AndRaise(fake_exc) - - self.mox.ReplayAll() - - res = self._make_request('/servers/%s/action' % instance.uuid, - {'migrate_live': - {'host': 'hostname', - 'block_migration': False, - 'disk_over_commit': False}}) - self.assertEqual(400, res.status_int) - self.assertIn(unicode(fake_exc), res.body) - - def test_migrate_live_compute_service_unavailable(self): - self._test_migrate_live_failed_with_exception( - exception.ComputeServiceUnavailable(host='host')) - - def test_migrate_live_invalid_hypervisor_type(self): - self._test_migrate_live_failed_with_exception( - exception.InvalidHypervisorType()) - - def test_migrate_live_unable_to_migrate_to_self(self): - uuid = uuidutils.generate_uuid() - self._test_migrate_live_failed_with_exception( - exception.UnableToMigrateToSelf(instance_id=uuid, - host='host'), - uuid=uuid) - - def test_migrate_live_destination_hypervisor_too_old(self): - self._test_migrate_live_failed_with_exception( - exception.DestinationHypervisorTooOld()) - - def test_migrate_live_no_valid_host(self): - self._test_migrate_live_failed_with_exception( - exception.NoValidHost(reason='')) - - def test_migrate_live_invalid_local_storage(self): - self._test_migrate_live_failed_with_exception( - exception.InvalidLocalStorage(path='', reason='')) - - def test_migrate_live_invalid_shared_storage(self): - self._test_migrate_live_failed_with_exception( - exception.InvalidSharedStorage(path='', reason='')) - - def test_migrate_live_pre_check_error(self): - self._test_migrate_live_failed_with_exception( - exception.MigrationPreCheckError(reason='')) - class CreateBackupTests(CommonMixin, test.NoDBTestCase): def setUp(self): diff --git a/nova/tests/api/openstack/compute/plugins/v3/test_migrate_server.py b/nova/tests/api/openstack/compute/plugins/v3/test_migrate_server.py new file mode 100644 index 000000000000..42ead8af52ee --- /dev/null +++ b/nova/tests/api/openstack/compute/plugins/v3/test_migrate_server.py @@ -0,0 +1,186 @@ +# Copyright 2011 OpenStack Foundation +# Copyright 2013 IBM Corp. +# +# 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 nova.api.openstack.compute.plugins.v3 import migrate_server +from nova import exception +from nova.openstack.common import uuidutils +from nova.tests.api.openstack.compute.plugins.v3 import \ + admin_only_action_common +from nova.tests.api.openstack import fakes + + +class MigrateServerTests(admin_only_action_common.CommonTests): + def setUp(self): + super(MigrateServerTests, self).setUp() + self.controller = migrate_server.MigrateServerController() + self.compute_api = self.controller.compute_api + + def _fake_controller(*args, **kwargs): + return self.controller + + self.stubs.Set(migrate_server, 'MigrateServerController', + _fake_controller) + self.app = fakes.wsgi_app_v3(init_only=('servers', + 'os-migrate-server'), + fake_auth_context=self.context) + self.mox.StubOutWithMock(self.compute_api, 'get') + + def test_migrate(self): + method_translations = {'migrate': 'resize', + 'migrate_live': 'live_migrate'} + body_map = {'migrate_live': {'host': 'hostname', + 'block_migration': False, + 'disk_over_commit': False}} + args_map = {'migrate_live': ((False, False, 'hostname'), {})} + self._test_actions(['migrate', 'migrate_live'], body_map=body_map, + method_translations=method_translations, + args_map=args_map) + + def test_migrate_with_non_existed_instance(self): + body_map = {'migrate_live': {'host': 'hostname', + 'block_migration': False, + 'disk_over_commit': False}} + self._test_actions_with_non_existed_instance( + ['migrate', 'migrate_live'], body_map=body_map) + + def test_migrate_raise_conflict_on_invalid_state(self): + method_translations = {'migrate': 'resize', + 'migrate_live': 'live_migrate'} + body_map = {'migrate_live': {'host': 'hostname', + 'block_migration': False, + 'disk_over_commit': False}} + args_map = {'migrate_live': ((False, False, 'hostname'), {})} + self._test_actions_raise_conflict_on_invalid_state( + ['migrate', 'migrate_live'], body_map=body_map, args_map=args_map, + method_translations=method_translations) + + def test_actions_with_locked_instance(self): + method_translations = {'migrate': 'resize'} + self._test_actions_with_locked_instance(['migrate'], + method_translations=method_translations) + + def _test_migrate_exception(self, exc_info, expected_result): + self.mox.StubOutWithMock(self.compute_api, 'resize') + instance = self._stub_instance_get() + self.compute_api.resize(self.context, instance).AndRaise(exc_info) + + self.mox.ReplayAll() + + res = self._make_request('/servers/%s/action' % instance['uuid'], + {'migrate': None}) + self.assertEqual(expected_result, res.status_int) + + def test_migrate_too_many_instances(self): + exc_info = exception.TooManyInstances(overs='', req='', used=0, + allowed=0, resource='') + self._test_migrate_exception(exc_info, 413) + + def _test_migrate_live_succeeded(self, param): + self.mox.StubOutWithMock(self.compute_api, 'live_migrate') + instance = self._stub_instance_get() + self.compute_api.live_migrate(self.context, instance, False, + False, 'hostname') + + self.mox.ReplayAll() + + res = self._make_request('/servers/%s/action' % instance.uuid, + {'migrate_live': param}) + self.assertEqual(202, res.status_int) + + def test_migrate_live_enabled(self): + param = {'host': 'hostname', + 'block_migration': False, + 'disk_over_commit': False} + self._test_migrate_live_succeeded(param) + + def test_migrate_live_enabled_with_string_param(self): + param = {'host': 'hostname', + 'block_migration': "False", + 'disk_over_commit': "False"} + self._test_migrate_live_succeeded(param) + + def test_migrate_live_missing_dict_param(self): + res = self._make_request('/servers/FAKE/action', + {'migrate_live': {'dummy': 'hostname', + 'block_migration': False, + 'disk_over_commit': False}}) + self.assertEqual(400, res.status_int) + + def test_migrate_live_with_invalid_block_migration(self): + res = self._make_request('/servers/FAKE/action', + {'migrate_live': {'host': 'hostname', + 'block_migration': "foo", + 'disk_over_commit': False}}) + self.assertEqual(400, res.status_int) + + def test_migrate_live_with_invalid_disk_over_commit(self): + res = self._make_request('/servers/FAKE/action', + {'migrate_live': {'host': 'hostname', + 'block_migration': False, + 'disk_over_commit': "foo"}}) + self.assertEqual(400, res.status_int) + + def _test_migrate_live_failed_with_exception(self, fake_exc, + uuid=None): + self.mox.StubOutWithMock(self.compute_api, 'live_migrate') + + instance = self._stub_instance_get(uuid=uuid) + self.compute_api.live_migrate(self.context, instance, False, + False, 'hostname').AndRaise(fake_exc) + + self.mox.ReplayAll() + + res = self._make_request('/servers/%s/action' % instance.uuid, + {'migrate_live': + {'host': 'hostname', + 'block_migration': False, + 'disk_over_commit': False}}) + self.assertEqual(400, res.status_int) + self.assertIn(unicode(fake_exc), res.body) + + def test_migrate_live_compute_service_unavailable(self): + self._test_migrate_live_failed_with_exception( + exception.ComputeServiceUnavailable(host='host')) + + def test_migrate_live_invalid_hypervisor_type(self): + self._test_migrate_live_failed_with_exception( + exception.InvalidHypervisorType()) + + def test_migrate_live_unable_to_migrate_to_self(self): + uuid = uuidutils.generate_uuid() + self._test_migrate_live_failed_with_exception( + exception.UnableToMigrateToSelf(instance_id=uuid, + host='host'), + uuid=uuid) + + def test_migrate_live_destination_hypervisor_too_old(self): + self._test_migrate_live_failed_with_exception( + exception.DestinationHypervisorTooOld()) + + def test_migrate_live_no_valid_host(self): + self._test_migrate_live_failed_with_exception( + exception.NoValidHost(reason='')) + + def test_migrate_live_invalid_local_storage(self): + self._test_migrate_live_failed_with_exception( + exception.InvalidLocalStorage(path='', reason='')) + + def test_migrate_live_invalid_shared_storage(self): + self._test_migrate_live_failed_with_exception( + exception.InvalidSharedStorage(path='', reason='')) + + def test_migrate_live_pre_check_error(self): + self._test_migrate_live_failed_with_exception( + exception.MigrationPreCheckError(reason='')) diff --git a/nova/tests/fake_policy.py b/nova/tests/fake_policy.py index f2effce35761..0ee59675a65a 100644 --- a/nova/tests/fake_policy.py +++ b/nova/tests/fake_policy.py @@ -118,9 +118,7 @@ policy_data = """ "compute_extension:v3:os-admin-actions:reset_network": "", "compute_extension:v3:os-admin-actions:inject_network_info": "", "compute_extension:v3:os-admin-actions:create_backup": "", - "compute_extension:v3:os-admin-actions:migrate_live": "", "compute_extension:v3:os-admin-actions:reset_state": "", - "compute_extension:v3:os-admin-actions:migrate": "", "compute_extension:v3:os-admin-password": "", "compute_extension:aggregates": "rule:admin_api", "compute_extension:v3:os-aggregates:index": "rule:admin_api", @@ -229,6 +227,8 @@ policy_data = """ "compute_extension:v3:keypairs:delete": "", "compute_extension:v3:os-lock-server:lock": "", "compute_extension:v3:os-lock-server:unlock": "", + "compute_extension:v3:os-migrate-server:migrate": "", + "compute_extension:v3:os-migrate-server:migrate_live": "", "compute_extension:multinic": "", "compute_extension:v3:os-multinic": "", "compute_extension:networks": "", diff --git a/nova/tests/integrated/v3/api_samples/os-admin-actions/admin-actions-live-migrate.json.tpl b/nova/tests/integrated/v3/api_samples/os-migrate-server/live-migrate-server.json.tpl similarity index 100% rename from nova/tests/integrated/v3/api_samples/os-admin-actions/admin-actions-live-migrate.json.tpl rename to nova/tests/integrated/v3/api_samples/os-migrate-server/live-migrate-server.json.tpl diff --git a/nova/tests/integrated/v3/api_samples/os-admin-actions/admin-actions-migrate.json.tpl b/nova/tests/integrated/v3/api_samples/os-migrate-server/migrate-server.json.tpl similarity index 100% rename from nova/tests/integrated/v3/api_samples/os-admin-actions/admin-actions-migrate.json.tpl rename to nova/tests/integrated/v3/api_samples/os-migrate-server/migrate-server.json.tpl diff --git a/nova/tests/integrated/v3/api_samples/os-migrate-server/server-post-req.json.tpl b/nova/tests/integrated/v3/api_samples/os-migrate-server/server-post-req.json.tpl new file mode 100644 index 000000000000..d9a7537dfb34 --- /dev/null +++ b/nova/tests/integrated/v3/api_samples/os-migrate-server/server-post-req.json.tpl @@ -0,0 +1,10 @@ +{ + "server" : { + "name" : "new-server-test", + "image_ref" : "%(glance_host)s/images/%(image_id)s", + "flavor_ref" : "%(host)s/flavors/1", + "metadata" : { + "My Server Name" : "Apache1" + } + } +} diff --git a/nova/tests/integrated/v3/api_samples/os-migrate-server/server-post-resp.json.tpl b/nova/tests/integrated/v3/api_samples/os-migrate-server/server-post-resp.json.tpl new file mode 100644 index 000000000000..eb3f76ebe6d3 --- /dev/null +++ b/nova/tests/integrated/v3/api_samples/os-migrate-server/server-post-resp.json.tpl @@ -0,0 +1,16 @@ +{ + "server": { + "admin_password": "%(password)s", + "id": "%(id)s", + "links": [ + { + "href": "%(host)s/v3/servers/%(uuid)s", + "rel": "self" + }, + { + "href": "%(host)s/servers/%(uuid)s", + "rel": "bookmark" + } + ] + } +} diff --git a/nova/tests/integrated/v3/test_admin_actions.py b/nova/tests/integrated/v3/test_admin_actions.py index d76d5be1697d..4c30c8a7af26 100644 --- a/nova/tests/integrated/v3/test_admin_actions.py +++ b/nova/tests/integrated/v3/test_admin_actions.py @@ -13,8 +13,6 @@ # License for the specific language governing permissions and limitations # under the License. -from nova.conductor import manager as conductor_manager -from nova import db from nova.tests.image import fake from nova.tests.integrated.v3 import test_servers @@ -30,12 +28,6 @@ class AdminActionsSamplesJsonTest(test_servers.ServersSampleBase): super(AdminActionsSamplesJsonTest, self).setUp() self.uuid = self._post_server() - def test_post_migrate(self): - # Get api samples to migrate server request. - response = self._do_post('servers/%s/action' % self.uuid, - 'admin-actions-migrate', {}) - self.assertEqual(response.status, 202) - def test_post_reset_network(self): # Get api samples to reset server network request. response = self._do_post('servers/%s/action' % self.uuid, @@ -63,35 +55,6 @@ class AdminActionsSamplesJsonTest(test_servers.ServersSampleBase): 'admin-actions-backup-server', {}) self.assertEqual(response.status, 202) - def test_post_live_migrate_server(self): - # Get api samples to server live migrate request. - def fake_live_migrate(_self, context, instance, scheduler_hint, - block_migration, disk_over_commit): - self.assertEqual(self.uuid, instance["uuid"]) - host = scheduler_hint["host"] - self.assertEqual(self.compute.host, host) - - self.stubs.Set(conductor_manager.ComputeTaskManager, - '_live_migrate', - fake_live_migrate) - - def fake_get_compute(context, host): - service = dict(host=host, - binary='nova-compute', - topic='compute', - report_count=1, - updated_at='foo', - hypervisor_type='bar', - hypervisor_version='1', - disabled=False) - return {'compute_node': [service]} - self.stubs.Set(db, "service_get_by_compute_host", fake_get_compute) - - response = self._do_post('servers/%s/action' % self.uuid, - 'admin-actions-live-migrate', - {'hostname': self.compute.host}) - self.assertEqual(response.status, 202) - def test_post_reset_state(self): # get api samples to server reset state request. response = self._do_post('servers/%s/action' % self.uuid, diff --git a/nova/tests/integrated/v3/test_migrate_server.py b/nova/tests/integrated/v3/test_migrate_server.py new file mode 100644 index 000000000000..5e2216575c56 --- /dev/null +++ b/nova/tests/integrated/v3/test_migrate_server.py @@ -0,0 +1,66 @@ +# Copyright 2012 Nebula, Inc. +# Copyright 2013 IBM Corp. +# +# 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 nova.conductor import manager as conductor_manager +from nova import db +from nova.tests.integrated.v3 import test_servers + + +class MigrateServerSamplesJsonTest(test_servers.ServersSampleBase): + extension_name = "os-migrate-server" + ctype = 'json' + + def setUp(self): + """setUp Method for MigrateServer api samples extension + + This method creates the server that will be used in each tests + """ + super(MigrateServerSamplesJsonTest, self).setUp() + self.uuid = self._post_server() + + def test_post_migrate(self): + # Get api samples to migrate server request. + response = self._do_post('servers/%s/action' % self.uuid, + 'migrate-server', {}) + self.assertEqual(response.status, 202) + + def test_post_live_migrate_server(self): + # Get api samples to server live migrate request. + def fake_live_migrate(_self, context, instance, scheduler_hint, + block_migration, disk_over_commit): + self.assertEqual(self.uuid, instance["uuid"]) + host = scheduler_hint["host"] + self.assertEqual(self.compute.host, host) + + self.stubs.Set(conductor_manager.ComputeTaskManager, + '_live_migrate', + fake_live_migrate) + + def fake_get_compute(context, host): + service = dict(host=host, + binary='nova-compute', + topic='compute', + report_count=1, + updated_at='foo', + hypervisor_type='bar', + hypervisor_version='1', + disabled=False) + return {'compute_node': [service]} + self.stubs.Set(db, "service_get_by_compute_host", fake_get_compute) + + response = self._do_post('servers/%s/action' % self.uuid, + 'live-migrate-server', + {'hostname': self.compute.host}) + self.assertEqual(response.status, 202) diff --git a/setup.cfg b/setup.cfg index 036415bc942e..46f79b7b5092 100644 --- a/setup.cfg +++ b/setup.cfg @@ -89,6 +89,7 @@ nova.api.v3.extensions = instance_usage_audit_log = nova.api.openstack.compute.plugins.v3.instance_usage_audit_log:InstanceUsageAuditLog keypairs = nova.api.openstack.compute.plugins.v3.keypairs:Keypairs lock_server = nova.api.openstack.compute.plugins.v3.lock_server:LockServer + migrate_server = nova.api.openstack.compute.plugins.v3.migrate_server:MigrateServer migrations = nova.api.openstack.compute.plugins.v3.migrations:Migrations multinic = nova.api.openstack.compute.plugins.v3.multinic:Multinic multiple_create = nova.api.openstack.compute.plugins.v3.multiple_create:MultipleCreate