Merge pull request #10 from alvarolopez/refactor_rendering

Refactor rendering
This commit is contained in:
Enol Fernández 2015-04-08 10:16:20 +02:00
commit 8ab4be59bf
21 changed files with 379 additions and 144 deletions

View File

@ -14,7 +14,9 @@
# License for the specific language governing permissions and limitations
# under the License.
from ooi.wsgi import utils
from ooi import utils
import webob.exc
class Controller(object):
@ -31,3 +33,53 @@ class Controller(object):
if body is not None:
req.body = utils.utf8(body)
return req
@staticmethod
def get_from_response(response, element, default):
"""Get a JSON element from a valid response or raise an exception.
This method will extract an element a JSON response (falling back to a
default value) if the response has a code of 200, otherwise it will
raise a webob.exc.exception
:param response: The webob.Response object
:param element: The element to look for in the JSON body
:param default: The default element to be returned if not found.
"""
if response.status_int == 200:
return response.json_body.get(element, default)
else:
raise exception_from_response(response)
def exception_from_response(response):
"""Convert an OpenStack V2 Fault into a webob exception.
Since we are calling the OpenStack API we should process the Faults
produced by them. Extract the Fault information according to [1] and
convert it back to a webob exception.
[1] http://docs.openstack.org/developer/nova/v2/faults.html
:param response: a webob.Response containing an exception
:returns: a webob.exc.exception object
"""
exceptions = {
400: webob.exc.HTTPBadRequest,
401: webob.exc.HTTPUnauthorized,
403: webob.exc.HTTPForbidden,
404: webob.exc.HTTPNotFound,
405: webob.exc.HTTPMethodNotAllowed,
406: webob.exc.HTTPNotAcceptable,
409: webob.exc.HTTPConflict,
413: webob.exc.HTTPRequestEntityTooLarge,
415: webob.exc.HTTPUnsupportedMediaType,
429: webob.exc.HTTPTooManyRequests,
501: webob.exc.HTTPNotImplemented,
503: webob.exc.HTTPServiceUnavailable,
}
code = response.status_int
message = response.json_body.popitem()[1].get("message")
exc = exceptions.get(code, webob.exc.HTTPInternalServerError)
return exc(explanation=message)

View File

@ -38,7 +38,7 @@ class Controller(ooi.api.base.Controller):
tenant_id = req.environ["keystone.token_auth"].user.project_id
req = self._get_req(req, path="/%s/servers" % tenant_id)
response = req.get_response(self.app)
servers = response.json_body.get("servers", [])
servers = self.get_from_response(response, "servers", [])
occi_compute_resources = self._get_compute_resources(servers)
return collection.Collection(resources=occi_compute_resources)
@ -63,7 +63,7 @@ class Controller(ooi.api.base.Controller):
}}))
response = req.get_response(self.app)
# We only get one server
server = response.json_body.get("server", {})
server = self.get_from_response(response, "server", {})
# The returned JSON does not contain the server name
server["name"] = params["/occi/infrastructure"]
@ -77,13 +77,13 @@ class Controller(ooi.api.base.Controller):
# get info from server
req = self._get_req(req, path="/%s/servers/%s" % (tenant_id, id))
response = req.get_response(self.app)
s = response.json_body.get("server", {})
s = self.get_from_response(response, "server", {})
# get info from flavor
req = self._get_req(req, path="/%s/flavors/%s" % (tenant_id,
s["flavor"]["id"]))
response = req.get_response(self.app)
flavor = response.json_body.get("flavor", {})
flavor = self.get_from_response(response, "flavor", {})
res_tpl = templates.OpenStackResourceTemplate(flavor["name"],
flavor["vcpus"],
flavor["ram"],
@ -93,7 +93,7 @@ class Controller(ooi.api.base.Controller):
req = self._get_req(req, path="/%s/images/%s" % (tenant_id,
s["image"]["id"]))
response = req.get_response(self.app)
image = response.json_body.get("image", {})
image = self.get_from_response(response, "image", {})
os_tpl = templates.OpenStackOSTemplate(image["id"],
image["name"])

View File

@ -29,7 +29,7 @@ class Controller(base.Controller):
tenant_id = req.environ["keystone.token_auth"].user.project_id
req = self._get_req(req, path="/%s/flavors/detail" % tenant_id)
response = req.get_response(self.app)
flavors = response.json_body.get("flavors", [])
flavors = self.get_from_response(response, "flavors", [])
occi_resource_templates = []
if flavors:
for f in flavors:
@ -44,7 +44,7 @@ class Controller(base.Controller):
tenant_id = req.environ["keystone.token_auth"].user.project_id
req = self._get_req(req, path="/%s/images/detail" % tenant_id)
response = req.get_response(self.app)
images = response.json_body.get("images", [])
images = self.get_from_response(response, "images", [])
occi_os_templates = []
if images:
for i in images:

View File

@ -24,5 +24,10 @@ class Action(category.Category):
instance.
"""
def __init__(self, scheme, term, title, attributes=None, location=None):
super(Action, self).__init__(scheme, term, title,
attributes=attributes,
location="?action=%s" % term)
def _class_name(self):
return "action"

View File

@ -35,24 +35,6 @@ class Attribute(object):
def value(self):
return self._value
def _as_str(self):
value_str = ''
if isinstance(self._value, six.string_types):
value_str = '"%s"' % self._value
elif isinstance(self._value, bool):
value_str = '"%s"' % str(self._value).lower()
else:
value_str = "%s" % self._value
return "%s=%s" % (self.name, value_str)
def __str__(self):
"""Render the attribute to text/plain."""
return ": ".join(self.headers()[0])
def headers(self):
"""Render the attribute to text/occi."""
return [("X-OCCI-Attribute", self._as_str())]
class MutableAttribute(Attribute):
@Attribute.value.setter

View File

@ -15,6 +15,7 @@
# under the License.
from ooi.occi.core import attribute
from ooi import utils
class Category(object):
@ -37,17 +38,10 @@ class Category(object):
"""Returns this class name (see OCCI v1.1 rendering)."""
raise ValueError
def _as_str(self):
d = {
"term": self.term,
"scheme": self.scheme,
"class": self._class_name()
}
@property
def occi_class(self):
return self._class_name()
return '%(term)s; scheme="%(scheme)s"; class="%(class)s"' % d
def headers(self):
return [("Category", self._as_str())]
def __str__(self):
return ": ".join(self.headers()[0])
@property
def type_id(self):
return utils.join_url(self.scheme, "#%s" % self.term)

View File

@ -30,27 +30,3 @@ class Collection(object):
self.actions = actions
self.resources = resources
self.links = links
def __str__(self):
"""Render the collection to text/plain."""
# NOTE(aloga): This is unfinished, we need to check what is inside the
# collection and render it properly. For example, if we have a
# collection of resources, we should render only their locations.
ret = []
for what in [self.kinds, self.mixins, self.actions,
self.resources, self.links]:
for el in what:
ret.append("X-OCCI-Location: %s" % el.location)
return "\n".join(ret)
def headers(self):
"""Render the collection to text/occi."""
# NOTE(aloga): This is unfinished, we need to check what is inside the
# collection and render it properly. For example, if we have a
# collection of resources, we should render only their locations.
headers = []
for what in [self.kinds, self.mixins, self.actions,
self.resources, self.links]:
for el in what:
headers.append(("X-OCCI-Location", el.location))
return headers

View File

@ -22,6 +22,7 @@ from ooi.occi.core import attribute
from ooi.occi.core import kind
from ooi.occi.core import mixin
from ooi.occi import helpers
from ooi import utils
class EntityMeta(type):
@ -94,21 +95,4 @@ class Entity(object):
@property
def location(self):
return helpers.join_url(self.kind.location, self.id)
def headers(self):
"""Render the entity to text/occi."""
h = self.kind.headers()
for m in self.mixins:
h.extend(m.headers())
for attr_name in self.attributes:
if self.attributes[attr_name].value is not None:
h.extend(self.attributes[attr_name].headers())
return h
def __str__(self):
"""Render the entity to text/plain."""
ret = []
for h in self.headers():
ret.append(": ".join(h))
return "\n".join(ret)
return utils.join_url(self.kind.location, self.id)

View File

@ -60,10 +60,3 @@ class Resource(entity.Entity):
@summary.setter
def summary(self, value):
self.attributes["occi.core.summary"].value = value
def __str__(self):
"""Render the resource to text/plain."""
ret = [super(Resource, self).__str__()]
for link in self.links:
ret.append("%s" % link)
return "\n".join(ret)

View File

@ -30,9 +30,3 @@ def check_type(obj_list, obj_type):
if not all([isinstance(i, obj_type) for i in obj_list]):
raise TypeError('object must be of class %s' % obj_type)
def join_url(prefix, remainder, fragments=None):
if fragments:
remainder = "%s#%s" % (remainder, fragments)
return urlparse.urljoin(prefix, remainder)

View File

View File

@ -0,0 +1,146 @@
# -*- coding: utf-8 -*-
# Copyright 2015 Spanish National Research Council
#
# 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 six
import webob.exc
from ooi.occi.core import action
from ooi.occi.core import collection
from ooi.occi.core import kind
from ooi.occi.core import mixin
from ooi.occi.core import resource
from ooi import utils
class HeaderRenderer(object):
def __init__(self, obj):
self.obj = obj
def render(self, env={}):
raise NotImplementedError("%s for %s object not implemented" %
(type(self), type(self.obj)))
class ExceptionRenderer(HeaderRenderer):
def render(self, env={}):
return []
class CategoryRenderer(HeaderRenderer):
def render(self, env={}):
d = {
"term": self.obj.term,
"scheme": self.obj.scheme,
"class": self.obj.occi_class
}
return [('Category',
'%(term)s; scheme="%(scheme)s"; class="%(class)s"' % d)]
class KindRenderer(CategoryRenderer):
pass
class ActionRenderer(CategoryRenderer):
def render(self, instance=None, env={}):
# We have an instance id, render it as a link
if instance is not None:
url = env.get("application_url", "")
url = utils.join_url(url, [instance, self.obj.location])
d = {"location": url,
"rel": self.obj.type_id}
link = "<%(location)s>; rel=%(rel)s" % d
return [('Link', link)]
else:
# Otherwise, render as category
return super(ActionRenderer, self).render(env=env)
class MixinRenderer(CategoryRenderer):
pass
class CollectionRenderer(HeaderRenderer):
def render(self, env={}):
app_url = env.get("application_url", "")
ret = []
for what in [self.obj.kinds, self.obj.mixins, self.obj.actions,
self.obj.resources, self.obj.links]:
for el in what:
url = app_url + el.location
ret.append(('X-OCCI-Location', '%s' % url))
return ret
class AttributeRenderer(HeaderRenderer):
def render(self, env={}):
value_str = ''
if isinstance(self.obj.value, six.string_types):
value_str = '"%s"' % self.obj.value
elif isinstance(self.obj.value, bool):
value_str = '"%s"' % str(self.obj.value).lower()
else:
value_str = "%s" % self.obj.value
return [('X-OCCI-Attribute', '%s=%s' % (self.obj.name, value_str))]
class ResourceRenderer(HeaderRenderer):
def render(self, env={}):
ret = []
ret.extend(KindRenderer(self.obj.kind).render())
for m in self.obj.mixins:
ret.extend(MixinRenderer(m).render())
for a in self.obj.attributes:
# FIXME(aloga): I dont like this test here
if self.obj.attributes[a].value is None:
continue
ret.extend(AttributeRenderer(self.obj.attributes[a]).render())
for a in self.obj.actions:
ret.extend(ActionRenderer(a).render(instance=self.obj.id))
for l in self.obj.links:
pass
# FIXME(aloga): we need to fix this
# ret.append(LinkRenderer(l))
return ret
_MAP = {
"action": ActionRenderer,
"kind": KindRenderer,
"mixin": MixinRenderer,
"collection": CollectionRenderer,
"resource": ResourceRenderer,
"exception": ExceptionRenderer,
None: HeaderRenderer,
}
def get_renderer(obj):
if isinstance(obj, action.Action):
type_ = "action"
elif isinstance(obj, collection.Collection):
type_ = "collection"
elif isinstance(obj, mixin.Mixin):
type_ = "mixin"
elif isinstance(obj, kind.Kind):
type_ = "kind"
elif isinstance(obj, resource.Resource):
type_ = "resource"
elif isinstance(obj, webob.exc.HTTPException):
type_ = "exception"
else:
type_ = None
return _MAP.get(type_)(obj)

View File

@ -0,0 +1,54 @@
# -*- coding: utf-8 -*-
# Copyright 2015 Spanish National Research Council
#
# 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.exc
from ooi.occi.rendering import headers
class TextRenderer(object):
"""Render OCCI objects into text.
The text rendering is just the representation of the OCCI HTTP headers into
text plain, so this renderer wraps around the actual header renderer and
converts the headers into text.
"""
def __init__(self, renderer):
self.renderer = renderer
def render(self, *args, **kwargs):
"""Render the OCCI object into text."""
hdrs = self.renderer.render(*args, **kwargs)
result = []
for hdr in hdrs:
result.append("%s: %s" % hdr)
return "\n".join(result)
class ExceptionRenderer(object):
def __init__(self, obj):
self.obj = obj
def render(self, *args, **kwargs):
return self.obj.explanation
def get_renderer(obj):
"""Get the correct renderer for the given object."""
if isinstance(obj, webob.exc.HTTPException):
return ExceptionRenderer(obj)
else:
return TextRenderer(headers.get_renderer(obj))

View File

@ -18,6 +18,10 @@ import json
import uuid
import webob.dec
import webob.exc
from ooi import utils
import ooi.wsgi
tenants = {
@ -156,6 +160,35 @@ def fake_query_results():
return result
class FakeOpenStackFault(ooi.wsgi.Fault):
_fault_names = {
400: "badRequest",
401: "unauthorized",
403: "forbidden",
404: "itemNotFound",
405: "badMethod",
406: "notAceptable",
409: "conflictingRequest",
413: "overLimit",
415: "badMediaType",
429: "overLimit",
501: "notImplemented",
503: "serviceUnavailable"}
@webob.dec.wsgify()
def __call__(self, req):
code = self.wrapped_exc.status_int
fault_name = self._fault_names.get(code)
explanation = self.wrapped_exc.explanation
fault_data = {
fault_name: {
'code': code,
'message': explanation}}
self.wrapped_exc.body = utils.utf8(json.dumps(fault_data))
self.wrapped_exc.content_type = "application/json"
return self.wrapped_exc
class FakeApp(object):
"""Poor man's fake application."""
@ -205,8 +238,9 @@ class FakeApp(object):
def _do_get(self, req):
try:
ret = self.routes[req.path_info]
except Exception:
raise
except KeyError:
exc = webob.exc.HTTPNotFound()
ret = FakeOpenStackFault(exc)
return ret

View File

@ -14,6 +14,8 @@
# License for the specific language governing permissions and limitations
# under the License.
import uuid
import mock
from ooi.tests import fakes
@ -55,9 +57,21 @@ def build_occi_server(server):
'occi.compute.hostname="%s"' % name,
'occi.core.id="%s"' % server_id,
]
links = []
links.append('<%s?action=restart>; rel=http://schemas.ogf.org/occi/'
'infrastructure/compute/action#restart' % server_id)
links.append('<%s?action=start>; rel=http://schemas.ogf.org/occi/'
'infrastructure/compute/action#start' % server_id)
links.append('<%s?action=stop>; rel=http://schemas.ogf.org/occi/'
'infrastructure/compute/action#stop' % server_id)
links.append('<%s?action=suspend>; rel=http://schemas.ogf.org/occi/'
'infrastructure/compute/action#suspend' % server_id)
result = []
for c in cats:
result.append(("Category", c))
for l in links:
result.append(("Link", l))
for a in attrs:
result.append(("X-OCCI-Attribute", a))
return result
@ -115,6 +129,15 @@ class TestComputeController(test_middleware.TestMiddleware):
self.assertExpectedResult(expected, resp)
self.assertEqual(200, resp.status_code)
def test_vm_not_found(self):
tenant = fakes.tenants["foo"]
app = self.get_app()
req = self._build_req("/compute/%s" % uuid.uuid4().hex,
tenant["id"], method="GET")
resp = req.get_response(app)
self.assertEqual(404, resp.status_code)
def test_create_vm(self):
tenant = fakes.tenants["foo"]

View File

@ -16,6 +16,8 @@
import mock
import webob
import webob.dec
import webob.exc
from ooi.tests import base
from ooi.tests import fakes
@ -63,6 +65,16 @@ class TestMiddleware(base.TestCase):
result = self._build_req("/", "tenant").get_response(self.get_app())
self.assertEqual(404, result.status_code)
def test_400_from_openstack(self):
@webob.dec.wsgify()
def _fake_app(req):
exc = webob.exc.HTTPBadRequest()
resp = fakes.FakeOpenStackFault(exc)
return resp
result = self._build_req("/-/", "tenant").get_response(_fake_app)
self.assertEqual(400, result.status_code)
class TestMiddlewareTextPlain(TestMiddleware):
"""OCCI middleware test with Accept: text/plain."""

View File

@ -46,16 +46,6 @@ class TestAttributes(base.TestCase):
self.assertRaises(AttributeError, set_val)
def test_as_str(self):
attr = attribute.MutableAttribute("occi.foo.bar", "bar")
self.assertEqual('occi.foo.bar="bar"', attr._as_str())
attr.value = True
self.assertEqual('occi.foo.bar="true"', attr._as_str())
attr.value = False
self.assertEqual('occi.foo.bar="false"', attr._as_str())
attr.value = 4.5
self.assertEqual("occi.foo.bar=4.5", attr._as_str())
class TestAttributeCollection(base.TestCase):
def test_collection(self):
@ -121,9 +111,7 @@ class BaseTestCoreOCCICategory(base.TestCase):
class TestCoreOCCICategory(BaseTestCoreOCCICategory):
def test_str(self):
cat = self.obj(*self.args)
self.assertRaises(ValueError, cat.__str__)
pass
class TestCoreOCCIKind(BaseTestCoreOCCICategory):

View File

@ -42,7 +42,7 @@ class FakeController(object):
def show(self, req, id):
# Returning a ResponseObject should stop the pipepline
# so the application won't be called.
resp = wsgi.ResponseObject("Show and stop")
resp = wsgi.ResponseObject([])
return resp
@ -69,7 +69,7 @@ class TestMiddleware(base.TestCase):
result = webob.Request.blank("/foos/stop",
method="GET").get_response(self.app)
self.assertEqual(200, result.status_code)
self.assertEqual("Show and stop", result.text)
self.assertEqual("", result.text)
def test_post(self):
result = webob.Request.blank("/foos",

View File

@ -15,6 +15,7 @@
# under the License.
import six
import six.moves.urllib.parse as urlparse
def utf8(value):
@ -28,3 +29,18 @@ def utf8(value):
return value.encode('utf-8')
assert isinstance(value, str)
return value
def join_url(base, parts):
"""Join several parts into a url.
:param base: the base url
:parts: parts to join into the url
"""
url = base
if not isinstance(parts, (list, tuple)):
parts = [parts]
for p in parts:
url = urlparse.urljoin(url, p)
return url

View File

@ -23,8 +23,8 @@ import ooi.api.compute
from ooi.api import query
from ooi import exception
from ooi.occi.core import collection
from ooi import utils
from ooi.wsgi import serializers
from ooi.wsgi import utils
LOG = logging.getLogger(__name__)
@ -358,20 +358,6 @@ class ResourceExceptionHandler(object):
class Fault(webob.exc.HTTPException):
"""Wrap webob.exc.HTTPException to provide API friendly response."""
_fault_names = {
400: "badRequest",
401: "unauthorized",
403: "forbidden",
404: "itemNotFound",
405: "badMethod",
406: "notAceptable",
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
@ -385,25 +371,16 @@ class Fault(webob.exc.HTTPException):
# Replace the body with fault details.
code = self.wrapped_exc.status_int
fault_name = self._fault_names.get(code, "occiFault")
explanation = self.wrapped_exc.explanation
LOG.debug("Returning %(code)s to user: %(explanation)s",
{'code': code, 'explanation': explanation})
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
content_type = req.content_type or "text/plain"
mtype = serializers.get_media_map().get(content_type,
"text")
serializer = serializers.get_default_serializers()[mtype]
self.wrapped_exc.body = serializer().serialize(fault_data)[-1]
serialized_exc = serializer().serialize(self.wrapped_exc)
self.wrapped_exc.body = serialized_exc[1]
self.wrapped_exc.content_type = content_type
return self.wrapped_exc

View File

@ -16,6 +16,8 @@
import collections
from ooi.occi.rendering import headers as header_rendering
from ooi.occi.rendering import text as text_rendering
from ooi.wsgi import utils
@ -30,7 +32,11 @@ class TextSerializer(object):
if not isinstance(data, list):
data = [data]
ret = "\n".join([str(d) for d in data])
renderers = []
for d in data:
renderers.append(text_rendering.get_renderer(d))
ret = "\n".join([r.render() for r in renderers])
return None, utils.utf8(ret)
@ -39,14 +45,13 @@ class HeaderSerializer(object):
if not isinstance(data, list):
data = [data]
headers = []
renderers = []
for d in data:
if hasattr(d, "headers"):
headers.extend(d.headers())
else:
# NOTE(aloga): we should not be here.
pass
renderers.append(header_rendering.get_renderer(d))
# Header renderers will return a list, so we must flatten the results
# before returning them
headers = [i for r in renderers for i in r.render()]
return headers, ""