From 6e66148807e54824ef7a9079141b42593244a245 Mon Sep 17 00:00:00 2001 From: Eli Qiao Date: Wed, 13 Aug 2014 15:23:33 +0800 Subject: [PATCH] Changes V3 evacuate extension into v2.1 This patch changes v3 evacuate API to v2.1, and also share v2.1 and v2 test case. The differences between v2 and v3 are described on the wiki page https://wiki.openstack.org/wiki/NovaAPIv2tov3 . Partially implements blueprint v2-on-v3-api Change-Id: Ib105e367be5a921f1b9cade51204b224d99343ec --- .../server-evacuate-find-host-req.json | 4 +- .../server-evacuate-find-host-resp.json | 4 +- .../os-evacuate/server-evacuate-req.json | 6 +- .../os-evacuate/server-evacuate-resp.json | 4 +- .../openstack/compute/plugins/v3/evacuate.py | 12 +- .../openstack/compute/schemas/v3/evacuate.py | 6 +- .../compute/contrib/test_evacuate.py | 90 ++++++- .../compute/plugins/v3/test_evacuate.py | 248 ------------------ .../server-evacuate-find-host-req.json.tpl | 4 +- .../server-evacuate-find-host-resp.json.tpl | 2 +- .../os-evacuate/server-evacuate-req.json.tpl | 4 +- .../os-evacuate/server-evacuate-resp.json.tpl | 2 +- nova/tests/integrated/v3/test_evacuate.py | 4 +- 13 files changed, 108 insertions(+), 282 deletions(-) delete mode 100644 nova/tests/api/openstack/compute/plugins/v3/test_evacuate.py diff --git a/doc/v3/api_samples/os-evacuate/server-evacuate-find-host-req.json b/doc/v3/api_samples/os-evacuate/server-evacuate-find-host-req.json index a8a2162381a1..7e3319904ad8 100644 --- a/doc/v3/api_samples/os-evacuate/server-evacuate-find-host-req.json +++ b/doc/v3/api_samples/os-evacuate/server-evacuate-find-host-req.json @@ -1,6 +1,6 @@ { "evacuate": { - "admin_password": "MySecretPass", - "on_shared_storage": "False" + "adminPass": "MySecretPass", + "onSharedStorage": "False" } } diff --git a/doc/v3/api_samples/os-evacuate/server-evacuate-find-host-resp.json b/doc/v3/api_samples/os-evacuate/server-evacuate-find-host-resp.json index fcd865c04336..a023c720b974 100644 --- a/doc/v3/api_samples/os-evacuate/server-evacuate-find-host-resp.json +++ b/doc/v3/api_samples/os-evacuate/server-evacuate-find-host-resp.json @@ -1,3 +1,3 @@ { - "admin_password": "MySecretPass" -} \ No newline at end of file + "adminPass": "MySecretPass" +} diff --git a/doc/v3/api_samples/os-evacuate/server-evacuate-req.json b/doc/v3/api_samples/os-evacuate/server-evacuate-req.json index 6d041f7da7d6..583ce9bc742a 100644 --- a/doc/v3/api_samples/os-evacuate/server-evacuate-req.json +++ b/doc/v3/api_samples/os-evacuate/server-evacuate-req.json @@ -1,7 +1,7 @@ { "evacuate": { "host": "b419863b7d814906a68fb31703c0dbd6", - "admin_password": "MySecretPass", - "on_shared_storage": "False" + "adminPass": "MySecretPass", + "onSharedStorage": "False" } -} \ No newline at end of file +} diff --git a/doc/v3/api_samples/os-evacuate/server-evacuate-resp.json b/doc/v3/api_samples/os-evacuate/server-evacuate-resp.json index fcd865c04336..a023c720b974 100644 --- a/doc/v3/api_samples/os-evacuate/server-evacuate-resp.json +++ b/doc/v3/api_samples/os-evacuate/server-evacuate-resp.json @@ -1,3 +1,3 @@ { - "admin_password": "MySecretPass" -} \ No newline at end of file + "adminPass": "MySecretPass" +} diff --git a/nova/api/openstack/compute/plugins/v3/evacuate.py b/nova/api/openstack/compute/plugins/v3/evacuate.py index 6bb616a7576e..5f366277c7af 100644 --- a/nova/api/openstack/compute/plugins/v3/evacuate.py +++ b/nova/api/openstack/compute/plugins/v3/evacuate.py @@ -41,7 +41,9 @@ class EvacuateController(wsgi.Controller): self.compute_api = compute.API() self.host_api = compute.HostAPI() - @wsgi.response(202) + # TODO(eliqiao): Should be responding here with 202 Accept + # because evacuate is an async call, but keep to 200 for + # backwards compatibility reasons. @extensions.expected_errors((400, 404, 409)) @wsgi.action('evacuate') @validation.schema(evacuate.evacuate) @@ -55,17 +57,17 @@ class EvacuateController(wsgi.Controller): evacuate_body = body["evacuate"] host = evacuate_body.get("host") on_shared_storage = strutils.bool_from_string( - evacuate_body["on_shared_storage"]) + evacuate_body["onSharedStorage"]) password = None - if 'admin_password' in evacuate_body: + if 'adminPass' in evacuate_body: # check that if requested to evacuate server on shared storage # password not specified if on_shared_storage: msg = _("admin password can't be changed on existing disk") raise exc.HTTPBadRequest(explanation=msg) - password = evacuate_body['admin_password'] + password = evacuate_body['adminPass'] elif not on_shared_storage: password = utils.generate_password() @@ -92,7 +94,7 @@ class EvacuateController(wsgi.Controller): raise exc.HTTPBadRequest(explanation=e.format_message()) if CONF.enable_instance_password: - return {'admin_password': password} + return {'adminPass': password} else: return {} diff --git a/nova/api/openstack/compute/schemas/v3/evacuate.py b/nova/api/openstack/compute/schemas/v3/evacuate.py index a71e995dc6cc..ac0aac5e1fb0 100644 --- a/nova/api/openstack/compute/schemas/v3/evacuate.py +++ b/nova/api/openstack/compute/schemas/v3/evacuate.py @@ -22,10 +22,10 @@ evacuate = { 'type': 'object', 'properties': { 'host': parameter_types.hostname, - 'on_shared_storage': parameter_types.boolean, - 'admin_password': parameter_types.admin_password, + 'onSharedStorage': parameter_types.boolean, + 'adminPass': parameter_types.admin_password, }, - 'required': ['on_shared_storage'], + 'required': ['onSharedStorage'], 'additionalProperties': False, }, }, diff --git a/nova/tests/api/openstack/compute/contrib/test_evacuate.py b/nova/tests/api/openstack/compute/contrib/test_evacuate.py index 8f3e3ab079fd..62d77c8eba12 100644 --- a/nova/tests/api/openstack/compute/contrib/test_evacuate.py +++ b/nova/tests/api/openstack/compute/contrib/test_evacuate.py @@ -57,30 +57,29 @@ def fake_service_get_by_compute_host(self, context, host): } -class EvacuateTest(test.NoDBTestCase): +class EvacuateTestV21(test.NoDBTestCase): _methods = ('resize', 'evacuate') - fake_url = '/v2/fake' + fake_url = '/v3' def setUp(self): - super(EvacuateTest, self).setUp() + super(EvacuateTestV21, self).setUp() self.stubs.Set(compute_api.API, 'get', fake_compute_api_get) self.stubs.Set(compute_api.HostAPI, 'service_get_by_compute_host', fake_service_get_by_compute_host) self.UUID = uuid.uuid4() for _method in self._methods: self.stubs.Set(compute_api.API, _method, fake_compute_api) - self.flags( - osapi_compute_extension=[ - 'nova.api.openstack.compute.contrib.select_extensions'], - osapi_compute_ext_list=['Evacuate']) + + def _fake_wsgi_app(self, ctxt): + return fakes.wsgi_app_v3(fake_auth_context=ctxt) def _gen_resource_with_app(self, json_load, is_admin=True, uuid=None): ctxt = context.get_admin_context() ctxt.user_id = 'fake' ctxt.project_id = 'fake' ctxt.is_admin = is_admin - app = fakes.wsgi_app(fake_auth_context=ctxt) + app = self._fake_wsgi_app(ctxt) req = webob.Request.blank('%s/servers/%s/action' % (self.fake_url, uuid or self.UUID)) req.method = 'POST' @@ -122,13 +121,33 @@ class EvacuateTest(test.NoDBTestCase): def test_evacuate_instance_with_no_target(self): res = self._gen_resource_with_app({'onSharedStorage': 'False', 'adminPass': 'MyNewPass'}) - self.assertEqual(400, res.status_int) + self.assertEqual(200, res.status_int) def test_evacuate_instance_without_on_shared_storage(self): res = self._gen_resource_with_app({'host': 'my-host', 'adminPass': 'MyNewPass'}) self.assertEqual(res.status_int, 400) + def test_evacuate_instance_with_invalid_characters_host(self): + host = 'abc!#' + res = self._gen_resource_with_app({'host': host, + 'onSharedStorage': 'False', + 'adminPass': 'MyNewPass'}) + self.assertEqual(400, res.status_int) + + def test_evacuate_instance_with_too_long_host(self): + host = 'a' * 256 + res = self._gen_resource_with_app({'host': host, + 'onSharedStorage': 'False', + 'adminPass': 'MyNewPass'}) + self.assertEqual(400, res.status_int) + + def test_evacuate_instance_with_invalid_on_shared_storage(self): + res = self._gen_resource_with_app({'host': 'my-host', + 'onSharedStorage': 'foo', + 'adminPass': 'MyNewPass'}) + self.assertEqual(400, res.status_int) + def test_evacuate_instance_with_bad_target(self): res = self._gen_resource_with_app({'host': 'bad-host', 'onSharedStorage': 'False', @@ -195,3 +214,56 @@ class EvacuateTest(test.NoDBTestCase): self.assertEqual(200, res.status_int) resp_json = jsonutils.loads(res.body) self.assertEqual("MyNewPass", resp_json['adminPass']) + + def test_evacuate_disable_password_return(self): + self._test_evacuate_enable_instance_password_conf(False) + + def test_evacuate_enable_password_return(self): + self._test_evacuate_enable_instance_password_conf(True) + + def _test_evacuate_enable_instance_password_conf(self, enable_pass): + self.flags(enable_instance_password=enable_pass) + self.stubs.Set(compute_api.API, 'update', self._fake_update) + + res = self._gen_resource_with_app({'host': 'my_host', + 'onSharedStorage': 'False'}) + self.assertEqual(res.status_int, 200) + resp_json = jsonutils.loads(res.body) + if enable_pass: + self.assertIn('adminPass', resp_json) + else: + self.assertIsNone(resp_json.get('adminPass')) + + +class EvacuateTestV2(EvacuateTestV21): + fake_url = '/v2/fake' + + def setUp(self): + super(EvacuateTestV2, self).setUp() + self.flags( + osapi_compute_extension=[ + 'nova.api.openstack.compute.contrib.select_extensions'], + osapi_compute_ext_list=['Evacuate']) + + def _fake_wsgi_app(self, ctxt): + return fakes.wsgi_app(fake_auth_context=ctxt) + + def test_evacuate_instance_with_no_target(self): + res = self._gen_resource_with_app({'onSharedStorage': 'False', + 'adminPass': 'MyNewPass'}) + self.assertEqual(400, res.status_int) + + def test_evacuate_instance_with_too_long_host(self): + pass + + def test_evacuate_instance_with_invalid_characters_host(self): + pass + + def test_evacuate_instance_with_invalid_on_shared_storage(self): + pass + + def test_evacuate_disable_password_return(self): + pass + + def test_evacuate_enable_password_return(self): + pass diff --git a/nova/tests/api/openstack/compute/plugins/v3/test_evacuate.py b/nova/tests/api/openstack/compute/plugins/v3/test_evacuate.py deleted file mode 100644 index cc168739efdc..000000000000 --- a/nova/tests/api/openstack/compute/plugins/v3/test_evacuate.py +++ /dev/null @@ -1,248 +0,0 @@ -# Copyright 2013 OpenStack Foundation -# -# 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 uuid - -import mock -from oslo.config import cfg -import webob - -from nova.compute import api as compute_api -from nova.compute import vm_states -from nova import context -from nova import exception -from nova.openstack.common import jsonutils -from nova import test -from nova.tests.api.openstack import fakes -from nova.tests import fake_instance - -CONF = cfg.CONF -CONF.import_opt('password_length', 'nova.utils') - - -def fake_compute_api(*args, **kwargs): - return True - - -def fake_compute_api_get(self, context, instance_id, want_objects=False, - **kwargs): - return fake_instance.fake_instance_obj(context, id=1, uuid=instance_id, - vm_state=vm_states.ACTIVE, - task_state=None, host='host1') - - -def fake_service_get_by_compute_host(self, context, host): - if host == 'bad-host': - raise exception.ComputeHostNotFound(host=host) - else: - return { - 'host_name': host, - 'service': 'compute', - 'zone': 'nova'} - - -class EvacuateTest(test.NoDBTestCase): - - _methods = ('resize', 'evacuate') - - def setUp(self): - super(EvacuateTest, self).setUp() - self.stubs.Set(compute_api.API, 'get', fake_compute_api_get) - self.stubs.Set(compute_api.HostAPI, 'service_get_by_compute_host', - fake_service_get_by_compute_host) - self.UUID = uuid.uuid4() - for _method in self._methods: - self.stubs.Set(compute_api.API, _method, fake_compute_api) - - def _fake_update(self, context, instance, - task_state, expected_task_state): - return - - def _gen_request_with_app(self, json_load, is_admin=True): - ctxt = context.get_admin_context() - ctxt.user_id = 'fake' - ctxt.project_id = 'fake' - ctxt.is_admin = is_admin - app = fakes.wsgi_app_v3(fake_auth_context=ctxt) - req = webob.Request.blank('/v3/servers/%s/action' % self.UUID) - req.method = 'POST' - base_json_load = {'evacuate': json_load} - req.body = jsonutils.dumps(base_json_load) - req.content_type = 'application/json' - - return req, app - - @mock.patch('nova.compute.api.API.evacuate') - def test_evacuate_instance_with_no_target(self, evacuate_mock): - req, app = self._gen_request_with_app({'on_shared_storage': 'False', - 'admin_password': 'MyNewPass'}) - res = req.get_response(app) - self.assertEqual(202, res.status_int) - evacuate_mock.assert_called_once_with(mock.ANY, mock.ANY, None, - mock.ANY, mock.ANY) - - def test_evacuate_instance_with_empty_host(self): - req, app = self._gen_request_with_app({'host': '', - 'on_shared_storage': 'False', - 'admin_password': 'MyNewPass'}) - res = req.get_response(app) - jsonutils.loads(res.body) - self.assertEqual(400, res.status_int) - - def test_evacuate_instance_with_too_long_host(self): - host = 'a' * 256 - req, app = self._gen_request_with_app({'host': host, - 'on_shared_storage': 'False', - 'admin_password': 'MyNewPass'}) - res = req.get_response(app) - jsonutils.loads(res.body) - self.assertEqual(400, res.status_int) - - def test_evacuate_instance_with_invalid_characters_host(self): - host = 'abc!#' - req, app = self._gen_request_with_app({'host': host, - 'on_shared_storage': 'False', - 'admin_password': 'MyNewPass'}) - res = req.get_response(app) - jsonutils.loads(res.body) - self.assertEqual(400, res.status_int) - - def test_evacuate_instance_with_invalid_on_shared_storage(self): - req, app = self._gen_request_with_app({'host': 'my-host', - 'on_shared_storage': 'foo', - 'admin_password': 'MyNewPass'}) - res = req.get_response(app) - jsonutils.loads(res.body) - self.assertEqual(400, res.status_int) - - def test_evacuate_instance_without_on_shared_storage(self): - req, app = self._gen_request_with_app({'host': 'my-host', - 'admin_password': 'MyNewPass'}) - res = req.get_response(app) - self.assertEqual(400, res.status_int) - - def test_evacuate_instance_with_bad_host(self): - req, app = self._gen_request_with_app({'host': 'bad-host', - 'on_shared_storage': 'False', - 'admin_password': 'MyNewPass'}) - - res = req.get_response(app) - self.assertEqual(404, res.status_int) - - def test_evacuate_instance_with_target(self): - req, app = self._gen_request_with_app({'host': 'my-host', - 'on_shared_storage': 'False', - 'admin_password': 'MyNewPass'}) - - self.stubs.Set(compute_api.API, 'update', self._fake_update) - - resp = req.get_response(app) - self.assertEqual(202, resp.status_int) - resp_json = jsonutils.loads(resp.body) - self.assertEqual("MyNewPass", resp_json['admin_password']) - - def test_evacuate_instance_with_underscore_in_hostname(self): - # NOTE: The hostname grammar in RFC952 does not allow for - # underscores in hostnames. However, we should test that it - # is supported because it sometimes occurs in real systems. - req, app = self._gen_request_with_app({'host': 'underscore_hostname', - 'on_shared_storage': 'False', - 'admin_password': 'MyNewPass'}) - - self.stubs.Set(compute_api.API, 'update', self._fake_update) - - resp = req.get_response(app) - self.assertEqual(202, resp.status_int) - resp_json = jsonutils.loads(resp.body) - self.assertEqual("MyNewPass", resp_json['admin_password']) - - def test_evacuate_shared_and_pass(self): - req, app = self._gen_request_with_app({'host': 'my-host', - 'on_shared_storage': 'True', - 'admin_password': 'MyNewPass'}) - self.stubs.Set(compute_api.API, 'update', self._fake_update) - - res = req.get_response(app) - self.assertEqual(400, res.status_int) - - def test_evacuate_not_shared_pass_generated(self): - req, app = self._gen_request_with_app({'host': 'my-host', - 'on_shared_storage': 'False'}) - - self.stubs.Set(compute_api.API, 'update', self._fake_update) - - resp = req.get_response(app) - self.assertEqual(202, resp.status_int) - resp_json = jsonutils.loads(resp.body) - self.assertEqual(CONF.password_length, - len(resp_json['admin_password'])) - - def test_evacuate_shared(self): - req, app = self._gen_request_with_app({'host': 'my-host', - 'on_shared_storage': 'True'}) - self.stubs.Set(compute_api.API, 'update', self._fake_update) - - res = req.get_response(app) - self.assertEqual(202, res.status_int) - resp_json = jsonutils.loads(res.body) - self.assertIsNone(resp_json['admin_password']) - - def test_evacuate_with_active_service(self): - req, app = self._gen_request_with_app({'host': 'my-host', - 'on_shared_storage': 'false', - 'admin_password': 'MyNewPass'}) - - def fake_evacuate(*args, **kwargs): - raise exception.ComputeServiceInUse("Service still in use") - - self.stubs.Set(compute_api.API, 'evacuate', fake_evacuate) - - res = req.get_response(app) - self.assertEqual(400, res.status_int) - - def test_not_admin(self): - req, app = self._gen_request_with_app({'host': 'my-host', - 'on_shared_storage': 'True'}, - is_admin=False) - - req.content_type = 'application/json' - res = req.get_response(app) - self.assertEqual(403, res.status_int) - - def test_evacuate_disable_password_return(self): - self._test_evacuate_enable_instance_password_conf(False) - - def test_evacuate_enable_password_return(self): - self._test_evacuate_enable_instance_password_conf(True) - - def _test_evacuate_enable_instance_password_conf(self, enable_pass): - self.flags(enable_instance_password=enable_pass) - req, app = self._gen_request_with_app({'host': 'my_host', - 'on_shared_storage': 'False'}) - self.stubs.Set(compute_api.API, 'update', self._fake_update) - - res = req.get_response(app) - self.assertEqual(res.status_int, 202) - resp_json = jsonutils.loads(res.body) - if enable_pass: - self.assertIn('admin_password', resp_json) - else: - self.assertIsNone(resp_json.get('admin_password')) - - def test_evacuate_to_same_host(self): - req, app = self._gen_request_with_app({'host': 'host1', - 'on_shared_storage': 'False', - 'admin_password': 'MyNewPass'}) - res = req.get_response(app) - self.assertEqual(400, res.status_int) diff --git a/nova/tests/integrated/v3/api_samples/os-evacuate/server-evacuate-find-host-req.json.tpl b/nova/tests/integrated/v3/api_samples/os-evacuate/server-evacuate-find-host-req.json.tpl index 7ba9398ba6a9..5e2c2e6ef01a 100644 --- a/nova/tests/integrated/v3/api_samples/os-evacuate/server-evacuate-find-host-req.json.tpl +++ b/nova/tests/integrated/v3/api_samples/os-evacuate/server-evacuate-find-host-req.json.tpl @@ -1,6 +1,6 @@ { "evacuate": { - "admin_password": "%(adminPass)s", - "on_shared_storage": "%(onSharedStorage)s" + "adminPass": "%(adminPass)s", + "onSharedStorage": "%(onSharedStorage)s" } } diff --git a/nova/tests/integrated/v3/api_samples/os-evacuate/server-evacuate-find-host-resp.json.tpl b/nova/tests/integrated/v3/api_samples/os-evacuate/server-evacuate-find-host-resp.json.tpl index e6d6ad9ed18c..0da07da5b8f0 100644 --- a/nova/tests/integrated/v3/api_samples/os-evacuate/server-evacuate-find-host-resp.json.tpl +++ b/nova/tests/integrated/v3/api_samples/os-evacuate/server-evacuate-find-host-resp.json.tpl @@ -1,3 +1,3 @@ { - "admin_password": "%(password)s" + "adminPass": "%(password)s" } diff --git a/nova/tests/integrated/v3/api_samples/os-evacuate/server-evacuate-req.json.tpl b/nova/tests/integrated/v3/api_samples/os-evacuate/server-evacuate-req.json.tpl index 3639d1ed8ea7..179cddce73b1 100644 --- a/nova/tests/integrated/v3/api_samples/os-evacuate/server-evacuate-req.json.tpl +++ b/nova/tests/integrated/v3/api_samples/os-evacuate/server-evacuate-req.json.tpl @@ -1,7 +1,7 @@ { "evacuate": { "host": "%(host)s", - "admin_password": "%(adminPass)s", - "on_shared_storage": "%(onSharedStorage)s" + "adminPass": "%(adminPass)s", + "onSharedStorage": "%(onSharedStorage)s" } } diff --git a/nova/tests/integrated/v3/api_samples/os-evacuate/server-evacuate-resp.json.tpl b/nova/tests/integrated/v3/api_samples/os-evacuate/server-evacuate-resp.json.tpl index e6d6ad9ed18c..0da07da5b8f0 100644 --- a/nova/tests/integrated/v3/api_samples/os-evacuate/server-evacuate-resp.json.tpl +++ b/nova/tests/integrated/v3/api_samples/os-evacuate/server-evacuate-resp.json.tpl @@ -1,3 +1,3 @@ { - "admin_password": "%(password)s" + "adminPass": "%(password)s" } diff --git a/nova/tests/integrated/v3/test_evacuate.py b/nova/tests/integrated/v3/test_evacuate.py index b4666b026343..0f17ad122066 100644 --- a/nova/tests/integrated/v3/test_evacuate.py +++ b/nova/tests/integrated/v3/test_evacuate.py @@ -66,7 +66,7 @@ class EvacuateJsonTest(test_servers.ServersSampleBase): "onSharedStorage": 'False' } self._test_evacuate(req_subs, 'server-evacuate-req', - 'server-evacuate-resp', 202) + 'server-evacuate-resp', 200) rebuild_mock.assert_called_once_with(mock.ANY, instance=mock.ANY, orig_image_ref=mock.ANY, image_ref=mock.ANY, injected_files=mock.ANY, new_pass="MySecretPass", @@ -81,7 +81,7 @@ class EvacuateJsonTest(test_servers.ServersSampleBase): "onSharedStorage": 'False' } self._test_evacuate(req_subs, 'server-evacuate-find-host-req', - 'server-evacuate-find-host-resp', 202) + 'server-evacuate-find-host-resp', 200) rebuild_mock.assert_called_once_with(mock.ANY, instance=mock.ANY, orig_image_ref=mock.ANY, image_ref=mock.ANY,