diff --git a/quark/api/extensions/ports_quark.py b/quark/api/extensions/ports_quark.py index 8b549d5..d4b9c7c 100644 --- a/quark/api/extensions/ports_quark.py +++ b/quark/api/extensions/ports_quark.py @@ -45,7 +45,10 @@ EXTENDED_ATTRIBUTES_2_0 = { "vlan_id": { "allow_post": False, "allow_put": False, - "is_visible": True}}} + "is_visible": True}, + "network_plugin": {"allow_post": True, "allow_put": False, + "enforce_policy": True, + "is_visible": False, "default": ''}}} class Ports_quark(object): diff --git a/quark/db/migration/alembic/versions/374c1bdb4480_add_port_network_plugin.py b/quark/db/migration/alembic/versions/374c1bdb4480_add_port_network_plugin.py new file mode 100644 index 0000000..27a7d58 --- /dev/null +++ b/quark/db/migration/alembic/versions/374c1bdb4480_add_port_network_plugin.py @@ -0,0 +1,23 @@ +"""add_port_network_plugin + +Revision ID: 374c1bdb4480 +Revises: 4da4444d7706 +Create Date: 2015-10-20 12:08:24.780056 + +""" + +# revision identifiers, used by Alembic. +revision = '374c1bdb4480' +down_revision = '4da4444d7706' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.add_column('quark_ports', sa.Column('network_plugin', + sa.String(length=36), nullable=True)) + + +def downgrade(): + op.drop_column('quark_ports', 'network_plugin') diff --git a/quark/db/migration/alembic/versions/HEAD b/quark/db/migration/alembic/versions/HEAD index e73a2b7..27305bf 100644 --- a/quark/db/migration/alembic/versions/HEAD +++ b/quark/db/migration/alembic/versions/HEAD @@ -1 +1 @@ -4da4444d7706 \ No newline at end of file +374c1bdb4480 \ No newline at end of file diff --git a/quark/db/models.py b/quark/db/models.py index 8ed3492..c9eebe7 100644 --- a/quark/db/models.py +++ b/quark/db/models.py @@ -430,6 +430,8 @@ class Port(BASEV2, models.HasTenant, models.HasId, IsHazTags): bridge = sa.Column(sa.String(255)) associations = orm.relationship(PortIpAssociation, backref="port") + network_plugin = sa.Column(sa.String(36), nullable=True) + @declarative.declared_attr def ip_addresses(cls): primaryjoin = cls.id == port_ip_association_table.c.port_id diff --git a/quark/drivers/registry.py b/quark/drivers/registry.py index b39b8f3..e4ea4c6 100644 --- a/quark/drivers/registry.py +++ b/quark/drivers/registry.py @@ -18,6 +18,10 @@ from quark.drivers import optimized_nvp_driver as optnvp from quark.drivers.registry_base import DriverRegistryBase from quark.drivers import unmanaged +from oslo_log import log as logging + +LOG = logging.getLogger(__name__) + class DriverRegistry(DriverRegistryBase): def __init__(self): @@ -28,5 +32,45 @@ class DriverRegistry(DriverRegistryBase): optnvp.OptimizedNVPDriver.get_name(): optnvp.OptimizedNVPDriver(), unmanaged.UnmanagedDriver.get_name(): unmanaged.UnmanagedDriver()}) + # You may optionally specify a port-level driver name that will + # be used intead of the underlying network driver. This map determines + # which drivers are allowed to be used in this way. + # example: {"MY_DRIVER": ["MY_OTHER_DRIVER"]} + # The above example would allow ports created with "MY_DRIVER" + # specified to be used with networks that use "MY_OTHER_DRIVER", + # but *not* the inverse. + # Note that drivers are automatically compatible with themselves. + self.port_driver_compat_map = {} + + def get_driver(self, net_driver, port_driver=None): + LOG.info("Selecting driver for net_driver:%s " + "port_driver:%s" % (net_driver, port_driver)) + + if port_driver: + + # Check port_driver is valid driver + if port_driver not in self.drivers: + raise Exception("Driver %s is not registered." % port_driver) + + # Net drivers are compatible with themselves + if port_driver == net_driver: + LOG.info("Selecting port_driver:%s" % (port_driver)) + return self.drivers[port_driver] + + # Check port_driver is compatible with the given net_driver + allowed = self.port_driver_compat_map.get(port_driver, []) + if net_driver not in allowed: + raise Exception("Port driver %s not allowed for " + "underlying network driver %s." + % (port_driver, net_driver)) + + LOG.info("Selecting port_driver:%s" % (port_driver)) + return self.drivers[port_driver] + + elif net_driver in self.drivers: + LOG.info("Selecting net_driver:%s" % (net_driver)) + return self.drivers[net_driver] + + raise Exception("Driver %s is not registered." % net_driver) DRIVER_REGISTRY = DriverRegistry() diff --git a/quark/plugin_modules/ports.py b/quark/plugin_modules/ports.py index 0d7ce0e..700f968 100644 --- a/quark/plugin_modules/ports.py +++ b/quark/plugin_modules/ports.py @@ -43,6 +43,19 @@ def _raise_if_unauthorized(tenant_id, net): raise exceptions.NotAuthorized() +def _get_net_driver(network, port=None): + port_driver = None + if port and port.get("network_plugin"): + port_driver = port.get("network_plugin") + + try: + return registry.DRIVER_REGISTRY.get_driver( + network["network_plugin"], port_driver=port_driver) + except Exception as e: + raise exceptions.BadRequest(resource="ports", + msg="invalid network_plugin: %s" % e) + + def split_and_validate_requested_subnets(context, net_id, segment_id, fixed_ips): subnets = [] @@ -90,7 +103,7 @@ def create_port(context, port): port_attrs = port["port"] admin_only = ["mac_address", "device_owner", "bridge", "admin_state_up", - "use_forbidden_mac_range"] + "use_forbidden_mac_range", "network_plugin"] utils.filter_body(context, port_attrs, admin_only=admin_only) port_attrs = port["port"] @@ -144,7 +157,7 @@ def create_port(context, port): ipam_driver = ipam.IPAM_REGISTRY.get_strategy(net["ipam_strategy"]) - net_driver = registry.DRIVER_REGISTRY.get_driver(net["network_plugin"]) + net_driver = _get_net_driver(net, port=port_attrs) # TODO(anyone): security groups are not currently supported on port create. # Please see JIRA:NCP-801 @@ -285,7 +298,7 @@ def update_port(context, id, port): admin_only = ["mac_address", "device_owner", "bridge", "admin_state_up", "device_id"] - always_filter = ["network_id", "backend_key"] + always_filter = ["network_id", "backend_key", "network_plugin"] utils.filter_body(context, port_dict, admin_only=admin_only, always_filter=always_filter) @@ -380,8 +393,9 @@ def update_port(context, id, port): port_dict["addresses"] = port_db["ip_addresses"] port_dict["addresses"].extend(addresses) - net_driver = registry.DRIVER_REGISTRY.get_driver( - port_db.network["network_plugin"]) + # NOTE(morgabra) Updating network_plugin on port objects is explicitly + # disallowed in the api, so we use whatever exists in the db. + net_driver = _get_net_driver(port_db.network, port=port_db) # TODO(anyone): What do we want to have happen here if this fails? Is it # ok to continue to keep the IPs but fail to apply security @@ -520,8 +534,7 @@ def delete_port(context, id): ipam_driver.deallocate_ips_by_port( context, port, ipam_reuse_after=CONF.QUARK.ipam_reuse_after) - net_driver = registry.DRIVER_REGISTRY.get_driver( - port.network["network_plugin"]) + net_driver = _get_net_driver(port.network, port=port) net_driver.delete_port(context, backend_key, device_id=port["device_id"], mac_address=port["mac_address"]) @@ -531,8 +544,7 @@ def delete_port(context, id): def _diag_port(context, port, fields): p = v._make_port_dict(port) - net_driver = registry.DRIVER_REGISTRY.get_driver( - port.network["network_plugin"]) + net_driver = _get_net_driver(port.network, port=port) if 'config' in fields: p.update(net_driver.diag_port( context, port["backend_key"], get_status='status' in fields)) diff --git a/quark/tests/plugin_modules/test_ports.py b/quark/tests/plugin_modules/test_ports.py index e5e949d..612f921 100644 --- a/quark/tests/plugin_modules/test_ports.py +++ b/quark/tests/plugin_modules/test_ports.py @@ -1132,11 +1132,49 @@ class TestPortDiagnose(test_quark_plugin.TestQuarkPlugin): self.plugin.diagnose_port(self.context, 1, []) -class TestPortBadNetworkPlugin(test_quark_plugin.TestQuarkPlugin): +class TestPortNetworkPlugin(test_quark_plugin.TestQuarkPlugin): + @contextlib.contextmanager + def _stubs(self, network=None, addr=None, mac=None, compat_map=None): + network["ipam_strategy"] = "ANY" + + # Mock out the driver registry + foo_driver = mock.Mock() + foo_driver.create_port.return_value = {"uuid": 1} + bar_driver = mock.Mock() + bar_driver.create_port.return_value = {"uuid": 1} + drivers = {"FOO": foo_driver, + "BAR": bar_driver} + compat_map = compat_map or {} + + with contextlib.nested( + mock.patch("quark.db.api.port_create"), + mock.patch("quark.db.api.network_find"), + mock.patch("quark.ipam.QuarkIpam.allocate_ip_address"), + mock.patch("quark.ipam.QuarkIpam.allocate_mac_address"), + mock.patch("oslo_utils.uuidutils.generate_uuid"), + mock.patch("quark.plugin_views._make_port_dict"), + mock.patch("quark.db.api.port_count_all"), + mock.patch("neutron.quota.QuotaEngine.limit_check"), + mock.patch("quark.plugin_modules.ports.registry." + "DRIVER_REGISTRY.drivers", + new_callable=mock.PropertyMock(return_value=drivers)), + mock.patch("quark.plugin_modules.ports.registry." + "DRIVER_REGISTRY.port_driver_compat_map", + new_callable=mock.PropertyMock(return_value=compat_map)) + ) as (port_create, net_find, alloc_ip, alloc_mac, gen_uuid, make_port, + port_count, limit_check, _, _): + net_find.return_value = network + alloc_ip.return_value = addr + alloc_mac.return_value = mac + gen_uuid.return_value = 1 + port_count.return_value = 0 + yield port_create, alloc_mac, net_find + def test_create_port_with_bad_network_plugin_fails(self): - network_dict = dict(id=1) + network_dict = dict(id=1, tenant_id=self.context.tenant_id) port_name = "foobar" mac = dict(address="AA:BB:CC:DD:EE:FF") + ip = dict() port = dict(port=dict(mac_address=mac["address"], network_id=1, tenant_id=self.context.tenant_id, device_id=2, name=port_name)) @@ -1147,20 +1185,153 @@ class TestPortBadNetworkPlugin(test_quark_plugin.TestQuarkPlugin): port_model.update(port) port_models = port_model - with contextlib.nested( - mock.patch("quark.db.api.port_create"), - mock.patch("quark.db.api.network_find"), - mock.patch("quark.ipam.QuarkIpam.allocate_ip_address"), - mock.patch("quark.ipam.QuarkIpam.allocate_mac_address"), - ) as (port_create, net_find, alloc_ip, alloc_mac): + with self._stubs(network=network, addr=ip, + mac=mac) as (port_create, alloc_mac, net_find): port_create.return_value = port_models - net_find.return_value = network - alloc_ip.return_value = {} - alloc_mac.return_value = mac - with self.assertRaises(Exception): # noqa + exc = "Driver FAIL is not registered." + with self.assertRaisesRegexp(exceptions.BadRequest, exc): self.plugin.create_port(self.context, port) + def test_create_port_with_bad_port_network_plugin_fails(self): + network_dict = dict(id=1, tenant_id=self.context.tenant_id) + port_name = "foobar" + mac = dict(address="AA:BB:CC:DD:EE:FF") + ip = dict() + port = dict(port=dict(mac_address=mac["address"], network_id=1, + tenant_id=self.context.tenant_id, device_id=2, + name=port_name, network_plugin="FAIL")) + network = models.Network() + network.update(network_dict) + network["network_plugin"] = "FOO" + port_model = models.Port() + port_model.update(port) + port_models = port_model + + with self._stubs(network=network, addr=ip, + mac=mac) as (port_create, alloc_mac, net_find): + port_create.return_value = port_models + + exc = "Driver FAIL is not registered." + admin_ctx = self.context.elevated() + with self.assertRaisesRegexp(exceptions.BadRequest, exc): + self.plugin.create_port(admin_ctx, port) + + def test_create_port_with_incompatable_port_network_plugin_fails(self): + network_dict = dict(id=1, tenant_id=self.context.tenant_id) + port_name = "foobar" + mac = dict(address="AA:BB:CC:DD:EE:FF") + ip = dict() + port = dict(port=dict(mac_address=mac["address"], network_id=1, + tenant_id=self.context.tenant_id, device_id=2, + name=port_name, network_plugin="BAR")) + network = models.Network() + network.update(network_dict) + network["network_plugin"] = "FOO" + port_model = models.Port() + port_model.update(port) + port_models = port_model + + with self._stubs(network=network, addr=ip, + mac=mac) as (port_create, alloc_mac, net_find): + port_create.return_value = port_models + + exc = ("Port driver BAR not allowed for underlying network " + "driver FOO.") + admin_ctx = self.context.elevated() + with self.assertRaisesRegexp(exceptions.BadRequest, exc): + self.plugin.create_port(admin_ctx, port) + + def test_create_port_with_port_network_plugin(self): + network = dict(id=1, tenant_id=self.context.tenant_id, + network_plugin="FOO") + mac = dict(address="AA:BB:CC:DD:EE:FF") + port_name = "foobar" + ip = dict() + + port = dict(port=dict(mac_address=mac["address"], network_id=1, + tenant_id=self.context.tenant_id, device_id=2, + name=port_name, device_owner="quark_tests", + bridge="quark_bridge", admin_state_up=False)) + + expected_mac = "DE:AD:BE:EF:00:00" + expected_bridge = "new_bridge" + expected_device_owner = "new_device_owner" + expected_admin_state = "new_state" + expected_network_plugin = "FOO" + + port_create_dict = {} + port_create_dict["port"] = port["port"].copy() + port_create_dict["port"]["mac_address"] = expected_mac + port_create_dict["port"]["device_owner"] = expected_device_owner + port_create_dict["port"]["bridge"] = expected_bridge + port_create_dict["port"]["admin_state_up"] = expected_admin_state + port_create_dict["port"]["network_plugin"] = expected_network_plugin + + admin_ctx = self.context.elevated() + with self._stubs(network=network, addr=ip, + mac=mac) as (port_create, alloc_mac, net_find): + self.plugin.create_port(admin_ctx, port_create_dict) + + alloc_mac.assert_called_once_with( + admin_ctx, network["id"], 1, + cfg.CONF.QUARK.ipam_reuse_after, + mac_address=expected_mac, use_forbidden_mac_range=False) + + port_create.assert_called_once_with( + admin_ctx, bridge=expected_bridge, uuid=1, name="foobar", + admin_state_up=expected_admin_state, network_id=1, + tenant_id="fake", id=1, device_owner=expected_device_owner, + mac_address=mac["address"], device_id=2, backend_key=1, + security_groups=[], addresses=[], + network_plugin=expected_network_plugin) + + def test_create_port_with_compatable_port_network_plugin(self): + network = dict(id=1, tenant_id=self.context.tenant_id, + network_plugin="FOO") + mac = dict(address="AA:BB:CC:DD:EE:FF") + port_name = "foobar" + ip = dict() + + port = dict(port=dict(mac_address=mac["address"], network_id=1, + tenant_id=self.context.tenant_id, device_id=2, + name=port_name, device_owner="quark_tests", + bridge="quark_bridge", admin_state_up=False)) + + expected_mac = "DE:AD:BE:EF:00:00" + expected_bridge = "new_bridge" + expected_device_owner = "new_device_owner" + expected_admin_state = "new_state" + expected_network_plugin = "BAR" + + port_create_dict = {} + port_create_dict["port"] = port["port"].copy() + port_create_dict["port"]["mac_address"] = expected_mac + port_create_dict["port"]["device_owner"] = expected_device_owner + port_create_dict["port"]["bridge"] = expected_bridge + port_create_dict["port"]["admin_state_up"] = expected_admin_state + port_create_dict["port"]["network_plugin"] = expected_network_plugin + + compat_map = {"BAR": ["FOO"]} + admin_ctx = self.context.elevated() + with self._stubs(network=network, addr=ip, mac=mac, + compat_map=compat_map) as (port_create, alloc_mac, + net_find): + self.plugin.create_port(admin_ctx, port_create_dict) + + alloc_mac.assert_called_once_with( + admin_ctx, network["id"], 1, + cfg.CONF.QUARK.ipam_reuse_after, + mac_address=expected_mac, use_forbidden_mac_range=False) + + port_create.assert_called_once_with( + admin_ctx, bridge=expected_bridge, uuid=1, name="foobar", + admin_state_up=expected_admin_state, network_id=1, + tenant_id="fake", id=1, device_owner=expected_device_owner, + mac_address=mac["address"], device_id=2, backend_key=1, + security_groups=[], addresses=[], + network_plugin=expected_network_plugin) + class TestQuarkPortCreateFiltering(test_quark_plugin.TestQuarkPlugin): @contextlib.contextmanager @@ -1202,6 +1373,7 @@ class TestQuarkPortCreateFiltering(test_quark_plugin.TestQuarkPlugin): port_create_dict["port"]["device_owner"] = "ignored" port_create_dict["port"]["bridge"] = "ignored" port_create_dict["port"]["admin_state_up"] = "ignored" + port_create_dict["port"]["network_plugin"] = "ignored" with self._stubs(network=network, addr=ip, mac=mac) as (port_create, alloc_mac, net_find): @@ -1231,6 +1403,7 @@ class TestQuarkPortCreateFiltering(test_quark_plugin.TestQuarkPlugin): expected_bridge = "new_bridge" expected_device_owner = "new_device_owner" expected_admin_state = "new_state" + expected_network_plugin = "BASE" port_create_dict = {} port_create_dict["port"] = port["port"].copy() @@ -1238,6 +1411,7 @@ class TestQuarkPortCreateFiltering(test_quark_plugin.TestQuarkPlugin): port_create_dict["port"]["device_owner"] = expected_device_owner port_create_dict["port"]["bridge"] = expected_bridge port_create_dict["port"]["admin_state_up"] = expected_admin_state + port_create_dict["port"]["network_plugin"] = expected_network_plugin admin_ctx = self.context.elevated() with self._stubs(network=network, addr=ip, @@ -1254,7 +1428,8 @@ class TestQuarkPortCreateFiltering(test_quark_plugin.TestQuarkPlugin): admin_state_up=expected_admin_state, network_id=1, tenant_id="fake", id=1, device_owner=expected_device_owner, mac_address=mac["address"], device_id=2, backend_key=1, - security_groups=[], addresses=[]) + security_groups=[], addresses=[], + network_plugin=expected_network_plugin) class TestQuarkPortUpdateFiltering(test_quark_plugin.TestQuarkPlugin): @@ -1274,7 +1449,8 @@ class TestQuarkPortUpdateFiltering(test_quark_plugin.TestQuarkPlugin): new_port["port"] = { "mac_address": "DD:EE:FF:00:00:00", "device_owner": "new_owner", "bridge": "new_bridge", "admin_state_up": False, "device_id": 3, - "network_id": 10, "backend_key": 1234, "name": "new_name"} + "network_id": 10, "backend_key": 1234, "name": "new_name", + "network_plugin": "BASE"} with self._stubs() as (port_find, port_update): self.plugin.update_port(self.context, 1, new_port) @@ -1289,7 +1465,8 @@ class TestQuarkPortUpdateFiltering(test_quark_plugin.TestQuarkPlugin): new_port["port"] = { "mac_address": "DD:EE:FF:00:00:00", "device_owner": "new_owner", "bridge": "new_bridge", "admin_state_up": False, "device_id": 3, - "network_id": 10, "backend_key": 1234, "name": "new_name"} + "network_id": 10, "backend_key": 1234, "name": "new_name", + "network_plugin": "BASE"} admin_ctx = self.context.elevated() with self._stubs() as (port_find, port_update): diff --git a/quark/tests/test_driver_registry.py b/quark/tests/test_driver_registry.py new file mode 100644 index 0000000..f067691 --- /dev/null +++ b/quark/tests/test_driver_registry.py @@ -0,0 +1,98 @@ +# Copyright 2013 Openstack Foundation +# All Rights Reserved. +# +# 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 quark.drivers import registry +from quark.drivers import registry_base +from quark.tests import test_base + + +class FakeBaseRegistry(registry_base.DriverRegistryBase): + + def __init__(self): + self.drivers = {"test_driver_1": 1, + "test_driver_2": 2} + + +class FakeNetDriverRegistry(registry.DriverRegistry): + + def __init__(self): + self.drivers = {"test_driver_1": 1, + "test_driver_2": 2, + "test_driver_3": 3} + + self.port_driver_compat_map = { + "test_driver_2": ["test_driver_1"], + "test_driver_3": ["test_driver_1", + "test_driver_2"] + } + + +class TestRegistryBase(test_base.TestBase): + + def setUp(self): + self.registry = FakeBaseRegistry() + + def test_get_valid(self): + driver = self.registry.get_driver("test_driver_1") + self.assertEqual(driver, 1) + + driver = self.registry.get_driver("test_driver_2") + self.assertEqual(driver, 2) + + def test_get_invalid(self): + exc = "Driver does_not_exist is not registered." + with self.assertRaisesRegexp(Exception, exc): + self.registry.get_driver("does_not_exist") + + +class TestDriverRegistry(TestRegistryBase): + + def setUp(self): + self.registry = FakeNetDriverRegistry() + + def test_get_port_driver(self): + driver = self.registry.get_driver( + "test_driver_1", port_driver="test_driver_1") + self.assertEqual(driver, 1) + + driver = self.registry.get_driver( + "test_driver_2", port_driver="test_driver_2") + self.assertEqual(driver, 2) + + def test_get_invalid_port_driver(self): + exc = "Driver does_not_exist is not registered." + with self.assertRaisesRegexp(Exception, exc): + self.registry.get_driver( + "test_driver_1", port_driver="does_not_exist") + + def test_get_compatable_port_driver(self): + driver = self.registry.get_driver( + "test_driver_1", port_driver="test_driver_2") + self.assertEqual(driver, 2) + + driver = self.registry.get_driver( + "test_driver_1", port_driver="test_driver_3") + self.assertEqual(driver, 3) + + driver = self.registry.get_driver( + "test_driver_2", port_driver="test_driver_3") + self.assertEqual(driver, 3) + + def test_get_incompatable_port_driver(self): + exc = ("Port driver test_driver_2 not allowed for " + "underlying network driver test_driver_3.") + with self.assertRaisesRegexp(Exception, exc): + self.registry.get_driver( + "test_driver_3", port_driver="test_driver_2")