diff --git a/ooi/api/base.py b/ooi/api/base.py index 25ef4a6..503d30e 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. +import copy + from ooi import utils import webob.exc @@ -25,14 +27,29 @@ def __init__(self, app, openstack_version): self.openstack_version = openstack_version def _get_req(self, req, path=None, content_type=None, body=None): - req.script_name = self.openstack_version + """Return a new Request object to interact with OpenStack. + + This method will create a new request starting with the same WSGI + environment as the original request, prepared to interact with + OpenStack. Namely, it will override the script name to match the + OpenStack version. It will also override the path, content_type and + body of the request, if any of those keyword arguments are passed. + + :param req: the original request + :param path: new path for the request + :param content_type: new content type for the request + :param body: new body for the request + :returns: a Request object + """ + new_req = webob.Request(copy.copy(req.environ)) + new_req.script_name = self.openstack_version if path is not None: - req.path_info = path + new_req.path_info = path if content_type is not None: - req.content_type = content_type + new_req.content_type = content_type if body is not None: - req.body = utils.utf8(body) - return req + new_req.body = utils.utf8(body) + return new_req @staticmethod def get_from_response(response, element, default): diff --git a/ooi/api/compute.py b/ooi/api/compute.py index 75810b7..f901cc0 100644 --- a/ooi/api/compute.py +++ b/ooi/api/compute.py @@ -20,6 +20,8 @@ import ooi.api.base from ooi.occi.core import collection from ooi.occi.infrastructure import compute +from ooi.occi.infrastructure import storage +from ooi.occi.infrastructure import storage_link from ooi.openstack import helpers from ooi.openstack import templates @@ -103,6 +105,11 @@ def show(self, id, req): cores=flavor["vcpus"], hostname=s["name"], memory=flavor["ram"], - state=helpers.occi_state(s["status"]), + state=helpers.vm_state(s["status"]), mixins=[os_tpl, res_tpl]) + # storage links + vols_attached = s.get("os-extended-volumes:volumes_attached", []) + for v in vols_attached: + st = storage.StorageResource(title="storage", id=v["id"]) + comp._links.append(storage_link.StorageLink(comp, st)) return [comp] diff --git a/ooi/api/query.py b/ooi/api/query.py index 58a0aa1..56febdf 100644 --- a/ooi/api/query.py +++ b/ooi/api/query.py @@ -19,6 +19,8 @@ from ooi.occi.core import link from ooi.occi.core import resource from ooi.occi.infrastructure import compute +from ooi.occi.infrastructure import storage +from ooi.occi.infrastructure import storage_link from ooi.occi.infrastructure import templates as infra_templates from ooi.openstack import mixins from ooi.openstack import templates @@ -63,6 +65,11 @@ def index(self, req): l.append(compute.ComputeResource.kind) l.extend(compute.ComputeResource.actions) + # OCCI infra Storage + l.append(storage.StorageResource.kind) + l.append(storage_link.StorageLink.kind) + l.extend(storage.StorageResource.actions) + # OCCI infra mixins l.append(infra_templates.os_tpl) l.append(infra_templates.resource_tpl) diff --git a/ooi/api/storage.py b/ooi/api/storage.py new file mode 100644 index 0000000..34b6d1c --- /dev/null +++ b/ooi/api/storage.py @@ -0,0 +1,48 @@ +# -*- 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.api import base +from ooi.occi.core import collection +from ooi.occi.infrastructure import storage +from ooi.openstack import helpers + + +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/os-volumes" % tenant_id) + response = req.get_response(self.app) + volumes = self.get_from_response(response, "volumes", []) + occi_storage_resources = [] + if volumes: + for v in volumes: + s = storage.StorageResource(title=v["displayName"], id=v["id"]) + occi_storage_resources.append(s) + + return collection.Collection(resources=occi_storage_resources) + + 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/os-volumes/%s" % (tenant_id, id)) + response = req.get_response(self.app) + v = self.get_from_response(response, "volume", {}) + + state = helpers.vol_state(v["status"]) + st = storage.StorageResource(title=v["displayName"], id=v["id"], + size=v["size"], state=state) + return [st] diff --git a/ooi/occi/core/entity.py b/ooi/occi/core/entity.py index 636dc4d..35fb778 100644 --- a/ooi/occi/core/entity.py +++ b/ooi/occi/core/entity.py @@ -62,7 +62,7 @@ class Entity(object): "occi.core.title"]) kind = kind.Kind(helpers.build_scheme('core'), 'entity', - 'entity', attributes, '/entity/') + 'entity', attributes, 'entity/') def __init__(self, title, mixins, id=None): helpers.check_type(mixins, mixin.Mixin) diff --git a/ooi/occi/core/link.py b/ooi/occi/core/link.py index 0ccaa2d..7750684 100644 --- a/ooi/occi/core/link.py +++ b/ooi/occi/core/link.py @@ -31,10 +31,10 @@ class Link(entity.Entity): "occi.core.target"]) kind = kind.Kind(helpers.build_scheme("core"), 'link', 'link', - attributes, '/link/') + attributes, 'link/') - def __init__(self, title, mixins, source, target): - super(Link, self).__init__(title, mixins) + def __init__(self, title, mixins, source, target, id=None): + super(Link, self).__init__(title, mixins, id) self.attributes["occi.core.source"] = attribute.MutableAttribute( "occi.core.source", source) self.attributes["occi.core.target"] = attribute.MutableAttribute( diff --git a/ooi/occi/core/resource.py b/ooi/occi/core/resource.py index cf54dae..7dfcc3c 100644 --- a/ooi/occi/core/resource.py +++ b/ooi/occi/core/resource.py @@ -36,7 +36,7 @@ class Resource(entity.Entity): attributes = attribute.AttributeCollection(["occi.core.summary"]) kind = kind.Kind(helpers.build_scheme('core'), 'resource', - 'resource', attributes, '/resource/', + 'resource', attributes, 'resource/', related=[entity.Entity.kind]) def __init__(self, title, mixins, id=None, summary=None): diff --git a/ooi/occi/infrastructure/compute.py b/ooi/occi/infrastructure/compute.py index 5f078fd..6a93272 100644 --- a/ooi/occi/infrastructure/compute.py +++ b/ooi/occi/infrastructure/compute.py @@ -42,7 +42,7 @@ class ComputeResource(resource.Resource): "occi.compute.state"]) actions = (start, stop, restart, suspend) kind = kind.Kind(helpers.build_scheme('infrastructure'), 'compute', - 'compute resource', attributes, '/compute/', + 'compute resource', attributes, 'compute/', actions=actions, related=[resource.Resource.kind]) diff --git a/ooi/occi/infrastructure/storage.py b/ooi/occi/infrastructure/storage.py new file mode 100644 index 0000000..155060e --- /dev/null +++ b/ooi/occi/infrastructure/storage.py @@ -0,0 +1,67 @@ +# -*- 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.occi.core import action +from ooi.occi.core import attribute as attr +from ooi.occi.core import kind +from ooi.occi.core import resource +from ooi.occi import helpers + +online = action.Action(helpers.build_scheme('infrastructure/storage/action'), + "online", "online storage instance") + +offline = action.Action(helpers.build_scheme('infrastructure/storage/action'), + "offline", "offline storage instance") + +backup = action.Action(helpers.build_scheme('infrastructure/storage/action'), + "backup", "backup storage instance") + +snapshot = action.Action(helpers.build_scheme('infrastructure/storage/action'), + "snapshot", "snapshot storage instance") + +resize = action.Action(helpers.build_scheme('infrastructure/storage/action'), + "resize", "resize storage instance") + + +class StorageResource(resource.Resource): + attributes = attr.AttributeCollection(["occi.storage.size", + "occi.storage.state"]) + actions = (online, offline, backup, snapshot, resize) + kind = kind.Kind(helpers.build_scheme('infrastructure'), 'storage', + 'storage resource', attributes, '/storage/', + actions=actions, + related=[resource.Resource.kind]) + + def __init__(self, title, summary=None, id=None, size=None, state=None): + mixins = [] + super(StorageResource, self).__init__(title, mixins, summary=summary, + id=id) + self.attributes["occi.storage.size"] = attr.MutableAttribute( + "occi.storage.size", size) + self.attributes["occi.storage.state"] = attr.InmutableAttribute( + "occi.storage.state", state) + + @property + def size(self): + return self.attributes["occi.storage.size"].value + + @size.setter + def size(self, value): + self.attributes["occi.storage.size"].value = value + + @property + def state(self): + return self.attributes["occi.storage.state"].value diff --git a/ooi/occi/infrastructure/storage_link.py b/ooi/occi/infrastructure/storage_link.py new file mode 100644 index 0000000..e22d6cc --- /dev/null +++ b/ooi/occi/infrastructure/storage_link.py @@ -0,0 +1,63 @@ +# -*- 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.occi.core import attribute as attr +from ooi.occi.core import kind +from ooi.occi.core import link +from ooi.occi import helpers + + +class StorageLink(link.Link): + attributes = attr.AttributeCollection(["occi.storagelink.deviceid", + "occi.storagelink.mountpoint", + "occi.storagelink.state"]) + kind = kind.Kind(helpers.build_scheme('infrastructure'), 'storagelink', + 'storage link resource', attributes, '/storagelink/', + related=[link.Link.kind]) + + def __init__(self, source, target, deviceid=None, mountpoint=None, + state=None): + + # TODO(enolfc): is this a valid link id? + link_id = '_'.join([source.id, target.id]) + super(StorageLink, self).__init__(None, [], source, target, link_id) + + self.attributes["occi.storagelink.deviceid"] = attr.MutableAttribute( + "occi.storagelink.deviceid", deviceid) + self.attributes["occi.storagelink.mountpoint"] = attr.MutableAttribute( + "occi.storagelink.mountpoint", mountpoint) + self.attributes["occi.storagelink.state"] = attr.InmutableAttribute( + "occi.storagelink.state", state) + + @property + def deviceid(self): + return self.attributes["occi.storagelink.deviceid"].value + + @deviceid.setter + def deviceid(self, value): + self.attributes["occi.storagelink.deviceid"].value = value + + @property + def mountpoint(self): + return self.attributes["occi.storagelink.mountpoint"].value + + @mountpoint.setter + def mountpoint(self, value): + self.attributes["occi.storagelink.mountpoint"].value = value + + @property + def state(self): + return self.attributes["occi.storagelink.state"].value diff --git a/ooi/occi/rendering/headers.py b/ooi/occi/rendering/headers.py index fcded22..8a91542 100644 --- a/ooi/occi/rendering/headers.py +++ b/ooi/occi/rendering/headers.py @@ -19,6 +19,7 @@ from ooi.occi.core import action from ooi.occi.core import collection +from ooi.occi.core import entity from ooi.occi.core import kind from ooi.occi.core import mixin from ooi.occi.core import resource @@ -55,11 +56,12 @@ class KindRenderer(CategoryRenderer): class ActionRenderer(CategoryRenderer): - def render(self, instance=None, env={}): - # We have an instance id, render it as a link - if instance is not None: + def render(self, ass_obj=None, env={}): + # We have an associated object, render it as a link to that object + if ass_obj is not None: url = env.get("application_url", "") - url = utils.join_url(url, [instance, self.obj.location]) + term = ass_obj.kind.term + "/" + url = utils.join_url(url, [term, ass_obj.id, self.obj.location]) d = {"location": url, "rel": self.obj.type_id} link = "<%(location)s>; rel=%(rel)s" % d @@ -80,40 +82,63 @@ def render(self, env={}): 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 + url = utils.join_url(app_url, el.location) ret.append(('X-OCCI-Location', '%s' % url)) return ret class AttributeRenderer(HeaderRenderer): - def render(self, env={}): + def render_attr(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() + elif isinstance(self.obj.value, entity.Entity): + value_str = '"%s"' % self.obj.value.id else: value_str = "%s" % self.obj.value - return [('X-OCCI-Attribute', '%s=%s' % (self.obj.name, value_str))] + return '%s=%s' % (self.obj.name, value_str) + + def render(self, env={}): + return [('X-OCCI-Attribute', self.render_attr(env))] + + +class LinkRenderer(HeaderRenderer): + def render(self, env={}): + ret = [] + url = env.get("application_url", "") + url = utils.join_url(url, self.obj.location) + d = {"location": url, + "scheme": self.obj.target.kind.scheme, + "term": self.obj.target.kind.term, + "self": url} + link = '<%(location)s>; rel="%(scheme)s#%(term)s"; self="%(self)s"' % d + ret.append(link) + for a in self.obj.attributes: + if self.obj.attributes[a].value is None: + continue + ret.append(AttributeRenderer(self.obj.attributes[a]).render_attr()) + return [('Link', '; '.join(ret))] class ResourceRenderer(HeaderRenderer): def render(self, env={}): ret = [] - ret.extend(KindRenderer(self.obj.kind).render()) + ret.extend(KindRenderer(self.obj.kind).render(env=env)) for m in self.obj.mixins: - ret.extend(MixinRenderer(m).render()) + ret.extend(MixinRenderer(m).render(env=env)) 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()) + r = AttributeRenderer(self.obj.attributes[a]) + ret.extend(r.render(env=env)) for a in self.obj.actions: - ret.extend(ActionRenderer(a).render(instance=self.obj.id)) + r = ActionRenderer(a) + ret.extend(r.render(ass_obj=self.obj, env=env)) for l in self.obj.links: - pass - # FIXME(aloga): we need to fix this -# ret.append(LinkRenderer(l)) + ret.extend(LinkRenderer(l).render()) return ret diff --git a/ooi/openstack/helpers.py b/ooi/openstack/helpers.py index ef66baa..1d4b033 100644 --- a/ooi/openstack/helpers.py +++ b/ooi/openstack/helpers.py @@ -24,10 +24,15 @@ def build_scheme(category): # TODO(enolfc): Check the correct names of nova states -def occi_state(nova_status): +def vm_state(nova_status): if nova_status in ["ACTIVE"]: return "active" elif nova_status in ["PAUSED", "SUSPENDED", "STOPPED"]: return "suspended" else: return "inactive" + + +# TODO(enolfc): Do really implement this. +def vol_state(nova_status): + return "online" diff --git a/ooi/tests/fakes.py b/ooi/tests/fakes.py index 2184d5b..19106c5 100644 --- a/ooi/tests/fakes.py +++ b/ooi/tests/fakes.py @@ -24,11 +24,15 @@ import ooi.wsgi +application_url = "https://foo.example.org:8774/ooiv1" + tenants = { "foo": {"id": uuid.uuid4().hex, "name": "foo"}, "bar": {"id": uuid.uuid4().hex, - "name": "bar"} + "name": "bar"}, + "baz": {"id": uuid.uuid4().hex, + "name": "baz"}, } flavors = { @@ -59,6 +63,38 @@ } } +volumes = { + tenants["foo"]["id"]: [ + { + "id": uuid.uuid4().hex, + "displayName": "foo", + "size": 2, + "status": "in-use", + }, + { + "id": uuid.uuid4().hex, + "displayName": "bar", + "size": 3, + "status": "in-use", + }, + { + "id": uuid.uuid4().hex, + "displayName": "baz", + "size": 5, + "status": "in-use", + }, + ], + tenants["bar"]["id"]: [], + tenants["baz"]["id"]: [ + { + "id": uuid.uuid4().hex, + "displayName": "volume", + "size": 5, + "status": "in-use", + }, + ], +} + servers = { tenants["foo"]["id"]: [ { @@ -84,11 +120,31 @@ }, ], tenants["bar"]["id"]: [], + tenants["baz"]["id"]: [ + { + "id": uuid.uuid4().hex, + "name": "withvolume", + "flavor": {"id": flavors[1]["id"]}, + "image": {"id": images["bar"]["id"]}, + "status": "ACTIVE", + "os-extended-volumes:volumes_attached": [ + {"id": volumes[tenants["baz"]["id"]][0]["id"]} + ] + }, + ], } def fake_query_results(): cats = [] + cats.append( + 'storage; ' + 'scheme="http://schemas.ogf.org/occi/infrastructure"; ' + 'class="kind"') + cats.append( + 'storagelink; ' + 'scheme="http://schemas.ogf.org/occi/infrastructure"; ' + 'class="kind"') cats.append( 'compute; ' 'scheme="http://schemas.ogf.org/occi/infrastructure"; ' @@ -105,6 +161,26 @@ def fake_query_results(): 'entity; ' 'scheme="http://schemas.ogf.org/occi/core"; ' 'class="kind"') + cats.append( + 'offline; ' + 'scheme="http://schemas.ogf.org/occi/infrastructure/storage/action"; ' + 'class="action"') + cats.append( + 'online; ' + 'scheme="http://schemas.ogf.org/occi/infrastructure/storage/action"; ' + 'class="action"') + cats.append( + 'backup; ' + 'scheme="http://schemas.ogf.org/occi/infrastructure/storage/action"; ' + 'class="action"') + cats.append( + 'resize; ' + 'scheme="http://schemas.ogf.org/occi/infrastructure/storage/action"; ' + 'class="action"') + cats.append( + 'snapshot; ' + 'scheme="http://schemas.ogf.org/occi/infrastructure/storage/action"; ' + 'class="action"') cats.append( 'start; ' 'scheme="http://schemas.ogf.org/occi/infrastructure/compute/action"; ' @@ -199,20 +275,24 @@ def __init__(self): path = "/%s" % tenant["id"] self._populate(path, "server", servers[tenant["id"]]) + self._populate(path, "volume", volumes[tenant["id"]], "os-volumes") # NOTE(aloga): dict_values un Py3 is not serializable in JSON self._populate(path, "image", list(images.values())) self._populate(path, "flavor", list(flavors.values())) - def _populate(self, path_base, obj_name, obj_list): + def _populate(self, path_base, obj_name, obj_list, objs_path=None): objs_name = "%ss" % obj_name - objs_path = "%s/%s" % (path_base, objs_name) - objs_details_path = "%s/%s/detail" % (path_base, objs_name) - self.routes[objs_path] = create_fake_json_resp({objs_name: obj_list}) + if objs_path: + path = "%s/%s" % (path_base, objs_path) + else: + path = "%s/%s" % (path_base, objs_name) + objs_details_path = "%s/detail" % path + self.routes[path] = create_fake_json_resp({objs_name: obj_list}) self.routes[objs_details_path] = create_fake_json_resp( {objs_name: obj_list}) for o in obj_list: - obj_path = "%s/%s" % (objs_path, o["id"]) + obj_path = "%s/%s" % (path, o["id"]) self.routes[obj_path] = create_fake_json_resp({obj_name: o}) @webob.dec.wsgify() diff --git a/ooi/tests/middleware/test_compute_controller.py b/ooi/tests/middleware/test_compute_controller.py index 95abf44..a954517 100644 --- a/ooi/tests/middleware/test_compute_controller.py +++ b/ooi/tests/middleware/test_compute_controller.py @@ -20,6 +20,7 @@ from ooi.tests import fakes from ooi.tests.middleware import test_middleware +from ooi import utils def build_occi_server(server): @@ -58,14 +59,22 @@ def build_occi_server(server): '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) + links.append('<%s/compute/%s?action=restart>; ' + 'rel=http://schemas.ogf.org/occi/' + 'infrastructure/compute/action#restart' % + (fakes.application_url, server_id)) + links.append('<%s/compute/%s?action=start>; ' + 'rel=http://schemas.ogf.org/occi/' + 'infrastructure/compute/action#start' % + (fakes.application_url, server_id)) + links.append('<%s/compute/%s?action=stop>; ' + 'rel=http://schemas.ogf.org/occi/' + 'infrastructure/compute/action#stop' % + (fakes.application_url, server_id)) + links.append('<%s/compute/%s?action=suspend>; ' + 'rel=http://schemas.ogf.org/occi/' + 'infrastructure/compute/action#suspend' % + (fakes.application_url, server_id)) result = [] for c in cats: @@ -92,14 +101,12 @@ def test_list_vms_empty(self): resp = req.get_response(app) - self.assertEqual("/%s/servers" % tenant["id"], req.path_info) - expected_result = "" self.assertContentType(resp) self.assertExpectedResult(expected_result, resp) self.assertEqual(200, resp.status_code) - def test_list_vms_one_vm(self): + def test_list_vms(self): tenant = fakes.tenants["foo"] app = self.get_app() @@ -107,12 +114,14 @@ def test_list_vms_one_vm(self): resp = req.get_response(app) - self.assertEqual("/%s/servers" % tenant["id"], req.path_info) - self.assertEqual(200, resp.status_code) expected = [] for s in fakes.servers[tenant["id"]]: - expected.append(("X-OCCI-Location", "/compute/%s" % s["id"])) + expected.append( + ("X-OCCI-Location", utils.join_url(self.application_url + "/", + "compute/%s" % s["id"])) + ) + self.assertContentType(resp) self.assertExpectedResult(expected, resp) def test_show_vm(self): @@ -158,11 +167,31 @@ def test_create_vm(self): headers=headers) resp = req.get_response(app) - expected = [("X-OCCI-Location", "/compute/%s" % "foo")] + expected = [("X-OCCI-Location", + utils.join_url(self.application_url + "/", + "compute/%s" % "foo"))] self.assertEqual(200, resp.status_code) self.assertExpectedResult(expected, resp) self.assertContentType(resp) + def test_vm_links(self): + tenant = fakes.tenants["baz"] + + app = self.get_app() + + for server in fakes.servers[tenant["id"]]: + req = self._build_req("/compute/%s" % server["id"], + tenant["id"], method="GET") + + resp = req.get_response(app) + + vol_id = server["os-extended-volumes:volumes_attached"][0]["id"] + link_id = '_'.join([server["id"], vol_id]) + + self.assertContentType(resp) + self.assertResultIncludesLink(link_id, server["id"], vol_id, resp) + self.assertEqual(200, resp.status_code) + class ComputeControllerTextPlain(test_middleware.TestMiddlewareTextPlain, TestComputeController): diff --git a/ooi/tests/middleware/test_middleware.py b/ooi/tests/middleware/test_middleware.py index 7763f8a..f1278db 100644 --- a/ooi/tests/middleware/test_middleware.py +++ b/ooi/tests/middleware/test_middleware.py @@ -35,6 +35,7 @@ def setUp(self): super(TestMiddleware, self).setUp() self.accept = None + self.application_url = fakes.application_url def get_app(self, resp=None): return wsgi.OCCIMiddleware(fakes.FakeApp()) @@ -47,9 +48,21 @@ def assertExpectedResult(self, expected, result): expected = ["%s: %s" % e for e in expected] # NOTE(aloga): the order of the result does not matter results = result.text.splitlines() - results.sort() - expected.sort() - self.assertEqual(expected, results) + self.assertItemsEqual(expected, results) + + def assertResultIncludesLink(self, link_id, source, target, result): + expected_attrs = set([ + 'occi.core.source="%s"' % source, + 'occi.core.target="%s"' % target, + 'occi.core.id="%s"' % link_id, + ]) + for lines in result.text.splitlines(): + r = lines.split(":", 1) + if r[0] == "Link": + attrs = set([s.strip() for s in r[1].split(";")]) + if expected_attrs.issubset(attrs): + return + self.fail("Failed to find %s in %s." % expected_attrs, result) def _build_req(self, path, tenant_id, **kwargs): if self.accept is not None: @@ -59,6 +72,8 @@ def _build_req(self, path, tenant_id, **kwargs): m.user.project_id = tenant_id environ = {"keystone.token_auth": m} + kwargs["base_url"] = self.application_url + return webob.Request.blank(path, environ=environ, **kwargs) def test_404(self): @@ -102,3 +117,15 @@ def assertExpectedResult(self, expected, result): def test_correct_accept(self): self.assertEqual("text/occi", self.accept) + + def assertResultIncludesLink(self, link_id, source, target, result): + expected_attrs = set([ + 'occi.core.source="%s"' % source, + 'occi.core.target="%s"' % target, + 'occi.core.id="%s"' % link_id, + ]) + for val in result.headers.getall("Link"): + attrs = set([s.strip() for s in val.split(";")]) + if expected_attrs.issubset(attrs): + return + self.fail("Failed to find %s in %s." % expected_attrs, result) diff --git a/ooi/tests/middleware/test_storage_controller.py b/ooi/tests/middleware/test_storage_controller.py new file mode 100644 index 0000000..47c7d53 --- /dev/null +++ b/ooi/tests/middleware/test_storage_controller.py @@ -0,0 +1,143 @@ +# -*- 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 uuid + +import mock + +from ooi.tests import fakes +from ooi.tests.middleware import test_middleware +from ooi import utils + + +def build_occi_volume(vol): + name = vol["displayName"] + vol_id = vol["id"] + size = vol["size"] + # TODO(enolfc): use proper status! + status = "online" + + cats = [] + cats.append('storage; ' + 'scheme="http://schemas.ogf.org/occi/infrastructure"; ' + 'class="kind"'), + attrs = [ + 'occi.core.title="%s"' % name, + 'occi.storage.size=%s' % size, + 'occi.storage.state="%s"' % status, + 'occi.core.id="%s"' % vol_id, + ] + links = [] + links.append('<%s/storage/%s?action=backup>; ' + 'rel=http://schemas.ogf.org/occi/' + 'infrastructure/storage/action#backup' % + (fakes.application_url, vol_id)) + links.append('<%s/storage/%s?action=resize>; ' + 'rel=http://schemas.ogf.org/occi/' + 'infrastructure/storage/action#resize' % + (fakes.application_url, vol_id)) + links.append('<%s/storage/%s?action=online>; ' + 'rel=http://schemas.ogf.org/occi/' + 'infrastructure/storage/action#online' % + (fakes.application_url, vol_id)) + links.append('<%s/storage/%s?action=snapshot>; ' + 'rel=http://schemas.ogf.org/occi/' + 'infrastructure/storage/action#snapshot' % + (fakes.application_url, vol_id)) + links.append('<%s/storage/%s?action=offline>; ' + 'rel=http://schemas.ogf.org/occi/' + 'infrastructure/storage/action#offline' % + (fakes.application_url, vol_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 + + +class TestStorageController(test_middleware.TestMiddleware): + """Test OCCI storage controller.""" + + def test_list_vols_empty(self): + tenant = fakes.tenants["bar"] + app = self.get_app() + + req = self._build_req("/storage", tenant["id"], method="GET") + + m = mock.MagicMock() + m.user.project_id = tenant["id"] + req.environ["keystone.token_auth"] = m + + resp = req.get_response(app) + + expected_result = "" + self.assertContentType(resp) + self.assertExpectedResult(expected_result, resp) + self.assertEqual(200, resp.status_code) + + def test_list_vols(self): + tenant = fakes.tenants["foo"] + app = self.get_app() + + req = self._build_req("/storage", tenant["id"], method="GET") + + resp = req.get_response(app) + + self.assertEqual(200, resp.status_code) + expected = [] + for s in fakes.volumes[tenant["id"]]: + expected.append( + ("X-OCCI-Location", utils.join_url(self.application_url + "/", + "storage/%s" % s["id"])) + ) + self.assertExpectedResult(expected, resp) + + def test_show_vol(self): + tenant = fakes.tenants["foo"] + app = self.get_app() + + for volume in fakes.volumes[tenant["id"]]: + req = self._build_req("/storage/%s" % volume["id"], + tenant["id"], method="GET") + + resp = req.get_response(app) + expected = build_occi_volume(volume) + self.assertContentType(resp) + self.assertExpectedResult(expected, resp) + self.assertEqual(200, resp.status_code) + + def test_vol_not_found(self): + tenant = fakes.tenants["foo"] + + app = self.get_app() + req = self._build_req("/storage/%s" % uuid.uuid4().hex, + tenant["id"], method="GET") + resp = req.get_response(app) + self.assertEqual(404, resp.status_code) + + +class StorageControllerTextPlain(test_middleware.TestMiddlewareTextPlain, + TestStorageController): + """Test OCCI compute controller with Accept: text/plain.""" + + +class StorageControllerTextOcci(test_middleware.TestMiddlewareTextOcci, + TestStorageController): + """Test OCCI compute controller with Accept: text/occi.""" diff --git a/ooi/tests/occi/test_occi_infrastructure.py b/ooi/tests/occi/test_occi_infrastructure.py index e937cc9..b8ff4c6 100644 --- a/ooi/tests/occi/test_occi_infrastructure.py +++ b/ooi/tests/occi/test_occi_infrastructure.py @@ -16,9 +16,12 @@ import uuid +from ooi.occi.core import link from ooi.occi.core import mixin from ooi.occi.core import resource from ooi.occi.infrastructure import compute +from ooi.occi.infrastructure import storage +from ooi.occi.infrastructure import storage_link from ooi.occi.infrastructure import templates from ooi.tests import base @@ -58,7 +61,7 @@ def test_compute(self): self.assertIsNone(c.memory) self.assertIsNone(c.speed) - def test_getters(self): + def test_setters(self): c = compute.ComputeResource("foo") c.architecture = "bar" self.assertEqual("bar", @@ -72,7 +75,7 @@ def test_getters(self): c.memory = 4 self.assertEqual(4, c.attributes["occi.compute.memory"].value) - def test_setters(self): + def test_getters(self): c = compute.ComputeResource("foo") c.attributes["occi.compute.architecture"].value = "bar" self.assertEqual("bar", c.architecture) @@ -86,6 +89,103 @@ def test_setters(self): self.assertEqual(9, c.memory) +class TestOCCIStorage(base.TestCase): + def test_storage_class(self): + s = storage.StorageResource + self.assertIn(storage.online, s.actions) + self.assertIn(storage.offline, s.actions) + self.assertIn(storage.backup, s.actions) + self.assertIn(storage.snapshot, s.actions) + self.assertIn(storage.resize, s.actions) + self.assertIn("occi.core.id", s.attributes) + self.assertIn("occi.core.summary", s.attributes) + self.assertIn("occi.core.title", s.attributes) + self.assertIn("occi.storage.size", s.attributes) + self.assertIn("occi.storage.state", s.attributes) + self.assertIn(resource.Resource.kind, s.kind.related) + # TODO(aloga): We need to check that the attributes are actually set + # after we get an object (we have to check this for this but also for + # the other resources) + + def test_storage(self): + id = uuid.uuid4().hex + s = storage.StorageResource("foo", + summary="This is a summary", + id=id) + self.assertEqual("foo", s.title) + self.assertEqual(id, s.id) + self.assertEqual("This is a summary", s.summary) + self.assertIsNone(s.size) + self.assertIsNone(s.state) + + def test_setters(self): + s = storage.StorageResource("foo") + s.size = 3 + self.assertEqual(3, s.attributes["occi.storage.size"].value) + + def test_getters(self): + s = storage.StorageResource("foo", size=5, state="foobar") + self.assertEqual(5, s.size) + self.assertEqual("foobar", s.state) + + +class TestOCCIStorageLink(base.TestCase): + def test_storagelink_class(self): + s = storage_link.StorageLink + self.assertIn("occi.core.id", s.attributes) + self.assertIn("occi.core.title", s.attributes) + self.assertIn("occi.core.source", s.attributes) + self.assertIn("occi.core.target", s.attributes) + self.assertIn("occi.storagelink.mountpoint", s.attributes) + self.assertIn("occi.storagelink.deviceid", s.attributes) + self.assertIn("occi.storagelink.state", s.attributes) + self.assertIn(link.Link.kind, s.kind.related) + + def test_storagelink(self): + server_id = uuid.uuid4().hex + c = compute.ComputeResource("foo", + summary="This is a summary", + id=server_id) + vol_id = uuid.uuid4().hex + s = storage.StorageResource("bar", + summary="This is a summary", + id=vol_id) + l = storage_link.StorageLink(c, s) + link_id = '%s_%s' % (server_id, vol_id) + self.assertEqual(link_id, l.id) + self.assertIsNone(l.deviceid) + self.assertIsNone(l.mountpoint) + self.assertIsNone(l.state) + + def test_setters(self): + c = compute.ComputeResource("foo", + summary="This is a summary", + id=uuid.uuid4().hex) + s = storage.StorageResource("bar", + summary="This is a summary", + id=uuid.uuid4().hex) + l = storage_link.StorageLink(c, s) + l.deviceid = "/dev/vdc" + self.assertEqual("/dev/vdc", + l.attributes["occi.storagelink.deviceid"].value) + l.mountpoint = "/mnt" + self.assertEqual("/mnt", + l.attributes["occi.storagelink.mountpoint"].value) + + def test_getters(self): + c = compute.ComputeResource("foo", + summary="This is a summary", + id=uuid.uuid4().hex) + s = storage.StorageResource("bar", + summary="This is a summary", + id=uuid.uuid4().hex) + l = storage_link.StorageLink(c, s, deviceid="/dev/vdc", + mountpoint="/mnt", state="foobar") + self.assertEqual("/dev/vdc", l.deviceid) + self.assertEqual("/mnt", l.mountpoint) + self.assertEqual("foobar", l.state) + + class TestTemplates(base.TestCase): def test_os_tpl(self): self.assertIsInstance(templates.os_tpl, diff --git a/ooi/tests/occi/test_openstack.py b/ooi/tests/occi/test_openstack.py index f7e902e..9fcad9d 100644 --- a/ooi/tests/occi/test_openstack.py +++ b/ooi/tests/occi/test_openstack.py @@ -64,12 +64,15 @@ def test_resource_template(self): 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")) + def test_vm_state(self): + self.assertEqual("active", helpers.vm_state("ACTIVE")) + self.assertEqual("suspended", helpers.vm_state("PAUSED")) + self.assertEqual("suspended", helpers.vm_state("SUSPENDED")) + self.assertEqual("suspended", helpers.vm_state("STOPPED")) + self.assertEqual("inactive", helpers.vm_state("BUILDING")) + + def test_vol_state(self): + self.assertEqual("online", helpers.vol_state("in-use")) class TestOpenStackUserData(base.TestCase): diff --git a/ooi/tests/test_base_controller.py b/ooi/tests/test_base_controller.py new file mode 100644 index 0000000..9409e00 --- /dev/null +++ b/ooi/tests/test_base_controller.py @@ -0,0 +1,87 @@ +# -*- 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 json + +import webob +import webob.exc + +import ooi.api.base +from ooi.tests import base +from ooi import utils + + +class TestController(base.TestCase): + def test_controller(self): + class Foo(object): + pass + controller = ooi.api.base.Controller(Foo(), "version") + self.assertIsInstance(controller.app, Foo) + self.assertEqual("version", controller.openstack_version) + + def test_new_request(self): + controller = ooi.api.base.Controller(None, "version") + req = webob.Request.blank("foo") + new_req = controller._get_req(req) + self.assertEqual("version", new_req.script_name) + self.assertEqual("foo", new_req.path_info) + self.assertIsNot(req, new_req) + + def test_new_request_with_path(self): + controller = ooi.api.base.Controller(None, "version") + req = webob.Request.blank("foo") + new_req = controller._get_req(req, path="bar") + self.assertEqual("bar", new_req.path_info) + + def test_new_request_with_body(self): + controller = ooi.api.base.Controller(None, "version") + req = webob.Request.blank("foo") + new_req = controller._get_req(req, body="bar") + self.assertEqual(utils.utf8("bar"), new_req.body) + + def test_new_request_with_content_type(self): + controller = ooi.api.base.Controller(None, "version") + req = webob.Request.blank("foo") + new_req = controller._get_req(req, content_type="foo/bar") + self.assertEqual("foo/bar", new_req.content_type) + + def test_get_from_response(self): + d = {"element": {"foo": "bar"}} + body = json.dumps(d) + response = webob.Response(status=200, body=body) + result = ooi.api.base.Controller.get_from_response(response, + "element", + {}) + self.assertEqual(d["element"], result) + + def test_get_from_response_with_default(self): + d = {"element": {"foo": "bar"}} + body = json.dumps({}) + response = webob.Response(status=200, body=body) + result = ooi.api.base.Controller.get_from_response(response, + "element", + d["element"]) + self.assertEqual(d["element"], result) + + def test_get_from_response_with_exception(self): + d = {"unauthorized": {"message": "unauthorized"}} + body = json.dumps(d) + response = webob.Response(status=403, body=body) + self.assertRaises(webob.exc.HTTPForbidden, + ooi.api.base.Controller.get_from_response, + response, + "foo", + {}) diff --git a/ooi/utils.py b/ooi/utils.py index 271a4b6..4ec94eb 100644 --- a/ooi/utils.py +++ b/ooi/utils.py @@ -42,5 +42,8 @@ def join_url(base, parts): parts = [parts] for p in parts: + if p.startswith("/"): + # We won't get an absolute url + p = p[1:] url = urlparse.urljoin(url, p) return url diff --git a/ooi/wsgi/__init__.py b/ooi/wsgi/__init__.py index fba2231..553fe39 100644 --- a/ooi/wsgi/__init__.py +++ b/ooi/wsgi/__init__.py @@ -21,6 +21,7 @@ import ooi.api.compute from ooi.api import query +import ooi.api.storage from ooi import exception from ooi.occi.core import collection from ooi import utils @@ -125,6 +126,11 @@ def index(self, *args, **kwargs): self.mapper.resource("server", "compute", controller=self.resources["compute"]) + self.resources["storage"] = self._create_resource( + ooi.api.storage.Controller) + self.mapper.resource("volume", "storage", + controller=self.resources["storage"]) + @webob.dec.wsgify(RequestClass=Request) def __call__(self, req): response = self.process_request(req) @@ -285,7 +291,8 @@ def serialize(self, request, content_type, default_serializers=None): else: _mtype, _serializer = self.get_serializer(content_type, default_serializers) - serializer = _serializer() + env = {"application_url": request.application_url + "/"} + serializer = _serializer(env) response = webob.Response() response.status_int = self.code @@ -379,7 +386,8 @@ def __call__(self, req): mtype = serializers.get_media_map().get(content_type, "text") serializer = serializers.get_default_serializers()[mtype] - serialized_exc = serializer().serialize(self.wrapped_exc) + env = {} + serialized_exc = serializer(env).serialize(self.wrapped_exc) self.wrapped_exc.body = serialized_exc[1] self.wrapped_exc.content_type = content_type diff --git a/ooi/wsgi/serializers.py b/ooi/wsgi/serializers.py index f6750e2..c4459b1 100644 --- a/ooi/wsgi/serializers.py +++ b/ooi/wsgi/serializers.py @@ -27,7 +27,12 @@ ]) -class TextSerializer(object): +class BaseSerializer(object): + def __init__(self, env): + self.env = env + + +class TextSerializer(BaseSerializer): def serialize(self, data): if not isinstance(data, list): data = [data] @@ -36,11 +41,11 @@ def serialize(self, data): for d in data: renderers.append(text_rendering.get_renderer(d)) - ret = "\n".join([r.render() for r in renderers]) + ret = "\n".join([r.render(env=self.env) for r in renderers]) return None, utils.utf8(ret) -class HeaderSerializer(object): +class HeaderSerializer(BaseSerializer): def serialize(self, data): if not isinstance(data, list): data = [data] @@ -51,7 +56,7 @@ def serialize(self, data): # 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()] + headers = [i for r in renderers for i in r.render(env=self.env)] return headers, ""