diff --git a/ooi/api/compute.py b/ooi/api/compute.py index 01ec546..b7a5c2f 100644 --- a/ooi/api/compute.py +++ b/ooi/api/compute.py @@ -20,6 +20,8 @@ import ooi.api 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 @@ -124,8 +126,13 @@ class Controller(ooi.api.base.Controller): 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] def delete(self, req, id): 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/link.py b/ooi/occi/core/link.py index 005b787..7750684 100644 --- a/ooi/occi/core/link.py +++ b/ooi/occi/core/link.py @@ -33,8 +33,8 @@ class Link(entity.Entity): kind = kind.Kind(helpers.build_scheme("core"), 'link', '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/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/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 114f716..75d588a 100644 --- a/ooi/tests/fakes.py +++ b/ooi/tests/fakes.py @@ -88,6 +88,30 @@ servers = { tenants["bar"]["id"]: [], } +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"]: [], +} + def fake_query_results(): cats = [] @@ -201,20 +225,24 @@ class FakeApp(object): 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_storage_controller.py b/ooi/tests/middleware/test_storage_controller.py new file mode 100644 index 0000000..d8e6c28 --- /dev/null +++ b/ooi/tests/middleware/test_storage_controller.py @@ -0,0 +1,133 @@ +# -*- 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 + + +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?action=backup>; rel=http://schemas.ogf.org/occi/' + 'infrastructure/storage/action#backup' % vol_id) + links.append('<%s?action=resize>; rel=http://schemas.ogf.org/occi/' + 'infrastructure/storage/action#resize' % vol_id) + links.append('<%s?action=snapshot>; rel=http://schemas.ogf.org/occi/' + 'infrastructure/storage/action#snapshot' % vol_id) + links.append('<%s?action=offline>; rel=http://schemas.ogf.org/occi/' + 'infrastructure/storage/action#offline' % vol_id) + links.append('<%s?action=online>; rel=http://schemas.ogf.org/occi/' + 'infrastructure/storage/action#online' % 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) + + self.assertEqual("/%s/os-volumes" % tenant["id"], req.path_info) + + 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("/%s/os-volumes" % tenant["id"], req.path_info) + + self.assertEqual(200, resp.status_code) + expected = [] + for s in fakes.volumes[tenant["id"]]: + expected.append(("X-OCCI-Location", "/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..fe6aa37 100644 --- a/ooi/tests/occi/test_occi_infrastructure.py +++ b/ooi/tests/occi/test_occi_infrastructure.py @@ -19,6 +19,7 @@ import uuid 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 templates from ooi.tests import base @@ -58,7 +59,7 @@ class TestOCCICompute(base.TestCase): 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 +73,7 @@ class TestOCCICompute(base.TestCase): 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 +87,46 @@ class TestOCCICompute(base.TestCase): 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 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 @@ class TestOpenStackResourceTemplate(base.TestCase): 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/wsgi/__init__.py b/ooi/wsgi/__init__.py index 9d3f4e2..f9178c7 100644 --- a/ooi/wsgi/__init__.py +++ b/ooi/wsgi/__init__.py @@ -21,6 +21,7 @@ import webob.dec import ooi.api.compute from ooi.api import query +import ooi.api.storage from ooi import exception from ooi import utils from ooi.wsgi import serializers @@ -123,6 +124,11 @@ class OCCIMiddleware(object): action="delete_all", conditions=dict(method=["DELETE"])) + 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)