From 7e79e6973e879bc14ca02977fe98e8382b507ea2 Mon Sep 17 00:00:00 2001 From: Francois Eleouet Date: Wed, 11 Sep 2013 14:19:39 +0200 Subject: [PATCH] Vxlan / L2population support to Linuxbridge Agent This patch introduces VXLAN support for Linuxbridge agent alongside with ml2 plugin support in linubridge mechnism driver. A new vxlan configuration section is added for vxlan related parameters. The agent also implements l2population RPC callbacks which allows ml2 plugin using l2population mechnism driver to populate vxlan forwarding and neighbor tables following portbinding events. It allows agent to respond locally to ARP requests for remote VMs and avoid dataplane based learning. This should help limiting the use of multicast or flooding for broadcast emulation in vxlan networks. These changes should anyway have a limited risk, as agent behaviour shouldn't be affected, except when vxlan is enabled alongside ml2 plugin. Implements: blueprint l2-population Change-Id: I99a961c53f9e451409f1affb079042936d8ae5c6 --- .../plugins/linuxbridge/linuxbridge_conf.ini | 27 ++ .../rootwrap.d/linuxbridge-plugin.filters | 1 + neutron/agent/linux/ip_lib.py | 28 ++ neutron/common/constants.py | 1 + neutron/common/exceptions.py | 4 + .../agent/linuxbridge_neutron_agent.py | 298 ++++++++++++++++-- neutron/plugins/linuxbridge/common/config.py | 21 ++ .../plugins/linuxbridge/common/constants.py | 9 + .../plugins/ml2/drivers/l2pop/constants.py | 3 +- .../plugins/ml2/drivers/mech_linuxbridge.py | 9 +- .../tests/unit/linuxbridge/test_defaults.py | 5 + .../unit/linuxbridge/test_lb_neutron_agent.py | 262 ++++++++++++++- .../tests/unit/ml2/test_mech_linuxbridge.py | 13 +- 13 files changed, 652 insertions(+), 29 deletions(-) diff --git a/etc/neutron/plugins/linuxbridge/linuxbridge_conf.ini b/etc/neutron/plugins/linuxbridge/linuxbridge_conf.ini index 8b459fcc3e..b8cbd3c0c1 100644 --- a/etc/neutron/plugins/linuxbridge/linuxbridge_conf.ini +++ b/etc/neutron/plugins/linuxbridge/linuxbridge_conf.ini @@ -31,6 +31,33 @@ # physical_interface_mappings = # Example: physical_interface_mappings = physnet1:eth1 +[vxlan] +# (BoolOpt) enable VXLAN on the agent +# VXLAN support can be enabled when agent is managed by ml2 plugin using +# linuxbridge mechanism driver. Useless if set while using linuxbridge plugin. +# enable_vxlan = False +# +# (IntOpt) use specific TTL for vxlan interface protocol packets +# ttl = +# +# (IntOpt) use specific TOS for vxlan interface protocol packets +# tos = +# +# (StrOpt) multicast group to use for broadcast emulation. +# This group must be the same on all the agents. +# vxlan_group = 224.0.0.1 +# +# (StrOpt) Local IP address to use for VXLAN endpoints (required) +# local_ip = +# +# (BoolOpt) Flag to enable l2population extension. This option should be used +# in conjunction with ml2 plugin l2population mechanism driver (in that case, +# both linuxbridge and l2population mechanism drivers should be loaded). +# It enables plugin to populate VXLAN forwarding table, in order to limit +# the use of broadcast emulation (multicast will be turned off if kernel and +# iproute2 supports unicast flooding - requires 3.11 kernel and iproute2 3.10) +# l2_population = False + [agent] # Agent's polling interval in seconds # polling_interval = 2 diff --git a/etc/neutron/rootwrap.d/linuxbridge-plugin.filters b/etc/neutron/rootwrap.d/linuxbridge-plugin.filters index 4f98ce94e6..46224fff61 100644 --- a/etc/neutron/rootwrap.d/linuxbridge-plugin.filters +++ b/etc/neutron/rootwrap.d/linuxbridge-plugin.filters @@ -12,6 +12,7 @@ # unclear whether both variants are necessary, but I'm transliterating # from the old mechanism brctl: CommandFilter, brctl, root +bridge: CommandFilter, bridge, root # ip_lib ip: IpFilter, ip, root diff --git a/neutron/agent/linux/ip_lib.py b/neutron/agent/linux/ip_lib.py index 3eadbfbb9a..c8a0e672a4 100644 --- a/neutron/agent/linux/ip_lib.py +++ b/neutron/agent/linux/ip_lib.py @@ -146,6 +146,27 @@ class IPWrapper(SubProcessBase): if self.namespace: device.link.set_netns(self.namespace) + def add_vxlan(self, name, vni, group=None, dev=None, ttl=None, tos=None, + local=None, port=None): + cmd = ['add', name, 'type', 'vxlan', 'id', vni, 'proxy'] + if group: + cmd.extend(['group', group]) + if dev: + cmd.extend(['dev', dev]) + if ttl: + cmd.extend(['ttl', ttl]) + if tos: + cmd.extend(['tos', tos]) + if local: + cmd.extend(['local', local]) + # tuple: min,max + if port and len(port) == 2: + cmd.extend(['port', port[0], port[1]]) + elif port: + raise exceptions.NetworkVxlanPortRangeError(vxlan_range=port) + self._as_root('', 'link', cmd) + return (IPDevice(name, self.root_helper, self.namespace)) + @classmethod def get_namespaces(cls, root_helper): output = cls._execute('', 'netns', ('list',), root_helper=root_helper) @@ -449,3 +470,10 @@ def device_exists(device_name, root_helper=None, namespace=None): except RuntimeError: return False return bool(address) + + +def iproute_arg_supported(command, arg, root_helper=None): + command += ['help'] + stdout, stderr = utils.execute(command, root_helper=root_helper, + check_exit_code=False, return_stderr=True) + return any(arg in line for line in stderr.split('\n')) diff --git a/neutron/common/constants.py b/neutron/common/constants.py index 03af7373cf..016fa94c6f 100644 --- a/neutron/common/constants.py +++ b/neutron/common/constants.py @@ -44,6 +44,7 @@ DHCP_RESPONSE_PORT = 68 MIN_VLAN_TAG = 1 MAX_VLAN_TAG = 4094 +MAX_VXLAN_VNI = 16777215 FLOODING_ENTRY = ['00:00:00:00:00:00', '0.0.0.0'] EXT_NS_COMP = '_backward_comp_e_ns' diff --git a/neutron/common/exceptions.py b/neutron/common/exceptions.py index cc64089be6..b51f91b77d 100644 --- a/neutron/common/exceptions.py +++ b/neutron/common/exceptions.py @@ -293,3 +293,7 @@ class NetworkVlanRangeError(NeutronException): if isinstance(kwargs['vlan_range'], tuple): kwargs['vlan_range'] = "%d:%d" % kwargs['vlan_range'] super(NetworkVlanRangeError, self).__init__(**kwargs) + + +class NetworkVxlanPortRangeError(object): + message = _("Invalid network VXLAN port range: '%(vxlan_range)s'") diff --git a/neutron/plugins/linuxbridge/agent/linuxbridge_neutron_agent.py b/neutron/plugins/linuxbridge/agent/linuxbridge_neutron_agent.py index f045f757dd..549a08c9e6 100755 --- a/neutron/plugins/linuxbridge/agent/linuxbridge_neutron_agent.py +++ b/neutron/plugins/linuxbridge/agent/linuxbridge_neutron_agent.py @@ -22,7 +22,9 @@ # Neutron OpenVSwitch Plugin. # @author: Sumit Naiksatam, Cisco Systems, Inc. +import distutils.version as dist_version import os +import platform import sys import time @@ -30,6 +32,7 @@ import eventlet from oslo.config import cfg import pyudev +from neutron.agent import l2population_rpc as l2pop_rpc from neutron.agent.linux import ip_lib from neutron.agent.linux import utils from neutron.agent import rpc as agent_rpc @@ -56,6 +59,14 @@ BRIDGE_NAME_PLACEHOLDER = "bridge_name" BRIDGE_INTERFACES_FS = BRIDGE_FS + BRIDGE_NAME_PLACEHOLDER + "/brif/" DEVICE_NAME_PLACEHOLDER = "device_name" BRIDGE_PORT_FS_FOR_DEVICE = BRIDGE_FS + DEVICE_NAME_PLACEHOLDER + "/brport" +VXLAN_INTERFACE_PREFIX = "vxlan-" + + +class NetworkSegment: + def __init__(self, network_type, physical_network, segmentation_id): + self.network_type = network_type + self.physical_network = physical_network + self.segmentation_id = segmentation_id class LinuxBridgeManager: @@ -63,6 +74,18 @@ class LinuxBridgeManager: self.interface_mappings = interface_mappings self.root_helper = root_helper self.ip = ip_lib.IPWrapper(self.root_helper) + # VXLAN related parameters: + self.local_ip = cfg.CONF.VXLAN.local_ip + self.vxlan_mode = lconst.VXLAN_NONE + if cfg.CONF.VXLAN.enable_vxlan: + self.local_int = self.get_interface_by_ip(self.local_ip) + if self.local_int: + self.check_vxlan_support() + else: + LOG.warning(_('VXLAN is enabled, a valid local_ip ' + 'must be provided')) + # Store network mapping to segments + self.network_map = {} self.udev = pyudev.Context() monitor = pyudev.Monitor.from_netlink(self.udev) @@ -105,6 +128,13 @@ class LinuxBridgeManager: tap_device_name = TAP_INTERFACE_PREFIX + interface_id[0:11] return tap_device_name + def get_vxlan_device_name(self, segmentation_id): + if 0 <= int(segmentation_id) <= constants.MAX_VXLAN_VNI: + return VXLAN_INTERFACE_PREFIX + str(segmentation_id) + else: + LOG.warning(_("Invalid Segementation ID: %s, will lead to " + "incorrect vxlan device name"), segmentation_id) + def get_all_neutron_bridges(self): neutron_bridge_list = [] bridge_list = os.listdir(BRIDGE_FS) @@ -119,6 +149,21 @@ class LinuxBridgeManager: BRIDGE_NAME_PLACEHOLDER, bridge_name) return os.listdir(bridge_interface_path) + def get_tap_devices_count(self, bridge_name): + bridge_interface_path = BRIDGE_INTERFACES_FS.replace( + BRIDGE_NAME_PLACEHOLDER, bridge_name) + try: + if_list = os.listdir(bridge_interface_path) + return len([interface for interface in if_list if + interface.startswith(TAP_INTERFACE_PREFIX)]) + except OSError: + return 0 + + def get_interface_by_ip(self, ip): + for device in self.ip.get_devices(): + if device.addr.list(to=ip): + return device.name + def get_bridge_for_tap_device(self, tap_device_name): bridges = self.get_all_neutron_bridges() for bridge in bridges: @@ -144,6 +189,18 @@ class LinuxBridgeManager: if self.ensure_bridge(bridge_name, interface, ips, gateway): return interface + def ensure_vxlan_bridge(self, network_id, segmentation_id): + """Create a vxlan and bridge unless they already exist.""" + interface = self.ensure_vxlan(segmentation_id) + if not interface: + LOG.error(_("Failed creating vxlan interface for " + "%(segmentation_id)s"), + {segmentation_id: segmentation_id}) + return + bridge_name = self.get_bridge_name(network_id) + self.ensure_bridge(bridge_name, interface) + return interface + def get_interface_details(self, interface): device = self.ip.device(interface) ips = device.addr.list(scope='global') @@ -184,6 +241,26 @@ class LinuxBridgeManager: LOG.debug(_("Done creating subinterface %s"), interface) return interface + def ensure_vxlan(self, segmentation_id): + """Create a vxlan unless it already exists.""" + interface = self.get_vxlan_device_name(segmentation_id) + if not self.device_exists(interface): + LOG.debug(_("Creating vxlan interface %(interface)s for " + "VNI %(segmentation_id)s"), + {'interface': interface, + 'segmentation_id': segmentation_id}) + args = {'dev': self.local_int} + if self.vxlan_mode == lconst.VXLAN_MCAST: + args['group'] = cfg.CONF.VXLAN.vxlan_group + if cfg.CONF.VXLAN.ttl: + args['ttl'] = cfg.CONF.VXLAN.ttl + if cfg.CONF.VXLAN.tos: + args['tos'] = cfg.CONF.VXLAN.tos + int_vxlan = self.ip.add_vxlan(interface, segmentation_id, **args) + int_vxlan.link.set_up() + LOG.debug(_("Done creating vxlan interface %s"), interface) + return interface + def update_interface_ip_details(self, destination, source, ips, gateway): if ips or gateway: @@ -244,6 +321,12 @@ class LinuxBridgeManager: # Check if the interface is part of the bridge if not self.interface_exists_on_bridge(bridge_name, interface): try: + # Check if the interface is not enslaved in another bridge + if self.is_device_on_bridge(interface): + bridge = self.get_bridge_for_tap_device(interface) + utils.execute(['brctl', 'delif', bridge, interface], + root_helper=self.root_helper) + utils.execute(['brctl', 'addif', bridge_name, interface], root_helper=self.root_helper) except Exception as e: @@ -258,6 +341,13 @@ class LinuxBridgeManager: network_type, physical_network, segmentation_id): + if network_type == lconst.TYPE_VXLAN: + if self.vxlan_mode == lconst.VXLAN_NONE: + LOG.error(_("Unable to add vxlan interface for network %s"), + network_id) + return + return self.ensure_vxlan_bridge(network_id, segmentation_id) + physical_interface = self.interface_mappings.get(physical_network) if not physical_interface: LOG.error(_("No mapping for physical network %s"), @@ -315,6 +405,9 @@ class LinuxBridgeManager: def add_interface(self, network_id, network_type, physical_network, segmentation_id, port_id): + self.network_map[network_id] = NetworkSegment(network_type, + physical_network, + segmentation_id) tap_device_name = self.get_tap_device_name(port_id) return self.add_tap_interface(network_id, network_type, physical_network, segmentation_id, @@ -333,9 +426,10 @@ class LinuxBridgeManager: self.update_interface_ip_details(interface, bridge_name, ips, gateway) - else: - if interface.startswith(physical_interface): - self.delete_vlan(interface) + elif interface.startswith(physical_interface): + self.delete_vlan(interface) + elif interface.startswith(VXLAN_INTERFACE_PREFIX): + self.delete_vxlan(interface) LOG.debug(_("Deleting bridge %s"), bridge_name) if utils.execute(['ip', 'link', 'set', bridge_name, 'down'], @@ -350,6 +444,13 @@ class LinuxBridgeManager: LOG.error(_("Cannot delete bridge %s, does not exist"), bridge_name) + def remove_empty_bridges(self): + for network_id in self.network_map.keys(): + bridge_name = self.get_bridge_name(network_id) + if not self.get_tap_devices_count(bridge_name): + self.delete_vlan_bridge(bridge_name) + del self.network_map[network_id] + def remove_interface(self, bridge_name, interface_name): if self.device_exists(bridge_name): if not self.is_device_on_bridge(interface_name): @@ -384,6 +485,15 @@ class LinuxBridgeManager: return LOG.debug(_("Done deleting subinterface %s"), interface) + def delete_vxlan(self, interface): + if self.device_exists(interface): + LOG.debug(_("Deleting vxlan interface %s for vlan"), + interface) + int_vxlan = self.ip.device(interface) + int_vxlan.link.set_down() + int_vxlan.link.delete() + LOG.debug(_("Done deleting vxlan interface %s"), interface) + def update_devices(self, registered_devices): devices = self.udev_get_tap_devices() if devices == registered_devices: @@ -408,8 +518,92 @@ class LinuxBridgeManager: def udev_get_name(self, device): return device.sys_name + def check_vxlan_support(self): + kernel_version = dist_version.LooseVersion(platform.release()) + if cfg.CONF.VXLAN.l2_population and ( + kernel_version > dist_version.LooseVersion( + lconst.MIN_VXLAN_KVER[lconst.VXLAN_UCAST])) and ( + ip_lib.iproute_arg_supported(['bridge', 'fdb'], + 'append', self.root_helper)): + self.vxlan_mode = lconst.VXLAN_UCAST + elif (kernel_version > dist_version.LooseVersion( + lconst.MIN_VXLAN_KVER[lconst.VXLAN_MCAST])) and ( + ip_lib.iproute_arg_supported(['ip', 'link', 'add', + 'type', 'vxlan'], 'proxy', + self.root_helper)): + if cfg.CONF.VXLAN.vxlan_group: + self.vxlan_mode = lconst.VXLAN_MCAST + else: + self.vxlan_mode = lconst.VXLAN_NONE + LOG.warning(_('VXLAN muticast group must be provided in ' + 'vxlan_group option to enable VXLAN')) + else: + self.vxlan_mode = lconst.VXLAN_NONE + LOG.warning(_('Unable to use VXLAN, it requires at least 3.8 ' + 'linux kernel and iproute2 3.8')) + LOG.debug(_('Using %s VXLAN mode'), self.vxlan_mode) -class LinuxBridgeRpcCallbacks(sg_rpc.SecurityGroupAgentRpcCallbackMixin): + def fdb_ip_entry_exists(self, mac, ip, interface): + entries = utils.execute(['ip', 'neigh', 'show', 'to', ip, + 'dev', interface], + root_helper=self.root_helper) + return mac in entries + + def fdb_bridge_entry_exists(self, mac, interface, agent_ip=None): + entries = utils.execute(['bridge', 'fdb', 'show', 'dev', interface], + root_helper=self.root_helper) + if not agent_ip: + return mac in entries + + return (agent_ip in entries and mac in entries) + + def add_fdb_ip_entry(self, mac, ip, interface): + utils.execute(['ip', 'neigh', 'add', ip, 'lladdr', mac, + 'dev', interface, 'nud', 'permanent'], + root_helper=self.root_helper, + check_exit_code=False) + + def remove_fdb_ip_entry(self, mac, ip, interface): + utils.execute(['ip', 'neigh', 'del', ip, 'lladdr', mac, + 'dev', interface], + root_helper=self.root_helper, + check_exit_code=False) + + def add_fdb_bridge_entry(self, mac, agent_ip, interface, operation="add"): + utils.execute(['bridge', 'fdb', operation, mac, 'dev', interface, + 'dst', agent_ip], + root_helper=self.root_helper, + check_exit_code=False) + + def remove_fdb_bridge_entry(self, mac, agent_ip, interface): + utils.execute(['bridge', 'fdb', 'del', mac, 'dev', interface, + 'dst', agent_ip], + root_helper=self.root_helper, + check_exit_code=False) + + def add_fdb_entries(self, agent_ip, ports, interface): + for mac, ip in ports: + if mac != constants.FLOODING_ENTRY[0]: + self.add_fdb_ip_entry(mac, ip, interface) + self.add_fdb_bridge_entry(mac, agent_ip, interface) + elif self.vxlan_mode == lconst.VXLAN_UCAST: + if self.fdb_bridge_entry_exists(mac, interface): + self.add_fdb_bridge_entry(mac, agent_ip, interface, + "append") + else: + self.add_fdb_bridge_entry(mac, agent_ip, interface) + + def remove_fdb_entries(self, agent_ip, ports, interface): + for mac, ip in ports: + if mac != constants.FLOODING_ENTRY[0]: + self.remove_fdb_ip_entry(mac, ip, interface) + self.remove_fdb_bridge_entry(mac, agent_ip, interface) + elif self.vxlan_mode == lconst.VXLAN_UCAST: + self.remove_fdb_bridge_entry(mac, agent_ip, interface) + + +class LinuxBridgeRpcCallbacks(sg_rpc.SecurityGroupAgentRpcCallbackMixin, + l2pop_rpc.L2populationRpcCallBackMixin): # Set RPC API version to 1.0 by default. # history @@ -451,13 +645,17 @@ class LinuxBridgeRpcCallbacks(sg_rpc.SecurityGroupAgentRpcCallbackMixin): segmentation_id) = lconst.interpret_vlan_id(vlan_id) physical_network = kwargs.get('physical_network') # create the networking for the port - self.agent.br_mgr.add_interface(port['network_id'], - network_type, - physical_network, - segmentation_id, - port['id']) - # update plugin about port status - self.agent.plugin_rpc.update_device_up(self.context, + if self.agent.br_mgr.add_interface(port['network_id'], + network_type, + physical_network, + segmentation_id, + port['id']): + # update plugin about port status + self.agent.plugin_rpc.update_device_up(self.context, + tap_device_name, + self.agent.agent_id) + else: + self.plugin_rpc.update_device_down(self.context, tap_device_name, self.agent.agent_id) else: @@ -472,6 +670,50 @@ class LinuxBridgeRpcCallbacks(sg_rpc.SecurityGroupAgentRpcCallbackMixin): except rpc_common.Timeout: LOG.error(_("RPC timeout while updating port %s"), port['id']) + def fdb_add(self, context, fdb_entries): + LOG.debug(_("fdb_add received")) + for network_id, values in fdb_entries.items(): + segment = self.agent.br_mgr.network_map.get(network_id) + if not segment: + return + + if segment.network_type != lconst.TYPE_VXLAN: + return + + interface = self.agent.br_mgr.get_vxlan_device_name( + segment.segmentation_id) + + agent_ports = values.get('ports') + for agent_ip, ports in agent_ports.items(): + if agent_ip == self.agent.br_mgr.local_ip: + continue + + self.agent.br_mgr.add_fdb_entries(agent_ip, + ports, + interface) + + def fdb_remove(self, context, fdb_entries): + LOG.debug(_("fdb_remove received")) + for network_id, values in fdb_entries.items(): + segment = self.agent.br_mgr.network_map.get(network_id) + if not segment: + return + + if segment.network_type != lconst.TYPE_VXLAN: + return + + interface = self.agent.br_mgr.get_vxlan_device_name( + segment.segmentation_id) + + agent_ports = values.get('ports') + for agent_ip, ports in agent_ports.items(): + if agent_ip == self.agent.br_mgr.local_ip: + continue + + self.agent.br_mgr.remove_fdb_entries(agent_ip, + ports, + interface) + def create_rpc_dispatcher(self): '''Get the rpc dispatcher for this manager. @@ -493,11 +735,16 @@ class LinuxBridgeNeutronAgentRPC(sg_rpc.SecurityGroupAgentRpcMixin): self.polling_interval = polling_interval self.root_helper = root_helper self.setup_linux_bridge(interface_mappings) + configurations = {'interface_mappings': interface_mappings} + if self.br_mgr.vxlan_mode is not lconst.VXLAN_NONE: + configurations['tunneling_ip'] = self.br_mgr.local_ip + configurations['tunnel_types'] = [lconst.TYPE_VXLAN] + configurations['l2_population'] = cfg.CONF.VXLAN.l2_population self.agent_state = { 'binary': 'neutron-linuxbridge-agent', 'host': cfg.CONF.host, 'topic': constants.L2_AGENT_TOPIC, - 'configurations': {'interface_mappings': interface_mappings}, + 'configurations': configurations, 'agent_type': constants.AGENT_TYPE_LINUXBRIDGE, 'start_flag': True} @@ -541,6 +788,9 @@ class LinuxBridgeNeutronAgentRPC(sg_rpc.SecurityGroupAgentRpcMixin): consumers = [[topics.PORT, topics.UPDATE], [topics.NETWORK, topics.DELETE], [topics.SECURITY_GROUP, topics.UPDATE]] + if cfg.CONF.VXLAN.l2_population: + consumers.append([topics.L2POPULATION, + topics.UPDATE, cfg.CONF.host]) self.connection = agent_rpc.create_consumers(self.dispatcher, self.topic, consumers) @@ -596,16 +846,20 @@ class LinuxBridgeNeutronAgentRPC(sg_rpc.SecurityGroupAgentRpcMixin): vlan_id = details.get('vlan_id') (network_type, segmentation_id) = lconst.interpret_vlan_id(vlan_id) - self.br_mgr.add_interface(details['network_id'], - network_type, - details['physical_network'], - segmentation_id, - details['port_id']) + if self.br_mgr.add_interface(details['network_id'], + network_type, + details['physical_network'], + segmentation_id, + details['port_id']): - # update plugin about port status - self.plugin_rpc.update_device_up(self.context, - device, - self.agent_id) + # update plugin about port status + self.plugin_rpc.update_device_up(self.context, + device, + self.agent_id) + else: + self.plugin_rpc.update_device_down(self.context, + device, + self.agent_id) else: self.remove_port_binding(details['network_id'], details['port_id']) @@ -628,9 +882,9 @@ class LinuxBridgeNeutronAgentRPC(sg_rpc.SecurityGroupAgentRpcMixin): resync = True if details['exists']: LOG.info(_("Port %s updated."), device) - # Nothing to do regarding local networking else: LOG.debug(_("Device %s not defined on plugin"), device) + self.br_mgr.remove_empty_bridges() return resync def daemon_loop(self): diff --git a/neutron/plugins/linuxbridge/common/config.py b/neutron/plugins/linuxbridge/common/config.py index 6a12f037e6..5b90021098 100644 --- a/neutron/plugins/linuxbridge/common/config.py +++ b/neutron/plugins/linuxbridge/common/config.py @@ -23,6 +23,7 @@ from neutron.agent.common import config DEFAULT_VLAN_RANGES = [] DEFAULT_INTERFACE_MAPPINGS = [] +DEFAULT_VXLAN_GROUP = '224.0.0.1' vlan_opts = [ @@ -35,6 +36,25 @@ vlan_opts = [ "or ")), ] +vxlan_opts = [ + cfg.BoolOpt('enable_vxlan', default=False, + help=_("Enable VXLAN on the agent. Can be enabled when " + "agent is managed by ml2 plugin using linuxbridge " + "mechanism driver")), + cfg.IntOpt('ttl', + help=_("TTL for vxlan interface protocol packets.")), + cfg.IntOpt('tos', + help=_("TOS for vxlan interface protocol packets.")), + cfg.StrOpt('vxlan_group', default=DEFAULT_VXLAN_GROUP, + help=_("Multicast group for vxlan interface.")), + cfg.StrOpt('local_ip', default='', + help=_("Local IP address of the VXLAN endpoints.")), + cfg.BoolOpt('l2_population', default=False, + help=_("Extension to use alongside ml2 plugin's l2population " + "mechanism driver. It enables the plugin to populate " + "VXLAN forwarding table.")), +] + bridge_opts = [ cfg.ListOpt('physical_interface_mappings', default=DEFAULT_INTERFACE_MAPPINGS, @@ -52,6 +72,7 @@ agent_opts = [ cfg.CONF.register_opts(vlan_opts, "VLANS") +cfg.CONF.register_opts(vxlan_opts, "VXLAN") cfg.CONF.register_opts(bridge_opts, "LINUX_BRIDGE") cfg.CONF.register_opts(agent_opts, "AGENT") config.register_agent_state_opts_helper(cfg.CONF) diff --git a/neutron/plugins/linuxbridge/common/constants.py b/neutron/plugins/linuxbridge/common/constants.py index 24614af8cc..2b258f5906 100644 --- a/neutron/plugins/linuxbridge/common/constants.py +++ b/neutron/plugins/linuxbridge/common/constants.py @@ -23,9 +23,18 @@ LOCAL_VLAN_ID = -2 # Values for network_type TYPE_FLAT = 'flat' TYPE_VLAN = 'vlan' +TYPE_VXLAN = 'vxlan' TYPE_LOCAL = 'local' TYPE_NONE = 'none' +# Supported VXLAN features +VXLAN_NONE = 'not_supported' +VXLAN_MCAST = 'multicast_flooding' +VXLAN_UCAST = 'unicast_flooding' + +# Corresponding minimal kernel versions requirements +MIN_VXLAN_KVER = {VXLAN_MCAST: '3.8', VXLAN_UCAST: '3.11'} + # TODO(rkukura): Eventually remove this function, which provides # temporary backward compatibility with pre-Havana RPC and DB vlan_id diff --git a/neutron/plugins/ml2/drivers/l2pop/constants.py b/neutron/plugins/ml2/drivers/l2pop/constants.py index 85ed7a5e4e..2c9b7f96fe 100644 --- a/neutron/plugins/ml2/drivers/l2pop/constants.py +++ b/neutron/plugins/ml2/drivers/l2pop/constants.py @@ -19,4 +19,5 @@ from neutron.common import constants -SUPPORTED_AGENT_TYPES = [constants.AGENT_TYPE_OVS] +SUPPORTED_AGENT_TYPES = [constants.AGENT_TYPE_OVS, + constants.AGENT_TYPE_LINUXBRIDGE] diff --git a/neutron/plugins/ml2/drivers/mech_linuxbridge.py b/neutron/plugins/ml2/drivers/mech_linuxbridge.py index 15ca7d66c2..233737f46a 100644 --- a/neutron/plugins/ml2/drivers/mech_linuxbridge.py +++ b/neutron/plugins/ml2/drivers/mech_linuxbridge.py @@ -40,12 +40,17 @@ class LinuxbridgeMechanismDriver(mech_agent.AgentMechanismDriverBase): def check_segment_for_agent(self, segment, agent): mappings = agent['configurations'].get('interface_mappings', {}) + tunnel_types = agent['configurations'].get('tunnel_types', []) LOG.debug(_("Checking segment: %(segment)s " - "for mappings: %(mappings)s"), - {'segment': segment, 'mappings': mappings}) + "for mappings: %(mappings)s " + "with tunnel_types: %(tunnel_types)s"), + {'segment': segment, 'mappings': mappings, + 'tunnel_types': tunnel_types}) network_type = segment[api.NETWORK_TYPE] if network_type == 'local': return True + elif network_type in tunnel_types: + return True elif network_type in ['flat', 'vlan']: return segment[api.PHYSICAL_NETWORK] in mappings else: diff --git a/neutron/tests/unit/linuxbridge/test_defaults.py b/neutron/tests/unit/linuxbridge/test_defaults.py index ae41630845..b9ff6e0d9f 100644 --- a/neutron/tests/unit/linuxbridge/test_defaults.py +++ b/neutron/tests/unit/linuxbridge/test_defaults.py @@ -35,3 +35,8 @@ class ConfigurationTest(base.BaseTestCase): self.assertEqual(0, len(cfg.CONF.LINUX_BRIDGE. physical_interface_mappings)) + self.assertEqual(False, cfg.CONF.VXLAN.enable_vxlan) + self.assertEqual(config.DEFAULT_VXLAN_GROUP, + cfg.CONF.VXLAN.vxlan_group) + self.assertEqual(0, len(cfg.CONF.VXLAN.local_ip)) + self.assertEqual(False, cfg.CONF.VXLAN.l2_population) diff --git a/neutron/tests/unit/linuxbridge/test_lb_neutron_agent.py b/neutron/tests/unit/linuxbridge/test_lb_neutron_agent.py index 9b60a9b9e6..fd40f74d21 100644 --- a/neutron/tests/unit/linuxbridge/test_lb_neutron_agent.py +++ b/neutron/tests/unit/linuxbridge/test_lb_neutron_agent.py @@ -23,11 +23,24 @@ import testtools from neutron.agent.linux import ip_lib from neutron.agent.linux import utils +from neutron.common import constants from neutron.openstack.common.rpc import common as rpc_common from neutron.plugins.linuxbridge.agent import linuxbridge_neutron_agent from neutron.plugins.linuxbridge.common import constants as lconst from neutron.tests import base +LOCAL_IP = '192.168.0.33' + + +class FakeIpLinkCommand(object): + def set_up(self): + pass + + +class FakeIpDevice(object): + def __init__(self): + self.link = FakeIpLinkCommand() + class TestLinuxBridge(base.BaseTestCase): @@ -61,6 +74,14 @@ class TestLinuxBridge(base.BaseTestCase): 'network_id', lconst.TYPE_VLAN, 'physnet1', 7) self.assertTrue(vlan_bridge_func.called) + def test_ensure_physical_in_bridge_vxlan(self): + self.linux_bridge.vxlan_mode = lconst.VXLAN_UCAST + with mock.patch.object(self.linux_bridge, + 'ensure_vxlan_bridge') as vxlan_bridge_func: + self.linux_bridge.ensure_physical_in_bridge( + 'network_id', 'vxlan', 'physnet1', 7) + self.assertTrue(vxlan_bridge_func.called) + class TestLinuxBridgeAgent(base.BaseTestCase): @@ -184,6 +205,12 @@ class TestLinuxBridgeManager(base.BaseTestCase): self.assertEqual(self.lbm.get_tap_device_name(if_id), "tap") + def test_get_vxlan_device_name(self): + vn_id = constants.MAX_VXLAN_VNI + self.assertEqual(self.lbm.get_vxlan_device_name(vn_id), + "vxlan-" + str(vn_id)) + self.assertEqual(self.lbm.get_vxlan_device_name(vn_id + 1), None) + def test_get_all_neutron_bridges(self): br_list = ["br-int", "brq1", "brq2", "br-ex"] with mock.patch.object(os, 'listdir') as listdir_fn: @@ -201,6 +228,25 @@ class TestLinuxBridgeManager(base.BaseTestCase): self.assertEqual(self.lbm.get_interfaces_on_bridge("br0"), ["qbr1"]) + def test_get_tap_devices_count(self): + with mock.patch.object(os, 'listdir') as listdir_fn: + listdir_fn.return_value = ['tap2101', 'eth0.100', 'vxlan-1000'] + self.assertEqual(self.lbm.get_tap_devices_count('br0'), 1) + listdir_fn.side_effect = OSError() + self.assertEqual(self.lbm.get_tap_devices_count('br0'), 0) + + def test_get_interface_by_ip(self): + with contextlib.nested( + mock.patch.object(ip_lib.IPWrapper, 'get_devices'), + mock.patch.object(ip_lib.IpAddrCommand, 'list') + ) as (get_dev_fn, ip_list_fn): + device = mock.Mock() + device.name = 'dev_name' + get_dev_fn.return_value = [device] + ip_list_fn.returnvalue = mock.Mock() + self.assertEqual(self.lbm.get_interface_by_ip(LOCAL_IP), + 'dev_name') + def test_get_bridge_for_tap_device(self): with contextlib.nested( mock.patch.object(self.lbm, "get_all_neutron_bridges"), @@ -299,6 +345,23 @@ class TestLinuxBridgeManager(base.BaseTestCase): self.assertIsNone(self.lbm.ensure_vlan("eth0", "1")) exec_fn.assert_called_once() + def test_ensure_vxlan(self): + seg_id = "12345678" + self.lbm.local_int = 'eth0' + self.lbm.vxlan_mode = lconst.VXLAN_MCAST + with mock.patch.object(self.lbm, 'device_exists') as de_fn: + de_fn.return_value = True + self.assertEqual(self.lbm.ensure_vxlan(seg_id), "vxlan-" + seg_id) + de_fn.return_value = False + with mock.patch.object(self.lbm.ip, + 'add_vxlan') as add_vxlan_fn: + add_vxlan_fn.return_value = FakeIpDevice() + self.assertEqual(self.lbm.ensure_vxlan(seg_id), + "vxlan-" + seg_id) + add_vxlan_fn.assert_called_with("vxlan-" + seg_id, seg_id, + group="224.0.0.1", + dev=self.lbm.local_int) + def test_update_interface_ip_details(self): gwdict = dict(gateway='1.1.1.1', metric=50) @@ -330,8 +393,10 @@ class TestLinuxBridgeManager(base.BaseTestCase): mock.patch.object(self.lbm, 'device_exists'), mock.patch.object(utils, 'execute'), mock.patch.object(self.lbm, 'update_interface_ip_details'), - mock.patch.object(self.lbm, 'interface_exists_on_bridge') - ) as (de_fn, exec_fn, upd_fn, ie_fn): + mock.patch.object(self.lbm, 'interface_exists_on_bridge'), + mock.patch.object(self.lbm, 'is_device_on_bridge'), + mock.patch.object(self.lbm, 'get_bridge_for_tap_device'), + ) as (de_fn, exec_fn, upd_fn, ie_fn, if_br_fn, get_if_br_fn): de_fn.return_value = False exec_fn.return_value = False self.assertEqual(self.lbm.ensure_bridge("br0", None), "br0") @@ -349,6 +414,20 @@ class TestLinuxBridgeManager(base.BaseTestCase): self.lbm.ensure_bridge("br0", "eth0") ie_fn.assert_called_with("br0", "eth0") + exec_fn.reset_mock() + exec_fn.side_effect = None + de_fn.return_value = True + ie_fn.return_value = False + get_if_br_fn.return_value = "br1" + self.lbm.ensure_bridge("br0", "eth0") + expected = [ + mock.call(['brctl', 'delif', 'br1', 'eth0'], + root_helper=self.root_helper), + mock.call(['brctl', 'addif', 'br0', 'eth0'], + root_helper=self.root_helper), + ] + exec_fn.assert_has_calls(expected) + def test_ensure_physical_in_bridge(self): self.assertFalse( self.lbm.ensure_physical_in_bridge("123", lconst.TYPE_VLAN, @@ -367,6 +446,14 @@ class TestLinuxBridgeManager(base.BaseTestCase): ) self.assertTrue(vlbr_fn.called) + with mock.patch.object(self.lbm, "ensure_vxlan_bridge") as vlbr_fn: + self.lbm.vxlan_mode = lconst.VXLAN_MCAST + self.assertTrue( + self.lbm.ensure_physical_in_bridge("123", lconst.TYPE_VXLAN, + "physnet1", "1") + ) + self.assertTrue(vlbr_fn.called) + def test_add_tap_interface(self): with mock.patch.object(self.lbm, "device_exists") as de_fn: de_fn.return_value = False @@ -434,6 +521,20 @@ class TestLinuxBridgeManager(base.BaseTestCase): updif_fn.assert_called_with("eth1", "br0", "ips", "gateway") del_vlan.assert_called_with("eth1.1") + def test_remove_empty_bridges(self): + self.lbm.network_map = {'net1': mock.Mock(), 'net2': mock.Mock()} + + def tap_count_side_effect(*args): + return 0 if args[0] == 'brqnet1' else 1 + + with contextlib.nested( + mock.patch.object(self.lbm, "delete_vlan_bridge"), + mock.patch.object(self.lbm, "get_tap_devices_count", + side_effect=tap_count_side_effect), + ) as (del_br_fn, count_tap_fn): + self.lbm.remove_empty_bridges() + del_br_fn.assert_called_once_with('brqnet1') + def test_remove_interface(self): with contextlib.nested( mock.patch.object(self.lbm, "device_exists"), @@ -481,11 +582,71 @@ class TestLinuxBridgeManager(base.BaseTestCase): "removed": set(["dev3"]) }) + def _check_vxlan_support(self, kernel_version, vxlan_proxy_supported, + fdb_append_supported, l2_population, + expected_mode): + def iproute_supported_side_effect(*args): + if args[1] == 'proxy': + return vxlan_proxy_supported + elif args[1] == 'append': + return fdb_append_supported + + with contextlib.nested( + mock.patch("platform.release", return_value=kernel_version), + mock.patch.object(ip_lib, 'iproute_arg_supported', + side_effect=iproute_supported_side_effect), + ) as (kver_fn, ip_arg_fn): + self.lbm.check_vxlan_support() + self.assertEqual(self.lbm.vxlan_mode, expected_mode) + + def test_vxlan_mode_ucast(self): + self._check_vxlan_support(kernel_version='3.12', + vxlan_proxy_supported=True, + fdb_append_supported=True, + l2_population=True, + expected_mode=lconst.VXLAN_MCAST) + + def test_vxlan_mode_mcast(self): + self._check_vxlan_support(kernel_version='3.12', + vxlan_proxy_supported=True, + fdb_append_supported=False, + l2_population=True, + expected_mode=lconst.VXLAN_MCAST) + self._check_vxlan_support(kernel_version='3.10', + vxlan_proxy_supported=True, + fdb_append_supported=True, + l2_population=True, + expected_mode=lconst.VXLAN_MCAST) + + def test_vxlan_mode_unsupported(self): + self._check_vxlan_support(kernel_version='3.7', + vxlan_proxy_supported=True, + fdb_append_supported=True, + l2_population=False, + expected_mode=lconst.VXLAN_NONE) + self._check_vxlan_support(kernel_version='3.10', + vxlan_proxy_supported=False, + fdb_append_supported=False, + l2_population=False, + expected_mode=lconst.VXLAN_NONE) + cfg.CONF.set_override('vxlan_group', '', 'VXLAN') + self._check_vxlan_support(kernel_version='3.12', + vxlan_proxy_supported=True, + fdb_append_supported=True, + l2_population=True, + expected_mode=lconst.VXLAN_NONE) + class TestLinuxBridgeRpcCallbacks(base.BaseTestCase): def setUp(self): + cfg.CONF.set_override('local_ip', LOCAL_IP, 'VXLAN') + self.addCleanup(cfg.CONF.reset) super(TestLinuxBridgeRpcCallbacks, self).setUp() + self.u_execute_p = mock.patch('neutron.agent.linux.utils.execute') + self.u_execute = self.u_execute_p.start() + self.addCleanup(self.u_execute_p.stop) + class FakeLBAgent(object): def __init__(self): self.agent_id = 1 @@ -493,11 +654,19 @@ class TestLinuxBridgeRpcCallbacks(base.BaseTestCase): LinuxBridgeManager({'physnet1': 'eth1'}, cfg.CONF.AGENT.root_helper)) + self.br_mgr.vxlan_mode = lconst.VXLAN_UCAST + segment = mock.Mock() + segment.network_type = 'vxlan' + segment.segmentation_id = 1 + self.br_mgr.network_map['net_id'] = segment + self.lb_rpc = linuxbridge_neutron_agent.LinuxBridgeRpcCallbacks( object(), FakeLBAgent() ) + self.root_helper = cfg.CONF.AGENT.root_helper + def test_network_delete(self): with contextlib.nested( mock.patch.object(self.lb_rpc.agent.br_mgr, "get_bridge_name"), @@ -620,3 +789,92 @@ class TestLinuxBridgeRpcCallbacks(base.BaseTestCase): self.lb_rpc.port_update(mock.Mock(), port=port) self.assertTrue(plugin_rpc.update_device_down.called) self.assertEqual(log.call_count, 1) + + def test_fdb_add(self): + fdb_entries = {'net_id': + {'ports': + {'agent_ip': [constants.FLOODING_ENTRY, + ['port_mac', 'port_ip']]}, + 'network_type': 'vxlan', + 'segment_id': 1}} + + with mock.patch.object(utils, 'execute', + return_value='') as execute_fn: + self.lb_rpc.fdb_add(None, fdb_entries) + + expected = [ + mock.call(['bridge', 'fdb', 'show', 'dev', 'vxlan-1'], + root_helper=self.root_helper), + mock.call(['bridge', 'fdb', 'add', + constants.FLOODING_ENTRY[0], + 'dev', 'vxlan-1', 'dst', 'agent_ip'], + root_helper=self.root_helper, + check_exit_code=False), + mock.call(['ip', 'neigh', 'add', 'port_ip', 'lladdr', + 'port_mac', 'dev', 'vxlan-1', 'nud', 'permanent'], + root_helper=self.root_helper, + check_exit_code=False), + mock.call(['bridge', 'fdb', 'add', 'port_mac', 'dev', + 'vxlan-1', 'dst', 'agent_ip'], + root_helper=self.root_helper, + check_exit_code=False), + ] + execute_fn.assert_has_calls(expected) + + def test_fdb_ignore(self): + fdb_entries = {'net_id': + {'ports': + {LOCAL_IP: [constants.FLOODING_ENTRY, + ['port_mac', 'port_ip']]}, + 'network_type': 'vxlan', + 'segment_id': 1}} + + with mock.patch.object(utils, 'execute', + return_value='') as execute_fn: + self.lb_rpc.fdb_add(None, fdb_entries) + self.lb_rpc.fdb_remove(None, fdb_entries) + + self.assertFalse(execute_fn.called) + + fdb_entries = {'other_net_id': + {'ports': + {'192.168.0.67': [constants.FLOODING_ENTRY, + ['port_mac', 'port_ip']]}, + 'network_type': 'vxlan', + 'segment_id': 1}} + + with mock.patch.object(utils, 'execute', + return_value='') as execute_fn: + self.lb_rpc.fdb_add(None, fdb_entries) + self.lb_rpc.fdb_remove(None, fdb_entries) + + self.assertFalse(execute_fn.called) + + def test_fdb_remove(self): + fdb_entries = {'net_id': + {'ports': + {'agent_ip': [constants.FLOODING_ENTRY, + ['port_mac', 'port_ip']]}, + 'network_type': 'vxlan', + 'segment_id': 1}} + + with mock.patch.object(utils, 'execute', + return_value='') as execute_fn: + self.lb_rpc.fdb_remove(None, fdb_entries) + + expected = [ + mock.call(['bridge', 'fdb', 'del', + constants.FLOODING_ENTRY[0], + 'dev', 'vxlan-1', 'dst', 'agent_ip'], + root_helper=self.root_helper, + check_exit_code=False), + mock.call(['ip', 'neigh', 'del', 'port_ip', 'lladdr', + 'port_mac', 'dev', 'vxlan-1'], + root_helper=self.root_helper, + check_exit_code=False), + mock.call(['bridge', 'fdb', 'del', 'port_mac', + 'dev', 'vxlan-1', 'dst', 'agent_ip'], + root_helper=self.root_helper, + check_exit_code=False), + ] + execute_fn.assert_has_calls(expected) diff --git a/neutron/tests/unit/ml2/test_mech_linuxbridge.py b/neutron/tests/unit/ml2/test_mech_linuxbridge.py index 6ccc5b0b68..66903c02bf 100644 --- a/neutron/tests/unit/ml2/test_mech_linuxbridge.py +++ b/neutron/tests/unit/ml2/test_mech_linuxbridge.py @@ -25,10 +25,14 @@ class LinuxbridgeMechanismBaseTestCase(base.AgentMechanismBaseTestCase): AGENT_TYPE = constants.AGENT_TYPE_LINUXBRIDGE GOOD_MAPPINGS = {'fake_physical_network': 'fake_interface'} - GOOD_CONFIGS = {'interface_mappings': GOOD_MAPPINGS} + GOOD_TUNNEL_TYPES = ['gre', 'vxlan'] + GOOD_CONFIGS = {'interface_mappings': GOOD_MAPPINGS, + 'tunnel_types': GOOD_TUNNEL_TYPES} BAD_MAPPINGS = {'wrong_physical_network': 'wrong_interface'} - BAD_CONFIGS = {'interface_mappings': BAD_MAPPINGS} + BAD_TUNNEL_TYPES = ['bad_tunnel_type'] + BAD_CONFIGS = {'interface_mappings': BAD_MAPPINGS, + 'tunnel_types': BAD_TUNNEL_TYPES} AGENTS = [{'alive': True, 'configurations': GOOD_CONFIGS}] @@ -63,3 +67,8 @@ class LinuxbridgeMechanismFlatTestCase(LinuxbridgeMechanismBaseTestCase, class LinuxbridgeMechanismVlanTestCase(LinuxbridgeMechanismBaseTestCase, base.AgentMechanismVlanTestCase): pass + + +class LinuxbridgeMechanismGreTestCase(LinuxbridgeMechanismBaseTestCase, + base.AgentMechanismGreTestCase): + pass