From b6133c35dd587f6b01e8ec12757347b2767713a6 Mon Sep 17 00:00:00 2001 From: Francois Eleouet Date: Thu, 22 Aug 2013 16:51:01 +0200 Subject: [PATCH] OVS agent implementation of l2-population This patchset implements l2-population RPC callbacks in OVS agents, it enables plugin to populate forwarding table following portbindings events. For now, it doesn't include ARP responder implementation which is deferred to a future patchset (As this feature isn't yet supported by OVS, it will require the use of an external responder such as ebtables) It anyway brings some improvements in tunnelling management, as agent will tear-down unecessary tunnels, and flood packets on a per-network basis rather than to all other agents. These changes should anyway have a limited risk, as tunnel management won't be affected as long as l2_population option is not set. This option must be used in conjonction with ml2 plugin using l2population mechanism driver. Implements: blueprint l2-population Change-Id: I5185eefedb0ff392bc8b99d16f810813e26ff58d --- .../openvswitch/ovs_neutron_plugin.ini | 8 + neutron/agent/linux/ovs_lib.py | 41 +++- .../plugins/ml2/drivers/l2pop/constants.py | 4 +- .../openvswitch/agent/ovs_neutron_agent.py | 192 +++++++++++++++--- neutron/plugins/openvswitch/common/config.py | 3 + .../tests/unit/openvswitch/test_ovs_lib.py | 30 ++- .../openvswitch/test_ovs_neutron_agent.py | 162 ++++++++++++++- .../tests/unit/openvswitch/test_ovs_tunnel.py | 16 +- 8 files changed, 401 insertions(+), 55 deletions(-) diff --git a/etc/neutron/plugins/openvswitch/ovs_neutron_plugin.ini b/etc/neutron/plugins/openvswitch/ovs_neutron_plugin.ini index 48442db5213..9a1b948d2d6 100644 --- a/etc/neutron/plugins/openvswitch/ovs_neutron_plugin.ini +++ b/etc/neutron/plugins/openvswitch/ovs_neutron_plugin.ini @@ -113,6 +113,14 @@ # veth_mtu = # Example: veth_mtu = 1504 +# (BoolOpt) Flag to enable l2-population extension. This option should only be +# used in conjunction with ml2 plugin and l2population mechanism driver. It'll +# enable plugin to populate remote ports macs and IPs (using fdb_add/remove +# RPC calbbacks instead of tunnel_sync/update) on OVS agents in order to +# optimize tunnel management. +# +# l2_population = False + [securitygroup] # Firewall driver for realizing neutron security group function. # firewall_driver = neutron.agent.firewall.NoopFirewallDriver diff --git a/neutron/agent/linux/ovs_lib.py b/neutron/agent/linux/ovs_lib.py index 2c2bd9d8b50..faa335ea6a6 100644 --- a/neutron/agent/linux/ovs_lib.py +++ b/neutron/agent/linux/ovs_lib.py @@ -49,6 +49,8 @@ class OVSBridge: self.br_name = br_name self.root_helper = root_helper self.re_id = self.re_compile_id() + self.defer_apply_flows = False + self.deferred_flows = {'add': '', 'mod': '', 'del': ''} def re_compile_id(self): external = 'external_ids\s*' @@ -92,10 +94,11 @@ class OVSBridge: args = ["clear", table_name, record, column] self.run_vsctl(args) - def run_ofctl(self, cmd, args): + def run_ofctl(self, cmd, args, process_input=None): full_args = ["ovs-ofctl", cmd, self.br_name] + args try: - return utils.execute(full_args, root_helper=self.root_helper) + return utils.execute(full_args, root_helper=self.root_helper, + process_input=process_input) except Exception as e: LOG.error(_("Unable to execute %(cmd)s. Exception: %(exception)s"), {'cmd': full_args, 'exception': e}) @@ -161,11 +164,17 @@ class OVSBridge: def add_flow(self, **kwargs): flow_str = self.add_or_mod_flow_str(**kwargs) - self.run_ofctl("add-flow", [flow_str]) + if self.defer_apply_flows: + self.deferred_flows['add'] += flow_str + '\n' + else: + self.run_ofctl("add-flow", [flow_str]) def mod_flow(self, **kwargs): flow_str = self.add_or_mod_flow_str(**kwargs) - self.run_ofctl("mod-flows", [flow_str]) + if self.defer_apply_flows: + self.deferred_flows['mod'] += flow_str + '\n' + else: + self.run_ofctl("mod-flows", [flow_str]) def delete_flows(self, **kwargs): kwargs['delete'] = True @@ -173,12 +182,32 @@ class OVSBridge: if "actions" in kwargs: flow_expr_arr.append("actions=%s" % (kwargs["actions"])) flow_str = ",".join(flow_expr_arr) - self.run_ofctl("del-flows", [flow_str]) + if self.defer_apply_flows: + self.deferred_flows['del'] += flow_str + '\n' + else: + self.run_ofctl("del-flows", [flow_str]) + + def defer_apply_on(self): + LOG.debug(_('defer_apply_on')) + self.defer_apply_flows = True + + def defer_apply_off(self): + LOG.debug(_('defer_apply_off')) + for action, flows in self.deferred_flows.items(): + if flows: + LOG.debug(_('Applying following deferred flows ' + 'to bridge %s'), self.br_name) + for line in flows.splitlines(): + LOG.debug(_('%(action)s: %(flow)s'), + {'action': action, 'flow': line}) + self.run_ofctl('%s-flows' % action, ['-'], flows) + self.defer_apply_flows = False + self.deferred_flows = {'add': '', 'mod': '', 'del': ''} def add_tunnel_port(self, port_name, remote_ip, local_ip, tunnel_type=constants.TYPE_GRE, vxlan_udp_port=constants.VXLAN_UDP_PORT): - self.run_vsctl(["add-port", self.br_name, port_name]) + self.run_vsctl(["--may-exist", "add-port", self.br_name, port_name]) self.set_db_attribute("Interface", port_name, "type", tunnel_type) if tunnel_type == constants.TYPE_VXLAN: # Only set the VXLAN UDP port if it's not the default diff --git a/neutron/plugins/ml2/drivers/l2pop/constants.py b/neutron/plugins/ml2/drivers/l2pop/constants.py index 74ca3a1ab9d..85ed7a5e4ec 100644 --- a/neutron/plugins/ml2/drivers/l2pop/constants.py +++ b/neutron/plugins/ml2/drivers/l2pop/constants.py @@ -17,4 +17,6 @@ # @author: Francois Eleouet, Orange # @author: Mathieu Rohon, Orange -SUPPORTED_AGENT_TYPES = [] +from neutron.common import constants + +SUPPORTED_AGENT_TYPES = [constants.AGENT_TYPE_OVS] diff --git a/neutron/plugins/openvswitch/agent/ovs_neutron_agent.py b/neutron/plugins/openvswitch/agent/ovs_neutron_agent.py index 5a5182dc7f7..eefe384367f 100644 --- a/neutron/plugins/openvswitch/agent/ovs_neutron_agent.py +++ b/neutron/plugins/openvswitch/agent/ovs_neutron_agent.py @@ -29,6 +29,7 @@ import time import eventlet from oslo.config import cfg +from neutron.agent import l2population_rpc from neutron.agent.linux import ip_lib from neutron.agent.linux import ovs_lib from neutron.agent import rpc as agent_rpc @@ -66,6 +67,8 @@ class LocalVLANMapping: self.physical_network = physical_network self.segmentation_id = segmentation_id self.vif_ports = vif_ports + # set of tunnel ports on which packets should be flooded + self.tun_ofports = set() def __str__(self): return ("lv-id = %s type = %s phys-net = %s phys-id = %s" % @@ -116,7 +119,8 @@ class OVSSecurityGroupAgent(sg_rpc.SecurityGroupAgentRpcMixin): self.init_firewall() -class OVSNeutronAgent(sg_rpc.SecurityGroupAgentRpcCallbackMixin): +class OVSNeutronAgent(sg_rpc.SecurityGroupAgentRpcCallbackMixin, + l2population_rpc.L2populationRpcCallBackMixin): '''Implements OVS-based tunneling, VLANs and flat networks. Two local bridges are created: an integration bridge (defaults to @@ -151,7 +155,7 @@ class OVSNeutronAgent(sg_rpc.SecurityGroupAgentRpcCallbackMixin): def __init__(self, integ_br, tun_br, local_ip, bridge_mappings, root_helper, polling_interval, tunnel_types=None, - veth_mtu=None): + veth_mtu=None, l2_population=False): '''Constructor. :param integ_br: name of the integration bridge. @@ -170,12 +174,15 @@ class OVSNeutronAgent(sg_rpc.SecurityGroupAgentRpcCallbackMixin): self.available_local_vlans = set(xrange(q_const.MIN_VLAN_TAG, q_const.MAX_VLAN_TAG)) self.tunnel_types = tunnel_types or [] + self.l2_pop = l2_population self.agent_state = { 'binary': 'neutron-openvswitch-agent', 'host': cfg.CONF.host, 'topic': q_const.L2_AGENT_TOPIC, 'configurations': {'bridge_mappings': bridge_mappings, - 'tunnel_types': self.tunnel_types}, + 'tunnel_types': self.tunnel_types, + 'tunneling_ip': local_ip, + 'l2_population': self.l2_pop}, 'agent_type': q_const.AGENT_TYPE_OVS, 'start_flag': True} @@ -187,8 +194,8 @@ class OVSNeutronAgent(sg_rpc.SecurityGroupAgentRpcCallbackMixin): self.setup_integration_br() self.setup_physical_bridges(bridge_mappings) self.local_vlan_map = {} - self.tun_br_ofports = {constants.TYPE_GRE: set(), - constants.TYPE_VXLAN: set()} + self.tun_br_ofports = {constants.TYPE_GRE: {}, + constants.TYPE_VXLAN: {}} self.polling_interval = polling_interval @@ -242,6 +249,9 @@ class OVSNeutronAgent(sg_rpc.SecurityGroupAgentRpcCallbackMixin): [topics.NETWORK, topics.DELETE], [constants.TUNNEL, topics.UPDATE], [topics.SECURITY_GROUP, topics.UPDATE]] + if self.l2_pop: + consumers.append([topics.L2POPULATION, + topics.UPDATE, cfg.CONF.host]) self.connection = agent_rpc.create_consumers(self.dispatcher, self.topic, consumers) @@ -263,7 +273,7 @@ class OVSNeutronAgent(sg_rpc.SecurityGroupAgentRpcCallbackMixin): # The network may not be defined on this agent lvm = self.local_vlan_map.get(network_id) if lvm: - self.reclaim_local_vlan(network_id, lvm) + self.reclaim_local_vlan(network_id) else: LOG.debug(_("Network %s not used on agent."), network_id) @@ -313,7 +323,94 @@ class OVSNeutronAgent(sg_rpc.SecurityGroupAgentRpcCallbackMixin): if tunnel_ip == self.local_ip: return tun_name = '%s-%s' % (tunnel_type, tunnel_id) - self.setup_tunnel_port(tun_name, tunnel_ip, tunnel_type) + if not self.l2_pop: + self.setup_tunnel_port(tun_name, tunnel_ip, tunnel_type) + + def fdb_add(self, context, fdb_entries): + LOG.debug(_("fdb_add received")) + for network_id, values in fdb_entries.items(): + lvm = self.local_vlan_map.get(network_id) + if not lvm: + # Agent doesn't manage any port in this network + continue + agent_ports = values.get('ports') + agent_ports.pop(self.local_ip, None) + if len(agent_ports): + self.tun_br.defer_apply_on() + for agent_ip, ports in agent_ports.items(): + # Ensure we have a tunnel port with this remote agent + ofport = self.tun_br_ofports[ + lvm.network_type].get(agent_ip) + if not ofport: + port_name = '%s-%s' % (lvm.network_type, agent_ip) + ofport = self.setup_tunnel_port(port_name, agent_ip, + lvm.network_type) + if ofport == 0: + continue + for port in ports: + self._add_fdb_flow(port, agent_ip, lvm, ofport) + self.tun_br.defer_apply_off() + + def fdb_remove(self, context, fdb_entries): + LOG.debug(_("fdb_remove received")) + for network_id, values in fdb_entries.items(): + lvm = self.local_vlan_map.get(network_id) + if not lvm: + # Agent doesn't manage any more ports in this network + continue + agent_ports = values.get('ports') + agent_ports.pop(self.local_ip, None) + if len(agent_ports): + self.tun_br.defer_apply_on() + for agent_ip, ports in agent_ports.items(): + ofport = self.tun_br_ofports[ + lvm.network_type].get(agent_ip) + if not ofport: + continue + for port in ports: + self._del_fdb_flow(port, agent_ip, lvm, ofport) + self.tun_br.defer_apply_off() + + def _add_fdb_flow(self, port_info, agent_ip, lvm, ofport): + if port_info == q_const.FLOODING_ENTRY: + lvm.tun_ofports.add(ofport) + ofports = ','.join(lvm.tun_ofports) + self.tun_br.mod_flow(table=constants.FLOOD_TO_TUN, + priority=1, + dl_vlan=lvm.vlan, + actions="strip_vlan,set_tunnel:%s," + "output:%s" % (lvm.segmentation_id, ofports)) + else: + # TODO(feleouet): add ARP responder entry + self.tun_br.add_flow(table=constants.UCAST_TO_TUN, + priority=2, + dl_vlan=lvm.vlan, + dl_dst=port_info[0], + actions="strip_vlan,set_tunnel:%s,output:%s" % + (lvm.segmentation_id, ofport)) + + def _del_fdb_flow(self, port_info, agent_ip, lvm, ofport): + if port_info == q_const.FLOODING_ENTRY: + lvm.tun_ofports.remove(ofport) + if len(lvm.tun_ofports) > 0: + ofports = ','.join(lvm.tun_ofports) + self.tun_br.mod_flow(table=constants.FLOOD_TO_TUN, + priority=1, + dl_vlan=lvm.vlan, + actions="strip_vlan," + "set_tunnel:%s,output:%s" % + (lvm.segmentation_id, ofports)) + else: + # This local vlan doesn't require any more tunelling + self.tun_br.delete_flows(table=constants.FLOOD_TO_TUN, + dl_vlan=lvm.vlan) + # Check if this tunnel port is still used + self.cleanup_tunnel_port(ofport, lvm.network_type) + else: + #TODO(feleouet): remove ARP responder entry + self.tun_br.delete_flows(table=constants.UCAST_TO_TUN, + dl_vlan=lvm.vlan, + dl_dst=port_info[0]) def create_rpc_dispatcher(self): '''Get the rpc dispatcher for this manager. @@ -348,12 +445,14 @@ class OVSNeutronAgent(sg_rpc.SecurityGroupAgentRpcCallbackMixin): if network_type in constants.TUNNEL_NETWORK_TYPES: if self.enable_tunneling: # outbound broadcast/multicast - ofports = ','.join(self.tun_br_ofports[network_type]) - self.tun_br.mod_flow(table=constants.FLOOD_TO_TUN, - priority=1, - dl_vlan=lvid, - actions="strip_vlan,set_tunnel:%s," - "output:%s" % (segmentation_id, ofports)) + ofports = ','.join(self.tun_br_ofports[network_type].values()) + if ofports: + self.tun_br.mod_flow(table=constants.FLOOD_TO_TUN, + priority=1, + dl_vlan=lvid, + actions="strip_vlan," + "set_tunnel:%s,output:%s" % + (segmentation_id, ofports)) # inbound from tunnels: set lvid in the right table # and resubmit to Table LEARN_FROM_TUN for mac learning self.tun_br.add_flow(table=constants.TUN_TABLE[network_type], @@ -415,13 +514,18 @@ class OVSNeutronAgent(sg_rpc.SecurityGroupAgentRpcCallbackMixin): {'network_type': network_type, 'net_uuid': net_uuid}) - def reclaim_local_vlan(self, net_uuid, lvm): + def reclaim_local_vlan(self, net_uuid): '''Reclaim a local VLAN. :param net_uuid: the network uuid associated with this vlan. :param lvm: a LocalVLANMapping object that tracks (vlan, lsw_id, vif_ids) mapping. ''' + lvm = self.local_vlan_map.pop(net_uuid, None) + if lvm is None: + LOG.debug(_("Network %s not used on agent."), net_uuid) + return + LOG.info(_("Reclaiming vlan = %(vlan_id)s from net-id = %(net_uuid)s"), {'vlan_id': lvm.vlan, 'net_uuid': net_uuid}) @@ -432,6 +536,10 @@ class OVSNeutronAgent(sg_rpc.SecurityGroupAgentRpcCallbackMixin): table=constants.TUN_TABLE[lvm.network_type], tun_id=lvm.segmentation_id) self.tun_br.delete_flows(dl_vlan=lvm.vlan) + if self.l2_pop: + # Try to remove tunnel ports if not used by other networks + for ofport in lvm.tun_ofports: + self.cleanup_tunnel_port(ofport, lvm.network_type) elif lvm.network_type == constants.TYPE_FLAT: if lvm.physical_network in self.phys_brs: # outbound @@ -463,7 +571,6 @@ class OVSNeutronAgent(sg_rpc.SecurityGroupAgentRpcCallbackMixin): {'network_type': lvm.network_type, 'net_uuid': net_uuid}) - del self.local_vlan_map[net_uuid] self.available_local_vlans.add(lvm.vlan) def port_bound(self, port, net_uuid, @@ -509,7 +616,7 @@ class OVSNeutronAgent(sg_rpc.SecurityGroupAgentRpcCallbackMixin): lvm.vif_ports.pop(vif_id, None) if not lvm.vif_ports: - self.reclaim_local_vlan(net_uuid, lvm) + self.reclaim_local_vlan(net_uuid) def port_dead(self, port): '''Once a port has no binding, put it on the "dead vlan". @@ -737,16 +844,19 @@ class OVSNeutronAgent(sg_rpc.SecurityGroupAgentRpcCallbackMixin): if ofport < 0: LOG.error(_("Failed to set-up %(type)s tunnel port to %(ip)s"), {'type': tunnel_type, 'ip': remote_ip}) - else: - self.tun_br_ofports[tunnel_type].add(ofport) - # Add flow in default table to resubmit to the right - # tunelling table (lvid will be set in the latter) - self.tun_br.add_flow(priority=1, - in_port=ofport, - actions="resubmit(,%s)" % - constants.TUN_TABLE[tunnel_type]) + return 0 + + self.tun_br_ofports[tunnel_type][remote_ip] = ofport + # Add flow in default table to resubmit to the right + # tunelling table (lvid will be set in the latter) + self.tun_br.add_flow(priority=1, + in_port=ofport, + actions="resubmit(,%s)" % + constants.TUN_TABLE[tunnel_type]) + + ofports = ','.join(self.tun_br_ofports[tunnel_type].values()) + if ofports and not self.l2_pop: # Update flooding flows to include the new tunnel - ofports = ','.join(self.tun_br_ofports[tunnel_type]) for network_id, vlan_mapping in self.local_vlan_map.iteritems(): if vlan_mapping.network_type == tunnel_type: self.tun_br.mod_flow(table=constants.FLOOD_TO_TUN, @@ -756,6 +866,20 @@ class OVSNeutronAgent(sg_rpc.SecurityGroupAgentRpcCallbackMixin): "set_tunnel:%s,output:%s" % (vlan_mapping.segmentation_id, ofports)) + return ofport + + def cleanup_tunnel_port(self, tun_ofport, tunnel_type): + # Check if this tunnel port is still used + for lvm in self.local_vlan_map.values(): + if tun_ofport in lvm.tun_ofports: + break + # If not, remove it + else: + for remote_ip, ofport in self.tun_br_ofports[tunnel_type].items(): + if ofport == tun_ofport: + port_name = '%s-%s' % (tunnel_type, remote_ip) + self.tun_br.delete_port(port_name) + self.tun_br_ofports[tunnel_type].pop(remote_ip, None) def treat_devices_added(self, devices): resync = False @@ -883,14 +1007,15 @@ class OVSNeutronAgent(sg_rpc.SecurityGroupAgentRpcCallbackMixin): details = self.plugin_rpc.tunnel_sync(self.context, self.local_ip, tunnel_type) - tunnels = details['tunnels'] - for tunnel in tunnels: - if self.local_ip != tunnel['ip_address']: - tunnel_id = tunnel.get('id', tunnel['ip_address']) - tun_name = '%s-%s' % (tunnel_type, tunnel_id) - self.setup_tunnel_port(tun_name, - tunnel['ip_address'], - tunnel_type) + if not self.l2_pop: + tunnels = details['tunnels'] + for tunnel in tunnels: + if self.local_ip != tunnel['ip_address']: + tunnel_id = tunnel.get('id', tunnel['ip_address']) + tun_name = '%s-%s' % (tunnel_type, tunnel_id) + self.setup_tunnel_port(tun_name, + tunnel['ip_address'], + tunnel_type) except Exception as e: LOG.debug(_("Unable to sync tunnel IP %(local_ip)s: %(e)s"), {'local_ip': self.local_ip, 'e': e}) @@ -1011,6 +1136,7 @@ def create_agent_config_map(config): polling_interval=config.AGENT.polling_interval, tunnel_types=config.AGENT.tunnel_types, veth_mtu=config.AGENT.veth_mtu, + l2_population=config.AGENT.l2_population, ) # If enable_tunneling is TRUE, set tunnel_type to default to GRE diff --git a/neutron/plugins/openvswitch/common/config.py b/neutron/plugins/openvswitch/common/config.py index 76e522f4fba..1aab164d8e7 100644 --- a/neutron/plugins/openvswitch/common/config.py +++ b/neutron/plugins/openvswitch/common/config.py @@ -69,6 +69,9 @@ agent_opts = [ help=_("The UDP port to use for VXLAN tunnels.")), cfg.IntOpt('veth_mtu', default=None, help=_("MTU size of veth interfaces")), + cfg.BoolOpt('l2_population', default=False, + help=_("Use ml2 l2population mechanism driver to learn " + "remote mac and IPs and improve tunnel scalability")), ] diff --git a/neutron/tests/unit/openvswitch/test_ovs_lib.py b/neutron/tests/unit/openvswitch/test_ovs_lib.py index 1b9486affe4..ab9ff8fe4f8 100644 --- a/neutron/tests/unit/openvswitch/test_ovs_lib.py +++ b/neutron/tests/unit/openvswitch/test_ovs_lib.py @@ -158,8 +158,8 @@ class OVS_Lib_Test(base.BaseTestCase): def test_count_flows(self): utils.execute(["ovs-ofctl", "dump-flows", self.BR_NAME], - root_helper=self.root_helper).AndReturn('ignore' - '\nflow-1\n') + root_helper=self.root_helper, + process_input=None).AndReturn('ignore\nflow-1\n') self.mox.ReplayAll() # counts the number of flows as total lines of output - 2 @@ -183,13 +183,37 @@ class OVS_Lib_Test(base.BaseTestCase): self.br.delete_flows(dl_vlan=vid) self.mox.VerifyAll() + def test_defer_apply_flows(self): + self.mox.StubOutWithMock(self.br, 'add_or_mod_flow_str') + self.br.add_or_mod_flow_str( + flow='added_flow_1').AndReturn('added_flow_1') + self.br.add_or_mod_flow_str( + flow='added_flow_2').AndReturn('added_flow_2') + + self.mox.StubOutWithMock(self.br, '_build_flow_expr_arr') + self.br._build_flow_expr_arr(delete=True, + flow='deleted_flow_1' + ).AndReturn(['deleted_flow_1']) + self.mox.StubOutWithMock(self.br, 'run_ofctl') + self.br.run_ofctl('add-flows', ['-'], 'added_flow_1\nadded_flow_2\n') + self.br.run_ofctl('del-flows', ['-'], 'deleted_flow_1\n') + self.mox.ReplayAll() + + self.br.defer_apply_on() + self.br.add_flow(flow='added_flow_1') + self.br.defer_apply_on() + self.br.add_flow(flow='added_flow_2') + self.br.delete_flows(flow='deleted_flow_1') + self.br.defer_apply_off() + self.mox.VerifyAll() + def test_add_tunnel_port(self): pname = "tap99" local_ip = "1.1.1.1" remote_ip = "9.9.9.9" ofport = "6" - utils.execute(["ovs-vsctl", self.TO, "add-port", + utils.execute(["ovs-vsctl", self.TO, "--may-exist", "add-port", self.BR_NAME, pname], root_helper=self.root_helper) utils.execute(["ovs-vsctl", self.TO, "set", "Interface", pname, "type=gre"], root_helper=self.root_helper) diff --git a/neutron/tests/unit/openvswitch/test_ovs_neutron_agent.py b/neutron/tests/unit/openvswitch/test_ovs_neutron_agent.py index 3cdd9d9eefd..e74fc314958 100644 --- a/neutron/tests/unit/openvswitch/test_ovs_neutron_agent.py +++ b/neutron/tests/unit/openvswitch/test_ovs_neutron_agent.py @@ -23,6 +23,7 @@ import testtools from neutron.agent.linux import ip_lib from neutron.agent.linux import ovs_lib +from neutron.common import constants as n_const from neutron.openstack.common.rpc import common as rpc_common from neutron.plugins.openvswitch.agent import ovs_neutron_agent from neutron.plugins.openvswitch.common import constants @@ -254,15 +255,18 @@ class TestOvsNeutronAgent(base.BaseTestCase): ) def test_network_delete(self): - with mock.patch.object(self.agent, "reclaim_local_vlan") as recl_fn: + with contextlib.nested( + mock.patch.object(self.agent, "reclaim_local_vlan"), + mock.patch.object(self.agent.tun_br, "cleanup_tunnel_port") + ) as (recl_fn, clean_tun_fn): self.agent.network_delete("unused_context", network_id="123") self.assertFalse(recl_fn.called) - self.agent.local_vlan_map["123"] = "LVM object" self.agent.network_delete("unused_context", network_id="123") - recl_fn.assert_called_with("123", self.agent.local_vlan_map["123"]) + self.assertFalse(clean_tun_fn.called) + recl_fn.assert_called_with("123") def test_port_update(self): with contextlib.nested( @@ -412,6 +416,158 @@ class TestOvsNeutronAgent(base.BaseTestCase): constants.MINIMUM_OVS_VXLAN_VERSION, expecting_ok=False) + def _prepare_l2_pop_ofports(self): + lvm1 = mock.Mock() + lvm1.network_type = 'gre' + lvm1.vlan = 'vlan1' + lvm1.segmentation_id = 'seg1' + lvm1.tun_ofports = set(['1']) + lvm2 = mock.Mock() + lvm2.network_type = 'gre' + lvm2.vlan = 'vlan2' + lvm2.segmentation_id = 'seg2' + lvm2.tun_ofports = set(['1', '2']) + self.agent.local_vlan_map = {'net1': lvm1, 'net2': lvm2} + self.agent.tun_br_ofports = {'gre': + {'ip_agent_1': '1', 'ip_agent_2': '2'}} + + def test_fdb_ignore_network(self): + self._prepare_l2_pop_ofports() + fdb_entry = {'net3': {}} + with contextlib.nested( + mock.patch.object(self.agent.tun_br, 'add_flow'), + mock.patch.object(self.agent.tun_br, 'delete_flows'), + mock.patch.object(self.agent, 'setup_tunnel_port'), + mock.patch.object(self.agent, 'cleanup_tunnel_port') + ) as (add_flow_fn, del_flow_fn, add_tun_fn, clean_tun_fn): + self.agent.fdb_add(None, fdb_entry) + self.assertFalse(add_flow_fn.called) + self.assertFalse(add_tun_fn.called) + self.agent.fdb_remove(None, fdb_entry) + self.assertFalse(del_flow_fn.called) + self.assertFalse(clean_tun_fn.called) + + def test_fdb_ignore_self(self): + self._prepare_l2_pop_ofports() + self.agent.local_ip = 'agent_ip' + fdb_entry = {'net2': + {'network_type': 'gre', + 'segment_id': 'tun2', + 'ports': + {'agent_ip': + [['mac', 'ip'], + n_const.FLOODING_ENTRY]}}} + with mock.patch.object(self.agent.tun_br, + "defer_apply_on") as defer_fn: + self.agent.fdb_add(None, fdb_entry) + self.assertFalse(defer_fn.called) + + self.agent.fdb_remove(None, fdb_entry) + self.assertFalse(defer_fn.called) + + def test_fdb_add_flows(self): + self._prepare_l2_pop_ofports() + fdb_entry = {'net1': + {'network_type': 'gre', + 'segment_id': 'tun1', + 'ports': + {'ip_agent_2': + [['mac', 'ip'], + n_const.FLOODING_ENTRY]}}} + with contextlib.nested( + mock.patch.object(self.agent.tun_br, 'add_flow'), + mock.patch.object(self.agent.tun_br, 'mod_flow'), + mock.patch.object(self.agent.tun_br, 'setup_tunnel_port'), + ) as (add_flow_fn, mod_flow_fn, add_tun_fn): + add_tun_fn.return_value = '2' + self.agent.fdb_add(None, fdb_entry) + add_flow_fn.assert_called_with(table=constants.UCAST_TO_TUN, + priority=2, + dl_vlan='vlan1', + dl_dst='mac', + actions='strip_vlan,' + 'set_tunnel:seg1,output:2') + mod_flow_fn.assert_called_with(table=constants.FLOOD_TO_TUN, + priority=1, + dl_vlan='vlan1', + actions='strip_vlan,' + 'set_tunnel:seg1,output:1,2') + + def test_fdb_del_flows(self): + self._prepare_l2_pop_ofports() + fdb_entry = {'net2': + {'network_type': 'gre', + 'segment_id': 'tun2', + 'ports': + {'ip_agent_2': + [['mac', 'ip'], + n_const.FLOODING_ENTRY]}}} + with contextlib.nested( + mock.patch.object(self.agent.tun_br, 'mod_flow'), + mock.patch.object(self.agent.tun_br, 'delete_flows'), + ) as (mod_flow_fn, del_flow_fn): + self.agent.fdb_remove(None, fdb_entry) + del_flow_fn.assert_called_with(table=constants.UCAST_TO_TUN, + dl_vlan='vlan2', + dl_dst='mac') + mod_flow_fn.assert_called_with(table=constants.FLOOD_TO_TUN, + priority=1, + dl_vlan='vlan2', + actions='strip_vlan,' + 'set_tunnel:seg2,output:1') + + def test_fdb_add_port(self): + self._prepare_l2_pop_ofports() + fdb_entry = {'net1': + {'network_type': 'gre', + 'segment_id': 'tun1', + 'ports': {'ip_agent_1': [['mac', 'ip']]}}} + with contextlib.nested( + mock.patch.object(self.agent.tun_br, 'add_flow'), + mock.patch.object(self.agent.tun_br, 'mod_flow'), + mock.patch.object(self.agent, 'setup_tunnel_port') + ) as (add_flow_fn, mod_flow_fn, add_tun_fn): + self.agent.fdb_add(None, fdb_entry) + self.assertFalse(add_tun_fn.called) + fdb_entry['net1']['ports']['ip_agent_3'] = [['mac', 'ip']] + self.agent.fdb_add(None, fdb_entry) + add_tun_fn.assert_called_with('gre-ip_agent_3', 'ip_agent_3', + 'gre') + + def test_fdb_del_port(self): + self._prepare_l2_pop_ofports() + fdb_entry = {'net2': + {'network_type': 'gre', + 'segment_id': 'tun2', + 'ports': {'ip_agent_2': [n_const.FLOODING_ENTRY]}}} + with contextlib.nested( + mock.patch.object(self.agent.tun_br, 'delete_flows'), + mock.patch.object(self.agent.tun_br, 'delete_port') + ) as (del_flow_fn, del_port_fn): + self.agent.fdb_remove(None, fdb_entry) + del_port_fn.assert_called_once_with('gre-ip_agent_2') + + def test_recl_lv_port_to_preserve(self): + self._prepare_l2_pop_ofports() + self.agent.l2_pop = True + self.agent.enable_tunneling = True + with mock.patch.object( + self.agent.tun_br, 'cleanup_tunnel_port' + ) as clean_tun_fn: + self.agent.reclaim_local_vlan('net1') + self.assertFalse(clean_tun_fn.called) + + def test_recl_lv_port_to_remove(self): + self._prepare_l2_pop_ofports() + self.agent.l2_pop = True + self.agent.enable_tunneling = True + with contextlib.nested( + mock.patch.object(self.agent.tun_br, 'delete_port'), + mock.patch.object(self.agent.tun_br, 'delete_flows') + ) as (del_port_fn, del_flow_fn): + self.agent.reclaim_local_vlan('net2') + del_port_fn.assert_called_once_with('gre-ip_agent_2') + class AncillaryBridgesTest(base.BaseTestCase): diff --git a/neutron/tests/unit/openvswitch/test_ovs_tunnel.py b/neutron/tests/unit/openvswitch/test_ovs_tunnel.py index e7196b358cf..d5bd080929e 100644 --- a/neutron/tests/unit/openvswitch/test_ovs_tunnel.py +++ b/neutron/tests/unit/openvswitch/test_ovs_tunnel.py @@ -44,10 +44,7 @@ LVM_FLAT = ovs_neutron_agent.LocalVLANMapping( LVM_VLAN = ovs_neutron_agent.LocalVLANMapping( LV_ID, 'vlan', 'net1', LS_ID, VIF_PORTS) -GRE_OFPORTS = set(['11', '12']) -VXLAN_OFPORTS = set(['13', '14']) -TUN_OFPORTS = {constants.TYPE_GRE: GRE_OFPORTS, - constants.TYPE_VXLAN: VXLAN_OFPORTS} +TUN_OFPORTS = {constants.TYPE_GRE: {'ip1': '11', 'ip2': '12'}} BCAST_MAC = "01:00:00:00:00:00/01:00:00:00:00:00" UCAST_MAC = "00:00:00:00:00:00/01:00:00:00:00:00" @@ -198,12 +195,13 @@ class TunnelTest(base.BaseTestCase): self.mox.VerifyAll() def testProvisionLocalVlan(self): + ofports = ','.join(TUN_OFPORTS[constants.TYPE_GRE].values()) self.mock_tun_bridge.mod_flow(table=constants.FLOOD_TO_TUN, priority=1, dl_vlan=LV_ID, actions="strip_vlan," "set_tunnel:%s,output:%s" % - (LS_ID, ','.join(GRE_OFPORTS))) + (LS_ID, ofports)) self.mock_tun_bridge.add_flow(table=constants.TUN_TABLE['gre'], priority=1, @@ -302,7 +300,7 @@ class TunnelTest(base.BaseTestCase): self.VETH_MTU) a.available_local_vlans = set() a.local_vlan_map[NET_UUID] = LVM - a.reclaim_local_vlan(NET_UUID, LVM) + a.reclaim_local_vlan(NET_UUID) self.assertTrue(LVM.vlan in a.available_local_vlans) self.mox.VerifyAll() @@ -325,7 +323,7 @@ class TunnelTest(base.BaseTestCase): a.available_local_vlans = set() a.local_vlan_map[NET_UUID] = LVM_FLAT - a.reclaim_local_vlan(NET_UUID, LVM_FLAT) + a.reclaim_local_vlan(NET_UUID) self.assertTrue(LVM_FLAT.vlan in a.available_local_vlans) self.mox.VerifyAll() @@ -348,7 +346,7 @@ class TunnelTest(base.BaseTestCase): a.available_local_vlans = set() a.local_vlan_map[NET_UUID] = LVM_VLAN - a.reclaim_local_vlan(NET_UUID, LVM_VLAN) + a.reclaim_local_vlan(NET_UUID) self.assertTrue(LVM_VLAN.vlan in a.available_local_vlans) self.mox.VerifyAll() @@ -370,7 +368,7 @@ class TunnelTest(base.BaseTestCase): def testPortUnbound(self): self.mox.StubOutWithMock( ovs_neutron_agent.OVSNeutronAgent, 'reclaim_local_vlan') - ovs_neutron_agent.OVSNeutronAgent.reclaim_local_vlan(NET_UUID, LVM) + ovs_neutron_agent.OVSNeutronAgent.reclaim_local_vlan(NET_UUID) self.mox.ReplayAll()