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:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"evacuate": {
|
||||
"admin_password": "MySecretPass",
|
||||
"on_shared_storage": "False"
|
||||
"adminPass": "MySecretPass",
|
||||
"onSharedStorage": "False"
|
||||
}
|
||||
}
|
||||
|
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"admin_password": "MySecretPass"
|
||||
}
|
||||
"adminPass": "MySecretPass"
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"evacuate": {
|
||||
"host": "b419863b7d814906a68fb31703c0dbd6",
|
||||
"admin_password": "MySecretPass",
|
||||
"on_shared_storage": "False"
|
||||
"adminPass": "MySecretPass",
|
||||
"onSharedStorage": "False"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"admin_password": "MySecretPass"
|
||||
}
|
||||
"adminPass": "MySecretPass"
|
||||
}
|
||||
|
@@ -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 {}
|
||||
|
||||
|
@@ -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,
|
||||
},
|
||||
},
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"evacuate": {
|
||||
"admin_password": "%(adminPass)s",
|
||||
"on_shared_storage": "%(onSharedStorage)s"
|
||||
"adminPass": "%(adminPass)s",
|
||||
"onSharedStorage": "%(onSharedStorage)s"
|
||||
}
|
||||
}
|
||||
|
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"admin_password": "%(password)s"
|
||||
"adminPass": "%(password)s"
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"evacuate": {
|
||||
"host": "%(host)s",
|
||||
"admin_password": "%(adminPass)s",
|
||||
"on_shared_storage": "%(onSharedStorage)s"
|
||||
"adminPass": "%(adminPass)s",
|
||||
"onSharedStorage": "%(onSharedStorage)s"
|
||||
}
|
||||
}
|
||||
|
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"admin_password": "%(password)s"
|
||||
"adminPass": "%(password)s"
|
||||
}
|
||||
|
@@ -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,
|
||||
|
Reference in New Issue
Block a user