Storage controller implementation (index and show).

New occi.infrastructure.storage and associated tests.

Added StorageLinks
This commit is contained in:
Enol Fernandez 2015-03-18 09:11:41 +00:00
parent 65edb4b8c7
commit fcaab700d3
11 changed files with 418 additions and 17 deletions

View File

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

48
ooi/api/storage.py Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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