From ec543fdb25854c78758e800c037c9bdb85a920ab Mon Sep 17 00:00:00 2001 From: Enol Fernandez Date: Wed, 15 Apr 2015 20:31:25 +0000 Subject: [PATCH 1/4] New network stuff. Added OCCI infrastructure network resource, mixins and links. New network controller and add links in compute. Some tests added. --- ooi/api/compute.py | 30 ++- ooi/api/network.py | 74 ++++++++ ooi/api/query.py | 11 +- ooi/occi/infrastructure/network.py | 78 ++++++++ ooi/occi/infrastructure/network_link.py | 69 +++++++ ooi/openstack/network.py | 38 ++++ ooi/tests/fakes.py | 172 +++++++++++++----- .../middleware/test_compute_controller.py | 34 +++- .../middleware/test_network_controller.py | 153 ++++++++++++++++ ooi/wsgi/__init__.py | 25 +++ 10 files changed, 632 insertions(+), 52 deletions(-) create mode 100644 ooi/api/network.py create mode 100644 ooi/occi/infrastructure/network.py create mode 100644 ooi/occi/infrastructure/network_link.py create mode 100644 ooi/openstack/network.py create mode 100644 ooi/tests/middleware/test_network_controller.py diff --git a/ooi/api/compute.py b/ooi/api/compute.py index 783a9dd..fce5829 100644 --- a/ooi/api/compute.py +++ b/ooi/api/compute.py @@ -17,17 +17,34 @@ import json import ooi.api.base +import ooi.api.network as network_api from ooi import exception from ooi.occi.core import collection from ooi.occi.infrastructure import compute +from ooi.occi.infrastructure import network from ooi.occi.infrastructure import storage from ooi.occi.infrastructure import storage_link from ooi.occi import validator as occi_validator from ooi.openstack import contextualization from ooi.openstack import helpers +from ooi.openstack import network as os_network from ooi.openstack import templates +def _create_network_link(addr, comp, floating_ips): + if addr["OS-EXT-IPS:type"] == "floating": + for ip in floating_ips: + if addr["addr"] == ip["ip"]: + net = network.NetworkResource( + title="network", + id="%s/%s" % (network_api.FLOATING_PREFIX, ip["pool"])) + else: + net = network.NetworkResource(title="network", id="fixed") + return os_network.OSNetworkInterface(comp, net, + addr["OS-EXT-IPS-MAC:mac_addr"], + addr["addr"]) + + class Controller(ooi.api.base.Controller): def __init__(self, *args, **kwargs): super(Controller, self).__init__(*args, **kwargs) @@ -180,7 +197,6 @@ def show(self, req, id): image["name"]) # build the compute object - # TODO(enolfc): link to network + storage comp = compute.ComputeResource(title=s["name"], id=s["id"], cores=flavor["vcpus"], hostname=s["name"], @@ -197,6 +213,18 @@ def show(self, req, id): st = storage.StorageResource(title="storage", id=v["volumeId"]) comp.add_link(storage_link.StorageLink(comp, st, deviceid=v["device"])) + + # network links + addresses = s.get("addresses", {}) + if addresses: + req = self._get_req(req, path="/%s/os-floating-ips" % tenant_id) + response = req.get_response(self.app) + floating_ips = self.get_from_response(response, "floating_ips", []) + for addr_type in addresses.values(): + for addr in addr_type: + comp.add_link(_create_network_link(addr, comp, + floating_ips)) + return [comp] def delete(self, req, id): diff --git a/ooi/api/network.py b/ooi/api/network.py new file mode 100644 index 0000000..e02bb2b --- /dev/null +++ b/ooi/api/network.py @@ -0,0 +1,74 @@ +# -*- 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 webob.exc + +from ooi.api import base +from ooi.occi.core import collection +from ooi.occi.infrastructure import network + +FLOATING_PREFIX = "floating" + + +def _build_network(name, prefix=None): + if prefix: + network_id = '/'.join([prefix, name]) + else: + network_id = name + return network.NetworkResource(title=name, + id=network_id, + state="active", + mixins=[network.ip_network]) + + +class NetworkController(base.Controller): + def _floating_index(self, req): + tenant_id = req.environ["keystone.token_auth"].user.project_id + + req = self._get_req(req, path="/%s/os-floating-ip-pools" % tenant_id) + response = req.get_response(self.app) + pools = self.get_from_response(response, "floating_ip_pools", []) + + occi_network_resources = [] + for p in pools: + occi_network_resources.append(_build_network(p["name"], + FLOATING_PREFIX)) + return occi_network_resources + + def general_index(self, req): + occi_network_resources = self._floating_index(req) + occi_network_resources.append(_build_network("fixed")) + return collection.Collection(resources=occi_network_resources) + + def index(self, req): + occi_network_resources = self._floating_index(req) + return collection.Collection(resources=occi_network_resources) + + def show_fixed(self, req): + return _build_network("fixed") + + def show(self, req, id): + tenant_id = req.environ["keystone.token_auth"].user.project_id + + # get info from server + req = self._get_req(req, path="/%s/os-floating-ip-pools" % tenant_id) + response = req.get_response(self.app) + + pools = self.get_from_response(response, "floating_ip_pools", []) + for p in pools: + if p['name'] == id: + return [_build_network(p["name"], FLOATING_PREFIX)] + raise webob.exc.HTTPNotFound() diff --git a/ooi/api/query.py b/ooi/api/query.py index 7f01c3c..277fd90 100644 --- a/ooi/api/query.py +++ b/ooi/api/query.py @@ -19,6 +19,8 @@ from ooi.occi.core import link from ooi.occi.core import resource from ooi.occi.infrastructure import compute +from ooi.occi.infrastructure import network +from ooi.occi.infrastructure import network_link from ooi.occi.infrastructure import storage from ooi.occi.infrastructure import storage_link from ooi.occi.infrastructure import templates as infra_templates @@ -70,7 +72,14 @@ def index(self, req): l.append(storage_link.StorageLink.kind) l.extend(storage.StorageResource.actions) - # OCCI infra mixins + # OCCI infra network + l.append(network.NetworkResource.kind) + l.extend(network.NetworkResource.actions) + l.append(network.ip_network) + l.append(network_link.NetworkInterface.kind) + l.append(network_link.ip_network_interface) + + # OCCI infra compute mixins l.append(infra_templates.os_tpl) l.append(infra_templates.resource_tpl) diff --git a/ooi/occi/infrastructure/network.py b/ooi/occi/infrastructure/network.py new file mode 100644 index 0000000..775c044 --- /dev/null +++ b/ooi/occi/infrastructure/network.py @@ -0,0 +1,78 @@ +# -*- 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 mixin +from ooi.occi.core import resource +from ooi.occi import helpers + +up = action.Action(helpers.build_scheme('infrastructure/network/action'), + "up", "up network instance") + +down = action.Action(helpers.build_scheme('infrastructure/network/action'), + "down", "down network instance") + + +class NetworkResource(resource.Resource): + attributes = attr.AttributeCollection(["occi.network.vlan", + "occi.network.label", + "occi.network.state"]) + actions = (up, down) + kind = kind.Kind(helpers.build_scheme('infrastructure'), 'network', + 'network resource', attributes, '/network/', + actions=actions, + related=[resource.Resource.kind]) + + def __init__(self, title, summary=None, id=None, vlan=None, label=None, + state=None, mixins=[]): + super(NetworkResource, self).__init__(title, mixins, summary=summary, + id=id) + self.attributes["occi.network.vlan"] = attr.MutableAttribute( + "occi.network.vlan", vlan) + self.attributes["occi.network.label"] = attr.MutableAttribute( + "occi.network.label", label) + self.attributes["occi.network.state"] = attr.InmutableAttribute( + "occi.network.state", state) + + @property + def vlan(self): + return self.attributes["occi.network.vlan"].value + + @vlan.setter + def vlan(self, value): + self.attributes["occi.network.vlan"].value = value + + @property + def label(self): + return self.attributes["occi.network.label"].value + + @label.setter + def label(self, value): + self.attributes["occi.network.label"].value = value + + @property + def state(self): + return self.attributes["occi.network.state"].value + + +ip_network = mixin.Mixin(helpers.build_scheme("infrastructure/network"), + "ipnetwork", "IP Networking Mixin", + attributes=attr.AttributeCollection([ + "occi.network.address", + "occi.network.gateway", + "occi.network.allocation"])) diff --git a/ooi/occi/infrastructure/network_link.py b/ooi/occi/infrastructure/network_link.py new file mode 100644 index 0000000..b2c71a8 --- /dev/null +++ b/ooi/occi/infrastructure/network_link.py @@ -0,0 +1,69 @@ +# -*- 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.core import mixin +from ooi.occi import helpers + + +class NetworkInterface(link.Link): + attributes = attr.AttributeCollection(["occi.networkinterface.interface", + "occi.networkinterface.mac", + "occi.networkinterface.state"]) + kind = kind.Kind(helpers.build_scheme('infrastructure'), + 'networkinterface', 'network link resource', + attributes, '/networklink/', + related=[link.Link.kind]) + + def __init__(self, mixins, source, target, id=None, interface=None, + mac=None, state=None): + + super(NetworkInterface, self).__init__(None, mixins, source, + target, id) + + self.attributes["occi.networkinterface.interface"] = ( + attr.InmutableAttribute("occi.networkinterface.interface", + interface)) + self.attributes["occi.networkinterface.mac"] = attr.MutableAttribute( + "occi.networkinterface.mac", mac) + self.attributes["occi.networkinterface.state"] = ( + attr.InmutableAttribute("occi.networkinterface.state", state)) + + @property + def interface(self): + return self.attributes["occi.networkinterface.interface"].value + + @property + def mac(self): + return self.attributes["occi.networkinterface.mac"].value + + @mac.setter + def mac(self, value): + self.attributes["occi.networkinterface.mac"].value = value + + @property + def state(self): + return self.attributes["occi.networkinterface.state"].value + +ip_network_interface = mixin.Mixin( + helpers.build_scheme("infrastructure/networkinterface"), + "ipnetworkinterface", "IP Network interface Mixin", + attributes=attr.AttributeCollection([ + "occi.networkinterface.address", + "occi.networkinterface.gateway", + "occi.networkinterface.allocation"])) diff --git a/ooi/openstack/network.py b/ooi/openstack/network.py new file mode 100644 index 0000000..52aef5a --- /dev/null +++ b/ooi/openstack/network.py @@ -0,0 +1,38 @@ +# -*- 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.infrastructure import network_link + + +class OSNetworkInterface(network_link.NetworkInterface): + attributes = attr.AttributeCollection(["occi.networkinterface.address", + "occi.networkinterface.gateway", + "occi.networkinterface.allocation"]) + + def __init__(self, source, target, mac, address): + link_id = '_'.join([source.id, address]) + mixins = [network_link.ip_network_interface] + super(OSNetworkInterface, self).__init__(mixins, source, target, + link_id, "eth0", mac, + "active") + self.attributes["occi.networkinterface.address"] = ( + attr.MutableAttribute("occi.networkinterface.address", address)) + self.attributes["occi.networkinterface.gateway"] = ( + attr.MutableAttribute("occi.networkinterface.gateway", None)) + self.attributes["occi.networkinterface.allocation"] = ( + attr.MutableAttribute("occi.networkinterface.allocation", + "dynamic")) diff --git a/ooi/tests/fakes.py b/ooi/tests/fakes.py index b4f0a20..bd9163d 100644 --- a/ooi/tests/fakes.py +++ b/ooi/tests/fakes.py @@ -98,6 +98,49 @@ ], } +pools = { + tenants["foo"]["id"]: [ + { + "id": "foo", + "name": "foo", + }, + { + "id": "bar", + "name": "bar", + } + ], + tenants["bar"]["id"]: [], + tenants["baz"]["id"]: [ + { + "id": "public", + "name": "public", + }, + ], +} + +linked_vm_id = uuid.uuid4().hex + +floating_ips = { + tenants["foo"]["id"]: [], + tenants["bar"]["id"]: [], + tenants["baz"]["id"]: [ + { + "fixed_ip": "10.0.0.2", + "id": uuid.uuid4().hex, + "instance_id": linked_vm_id, + "ip": "192.168.253.1", + "pool": pools[tenants["baz"]["id"]][0]["name"], + }, + { + "fixed_ip": None, + "id": uuid.uuid4().hex, + "instance_id": None, + "ip": "192.168.253.2", + "pool": pools[tenants["baz"]["id"]][0]["name"], + }, + ], +} + servers = { tenants["foo"]["id"]: [ { @@ -125,15 +168,25 @@ tenants["bar"]["id"]: [], tenants["baz"]["id"]: [ { - "id": uuid.uuid4().hex, + "id": linked_vm_id, "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"]} - ] - }, + ], + "addresses": { + "private": [ + {"addr": floating_ips[tenants["baz"]["id"]][0]["fixed_ip"], + "OS-EXT-IPS:type": "fixed", + "OS-EXT-IPS-MAC:mac_addr": "1234"}, + {"addr": floating_ips[tenants["baz"]["id"]][0]["ip"], + "OS-EXT-IPS:type": "floating", + "OS-EXT-IPS-MAC:mac_addr": "1234"}, + ] + } + } ], } @@ -154,18 +207,7 @@ 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#"; ' - 'class="kind"') + # OCCI Core cats.append( 'link; ' 'scheme="http://schemas.ogf.org/occi/core#"; ' @@ -178,26 +220,11 @@ def fake_query_results(): 'entity; ' 'scheme="http://schemas.ogf.org/occi/core#"; ' 'class="kind"') + # OCCI Infrastructure Compute 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"') + 'compute; ' + 'scheme="http://schemas.ogf.org/occi/infrastructure#"; ' + 'class="kind"') cats.append( 'start; ' 'scheme="http://schemas.ogf.org/occi/infrastructure/compute/action#"; ' @@ -214,6 +241,15 @@ def fake_query_results(): 'suspend; ' 'scheme="http://schemas.ogf.org/occi/infrastructure/compute/action#"; ' 'class="action"') + cats.append( + 'os_tpl; ' + 'scheme="http://schemas.ogf.org/occi/infrastructure#"; ' + 'class="mixin"') + cats.append( + 'resource_tpl; ' + 'scheme="http://schemas.ogf.org/occi/infrastructure#"; ' + 'class="mixin"') + # OpenStack Templates cats.append( 'bar; ' 'scheme="http://schemas.openstack.org/template/os#"; ' @@ -230,14 +266,7 @@ def fake_query_results(): 'foo; ' 'scheme="http://schemas.openstack.org/template/resource#"; ' 'class="mixin"') - cats.append( - 'os_tpl; ' - 'scheme="http://schemas.ogf.org/occi/infrastructure#"; ' - 'class="mixin"') - cats.append( - 'resource_tpl; ' - 'scheme="http://schemas.ogf.org/occi/infrastructure#"; ' - 'class="mixin"') + # OpenStack contextualization cats.append( 'user_data; ' 'scheme="http://schemas.openstack.org/compute/instance#"; ' @@ -246,7 +275,60 @@ def fake_query_results(): 'public_key; ' 'scheme="http://schemas.openstack.org/instance/credentials#"; ' 'class="mixin"') - + # OCCI Infrastructure Network + cats.append( + 'network; ' + 'scheme="http://schemas.ogf.org/occi/infrastructure#"; ' + 'class="kind"') + cats.append( + 'ipnetwork; ' + 'scheme="http://schemas.ogf.org/occi/infrastructure/network#"; ' + 'class="mixin"') + cats.append( + 'up; ' + 'scheme="http://schemas.ogf.org/occi/infrastructure/network/action#"; ' + 'class="action"') + cats.append( + 'down; ' + 'scheme="http://schemas.ogf.org/occi/infrastructure/network/action#"; ' + 'class="action"') + cats.append( + 'networkinterface; ' + 'scheme="http://schemas.ogf.org/occi/infrastructure#"; ' + 'class="kind"') + cats.append( + 'ipnetworkinterface; ' + 'scheme="http://schemas.ogf.org/occi/infrastructure/' + 'networkinterface#"; class="mixin"') + # OCCI Infrastructure Storage + 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( + '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"') result = [] for c in cats: result.append(("Category", c)) @@ -293,6 +375,10 @@ def __init__(self): self._populate(path, "server", servers[tenant["id"]], actions=True) self._populate(path, "volume", volumes[tenant["id"]], "os-volumes") + self._populate(path, "floating_ip_pool", pools[tenant["id"]], + "os-floating-ip-pools") + self._populate(path, "floating_ip", floating_ips[tenant["id"]], + "os-floating-ips") # 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())) diff --git a/ooi/tests/middleware/test_compute_controller.py b/ooi/tests/middleware/test_compute_controller.py index e938682..73c66cb 100644 --- a/ooi/tests/middleware/test_compute_controller.py +++ b/ooi/tests/middleware/test_compute_controller.py @@ -299,17 +299,37 @@ def test_vm_links(self): resp = req.get_response(app) - vol_id = server["os-extended-volumes:volumes_attached"][0]["id"] - link_id = '_'.join([server["id"], vol_id]) - self.assertDefaults(resp) self.assertContentType(resp) + self.assertEqual(200, resp.status_code) + source = utils.join_url(self.application_url + "/", "compute/%s" % server["id"]) - target = utils.join_url(self.application_url + "/", - "storage/%s" % vol_id) - self.assertResultIncludesLink(link_id, source, target, resp) - self.assertEqual(200, resp.status_code) + # volumes + vols = server.get("os-extended-volumes:volumes_attached", []) + for v in vols: + vol_id = v["id"] + link_id = '_'.join([server["id"], vol_id]) + + target = utils.join_url(self.application_url + "/", + "storage/%s" % vol_id) + self.assertResultIncludesLink(link_id, source, target, resp) + + # network + addresses = server.get("addresses", {}) + for addr_set in addresses.values(): + for addr in addr_set: + ip = addr["addr"] + link_id = '_'.join([server["id"], ip]) + if addr["OS-EXT-IPS:type"] == "fixed": + net_id = "fixed" + else: + name = fakes.pools[tenant["id"]][0]["name"] + net_id = "floating/%s" % name + target = utils.join_url(self.application_url + "/", + "network/%s" % net_id) + self.assertResultIncludesLink(link_id, source, target, + resp) class ComputeControllerTextPlain(test_middleware.TestMiddlewareTextPlain, diff --git a/ooi/tests/middleware/test_network_controller.py b/ooi/tests/middleware/test_network_controller.py new file mode 100644 index 0000000..12a4be8 --- /dev/null +++ b/ooi/tests/middleware/test_network_controller.py @@ -0,0 +1,153 @@ +# -*- 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_network(pool, floating=True): + cats = [] + cats.append('network; ' + 'scheme="http://schemas.ogf.org/occi/infrastructure#"; ' + 'class="kind"') + cats.append('ipnetwork; ' + 'scheme="http://schemas.ogf.org/occi/infrastructure/' + 'network#"; class="mixin"') + + if floating: + pool_id = "floating/%s" % pool["name"] + else: + pool_id = pool["name"] + attrs = [ + 'occi.core.title="%s"' % pool["name"], + 'occi.network.state="active"', + 'occi.core.id="%s"' % pool_id, + ] + links = [] + links.append('<%s/network/%s?action=up>; ' + 'rel=http://schemas.ogf.org/occi/' + 'infrastructure/network/action#up' % + (fakes.application_url, pool_id)) + links.append('<%s/network/%s?action=down>; ' + 'rel=http://schemas.ogf.org/occi/' + 'infrastructure/network/action#down' % + (fakes.application_url, pool_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 TestNetworkController(test_middleware.TestMiddleware): + """Test OCCI network controller.""" + + def test_list_pools_empty(self): + tenant = fakes.tenants["bar"] + app = self.get_app() + + for url in ("/network", "/network/"): + req = self._build_req(url, 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 = [ + ("X-OCCI-Location", + utils.join_url(self.application_url + "/", "network/fixed")) + ] + self.assertDefaults(resp) + self.assertExpectedResult(expected, resp) + self.assertEqual(200, resp.status_code) + + def test_list_pools(self): + tenant = fakes.tenants["foo"] + app = self.get_app() + + for url in ("/network", "/network/"): + req = self._build_req(url, tenant["id"], method="GET") + + resp = req.get_response(app) + + self.assertEqual(200, resp.status_code) + expected = [ + ("X-OCCI-Location", + utils.join_url(self.application_url + "/", "network/fixed")) + ] + for s in fakes.pools[tenant["id"]]: + expected.append( + ("X-OCCI-Location", + utils.join_url(self.application_url + "/", + "network/floating/%s" % s["name"])) + ) + self.assertDefaults(resp) + self.assertExpectedResult(expected, resp) + + def test_show_floating_pool(self): + tenant = fakes.tenants["foo"] + app = self.get_app() + + for pool in fakes.pools[tenant["id"]]: + req = self._build_req("/network/floating/%s" % pool["name"], + tenant["id"], method="GET") + + resp = req.get_response(app) + expected = build_occi_network(pool) + self.assertDefaults(resp) + self.assertExpectedResult(expected, resp) + self.assertEqual(200, resp.status_code) + + def test_show_fixed(self): + tenant = fakes.tenants["foo"] + app = self.get_app() + + req = self._build_req("/network/fixed", tenant["id"], method="GET") + + resp = req.get_response(app) + expected = build_occi_network({"name": "fixed"}, False) + self.assertDefaults(resp) + self.assertExpectedResult(expected, resp) + self.assertEqual(200, resp.status_code) + + def test_pool_not_found(self): + tenant = fakes.tenants["foo"] + + app = self.get_app() + req = self._build_req("/network/floating/%s" % uuid.uuid4().hex, + tenant["id"], method="GET") + resp = req.get_response(app) + self.assertEqual(404, resp.status_code) + + +class NetworkControllerTextPlain(test_middleware.TestMiddlewareTextPlain, + TestNetworkController): + """Test OCCI network controller with Accept: text/plain.""" + + +class NetworkControllerTextOcci(test_middleware.TestMiddlewareTextOcci, + TestNetworkController): + """Test OCCI network controller with Accept: text/occi.""" diff --git a/ooi/wsgi/__init__.py b/ooi/wsgi/__init__.py index 7728196..3d832af 100644 --- a/ooi/wsgi/__init__.py +++ b/ooi/wsgi/__init__.py @@ -23,6 +23,7 @@ import ooi import ooi.api.compute +import ooi.api.network from ooi.api import query import ooi.api.storage import ooi.api.storage_link @@ -179,6 +180,30 @@ def index(self, *args, **kwargs): action="index", conditions=dict(method=["GET"])) + # Network is a bit different from other resources + # we have /network and below that /network/fixed + # and /network/floating/* for the pools + self.resources["network"] = self._create_resource( + ooi.api.network.NetworkController) + self.mapper.connect("network", "/network", + controller=self.resources["network"], + action="general_index", + conditions=dict(method=["GET"])) + # OCCI states that paths must end with a "/" when operating on pahts, + # that are not location pahts or resource instances, so we should add + # this rule manually + self.mapper.connect("network", "/network/", + controller=self.resources["network"], + action="general_index", + conditions=dict(method=["GET"])) + self.mapper.connect("fixed_network", "/network/fixed", + controller=self.resources["network"], + action="show_fixed", + conditions=dict(method=["GET"])) + netpool_name = "network/%s" % ooi.api.network.FLOATING_PREFIX + self.mapper.resource("floating_network", netpool_name, + controller=self.resources["network"]) + @webob.dec.wsgify(RequestClass=Request) def __call__(self, req): response = self.process_request(req) From c098e325c2795736796066b78fcf00577ad4e2e5 Mon Sep 17 00:00:00 2001 From: Enol Fernandez Date: Fri, 17 Apr 2015 13:27:10 +0000 Subject: [PATCH 2/4] Network links. Adds a new controller for network links, supporting listing, show, create and delete. Includes some tests also. --- ooi/api/compute.py | 4 +- ooi/api/network_link.py | 155 +++++++++++ ooi/api/storage.py | 2 + ooi/api/storage_link.py | 1 + ooi/openstack/network.py | 27 +- ooi/tests/fakes.py | 22 +- .../middleware/test_compute_controller.py | 1 - ooi/tests/middleware/test_middleware.py | 2 +- .../test_netinterface_controller.py | 250 ++++++++++++++++++ .../middleware/test_storagelink_controller.py | 3 +- ooi/wsgi/__init__.py | 13 + 11 files changed, 472 insertions(+), 8 deletions(-) create mode 100644 ooi/api/network_link.py create mode 100644 ooi/tests/middleware/test_netinterface_controller.py diff --git a/ooi/api/compute.py b/ooi/api/compute.py index fce5829..8a9e9c4 100644 --- a/ooi/api/compute.py +++ b/ooi/api/compute.py @@ -220,8 +220,8 @@ def show(self, req, id): req = self._get_req(req, path="/%s/os-floating-ips" % tenant_id) response = req.get_response(self.app) floating_ips = self.get_from_response(response, "floating_ips", []) - for addr_type in addresses.values(): - for addr in addr_type: + for addr_set in addresses.values(): + for addr in addr_set: comp.add_link(_create_network_link(addr, comp, floating_ips)) diff --git a/ooi/api/network_link.py b/ooi/api/network_link.py new file mode 100644 index 0000000..b78acf3 --- /dev/null +++ b/ooi/api/network_link.py @@ -0,0 +1,155 @@ +# -*- coding: utf-8 -*- + +# Copyright 2015 Spanish National Research Council +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import json + +import webob.exc + +from ooi.api import base +from ooi.api import network as network_api +from ooi import exception +from ooi.occi.core import collection +from ooi.occi.infrastructure import compute +from ooi.occi.infrastructure import network +from ooi.occi.infrastructure import network_link +from ooi.occi import validator as occi_validator +from ooi.openstack import network as os_network + + +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-floating-ips" % tenant_id) + response = req.get_response(self.app) + floating_ips = self.get_from_response(response, "floating_ips", []) + occi_link_resources = [] + for ip in floating_ips: + if ip["instance_id"]: + net_id = "%s/%s" % (network_api.FLOATING_PREFIX, ip["pool"]) + n = network.NetworkResource(title="network", id=net_id) + c = compute.ComputeResource(title="Compute", + id=ip["instance_id"]) + # TODO(enolfc): get the MAC? + iface = os_network.OSNetworkInterface(c, n, "mac", ip["ip"]) + occi_link_resources.append(iface) + + return collection.Collection(resources=occi_link_resources) + + def _get_os_network_ip(self, req, addr): + if addr["OS-EXT-IPS:type"] == "fixed": + return network.NetworkResource(title="network", id="fixed"), None + else: + tenant_id = req.environ["keystone.token_auth"].user.project_id + req = self._get_req(req, path="/%s/os-floating-ips" % tenant_id) + response = req.get_response(self.app) + floating_ips = self.get_from_response(response, "floating_ips", []) + for ip in floating_ips: + if addr["addr"] == ip["ip"]: + net = network.NetworkResource( + title="network", + id="%s/%s" % (network_api.FLOATING_PREFIX, ip["pool"])) + return net, ip["id"] + raise webob.exc.HTTPNotFound() + + def _get_interface_from_id(self, req, id): + tenant_id = req.environ["keystone.token_auth"].user.project_id + try: + server_id, server_addr = id.split('_', 1) + except ValueError: + raise webob.exc.HTTPNotFound() + path = "/%s/servers/%s" % (tenant_id, server_id) + req = self._get_req(req, path=path, method="GET") + response = req.get_response(self.app) + s = self.get_from_response(response, "server", {}) + addresses = s.get("addresses", {}) + for addr_set in addresses.values(): + for addr in addr_set: + if addr["addr"] == server_addr: + n, ip_id = self._get_os_network_ip(req, addr) + c = compute.ComputeResource(title="Compute", + id=server_id) + # TODO(enolfc): get the MAC? + return os_network.OSNetworkInterface(c, n, "mac", + addr["addr"], ip_id) + raise webob.exc.HTTPNotFound() + + def show(self, req, id): + return [self._get_interface_from_id(req, id)] + + def create(self, req, body): + tenant_id = req.environ["keystone.token_auth"].user.project_id + parser = req.get_parser()(req.headers, req.body) + scheme = {"category": network_link.NetworkInterface.kind} + obj = parser.parse() + validator = occi_validator.Validator(obj) + validator.validate(scheme) + + attrs = obj.get("attributes", {}) + server_id = attrs.get("occi.core.source") + net_id = attrs.get("occi.core.target") + + # net_id is something like "fixed" or "floating/" + if net_id == "fixed": + raise exception.Invalid() + try: + _, pool_name = net_id.split("/", 1) + except ValueError: + raise webob.exc.HTTPNotFound() + + # Allocate IP + path = "/%s/os-floating-ips" % tenant_id + req = self._get_req(req, path="/%s/os-floating-ips" % tenant_id, + body=json.dumps({"pool": pool_name}), + method="POST") + response = req.get_response(self.app) + ip = self.get_from_response(response, "floating_ip", {}) + + # Add it to server + req_body = {"addFloatingIp": {"address": ip["ip"]}} + path = "/%s/servers/%s/action" % (tenant_id, server_id) + req = self._get_req(req, path=path, body=json.dumps(req_body), + method="POST") + response = req.get_response(self.app) + if response.status_int != 202: + raise base.exception_from_response(response) + n = network.NetworkResource(title="network", id=net_id) + c = compute.ComputeResource(title="Compute", id=server_id) + l = os_network.OSNetworkInterface(c, n, "mac", ip["ip"]) + return collection.Collection(resources=[l]) + + def delete(self, req, id): + iface = self._get_interface_from_id(req, id) + if iface.target.id == "fixed": + raise exception.Invalid() + + # remove floating IP + tenant_id = req.environ["keystone.token_auth"].user.project_id + req_body = {"removeFloatingIp": {"address": iface.address}} + path = "/%s/servers/%s/action" % (tenant_id, iface.source.id) + req = self._get_req(req, path=path, body=json.dumps(req_body), + method="POST") + response = req.get_response(self.app) + if response.status_int != 202: + raise base.exception_from_response(response) + + # release IP + path = "/%s/os-floating-ips/%s" % (tenant_id, iface.ip_id) + req = self._get_req(req, path=path, body=json.dumps(req_body), + method="DELETE") + response = req.get_response(self.app) + if response.status_int != 202: + raise base.exception_from_response(response) + return [] diff --git a/ooi/api/storage.py b/ooi/api/storage.py index 34b6d1c..0ea936d 100644 --- a/ooi/api/storage.py +++ b/ooi/api/storage.py @@ -46,3 +46,5 @@ def show(self, id, req): st = storage.StorageResource(title=v["displayName"], id=v["id"], size=v["size"], state=state) return [st] + + # TODO(enolfc): delete, create diff --git a/ooi/api/storage_link.py b/ooi/api/storage_link.py index 4eea994..25cc8bc 100644 --- a/ooi/api/storage_link.py +++ b/ooi/api/storage_link.py @@ -109,3 +109,4 @@ def delete(self, req, id): response = req.get_response(self.app) if response.status_int not in [202]: raise base.exception_from_response(response) + return [] diff --git a/ooi/openstack/network.py b/ooi/openstack/network.py index 52aef5a..f6bff33 100644 --- a/ooi/openstack/network.py +++ b/ooi/openstack/network.py @@ -23,12 +23,13 @@ class OSNetworkInterface(network_link.NetworkInterface): "occi.networkinterface.gateway", "occi.networkinterface.allocation"]) - def __init__(self, source, target, mac, address): + def __init__(self, source, target, mac, address, ip_id=None): link_id = '_'.join([source.id, address]) mixins = [network_link.ip_network_interface] super(OSNetworkInterface, self).__init__(mixins, source, target, link_id, "eth0", mac, "active") + self.ip_id = ip_id self.attributes["occi.networkinterface.address"] = ( attr.MutableAttribute("occi.networkinterface.address", address)) self.attributes["occi.networkinterface.gateway"] = ( @@ -36,3 +37,27 @@ def __init__(self, source, target, mac, address): self.attributes["occi.networkinterface.allocation"] = ( attr.MutableAttribute("occi.networkinterface.allocation", "dynamic")) + + @property + def address(self): + return self.attributes["occi.networkinterface.address"].value + + @address.setter + def address(self, value): + self.attributes["occi.networkinterface.address"].value = value + + @property + def gateway(self): + return self.attributes["occi.networkinterface.gateway"].value + + @gateway.setter + def gateway(self, value): + self.attributes["occi.networkinterface.gateway"].value = value + + @property + def allocation(self): + return self.attributes["occi.networkinterface.allocation"].value + + @allocation.setter + def allocation(self, value): + self.attributes["occi.networkinterface.allocation"].value = value diff --git a/ooi/tests/fakes.py b/ooi/tests/fakes.py index bd9163d..81a1f0b 100644 --- a/ooi/tests/fakes.py +++ b/ooi/tests/fakes.py @@ -120,6 +120,8 @@ linked_vm_id = uuid.uuid4().hex +allocated_ip = "192.168.253.23" + floating_ips = { tenants["foo"]["id"]: [], tenants["bar"]["id"]: [], @@ -447,22 +449,40 @@ def _do_create_attachment(self, req): "device": "/dev/vdb"}} return create_fake_json_resp(v, 202) + def _do_allocate_ip(self, req): + body = req.json_body.copy() + pool = body.popitem() + tenant = req.path_info.split('/')[1] + for p in pools[tenant]: + if p["name"] == pool[1]: + break + else: + exc = webob.exc.HTTPNotFound() + return FakeOpenStackFault(exc) + ip = {"floating_ip": {"ip": allocated_ip}} + return create_fake_json_resp(ip, 202) + def _do_post(self, req): if req.path_info.endswith("servers"): return self._do_create_server(req) elif req.path_info.endswith("action"): body = req.json_body.copy() action = body.popitem() - if action[0] in ["os-start", "os-stop", "reboot"]: + if action[0] in ["os-start", "os-stop", "reboot", + "addFloatingIp", "removeFloatingIp"]: return self._get_from_routes(req) elif req.path_info.endswith("os-volume_attachments"): return self._do_create_attachment(req) + elif req.path_info.endswith("os-floating-ips"): + return self._do_allocate_ip(req) raise Exception def _do_delete(self, req): self._do_get(req) if "os-volume_attachments" in req.path_info: return create_fake_json_resp({}, 202) + if "os-floating-ips" in req.path_info: + return create_fake_json_resp({}, 202) raise Exception def _do_get(self, req): diff --git a/ooi/tests/middleware/test_compute_controller.py b/ooi/tests/middleware/test_compute_controller.py index 73c66cb..8a6b6c8 100644 --- a/ooi/tests/middleware/test_compute_controller.py +++ b/ooi/tests/middleware/test_compute_controller.py @@ -113,7 +113,6 @@ def test_list_vms(self): for url in ("/compute/", "/compute"): req = self._build_req(url, tenant["id"], method="GET") - resp = req.get_response(app) self.assertEqual(200, resp.status_code) diff --git a/ooi/tests/middleware/test_middleware.py b/ooi/tests/middleware/test_middleware.py index 358fae8..e0fe4cf 100644 --- a/ooi/tests/middleware/test_middleware.py +++ b/ooi/tests/middleware/test_middleware.py @@ -168,4 +168,4 @@ def assertResultIncludesLink(self, link_id, source, target, result): 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) + self.fail("Failed to find %s in %s." % (expected_attrs, result)) diff --git a/ooi/tests/middleware/test_netinterface_controller.py b/ooi/tests/middleware/test_netinterface_controller.py new file mode 100644 index 0000000..ff8d7ce --- /dev/null +++ b/ooi/tests/middleware/test_netinterface_controller.py @@ -0,0 +1,250 @@ +# -*- 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 copy +import uuid + +import mock + +from ooi.tests import fakes +from ooi.tests.middleware import test_middleware +from ooi import utils + + +class TestNetInterfaceController(test_middleware.TestMiddleware): + """Test OCCI network interface controller.""" + def test_list_ifaces_empty(self): + tenant = fakes.tenants["bar"] + app = self.get_app() + + for url in ("/networklink/", "/networklink"): + req = self._build_req(url, 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_ifaces(self): + tenant = fakes.tenants["baz"] + app = self.get_app() + + for url in ("/networklink/", "/networklink"): + req = self._build_req(url, tenant["id"], method="GET") + + resp = req.get_response(app) + + self.assertEqual(200, resp.status_code) + expected = [] + for ip in fakes.floating_ips[tenant["id"]]: + if ip["instance_id"] is not None: + link_id = '_'.join([ip["instance_id"], ip["ip"]]) + expected.append( + ("X-OCCI-Location", + utils.join_url(self.application_url + "/", + "networklink/%s" % link_id)) + ) + self.assertExpectedResult(expected, resp) + + def test_show_iface(self): + tenant = fakes.tenants["baz"] + app = self.get_app() + + for ip in fakes.floating_ips[tenant["id"]]: + if ip["instance_id"] is not None: + link_id = '_'.join([ip["instance_id"], ip["ip"]]) + req = self._build_req("/networklink/%s" % link_id, + tenant["id"], method="GET") + + resp = req.get_response(app) + self.assertContentType(resp) + source = utils.join_url(self.application_url + "/", + "compute/%s" % ip["instance_id"]) + target = utils.join_url(self.application_url + "/", + "network/floating/%s" % ip["pool"]) + self.assertResultIncludesLink(link_id, source, target, resp) + self.assertEqual(200, resp.status_code) + + def test_show_invalid_id(self): + tenant = fakes.tenants["foo"] + + app = self.get_app() + req = self._build_req("/networklink/%s" % uuid.uuid4().hex, + tenant["id"], method="GET") + resp = req.get_response(app) + self.assertEqual(404, resp.status_code) + + def test_show_non_existant_compute(self): + tenant = fakes.tenants["foo"] + + app = self.get_app() + req = self._build_req("/networklink/%s_foo" % uuid.uuid4().hex, + tenant["id"], method="GET") + resp = req.get_response(app) + self.assertEqual(404, resp.status_code) + + def test_show_non_existant_volume(self): + tenant = fakes.tenants["foo"] + server_id = fakes.servers[tenant["id"]][0]["id"] + + app = self.get_app() + req = self._build_req("/networklink/%s_foo" % server_id, + tenant["id"], method="GET") + resp = req.get_response(app) + self.assertEqual(404, resp.status_code) + + def test_create_link_with_fixed(self): + tenant = fakes.tenants["foo"] + server_id = fakes.servers[tenant["id"]][0]["id"] + net_id = "fixed" + + app = self.get_app() + headers = { + 'Category': ( + 'networkinterface;' + 'scheme="http://schemas.ogf.org/occi/infrastructure#";' + 'class="kind"'), + 'X-OCCI-Attribute': ( + 'occi.core.source="%s", ' + 'occi.core.target="%s"' + ) % (server_id, net_id) + } + req = self._build_req("/networklink", tenant["id"], method="POST", + headers=headers) + resp = req.get_response(app) + + self.assertEqual(400, resp.status_code) + + def test_create_link_with_invalid_net(self): + tenant = fakes.tenants["foo"] + server_id = fakes.servers[tenant["id"]][0]["id"] + net_id = "notexistant" + + app = self.get_app() + headers = { + 'Category': ( + 'networkinterface;' + 'scheme="http://schemas.ogf.org/occi/infrastructure#";' + 'class="kind"'), + 'X-OCCI-Attribute': ( + 'occi.core.source="%s", ' + 'occi.core.target="%s"' + ) % (server_id, net_id) + } + req = self._build_req("/networklink", tenant["id"], method="POST", + headers=headers) + resp = req.get_response(app) + self.assertEqual(404, resp.status_code) + + def test_create_link_with_unexistant_net(self): + tenant = fakes.tenants["foo"] + server_id = fakes.servers[tenant["id"]][0]["id"] + net_id = "floating/nothere" + + app = self.get_app() + headers = { + 'Category': ( + 'networkinterface;' + 'scheme="http://schemas.ogf.org/occi/infrastructure#";' + 'class="kind"'), + 'X-OCCI-Attribute': ( + 'occi.core.source="%s", ' + 'occi.core.target="%s"' + ) % (server_id, net_id) + } + req = self._build_req("/networklink", tenant["id"], method="POST", + headers=headers) + resp = req.get_response(app) + self.assertEqual(404, resp.status_code) + + def test_create_link(self): + tenant = fakes.tenants["foo"] + + server_id = fakes.servers[tenant["id"]][0]["id"] + net_id = "floating/" + fakes.pools[tenant["id"]][0]["id"] + + app = self.get_app() + headers = { + 'Category': ( + 'networkinterface;' + 'scheme="http://schemas.ogf.org/occi/infrastructure#";' + 'class="kind"'), + 'X-OCCI-Attribute': ( + 'occi.core.source="%s", ' + 'occi.core.target="%s"' + ) % (server_id, net_id) + } + req = self._build_req("/networklink", tenant["id"], method="POST", + headers=headers) + resp = req.get_response(app) + + link_id = '_'.join([server_id, fakes.allocated_ip]) + expected = [("X-OCCI-Location", + utils.join_url(self.application_url + "/", + "networklink/%s" % link_id))] + self.assertEqual(200, resp.status_code) + self.assertExpectedResult(expected, resp) + self.assertDefaults(resp) + + def test_delete_fixed(self): + tenant = fakes.tenants["baz"] + app = self.get_app() + + for s in fakes.servers[tenant["id"]]: + addresses = copy.copy(s.get("addresses", {})) + while addresses: + addr_set = addresses.popitem() + for addr in addr_set[1]: + if addr["OS-EXT-IPS:type"] == "fixed": + link_id = '_'.join([s["id"], addr["addr"]]) + req = self._build_req("/networklink/%s" % link_id, + tenant["id"], method="DELETE") + resp = req.get_response(app) + self.assertContentType(resp) + self.assertEqual(400, resp.status_code) + + def test_delete_link(self): + tenant = fakes.tenants["baz"] + app = self.get_app() + + for s in fakes.servers[tenant["id"]]: + addresses = copy.copy(s.get("addresses", {})) + while addresses: + addr_set = addresses.popitem() + for addr in addr_set[1]: + if addr["OS-EXT-IPS:type"] == "floating": + link_id = '_'.join([s["id"], addr["addr"]]) + req = self._build_req("/networklink/%s" % link_id, + tenant["id"], method="DELETE") + resp = req.get_response(app) + self.assertContentType(resp) + self.assertEqual(204, resp.status_code) + + +class NetInterfaceControllerTextPlain(test_middleware.TestMiddlewareTextPlain, + TestNetInterfaceController): + """Test OCCI network link controller with Accept: text/plain.""" + + +class NetInterfaceControllerTextOcci(test_middleware.TestMiddlewareTextOcci, + TestNetInterfaceController): + """Test OCCI network link controller with Accept: text/occi.""" diff --git a/ooi/tests/middleware/test_storagelink_controller.py b/ooi/tests/middleware/test_storagelink_controller.py index b1d4327..73c45e2 100644 --- a/ooi/tests/middleware/test_storagelink_controller.py +++ b/ooi/tests/middleware/test_storagelink_controller.py @@ -200,8 +200,7 @@ def test_delete_link(self): tenant["id"], method="DELETE") resp = req.get_response(app) self.assertContentType(resp) - # TODO(enolfc): should this be 204? - self.assertEqual(200, resp.status_code) + self.assertEqual(204, resp.status_code) class StorageLinkControllerTextPlain(test_middleware.TestMiddlewareTextPlain, diff --git a/ooi/wsgi/__init__.py b/ooi/wsgi/__init__.py index 3d832af..f65c951 100644 --- a/ooi/wsgi/__init__.py +++ b/ooi/wsgi/__init__.py @@ -24,6 +24,7 @@ import ooi import ooi.api.compute import ooi.api.network +import ooi.api.network_link from ooi.api import query import ooi.api.storage import ooi.api.storage_link @@ -204,6 +205,18 @@ def index(self, *args, **kwargs): self.mapper.resource("floating_network", netpool_name, controller=self.resources["network"]) + self.resources["networklink"] = self._create_resource( + ooi.api.network_link.Controller) + self.mapper.resource("networkinterface", "networklink", + controller=self.resources["networklink"]) + # OCCI states that paths must end with a "/" when operating on pahts, + # that are not location pahts or resource instances, so we should add + # this rule manually + self.mapper.connect("networkinterface", "/networklink/", + controller=self.resources["networklink"], + action="index", + conditions=dict(method=["GET"])) + @webob.dec.wsgify(RequestClass=Request) def __call__(self, req): response = self.process_request(req) From 507c2ef511bb500c69a4787057032ba0a842d3e5 Mon Sep 17 00:00:00 2001 From: Enol Fernandez Date: Thu, 30 Apr 2015 19:44:01 +0000 Subject: [PATCH 3/4] Manual creation of the routes to resources. This avoids URL ending in .* to be splitted in id, format Needed for the network link ids which contain IP addresses. --- ooi/wsgi/__init__.py | 85 ++++++++++++++++++++------------------------ 1 file changed, 38 insertions(+), 47 deletions(-) diff --git a/ooi/wsgi/__init__.py b/ooi/wsgi/__init__.py index f65c951..619e983 100644 --- a/ooi/wsgi/__init__.py +++ b/ooi/wsgi/__init__.py @@ -94,6 +94,33 @@ def __init__(self, application, openstack_version="/v2.1"): def _create_resource(self, controller): return Resource(controller(self.application, self.openstack_version)) + def _setup_resource_routes(self, resource, controller): + path = "/" + resource + # This could be removed for total OCCI compliance + self.mapper.connect(resource, path, controller=controller, + action="index", conditions=dict(method=["GET"])) + # OCCI states that paths must end with a "/" when operating on pahts, + # that are not location pahts or resource instances + self.mapper.connect(resource, path + "/", controller=controller, + action="index", conditions=dict(method=["GET"])) + self.mapper.connect(resource, path, controller=controller, + action="create", conditions=dict(method=["POST"])) + self.mapper.connect(resource, path + "/{id}", controller=controller, + action="update", conditions=dict(method=["PUT"])) + self.mapper.connect(resource, path + "/{id}", controller=controller, + action="delete", + conditions=dict(method=["DELETE"])) + self.mapper.connect(resource, path + "/{id}", controller=controller, + action="show", conditions=dict(method=["GET"])) + # OCCI specific, delete all resources + self.mapper.connect(path + "/", controller=controller, + action="delete_all", + conditions=dict(method=["DELETE"])) + # Actions + self.mapper.connect(path + "/{id}", controller=controller, + action="run_action", + conditions=dict(method=["POST"])) + def _setup_routes(self): """Setup the mapper routes. @@ -140,47 +167,23 @@ def index(self, *args, **kwargs): self.resources["compute"] = self._create_resource( ooi.api.compute.Controller) - self.mapper.resource("server", "compute", - controller=self.resources["compute"]) - # OCCI states that paths must end with a "/" when operating on pahts, - # that are not location pahts or resource instances, so we should add - # this rule manually - self.mapper.connect("compute", "/compute/", - controller=self.resources["compute"], - action="index", - conditions=dict(method=["GET"])) - self.mapper.connect("compute", "/compute/", - controller=self.resources["compute"], - action="delete_all", - conditions=dict(method=["DELETE"])) - self.mapper.connect("/compute/{id}", - controller=self.resources["compute"], - action="run_action", - conditions=dict(method=["POST"])) + self._setup_resource_routes("compute", self.resources["compute"]) self.resources["storage"] = self._create_resource( ooi.api.storage.Controller) - self.mapper.resource("volume", "storage", - controller=self.resources["storage"]) - # OCCI states that paths must end with a "/" when operating on pahts, - # that are not location pahts or resource instances, so we should add - # this rule manually - self.mapper.connect("storage", "/storage/", - controller=self.resources["storage"], - action="index", - conditions=dict(method=["GET"])) + self._setup_resource_routes("storage", self.resources["storage"]) + self.resources["storagelink"] = self._create_resource( ooi.api.storage_link.Controller) - self.mapper.resource("volume", "storagelink", - controller=self.resources["storagelink"]) - # OCCI states that paths must end with a "/" when operating on pahts, - # that are not location pahts or resource instances, so we should add - # this rule manually - self.mapper.connect("storagelink", "/storagelink/", - controller=self.resources["storagelink"], - action="index", - conditions=dict(method=["GET"])) + self._setup_resource_routes("storagelink", + self.resources["storagelink"]) + self.resources["networklink"] = self._create_resource( + ooi.api.network_link.Controller) + self._setup_resource_routes("networklink", + self.resources["networklink"]) + + # TODO(enolfc): move to _setup_resource_routes or similar # Network is a bit different from other resources # we have /network and below that /network/fixed # and /network/floating/* for the pools @@ -205,18 +208,6 @@ def index(self, *args, **kwargs): self.mapper.resource("floating_network", netpool_name, controller=self.resources["network"]) - self.resources["networklink"] = self._create_resource( - ooi.api.network_link.Controller) - self.mapper.resource("networkinterface", "networklink", - controller=self.resources["networklink"]) - # OCCI states that paths must end with a "/" when operating on pahts, - # that are not location pahts or resource instances, so we should add - # this rule manually - self.mapper.connect("networkinterface", "/networklink/", - controller=self.resources["networklink"], - action="index", - conditions=dict(method=["GET"])) - @webob.dec.wsgify(RequestClass=Request) def __call__(self, req): response = self.process_request(req) From 9e8e2df46d3267325e2b6d53834288ad4186999e Mon Sep 17 00:00:00 2001 From: Enol Fernandez Date: Mon, 4 May 2015 06:51:41 +0000 Subject: [PATCH 4/4] Improve coverage of unit tests. --- ooi/tests/occi/test_occi_infrastructure.py | 120 +++++++++++++++++++++ ooi/tests/occi/test_openstack.py | 64 +++++++++++ 2 files changed, 184 insertions(+) diff --git a/ooi/tests/occi/test_occi_infrastructure.py b/ooi/tests/occi/test_occi_infrastructure.py index b8ff4c6..4e757d3 100644 --- a/ooi/tests/occi/test_occi_infrastructure.py +++ b/ooi/tests/occi/test_occi_infrastructure.py @@ -20,6 +20,8 @@ from ooi.occi.core import mixin from ooi.occi.core import resource from ooi.occi.infrastructure import compute +from ooi.occi.infrastructure import network +from ooi.occi.infrastructure import network_link from ooi.occi.infrastructure import storage from ooi.occi.infrastructure import storage_link from ooi.occi.infrastructure import templates @@ -198,3 +200,121 @@ def test_resource_tpl(self): mixin.Mixin) self.assertEqual("resource_tpl", templates.resource_tpl.term) + + +class TestOCCINetwork(base.TestCase): + def test_network_class(self): + n = network.NetworkResource + self.assertIn(network.up, n.actions) + self.assertIn(network.down, n.actions) + self.assertIn("occi.core.id", n.attributes) + self.assertIn("occi.core.summary", n.attributes) + self.assertIn("occi.core.title", n.attributes) + self.assertIn("occi.network.vlan", n.attributes) + self.assertIn("occi.network.label", n.attributes) + self.assertIn("occi.network.state", n.attributes) + self.assertIn(resource.Resource.kind, n.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_network(self): + id = uuid.uuid4().hex + n = network.NetworkResource("foo", + summary="This is a summary", + id=id) + self.assertEqual("foo", n.title) + self.assertEqual(id, n.id) + self.assertEqual("This is a summary", n.summary) + self.assertIsNone(n.vlan) + self.assertIsNone(n.label) + self.assertIsNone(n.state) + + def test_setters(self): + n = network.NetworkResource("foo") + n.vlan = "bar" + self.assertEqual("bar", n.attributes["occi.network.vlan"].value) + n.label = "baz" + self.assertEqual("baz", n.attributes["occi.network.label"].value) + + def test_getters(self): + n = network.NetworkResource("foo", vlan="bar", label="baz", + state="foobar") + self.assertEqual("bar", n.vlan) + self.assertEqual("baz", n.label) + self.assertEqual("foobar", n.state) + + +class TestNetwokrMixins(base.TestCase): + def test_ip_network(self): + self.assertIsInstance(network.ip_network, + mixin.Mixin) + self.assertEqual("ipnetwork", + network.ip_network.term) + self.assertIn("occi.network.address", network.ip_network.attributes) + self.assertIn("occi.network.gateway", network.ip_network.attributes) + self.assertIn("occi.network.allocation", network.ip_network.attributes) + + def test_ip_network_interface(self): + self.assertIsInstance(network_link.ip_network_interface, + mixin.Mixin) + self.assertEqual("ipnetworkinterface", + network_link.ip_network_interface.term) + self.assertIn("occi.networkinterface.address", + network_link.ip_network_interface.attributes) + self.assertIn("occi.networkinterface.gateway", + network_link.ip_network_interface.attributes) + self.assertIn("occi.networkinterface.allocation", + network_link.ip_network_interface.attributes) + + +class TestOCCINetworkInterface(base.TestCase): + def test_networkinterface_class(self): + l = network_link.NetworkInterface + self.assertIn("occi.core.id", l.attributes) + self.assertIn("occi.core.title", l.attributes) + self.assertIn("occi.core.source", l.attributes) + self.assertIn("occi.core.target", l.attributes) + self.assertIn("occi.networkinterface.interface", l.attributes) + self.assertIn("occi.networkinterface.mac", l.attributes) + self.assertIn("occi.networkinterface.state", l.attributes) + self.assertIn(link.Link.kind, l.kind.related) + + def test_networkinterface(self): + c = compute.ComputeResource("foo", + summary="This is a summary", + id=uuid.uuid4().hex) + n = network.NetworkResource("bar", + summary="This is a summary", + id=uuid.uuid4().hex) + l = network_link.NetworkInterface([], c, n) + self.assertEqual(c, l.source) + self.assertEqual(n, l.target) + self.assertIsNone(l.interface) + self.assertIsNone(l.mac) + self.assertIsNone(l.state) + + def test_setters(self): + c = compute.ComputeResource("foo", + summary="This is a summary", + id=uuid.uuid4().hex) + n = network.NetworkResource("bar", + summary="This is a summary", + id=uuid.uuid4().hex) + l = network_link.NetworkInterface([], c, n) + l.mac = "00:00:00:00:00:00" + self.assertEqual("00:00:00:00:00:00", + l.attributes["occi.networkinterface.mac"].value) + + def test_getters(self): + c = compute.ComputeResource("foo", + summary="This is a summary", + id=uuid.uuid4().hex) + n = network.NetworkResource("bar", + summary="This is a summary", + id=uuid.uuid4().hex) + l = network_link.NetworkInterface([], c, n, interface="eth1", + mac="00:01:02:03:04:05", state="foo") + self.assertEqual("eth1", l.interface) + self.assertEqual("00:01:02:03:04:05", l.mac) + self.assertEqual("foo", l.state) diff --git a/ooi/tests/occi/test_openstack.py b/ooi/tests/occi/test_openstack.py index 35fb005..f3a6d21 100644 --- a/ooi/tests/occi/test_openstack.py +++ b/ooi/tests/occi/test_openstack.py @@ -16,9 +16,13 @@ import uuid +from ooi.occi.infrastructure import compute +from ooi.occi.infrastructure import network +from ooi.occi.infrastructure import network_link from ooi.occi.infrastructure import templates as occi_templates from ooi.openstack import contextualization from ooi.openstack import helpers +from ooi.openstack import network as os_network from ooi.openstack import templates from ooi.tests import base @@ -97,3 +101,63 @@ def test_os_userdata(self): self.assertTrue(mxn.scheme.startswith(helpers._PREFIX)) self.assertEqual(key_name, mxn.name) self.assertEqual(key_data, mxn.data) + + +class TestOSNetworkInterface(base.TestCase): + def test_osnetwork_interface(self): + c = compute.ComputeResource("foo", + summary="This is a summary", + id=uuid.uuid4().hex) + n = network.NetworkResource("bar", + summary="This is a summary", + id=uuid.uuid4().hex) + i = os_network.OSNetworkInterface(c, n, "00:01:02:03:04:05", + "127.0.0.1") + self.assertEqual('_'.join([c.id, "127.0.0.1"]), i.id) + self.assertEqual(i.address, "127.0.0.1") + self.assertEqual(i.interface, "eth0") + self.assertEqual(i.mac, "00:01:02:03:04:05") + self.assertEqual(i.state, "active") + self.assertIsNone(i.gateway) + self.assertEqual(network_link.NetworkInterface.kind, i.kind) + self.assertIn(network_link.ip_network_interface, i.mixins) + # contains kind and mixins attributes + for att in network_link.NetworkInterface.kind.attributes: + self.assertIn(att, i.attributes) + for att in network_link.ip_network_interface.attributes: + self.assertIn(att, i.attributes) + + def test_setters(self): + c = compute.ComputeResource("foo", + summary="This is a summary", + id=uuid.uuid4().hex) + n = network.NetworkResource("bar", + summary="This is a summary", + id=uuid.uuid4().hex) + i = os_network.OSNetworkInterface(c, n, "00:01:02:03:04:05", + "127.0.0.1") + i.address = "192.163.1.2" + self.assertEqual( + "192.163.1.2", i.attributes["occi.networkinterface.address"].value) + i.gateway = "192.163.1.1" + self.assertEqual( + "192.163.1.1", i.attributes["occi.networkinterface.gateway"].value) + i.allocation = "static" + self.assertEqual( + "static", i.attributes["occi.networkinterface.allocation"].value) + + def test_getters(self): + c = compute.ComputeResource("foo", + summary="This is a summary", + id=uuid.uuid4().hex) + n = network.NetworkResource("bar", + summary="This is a summary", + id=uuid.uuid4().hex) + i = os_network.OSNetworkInterface(c, n, "00:01:02:03:04:05", + "127.0.0.1") + i.attributes["occi.networkinterface.address"].value = "192.163.1.2" + self.assertEqual("192.163.1.2", i.address) + i.attributes["occi.networkinterface.gateway"].value = "192.163.1.1" + self.assertEqual("192.163.1.1", i.gateway) + i.attributes["occi.networkinterface.allocation"].value = "static" + self.assertEqual("static", i.allocation)