Refactor wsgi of v2 API

Original purpose of this patch is that replacing deprecated
webob.Accept.best_match method to acceptable_offers to analize
'Accept' header of a HTTP request.

Instead of fixing legacy wsgi code, refactor v2 wsgi code not to
use legacy wsgi code because legacy wsgi code is complicated
and v2 wsgi code uses legacy wsgi code very little.

Original purpose mentioned above is included in the refactoring
at the same time.

Change-Id: I4ca0beda850ecd14d65d7bc1a59d465c5ecfeacb
This commit is contained in:
Itsuro Oda 2022-03-31 04:48:25 +00:00
parent acf84a168a
commit 15d2e110bc
4 changed files with 151 additions and 24 deletions

View File

@ -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()
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)
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,7 +144,7 @@ class SolResource(wsgi.Application):
return response
def check_api_version(self, request):
def _check_api_version(self, request):
# check and set api_version
ver = request.headers.get("Version")
if ver is None:
@ -133,25 +152,77 @@ class SolResource(wsgi.Application):
raise sol_ex.APIVersionMissing()
request.context.api_version = api_version.APIVersion(ver)
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()
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 +244,36 @@ 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 allowed_content_types(self, action):
# NOTE: if other than 'application/json' is expected depending
# on action, override this method in the subclass.
return ['application/json']
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']

View File

@ -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 specified in 'Accept' header"
" is not allowed.")
class NotSupportedContentType(SolHttpError415):
message = _("Content type specified in 'Content-Type' header"
" is not allowed.")
class MalformedRequestBody(SolHttpError400):
message = _("Malformed request body.")

View File

@ -652,3 +652,12 @@ class VnfLcmControllerV2(sol_wsgi.SolAPIController):
lcmocc.delete(context)
return sol_wsgi.SolResponse(204, None)
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' is OK for backward compatibility.
return ['application/mergepatch+json', 'application/json']
else:
return ['application/json']

View File

@ -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"
@ -45,4 +45,4 @@ class TestWsgi(base.TestCase):
request = mock.Mock()
request.headers = {}
self.assertRaises(sol_ex.APIVersionMissing,
resource.check_api_version, request)
resource._check_api_version, request)