From db9dae4a269a17e02ac27310d3a7953b609e4d51 Mon Sep 17 00:00:00 2001 From: Enol Fernandez Date: Wed, 15 Jun 2016 10:56:21 +0200 Subject: [PATCH] Update link handling in parser Change the link structure returned by the parser to better follow the OCCI 1.2 standard. The parser returns a dictionary where keys are the link target kind type and values are lists with dictionaries for each of the links of the links defined for that kind type. Those dictionaries contain the target location (occi.core.target), the link id if defined, and a dictionary with any extra attributes defined. Change-Id: I24205044a6e2b670ceb98f3a0e9224637bf9fcf4 --- ooi/api/compute.py | 49 +++++++-------- ooi/occi/validator.py | 18 +++--- ooi/tests/unit/controllers/test_compute.py | 69 ++++++++++++---------- ooi/tests/unit/test_parser.py | 26 +++++--- ooi/tests/unit/test_validator.py | 4 +- ooi/wsgi/parsers.py | 37 +++++++++--- 6 files changed, 120 insertions(+), 83 deletions(-) diff --git a/ooi/api/compute.py b/ooi/api/compute.py index 136ba33..f0d527f 100644 --- a/ooi/api/compute.py +++ b/ooi/api/compute.py @@ -97,22 +97,23 @@ class Controller(ooi.api.base.Controller): def _build_block_mapping(self, req, obj): mappings = [] - for l in obj.get("links", {}).values(): - if l["rel"] == storage.StorageResource.kind.type_id: - _, vol_id = ooi.api.helpers.get_id_with_kind( - req, - l.get("occi.core.target"), - storage.StorageResource.kind) - mapping = { - "source_type": "volume", - "uuid": vol_id, - "delete_on_termination": False, - } - try: - mapping['device_name'] = l['occi.storagelink.deviceid'] - except KeyError: - pass - mappings.append(mapping) + links = obj.get("links", {}) + for l in links.get(storage.StorageResource.kind.type_id, []): + _, vol_id = ooi.api.helpers.get_id_with_kind( + req, + l.get("target"), + storage.StorageResource.kind) + mapping = { + "source_type": "volume", + "uuid": vol_id, + "delete_on_termination": False, + } + try: + device_name = l['attributes']['occi.storagelink.deviceid'] + mapping['device_name'] = device_name + except KeyError: + pass + mappings.append(mapping) # this needs to be there if we have a mapping if mappings: image = obj["schemes"][templates.OpenStackOSTemplate.scheme][0] @@ -127,14 +128,14 @@ class Controller(ooi.api.base.Controller): def _get_network_from_req(self, req, obj): networks = [] - for l in obj.get("links", {}).values(): - if l["rel"] == network.NetworkResource.kind.type_id: - _, net_id = ooi.api.helpers.get_id_with_kind( - req, - l.get("occi.core.target"), - network.NetworkResource.kind) - net = {'uuid': net_id} - networks.append(net) + links = obj.get("links", {}) + for l in links.get(network.NetworkResource.kind.type_id, []): + _, net_id = ooi.api.helpers.get_id_with_kind( + req, + l.get("target"), + network.NetworkResource.kind) + net = {'uuid': net_id} + networks.append(net) return networks def create(self, req, body): diff --git a/ooi/occi/validator.py b/ooi/occi/validator.py index eddb312..c52d496 100644 --- a/ooi/occi/validator.py +++ b/ooi/occi/validator.py @@ -68,18 +68,14 @@ class Validator(object): return unmatched def _validate_optional_links(self, expected, links): - for uri, l in links.items(): - try: - rel = l['rel'] - except KeyError: - raise exception.OCCIMissingType(type_id=uri) - for ex in expected: - if rel == ex.type_id: - break + expected_types = [e.type_id for e in expected] + for l in links.keys(): + if l in expected_types: + break else: - expected_types = ', '.join([e.type_id for e in expected]) - raise exception.OCCISchemaMismatch(expected=expected_types, - found=l['rel']) + raise exception.OCCISchemaMismatch( + expected=', '.join(expected_types), + found=l) def validate_attributes(self, required): """Validate required attributes diff --git a/ooi/tests/unit/controllers/test_compute.py b/ooi/tests/unit/controllers/test_compute.py index bb4b40a..b242027 100644 --- a/ooi/tests/unit/controllers/test_compute.py +++ b/ooi/tests/unit/controllers/test_compute.py @@ -389,7 +389,7 @@ class TestComputeController(base.TestController): self.assertEqual([], ret) def test_build_block_mapping_invalid_rel(self): - obj = {'links': {'foo': {'rel': 'bar'}}} + obj = {"links": {"foo": [{"target": "bar"}]}} ret = self.controller._build_block_mapping(None, obj) self.assertEqual([], ret) @@ -398,12 +398,13 @@ class TestComputeController(base.TestController): vol_id = uuid.uuid4().hex image_id = uuid.uuid4().hex obj = { - 'links': { - 'l1': { - 'rel': ('http://schemas.ogf.org/occi/infrastructure#' - 'storage'), - 'occi.core.target': vol_id, - } + "links": { + "http://schemas.ogf.org/occi/infrastructure#storage": [ + { + "id": "l1", + "target": vol_id, + } + ] }, "schemes": { templates.OpenStackOSTemplate.scheme: [image_id], @@ -434,13 +435,16 @@ class TestComputeController(base.TestController): vol_id = uuid.uuid4().hex image_id = uuid.uuid4().hex obj = { - 'links': { - 'l1': { - 'rel': ('http://schemas.ogf.org/occi/infrastructure#' - 'storage'), - 'occi.core.target': vol_id, - 'occi.storagelink.deviceid': 'baz' - } + "links": { + "http://schemas.ogf.org/occi/infrastructure#storage": [ + { + "id": "l1", + "target": vol_id, + "attributes": { + "occi.storagelink.deviceid": "baz" + } + } + ] }, "schemes": { templates.OpenStackOSTemplate.scheme: [image_id], @@ -505,12 +509,14 @@ class TestComputeController(base.TestController): def test_get_network_from_req(self, m_get_id): net_id = uuid.uuid4().hex obj = { - 'links': { - 'l1': { - 'rel': '%s%s' % (occi_network.NetworkResource.kind.scheme, - occi_network.NetworkResource.kind.term), - 'occi.core.target': net_id, - } + "links": { + "%s%s" % (occi_network.NetworkResource.kind.scheme, + occi_network.NetworkResource.kind.term): [ + { + "id": "l1", + "target": net_id, + } + ] }, } m_get_id.return_value = (None, net_id) @@ -529,17 +535,18 @@ class TestComputeController(base.TestController): net_id_1 = uuid.uuid4().hex net_id_2 = uuid.uuid4().hex obj = { - 'links': { - 'l1': { - 'rel': '%s%s' % (occi_network.NetworkResource.kind.scheme, - occi_network.NetworkResource.kind.term), - 'occi.core.target': net_id_1, - }, - 'l2': { - 'rel': '%s%s' % (occi_network.NetworkResource.kind.scheme, - occi_network.NetworkResource.kind.term), - 'occi.core.target': net_id_2, - } + "links": { + "%s%s" % (occi_network.NetworkResource.kind.scheme, + occi_network.NetworkResource.kind.term): [ + { + "id": "l1", + "target": net_id_1, + }, + { + "id": "l2", + "target": net_id_2, + } + ] }, } m_get_id.side_effect = [(None, net_id_1), diff --git a/ooi/tests/unit/test_parser.py b/ooi/tests/unit/test_parser.py index 8325d65..a393bff 100644 --- a/ooi/tests/unit/test_parser.py +++ b/ooi/tests/unit/test_parser.py @@ -73,16 +73,27 @@ class BaseParserTest(object): self.assertEqual(expected_attrs, res["attributes"]) def test_link(self): + attrs = {"foo": 1234, "bazonk": "foo=123"} h, b = self.get_test_link( {"term": "foo", "scheme": "http://example.com/scheme#"}, { - "id": "bar", - "attributes": {"foo": 1234, "bazonk": "foo=123"} + "id": "link_id", + "target": "/bar", + "kind": "http://example.com/scheme#link", + "attributes": attrs, } ) parser = self._get_parser(h, b) res = parser.parse() - expected_links = {"bar": {"foo": 1234, "bazonk": "foo=123"}} + expected_links = { + "http://example.com/scheme#link": [ + { + "id": "link_id", + "target": "/bar", + "attributes": attrs, + } + ] + } self.assertEqual(expected_links, res["links"]) @@ -124,7 +135,8 @@ class TestHeaderParser(BaseParserTest, base.TestCase): def get_test_link(self, kind, link): h, b = self.get_test_kind(kind) - l = ["<%(id)s>" % link] + l = [('<%(id)s>; "rel"="%(kind)s"; ' + 'occi.core.target="%(target)s"') % link] for n, v in link["attributes"].items(): l.append('"%s"=%s' % (n, self._get_attribute_value(v))) h["Link"] = "; ".join(l) @@ -207,8 +219,8 @@ class TestJsonParser(BaseParserTest, base.TestCase): attrs = [] for n, v in link["attributes"].items(): attrs.append('"%s": %s' % (n, self._get_attribute_value(v))) - target = '"location": "%s"' % link["id"] - l = ('"links": [{"attributes": { %s }, "target": { %s } }]' - % (",".join(attrs), target)) + target = '"location": "%(target)s", "kind": "%(kind)s"' % link + l = ('"links": [{"attributes": { %s }, "target": { %s }, "id": "%s" }]' + % (",".join(attrs), target, link["id"])) body.append(l) return {}, "{ %s }" % ",".join(body) diff --git a/ooi/tests/unit/test_validator.py b/ooi/tests/unit/test_validator.py index 5b234c1..64f3443 100644 --- a/ooi/tests/unit/test_validator.py +++ b/ooi/tests/unit/test_validator.py @@ -188,7 +188,7 @@ class TestValidator(base.TestCase): def test_optional_links(self): mixins = collections.Counter() schemes = collections.defaultdict(list) - links = {"foo": {"rel": "http://example.com/scheme#foo"}} + links = {"http://example.com/scheme#foo": [{"location": "foo"}]} pobj = { "kind": "compute", "category": "foo type", @@ -205,7 +205,7 @@ class TestValidator(base.TestCase): def test_optional_links_invalid(self): mixins = collections.Counter() schemes = collections.defaultdict(list) - links = {"foo": {"rel": "http://example.com/scheme#foo"}} + links = {"http://example.com/scheme#foo": [{"location": "foo"}]} pobj = { "kind": "compute", "category": "foo type", diff --git a/ooi/wsgi/parsers.py b/ooi/wsgi/parsers.py index be0b177..ed84213 100644 --- a/ooi/wsgi/parsers.py +++ b/ooi/wsgi/parsers.py @@ -148,7 +148,7 @@ class TextParser(BaseParser): return attrs def parse_links(self, headers): - links = {} + links = collections.defaultdict(list) try: header_links = headers["Link"] except KeyError: @@ -158,15 +158,31 @@ class TextParser(BaseParser): # remove the "<" and ">" if ll[0][1] != "<" and ll[0][-1] != ">": raise exception.OCCIInvalidSchema("Unable to parse link") - link_dest = ll[0][1:-1] - d = {} + link_id = ll[0][1:-1] + target_location = None + target_kind = None + attrs = {} try: for attr in ll[1:]: n, v = attr.split("=", 1) - d[n.strip().strip('"')] = self.parse_attribute_value(v) + n = n.strip().strip('"') + v = self.parse_attribute_value(v) + if n == "rel": + target_kind = v + continue + elif n == "occi.core.target": + target_location = v + continue + attrs[n] = v except ValueError: raise exception.OCCIInvalidSchema("Unable to parse link") - links[link_dest] = d + if not (target_kind and target_location): + raise exception.OCCIInvalidSchema("Unable to parse link") + links[target_kind].append({ + "target": target_location, + "attributes": attrs, + "id": link_id, + }) return links def _convert_to_headers(self): @@ -224,11 +240,16 @@ class JsonParser(BaseParser): return {} def parse_links(self, obj): - links = {} + links = collections.defaultdict(list) for l in obj.get("links", []): - attrs = copy.copy(l.get("attributes", {})) try: - links[l["target"]["location"]] = attrs + d = { + "target": l["target"]["location"], + "attributes": copy.copy(l.get("attributes", {})), + } + if "id" in l: + d["id"] = l["id"] + links[l["target"]["kind"]].append(d) except KeyError: raise exception.OCCIInvalidSchema("Unable to parse link") return links