Support for async bay operations

Current implementation of magnum bay operations are synchronous
and as a result API requests are blocked until response from HEAT
service is received. With this change bay-create, bay-update and
bay-delete calls will be asynchronous.
Please note that with this change bay-create/bay-update api calls
will return bay uuid instead of bay object and also microversion
1.2 is added for new behavior.

Change-Id: I4ca1f9f386b6417726154c466e7a9104b6e6e5e1
Closes-Bug: #1588425
This commit is contained in:
Vijendar Komalla 2016-07-19 11:20:25 -05:00
parent fc9c1a8fc0
commit bf30b9b4cb
9 changed files with 137 additions and 40 deletions

View File

@ -13,6 +13,8 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import uuid
from oslo_log import log as logging from oslo_log import log as logging
from oslo_utils import timeutils from oslo_utils import timeutils
import pecan import pecan
@ -54,6 +56,13 @@ class BayPatchType(types.JsonPatchType):
return types.JsonPatchType.internal_attrs() + internal_attrs return types.JsonPatchType.internal_attrs() + internal_attrs
class BayID(wtypes.Base):
uuid = types.uuid
def __init__(self, uuid):
self.uuid = uuid
class Bay(base.APIBase): class Bay(base.APIBase):
"""API representation of a bay. """API representation of a bay.
@ -319,12 +328,33 @@ class BaysController(base.Controller):
return bay return bay
@base.Controller.api_version("1.1", "1.1")
@expose.expose(Bay, body=Bay, status_code=201) @expose.expose(Bay, body=Bay, status_code=201)
def post(self, bay): def post(self, bay):
"""Create a new bay. """Create a new bay.
:param bay: a bay within the request body. :param bay: a bay within the request body.
""" """
new_bay = self._post(bay)
res_bay = pecan.request.rpcapi.bay_create(new_bay,
bay.bay_create_timeout)
# Set the HTTP Location Header
pecan.response.location = link.build_url('bays', res_bay.uuid)
return Bay.convert_with_links(res_bay)
@base.Controller.api_version("1.2") # noqa
@expose.expose(BayID, body=Bay, status_code=202)
def post(self, bay):
"""Create a new bay.
:param bay: a bay within the request body.
"""
new_bay = self._post(bay)
pecan.request.rpcapi.bay_create_async(new_bay, bay.bay_create_timeout)
return BayID(new_bay.uuid)
def _post(self, bay):
context = pecan.request.context context = pecan.request.context
policy.enforce(context, 'bay:create', policy.enforce(context, 'bay:create',
action='bay:create') action='bay:create')
@ -340,13 +370,10 @@ class BaysController(base.Controller):
bay_dict['name'] = name bay_dict['name'] = name
new_bay = objects.Bay(context, **bay_dict) new_bay = objects.Bay(context, **bay_dict)
res_bay = pecan.request.rpcapi.bay_create(new_bay, new_bay.uuid = uuid.uuid4()
bay.bay_create_timeout) return new_bay
# Set the HTTP Location Header
pecan.response.location = link.build_url('bays', res_bay.uuid)
return Bay.convert_with_links(res_bay)
@base.Controller.api_version("1.1", "1.1")
@wsme.validate(types.uuid, [BayPatchType]) @wsme.validate(types.uuid, [BayPatchType])
@expose.expose(Bay, types.uuid_or_name, body=[BayPatchType]) @expose.expose(Bay, types.uuid_or_name, body=[BayPatchType])
def patch(self, bay_ident, patch): def patch(self, bay_ident, patch):
@ -355,6 +382,25 @@ class BaysController(base.Controller):
:param bay_ident: UUID or logical name of a bay. :param bay_ident: UUID or logical name of a bay.
:param patch: a json PATCH document to apply to this bay. :param patch: a json PATCH document to apply to this bay.
""" """
bay = self._patch(bay_ident, patch)
res_bay = pecan.request.rpcapi.bay_update(bay)
return Bay.convert_with_links(res_bay)
@base.Controller.api_version("1.2") # noqa
@wsme.validate(types.uuid, [BayPatchType])
@expose.expose(BayID, types.uuid_or_name, body=[BayPatchType],
status_code=202)
def patch(self, bay_ident, patch):
"""Update an existing bay.
:param bay_ident: UUID or logical name of a bay.
:param patch: a json PATCH document to apply to this bay.
"""
bay = self._patch(bay_ident, patch)
pecan.request.rpcapi.bay_update_async(bay)
return BayID(bay.uuid)
def _patch(self, bay_ident, patch):
context = pecan.request.context context = pecan.request.context
bay = api_utils.get_resource('Bay', bay_ident) bay = api_utils.get_resource('Bay', bay_ident)
policy.enforce(context, 'bay:update', bay, policy.enforce(context, 'bay:update', bay,
@ -380,19 +426,33 @@ class BaysController(base.Controller):
delta = bay.obj_what_changed() delta = bay.obj_what_changed()
validate_bay_properties(delta) validate_bay_properties(delta)
return bay
res_bay = pecan.request.rpcapi.bay_update(bay) @base.Controller.api_version("1.1", "1.1")
return Bay.convert_with_links(res_bay)
@expose.expose(None, types.uuid_or_name, status_code=204) @expose.expose(None, types.uuid_or_name, status_code=204)
def delete(self, bay_ident): def delete(self, bay_ident):
"""Delete a bay. """Delete a bay.
:param bay_ident: UUID of a bay or logical name of the bay. :param bay_ident: UUID of a bay or logical name of the bay.
""" """
bay = self._delete(bay_ident)
pecan.request.rpcapi.bay_delete(bay.uuid)
@base.Controller.api_version("1.2") # noqa
@expose.expose(None, types.uuid_or_name, status_code=204)
def delete(self, bay_ident):
"""Delete a bay.
:param bay_ident: UUID of a bay or logical name of the bay.
"""
bay = self._delete(bay_ident)
pecan.request.rpcapi.bay_delete_async(bay.uuid)
def _delete(self, bay_ident):
context = pecan.request.context context = pecan.request.context
bay = api_utils.get_resource('Bay', bay_ident) bay = api_utils.get_resource('Bay', bay_ident)
policy.enforce(context, 'bay:delete', bay, policy.enforce(context, 'bay:delete', bay,
action='bay:delete') action='bay:delete')
return bay
pecan.request.rpcapi.bay_delete(bay.uuid)

View File

@ -28,7 +28,9 @@ from magnum.i18n import _
# Add details of new api versions here: # Add details of new api versions here:
BASE_VER = '1.1' BASE_VER = '1.1'
CURRENT_MAX_VER = '1.1' CURRENT_MAX_VER = '1.2'
# 1.2 Async bay operations support
# 1.1 Initial version
class Version(object): class Version(object):

View File

@ -34,12 +34,22 @@ class API(rpc_service.API):
return self._call('bay_create', bay=bay, return self._call('bay_create', bay=bay,
bay_create_timeout=bay_create_timeout) bay_create_timeout=bay_create_timeout)
def bay_create_async(self, bay, bay_create_timeout):
self._cast('bay_create', bay=bay,
bay_create_timeout=bay_create_timeout)
def bay_delete(self, uuid): def bay_delete(self, uuid):
return self._call('bay_delete', uuid=uuid) return self._call('bay_delete', uuid=uuid)
def bay_delete_async(self, uuid):
self._cast('bay_delete', uuid=uuid)
def bay_update(self, bay): def bay_update(self, bay):
return self._call('bay_update', bay=bay) return self._call('bay_update', bay=bay)
def bay_update_async(self, bay):
self._cast('bay_update', bay=bay)
# CA operations # CA operations
def sign_certificate(self, bay, certificate): def sign_certificate(self, bay, certificate):

View File

@ -13,7 +13,6 @@
# under the License. # under the License.
import os import os
import uuid
from heatclient.common import template_utils from heatclient.common import template_utils
from heatclient import exc from heatclient import exc
@ -145,7 +144,6 @@ class Handler(object):
osc = clients.OpenStackClients(context) osc = clients.OpenStackClients(context)
bay.uuid = uuid.uuid4()
try: try:
# Create trustee/trust and set them to bay # Create trustee/trust and set them to bay
trust_manager.create_trustee_and_trust(osc, bay) trust_manager.create_trustee_and_trust(osc, bay)

View File

@ -13,6 +13,7 @@
import fixtures import fixtures
from oslo_log import log as logging from oslo_log import log as logging
from oslo_utils import uuidutils
from tempest.lib.common.utils import data_utils from tempest.lib.common.utils import data_utils
from tempest.lib import exceptions from tempest.lib import exceptions
import testtools import testtools
@ -90,16 +91,21 @@ class BayTest(base.BaseTempestTest):
resp, model = self.baymodel_client.delete_baymodel(baymodel_id) resp, model = self.baymodel_client.delete_baymodel(baymodel_id)
return resp, model return resp, model
def _create_bay(self, bay_model): def _create_bay(self, bay_model, is_async=False):
self.LOG.debug('We will create bay for %s' % bay_model) self.LOG.debug('We will create bay for %s' % bay_model)
resp, model = self.bay_client.post_bay(bay_model) headers = {'Content-Type': 'application/json',
'Accept': 'application/json'}
if is_async:
headers["OpenStack-API-Version"] = "container-infra 1.2"
resp, model = self.bay_client.post_bay(bay_model, headers=headers)
self.LOG.debug('Response: %s' % resp) self.LOG.debug('Response: %s' % resp)
if is_async:
self.assertEqual(202, resp.status)
else:
self.assertEqual(201, resp.status) self.assertEqual(201, resp.status)
self.assertIsNotNone(model.uuid) self.assertIsNotNone(model.uuid)
self.assertTrue(uuidutils.is_uuid_like(model.uuid))
self.bays.append(model.uuid) self.bays.append(model.uuid)
self.assertEqual(BayStatus.CREATE_IN_PROGRESS, model.status)
self.assertIsNone(model.status_reason)
self.assertEqual(model.baymodel_id, self.baymodel.uuid)
self.bay_uuid = model.uuid self.bay_uuid = model.uuid
if config.Config.copy_logs: if config.Config.copy_logs:
self.addOnException(self.copy_logs_handler( self.addOnException(self.copy_logs_handler(
@ -134,6 +140,8 @@ class BayTest(base.BaseTempestTest):
# test bay create # test bay create
_, temp_model = self._create_bay(gen_model) _, temp_model = self._create_bay(gen_model)
self.assertEqual(BayStatus.CREATE_IN_PROGRESS, temp_model.status)
self.assertIsNone(temp_model.status_reason)
# test bay list # test bay list
resp, model = self.bay_client.list_bays() resp, model = self.bay_client.list_bays()
@ -153,6 +161,26 @@ class BayTest(base.BaseTempestTest):
self._delete_bay(temp_model.uuid) self._delete_bay(temp_model.uuid)
self.bays.remove(temp_model.uuid) self.bays.remove(temp_model.uuid)
@testtools.testcase.attr('positive')
def test_create_delete_bays_async(self):
gen_model = datagen.valid_bay_data(
baymodel_id=self.baymodel.uuid, node_count=1)
# test bay create
_, temp_model = self._create_bay(gen_model, is_async=True)
self.assertNotIn('status', temp_model)
# test bay list
resp, model = self.bay_client.list_bays()
self.assertEqual(200, resp.status)
self.assertGreater(len(model.bays), 0)
self.assertIn(
temp_model.uuid, list([x['uuid'] for x in model.bays]))
# test bay delete
self._delete_bay(temp_model.uuid)
self.bays.remove(temp_model.uuid)
@testtools.testcase.attr('negative') @testtools.testcase.attr('negative')
def test_create_bay_for_nonexisting_baymodel(self): def test_create_bay_for_nonexisting_baymodel(self):
gen_model = datagen.valid_bay_data(baymodel_id='this-does-not-exist') gen_model = datagen.valid_bay_data(baymodel_id='this-does-not-exist')

View File

@ -40,7 +40,7 @@ class TestRootController(api_base.FunctionalTest):
[{u'href': u'http://localhost/v1/', [{u'href': u'http://localhost/v1/',
u'rel': u'self'}], u'rel': u'self'}],
u'status': u'CURRENT', u'status': u'CURRENT',
u'max_version': u'1.1', u'max_version': u'1.2',
u'min_version': u'1.1'}]} u'min_version': u'1.1'}]}
self.v1_expected = { self.v1_expected = {

View File

@ -16,7 +16,6 @@ import mock
from oslo_config import cfg from oslo_config import cfg
from oslo_utils import timeutils from oslo_utils import timeutils
from oslo_utils import uuidutils from oslo_utils import uuidutils
from six.moves.urllib import parse as urlparse
from magnum.api import attr_validator from magnum.api import attr_validator
from magnum.api.controllers.v1 import bay as api_bay from magnum.api.controllers.v1 import bay as api_bay
@ -428,10 +427,7 @@ class TestPost(api_base.FunctionalTest):
self.assertEqual(201, response.status_int) self.assertEqual(201, response.status_int)
# Check location header # Check location header
self.assertIsNotNone(response.location) self.assertIsNotNone(response.location)
expected_location = '/v1/bays/%s' % bdict['uuid'] self.assertTrue(uuidutils.is_uuid_like(response.json['uuid']))
self.assertEqual(expected_location,
urlparse.urlparse(response.location).path)
self.assertEqual(bdict['uuid'], response.json['uuid'])
self.assertNotIn('updated_at', response.json.keys) self.assertNotIn('updated_at', response.json.keys)
return_created_at = timeutils.parse_isotime( return_created_at = timeutils.parse_isotime(
response.json['created_at']).replace(tzinfo=None) response.json['created_at']).replace(tzinfo=None)

View File

@ -15,7 +15,6 @@
# under the License. # under the License.
import six import six
import uuid
from heatclient import exc from heatclient import exc
import mock import mock
@ -172,14 +171,11 @@ class TestHandler(db_base.DbTestCase):
@patch('magnum.conductor.handlers.bay_conductor.trust_manager') @patch('magnum.conductor.handlers.bay_conductor.trust_manager')
@patch('magnum.conductor.handlers.bay_conductor.cert_manager') @patch('magnum.conductor.handlers.bay_conductor.cert_manager')
@patch('magnum.conductor.handlers.bay_conductor._create_stack') @patch('magnum.conductor.handlers.bay_conductor._create_stack')
@patch('magnum.conductor.handlers.bay_conductor.uuid')
@patch('magnum.common.clients.OpenStackClients') @patch('magnum.common.clients.OpenStackClients')
def test_create(self, mock_openstack_client_class, mock_uuid, def test_create(self, mock_openstack_client_class,
mock_create_stack, mock_cert_manager, mock_trust_manager, mock_create_stack, mock_cert_manager, mock_trust_manager,
mock_heat_poller_class): mock_heat_poller_class):
timeout = 15 timeout = 15
test_uuid = uuid.uuid4()
mock_uuid.uuid4.return_value = test_uuid
mock_poller = mock.MagicMock() mock_poller = mock.MagicMock()
mock_poller.poll_and_check.return_value = loopingcall.LoopingCallDone() mock_poller.poll_and_check.return_value = loopingcall.LoopingCallDone()
mock_heat_poller_class.return_value = mock_poller mock_heat_poller_class.return_value = mock_poller
@ -187,7 +183,6 @@ class TestHandler(db_base.DbTestCase):
mock_openstack_client_class.return_value = osc mock_openstack_client_class.return_value = osc
def create_stack_side_effect(context, osc, bay, timeout): def create_stack_side_effect(context, osc, bay, timeout):
self.assertEqual(str(test_uuid), bay.uuid)
return {'stack': {'id': 'stack-id'}} return {'stack': {'id': 'stack-id'}}
mock_create_stack.side_effect = create_stack_side_effect mock_create_stack.side_effect = create_stack_side_effect
@ -334,16 +329,12 @@ class TestHandler(db_base.DbTestCase):
@patch('magnum.conductor.handlers.bay_conductor.trust_manager') @patch('magnum.conductor.handlers.bay_conductor.trust_manager')
@patch('magnum.conductor.handlers.bay_conductor.cert_manager') @patch('magnum.conductor.handlers.bay_conductor.cert_manager')
@patch('magnum.conductor.handlers.bay_conductor._create_stack') @patch('magnum.conductor.handlers.bay_conductor._create_stack')
@patch('magnum.conductor.handlers.bay_conductor.uuid')
@patch('magnum.common.clients.OpenStackClients') @patch('magnum.common.clients.OpenStackClients')
def test_create_with_invalid_unicode_name(self, def test_create_with_invalid_unicode_name(self,
mock_openstack_client_class, mock_openstack_client_class,
mock_uuid,
mock_create_stack, mock_create_stack,
mock_cert_manager, mock_cert_manager,
mock_trust_manager): mock_trust_manager):
test_uuid = uuid.uuid4()
mock_uuid.uuid4.return_value = test_uuid
error_message = six.u("""Invalid stack name 测试集群-zoyh253geukk error_message = six.u("""Invalid stack name 测试集群-zoyh253geukk
must contain only alphanumeric or "_-." must contain only alphanumeric or "_-."
characters, must start with alpha""") characters, must start with alpha""")
@ -376,11 +367,9 @@ class TestHandler(db_base.DbTestCase):
@patch('magnum.conductor.handlers.bay_conductor.trust_manager') @patch('magnum.conductor.handlers.bay_conductor.trust_manager')
@patch('magnum.conductor.handlers.bay_conductor.cert_manager') @patch('magnum.conductor.handlers.bay_conductor.cert_manager')
@patch('magnum.conductor.handlers.bay_conductor.short_id') @patch('magnum.conductor.handlers.bay_conductor.short_id')
@patch('magnum.conductor.handlers.bay_conductor.uuid')
@patch('magnum.common.clients.OpenStackClients') @patch('magnum.common.clients.OpenStackClients')
def test_create_with_environment(self, def test_create_with_environment(self,
mock_openstack_client_class, mock_openstack_client_class,
mock_uuid,
mock_short_id, mock_short_id,
mock_cert_manager, mock_cert_manager,
mock_trust_manager, mock_trust_manager,
@ -390,8 +379,6 @@ class TestHandler(db_base.DbTestCase):
mock_heat_poller_class): mock_heat_poller_class):
timeout = 15 timeout = 15
self.bay.baymodel_id = self.baymodel.uuid self.bay.baymodel_id = self.baymodel.uuid
test_uuid = uuid.uuid4()
mock_uuid.uuid4.return_value = test_uuid
bay_name = self.bay.name bay_name = self.bay.name
mock_short_id.generate_id.return_value = 'short_id' mock_short_id.generate_id.return_value = 'short_id'
mock_poller = mock.MagicMock() mock_poller = mock.MagicMock()

View File

@ -0,0 +1,16 @@
---
features:
- Current implementation of magnum bay operations are
synchronous and as a result API requests are blocked
until response from HEAT service is received. This release
adds support for asynchronous bay operations (bay-create,
bay-update, and bay-delete). Please note that with this
change, bay-create, bay-update API calls will return bay uuid
instead of bay object and also return HTTP status code 202
instead of 201. Microversion 1.2 is added for new behavior.
upgrade:
- Magnum bay operations API default behavior changed from
synchronous to asynchronous. User can specify
OpenStack-API-Version 1.1 in request header for synchronous
bay operations.