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