diff --git a/ooi/api/__init__.py b/ooi/api/__init__.py index 3bc60ed..9888593 100644 --- a/ooi/api/__init__.py +++ b/ooi/api/__init__.py @@ -14,94 +14,56 @@ # License for the specific language governing permissions and limitations # under the License. -import collections -import shlex -import six.moves.urllib.parse as urlparse +import copy from ooi import exception +from ooi.occi import helpers -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 compare_schemes(expected_type, actual): + actual_scheme, actual_term = helpers.decompose_type(actual) + if expected_type.scheme != actual_scheme: + return False + try: + if expected_type.term != actual_term: + return False + except AttributeError: + # ignore the fact the type does not have a term + pass + return True -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 validate(schema): 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.OCCISchemaOccurrencesMismatch( - 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) + # TODO(enolfc): proper testing and attribute checking. + def _validate(obj, req, body, *args, **kwargs): + parsed_obj = req.parse() + if "kind" in schema: + try: + if schema["kind"].type_id != parsed_obj["kind"]: + raise exception.OCCISchemaMismatch( + expected=schema["kind"].type_id, + found=parsed_obj["kind"]) + except KeyError: + raise exception.OCCIMissingType( + type_id=schema["kind"].type_id) + unmatched = copy.copy(parsed_obj["mixins"]) + for m in schema.get("mixins", []): + for um in unmatched: + if compare_schemes(m, um): + unmatched[um] -= 1 + break + else: + raise exception.OCCIMissingType(type_id=m.scheme) + for m in schema.get("optional_mixins", []): + for um in unmatched: + if compare_schemes(m, um): + unmatched[um] -= 1 + unexpected = [m for m in unmatched if unmatched[m]] + if unexpected: + raise exception.OCCISchemaMismatch(expected="", + found=unexpected) + return f(obj, parsed_obj, req, body, *args, **kwargs) return _validate return accepts diff --git a/ooi/api/compute.py b/ooi/api/compute.py index 01ec546..a599e9f 100644 --- a/ooi/api/compute.py +++ b/ooi/api/compute.py @@ -20,6 +20,9 @@ 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 contextualization from ooi.openstack import helpers from ooi.openstack import templates @@ -64,30 +67,43 @@ def index(self, req): 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): + @ooi.api.validate({"kind": compute.ComputeResource.kind, + "mixins": [ + templates.OpenStackOSTemplate, + templates.OpenStackResourceTemplate, + ], + "optional_mixins": [ + contextualization.user_data, + contextualization.public_key, + ] + }) + def create(self, obj, req, body): tenant_id = req.environ["keystone.token_auth"].user.project_id + name = obj.get("occi.core.title", "OCCI VM") + image = obj["schemes"][templates.OpenStackOSTemplate.scheme][0] + flavor = obj["schemes"][templates.OpenStackResourceTemplate.scheme][0] + req_body = {"server": { + "name": name, + "imageRef": image, + "flavorRef": flavor, + }} + if contextualization.user_data.scheme in obj["schemes"]: + req_body["user_data"] = obj.get("org.openstack.compute.user_data") 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"] + "name": name, + "imageRef": image, + "flavorRef": flavor, }})) response = req.get_response(self.app) # We only get one server server = self.get_from_response(response, "server", {}) # The returned JSON does not contain the server name - server["name"] = params["/occi/infrastructure"] + server["name"] = name occi_compute_resources = self._get_compute_resources([server]) return collection.Collection(resources=occi_compute_resources) @@ -124,8 +140,13 @@ def show(self, req, id): 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/query.py b/ooi/api/query.py index 58a0aa1..7f01c3c 100644 --- a/ooi/api/query.py +++ b/ooi/api/query.py @@ -19,8 +19,10 @@ 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 contextualization 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) @@ -71,7 +78,7 @@ def index(self, req): l.extend(self._resource_tpls(req)) l.extend(self._os_tpls(req)) - # OpenStack mixins (contextualization) - l.append(mixins.user_data) - l.append(mixins.public_key) + # OpenStack Contextualization + l.append(contextualization.user_data) + l.append(contextualization.public_key) return l 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/exception.py b/ooi/exception.py index 67f775a..549e7c7 100644 --- a/ooi/exception.py +++ b/ooi/exception.py @@ -94,21 +94,14 @@ class NotImplemented(OCCIException): code = 501 -class HeaderNotFound(Invalid): - msg_fmt = "Header '%(header)s' not found." +class OCCIInvalidSchema(Invalid): + msg_fmt = "Found invalid schema: '%(msg)s'." -class HeaderValidation(Invalid): - """Parent class for header validation error exceptions.""" +class OCCIMissingType(Invalid): + msg_fmt = "Missing OCCI types: '%(type_id)s'." -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'." +class OCCISchemaMismatch(Invalid): + msg_fmt = ("Schema does not match. Expecting '%(expected)s', " + "but found '%(found)s'.") 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/helpers.py b/ooi/occi/helpers.py index 34c7a96..fb7dd97 100644 --- a/ooi/occi/helpers.py +++ b/ooi/occi/helpers.py @@ -20,8 +20,8 @@ def build_scheme(category, prefix=_PREFIX): - category = "%s#" % category - return urlparse.urljoin(prefix, category) + scheme = urlparse.urljoin(prefix, category) + return '%s#' % scheme def check_type(obj_list, obj_type): @@ -30,3 +30,8 @@ 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 decompose_type(type_id): + scheme, term = type_id.split('#', 1) + return '%s#' % scheme, term 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 ccf4b35..ae3849c 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 @@ -87,15 +88,38 @@ def render(self, env={}): 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): @@ -114,9 +138,7 @@ def render(self, env={}): 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/mixins.py b/ooi/openstack/contextualization.py similarity index 83% rename from ooi/openstack/mixins.py rename to ooi/openstack/contextualization.py index 403dd8d..23fa114 100644 --- a/ooi/openstack/mixins.py +++ b/ooi/openstack/contextualization.py @@ -20,6 +20,9 @@ class OpenStackUserData(mixin.Mixin): + scheme = helpers.build_scheme("compute/instance") + term = "user_data" + def __init__(self, user_data=None): attrs = [ attribute.InmutableAttribute("org.openstack.compute.user_data", @@ -29,8 +32,8 @@ def __init__(self, user_data=None): attrs = attribute.AttributeCollection({a.name: a for a in attrs}) super(OpenStackUserData, self).__init__( - helpers.build_scheme("compute/instance"), - "user_data", "Contextualization extension - user_data", + OpenStackUserData.scheme, OpenStackUserData.term, + "Contextualization extension - user_data", attributes=attrs) @property @@ -39,6 +42,9 @@ def user_data(self): class OpenStackPublicKey(mixin.Mixin): + scheme = helpers.build_scheme("instance/credentials") + term = "public_key" + def __init__(self, name=None, data=None): attrs = [ attribute.InmutableAttribute( @@ -50,8 +56,8 @@ def __init__(self, name=None, data=None): attrs = attribute.AttributeCollection({a.name: a for a in attrs}) super(OpenStackPublicKey, self).__init__( - helpers.build_scheme("instance/credentials"), - "public_key", "Contextualization extension - public_key", + OpenStackPublicKey.scheme, OpenStackPublicKey.term, + "Contextualization extension - public_key", attributes=attrs) @property 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/openstack/templates.py b/ooi/openstack/templates.py index bc887ab..7ee2b5e 100644 --- a/ooi/openstack/templates.py +++ b/ooi/openstack/templates.py @@ -21,15 +21,18 @@ class OpenStackOSTemplate(mixin.Mixin): + scheme = helpers.build_scheme("template/os") + def __init__(self, uuid, name): super(OpenStackOSTemplate, self).__init__( - helpers.build_scheme("template/os"), + OpenStackOSTemplate.scheme, uuid, name, related=[templates.os_tpl]) class OpenStackResourceTemplate(mixin.Mixin): + scheme = helpers.build_scheme("template/resource") def __init__(self, name, cores, memory, disk, ephemeral=0, swap=0): attrs = [ @@ -43,7 +46,7 @@ def __init__(self, name, cores, memory, disk, ephemeral=0, swap=0): attrs = attribute.AttributeCollection({a.name: a for a in attrs}) super(OpenStackResourceTemplate, self).__init__( - helpers.build_scheme("template/resource"), + OpenStackResourceTemplate.scheme, name, "Flavor: %s" % name, related=[templates.resource_tpl], diff --git a/ooi/tests/fakes.py b/ooi/tests/fakes.py index 114f716..24ddc68 100644 --- a/ooi/tests/fakes.py +++ b/ooi/tests/fakes.py @@ -30,7 +30,9 @@ "foo": {"id": uuid.uuid4().hex, "name": "foo"}, "bar": {"id": uuid.uuid4().hex, - "name": "bar"} + "name": "bar"}, + "baz": {"id": uuid.uuid4().hex, + "name": "baz"}, } flavors = { @@ -61,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"]: [ { @@ -86,74 +120,114 @@ }, ], 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"; ' + 'scheme="http://schemas.ogf.org/occi/infrastructure#"; ' 'class="kind"') cats.append( 'link; ' - 'scheme="http://schemas.ogf.org/occi/core"; ' + 'scheme="http://schemas.ogf.org/occi/core#"; ' 'class="kind"') cats.append( 'resource; ' - 'scheme="http://schemas.ogf.org/occi/core"; ' + 'scheme="http://schemas.ogf.org/occi/core#"; ' 'class="kind"') cats.append( 'entity; ' - 'scheme="http://schemas.ogf.org/occi/core"; ' + '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"; ' + 'scheme="http://schemas.ogf.org/occi/infrastructure/compute/action#"; ' 'class="action"') cats.append( 'stop; ' - 'scheme="http://schemas.ogf.org/occi/infrastructure/compute/action"; ' + 'scheme="http://schemas.ogf.org/occi/infrastructure/compute/action#"; ' 'class="action"') cats.append( 'restart; ' - 'scheme="http://schemas.ogf.org/occi/infrastructure/compute/action"; ' + 'scheme="http://schemas.ogf.org/occi/infrastructure/compute/action#"; ' 'class="action"') cats.append( 'suspend; ' - 'scheme="http://schemas.ogf.org/occi/infrastructure/compute/action"; ' + 'scheme="http://schemas.ogf.org/occi/infrastructure/compute/action#"; ' 'class="action"') cats.append( 'bar; ' - 'scheme="http://schemas.openstack.org/template/os"; ' + 'scheme="http://schemas.openstack.org/template/os#"; ' 'class="mixin"') cats.append( 'bar; ' - 'scheme="http://schemas.openstack.org/template/resource"; ' + 'scheme="http://schemas.openstack.org/template/resource#"; ' 'class="mixin"') cats.append( 'foo; ' - 'scheme="http://schemas.openstack.org/template/os"; ' + 'scheme="http://schemas.openstack.org/template/os#"; ' 'class="mixin"') cats.append( 'foo; ' - 'scheme="http://schemas.openstack.org/template/resource"; ' + 'scheme="http://schemas.openstack.org/template/resource#"; ' 'class="mixin"') cats.append( 'os_tpl; ' - 'scheme="http://schemas.ogf.org/occi/infrastructure"; ' + 'scheme="http://schemas.ogf.org/occi/infrastructure#"; ' 'class="mixin"') cats.append( 'resource_tpl; ' - 'scheme="http://schemas.ogf.org/occi/infrastructure"; ' + 'scheme="http://schemas.ogf.org/occi/infrastructure#"; ' 'class="mixin"') cats.append( 'user_data; ' - 'scheme="http://schemas.openstack.org/compute/instance"; ' + 'scheme="http://schemas.openstack.org/compute/instance#"; ' 'class="mixin"') cats.append( 'public_key; ' - 'scheme="http://schemas.openstack.org/instance/credentials"; ' + 'scheme="http://schemas.openstack.org/instance/credentials#"; ' 'class="mixin"') result = [] @@ -201,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() @@ -225,6 +303,8 @@ def __call__(self, req): return self._do_post(req) def _do_create(self, req): + # TODO(enolfc): this should check the json is + # semantically correct s = {"server": {"id": "foo", "name": "foo", "flavor": {"id": "1"}, diff --git a/ooi/tests/middleware/test_compute_controller.py b/ooi/tests/middleware/test_compute_controller.py index 25eb925..50fbfa1 100644 --- a/ooi/tests/middleware/test_compute_controller.py +++ b/ooi/tests/middleware/test_compute_controller.py @@ -41,13 +41,13 @@ def build_occi_server(server): cats = [] cats.append('compute; ' - 'scheme="http://schemas.ogf.org/occi/infrastructure"; ' + 'scheme="http://schemas.ogf.org/occi/infrastructure#"; ' 'class="kind"'), cats.append('%s; ' - 'scheme="http://schemas.openstack.org/template/os"; ' + 'scheme="http://schemas.openstack.org/template/os#"; ' 'class="mixin"' % image_id), cats.append('%s; ' - 'scheme="http://schemas.openstack.org/template/resource"; ' + 'scheme="http://schemas.openstack.org/template/resource#"; ' 'class="mixin"' % flavor_name), attrs = [ @@ -106,7 +106,7 @@ def test_list_vms_empty(self): self.assertExpectedResult(expected_result, resp) self.assertEqual(204, resp.status_code) - def test_list_vms_one_vm(self): + def test_list_vms(self): tenant = fakes.tenants["foo"] app = self.get_app() @@ -187,6 +187,7 @@ def test_create_vm_incomplete(self): 'scheme="http://schemas.openstack.org/template/os#";' 'class="mixin"') } + req = self._build_req("/compute", tenant["id"], method="POST", headers=headers) resp = req.get_response(app) @@ -194,6 +195,59 @@ def test_create_vm_incomplete(self): self.assertEqual(400, resp.status_code) self.assertContentType(resp) + def test_create_with_context(self): + tenant = fakes.tenants["foo"] + + app = self.get_app() + headers = { + 'Category': ( + 'compute;' + 'scheme="http://schemas.ogf.org/occi/infrastructure#";' + 'class="kind",' + 'foo;' + 'scheme="http://schemas.openstack.org/template/resource#";' + 'class="mixin",' + 'bar;' + 'scheme="http://schemas.openstack.org/template/os#";' + 'class="mixin",' + 'user_data;' + 'scheme="http://schemas.openstack.org/compute/instance#";' + 'class="mixin"' + ), + 'X-OCCI-Attribute': ( + 'org.openstack.compute.user_data="foo"' + ) + } + + req = self._build_req("/compute", tenant["id"], method="POST", + headers=headers) + resp = req.get_response(app) + + 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 62ee2d7..14067da 100644 --- a/ooi/tests/middleware/test_middleware.py +++ b/ooi/tests/middleware/test_middleware.py @@ -53,6 +53,20 @@ def assertExpectedResult(self, expected, result): results = result.text.splitlines() 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: kwargs["accept"] = self.accept @@ -109,3 +123,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..77f2c7d --- /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(204, 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..35fb005 100644 --- a/ooi/tests/occi/test_openstack.py +++ b/ooi/tests/occi/test_openstack.py @@ -17,8 +17,8 @@ import uuid from ooi.occi.infrastructure import templates as occi_templates +from ooi.openstack import contextualization from ooi.openstack import helpers -from ooi.openstack import mixins from ooi.openstack import templates from ooi.tests import base @@ -64,19 +64,22 @@ 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): def test_os_userdata(self): user_data = "foobar" - mxn = mixins.OpenStackUserData(user_data) + mxn = contextualization.OpenStackUserData(user_data) self.assertEqual("user_data", mxn.term) self.assertTrue(mxn.scheme.startswith(helpers._PREFIX)) @@ -88,7 +91,7 @@ def test_os_userdata(self): key_name = "foobar" key_data = "1234" - mxn = mixins.OpenStackPublicKey(key_name, key_data) + mxn = contextualization.OpenStackPublicKey(key_name, key_data) self.assertEqual("public_key", mxn.term) self.assertTrue(mxn.scheme.startswith(helpers._PREFIX)) diff --git a/ooi/wsgi/__init__.py b/ooi/wsgi/__init__.py index 9d3f4e2..bdc9835 100644 --- a/ooi/wsgi/__init__.py +++ b/ooi/wsgi/__init__.py @@ -21,8 +21,10 @@ 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 parsers from ooi.wsgi import serializers LOG = logging.getLogger(__name__) @@ -36,7 +38,7 @@ def get_content_type(self): # FIXME: we should change this, since the content type does not depend # on the serializers, but on the parsers - if self.content_type not in serializers.get_supported_content_types(): + if self.content_type not in parsers.get_supported_content_types(): LOG.debug("Unrecognized Content-Type provided in request") raise exception.InvalidContentType(content_type=self.content_type) @@ -51,6 +53,10 @@ def get_best_match_content_type(self, default_match=None): raise exception.InvalidAccept(content_type=content_type) return content_type + def parse(self): + parser = parsers.HeaderParser() + return parser.parse(self.headers, self.body) + class OCCIMiddleware(object): @classmethod @@ -123,6 +129,11 @@ def index(self, *args, **kwargs): 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) diff --git a/ooi/wsgi/parsers.py b/ooi/wsgi/parsers.py new file mode 100644 index 0000000..71ad703 --- /dev/null +++ b/ooi/wsgi/parsers.py @@ -0,0 +1,110 @@ +# -*- 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 + +from ooi import exception + + +_MEDIA_TYPE_MAP = collections.OrderedDict([ + # ('text/plain', 'text'), + ('text/occi', 'header') +]) + + +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) + + +class BaseParser(object): + def validate(self): + return False + + +class TextParser(BaseParser): + pass + + +class HeaderParser(BaseParser): + def parse_categories(self, headers, body): + kind = None + mixins = collections.Counter() + schemes = collections.defaultdict(list) + try: + categories = headers["Category"] + except KeyError: + raise exception.OCCIInvalidSchema("No categories") + for ctg in _lexize(categories, 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:]])) + ctg_class = d.get("class", None) + ctg_type = '%(scheme)s%(term)s' % d + if ctg_class == "kind": + if kind is not None: + raise exception.OCCIInvalidSchema("Duplicated Kind") + kind = ctg_type + elif ctg_class == "mixin": + mixins[ctg_type] += 1 + schemes[d["scheme"]].append(d["term"]) + return { + "kind": kind, + "mixins": mixins, + "schemes": schemes, + } + + def parse_attributes(self, headers, body): + attrs = {} + try: + header_attrs = headers["X-OCCI-Attribute"] + for attr in _lexize(header_attrs, separator=",", + ignore_whitespace=True): + n, v = attr.split('=', 1) + attrs[n.strip()] = v + except KeyError: + pass + return attrs + + def parse(self, headers, body): + obj = self.parse_categories(headers, body) + obj['attributes'] = self.parse_attributes(headers, body) + return obj + + +_PARSERS_MAP = { + "text": TextParser, + "header": HeaderParser, +} + + +def get_media_map(): + return _MEDIA_TYPE_MAP + + +def get_default_parsers(): + return _PARSERS_MAP + + +def get_supported_content_types(): + return _MEDIA_TYPE_MAP.keys()