From 73900fd0f4c1a343c880d8529aff4f51dd071d4b Mon Sep 17 00:00:00 2001 From: gongysh Date: Thu, 21 Mar 2013 14:34:19 +0800 Subject: [PATCH] add db to save host for port blueprint portbinding-ex-db related patch in nova: https://review.openstack.org/#/c/21141/ Only OVS and linux bridge plugins now support this feature. Change-Id: I42d9bc59130e2758dd6a221d8953d63ec10e1f3c --- etc/policy.json | 6 + .../176a85fc7d79_add_portbindings_db.py | 59 +++++++++ quantum/db/portbindings_db.py | 124 ++++++++++++++++++ quantum/extensions/portbindings.py | 3 +- .../plugins/linuxbridge/lb_quantum_plugin.py | 38 +++--- .../plugins/openvswitch/ovs_quantum_plugin.py | 51 +++---- .../unit/_test_extension_portbindings.py | 117 ++++++++++++++++- .../linuxbridge/test_linuxbridge_plugin.py | 6 + .../openvswitch/test_openvswitch_plugin.py | 6 + 9 files changed, 363 insertions(+), 47 deletions(-) create mode 100644 quantum/db/migration/alembic_migrations/versions/176a85fc7d79_add_portbindings_db.py create mode 100644 quantum/db/portbindings_db.py diff --git a/etc/policy.json b/etc/policy.json index 42625665a9b..121f96c4118 100644 --- a/etc/policy.json +++ b/etc/policy.json @@ -15,6 +15,12 @@ "extension:port_binding:view": "rule:admin_only", "extension:port_binding:set": "rule:admin_only", + "get_port:binding:host_id": "rule:admin_only", + "get_port:binding:vif_type": "rule:admin_only", + "get_port:binding:profile": "rule:admin_only", + "get_port:binding:capabilities": "rule:admin_only", + "create_port:binding:host_id": "rule:admin_only", + "update_port:binding:host_id": "rule:admin_only", "subnets:private:read": "rule:admin_or_owner", "subnets:private:write": "rule:admin_or_owner", diff --git a/quantum/db/migration/alembic_migrations/versions/176a85fc7d79_add_portbindings_db.py b/quantum/db/migration/alembic_migrations/versions/176a85fc7d79_add_portbindings_db.py new file mode 100644 index 00000000000..0cda7b9c832 --- /dev/null +++ b/quantum/db/migration/alembic_migrations/versions/176a85fc7d79_add_portbindings_db.py @@ -0,0 +1,59 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2013 OpenStack Foundation +# +# 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. +# + +"""Add portbindings db + +Revision ID: 176a85fc7d79 +Revises: grizzly +Create Date: 2013-03-21 14:59:53.052600 + +""" + +# revision identifiers, used by Alembic. +revision = '176a85fc7d79' +down_revision = 'grizzly' + +# Change to ['*'] if this migration applies to all plugins + +migration_for_plugins = [ + 'quantum.plugins.openvswitch.ovs_quantum_plugin.OVSQuantumPluginV2', + 'quantum.plugins.linuxbridge.lb_quantum_plugin.LinuxBridgePluginV2', +] + +from alembic import op +import sqlalchemy as sa + +from quantum.db import migration + + +def upgrade(active_plugin=None, options=None): + if not migration.should_run(active_plugin, migration_for_plugins): + return + + op.create_table( + 'portbindingports', + sa.Column('port_id', sa.String(length=36), nullable=False), + sa.Column('host', sa.String(length=255), nullable=False), + sa.ForeignKeyConstraint(['port_id'], ['ports.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('port_id') + ) + + +def downgrade(active_plugin=None, options=None): + if not migration.should_run(active_plugin, migration_for_plugins): + return + op.drop_table('portbindingports') diff --git a/quantum/db/portbindings_db.py b/quantum/db/portbindings_db.py new file mode 100644 index 00000000000..d93b2817f63 --- /dev/null +++ b/quantum/db/portbindings_db.py @@ -0,0 +1,124 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 IBM Corp. +# 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. +# @author: Yong Sheng Gong, IBM, Corp. + +import sqlalchemy as sa +from sqlalchemy import orm + +from quantum.api.v2 import attributes +from quantum.db import db_base_plugin_v2 +from quantum.db import model_base +from quantum.db import models_v2 +from quantum.extensions import portbindings +from quantum.openstack.common import log as logging +from quantum import policy + + +LOG = logging.getLogger(__name__) + + +class PortBindingPort(model_base.BASEV2): + port_id = sa.Column(sa.String(36), + sa.ForeignKey('ports.id', ondelete="CASCADE"), + primary_key=True) + host = sa.Column(sa.String(255), nullable=False) + port = orm.relationship( + models_v2.Port, + backref=orm.backref("portbinding", + lazy='joined', uselist=False, + cascade='delete')) + + +class PortBindingMixin(object): + extra_binding_dict = None + + def _port_model_hook(self, context, original_model, query): + query = query.outerjoin(PortBindingPort, + (original_model.id == + PortBindingPort.port_id)) + return query + + def _port_result_filter_hook(self, query, filters): + values = filters and filters.get(portbindings.HOST_ID, []) + if not values: + return query + if len(values) == 1: + query = query.filter(PortBindingPort.host == values[0]) + else: + query = query.filter(PortBindingPort.host.in_(values)) + return query + + db_base_plugin_v2.QuantumDbPluginV2.register_model_query_hook( + models_v2.Port, + "portbindings_port", + _port_model_hook, + None, + _port_result_filter_hook) + + def _check_portbindings_view_auth(self, context, port): + #TODO(salv-orlando): Remove this as part of bp/make-authz-orthogonal + keys_to_delete = [] + for key in port: + if key.startswith('binding'): + policy_rule = "get_port:%s" % key + if not policy.check(context, policy_rule, port): + keys_to_delete.append(key) + for key in keys_to_delete: + del port[key] + return port + + def _process_portbindings_create_and_update(self, context, port_data, + port): + host = port_data.get(portbindings.HOST_ID) + host_set = attributes.is_attr_set(host) + if not host_set: + _extend_port_dict_binding_host(self, port, None) + return + with context.session.begin(subtransactions=True): + bind_port = context.session.query( + PortBindingPort).filter_by(port_id=port['id']).first() + if not bind_port: + context.session.add(PortBindingPort(port_id=port['id'], + host=host)) + else: + bind_port.host = host + _extend_port_dict_binding_host(self, port, host) + + def get_port_host(self, context, port_id): + with context.session.begin(subtransactions=True): + bind_port = context.session.query( + PortBindingPort).filter_by(port_id=port_id).first() + return bind_port and bind_port.host or None + + +def _extend_port_dict_binding_host(plugin, port_res, host): + port_res[portbindings.HOST_ID] = host + if plugin.extra_binding_dict: + port_res.update(plugin.extra_binding_dict) + return port_res + + +def _extend_port_dict_binding(plugin, port_res, port_db): + if not isinstance(plugin, PortBindingMixin): + return + host = (port_db.portbinding and port_db.portbinding.host or None) + return _extend_port_dict_binding_host( + plugin, port_res, host) + + # Register dict extend functions for ports +db_base_plugin_v2.QuantumDbPluginV2.register_dict_extend_funcs( + attributes.PORTS, [_extend_port_dict_binding]) diff --git a/quantum/extensions/portbindings.py b/quantum/extensions/portbindings.py index 403c7532a1f..0bc3b65e381 100644 --- a/quantum/extensions/portbindings.py +++ b/quantum/extensions/portbindings.py @@ -49,7 +49,8 @@ 'is_visible': True}, HOST_ID: {'allow_post': True, 'allow_put': True, 'default': attributes.ATTR_NOT_SPECIFIED, - 'is_visible': True}, + 'is_visible': True, + 'enforce_policy': True}, PROFILE: {'allow_post': True, 'allow_put': True, 'default': attributes.ATTR_NOT_SPECIFIED, 'validate': {'type:dict': None}, diff --git a/quantum/plugins/linuxbridge/lb_quantum_plugin.py b/quantum/plugins/linuxbridge/lb_quantum_plugin.py index 4a1716418f9..57a83c5efd2 100644 --- a/quantum/plugins/linuxbridge/lb_quantum_plugin.py +++ b/quantum/plugins/linuxbridge/lb_quantum_plugin.py @@ -32,6 +32,7 @@ from quantum.db import dhcp_rpc_base from quantum.db import extraroute_db from quantum.db import l3_rpc_base +from quantum.db import portbindings_db from quantum.db import quota_db # noqa from quantum.db import securitygroups_rpc_base as sg_db_rpc from quantum.extensions import portbindings @@ -176,7 +177,8 @@ def port_update(self, context, port, physical_network, vlan_id): class LinuxBridgePluginV2(db_base_plugin_v2.QuantumDbPluginV2, extraroute_db.ExtraRoute_db_mixin, sg_db_rpc.SecurityGroupServerRpcMixin, - agentschedulers_db.AgentSchedulerDbMixin): + agentschedulers_db.AgentSchedulerDbMixin, + portbindings_db.PortBindingMixin): """Implement the Quantum abstractions using Linux bridging. A new VLAN is created for each network. An agent is relied upon @@ -214,10 +216,13 @@ def supported_extension_aliases(self): network_view = "extension:provider_network:view" network_set = "extension:provider_network:set" - binding_view = "extension:port_binding:view" - binding_set = "extension:port_binding:set" def __init__(self): + self.extra_binding_dict = { + portbindings.VIF_TYPE: portbindings.VIF_TYPE_BRIDGE, + portbindings.CAPABILITIES: { + portbindings.CAP_PORT_FILTER: + 'security-group' in self.supported_extension_aliases}} db.initialize() self._parse_network_vlan_ranges() db.sync_network_states(self.network_vlan_ranges) @@ -441,21 +446,12 @@ def get_networks(self, context, filters=None, fields=None, return [self._fields(net, fields) for net in nets] - def _extend_port_dict_binding(self, context, port): - if self._check_view_auth(context, port, self.binding_view): - port[portbindings.VIF_TYPE] = portbindings.VIF_TYPE_BRIDGE - port[portbindings.CAPABILITIES] = { - portbindings.CAP_PORT_FILTER: - 'security-group' in self.supported_extension_aliases} - return port - def get_port(self, context, id, fields=None): with context.session.begin(subtransactions=True): port = super(LinuxBridgePluginV2, self).get_port(context, id, fields) - self._extend_port_dict_binding(context, port), - return self._fields(port, fields) + return self._check_portbindings_view_auth(context, port) def get_ports(self, context, filters=None, fields=None, sorts=None, limit=None, marker=None, page_reverse=False): @@ -464,14 +460,14 @@ def get_ports(self, context, filters=None, fields=None, ports = super(LinuxBridgePluginV2, self).get_ports(context, filters, fields, sorts, limit, marker, page_reverse) - #TODO(nati) filter by security group for port in ports: - self._extend_port_dict_binding(context, port) - res_ports.append(self._fields(port, fields)) + self._check_portbindings_view_auth(context, port) + res_ports.append(port) return res_ports def create_port(self, context, port): session = context.session + port_data = port['port'] with session.begin(subtransactions=True): self._ensure_default_security_group_on_port(context, port) sgids = self._get_security_groups_on_port(context, port) @@ -480,10 +476,13 @@ def create_port(self, context, port): port = super(LinuxBridgePluginV2, self).create_port(context, port) + self._process_portbindings_create_and_update(context, + port_data, + port) self._process_port_create_security_group( context, port, sgids) self.notify_security_groups_member_updated(context, port) - return self._extend_port_dict_binding(context, port) + return self._check_portbindings_view_auth(context, port) def update_port(self, context, id, port): original_port = self.get_port(context, id) @@ -493,6 +492,9 @@ def update_port(self, context, id, port): with session.begin(subtransactions=True): updated_port = super(LinuxBridgePluginV2, self).update_port( context, id, port) + self._process_portbindings_create_and_update(context, + port['port'], + updated_port) need_port_update_notify = self.update_security_group_on_port( context, id, port, original_port, updated_port) @@ -504,7 +506,7 @@ def update_port(self, context, id, port): if need_port_update_notify: self._notify_port_updated(context, updated_port) - return self._extend_port_dict_binding(context, updated_port) + return self._check_portbindings_view_auth(context, updated_port) def delete_port(self, context, id, l3_port_check=True): diff --git a/quantum/plugins/openvswitch/ovs_quantum_plugin.py b/quantum/plugins/openvswitch/ovs_quantum_plugin.py index f896b69ce37..e2bb77f1f42 100644 --- a/quantum/plugins/openvswitch/ovs_quantum_plugin.py +++ b/quantum/plugins/openvswitch/ovs_quantum_plugin.py @@ -38,6 +38,7 @@ from quantum.db import dhcp_rpc_base from quantum.db import extraroute_db from quantum.db import l3_rpc_base +from quantum.db import portbindings_db from quantum.db import quota_db # noqa from quantum.db import securitygroups_rpc_base as sg_db_rpc from quantum.extensions import portbindings @@ -214,7 +215,8 @@ def tunnel_update(self, context, tunnel_ip, tunnel_id): class OVSQuantumPluginV2(db_base_plugin_v2.QuantumDbPluginV2, extraroute_db.ExtraRoute_db_mixin, sg_db_rpc.SecurityGroupServerRpcMixin, - agentschedulers_db.AgentSchedulerDbMixin): + agentschedulers_db.AgentSchedulerDbMixin, + portbindings_db.PortBindingMixin): """Implement the Quantum abstractions using Open vSwitch. @@ -254,10 +256,13 @@ def supported_extension_aliases(self): network_view = "extension:provider_network:view" network_set = "extension:provider_network:set" - binding_view = "extension:port_binding:view" - binding_set = "extension:port_binding:set" def __init__(self, configfile=None): + self.extra_binding_dict = { + portbindings.VIF_TYPE: portbindings.VIF_TYPE_OVS, + portbindings.CAPABILITIES: { + portbindings.CAP_PORT_FILTER: + 'security-group' in self.supported_extension_aliases}} ovs_db_v2.initialize() self._parse_network_vlan_ranges() ovs_db_v2.sync_vlan_allocations(self.network_vlan_ranges) @@ -530,44 +535,39 @@ def get_networks(self, context, filters=None, fields=None, return [self._fields(net, fields) for net in nets] - def _extend_port_dict_binding(self, context, port): - if self._check_view_auth(context, port, self.binding_view): - port[portbindings.VIF_TYPE] = portbindings.VIF_TYPE_OVS - port[portbindings.CAPABILITIES] = { - portbindings.CAP_PORT_FILTER: - 'security-group' in self.supported_extension_aliases} - return port - def create_port(self, context, port): # Set port status as 'DOWN'. This will be updated by agent port['port']['status'] = q_const.PORT_STATUS_DOWN + port_data = port['port'] session = context.session with session.begin(subtransactions=True): self._ensure_default_security_group_on_port(context, port) sgids = self._get_security_groups_on_port(context, port) port = super(OVSQuantumPluginV2, self).create_port(context, port) + self._process_portbindings_create_and_update(context, + port_data, port) self._process_port_create_security_group(context, port, sgids) self.notify_security_groups_member_updated(context, port) - return self._extend_port_dict_binding(context, port) + return self._check_portbindings_view_auth(context, port) def get_port(self, context, id, fields=None): with context.session.begin(subtransactions=True): port = super(OVSQuantumPluginV2, self).get_port(context, - id, fields) - self._extend_port_dict_binding(context, port) - return self._fields(port, fields) + id, + fields) + return self._check_portbindings_view_auth(context, port) def get_ports(self, context, filters=None, fields=None, - sorts=None, limit=None, marker=None, - page_reverse=False): + sorts=None, limit=None, marker=None, page_reverse=False): + res_ports = [] with context.session.begin(subtransactions=True): - ports = super(OVSQuantumPluginV2, self).get_ports( - context, filters, fields, sorts, limit, marker, - page_reverse) - #TODO(nati) filter by security group + ports = super(OVSQuantumPluginV2, + self).get_ports(context, filters, fields, sorts, + limit, marker, page_reverse) for port in ports: - self._extend_port_dict_binding(context, port) - return [self._fields(port, fields) for port in ports] + self._check_portbindings_view_auth(context, port) + res_ports.append(port) + return res_ports def update_port(self, context, id, port): session = context.session @@ -579,6 +579,9 @@ def update_port(self, context, id, port): context, id, port) need_port_update_notify = self.update_security_group_on_port( context, id, port, original_port, updated_port) + self._process_portbindings_create_and_update(context, + port['port'], + updated_port) need_port_update_notify |= self.is_security_group_member_updated( context, original_port, updated_port) if original_port['admin_state_up'] != updated_port['admin_state_up']: @@ -591,7 +594,7 @@ def update_port(self, context, id, port): binding.network_type, binding.segmentation_id, binding.physical_network) - return self._extend_port_dict_binding(context, updated_port) + return self._check_portbindings_view_auth(context, updated_port) def delete_port(self, context, id, l3_port_check=True): diff --git a/quantum/tests/unit/_test_extension_portbindings.py b/quantum/tests/unit/_test_extension_portbindings.py index 15ed8ab6cbf..57a1afd9008 100644 --- a/quantum/tests/unit/_test_extension_portbindings.py +++ b/quantum/tests/unit/_test_extension_portbindings.py @@ -21,6 +21,7 @@ import contextlib from oslo.config import cfg +from webob import exc from quantum import context from quantum.extensions import portbindings @@ -48,21 +49,21 @@ def _check_response_no_portbindings(self, port): self.assertFalse(portbindings.CAPABILITIES in port) def test_port_vif_details(self): - plugin = QuantumManager.get_plugin() with self.port(name='name') as port: port_id = port['port']['id'] # Check a response of create_port self._check_response_portbindings(port['port']) # Check a response of get_port ctx = context.get_admin_context() - port = plugin.get_port(ctx, port_id) + port = self._show('ports', port_id, quantum_context=ctx)['port'] self._check_response_portbindings(port) # By default user is admin - now test non admin user ctx = context.Context(user_id=None, tenant_id=self._tenant_id, is_admin=False, read_deleted="no") - non_admin_port = plugin.get_port(ctx, port_id) + non_admin_port = self._show( + 'ports', port_id, quantum_context=ctx)['port'] self._check_response_no_portbindings(non_admin_port) def test_ports_vif_details(self): @@ -79,7 +80,115 @@ def test_ports_vif_details(self): tenant_id=self._tenant_id, is_admin=False, read_deleted="no") - ports = plugin.get_ports(ctx) + ports = self._list('ports', quantum_context=ctx)['ports'] self.assertEqual(len(ports), 2) for non_admin_port in ports: self._check_response_no_portbindings(non_admin_port) + + +class PortBindingsHostTestCaseMixin(object): + fmt = 'json' + hostname = 'testhost' + + def _check_response_portbindings_host(self, port): + self.assertEqual(port[portbindings.HOST_ID], self.hostname) + + def _check_response_no_portbindings_host(self, port): + self.assertIn('status', port) + self.assertNotIn(portbindings.HOST_ID, port) + + def test_port_vif_non_admin(self): + with self.network(set_context=True, + tenant_id='test') as net1: + with self.subnet(network=net1) as subnet1: + host_arg = {portbindings.HOST_ID: self.hostname} + try: + with self.port(subnet=subnet1, + expected_res_status=403, + arg_list=(portbindings.HOST_ID,), + set_context=True, + tenant_id='test', + **host_arg): + pass + except exc.HTTPClientError: + pass + + def test_port_vif_host(self): + host_arg = {portbindings.HOST_ID: self.hostname} + with self.port(name='name', arg_list=(portbindings.HOST_ID,), + **host_arg) as port: + port_id = port['port']['id'] + # Check a response of create_port + self._check_response_portbindings_host(port['port']) + # Check a response of get_port + ctx = context.get_admin_context() + port = self._show('ports', port_id, quantum_context=ctx)['port'] + self._check_response_portbindings_host(port) + # By default user is admin - now test non admin user + ctx = context.Context(user_id=None, + tenant_id=self._tenant_id, + is_admin=False, + read_deleted="no") + non_admin_port = self._show( + 'ports', port_id, quantum_context=ctx)['port'] + self._check_response_no_portbindings_host(non_admin_port) + + def test_ports_vif_host(self): + cfg.CONF.set_default('allow_overlapping_ips', True) + host_arg = {portbindings.HOST_ID: self.hostname} + with contextlib.nested( + self.port(name='name1', + arg_list=(portbindings.HOST_ID,), + **host_arg), + self.port(name='name2')): + ctx = context.get_admin_context() + ports = self._list('ports', quantum_context=ctx)['ports'] + self.assertEqual(2, len(ports)) + for port in ports: + if port['name'] == 'name1': + self._check_response_portbindings_host(port) + else: + self.assertFalse(port[portbindings.HOST_ID]) + # By default user is admin - now test non admin user + ctx = context.Context(user_id=None, + tenant_id=self._tenant_id, + is_admin=False, + read_deleted="no") + ports = self._list('ports', quantum_context=ctx)['ports'] + self.assertEqual(2, len(ports)) + for non_admin_port in ports: + self._check_response_no_portbindings_host(non_admin_port) + + def test_ports_vif_host_update(self): + cfg.CONF.set_default('allow_overlapping_ips', True) + host_arg = {portbindings.HOST_ID: self.hostname} + with contextlib.nested( + self.port(name='name1', + arg_list=(portbindings.HOST_ID,), + **host_arg), + self.port(name='name2')) as (port1, port2): + data = {'port': {portbindings.HOST_ID: 'testhosttemp'}} + req = self.new_update_request('ports', data, port1['port']['id']) + req.get_response(self.api) + req = self.new_update_request('ports', data, port2['port']['id']) + ctx = context.get_admin_context() + req.get_response(self.api) + ports = self._list('ports', quantum_context=ctx)['ports'] + self.assertEqual(2, len(ports)) + for port in ports: + self.assertEqual('testhosttemp', port[portbindings.HOST_ID]) + + def test_ports_vif_host_list(self): + cfg.CONF.set_default('allow_overlapping_ips', True) + host_arg = {portbindings.HOST_ID: self.hostname} + with contextlib.nested( + self.port(name='name1', + arg_list=(portbindings.HOST_ID,), + **host_arg), + self.port(name='name2'), + self.port(name='name3', + arg_list=(portbindings.HOST_ID,), + **host_arg),) as (port1, _port2, port3): + self._test_list_resources( + 'port', (port1, port3), + query_params='%s=%s' % (portbindings.HOST_ID, self.hostname)) diff --git a/quantum/tests/unit/linuxbridge/test_linuxbridge_plugin.py b/quantum/tests/unit/linuxbridge/test_linuxbridge_plugin.py index da36a86a7cf..4e581949f39 100644 --- a/quantum/tests/unit/linuxbridge/test_linuxbridge_plugin.py +++ b/quantum/tests/unit/linuxbridge/test_linuxbridge_plugin.py @@ -69,3 +69,9 @@ def setUp(self): class TestLinuxBridgePortBindingNoSG(TestLinuxBridgePortBinding): HAS_PORT_FILTER = False FIREWALL_DRIVER = test_sg_rpc.FIREWALL_NOOP_DRIVER + + +class TestOpenvswitchPortBindingHost( + LinuxBridgePluginV2TestCase, + test_bindings.PortBindingsHostTestCaseMixin): + pass diff --git a/quantum/tests/unit/openvswitch/test_openvswitch_plugin.py b/quantum/tests/unit/openvswitch/test_openvswitch_plugin.py index 08bb85adb43..a23925ee60b 100644 --- a/quantum/tests/unit/openvswitch/test_openvswitch_plugin.py +++ b/quantum/tests/unit/openvswitch/test_openvswitch_plugin.py @@ -67,3 +67,9 @@ def setUp(self, firewall_driver=None): class TestOpenvswitchPortBindingNoSG(TestOpenvswitchPortBinding): HAS_PORT_FILTER = False FIREWALL_DRIVER = test_sg_rpc.FIREWALL_NOOP_DRIVER + + +class TestOpenvswitchPortBindingHost( + OpenvswitchPluginV2TestCase, + test_bindings.PortBindingsHostTestCaseMixin): + pass