From cab5f1374ae5edadb763c6d63b5e0fe53ae7615d Mon Sep 17 00:00:00 2001 From: Alvaro Lopez Garcia Date: Thu, 5 Mar 2015 10:10:07 +0100 Subject: [PATCH 01/19] Pass and call the application from the controllers Our idea is to convert an incoming OCCI request into an OpenStack request, so that we can pass it downstream so as to be processed by nova. However, we should call the final application in each of the controlers instead of just modifying the request, in order to be able to process the response from nova and "re-occi-fy" it. In order to do so, the controllers should receive the application. - This change modifies the BaseController so that in receives the app. If the controller returns a response (because it calls the application, gets the response and process it or by any other reason), that response will be used. If it returns None, the application will be called. - Also modifies the compute controller so that it implements the create_resources() function. --- ooi/api/__init__.py | 3 ++- ooi/api/compute.py | 6 +++++ ooi/api/query.py | 9 ++++---- ooi/tests/test_compute_controller.py | 33 ++++++++++++++++++---------- ooi/wsgi/__init__.py | 6 ++--- 5 files changed, 38 insertions(+), 19 deletions(-) diff --git a/ooi/api/__init__.py b/ooi/api/__init__.py index 915ef58..4cadf7d 100644 --- a/ooi/api/__init__.py +++ b/ooi/api/__init__.py @@ -16,4 +16,5 @@ class BaseController(object): - pass + def __init__(self, app): + self.app = app diff --git a/ooi/api/compute.py b/ooi/api/compute.py index b5ba633..9dad94c 100644 --- a/ooi/api/compute.py +++ b/ooi/api/compute.py @@ -15,9 +15,15 @@ # under the License. import ooi.api +import ooi.wsgi class ComputeController(ooi.api.BaseController): def index(self, req): tenant_id = req.environ["keystone.token_auth"].user.project_id req.path_info = "/%s/servers" % tenant_id + return req.get_response(self.app) + + +def create_resource(app): + return ooi.wsgi.Resource(ComputeController(app)) diff --git a/ooi/api/query.py b/ooi/api/query.py index 7bc61e0..97be49e 100644 --- a/ooi/api/query.py +++ b/ooi/api/query.py @@ -14,16 +14,17 @@ # License for the specific language governing permissions and limitations # under the License. +import ooi.api from ooi.occi.infrastructure import compute import ooi.wsgi -class Controller(object): - def index(self, *args, **kwargs): +class Controller(ooi.api.BaseController): + def index(self, req): l = [] l.extend(compute.ComputeResource.actions) return l -def create_resource(): - return ooi.wsgi.Resource(Controller()) +def create_resource(app): + return ooi.wsgi.Resource(Controller(app)) diff --git a/ooi/tests/test_compute_controller.py b/ooi/tests/test_compute_controller.py index ed90114..e1ffafa 100644 --- a/ooi/tests/test_compute_controller.py +++ b/ooi/tests/test_compute_controller.py @@ -14,6 +14,8 @@ # License for the specific language governing permissions and limitations # under the License. +import json + import mock import webob import webob.dec @@ -23,27 +25,36 @@ from ooi import wsgi -@webob.dec.wsgify -def fake_app(req): - resp = webob.Response("Hi") - return resp +def fake_app(resp): + @webob.dec.wsgify + def app(req): + return resp + return app -class TestComputeMiddleware(base.TestCase): - def setUp(self): - super(TestComputeMiddleware, self).setUp() +def create_fake_json_resp(data): + r = webob.Response() + r.headers["Content-Type"] = "application/json" + r.charset = "utf8" + r.body = json.dumps(data).encode("utf8") + return r - self.app = wsgi.OCCIMiddleware(fake_app) +class TestComputeMiddleware(base.TestCase): def test_list_vms_all(self): - req = webob.Request.blank("/compute", - method="GET") + d = {"servers": []} + fake_resp = create_fake_json_resp(d) + + app = wsgi.OCCIMiddleware(fake_app(fake_resp)) + req = webob.Request.blank("/compute", method="GET") m = mock.MagicMock() m.user.project_id = "3dd7b3f6-c19d-11e4-8dfc-aa07a5b093db" req.environ["keystone.token_auth"] = m - req.get_response(self.app) + resp = req.get_response(app) self.assertEqual("/3dd7b3f6-c19d-11e4-8dfc-aa07a5b093db/servers", req.environ["PATH_INFO"]) + + self.assertEqual(d, resp.json_body) diff --git a/ooi/wsgi/__init__.py b/ooi/wsgi/__init__.py index ae4bfc9..135cdb7 100644 --- a/ooi/wsgi/__init__.py +++ b/ooi/wsgi/__init__.py @@ -110,13 +110,13 @@ def create_resource(): """ self.mapper.redirect("", "/") - self.resources["query"] = query.create_resource() + self.resources["query"] = query.create_resource(self.application) self.mapper.connect("query", "/-/", controller=self.resources["query"], action="index") - self.resources["compute"] = Resource( - ooi.api.compute.ComputeController()) + self.resources["compute"] = ooi.api.compute.create_resource( + self.application) self.mapper.resource("server", "compute", controller=self.resources["compute"]) From bbfeddf63ec8dda4ad18430f06eca2a9abe9f53c Mon Sep 17 00:00:00 2001 From: Alvaro Lopez Garcia Date: Thu, 5 Mar 2015 11:24:02 +0100 Subject: [PATCH 02/19] Reorganize controllers - Rename ooi.api.compute.ComputeController to ooi.api.compute.Controller - Move ooi.api.BaseController to ooi.api.base.Controller --- ooi/api/__init__.py | 20 -------------------- ooi/api/base.py | 20 ++++++++++++++++++++ ooi/api/compute.py | 6 +++--- ooi/api/query.py | 4 ++-- 4 files changed, 25 insertions(+), 25 deletions(-) create mode 100644 ooi/api/base.py diff --git a/ooi/api/__init__.py b/ooi/api/__init__.py index 4cadf7d..e69de29 100644 --- a/ooi/api/__init__.py +++ b/ooi/api/__init__.py @@ -1,20 +0,0 @@ -# -*- 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. - - -class BaseController(object): - def __init__(self, app): - self.app = app diff --git a/ooi/api/base.py b/ooi/api/base.py new file mode 100644 index 0000000..e8bd42c --- /dev/null +++ b/ooi/api/base.py @@ -0,0 +1,20 @@ +# -*- 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. + + +class Controller(object): + def __init__(self, app): + self.app = app diff --git a/ooi/api/compute.py b/ooi/api/compute.py index 9dad94c..ce5e066 100644 --- a/ooi/api/compute.py +++ b/ooi/api/compute.py @@ -14,11 +14,11 @@ # License for the specific language governing permissions and limitations # under the License. -import ooi.api +from ooi.api import base import ooi.wsgi -class ComputeController(ooi.api.BaseController): +class Controller(base.Controller): def index(self, req): tenant_id = req.environ["keystone.token_auth"].user.project_id req.path_info = "/%s/servers" % tenant_id @@ -26,4 +26,4 @@ def index(self, req): def create_resource(app): - return ooi.wsgi.Resource(ComputeController(app)) + return ooi.wsgi.Resource(Controller(app)) diff --git a/ooi/api/query.py b/ooi/api/query.py index 97be49e..9f8c1d3 100644 --- a/ooi/api/query.py +++ b/ooi/api/query.py @@ -14,12 +14,12 @@ # License for the specific language governing permissions and limitations # under the License. -import ooi.api +from ooi.api import base from ooi.occi.infrastructure import compute import ooi.wsgi -class Controller(ooi.api.BaseController): +class Controller(base.Controller): def index(self, req): l = [] l.extend(compute.ComputeResource.actions) From 7bf1f8027edeb064c3bb4bebbf27c88d30f8767b Mon Sep 17 00:00:00 2001 From: Alvaro Lopez Garcia Date: Thu, 5 Mar 2015 14:08:04 +0100 Subject: [PATCH 03/19] Pass down the openstack version to the controllers OpenStack does some checks searching for a version string in the URL, so we need to change the script path in the request to match what they're looking for. This change modifies the controllers so that they receive the version, and adds a helpher method that prepares the request for passing it down to the application. --- ooi/api/base.py | 9 ++++++++- ooi/api/compute.py | 7 +------ ooi/api/query.py | 5 ----- ooi/wsgi/__init__.py | 30 +++++++++++++++++------------- 4 files changed, 26 insertions(+), 25 deletions(-) diff --git a/ooi/api/base.py b/ooi/api/base.py index e8bd42c..1d1aa0e 100644 --- a/ooi/api/base.py +++ b/ooi/api/base.py @@ -16,5 +16,12 @@ class Controller(object): - def __init__(self, app): + def __init__(self, app, openstack_version): self.app = app + self.openstack_version = openstack_version + + def _get_req(self, req, path=None): + req.script_name = self.openstack_version + if path is not None: + req.path_info = path + return req diff --git a/ooi/api/compute.py b/ooi/api/compute.py index ce5e066..4520333 100644 --- a/ooi/api/compute.py +++ b/ooi/api/compute.py @@ -15,15 +15,10 @@ # under the License. from ooi.api import base -import ooi.wsgi class Controller(base.Controller): def index(self, req): tenant_id = req.environ["keystone.token_auth"].user.project_id - req.path_info = "/%s/servers" % tenant_id + req = self._get_req(req, path="/%s/servers" % tenant_id) return req.get_response(self.app) - - -def create_resource(app): - return ooi.wsgi.Resource(Controller(app)) diff --git a/ooi/api/query.py b/ooi/api/query.py index 9f8c1d3..86222a0 100644 --- a/ooi/api/query.py +++ b/ooi/api/query.py @@ -16,7 +16,6 @@ from ooi.api import base from ooi.occi.infrastructure import compute -import ooi.wsgi class Controller(base.Controller): @@ -24,7 +23,3 @@ def index(self, req): l = [] l.extend(compute.ComputeResource.actions) return l - - -def create_resource(app): - return ooi.wsgi.Resource(Controller(app)) diff --git a/ooi/wsgi/__init__.py b/ooi/wsgi/__init__.py index 135cdb7..d0658d7 100644 --- a/ooi/wsgi/__init__.py +++ b/ooi/wsgi/__init__.py @@ -61,16 +61,24 @@ class OCCIMiddleware(object): @classmethod def factory(cls, global_conf, **local_conf): """Factory method for paste.deploy.""" - return cls + def _factory(app): + conf = global_conf.copy() + conf.update(local_conf) + return cls(app, **local_conf) + return _factory - def __init__(self, application): + def __init__(self, application, openstack_version="/v2.1"): self.application = application + self.openstack_version = openstack_version self.resources = {} self.mapper = routes.Mapper() self._setup_routes() + def _create_resource(self, controller): + return Resource(controller(self.application, self.openstack_version)) + def _setup_routes(self): """Setup the mapper routes. @@ -86,15 +94,11 @@ def index(self, *args, **kwargs): # Currently we do not have anything to do here return None - - def create_resource(): - return ooi.wsgi.Resource(Controller()) - This method could populate the mapper as follows: .. code-block:: python - self.resources["query"] = query.create_resource() + self.resources["query"] = self._create_resource(query.Controller) self.mapper.connect("query", "/-/", controller=self.resources["query"], action="index") @@ -103,20 +107,20 @@ def create_resource(): .. code-block:: python - self.resources["resources"] = query.create_resource() - self.mapper.resource("resource", "resources", - controller=self.resources["resources"]) + self.resources["servers"] = self._create_resource(query.Controller) + self.mapper.resource("server", "servers", + controller=self.resources["servers"]) """ self.mapper.redirect("", "/") - self.resources["query"] = query.create_resource(self.application) + self.resources["query"] = self._create_resource(query.Controller) self.mapper.connect("query", "/-/", controller=self.resources["query"], action="index") - self.resources["compute"] = ooi.api.compute.create_resource( - self.application) + self.resources["compute"] = self._create_resource( + ooi.api.compute.Controller) self.mapper.resource("server", "compute", controller=self.resources["compute"]) From 61e05ea43e95c32dd3a12edf5b4b69ce034ba34a Mon Sep 17 00:00:00 2001 From: Alvaro Lopez Garcia Date: Thu, 5 Mar 2015 17:45:29 +0100 Subject: [PATCH 04/19] Add an initial OCCI collection object Depending on what OCCI objects we have and depending on the rendering we should render one thing or another. This collection should be used to do so. Currently only supports the rendering of collections of resources, that are rendered to their locations in both text/occi and text/plain --- ooi/occi/core/collection.py | 56 +++++++++++++++++++++++++++++++++++++ ooi/wsgi/__init__.py | 4 ++- 2 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 ooi/occi/core/collection.py diff --git a/ooi/occi/core/collection.py b/ooi/occi/core/collection.py new file mode 100644 index 0000000..bf83696 --- /dev/null +++ b/ooi/occi/core/collection.py @@ -0,0 +1,56 @@ +# -*- 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. + + +class Collection(object): + """An OCCI Collection is used to render a set of OCCI objects. + + Depending on the rendering and the contents of the collection, there will + be one output or another. This class should do the magic and render the + proper information, taking into account what is in the collection. + """ + def __init__(self, kinds=[], mixins=[], actions=[], + resources=[], links=[]): + + self.kinds = kinds + self.mixins = mixins + 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 diff --git a/ooi/wsgi/__init__.py b/ooi/wsgi/__init__.py index d0658d7..cf8b1bc 100644 --- a/ooi/wsgi/__init__.py +++ b/ooi/wsgi/__init__.py @@ -22,6 +22,7 @@ import ooi.api.compute from ooi.api import query from ooi import exception +from ooi.occi.core import collection from ooi.wsgi import serializers from ooi.wsgi import utils @@ -218,7 +219,8 @@ def __call__(self, request, args): if not response: resp_obj = None # We got something - if type(action_result) is list: +# if type(action_result) is list: + if isinstance(action_result, (list, collection.Collection)): resp_obj = ResponseObject(action_result) elif isinstance(action_result, ResponseObject): resp_obj = action_result From ad57496c32c2804def6ec6216899020c10d5ded7 Mon Sep 17 00:00:00 2001 From: Alvaro Lopez Garcia Date: Thu, 5 Mar 2015 17:50:51 +0100 Subject: [PATCH 05/19] Add a location property to the entities Build the location, based on its kind location and their uuid --- ooi/occi/core/entity.py | 4 ++++ ooi/occi/helpers.py | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/ooi/occi/core/entity.py b/ooi/occi/core/entity.py index 86abf81..4f26a74 100644 --- a/ooi/occi/core/entity.py +++ b/ooi/occi/core/entity.py @@ -91,3 +91,7 @@ def title(self): @title.setter def title(self, value): self.attributes["occi.core.title"].value = value + + @property + def location(self): + return helpers.join_url(self.kind.location, self.id) diff --git a/ooi/occi/helpers.py b/ooi/occi/helpers.py index 34c7a96..ee30077 100644 --- a/ooi/occi/helpers.py +++ b/ooi/occi/helpers.py @@ -30,3 +30,9 @@ 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) From 08c57d4b8b505c21464bfc651375e93c345594f4 Mon Sep 17 00:00:00 2001 From: Alvaro Lopez Garcia Date: Thu, 5 Mar 2015 13:57:25 +0100 Subject: [PATCH 06/19] compute controller: list servers in OCCI With the new code the controllers should call the application themselves, get the response, and "re-occi-fy" it. We do this in the compute controller so we call nova, capture the response, parse it, build the OCCI objects and return an OCCI collection so that it is properly rendered. --- ooi/api/compute.py | 13 +++++++++++- ooi/tests/test_compute_controller.py | 31 ++++++++++++++++++++++++++-- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/ooi/api/compute.py b/ooi/api/compute.py index 4520333..e47ab73 100644 --- a/ooi/api/compute.py +++ b/ooi/api/compute.py @@ -15,10 +15,21 @@ # under the License. from ooi.api import base +from ooi.occi.core import collection +from ooi.occi.infrastructure import compute class Controller(base.Controller): def index(self, req): tenant_id = req.environ["keystone.token_auth"].user.project_id req = self._get_req(req, path="/%s/servers" % tenant_id) - return req.get_response(self.app) + response = req.get_response(self.app) + + servers = response.json_body.get("servers", []) + occi_compute_resources = [] + if servers: + for s in servers: + s = compute.ComputeResource(title=s["name"], id=s["id"]) + occi_compute_resources.append(s) + + return collection.Collection(resources=occi_compute_resources) diff --git a/ooi/tests/test_compute_controller.py b/ooi/tests/test_compute_controller.py index e1ffafa..14d0c01 100644 --- a/ooi/tests/test_compute_controller.py +++ b/ooi/tests/test_compute_controller.py @@ -15,6 +15,7 @@ # under the License. import json +import uuid import mock import webob @@ -41,7 +42,7 @@ def create_fake_json_resp(data): class TestComputeMiddleware(base.TestCase): - def test_list_vms_all(self): + def test_list_vms_empty(self): d = {"servers": []} fake_resp = create_fake_json_resp(d) @@ -57,4 +58,30 @@ def test_list_vms_all(self): self.assertEqual("/3dd7b3f6-c19d-11e4-8dfc-aa07a5b093db/servers", req.environ["PATH_INFO"]) - self.assertEqual(d, resp.json_body) + self.assertEqual(200, resp.status_code) + self.assertEqual("", resp.text) + + def test_list_vms_one_vm(self): + tenant = uuid.uuid4().hex + + d = {"servers": [{"id": uuid.uuid4().hex, "name": "foo"}, + {"id": uuid.uuid4().hex, "name": "bar"}, + {"id": uuid.uuid4().hex, "name": "baz"}]} + + fake_resp = create_fake_json_resp(d) + + app = wsgi.OCCIMiddleware(fake_app(fake_resp)) + req = webob.Request.blank("/compute", method="GET") + + m = mock.MagicMock() + m.user.project_id = tenant + req.environ["keystone.token_auth"] = m + + resp = req.get_response(app) + + self.assertEqual("/%s/servers" % tenant, req.environ["PATH_INFO"]) + + self.assertEqual(200, resp.status_code) + for s in d["servers"]: + expected = "X-OCCI-Location: /compute/%s" % s["id"] + self.assertIn(expected, resp.text) From ab5084506164bbf1d87f37dcfa0a36849a0d5f0d Mon Sep 17 00:00:00 2001 From: Enol Fernandez Date: Fri, 6 Mar 2015 11:15:57 +0000 Subject: [PATCH 07/19] compute_controller: show. Still missing the creation of links and mixins of the compute resource. --- ooi/api/compute.py | 22 ++++++++++++++++++++ ooi/occi/core/attribute.py | 12 +++++++++++ ooi/occi/core/category.py | 2 +- ooi/occi/infrastructure/compute.py | 12 +++++++++++ ooi/tests/test_compute_controller.py | 30 ++++++++++++++++++++++++++++ 5 files changed, 77 insertions(+), 1 deletion(-) diff --git a/ooi/api/compute.py b/ooi/api/compute.py index e47ab73..79ca16b 100644 --- a/ooi/api/compute.py +++ b/ooi/api/compute.py @@ -19,6 +19,17 @@ from ooi.occi.infrastructure import compute +# TODO(enolfc): move this function elsewhere? Check the correct names +# of nova states +def map_occi_state(nova_status): + if nova_status in ["ACTIVE"]: + return "active" + elif nova_status in ["PAUSED", "SUSPENDED", "STOPPED"]: + return "suspended" + else: + return "inactive" + + class Controller(base.Controller): def index(self, req): tenant_id = req.environ["keystone.token_auth"].user.project_id @@ -33,3 +44,14 @@ def index(self, req): occi_compute_resources.append(s) return collection.Collection(resources=occi_compute_resources) + + def show(self, id, req): + tenant_id = req.environ["keystone.token_auth"].user.project_id + req = self._get_req(req, path="/%s/servers/%s" % (tenant_id, id)) + response = req.get_response(self.app) + + s = response.json_body.get("server", []) + comp = compute.ComputeResource(title=s["name"], id=s["id"], + hostname=s["name"], + state=map_occi_state(s["status"])) + return [comp] diff --git a/ooi/occi/core/attribute.py b/ooi/occi/core/attribute.py index 324f489..e108a39 100644 --- a/ooi/occi/core/attribute.py +++ b/ooi/occi/core/attribute.py @@ -35,6 +35,18 @@ def name(self): def value(self): return self._value + def _value_str(self): + if isinstance(self._value, six.string_types): + return '"%s"' % self._value + elif isinstance(self._value, bool): + return '"%s"' % str(self._value).lower() + else: + "%s" % self._value + + def __str__(self): + """Render the attribute to text/plain.""" + return "X-OCCI-Attribute: %s=%s" % (self.name, self._value_str()) + class MutableAttribute(Attribute): @Attribute.value.setter diff --git a/ooi/occi/core/category.py b/ooi/occi/core/category.py index 4abdba4..ae8c665 100644 --- a/ooi/occi/core/category.py +++ b/ooi/occi/core/category.py @@ -40,7 +40,7 @@ def _as_str(self): "class": self.__class__.__name__.lower() } - return "%(term)s; scheme=%(scheme)s; class=%(class)s" % d + return '%(term)s; scheme="%(scheme)s"; class=%(class)s' % d def headers(self): return [("Category", self._as_str())] diff --git a/ooi/occi/infrastructure/compute.py b/ooi/occi/infrastructure/compute.py index 5a9e4f2..72fe7f2 100644 --- a/ooi/occi/infrastructure/compute.py +++ b/ooi/occi/infrastructure/compute.py @@ -14,6 +14,8 @@ # License for the specific language governing permissions and limitations # under the License. +import itertools + from ooi.occi.core import action from ooi.occi.core import attribute as attr from ooi.occi.core import kind @@ -111,3 +113,13 @@ def memory(self, value): @property def state(self): return self.attributes["occi.compute.state"].value + + def __str__(self): + """Render the resource to text/plain.""" + ret = ["%s" % self.kind] + for what in itertools.chain(self.mixins, self.actions): + ret.append("%s" % what) + for attr_name in self.attributes: + if self.attributes[attr_name].value is not None: + ret.append("%s" % self.attributes[attr_name]) + return "\n".join(ret) diff --git a/ooi/tests/test_compute_controller.py b/ooi/tests/test_compute_controller.py index 14d0c01..7703a33 100644 --- a/ooi/tests/test_compute_controller.py +++ b/ooi/tests/test_compute_controller.py @@ -41,6 +41,8 @@ def create_fake_json_resp(data): return r +# TODO(enolfc): split tests? i.e. one test to check that the correct +# PATH_INFO, other for correct output (not text, but objects) class TestComputeMiddleware(base.TestCase): def test_list_vms_empty(self): d = {"servers": []} @@ -85,3 +87,31 @@ def test_list_vms_one_vm(self): for s in d["servers"]: expected = "X-OCCI-Location: /compute/%s" % s["id"] self.assertIn(expected, resp.text) + + def test_show_vm(self): + tenant = uuid.uuid4().hex + + server_id = uuid.uuid4().hex + d = {"server": {"id": server_id, + "name": "foo", + "flavor": {"id": "1"}, + "image": {"id": uuid.uuid4().hex}, + "status": "ACTIVE"}} + + fake_resp = create_fake_json_resp(d) + + app = wsgi.OCCIMiddleware(fake_app(fake_resp)) + req = webob.Request.blank("/compute/%s" % server_id, method="GET") + + m = mock.MagicMock() + m.user.project_id = tenant + req.environ["keystone.token_auth"] = m + + resp = req.get_response(app) + + self.assertEqual("/%s/servers/%s" % (tenant, server_id), + req.environ["PATH_INFO"]) + + self.assertEqual(200, resp.status_code) + expected = 'X-OCCI-Attribute: occi.core.id="%s"' % server_id + self.assertIn(expected, resp.text) From 1a99f3482b337bf3d023c9bc2716b44c49c6c373 Mon Sep 17 00:00:00 2001 From: Enol Fernandez Date: Fri, 6 Mar 2015 15:27:31 +0000 Subject: [PATCH 08/19] Moved part of the rendering to entity. --- ooi/api/compute.py | 1 - ooi/occi/core/action.py | 4 +++- ooi/occi/core/attribute.py | 6 +++--- ooi/occi/core/category.py | 8 ++++++-- ooi/occi/core/entity.py | 10 ++++++++++ ooi/occi/core/kind.py | 3 +++ ooi/occi/core/mixin.py | 3 +++ ooi/occi/core/resource.py | 7 +++++++ ooi/occi/infrastructure/compute.py | 12 ------------ 9 files changed, 35 insertions(+), 19 deletions(-) diff --git a/ooi/api/compute.py b/ooi/api/compute.py index 79ca16b..3be4184 100644 --- a/ooi/api/compute.py +++ b/ooi/api/compute.py @@ -18,7 +18,6 @@ from ooi.occi.core import collection from ooi.occi.infrastructure import compute - # TODO(enolfc): move this function elsewhere? Check the correct names # of nova states def map_occi_state(nova_status): diff --git a/ooi/occi/core/action.py b/ooi/occi/core/action.py index f5ae182..307247b 100644 --- a/ooi/occi/core/action.py +++ b/ooi/occi/core/action.py @@ -23,4 +23,6 @@ class Action(category.Category): An Action represents an invocable operation applicable to a resource instance. """ - pass + + def _class_name(self): + return "action" diff --git a/ooi/occi/core/attribute.py b/ooi/occi/core/attribute.py index e108a39..2d3b0a7 100644 --- a/ooi/occi/core/attribute.py +++ b/ooi/occi/core/attribute.py @@ -35,17 +35,17 @@ def name(self): def value(self): return self._value - def _value_str(self): + def _as_str(self): if isinstance(self._value, six.string_types): return '"%s"' % self._value elif isinstance(self._value, bool): return '"%s"' % str(self._value).lower() else: - "%s" % self._value + return "%s" % self._value def __str__(self): """Render the attribute to text/plain.""" - return "X-OCCI-Attribute: %s=%s" % (self.name, self._value_str()) + return "X-OCCI-Attribute: %s=%s" % (self.name, self._as_str()) class MutableAttribute(Attribute): diff --git a/ooi/occi/core/category.py b/ooi/occi/core/category.py index ae8c665..c9cb2e2 100644 --- a/ooi/occi/core/category.py +++ b/ooi/occi/core/category.py @@ -33,14 +33,18 @@ def __init__(self, scheme, term, title, attributes=None, location=None): self.attributes = attributes self.location = location + def _class_name(self): + """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__.lower() + "class": self._class_name() } - return '%(term)s; scheme="%(scheme)s"; class=%(class)s' % d + return '%(term)s; scheme="%(scheme)s"; class="%(class)s"' % d def headers(self): return [("Category", self._as_str())] diff --git a/ooi/occi/core/entity.py b/ooi/occi/core/entity.py index 4f26a74..e67f52e 100644 --- a/ooi/occi/core/entity.py +++ b/ooi/occi/core/entity.py @@ -95,3 +95,13 @@ def title(self, value): @property def location(self): return helpers.join_url(self.kind.location, self.id) + + def __str__(self): + """Render the entity to text/plain.""" + ret = ["%s" % self.kind] + for m in self.mixins: + ret.append("%s" % m) + for attr_name in self.attributes: + if self.attributes[attr_name].value is not None: + ret.append("%s" % self.attributes[attr_name]) + return "\n".join(ret) diff --git a/ooi/occi/core/kind.py b/ooi/occi/core/kind.py index 68f0739..2e6f79b 100644 --- a/ooi/occi/core/kind.py +++ b/ooi/occi/core/kind.py @@ -37,3 +37,6 @@ def __init__(self, scheme, term, title, attributes=None, location=None, self.related = related self.actions = actions + + def _class_name(self): + return "kind" diff --git a/ooi/occi/core/mixin.py b/ooi/occi/core/mixin.py index 954fc64..0b16761 100644 --- a/ooi/occi/core/mixin.py +++ b/ooi/occi/core/mixin.py @@ -37,3 +37,6 @@ def __init__(self, scheme, term, title, attributes=None, location=None, self.related = related self.actions = actions + + def _class_name(self): + return "mixin" diff --git a/ooi/occi/core/resource.py b/ooi/occi/core/resource.py index cf54dae..ad026ca 100644 --- a/ooi/occi/core/resource.py +++ b/ooi/occi/core/resource.py @@ -60,3 +60,10 @@ def summary(self): @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) diff --git a/ooi/occi/infrastructure/compute.py b/ooi/occi/infrastructure/compute.py index 72fe7f2..5a9e4f2 100644 --- a/ooi/occi/infrastructure/compute.py +++ b/ooi/occi/infrastructure/compute.py @@ -14,8 +14,6 @@ # License for the specific language governing permissions and limitations # under the License. -import itertools - from ooi.occi.core import action from ooi.occi.core import attribute as attr from ooi.occi.core import kind @@ -113,13 +111,3 @@ def memory(self, value): @property def state(self): return self.attributes["occi.compute.state"].value - - def __str__(self): - """Render the resource to text/plain.""" - ret = ["%s" % self.kind] - for what in itertools.chain(self.mixins, self.actions): - ret.append("%s" % what) - for attr_name in self.attributes: - if self.attributes[attr_name].value is not None: - ret.append("%s" % self.attributes[attr_name]) - return "\n".join(ret) From 2f0dd98f1584156a486d2b13fc36dfe5f65aabb4 Mon Sep 17 00:00:00 2001 From: Enol Fernandez Date: Wed, 11 Mar 2015 10:32:41 +0000 Subject: [PATCH 09/19] Added compute mixins. Fix tests. Use a dictionary for the responses of the fake app. Moved function to openstack.helpers. --- ooi/api/compute.py | 41 +++++++++++++++++-------- ooi/occi/infrastructure/compute.py | 6 ++-- ooi/openstack/helpers.py | 10 +++++++ ooi/tests/test_compute_controller.py | 43 ++++++++++++++++----------- ooi/tests/test_occi.py | 22 ++++++++++++-- ooi/tests/test_occi_infrastructure.py | 27 +++++++++++++++++ ooi/tests/test_openstack.py | 9 ++++++ 7 files changed, 122 insertions(+), 36 deletions(-) diff --git a/ooi/api/compute.py b/ooi/api/compute.py index 3be4184..b15adcc 100644 --- a/ooi/api/compute.py +++ b/ooi/api/compute.py @@ -17,16 +17,8 @@ from ooi.api import base from ooi.occi.core import collection from ooi.occi.infrastructure import compute - -# TODO(enolfc): move this function elsewhere? Check the correct names -# of nova states -def map_occi_state(nova_status): - if nova_status in ["ACTIVE"]: - return "active" - elif nova_status in ["PAUSED", "SUSPENDED", "STOPPED"]: - return "suspended" - else: - return "inactive" +from ooi.openstack import helpers +from ooi.openstack import templates class Controller(base.Controller): @@ -46,11 +38,36 @@ def index(self, req): def show(self, id, req): tenant_id = req.environ["keystone.token_auth"].user.project_id + + # 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", {}) + + # 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", {}) + res_tpl = templates.OpenStackResourceTemplate(flavor["name"], + flavor["vcpus"], + flavor["ram"], + flavor["disk"]) + + # get info from image + 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", {}) + os_tpl = templates.OpenStackOSTemplate(image["id"], + image["name"]) - s = response.json_body.get("server", []) + # build the compute object + # TODO(enolfc): link to network + storage comp = compute.ComputeResource(title=s["name"], id=s["id"], + cores=flavor["vcpus"], hostname=s["name"], - state=map_occi_state(s["status"])) + memory=flavor["ram"], + state=helpers.occi_state(s["status"]), + mixins=[os_tpl, res_tpl]) return [comp] diff --git a/ooi/occi/infrastructure/compute.py b/ooi/occi/infrastructure/compute.py index 5a9e4f2..5f078fd 100644 --- a/ooi/occi/infrastructure/compute.py +++ b/ooi/occi/infrastructure/compute.py @@ -48,9 +48,7 @@ class ComputeResource(resource.Resource): def __init__(self, title, summary=None, id=None, architecture=None, cores=None, hostname=None, speed=None, memory=None, - state=None): - - mixins = [] + state=None, mixins=[]): super(ComputeResource, self).__init__(title, mixins, summary=summary, id=id) @@ -98,7 +96,7 @@ def speed(self): @speed.setter def speed(self, value): - self.attributes["occi.compute.speed.speed"].value = value + self.attributes["occi.compute.speed"].value = value @property def memory(self): diff --git a/ooi/openstack/helpers.py b/ooi/openstack/helpers.py index 93d9b6f..ef66baa 100644 --- a/ooi/openstack/helpers.py +++ b/ooi/openstack/helpers.py @@ -21,3 +21,13 @@ def build_scheme(category): return helpers.build_scheme(category, prefix=_PREFIX) + + +# TODO(enolfc): Check the correct names of nova states +def occi_state(nova_status): + if nova_status in ["ACTIVE"]: + return "active" + elif nova_status in ["PAUSED", "SUSPENDED", "STOPPED"]: + return "suspended" + else: + return "inactive" diff --git a/ooi/tests/test_compute_controller.py b/ooi/tests/test_compute_controller.py index 7703a33..39c5de1 100644 --- a/ooi/tests/test_compute_controller.py +++ b/ooi/tests/test_compute_controller.py @@ -29,7 +29,7 @@ def fake_app(resp): @webob.dec.wsgify def app(req): - return resp + return resp[req.path_info] return app @@ -41,24 +41,25 @@ def create_fake_json_resp(data): return r -# TODO(enolfc): split tests? i.e. one test to check that the correct -# PATH_INFO, other for correct output (not text, but objects) +# TODO(enolfc): this should check the resulting obects, not the text. class TestComputeMiddleware(base.TestCase): def test_list_vms_empty(self): + tenant = uuid.uuid4().hex d = {"servers": []} - fake_resp = create_fake_json_resp(d) + fake_resp = { + '/%s/servers' % tenant: create_fake_json_resp(d), + } app = wsgi.OCCIMiddleware(fake_app(fake_resp)) req = webob.Request.blank("/compute", method="GET") m = mock.MagicMock() - m.user.project_id = "3dd7b3f6-c19d-11e4-8dfc-aa07a5b093db" + m.user.project_id = tenant req.environ["keystone.token_auth"] = m resp = req.get_response(app) - self.assertEqual("/3dd7b3f6-c19d-11e4-8dfc-aa07a5b093db/servers", - req.environ["PATH_INFO"]) + self.assertEqual("/%s/servers" % tenant, req.environ["PATH_INFO"]) self.assertEqual(200, resp.status_code) self.assertEqual("", resp.text) @@ -70,7 +71,9 @@ def test_list_vms_one_vm(self): {"id": uuid.uuid4().hex, "name": "bar"}, {"id": uuid.uuid4().hex, "name": "baz"}]} - fake_resp = create_fake_json_resp(d) + fake_resp = { + '/%s/servers' % tenant: create_fake_json_resp(d), + } app = wsgi.OCCIMiddleware(fake_app(fake_resp)) req = webob.Request.blank("/compute", method="GET") @@ -90,16 +93,25 @@ def test_list_vms_one_vm(self): def test_show_vm(self): tenant = uuid.uuid4().hex - server_id = uuid.uuid4().hex - d = {"server": {"id": server_id, + s = {"server": {"id": server_id, "name": "foo", "flavor": {"id": "1"}, - "image": {"id": uuid.uuid4().hex}, + "image": {"id": "2"}, "status": "ACTIVE"}} - - fake_resp = create_fake_json_resp(d) - + f = {"flavor": {"id": 1, + "name": "foo", + "vcpus": 2, + "ram": 256, + "disk": 10}} + i = {"image": {"id": 2, + "name": "bar"}} + + fake_resp = { + '/%s/servers/%s' % (tenant, server_id): create_fake_json_resp(s), + '/%s/flavors/1' % tenant: create_fake_json_resp(f), + '/%s/images/2' % tenant: create_fake_json_resp(i), + } app = wsgi.OCCIMiddleware(fake_app(fake_resp)) req = webob.Request.blank("/compute/%s" % server_id, method="GET") @@ -109,9 +121,6 @@ def test_show_vm(self): resp = req.get_response(app) - self.assertEqual("/%s/servers/%s" % (tenant, server_id), - req.environ["PATH_INFO"]) - self.assertEqual(200, resp.status_code) expected = 'X-OCCI-Attribute: occi.core.id="%s"' % server_id self.assertIn(expected, resp.text) diff --git a/ooi/tests/test_occi.py b/ooi/tests/test_occi.py index 01a20a7..09049ca 100644 --- a/ooi/tests/test_occi.py +++ b/ooi/tests/test_occi.py @@ -46,6 +46,16 @@ def set_val(): self.assertRaises(AttributeError, set_val) + def test_as_str(self): + attr = attribute.MutableAttribute("occi.foo.bar", "bar") + self.assertEqual('"bar"', attr._as_str()) + attr.value = True + self.assertEqual('"true"', attr._as_str()) + attr.value = False + self.assertEqual('"false"', attr._as_str()) + attr.value = 4.5 + self.assertEqual("4.5", attr._as_str()) + class TestAttributeCollection(base.TestCase): def test_collection(self): @@ -99,7 +109,7 @@ def test_collection_from_invalid(self): mapping) -class TestCoreOCCICategory(base.TestCase): +class BaseTestCoreOCCICategory(base.TestCase): args = ("scheme", "term", "title") obj = category.Category @@ -110,7 +120,13 @@ def test_obj(self): self.assertEqual(i, getattr(cat, i)) -class TestCoreOCCIKind(TestCoreOCCICategory): +class TestCoreOCCICategory(BaseTestCoreOCCICategory): + def test_str(self): + cat = self.obj(*self.args) + self.assertRaises(ValueError, cat.__str__) + + +class TestCoreOCCIKind(BaseTestCoreOCCICategory): obj = kind.Kind def setUp(self): @@ -186,7 +202,7 @@ class TestCoreOCCIMixin(TestCoreOCCIKind): obj = mixin.Mixin -class TestCoreOCCIAction(TestCoreOCCICategory): +class TestCoreOCCIAction(BaseTestCoreOCCICategory): obj = action.Action diff --git a/ooi/tests/test_occi_infrastructure.py b/ooi/tests/test_occi_infrastructure.py index 326569e..e937cc9 100644 --- a/ooi/tests/test_occi_infrastructure.py +++ b/ooi/tests/test_occi_infrastructure.py @@ -58,6 +58,33 @@ def test_compute(self): self.assertIsNone(c.memory) self.assertIsNone(c.speed) + def test_getters(self): + c = compute.ComputeResource("foo") + c.architecture = "bar" + self.assertEqual("bar", + c.attributes["occi.compute.architecture"].value) + c.cores = 5 + self.assertEqual(5, c.attributes["occi.compute.cores"].value) + c.hostname = "foobar" + self.assertEqual("foobar", c.attributes["occi.compute.hostname"].value) + c.speed = 8 + self.assertEqual(8, c.attributes["occi.compute.speed"].value) + c.memory = 4 + self.assertEqual(4, c.attributes["occi.compute.memory"].value) + + def test_setters(self): + c = compute.ComputeResource("foo") + c.attributes["occi.compute.architecture"].value = "bar" + self.assertEqual("bar", c.architecture) + c.attributes["occi.compute.cores"].value = 5 + self.assertEqual(5, c.cores) + c.attributes["occi.compute.hostname"].value = "foobar" + self.assertEqual("foobar", c.hostname) + c.attributes["occi.compute.speed"].value = 8 + self.assertEqual(8, c.speed) + c.attributes["occi.compute.memory"].value = 9 + self.assertEqual(9, c.memory) + class TestTemplates(base.TestCase): def test_os_tpl(self): diff --git a/ooi/tests/test_openstack.py b/ooi/tests/test_openstack.py index cfb4da9..1838c69 100644 --- a/ooi/tests/test_openstack.py +++ b/ooi/tests/test_openstack.py @@ -60,3 +60,12 @@ def test_resource_template(self): self.assertEqual(disk, tpl.disk) self.assertEqual(swap, tpl.swap) self.assertEqual(ephemeral, tpl.ephemeral) + + +class TestHelpers(base.TestCase): + def test_occi_state(self): + self.assertEqual("active", helpers.occi_state("ACTIVE")) + self.assertEqual("suspended", helpers.occi_state("PAUSED")) + self.assertEqual("suspended", helpers.occi_state("SUSPENDED")) + self.assertEqual("suspended", helpers.occi_state("STOPPED")) + self.assertEqual("inactive", helpers.occi_state("BUILDING")) From 7332183b55a7ca8db96f6ea7b3bb302b2f40a58d Mon Sep 17 00:00:00 2001 From: Alvaro Lopez Garcia Date: Tue, 17 Mar 2015 11:16:35 +0100 Subject: [PATCH 10/19] Improve OCCI WSGI middleware tests In the previuos code we were only checking that the rendering was done OK, but we were relying on the result objects so as to check the expected result. Now we specify explicitly which is the expected result. --- ooi/tests/test_occi_middleware.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/ooi/tests/test_occi_middleware.py b/ooi/tests/test_occi_middleware.py index 16a8c24..7d27cde 100644 --- a/ooi/tests/test_occi_middleware.py +++ b/ooi/tests/test_occi_middleware.py @@ -16,7 +16,6 @@ import webob -from ooi.occi.infrastructure import compute from ooi.tests import base import ooi.tests.test_wsgi from ooi import wsgi @@ -36,8 +35,8 @@ def assertContentType(self, result): self.assertEqual(expected, result.content_type) def assertExpectedResult(self, expected, result): - for e in expected: - self.assertIn(str(e), result.text) + expected = ["%s: %s" % e for e in expected] + self.assertEqual("\n".join(expected), result.text) def _build_req(self, path, **kwargs): if self.accept is not None: @@ -52,8 +51,15 @@ def test_404(self): def test_query(self): result = self._build_req("/-/").get_response(self.app) + expected_result = [ + ('Category', 'start; scheme="http://schemas.ogf.org/occi/infrastructure/compute/action"; class="action"'), # noqa + ('Category', 'stop; scheme="http://schemas.ogf.org/occi/infrastructure/compute/action"; class="action"'), # noqa + ('Category', 'restart; scheme="http://schemas.ogf.org/occi/infrastructure/compute/action"; class="action"'), # noqa + ('Category', 'suspend; scheme="http://schemas.ogf.org/occi/infrastructure/compute/action"; class="action"'), # noqa + ] + self.assertContentType(result) - self.assertExpectedResult(compute.ComputeResource.actions, result) + self.assertExpectedResult(expected_result, result) self.assertEqual(200, result.status_code) @@ -73,6 +79,5 @@ def setUp(self): self.accept = "text/occi" def assertExpectedResult(self, expected, result): - for e in expected: - for hdr, val in e.headers(): - self.assertIn(val, result.headers.getall(hdr)) + for hdr, val in expected: + self.assertIn(val, result.headers.getall(hdr)) From 5108d73ae9a58611d1046f103aa3c364c6b38411 Mon Sep 17 00:00:00 2001 From: Alvaro Lopez Garcia Date: Tue, 17 Mar 2015 11:35:47 +0100 Subject: [PATCH 11/19] Refactor the OCCI middleware tests Create a new skeleton to ensure that all the controllers undergo the same tests. Using a base test that does not set the "Accept" header on the request (according to the OCCI specification this should render into "text/plain"), and two subclasses that set the it to "text/plain" and "text/occi". Afterwars, we can test each controller creating subclasses of these three "base" tests. --- ooi/tests/test_occi_middleware.py | 64 ++++++++++++++++++++++--------- 1 file changed, 46 insertions(+), 18 deletions(-) diff --git a/ooi/tests/test_occi_middleware.py b/ooi/tests/test_occi_middleware.py index 7d27cde..89f5afe 100644 --- a/ooi/tests/test_occi_middleware.py +++ b/ooi/tests/test_occi_middleware.py @@ -23,9 +23,15 @@ fake_app = ooi.tests.test_wsgi.fake_app -class TestOCCIMiddleware(base.TestCase): +class TestMiddleware(base.TestCase): + """OCCI middleware test without Accept header. + + According to the OCCI HTTP rendering, no Accept header + means text/plain. + """ + def setUp(self): - super(TestOCCIMiddleware, self).setUp() + super(TestMiddleware, self).setUp() self.app = wsgi.OCCIMiddleware(fake_app) self.accept = None @@ -48,6 +54,40 @@ def test_404(self): result = self._build_req("/").get_response(self.app) self.assertEqual(404, result.status_code) + +class TestMiddlewareTextPlain(TestMiddleware): + """OCCI middleware test with Accept: text/plain.""" + + def setUp(self): + super(TestMiddlewareTextPlain, self).setUp() + + self.app = wsgi.OCCIMiddleware(fake_app) + self.accept = "text/plain" + + def test_correct_accept(self): + self.assertEqual("text/plain", self.accept) + + +class TestMiddlewareTextOcci(TestMiddleware): + """OCCI middleware text with Accept: text/occi.""" + + def setUp(self): + super(TestMiddlewareTextOcci, self).setUp() + + self.app = wsgi.OCCIMiddleware(fake_app) + self.accept = "text/occi" + + def assertExpectedResult(self, expected, result): + for hdr, val in expected: + self.assertIn(val, result.headers.getall(hdr)) + + def test_correct_accept(self): + self.assertEqual("text/occi", self.accept) + + +class TestQueryController(TestMiddleware): + """Test OCCI query controller.""" + def test_query(self): result = self._build_req("/-/").get_response(self.app) @@ -63,21 +103,9 @@ def test_query(self): self.assertEqual(200, result.status_code) -class TestOCCIMiddlewareContentTypeText(TestOCCIMiddleware): - def setUp(self): - super(TestOCCIMiddlewareContentTypeText, self).setUp() - - self.app = wsgi.OCCIMiddleware(fake_app) - self.accept = "text/plain" - +class QueryControllerTextPlain(TestMiddlewareTextPlain, TestQueryController): + """Test OCCI query controller with Accept: text/plain.""" -class TestOCCIMiddlewareContentTypeOCCIHeaders(TestOCCIMiddleware): - def setUp(self): - super(TestOCCIMiddlewareContentTypeOCCIHeaders, self).setUp() - self.app = wsgi.OCCIMiddleware(fake_app) - self.accept = "text/occi" - - def assertExpectedResult(self, expected, result): - for hdr, val in expected: - self.assertIn(val, result.headers.getall(hdr)) +class QueryControllerTextOcci(TestMiddlewareTextOcci, TestQueryController): + """Test OCCI query controller with Accept: text/cci.""" From 65c13a9640b7874e1e2c6aa849c2c0237ae90ec9 Mon Sep 17 00:00:00 2001 From: Alvaro Lopez Garcia Date: Tue, 17 Mar 2015 11:43:14 +0100 Subject: [PATCH 12/19] Reorganize test layout --- ooi/tests/middleware/__init__.py | 0 ooi/tests/{ => middleware}/test_compute_controller.py | 0 .../{test_occi_middleware.py => middleware/test_middleware.py} | 0 ooi/tests/occi/__init__.py | 0 ooi/tests/{test_occi.py => occi/test_occi_core.py} | 0 ooi/tests/{ => occi}/test_occi_infrastructure.py | 0 ooi/tests/{ => occi}/test_openstack.py | 0 7 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 ooi/tests/middleware/__init__.py rename ooi/tests/{ => middleware}/test_compute_controller.py (100%) rename ooi/tests/{test_occi_middleware.py => middleware/test_middleware.py} (100%) create mode 100644 ooi/tests/occi/__init__.py rename ooi/tests/{test_occi.py => occi/test_occi_core.py} (100%) rename ooi/tests/{ => occi}/test_occi_infrastructure.py (100%) rename ooi/tests/{ => occi}/test_openstack.py (100%) diff --git a/ooi/tests/middleware/__init__.py b/ooi/tests/middleware/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ooi/tests/test_compute_controller.py b/ooi/tests/middleware/test_compute_controller.py similarity index 100% rename from ooi/tests/test_compute_controller.py rename to ooi/tests/middleware/test_compute_controller.py diff --git a/ooi/tests/test_occi_middleware.py b/ooi/tests/middleware/test_middleware.py similarity index 100% rename from ooi/tests/test_occi_middleware.py rename to ooi/tests/middleware/test_middleware.py diff --git a/ooi/tests/occi/__init__.py b/ooi/tests/occi/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ooi/tests/test_occi.py b/ooi/tests/occi/test_occi_core.py similarity index 100% rename from ooi/tests/test_occi.py rename to ooi/tests/occi/test_occi_core.py diff --git a/ooi/tests/test_occi_infrastructure.py b/ooi/tests/occi/test_occi_infrastructure.py similarity index 100% rename from ooi/tests/test_occi_infrastructure.py rename to ooi/tests/occi/test_occi_infrastructure.py diff --git a/ooi/tests/test_openstack.py b/ooi/tests/occi/test_openstack.py similarity index 100% rename from ooi/tests/test_openstack.py rename to ooi/tests/occi/test_openstack.py From 67ecc164a133d12efc7c1fa22f1efb5122803e85 Mon Sep 17 00:00:00 2001 From: Alvaro Lopez Garcia Date: Tue, 17 Mar 2015 11:53:17 +0100 Subject: [PATCH 13/19] Move query controller tests into its own test module Instead of having a single monolithic test file it's better to separate it into its own modules. --- ooi/tests/middleware/test_middleware.py | 26 ----------- ooi/tests/middleware/test_query_controller.py | 46 +++++++++++++++++++ 2 files changed, 46 insertions(+), 26 deletions(-) create mode 100644 ooi/tests/middleware/test_query_controller.py diff --git a/ooi/tests/middleware/test_middleware.py b/ooi/tests/middleware/test_middleware.py index 89f5afe..2c5a5a2 100644 --- a/ooi/tests/middleware/test_middleware.py +++ b/ooi/tests/middleware/test_middleware.py @@ -83,29 +83,3 @@ def assertExpectedResult(self, expected, result): def test_correct_accept(self): self.assertEqual("text/occi", self.accept) - - -class TestQueryController(TestMiddleware): - """Test OCCI query controller.""" - - def test_query(self): - result = self._build_req("/-/").get_response(self.app) - - expected_result = [ - ('Category', 'start; scheme="http://schemas.ogf.org/occi/infrastructure/compute/action"; class="action"'), # noqa - ('Category', 'stop; scheme="http://schemas.ogf.org/occi/infrastructure/compute/action"; class="action"'), # noqa - ('Category', 'restart; scheme="http://schemas.ogf.org/occi/infrastructure/compute/action"; class="action"'), # noqa - ('Category', 'suspend; scheme="http://schemas.ogf.org/occi/infrastructure/compute/action"; class="action"'), # noqa - ] - - self.assertContentType(result) - self.assertExpectedResult(expected_result, result) - self.assertEqual(200, result.status_code) - - -class QueryControllerTextPlain(TestMiddlewareTextPlain, TestQueryController): - """Test OCCI query controller with Accept: text/plain.""" - - -class QueryControllerTextOcci(TestMiddlewareTextOcci, TestQueryController): - """Test OCCI query controller with Accept: text/cci.""" diff --git a/ooi/tests/middleware/test_query_controller.py b/ooi/tests/middleware/test_query_controller.py new file mode 100644 index 0000000..78dabfb --- /dev/null +++ b/ooi/tests/middleware/test_query_controller.py @@ -0,0 +1,46 @@ +# -*- 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. + + +from ooi.tests.middleware import test_middleware + + +class TestQueryController(test_middleware.TestMiddleware): + """Test OCCI query controller.""" + + def test_query(self): + result = self._build_req("/-/").get_response(self.app) + + expected_result = [ + ('Category', 'start; scheme="http://schemas.ogf.org/occi/infrastructure/compute/action"; class="action"'), # noqa + ('Category', 'stop; scheme="http://schemas.ogf.org/occi/infrastructure/compute/action"; class="action"'), # noqa + ('Category', 'restart; scheme="http://schemas.ogf.org/occi/infrastructure/compute/action"; class="action"'), # noqa + ('Category', 'suspend; scheme="http://schemas.ogf.org/occi/infrastructure/compute/action"; class="action"'), # noqa + ] + + self.assertContentType(result) + self.assertExpectedResult(expected_result, result) + self.assertEqual(200, result.status_code) + + +class QueryControllerTextPlain(test_middleware.TestMiddlewareTextPlain, + TestQueryController): + """Test OCCI query controller with Accept: text/plain.""" + + +class QueryControllerTextOcci(test_middleware.TestMiddlewareTextOcci, + TestQueryController): + """Test OCCI query controller with Accept: text/cci.""" From 08031c5564e931be2e81da76f5926299642e8b51 Mon Sep 17 00:00:00 2001 From: Alvaro Lopez Garcia Date: Tue, 17 Mar 2015 13:18:17 +0100 Subject: [PATCH 14/19] Update and adapt compute controller tests Adapt the compute controller tests so that it uses the new tests skeleton. --- .../middleware/test_compute_controller.py | 61 ++++++++++++------- ooi/tests/middleware/test_middleware.py | 18 +++--- ooi/tests/middleware/test_query_controller.py | 2 +- ooi/wsgi/__init__.py | 1 - ooi/wsgi/serializers.py | 6 +- 5 files changed, 54 insertions(+), 34 deletions(-) diff --git a/ooi/tests/middleware/test_compute_controller.py b/ooi/tests/middleware/test_compute_controller.py index 39c5de1..448a41c 100644 --- a/ooi/tests/middleware/test_compute_controller.py +++ b/ooi/tests/middleware/test_compute_controller.py @@ -22,15 +22,7 @@ import webob.dec import webob.exc -from ooi.tests import base -from ooi import wsgi - - -def fake_app(resp): - @webob.dec.wsgify - def app(req): - return resp[req.path_info] - return app +from ooi.tests.middleware import test_middleware def create_fake_json_resp(data): @@ -41,17 +33,18 @@ def create_fake_json_resp(data): return r -# TODO(enolfc): this should check the resulting obects, not the text. -class TestComputeMiddleware(base.TestCase): +class TestComputeController(test_middleware.TestMiddleware): + """Test OCCI compute controller.""" + def test_list_vms_empty(self): tenant = uuid.uuid4().hex d = {"servers": []} fake_resp = { '/%s/servers' % tenant: create_fake_json_resp(d), } + app = self.get_app(resp=fake_resp) - app = wsgi.OCCIMiddleware(fake_app(fake_resp)) - req = webob.Request.blank("/compute", method="GET") + req = self._build_req("/compute", method="GET") m = mock.MagicMock() m.user.project_id = tenant @@ -61,8 +54,10 @@ def test_list_vms_empty(self): self.assertEqual("/%s/servers" % tenant, req.environ["PATH_INFO"]) + expected_result = "" + self.assertContentType(resp) + self.assertExpectedResult(expected_result, resp) self.assertEqual(200, resp.status_code) - self.assertEqual("", resp.text) def test_list_vms_one_vm(self): tenant = uuid.uuid4().hex @@ -75,8 +70,8 @@ def test_list_vms_one_vm(self): '/%s/servers' % tenant: create_fake_json_resp(d), } - app = wsgi.OCCIMiddleware(fake_app(fake_resp)) - req = webob.Request.blank("/compute", method="GET") + app = self.get_app(resp=fake_resp) + req = self._build_req("/compute", method="GET") m = mock.MagicMock() m.user.project_id = tenant @@ -87,9 +82,10 @@ def test_list_vms_one_vm(self): self.assertEqual("/%s/servers" % tenant, req.environ["PATH_INFO"]) self.assertEqual(200, resp.status_code) + expected = [] for s in d["servers"]: - expected = "X-OCCI-Location: /compute/%s" % s["id"] - self.assertIn(expected, resp.text) + expected.append(("X-OCCI-Location", "/compute/%s" % s["id"])) + self.assertExpectedResult(expected, resp) def test_show_vm(self): tenant = uuid.uuid4().hex @@ -112,8 +108,8 @@ def test_show_vm(self): '/%s/flavors/1' % tenant: create_fake_json_resp(f), '/%s/images/2' % tenant: create_fake_json_resp(i), } - app = wsgi.OCCIMiddleware(fake_app(fake_resp)) - req = webob.Request.blank("/compute/%s" % server_id, method="GET") + app = self.get_app(resp=fake_resp) + req = self._build_req("/compute/%s" % server_id, method="GET") m = mock.MagicMock() m.user.project_id = tenant @@ -121,6 +117,27 @@ def test_show_vm(self): resp = req.get_response(app) + expected = [ + ('Category', 'compute; scheme="http://schemas.ogf.org/occi/infrastructure"; class="kind"'), # noqa + ('Category', '2; scheme="http://schemas.openstack.org/template/os"; class="mixin"'), # noqa + ('Category', 'foo; scheme="http://schemas.openstack.org/template/resource"; class="mixin"'), # noqa + ('X-OCCI-Attribute', 'occi.core.title="foo"'), + ('X-OCCI-Attribute', 'occi.compute.state="active"'), + ('X-OCCI-Attribute', 'occi.compute.memory=256'), + ('X-OCCI-Attribute', 'occi.compute.cores=2'), + ('X-OCCI-Attribute', 'occi.compute.hostname="foo"'), + ('X-OCCI-Attribute', 'occi.core.id="%s"' % server_id), + ] + self.assertContentType(resp) + self.assertExpectedResult(expected, resp) self.assertEqual(200, resp.status_code) - expected = 'X-OCCI-Attribute: occi.core.id="%s"' % server_id - self.assertIn(expected, resp.text) + + +class ComputeControllerTextPlain(test_middleware.TestMiddlewareTextPlain, + TestComputeController): + """Test OCCI compute controller with Accept: text/plain.""" + + +class ComputeControllerTextOcci(test_middleware.TestMiddlewareTextOcci, + TestComputeController): + """Test OCCI compute controller with Accept: text/occi.""" diff --git a/ooi/tests/middleware/test_middleware.py b/ooi/tests/middleware/test_middleware.py index 2c5a5a2..2c275c6 100644 --- a/ooi/tests/middleware/test_middleware.py +++ b/ooi/tests/middleware/test_middleware.py @@ -17,11 +17,8 @@ import webob from ooi.tests import base -import ooi.tests.test_wsgi from ooi import wsgi -fake_app = ooi.tests.test_wsgi.fake_app - class TestMiddleware(base.TestCase): """OCCI middleware test without Accept header. @@ -33,9 +30,18 @@ class TestMiddleware(base.TestCase): def setUp(self): super(TestMiddleware, self).setUp() - self.app = wsgi.OCCIMiddleware(fake_app) self.accept = None + def get_app(self, resp=None): + if resp is None: + resp = webob.Response() + + @webob.dec.wsgify + def app(req): + # FIXME(aloga): raise some exception here + return resp.get(req.path_info) + return wsgi.OCCIMiddleware(app) + def assertContentType(self, result): expected = self.accept or "text/plain" self.assertEqual(expected, result.content_type) @@ -51,7 +57,7 @@ def _build_req(self, path, **kwargs): **kwargs) def test_404(self): - result = self._build_req("/").get_response(self.app) + result = self._build_req("/").get_response(self.get_app()) self.assertEqual(404, result.status_code) @@ -61,7 +67,6 @@ class TestMiddlewareTextPlain(TestMiddleware): def setUp(self): super(TestMiddlewareTextPlain, self).setUp() - self.app = wsgi.OCCIMiddleware(fake_app) self.accept = "text/plain" def test_correct_accept(self): @@ -74,7 +79,6 @@ class TestMiddlewareTextOcci(TestMiddleware): def setUp(self): super(TestMiddlewareTextOcci, self).setUp() - self.app = wsgi.OCCIMiddleware(fake_app) self.accept = "text/occi" def assertExpectedResult(self, expected, result): diff --git a/ooi/tests/middleware/test_query_controller.py b/ooi/tests/middleware/test_query_controller.py index 78dabfb..f37513d 100644 --- a/ooi/tests/middleware/test_query_controller.py +++ b/ooi/tests/middleware/test_query_controller.py @@ -22,7 +22,7 @@ class TestQueryController(test_middleware.TestMiddleware): """Test OCCI query controller.""" def test_query(self): - result = self._build_req("/-/").get_response(self.app) + result = self._build_req("/-/").get_response(self.get_app()) expected_result = [ ('Category', 'start; scheme="http://schemas.ogf.org/occi/infrastructure/compute/action"; class="action"'), # noqa diff --git a/ooi/wsgi/__init__.py b/ooi/wsgi/__init__.py index cf8b1bc..1d74571 100644 --- a/ooi/wsgi/__init__.py +++ b/ooi/wsgi/__init__.py @@ -219,7 +219,6 @@ def __call__(self, request, args): if not response: resp_obj = None # We got something -# if type(action_result) is list: if isinstance(action_result, (list, collection.Collection)): resp_obj = ResponseObject(action_result) elif isinstance(action_result, ResponseObject): diff --git a/ooi/wsgi/serializers.py b/ooi/wsgi/serializers.py index 636e74d..1e6e36b 100644 --- a/ooi/wsgi/serializers.py +++ b/ooi/wsgi/serializers.py @@ -40,14 +40,14 @@ def serialize(self, data): data = [data] headers = [] - body = [] for d in data: if hasattr(d, "headers"): headers.extend(d.headers()) else: - body.append(str(d)) + # NOTE(aloga): we should not be here. + pass - return headers, body + return headers, "" _SERIALIZERS_MAP = { From cf2ba6a1a30d95424257378bda285ab733afd839 Mon Sep 17 00:00:00 2001 From: Alvaro Lopez Garcia Date: Tue, 17 Mar 2015 17:25:33 +0100 Subject: [PATCH 15/19] Do not take into account order in rendered results The order of the items in the text result does not matter, only the contents. --- ooi/tests/middleware/test_middleware.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ooi/tests/middleware/test_middleware.py b/ooi/tests/middleware/test_middleware.py index 2c275c6..9352e39 100644 --- a/ooi/tests/middleware/test_middleware.py +++ b/ooi/tests/middleware/test_middleware.py @@ -48,7 +48,11 @@ def assertContentType(self, result): def assertExpectedResult(self, expected, result): expected = ["%s: %s" % e for e in expected] - self.assertEqual("\n".join(expected), result.text) + # NOTE(aloga): the order of the result does not matter + results = result.text.splitlines() + results.sort() + expected.sort() + self.assertEqual(expected, results) def _build_req(self, path, **kwargs): if self.accept is not None: From 546d3b63a69b914dc8b29e4a257a9ccd77b8a163 Mon Sep 17 00:00:00 2001 From: Enol Fernandez Date: Tue, 17 Mar 2015 18:31:53 +0100 Subject: [PATCH 16/19] Added headers rendering. --- ooi/occi/core/attribute.py | 14 ++++++++++---- ooi/occi/core/category.py | 2 +- ooi/occi/core/entity.py | 17 ++++++++++++----- ooi/tests/occi/test_occi_core.py | 8 ++++---- 4 files changed, 27 insertions(+), 14 deletions(-) diff --git a/ooi/occi/core/attribute.py b/ooi/occi/core/attribute.py index 2d3b0a7..c5c3403 100644 --- a/ooi/occi/core/attribute.py +++ b/ooi/occi/core/attribute.py @@ -36,16 +36,22 @@ def value(self): return self._value def _as_str(self): + value_str = '' if isinstance(self._value, six.string_types): - return '"%s"' % self._value + value_str = '"%s"' % self._value elif isinstance(self._value, bool): - return '"%s"' % str(self._value).lower() + value_str = '"%s"' % str(self._value).lower() else: - return "%s" % self._value + value_str = "%s" % self._value + return "%s=%s" % (self.name, value_str) def __str__(self): """Render the attribute to text/plain.""" - return "X-OCCI-Attribute: %s=%s" % (self.name, self._as_str()) + return ": ".join(self.headers()[0]) + + def headers(self): + """Render the attribute to text/occi.""" + return [("X-OCCI-Attribute", self._as_str())] class MutableAttribute(Attribute): diff --git a/ooi/occi/core/category.py b/ooi/occi/core/category.py index c9cb2e2..5fb94d5 100644 --- a/ooi/occi/core/category.py +++ b/ooi/occi/core/category.py @@ -50,4 +50,4 @@ def headers(self): return [("Category", self._as_str())] def __str__(self): - return "Category: %s" % self._as_str() + return ": ".join(self.headers()[0]) diff --git a/ooi/occi/core/entity.py b/ooi/occi/core/entity.py index e67f52e..9b4bd03 100644 --- a/ooi/occi/core/entity.py +++ b/ooi/occi/core/entity.py @@ -96,12 +96,19 @@ def title(self, value): def location(self): return helpers.join_url(self.kind.location, self.id) - def __str__(self): - """Render the entity to text/plain.""" - ret = ["%s" % self.kind] + def headers(self): + """Render the entity to text/occi.""" + h = self.kind.headers() for m in self.mixins: - ret.append("%s" % m) + h.extend(m.headers()) for attr_name in self.attributes: if self.attributes[attr_name].value is not None: - ret.append("%s" % self.attributes[attr_name]) + 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) diff --git a/ooi/tests/occi/test_occi_core.py b/ooi/tests/occi/test_occi_core.py index 09049ca..6c9465d 100644 --- a/ooi/tests/occi/test_occi_core.py +++ b/ooi/tests/occi/test_occi_core.py @@ -48,13 +48,13 @@ def set_val(): def test_as_str(self): attr = attribute.MutableAttribute("occi.foo.bar", "bar") - self.assertEqual('"bar"', attr._as_str()) + self.assertEqual('occi.foo.bar="bar"', attr._as_str()) attr.value = True - self.assertEqual('"true"', attr._as_str()) + self.assertEqual('occi.foo.bar="true"', attr._as_str()) attr.value = False - self.assertEqual('"false"', attr._as_str()) + self.assertEqual('occi.foo.bar="false"', attr._as_str()) attr.value = 4.5 - self.assertEqual("4.5", attr._as_str()) + self.assertEqual("occi.foo.bar=4.5", attr._as_str()) class TestAttributeCollection(base.TestCase): From 08773c9ab1490c3e5c2058a0c3a549aa2901f8c9 Mon Sep 17 00:00:00 2001 From: Pablo Orviz Date: Wed, 4 Mar 2015 09:12:50 +0100 Subject: [PATCH 17/19] Add create() method to Compute controller. --- ooi/api/__init__.py | 107 ++++++++++++++++++++++++++++++++++++++++++++ ooi/api/base.py | 6 ++- ooi/api/compute.py | 44 +++++++++++++++--- ooi/exception.py | 16 +++++++ 4 files changed, 165 insertions(+), 8 deletions(-) diff --git a/ooi/api/__init__.py b/ooi/api/__init__.py index e69de29..61bd632 100644 --- a/ooi/api/__init__.py +++ b/ooi/api/__init__.py @@ -0,0 +1,107 @@ +# -*- 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 collections +import shlex + +import six.moves.urllib.parse as urlparse + +from ooi import exception + + +def _lexize(s, separator, ignore_whitespace=False): + lex = shlex.shlex(instream=s, posix=True) + lex.commenters = "" + if ignore_whitespace: + lex.whitespace = separator + else: + lex.whitespace += separator + lex.whitespace_split = True + return list(lex) + + +def parse(f): + def _parse(obj, req, *args, **kwargs): + headers = {} + try: + l = [] + params = {} + for ctg in _lexize(req.headers["Category"], + separator=',', + ignore_whitespace=True): + ll = _lexize(ctg, ';') + d = {"term": ll[0]} # assumes 1st element => term's value + d.update(dict([i.split('=') for i in ll[1:]])) + l.append(d) + params[urlparse.urlparse(d["scheme"]).path] = d["term"] + headers["Category"] = l + except KeyError: + raise exception.HeaderNotFound(header="Category") + + return f(obj, req, headers, params, *args, **kwargs) + return _parse + + +def _get_header_by_class(headers, class_id): + return [h for h in headers["Category"] if h["class"] in [class_id]] + + +def validate(class_id, schemas, term=None): + def accepts(f): + def _validate(obj, req, headers, params, *args, **kwargs): + """Category headers validation. + + Arguments:: + class_id: type of OCCI class (kind, mixin, ..). + schemas: dict mapping the mandatory schemas with its + occurrences. + term (optional): if present, validates its value. + + Validation checks:: + class_presence: asserts the existance of given class_id. + scheme_occurrences: enforces the number of occurrences of the + given schemas. + term_validation: asserts the correct term value of the + matching headers. + """ + header_l = _get_header_by_class(headers, class_id) + + def class_presence(): + if not header_l: + raise exception.OCCINoClassFound(class_id=class_id) + + def scheme_occurrences(): + d = collections.Counter([h["scheme"] + for h in header_l]) + s = set(d.items()) ^ set(schemas.items()) + if len(s) != 0: + mismatched_schemas = [(scheme, d[scheme]) + for scheme in dict(s).keys()] + raise exception.OCCISchemaOcurrencesMismatch( + mismatched_schemas=mismatched_schemas) + + def term_validation(): + if [h for h in header_l if h["term"] not in [term]]: + raise exception.OCCINotCompliantTerm(term=term) + + class_presence() + scheme_occurrences() + if term: + term_validation() + + return f(obj, req, headers, params, *args, **kwargs) + return _validate + return accepts diff --git a/ooi/api/base.py b/ooi/api/base.py index 1d1aa0e..a1dc69f 100644 --- a/ooi/api/base.py +++ b/ooi/api/base.py @@ -20,8 +20,12 @@ def __init__(self, app, openstack_version): self.app = app self.openstack_version = openstack_version - def _get_req(self, req, path=None): + def _get_req(self, req, path=None, content_type=None, body=None): req.script_name = self.openstack_version if path is not None: req.path_info = path + if content_type is not None: + req.content_type = content_type + if body is not None: + req.body = body return req diff --git a/ooi/api/compute.py b/ooi/api/compute.py index b15adcc..8ad90a7 100644 --- a/ooi/api/compute.py +++ b/ooi/api/compute.py @@ -14,19 +14,18 @@ # License for the specific language governing permissions and limitations # under the License. -from ooi.api import base +import json + +import ooi.api +import ooi.api.base from ooi.occi.core import collection from ooi.occi.infrastructure import compute from ooi.openstack import helpers from ooi.openstack import templates -class Controller(base.Controller): - def index(self, req): - 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) - +class Controller(ooi.api.base.Controller): + def _get_compute_resources(self, response): servers = response.json_body.get("servers", []) occi_compute_resources = [] if servers: @@ -34,6 +33,37 @@ def index(self, req): s = compute.ComputeResource(title=s["name"], id=s["id"]) occi_compute_resources.append(s) + return occi_compute_resources + + def index(self, req): + 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) + occi_compute_resources = self._get_compute_resources(response) + + return collection.Collection(resources=occi_compute_resources) + + @ooi.api.parse + @ooi.api.validate("kind", + {"http://schemas.ogf.org/occi/infrastructure#": 1}, + term="compute") + @ooi.api.validate("mixin", + {"http://schemas.openstack.org/template/resource#": 1, + "http://schemas.openstack.org/template/os#": 1}) + def create(self, req, headers, params, body): + tenant_id = req.environ["keystone.token_auth"].user.project_id + req = self._get_req(req, + path="/%s/servers" % tenant_id, + content_type="application/json", + body=json.dumps({ + "server": { + "name": params["/occi/infrastructure"], + "imageRef": params["/template/os"], + "flavorRef": params["/template/resource"] + }})) + response = req.get_response(self.app) + occi_compute_resources = self._get_compute_resources(response) + return collection.Collection(resources=occi_compute_resources) def show(self, id, req): diff --git a/ooi/exception.py b/ooi/exception.py index c2a79a0..242a012 100644 --- a/ooi/exception.py +++ b/ooi/exception.py @@ -88,3 +88,19 @@ class NotImplemented(OCCIException): class HeaderNotFound(Invalid): msg_fmt = "Header '%(header)s' not found." + + +class HeaderValidation(Invalid): + """Parent class for header validation error exceptions.""" + + +class OCCINoClassFound(HeaderValidation): + msg_fmt = "Found no headers matching class '%(class_id)s'." + + +class OCCISchemaOccurrencesMismatch(HeaderValidation): + msg_fmt = "Schema occurrences do not match: '%(mismatched_schemas)s'." + + +class OCCINotCompliantTerm(HeaderValidation): + msg_fmt = "Found a non-compliant term '%(term)s'." From 459480c31425a810ac7a55af1cafff6d8ee45afa Mon Sep 17 00:00:00 2001 From: Alvaro Lopez Garcia Date: Wed, 18 Mar 2015 13:07:04 +0100 Subject: [PATCH 18/19] Ensure that the Request.body has the correct type Pass the body trough the ooi.wsgi.utils.utf8() function so that we get the correct type. --- ooi/api/base.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ooi/api/base.py b/ooi/api/base.py index a1dc69f..9e7d090 100644 --- a/ooi/api/base.py +++ b/ooi/api/base.py @@ -14,6 +14,8 @@ # License for the specific language governing permissions and limitations # under the License. +from ooi.wsgi import utils + class Controller(object): def __init__(self, app, openstack_version): @@ -27,5 +29,5 @@ def _get_req(self, req, path=None, content_type=None, body=None): if content_type is not None: req.content_type = content_type if body is not None: - req.body = body + req.body = utils.utf8(body) return req From 7d522e2debca337046cffedf45559fffb449105b Mon Sep 17 00:00:00 2001 From: Alvaro Lopez Garcia Date: Wed, 18 Mar 2015 09:37:58 +0100 Subject: [PATCH 19/19] Add some unit tests to compute controller create Just some initial testing, but there is a lot of work needed on the testing of the whole controller. --- ooi/api/compute.py | 13 +++++-- .../middleware/test_compute_controller.py | 37 +++++++++++++++++++ 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/ooi/api/compute.py b/ooi/api/compute.py index 8ad90a7..c5983eb 100644 --- a/ooi/api/compute.py +++ b/ooi/api/compute.py @@ -25,8 +25,7 @@ class Controller(ooi.api.base.Controller): - def _get_compute_resources(self, response): - servers = response.json_body.get("servers", []) + def _get_compute_resources(self, servers): occi_compute_resources = [] if servers: for s in servers: @@ -39,7 +38,8 @@ def index(self, req): 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) - occi_compute_resources = self._get_compute_resources(response) + servers = response.json_body.get("servers", []) + occi_compute_resources = self._get_compute_resources(servers) return collection.Collection(resources=occi_compute_resources) @@ -62,7 +62,12 @@ def create(self, req, headers, params, body): "flavorRef": params["/template/resource"] }})) response = req.get_response(self.app) - occi_compute_resources = self._get_compute_resources(response) + # We only get one server + server = response.json_body.get("server", {}) + + # The returned JSON does not contain the server name + server["name"] = params["/occi/infrastructure"] + occi_compute_resources = self._get_compute_resources([server]) return collection.Collection(resources=occi_compute_resources) diff --git a/ooi/tests/middleware/test_compute_controller.py b/ooi/tests/middleware/test_compute_controller.py index 448a41c..6f5c98a 100644 --- a/ooi/tests/middleware/test_compute_controller.py +++ b/ooi/tests/middleware/test_compute_controller.py @@ -132,6 +132,43 @@ def test_show_vm(self): self.assertExpectedResult(expected, resp) self.assertEqual(200, resp.status_code) + def test_create_vm(self): + tenant = uuid.uuid4().hex + server_id = uuid.uuid4().hex + + s = {"server": {"id": server_id, + "name": "foo", + "flavor": {"id": "1"}, + "image": {"id": "2"}, + "status": "ACTIVE"}} + + fake_resp = {"/%s/servers" % tenant: create_fake_json_resp(s)} + app = self.get_app(resp=fake_resp) + headers = { + 'Category': ( + 'compute;' + 'scheme="http://schemas.ogf.org/occi/infrastructure#";' + 'class="kind",' + 'big;' + 'scheme="http://schemas.openstack.org/template/resource#";' + 'class="mixin",' + 'cirros;' + 'scheme="http://schemas.openstack.org/template/os#";' + 'class="mixin"') + } + req = self._build_req("/compute", method="POST", headers=headers) + + m = mock.MagicMock() + m.user.project_id = tenant + req.environ["keystone.token_auth"] = m + + resp = req.get_response(app) + + expected = [("X-OCCI-Location", "/compute/%s" % server_id)] + self.assertEqual(200, resp.status_code) + self.assertExpectedResult(expected, resp) + self.assertContentType(resp) + class ComputeControllerTextPlain(test_middleware.TestMiddlewareTextPlain, TestComputeController):