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:
Niraj Singh 2019-06-03 07:12:34 +00:00 committed by nirajsingh
parent a131f4eac3
commit 2e461e1223
8 changed files with 447 additions and 80 deletions

View File

@ -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

View File

View File

View 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)

View 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)

View File

@ -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.")

View File

@ -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)

View File

@ -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