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
This commit is contained in:
Eli Qiao
2014-08-13 15:23:33 +08:00
parent aca911b2b4
commit 6e66148807
13 changed files with 108 additions and 282 deletions

View File

@@ -1,6 +1,6 @@
{
"evacuate": {
"admin_password": "MySecretPass",
"on_shared_storage": "False"
"adminPass": "MySecretPass",
"onSharedStorage": "False"
}
}

View File

@@ -1,3 +1,3 @@
{
"admin_password": "MySecretPass"
}
"adminPass": "MySecretPass"
}

View File

@@ -1,7 +1,7 @@
{
"evacuate": {
"host": "b419863b7d814906a68fb31703c0dbd6",
"admin_password": "MySecretPass",
"on_shared_storage": "False"
"adminPass": "MySecretPass",
"onSharedStorage": "False"
}
}
}

View File

@@ -1,3 +1,3 @@
{
"admin_password": "MySecretPass"
}
"adminPass": "MySecretPass"
}

View File

@@ -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 {}

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"evacuate": {
"admin_password": "%(adminPass)s",
"on_shared_storage": "%(onSharedStorage)s"
"adminPass": "%(adminPass)s",
"onSharedStorage": "%(onSharedStorage)s"
}
}

View File

@@ -1,3 +1,3 @@
{
"admin_password": "%(password)s"
"adminPass": "%(password)s"
}

View File

@@ -1,7 +1,7 @@
{
"evacuate": {
"host": "%(host)s",
"admin_password": "%(adminPass)s",
"on_shared_storage": "%(onSharedStorage)s"
"adminPass": "%(adminPass)s",
"onSharedStorage": "%(onSharedStorage)s"
}
}

View File

@@ -1,3 +1,3 @@
{
"admin_password": "%(password)s"
"adminPass": "%(password)s"
}

View File

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