Add vnfpkgm api support
Added base framework for vnf packages. Partial-Implements: blueprint tosca-csar-mgmt-driver Co-Author: Neha Alhat <neha.alhat@nttdata.com> Change-Id: I7efdc03ed2b7ef15bf3006523dbc09894366779c
This commit is contained in:
parent
a131f4eac3
commit
2e461e1223
@ -2,12 +2,18 @@
|
||||
use = egg:Paste#urlmap
|
||||
/: tackerversions
|
||||
/v1.0: tackerapi_v1_0
|
||||
/vnfpkgm/v1: vnfpkgmapi_v1
|
||||
|
||||
[composite:tackerapi_v1_0]
|
||||
use = call:tacker.auth:pipeline_factory
|
||||
noauth = request_id catch_errors extensions tackerapiapp_v1_0
|
||||
keystone = request_id catch_errors alarm_receiver authtoken keystonecontext extensions tackerapiapp_v1_0
|
||||
|
||||
[composite:vnfpkgmapi_v1]
|
||||
use = call:tacker.auth:pipeline_factory
|
||||
noauth = request_id catch_errors extensions vnfpkgmapp_v1
|
||||
keystone = request_id catch_errors authtoken keystonecontext extensions vnfpkgmapp_v1
|
||||
|
||||
[filter:request_id]
|
||||
paste.filter_factory = oslo_middleware:RequestId.factory
|
||||
|
||||
@ -31,3 +37,6 @@ paste.app_factory = tacker.api.versions:Versions.factory
|
||||
|
||||
[app:tackerapiapp_v1_0]
|
||||
paste.app_factory = tacker.api.v1.router:APIRouter.factory
|
||||
|
||||
[app:vnfpkgmapp_v1]
|
||||
paste.app_factory = tacker.api.vnfpkgm.v1.router:VnfpkgmAPIRouter.factory
|
||||
|
0
tacker/api/vnfpkgm/__init__.py
Normal file
0
tacker/api/vnfpkgm/__init__.py
Normal file
0
tacker/api/vnfpkgm/v1/__init__.py
Normal file
0
tacker/api/vnfpkgm/v1/__init__.py
Normal file
49
tacker/api/vnfpkgm/v1/controller.py
Normal file
49
tacker/api/vnfpkgm/v1/controller.py
Normal file
@ -0,0 +1,49 @@
|
||||
# Copyright (C) 2019 NTT DATA
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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 webob
|
||||
|
||||
from tacker import wsgi
|
||||
|
||||
|
||||
class VnfPkgmController(wsgi.Controller):
|
||||
|
||||
def create(self, request, body):
|
||||
raise webob.exc.HTTPNotImplemented()
|
||||
|
||||
def show(self, request, id):
|
||||
raise webob.exc.HTTPNotImplemented()
|
||||
|
||||
def index(self, request):
|
||||
raise webob.exc.HTTPNotImplemented()
|
||||
|
||||
def delete(self, request, id):
|
||||
raise webob.exc.HTTPNotImplemented()
|
||||
|
||||
def upload_vnf_package_content(self, request, id, body):
|
||||
raise webob.exc.HTTPNotImplemented()
|
||||
|
||||
def upload_vnf_package_from_uri(self, request, id, body):
|
||||
raise webob.exc.HTTPNotImplemented()
|
||||
|
||||
|
||||
def create_resource():
|
||||
body_deserializers = {
|
||||
'application/zip': wsgi.ZipDeserializer()
|
||||
}
|
||||
|
||||
deserializer = wsgi.RequestDeserializer(
|
||||
body_deserializers=body_deserializers)
|
||||
return wsgi.Resource(VnfPkgmController(), deserializer=deserializer)
|
75
tacker/api/vnfpkgm/v1/router.py
Normal file
75
tacker/api/vnfpkgm/v1/router.py
Normal file
@ -0,0 +1,75 @@
|
||||
# Copyright (c) 2019 OpenStack Foundation
|
||||
#
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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 routes
|
||||
|
||||
from tacker.api.vnfpkgm.v1 import controller as vnf_pkgm_controller
|
||||
from tacker import wsgi
|
||||
|
||||
|
||||
class VnfpkgmAPIRouter(wsgi.Router):
|
||||
"""Routes requests on the API to the appropriate controller and method."""
|
||||
|
||||
def __init__(self):
|
||||
mapper = routes.Mapper()
|
||||
super(VnfpkgmAPIRouter, self).__init__(mapper)
|
||||
|
||||
def _setup_route(self, mapper, url, methods, controller, default_resource):
|
||||
all_methods = ['HEAD', 'GET', 'POST', 'PUT', 'PATCH', 'DELETE']
|
||||
missing_methods = [m for m in all_methods if m not in methods.keys()]
|
||||
allowed_methods_str = ",".join(methods.keys())
|
||||
|
||||
for method, action in methods.items():
|
||||
mapper.connect(url,
|
||||
controller=controller,
|
||||
action=action,
|
||||
conditions={'method': [method]})
|
||||
|
||||
if missing_methods:
|
||||
mapper.connect(url,
|
||||
controller=default_resource,
|
||||
action='reject',
|
||||
allowed_methods=allowed_methods_str,
|
||||
conditions={'method': missing_methods})
|
||||
|
||||
def _setup_routes(self, mapper):
|
||||
default_resource = wsgi.Resource(wsgi.DefaultMethodController(),
|
||||
wsgi.RequestDeserializer())
|
||||
|
||||
controller = vnf_pkgm_controller.create_resource()
|
||||
|
||||
# Allowed methods on /vnf_packages resource
|
||||
methods = {"GET": "index", "POST": "create"}
|
||||
self._setup_route(mapper, "/vnf_packages",
|
||||
methods, controller, default_resource)
|
||||
|
||||
# Allowed methods on /vnf_packages/{id} resource
|
||||
methods = {"DELETE": "delete", "GET": "show"}
|
||||
self._setup_route(mapper, "/vnf_packages/{id}",
|
||||
methods, controller, default_resource)
|
||||
|
||||
# Allowed methods on /vnf_packages/{id}/package_content resource
|
||||
methods = {"PUT": "upload_vnf_package_content"}
|
||||
self._setup_route(mapper, "/vnf_packages/{id}/package_content",
|
||||
methods, controller, default_resource)
|
||||
|
||||
# Allowed methods on
|
||||
# /vnf_packages/{id}/package_content/upload_from_uri resource
|
||||
methods = {"POST": "upload_vnf_package_from_uri"}
|
||||
self._setup_route(mapper,
|
||||
"/vnf_packages/{id}/package_content/upload_from_uri",
|
||||
methods, controller, default_resource)
|
@ -17,11 +17,39 @@
|
||||
Tacker base exception handling.
|
||||
"""
|
||||
|
||||
from oslo_utils import excutils
|
||||
from oslo_log import log as logging
|
||||
import webob.exc
|
||||
from webob import util as woutil
|
||||
|
||||
from tacker._i18n import _
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ConvertedException(webob.exc.WSGIHTTPException):
|
||||
def __init__(self, code, title="", explanation=""):
|
||||
self.code = code
|
||||
# There is a strict rule about constructing status line for HTTP:
|
||||
# '...Status-Line, consisting of the protocol version followed by a
|
||||
# numeric status code and its associated textual phrase, with each
|
||||
# element separated by SP characters'
|
||||
# (http://www.faqs.org/rfcs/rfc2616.html)
|
||||
# 'code' and 'title' can not be empty because they correspond
|
||||
# to numeric status code and its associated text
|
||||
if title:
|
||||
self.title = title
|
||||
else:
|
||||
try:
|
||||
self.title = woutil.status_reasons[self.code]
|
||||
except KeyError:
|
||||
msg = _("Improper or unknown HTTP status code used: %d")
|
||||
LOG.error(msg, code)
|
||||
self.title = woutil.status_generic_reasons[self.code // 100]
|
||||
self.explanation = explanation
|
||||
super(ConvertedException, self).__init__()
|
||||
|
||||
|
||||
class TackerException(Exception):
|
||||
"""Base Tacker Exception.
|
||||
|
||||
@ -30,21 +58,24 @@ class TackerException(Exception):
|
||||
with the keyword arguments provided to the constructor.
|
||||
"""
|
||||
message = _("An unknown exception occurred.")
|
||||
code = 500
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
try:
|
||||
super(TackerException, self).__init__(self.message % kwargs)
|
||||
self.msg = self.message % kwargs
|
||||
except Exception:
|
||||
with excutils.save_and_reraise_exception() as ctxt:
|
||||
if not self.use_fatal_exceptions():
|
||||
ctxt.reraise = False
|
||||
# at least get the core message out if something happened
|
||||
super(TackerException, self).__init__(self.message)
|
||||
def __init__(self, message=None, **kwargs):
|
||||
if not message:
|
||||
try:
|
||||
message = self.message % kwargs
|
||||
except Exception:
|
||||
message = self.message
|
||||
|
||||
self.msg = message
|
||||
super(TackerException, self).__init__(message)
|
||||
|
||||
def __str__(self):
|
||||
return self.msg
|
||||
|
||||
def format_message(self):
|
||||
return self.args[0]
|
||||
|
||||
def use_fatal_exceptions(self):
|
||||
"""Is the instance using fatal exceptions.
|
||||
|
||||
@ -55,6 +86,7 @@ class TackerException(Exception):
|
||||
|
||||
class BadRequest(TackerException):
|
||||
message = _('Bad %(resource)s request: %(msg)s')
|
||||
code = 400
|
||||
|
||||
|
||||
class NotFound(TackerException):
|
||||
@ -67,6 +99,12 @@ class Conflict(TackerException):
|
||||
|
||||
class NotAuthorized(TackerException):
|
||||
message = _("Not authorized.")
|
||||
code = 401
|
||||
|
||||
|
||||
class Forbidden(TackerException):
|
||||
msg_fmt = _("Forbidden")
|
||||
code = 403
|
||||
|
||||
|
||||
class ServiceUnavailable(TackerException):
|
||||
@ -77,7 +115,7 @@ class AdminRequired(NotAuthorized):
|
||||
message = _("User does not have admin privileges: %(reason)s")
|
||||
|
||||
|
||||
class PolicyNotAuthorized(NotAuthorized):
|
||||
class PolicyNotAuthorized(Forbidden):
|
||||
message = _("Policy doesn't allow %(action)s to be performed.")
|
||||
|
||||
|
||||
|
@ -574,10 +574,10 @@ class ResourceTest(base.BaseTestCase):
|
||||
return index
|
||||
|
||||
def test_dispatch(self):
|
||||
resource = wsgi.Resource(self.Controller(),
|
||||
self.my_fault_body_function)
|
||||
resource = wsgi.Resource(self.Controller())
|
||||
req = wsgi.Request.blank('/')
|
||||
actual = resource.dispatch(
|
||||
resource.controller, 'index', action_args={'index': 'off'})
|
||||
req, 'index', action_args={'index': 'off'})
|
||||
expected = 'off'
|
||||
|
||||
self.assertEqual(expected, actual)
|
||||
@ -590,7 +590,7 @@ class ResourceTest(base.BaseTestCase):
|
||||
resource.controller, 'create', {})
|
||||
|
||||
def test_malformed_request_body_throws_bad_request(self):
|
||||
resource = wsgi.Resource(None, self.my_fault_body_function)
|
||||
resource = wsgi.Resource(None)
|
||||
request = wsgi.Request.blank(
|
||||
"/", body=b"{mal:formed", method='POST',
|
||||
headers={'Content-Type': "application/json"})
|
||||
@ -599,7 +599,7 @@ class ResourceTest(base.BaseTestCase):
|
||||
self.assertEqual(400, response.status_int)
|
||||
|
||||
def test_wrong_content_type_throws_unsupported_media_type_error(self):
|
||||
resource = wsgi.Resource(None, self.my_fault_body_function)
|
||||
resource = wsgi.Resource(None)
|
||||
request = wsgi.Request.blank(
|
||||
"/", body=b"{some:json}", method='POST',
|
||||
headers={'Content-Type': "xxx"})
|
||||
@ -607,13 +607,13 @@ class ResourceTest(base.BaseTestCase):
|
||||
response = resource(request)
|
||||
self.assertEqual(400, response.status_int)
|
||||
|
||||
def test_wrong_content_type_server_error(self):
|
||||
resource = wsgi.Resource(None, self.my_fault_body_function)
|
||||
def test_wrong_content_type_bad_request_error(self):
|
||||
resource = wsgi.Resource(self.Controller())
|
||||
request = wsgi.Request.blank(
|
||||
"/", method='POST', headers={'Content-Type': "unknow"})
|
||||
|
||||
response = resource(request)
|
||||
self.assertEqual(500, response.status_int)
|
||||
self.assertEqual(400, response.status_int)
|
||||
|
||||
def test_call_resource_class_bad_request(self):
|
||||
class FakeRequest(object):
|
||||
@ -628,23 +628,20 @@ class ResourceTest(base.BaseTestCase):
|
||||
def best_match_content_type(self):
|
||||
return 'best_match_content_type'
|
||||
|
||||
resource = wsgi.Resource(self.Controller(),
|
||||
self.my_fault_body_function)
|
||||
resource = wsgi.Resource(self.Controller())
|
||||
request = FakeRequest()
|
||||
result = resource(request)
|
||||
self.assertEqual(415, result.status_int)
|
||||
|
||||
def test_type_error(self):
|
||||
resource = wsgi.Resource(self.Controller(),
|
||||
self.my_fault_body_function)
|
||||
resource = wsgi.Resource(self.Controller())
|
||||
request = wsgi.Request.blank(
|
||||
"/", method='POST', headers={'Content-Type': "json"})
|
||||
"/", method='GET', headers={'Content-Type': "json"})
|
||||
|
||||
response = resource.dispatch(
|
||||
request, action='index', action_args='test')
|
||||
response = resource(request)
|
||||
self.assertEqual(400, response.status_int)
|
||||
|
||||
def test_call_resource_class_internal_error(self):
|
||||
def test_call_resource_class_bad_request_error(self):
|
||||
class FakeRequest(object):
|
||||
def __init__(self):
|
||||
self.url = 'http://where.no'
|
||||
@ -657,11 +654,10 @@ class ResourceTest(base.BaseTestCase):
|
||||
def best_match_content_type(self):
|
||||
return 'application/json'
|
||||
|
||||
resource = wsgi.Resource(self.Controller(),
|
||||
self.my_fault_body_function)
|
||||
resource = wsgi.Resource(self.Controller())
|
||||
request = FakeRequest()
|
||||
result = resource(request)
|
||||
self.assertEqual(500, result.status_int)
|
||||
self.assertEqual(400, result.status_int)
|
||||
|
||||
|
||||
class MiddlewareTest(base.BaseTestCase):
|
||||
@ -677,14 +673,16 @@ class MiddlewareTest(base.BaseTestCase):
|
||||
class FaultTest(base.BaseTestCase):
|
||||
def test_call_fault(self):
|
||||
class MyException(object):
|
||||
status_int = 415
|
||||
code = 415
|
||||
explanation = 'test'
|
||||
|
||||
my_exceptions = MyException()
|
||||
my_fault = wsgi.Fault(exception=my_exceptions)
|
||||
request = wsgi.Request.blank(
|
||||
"/", method='POST', headers={'Content-Type': "unknow"})
|
||||
response = my_fault(request)
|
||||
my_exception = MyException()
|
||||
converted_exp = exception.ConvertedException(code=my_exception.code,
|
||||
explanation=my_exception.explanation)
|
||||
my_fault = wsgi.Fault(converted_exp)
|
||||
req = wsgi.Request.blank("/", method='POST',
|
||||
headers={'Content-Type': "unknow"})
|
||||
response = my_fault(req)
|
||||
self.assertEqual(415, response.status_int)
|
||||
|
||||
|
||||
|
284
tacker/wsgi.py
284
tacker/wsgi.py
@ -486,6 +486,15 @@ class JSONDeserializer(TextDeserializer):
|
||||
return {'body': self._from_json(datastring)}
|
||||
|
||||
|
||||
class ZipDeserializer(ActionDispatcher):
|
||||
|
||||
def deserialize(self, body_file, action='default'):
|
||||
return self.dispatch(body_file, action=action)
|
||||
|
||||
def default(self, body_file):
|
||||
return {'body': body_file}
|
||||
|
||||
|
||||
class RequestHeadersDeserializer(ActionDispatcher):
|
||||
"""Default request headers deserializer."""
|
||||
|
||||
@ -552,7 +561,12 @@ class RequestDeserializer(object):
|
||||
LOG.debug("Unable to deserialize body as provided "
|
||||
"Content-Type")
|
||||
|
||||
return deserializer.deserialize(request.body, action)
|
||||
if isinstance(deserializer, ZipDeserializer):
|
||||
body = request.body_file
|
||||
else:
|
||||
body = request.body
|
||||
|
||||
return deserializer.deserialize(body, action)
|
||||
|
||||
def get_body_deserializer(self, content_type):
|
||||
try:
|
||||
@ -683,6 +697,33 @@ class Debug(Middleware):
|
||||
print()
|
||||
|
||||
|
||||
class DefaultMethodController(object):
|
||||
"""Controller that handles the OPTIONS request method.
|
||||
|
||||
This controller handles the OPTIONS request method and any of the HTTP
|
||||
methods that are not explicitly implemented by the application.
|
||||
"""
|
||||
|
||||
def options(self, request, **kwargs):
|
||||
"""Return a response that includes the 'Allow' header.
|
||||
|
||||
Return a response that includes the 'Allow' header listing the methods
|
||||
that are implemented. A 204 status code is used for this response.
|
||||
"""
|
||||
headers = [('Allow', kwargs.get('allowed_methods'))]
|
||||
raise webob.exc.HTTPNoContent(headers=headers)
|
||||
|
||||
def reject(self, request, **kwargs):
|
||||
"""Return a 405 method not allowed error.
|
||||
|
||||
As a convenience, the 'Allow' header with the list of implemented
|
||||
methods is included in the response as well.
|
||||
"""
|
||||
headers = [('Allow', kwargs.get('allowed_methods'))]
|
||||
raise webob.exc.HTTPMethodNotAllowed(
|
||||
headers=headers)
|
||||
|
||||
|
||||
class Router(object):
|
||||
"""WSGI middleware that maps incoming requests to WSGI apps."""
|
||||
|
||||
@ -715,6 +756,7 @@ class Router(object):
|
||||
mapper.connect(None, "/v1.0/{path_info:.*}", controller=BlogApp())
|
||||
"""
|
||||
self.map = mapper
|
||||
self._setup_routes(self.map)
|
||||
self._router = routes.middleware.RoutesMiddleware(self._dispatch,
|
||||
self.map)
|
||||
|
||||
@ -744,6 +786,131 @@ class Router(object):
|
||||
app = match['controller']
|
||||
return app
|
||||
|
||||
def _setup_routes(self, mapper):
|
||||
pass
|
||||
|
||||
|
||||
class ResourceExceptionHandler(object):
|
||||
"""Context manager to handle Resource exceptions.
|
||||
|
||||
Used when processing exceptions generated by API implementation
|
||||
methods. Converts most exceptions to Fault
|
||||
exceptions, with the appropriate logging.
|
||||
"""
|
||||
|
||||
def __enter__(self):
|
||||
return None
|
||||
|
||||
def __exit__(self, ex_type, ex_value, ex_traceback):
|
||||
if not ex_value:
|
||||
return True
|
||||
if isinstance(ex_value, exception.Forbidden):
|
||||
raise Fault(webob.exc.HTTPForbidden(
|
||||
explanation=ex_value.format_message()))
|
||||
elif isinstance(ex_value, exception.BadRequest):
|
||||
raise Fault(exception.ConvertedException(
|
||||
code=ex_value.code,
|
||||
explanation=ex_value.format_message()))
|
||||
elif isinstance(ex_value, TypeError):
|
||||
exc_info = (ex_type, ex_value, ex_traceback)
|
||||
LOG.error('Exception handling resource: %s', ex_value,
|
||||
exc_info=exc_info)
|
||||
raise Fault(webob.exc.HTTPBadRequest())
|
||||
elif isinstance(ex_value, Fault):
|
||||
LOG.error("Fault thrown: %s", ex_value)
|
||||
raise ex_value
|
||||
elif isinstance(ex_value, webob.exc.HTTPException):
|
||||
LOG.error("HTTP exception thrown: %s", ex_value)
|
||||
raise Fault(ex_value)
|
||||
|
||||
# We didn't handle the exception
|
||||
return False
|
||||
|
||||
|
||||
class ResponseObject(object):
|
||||
"""Bundles a response object
|
||||
|
||||
Object that app methods may return in order to allow its response
|
||||
to be modified by extensions in the code. Its use is optional (and
|
||||
should only be used if you really know what you are doing).
|
||||
"""
|
||||
|
||||
def __init__(self, obj, code=None, headers=None):
|
||||
"""Builds a response object."""
|
||||
|
||||
self.obj = obj
|
||||
self._default_code = 200
|
||||
self._code = code
|
||||
self._headers = headers or {}
|
||||
self.serializer = JSONDictSerializer()
|
||||
|
||||
def __getitem__(self, key):
|
||||
"""Retrieves a header with the given name."""
|
||||
|
||||
return self._headers[key.lower()]
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
"""Sets a header with the given name to the given value."""
|
||||
|
||||
self._headers[key.lower()] = value
|
||||
|
||||
def __delitem__(self, key):
|
||||
"""Deletes the header with the given name."""
|
||||
|
||||
del self._headers[key.lower()]
|
||||
|
||||
def serialize(self, request, content_type):
|
||||
"""Serializes the wrapped object.
|
||||
|
||||
Utility method for serializing the wrapped object. Returns a
|
||||
webob.Response object.
|
||||
|
||||
Header values are set to the appropriate Python type and
|
||||
encoding demanded by PEP 3333: whatever the native str type is.
|
||||
"""
|
||||
|
||||
serializer = self.serializer
|
||||
|
||||
body = None
|
||||
if self.obj is not None:
|
||||
body = serializer.serialize(self.obj)
|
||||
response = webob.Response(body=body)
|
||||
response.status_int = self.code
|
||||
for hdr, val in self._headers.items():
|
||||
if six.PY2:
|
||||
# In Py2.X Headers must be a UTF-8 encode str.
|
||||
response.headers[hdr] = encodeutils.safe_encode(val)
|
||||
else:
|
||||
# In Py3.X Headers must be a str that was first safely
|
||||
# encoded to UTF-8 (to catch any bad encodings) and then
|
||||
# decoded back to a native str.
|
||||
response.headers[hdr] = encodeutils.safe_decode(
|
||||
encodeutils.safe_encode(val))
|
||||
# Deal with content_type
|
||||
if not isinstance(content_type, six.text_type):
|
||||
content_type = six.text_type(content_type)
|
||||
if six.PY2:
|
||||
# In Py2.X Headers must be a UTF-8 encode str.
|
||||
response.headers['Content-Type'] = encodeutils.safe_encode(
|
||||
content_type)
|
||||
else:
|
||||
# In Py3.X Headers must be a str.
|
||||
response.headers['Content-Type'] = encodeutils.safe_decode(
|
||||
encodeutils.safe_encode(content_type))
|
||||
return response
|
||||
|
||||
@property
|
||||
def code(self):
|
||||
"""Retrieve the response status."""
|
||||
|
||||
return self._code or self._default_code
|
||||
|
||||
@property
|
||||
def headers(self):
|
||||
"""Retrieve the headers."""
|
||||
|
||||
return self._headers.copy()
|
||||
|
||||
|
||||
class Resource(Application):
|
||||
"""WSGI app that handles (de)serialization and controller dispatch.
|
||||
@ -758,8 +925,7 @@ class Resource(Application):
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, controller, fault_body_function,
|
||||
deserializer=None, serializer=None):
|
||||
def __init__(self, controller, deserializer=None, serializer=None):
|
||||
"""Object initialization.
|
||||
|
||||
:param controller: object that implement methods created by routes lib
|
||||
@ -767,15 +933,10 @@ class Resource(Application):
|
||||
controller into a webob response
|
||||
:param serializer: object that can deserialize a webob request
|
||||
into necessary pieces
|
||||
:param fault_body_function: a function that will build the response
|
||||
body for HTTP errors raised by operations
|
||||
on this resource object
|
||||
|
||||
"""
|
||||
self.controller = controller
|
||||
self.deserializer = deserializer or RequestDeserializer()
|
||||
self.serializer = serializer or ResponseSerializer()
|
||||
self._fault_body_function = fault_body_function
|
||||
|
||||
@webob.dec.wsgify(RequestClass=Request)
|
||||
def __call__(self, request):
|
||||
@ -795,24 +956,33 @@ class Resource(Application):
|
||||
return Fault(webob.exc.HTTPBadRequest(
|
||||
explanation=_("Malformed request body")))
|
||||
|
||||
response = None
|
||||
try:
|
||||
action_result = self.dispatch(request, action, args)
|
||||
except webob.exc.HTTPException as ex:
|
||||
LOG.info("HTTP exception thrown: %s", ex)
|
||||
action_result = Fault(ex,
|
||||
self._fault_body_function)
|
||||
except Exception:
|
||||
LOG.exception("Internal error")
|
||||
# Do not include the traceback to avoid returning it to clients.
|
||||
action_result = Fault(webob.exc.HTTPServerError(),
|
||||
self._fault_body_function)
|
||||
with ResourceExceptionHandler():
|
||||
action_result = self.dispatch(request, action, args)
|
||||
except Fault as ex:
|
||||
response = ex
|
||||
except Exception as ex:
|
||||
raise Fault(webob.exc.HTTPInternalServerError())
|
||||
|
||||
if isinstance(action_result, dict) or action_result is None:
|
||||
response = self.serializer.serialize(action_result,
|
||||
accept,
|
||||
action=action)
|
||||
else:
|
||||
response = action_result
|
||||
if not response:
|
||||
resp_obj = None
|
||||
if type(action_result) is dict or action_result is None:
|
||||
resp_obj = ResponseObject(action_result)
|
||||
elif isinstance(action_result, ResponseObject):
|
||||
resp_obj = action_result
|
||||
else:
|
||||
response = action_result
|
||||
|
||||
# Run post-processing extensions
|
||||
if resp_obj:
|
||||
method = getattr(self.controller, action)
|
||||
# Do a preserialize to set up the response object
|
||||
if hasattr(method, 'wsgi_code'):
|
||||
resp_obj._default_code = method.wsgi_code
|
||||
|
||||
if resp_obj and not response:
|
||||
response = resp_obj.serialize(request, accept)
|
||||
|
||||
try:
|
||||
msg_dict = dict(url=request.url, status=response.status_int)
|
||||
@ -829,13 +999,7 @@ class Resource(Application):
|
||||
"""Find action-spefic method on controller and call it."""
|
||||
|
||||
controller_method = getattr(self.controller, action)
|
||||
try:
|
||||
# NOTE(salvatore-orlando): the controller method must have
|
||||
# an argument whose name is 'request'
|
||||
return controller_method(request=request, **action_args)
|
||||
except TypeError as exc:
|
||||
LOG.exception(exc)
|
||||
return Fault(webob.exc.HTTPBadRequest())
|
||||
return controller_method(request=request, **action_args)
|
||||
|
||||
|
||||
def _default_body_function(wrapped_exc):
|
||||
@ -850,28 +1014,62 @@ def _default_body_function(wrapped_exc):
|
||||
|
||||
|
||||
class Fault(webob.exc.HTTPException):
|
||||
"""Generates an HTTP response from a webob HTTP exception."""
|
||||
"""Wrap webob.exc.HTTPException to provide API friendly response."""
|
||||
|
||||
def __init__(self, exception, body_function=None):
|
||||
"""Creates a Fault for the given webob.exc.exception."""
|
||||
_fault_names = {
|
||||
400: "badRequest",
|
||||
401: "unauthorized",
|
||||
403: "forbidden",
|
||||
404: "itemNotFound",
|
||||
405: "badMethod",
|
||||
409: "conflictingRequest",
|
||||
413: "overLimit",
|
||||
415: "badMediaType",
|
||||
429: "overLimit",
|
||||
501: "notImplemented",
|
||||
503: "serviceUnavailable"}
|
||||
|
||||
def __init__(self, exception):
|
||||
"""Create a Fault for the given webob.exc.exception."""
|
||||
self.wrapped_exc = exception
|
||||
self.status_int = self.wrapped_exc.status_int
|
||||
self._body_function = body_function or _default_body_function
|
||||
for key, value in list(self.wrapped_exc.headers.items()):
|
||||
self.wrapped_exc.headers[key] = str(value)
|
||||
self.status_int = exception.status_int
|
||||
|
||||
@webob.dec.wsgify(RequestClass=Request)
|
||||
def __call__(self, req):
|
||||
"""Generate a WSGI response based on the exception passed to ctor."""
|
||||
user_locale = req.best_match_language()
|
||||
# Replace the body with fault details.
|
||||
fault_data, metadata = self._body_function(self.wrapped_exc)
|
||||
content_type = req.best_match_content_type()
|
||||
serializer = {
|
||||
'application/json': JSONDictSerializer(),
|
||||
}[content_type]
|
||||
code = self.wrapped_exc.status_int
|
||||
fault_name = self._fault_names.get(code, "tackerFault")
|
||||
explanation = self.wrapped_exc.explanation
|
||||
LOG.debug("Returning %(code)s to user: %(explanation)s",
|
||||
{'code': code, 'explanation': explanation})
|
||||
|
||||
explanation = i18n.translate(explanation, user_locale)
|
||||
fault_data = {
|
||||
fault_name: {
|
||||
'code': code,
|
||||
'message': explanation}}
|
||||
if code == 413 or code == 429:
|
||||
retry = self.wrapped_exc.headers.get('Retry-After', None)
|
||||
if retry:
|
||||
fault_data[fault_name]['retryAfter'] = retry
|
||||
|
||||
self.wrapped_exc.content_type = 'application/json'
|
||||
self.wrapped_exc.charset = 'UTF-8'
|
||||
|
||||
body = JSONDictSerializer().serialize(fault_data)
|
||||
if isinstance(body, six.text_type):
|
||||
body = body.encode('utf-8')
|
||||
self.wrapped_exc.body = body
|
||||
|
||||
self.wrapped_exc.body = serializer.serialize(fault_data)
|
||||
self.wrapped_exc.content_type = content_type
|
||||
return self.wrapped_exc
|
||||
|
||||
def __str__(self):
|
||||
return self.wrapped_exc.__str__()
|
||||
|
||||
|
||||
# NOTE(salvatore-orlando): this class will go once the
|
||||
# extension API framework is updated
|
||||
|
Loading…
Reference in New Issue
Block a user