Merge "Refactor wsgi of v2 API"
This commit is contained in:
commit
1b684860f0
|
@ -35,20 +35,27 @@ supported_versions_v2 = {
|
|||
|
||||
CURRENT_VERSION = '2.0.0'
|
||||
|
||||
supported_versions = [
|
||||
v1_versions = [
|
||||
item['version'] for item in supported_versions_v1['apiVersions']
|
||||
]
|
||||
|
||||
v2_versions = [
|
||||
item['version'] for item in supported_versions_v2['apiVersions']
|
||||
]
|
||||
|
||||
|
||||
class APIVersion(object):
|
||||
|
||||
def __init__(self, version_string=None):
|
||||
def __init__(self, version_string=None, supported_versions=None):
|
||||
self.ver_major = 0
|
||||
self.ver_minor = 0
|
||||
self.ver_patch = 0
|
||||
|
||||
if version_string is None:
|
||||
return
|
||||
if supported_versions is None:
|
||||
return
|
||||
else:
|
||||
raise sol_ex.APIVersionMissing()
|
||||
|
||||
version_string = self._get_version_id(version_string)
|
||||
match = re.match(r"^([1-9]\d*)\.([1-9]\d*|0)\.([1-9]\d*|0)$",
|
||||
|
@ -60,7 +67,8 @@ class APIVersion(object):
|
|||
else:
|
||||
raise sol_ex.InvalidAPIVersionString(version=version_string)
|
||||
|
||||
if version_string not in supported_versions:
|
||||
if (supported_versions is not None and
|
||||
version_string not in supported_versions):
|
||||
raise sol_ex.APIVersionNotSupported(version=version_string)
|
||||
|
||||
def _get_version_id(self, version_string):
|
||||
|
|
|
@ -18,9 +18,10 @@ import webob
|
|||
|
||||
import oslo_i18n as i18n
|
||||
from oslo_log import log as logging
|
||||
from oslo_serialization import jsonutils
|
||||
|
||||
from tacker.common import exceptions as common_ex
|
||||
from tacker import wsgi
|
||||
from tacker import context
|
||||
|
||||
from tacker.sol_refactored.api import api_version
|
||||
from tacker.sol_refactored.common import config
|
||||
|
@ -30,6 +31,28 @@ from tacker.sol_refactored.common import exceptions as sol_ex
|
|||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SolRequest(webob.Request):
|
||||
|
||||
def best_match_accept(self, content_types):
|
||||
offers = self.accept.acceptable_offers(content_types)
|
||||
if not offers:
|
||||
raise sol_ex.NotAllowedContentType(header=self.accept.header_value)
|
||||
|
||||
return offers[0][0]
|
||||
|
||||
def best_match_language(self):
|
||||
if not self.accept_language:
|
||||
return None
|
||||
all_languages = i18n.get_available_languages('tacker')
|
||||
return self.accept_language.best_match(all_languages)
|
||||
|
||||
@property
|
||||
def context(self):
|
||||
if 'tacker.context' not in self.environ:
|
||||
self.environ['tacker.context'] = context.get_admin_context()
|
||||
return self.environ['tacker.context']
|
||||
|
||||
|
||||
class SolResponse(object):
|
||||
|
||||
# SOL013 4.2.3 Response header field
|
||||
|
@ -47,7 +70,7 @@ class SolResponse(object):
|
|||
self.headers.setdefault('version', api_version.CURRENT_VERSION)
|
||||
self.headers.setdefault('accept-ranges', 'none')
|
||||
|
||||
def serialize(self, request, content_type):
|
||||
def serialize(self, content_type):
|
||||
self.headers.setdefault('content_type', content_type)
|
||||
content_type = self.headers['content_type']
|
||||
if self.body is None:
|
||||
|
@ -57,8 +80,7 @@ class SolResponse(object):
|
|||
elif content_type == 'application/zip':
|
||||
body = self.body
|
||||
else: # 'application/json'
|
||||
serializer = wsgi.JSONDictSerializer()
|
||||
body = serializer.serialize(self.body)
|
||||
body = jsonutils.dump_as_bytes(self.body)
|
||||
if len(body) > config.CONF.v2_vnfm.max_content_length:
|
||||
raise sol_ex.ResponseTooBig(
|
||||
size=config.CONF.v2_vnfm.max_content_length)
|
||||
|
@ -71,8 +93,7 @@ class SolResponse(object):
|
|||
|
||||
class SolErrorResponse(SolResponse):
|
||||
|
||||
def __init__(self, ex, req):
|
||||
user_locale = req.best_match_language()
|
||||
def __init__(self, ex, user_locale):
|
||||
problem_details = {}
|
||||
if isinstance(ex, sol_ex.SolException):
|
||||
problem_details = ex.make_problem_details()
|
||||
|
@ -94,28 +115,26 @@ class SolErrorResponse(SolResponse):
|
|||
problem_details)
|
||||
|
||||
|
||||
class SolResource(wsgi.Application):
|
||||
class SolResource(object):
|
||||
|
||||
def __init__(self, controller, policy_name=None):
|
||||
self.controller = controller
|
||||
self.policy_name = policy_name
|
||||
self.deserializer = wsgi.RequestDeserializer()
|
||||
|
||||
@webob.dec.wsgify(RequestClass=wsgi.Request)
|
||||
@webob.dec.wsgify(RequestClass=SolRequest)
|
||||
def __call__(self, request):
|
||||
LOG.info("%(method)s %(url)s", {"method": request.method,
|
||||
"url": request.url})
|
||||
try:
|
||||
action, args, accept = self.deserializer.deserialize(request)
|
||||
self.check_api_version(request)
|
||||
self.check_policy(request, action)
|
||||
result = self.dispatch(request, action, args)
|
||||
response = result.serialize(request, accept)
|
||||
action, args, accept = self._deserialize_request(request)
|
||||
self._check_api_version(request, action)
|
||||
self._check_policy(request, action)
|
||||
result = self._dispatch(request, action, args)
|
||||
response = result.serialize(accept)
|
||||
except Exception as ex:
|
||||
result = SolErrorResponse(ex, request)
|
||||
result = SolErrorResponse(ex, request.best_match_language())
|
||||
try:
|
||||
response = result.serialize(request,
|
||||
'application/problem+json')
|
||||
response = result.serialize('application/problem+json')
|
||||
except Exception:
|
||||
LOG.exception("Unknown error")
|
||||
return webob.exc.HTTPBadRequest(explanation="Unknown error")
|
||||
|
@ -125,33 +144,83 @@ class SolResource(wsgi.Application):
|
|||
|
||||
return response
|
||||
|
||||
def check_api_version(self, request):
|
||||
def _check_api_version(self, request, action):
|
||||
# check and set api_version
|
||||
ver = request.headers.get("Version")
|
||||
if ver is None:
|
||||
LOG.info("Version missing")
|
||||
raise sol_ex.APIVersionMissing()
|
||||
request.context.api_version = api_version.APIVersion(ver)
|
||||
request.context.api_version = api_version.APIVersion(
|
||||
ver, self.controller.supported_api_versions(action))
|
||||
|
||||
def check_policy(self, request, action):
|
||||
def _check_policy(self, request, action):
|
||||
if self.policy_name is None:
|
||||
return
|
||||
if action == 'reject':
|
||||
return
|
||||
request.context.can(self.policy_name.format(action))
|
||||
|
||||
def dispatch(self, request, action, action_args):
|
||||
def _dispatch(self, request, action, action_args):
|
||||
controller_method = getattr(self.controller, action)
|
||||
return controller_method(request=request, **action_args)
|
||||
|
||||
def _deserialize_request(self, request):
|
||||
action_args = request.environ['wsgiorg.routing_args'][1].copy()
|
||||
action = action_args.pop('action', None)
|
||||
action_args.pop('controller', None)
|
||||
|
||||
class SolAPIRouter(wsgi.Router):
|
||||
body = self._deserialize_body(request, action)
|
||||
if body is not None:
|
||||
action_args.update({'body': body})
|
||||
|
||||
accept = request.best_match_accept(
|
||||
self.controller.allowed_accept(action))
|
||||
|
||||
return (action, action_args, accept)
|
||||
|
||||
def _deserialize_body(self, request, action):
|
||||
if request.method not in ('POST', 'PATCH', 'PUT'):
|
||||
return
|
||||
|
||||
if not request.body:
|
||||
LOG.debug("Empty body provided in request")
|
||||
return
|
||||
|
||||
content_type = request.content_type
|
||||
allowed_content_types = self.controller.allowed_content_types(action)
|
||||
if not content_type:
|
||||
content_type = allowed_content_types[0]
|
||||
elif content_type not in allowed_content_types:
|
||||
raise sol_ex.NotSupportedContentType(header=content_type)
|
||||
|
||||
if content_type == 'application/zip':
|
||||
return request.body_file
|
||||
else:
|
||||
# assume json format
|
||||
# ex. 'application/json', 'application/mergepatch+json'
|
||||
try:
|
||||
return request.json
|
||||
except Exception:
|
||||
raise sol_ex.MalformedRequestBody()
|
||||
|
||||
|
||||
class SolAPIRouter(object):
|
||||
"""WSGI middleware that maps incoming requests to WSGI apps."""
|
||||
|
||||
controller = None
|
||||
route_list = {}
|
||||
|
||||
@classmethod
|
||||
def factory(cls, global_config, **local_config):
|
||||
"""Return an instance of the WSGI Router class."""
|
||||
return cls()
|
||||
|
||||
def __init__(self):
|
||||
super(SolAPIRouter, self).__init__(routes.Mapper())
|
||||
mapper = routes.Mapper()
|
||||
self._setup_routes(mapper)
|
||||
self._router = routes.middleware.RoutesMiddleware(self._dispatch,
|
||||
mapper)
|
||||
|
||||
@webob.dec.wsgify
|
||||
def __call__(self, req):
|
||||
return self._router
|
||||
|
||||
def _setup_routes(self, mapper):
|
||||
for path, methods in self.route_list:
|
||||
|
@ -173,8 +242,44 @@ class SolAPIRouter(wsgi.Router):
|
|||
action='reject',
|
||||
conditions={'method': missing_methods})
|
||||
|
||||
@staticmethod
|
||||
@webob.dec.wsgify(RequestClass=SolRequest)
|
||||
def _dispatch(req):
|
||||
"""Dispatch a Request.
|
||||
|
||||
Called by self._router after matching the incoming request to a route
|
||||
and putting the information into req.environ. Either returns 404
|
||||
or the routed WSGI app's response.
|
||||
"""
|
||||
match = req.environ['wsgiorg.routing_args'][1]
|
||||
if not match:
|
||||
language = req.best_match_language()
|
||||
msg = 'The resource could not be found.'
|
||||
msg = i18n.translate(msg, language)
|
||||
return webob.exc.HTTPNotFound(explanation=msg)
|
||||
app = match['controller']
|
||||
return app
|
||||
|
||||
|
||||
class SolAPIController(object):
|
||||
|
||||
def reject(self, request, **kwargs):
|
||||
raise sol_ex.MethodNotAllowed(method=request.method)
|
||||
|
||||
def supported_api_versions(self, action):
|
||||
# NOTE: support v2 API by default. if a contorller supports
|
||||
# and/or v1 API, or depending on action, override this method
|
||||
# in the subclass.
|
||||
return api_version.v2_versions
|
||||
|
||||
def allowed_content_types(self, action):
|
||||
# NOTE: if other than 'application/json' is expected depending
|
||||
# on action, override this method in the subclass.
|
||||
# NOTE: 'text/plain' is allowed for backward compatibility.
|
||||
# the body is assumed as json.
|
||||
return ['application/json', 'text/plain']
|
||||
|
||||
def allowed_accept(self, action):
|
||||
# NOTE: if other than 'application/json' is expected depending
|
||||
# on action, override this method in the subclass.
|
||||
return ['application/json']
|
||||
|
|
|
@ -80,6 +80,11 @@ class SolHttpError409(SolException):
|
|||
title = 'Conflict'
|
||||
|
||||
|
||||
class SolHttpError415(SolException):
|
||||
status = 415
|
||||
title = 'Unsupported Media Type'
|
||||
|
||||
|
||||
class SolHttpError422(SolException):
|
||||
status = 422
|
||||
title = 'Unprocessable Entity'
|
||||
|
@ -328,3 +333,17 @@ class UpdateK8SResourceFailed(SolHttpError400):
|
|||
|
||||
class NotSupportOperationType(SolHttpError404):
|
||||
message = _("This operation is not currently supported.")
|
||||
|
||||
|
||||
class NotAllowedContentType(SolHttpError406):
|
||||
message = _("Content type '%(header)s' specified in 'Accept' header"
|
||||
" is not allowed.")
|
||||
|
||||
|
||||
class NotSupportedContentType(SolHttpError415):
|
||||
message = _("Content type '%(header)s' specified in 'Content-Type' header"
|
||||
" is not allowed.")
|
||||
|
||||
|
||||
class MalformedRequestBody(SolHttpError400):
|
||||
message = _("Malformed request body.")
|
||||
|
|
|
@ -19,7 +19,7 @@ from datetime import datetime
|
|||
from oslo_log import log as logging
|
||||
from oslo_utils import uuidutils
|
||||
|
||||
from tacker.sol_refactored.api.api_version import supported_versions_v2
|
||||
from tacker.sol_refactored.api import api_version
|
||||
from tacker.sol_refactored.api.schemas import vnflcm_v2 as schema
|
||||
from tacker.sol_refactored.api import validator
|
||||
from tacker.sol_refactored.api import wsgi as sol_wsgi
|
||||
|
@ -53,7 +53,7 @@ class VnfLcmControllerV2(sol_wsgi.SolAPIController):
|
|||
self._subsc_view = vnflcm_view.SubscriptionViewBuilder(self.endpoint)
|
||||
|
||||
def api_versions(self, request):
|
||||
return sol_wsgi.SolResponse(200, supported_versions_v2)
|
||||
return sol_wsgi.SolResponse(200, api_version.supported_versions_v2)
|
||||
|
||||
@validator.schema(schema.CreateVnfRequest_V200, '2.0.0')
|
||||
def create(self, request, body):
|
||||
|
@ -652,3 +652,21 @@ class VnfLcmControllerV2(sol_wsgi.SolAPIController):
|
|||
lcmocc.delete(context)
|
||||
|
||||
return sol_wsgi.SolResponse(204, None)
|
||||
|
||||
def supported_api_versions(self, action):
|
||||
if action == 'api_versions':
|
||||
# support all versions and it is OK there is no Version header.
|
||||
return None
|
||||
else:
|
||||
return api_version.v2_versions
|
||||
|
||||
def allowed_content_types(self, action):
|
||||
if action == 'update':
|
||||
# Content-Type of Modify request shall be
|
||||
# 'application/mergepatch+json' according to SOL spec.
|
||||
# But 'application/json' and 'text/plain' is OK for backward
|
||||
# compatibility.
|
||||
return ['application/mergepatch+json', 'application/json',
|
||||
'text/plain']
|
||||
else:
|
||||
return ['application/json', 'text/plain']
|
||||
|
|
|
@ -25,3 +25,7 @@ class VnfLcmVersionsController(sol_wsgi.SolAPIController):
|
|||
body = {"uriPrefix": "/vnflcm",
|
||||
"apiVersions": api_versions}
|
||||
return sol_wsgi.SolResponse(200, body)
|
||||
|
||||
def supported_api_versions(self, action):
|
||||
# support all versions and it is OK there is no Version header.
|
||||
return None
|
||||
|
|
|
@ -13,8 +13,6 @@
|
|||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from unittest import mock
|
||||
|
||||
from tacker.sol_refactored.api import api_version
|
||||
from tacker.sol_refactored.common import exceptions as sol_ex
|
||||
from tacker.tests import base
|
||||
|
@ -26,37 +24,37 @@ class TestAPIVersion(base.BaseTestCase):
|
|||
vers = api_version.APIVersion()
|
||||
self.assertTrue(vers.is_null())
|
||||
|
||||
@mock.patch.object(api_version, 'supported_versions',
|
||||
new=["3.1.4159", "2.0.0"])
|
||||
def test_init(self):
|
||||
supported_versions = ["3.1.4159", "2.0.0"]
|
||||
for vers, vers_str in [("2.0.0", "2.0.0"),
|
||||
("3.1.4159", "3.1.4159"),
|
||||
("2.0.0-impl:foobar", "2.0.0")]:
|
||||
v = api_version.APIVersion(vers)
|
||||
v = api_version.APIVersion(vers, supported_versions)
|
||||
self.assertEqual(str(v), vers_str)
|
||||
|
||||
def test_init_exceptions(self):
|
||||
supported_versions = ["2.0.0"]
|
||||
self.assertRaises(sol_ex.InvalidAPIVersionString,
|
||||
api_version.APIVersion, "0.1.2")
|
||||
api_version.APIVersion, "0.1.2", supported_versions)
|
||||
|
||||
self.assertRaises(sol_ex.APIVersionNotSupported,
|
||||
api_version.APIVersion, "9.9.9")
|
||||
api_version.APIVersion, "9.9.9", supported_versions)
|
||||
|
||||
@mock.patch.object(api_version, 'supported_versions',
|
||||
new=["1.3.0", "1.3.1", "2.0.0"])
|
||||
def test_compare(self):
|
||||
self.assertTrue(api_version.APIVersion("1.3.0") <
|
||||
api_version.APIVersion("1.3.1"))
|
||||
supported_versions = ["1.3.0", "1.3.1", "2.0.0"]
|
||||
self.assertTrue(api_version.APIVersion("1.3.0", supported_versions) <
|
||||
api_version.APIVersion("1.3.1", supported_versions))
|
||||
|
||||
self.assertTrue(api_version.APIVersion("2.0.0") >
|
||||
api_version.APIVersion("1.3.1"))
|
||||
self.assertTrue(api_version.APIVersion("2.0.0", supported_versions) >
|
||||
api_version.APIVersion("1.3.1", supported_versions))
|
||||
|
||||
@mock.patch.object(api_version, 'supported_versions',
|
||||
new=["1.3.0", "1.3.1", "2.0.0"])
|
||||
def test_matches(self):
|
||||
supported_versions = ["1.3.0", "1.3.1", "2.0.0"]
|
||||
vers = api_version.APIVersion("2.0.0")
|
||||
self.assertTrue(vers.matches(api_version.APIVersion("1.3.0"),
|
||||
api_version.APIVersion()))
|
||||
self.assertTrue(
|
||||
vers.matches(api_version.APIVersion("1.3.0", supported_versions),
|
||||
api_version.APIVersion()))
|
||||
|
||||
self.assertFalse(vers.matches(api_version.APIVersion(),
|
||||
api_version.APIVersion("1.3.1")))
|
||||
self.assertFalse(
|
||||
vers.matches(api_version.APIVersion(),
|
||||
api_version.APIVersion("1.3.1", supported_versions)))
|
||||
|
|
|
@ -56,25 +56,28 @@ class TestValidator(base.BaseTestCase):
|
|||
def _test_method(self, request, body):
|
||||
return True
|
||||
|
||||
@mock.patch.object(api_version, 'supported_versions',
|
||||
new=['2.0.0', '2.0.1', '2.0.2', '2.1.0', '2.2.0'])
|
||||
def test_validator(self):
|
||||
supported_versions = ['2.0.0', '2.0.1', '2.0.2', '2.1.0', '2.2.0']
|
||||
body = {"vnfdId": "vnfd_id", "ProductId": "product_id"}
|
||||
for ok_ver in ['2.0.0', '2.0.1', '2.0.2']:
|
||||
self.context.api_version = api_version.APIVersion(ok_ver)
|
||||
self.context.api_version = api_version.APIVersion(
|
||||
ok_ver, supported_versions)
|
||||
result = self._test_method(request=self.request, body=body)
|
||||
self.assertTrue(result)
|
||||
for ng_ver in ['2.1.0', '2.2.0']:
|
||||
self.context.api_version = api_version.APIVersion(ng_ver)
|
||||
self.context.api_version = api_version.APIVersion(
|
||||
ng_ver, supported_versions)
|
||||
self.assertRaises(sol_ex.SolValidationError,
|
||||
self._test_method, request=self.request, body=body)
|
||||
|
||||
body = {"vnfdId": "vnfd_id", "flavourId": "flavour_id"}
|
||||
for ok_ver in ['2.1.0', '2.2.0']:
|
||||
self.context.api_version = api_version.APIVersion(ok_ver)
|
||||
self.context.api_version = api_version.APIVersion(
|
||||
ok_ver, supported_versions)
|
||||
result = self._test_method(request=self.request, body=body)
|
||||
self.assertTrue(result)
|
||||
for ng_ver in ['2.0.0', '2.0.1', '2.0.2']:
|
||||
self.context.api_version = api_version.APIVersion(ng_ver)
|
||||
self.context.api_version = api_version.APIVersion(
|
||||
ng_ver, supported_versions)
|
||||
self.assertRaises(sol_ex.SolValidationError,
|
||||
self._test_method, request=self.request, body=body)
|
||||
|
|
|
@ -27,7 +27,7 @@ class TestWsgi(base.TestCase):
|
|||
body = {"key": "value0123456789"}
|
||||
response = sol_wsgi.SolResponse(200, body)
|
||||
self.assertRaises(sol_ex.ResponseTooBig,
|
||||
response.serialize, mock.Mock(), 'application/json')
|
||||
response.serialize, 'application/json')
|
||||
|
||||
def test_unknown_error_response(self):
|
||||
err_msg = "Test error"
|
||||
|
@ -41,8 +41,8 @@ class TestWsgi(base.TestCase):
|
|||
self.assertEqual(problem_details, response.body)
|
||||
|
||||
def test_check_api_version_no_version(self):
|
||||
resource = sol_wsgi.SolResource(mock.Mock())
|
||||
resource = sol_wsgi.SolResource(sol_wsgi.SolAPIController())
|
||||
request = mock.Mock()
|
||||
request.headers = {}
|
||||
self.assertRaises(sol_ex.APIVersionMissing,
|
||||
resource.check_api_version, request)
|
||||
resource._check_api_version, request, 'action')
|
||||
|
|
Loading…
Reference in New Issue