OVS: Add support for IPv6 addresses as tunnel endpoints
Remove IPv4 restriction for local_ip configuration statement. Check for IP version mismatch of local_ip and remote_ip before creating tunnel. Create hash of remote IPv6 address for OVS interface/port name with least posibility for collissions. Fix existing tests that fail because of the added check for IP version and subsequently valid IP addresses in _setup_tunnel_port. DocImpact Change-Id: I9ec137ef8c688b678a0c61f07e9a01382acbeb13 Closes-Bug: #1525895
This commit is contained in:
parent
fe702f8f2a
commit
773394a188
@ -44,8 +44,9 @@ ovs_opts = [
|
||||
cfg.StrOpt('tun_peer_patch_port', default='patch-int',
|
||||
help=_("Peer patch port in tunnel bridge for integration "
|
||||
"bridge.")),
|
||||
cfg.IPOpt('local_ip', version=4,
|
||||
help=_("Local IP address of tunnel endpoint.")),
|
||||
cfg.IPOpt('local_ip',
|
||||
help=_("Local IP address of tunnel endpoint. Can be either "
|
||||
"an IPv4 or IPv6 address.")),
|
||||
cfg.ListOpt('bridge_mappings',
|
||||
default=DEFAULT_BRIDGE_MAPPINGS,
|
||||
help=_("Comma-separated list of <physical_network>:<bridge> "
|
||||
|
@ -13,8 +13,10 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import base64
|
||||
import collections
|
||||
import functools
|
||||
import hashlib
|
||||
import signal
|
||||
import sys
|
||||
import time
|
||||
@ -1404,6 +1406,18 @@ class OVSNeutronAgent(sg_rpc.SecurityGroupAgentRpcCallbackMixin,
|
||||
return port_needs_binding
|
||||
|
||||
def _setup_tunnel_port(self, br, port_name, remote_ip, tunnel_type):
|
||||
try:
|
||||
if (netaddr.IPAddress(self.local_ip).version !=
|
||||
netaddr.IPAddress(remote_ip).version):
|
||||
LOG.error(_LE("IP version mismatch, cannot create tunnel: "
|
||||
"local_ip=%(lip)s remote_ip=%(rip)s"),
|
||||
{'lip': self.local_ip, 'rip': remote_ip})
|
||||
return 0
|
||||
except Exception:
|
||||
LOG.error(_LE("Invalid local or remote IP, cannot create tunnel: "
|
||||
"local_ip=%(lip)s remote_ip=%(rip)s"),
|
||||
{'lip': self.local_ip, 'rip': remote_ip})
|
||||
return 0
|
||||
ofport = br.add_tunnel_port(port_name,
|
||||
remote_ip,
|
||||
self.local_ip,
|
||||
@ -1661,9 +1675,18 @@ class OVSNeutronAgent(sg_rpc.SecurityGroupAgentRpcCallbackMixin,
|
||||
return failed_devices
|
||||
|
||||
@classmethod
|
||||
def get_ip_in_hex(cls, ip_address):
|
||||
def get_tunnel_hash(cls, ip_address, hashlen):
|
||||
try:
|
||||
return '%08x' % netaddr.IPAddress(ip_address, version=4)
|
||||
addr = netaddr.IPAddress(ip_address)
|
||||
if addr.version == n_const.IP_VERSION_4:
|
||||
# We cannot change this from 8, since it could break
|
||||
# backwards-compatibility
|
||||
return '%08x' % addr
|
||||
else:
|
||||
# Create 32-bit Base32 encoded hash
|
||||
sha1 = hashlib.sha1(ip_address.encode())
|
||||
iphash = base64.b32encode(sha1.digest())
|
||||
return iphash[:hashlen].decode().lower()
|
||||
except Exception:
|
||||
LOG.warning(_LW("Invalid remote IP: %s"), ip_address)
|
||||
return
|
||||
@ -1698,10 +1721,18 @@ class OVSNeutronAgent(sg_rpc.SecurityGroupAgentRpcCallbackMixin,
|
||||
|
||||
@classmethod
|
||||
def get_tunnel_name(cls, network_type, local_ip, remote_ip):
|
||||
remote_ip_hex = cls.get_ip_in_hex(remote_ip)
|
||||
if not remote_ip_hex:
|
||||
# This string is used to build port and interface names in OVS.
|
||||
# Port and interface names can be max 16 characters long,
|
||||
# including NULL, and must be unique per table per host.
|
||||
# We make the name as long as possible given the network_type,
|
||||
# for example, 'vxlan-012345678' or 'geneve-01234567'.
|
||||
|
||||
# Remove length of network type and dash
|
||||
hashlen = n_const.DEVICE_NAME_MAX_LEN - len(network_type) - 1
|
||||
remote_tunnel_hash = cls.get_tunnel_hash(remote_ip, hashlen)
|
||||
if not remote_tunnel_hash:
|
||||
return None
|
||||
return '%s-%s' % (network_type, remote_ip_hex)
|
||||
return '%s-%s' % (network_type, remote_tunnel_hash)
|
||||
|
||||
def _agent_has_updates(self, polling_manager):
|
||||
return (polling_manager.is_polling_required or
|
||||
|
@ -25,18 +25,19 @@ from neutron.plugins.ml2.drivers.openvswitch.agent.ovs_neutron_agent \
|
||||
|
||||
def get_tunnel_name_full(cls, network_type, local_ip, remote_ip):
|
||||
network_type = network_type[:3]
|
||||
remote_ip_hex = cls.get_ip_in_hex(remote_ip)
|
||||
if not remote_ip_hex:
|
||||
return None
|
||||
|
||||
# Remove length of network_type and two dashes
|
||||
hashlen = (n_const.DEVICE_NAME_MAX_LEN - len(network_type) - 2) // 2
|
||||
remote_ip_hex = encodeutils.to_utf8(remote_ip_hex)
|
||||
remote_ip_hash = hashlib.sha1(remote_ip_hex).hexdigest()[:hashlen]
|
||||
|
||||
local_ip_hex = cls.get_ip_in_hex(local_ip)
|
||||
local_ip_hex = encodeutils.to_utf8(local_ip_hex)
|
||||
source_ip_hash = hashlib.sha1(local_ip_hex).hexdigest()[:hashlen]
|
||||
remote_tunnel_hash = cls.get_tunnel_hash(remote_ip, hashlen)
|
||||
if not remote_tunnel_hash:
|
||||
return None
|
||||
|
||||
remote_tunnel_hash = encodeutils.to_utf8(remote_tunnel_hash)
|
||||
remote_ip_hash = hashlib.sha1(remote_tunnel_hash).hexdigest()[:hashlen]
|
||||
|
||||
local_tunnel_hash = cls.get_tunnel_hash(local_ip, hashlen)
|
||||
local_tunnel_hash = encodeutils.to_utf8(local_tunnel_hash)
|
||||
source_ip_hash = hashlib.sha1(local_tunnel_hash).hexdigest()[:hashlen]
|
||||
|
||||
return '%s-%s-%s' % (network_type, source_ip_hash, remote_ip_hash)
|
||||
|
||||
|
@ -99,7 +99,8 @@ class OVSAgentTestFramework(base.BaseOVSLinuxTestCase):
|
||||
'br_tun': br_tun.OVSTunnelBridge
|
||||
}
|
||||
|
||||
def create_agent(self, create_tunnels=True, ancillary_bridge=None):
|
||||
def create_agent(self, create_tunnels=True, ancillary_bridge=None,
|
||||
local_ip='192.168.10.1'):
|
||||
if create_tunnels:
|
||||
tunnel_types = [p_const.TYPE_VXLAN]
|
||||
else:
|
||||
@ -108,7 +109,7 @@ class OVSAgentTestFramework(base.BaseOVSLinuxTestCase):
|
||||
self.config.set_override('tunnel_types', tunnel_types, "AGENT")
|
||||
self.config.set_override('polling_interval', 1, "AGENT")
|
||||
self.config.set_override('prevent_arp_spoofing', False, "AGENT")
|
||||
self.config.set_override('local_ip', '192.168.10.1', "OVS")
|
||||
self.config.set_override('local_ip', local_ip, "OVS")
|
||||
self.config.set_override('bridge_mappings', bridge_mappings, "OVS")
|
||||
# Physical bridges should be created prior to running
|
||||
self._bridge_classes()['br_phys'](self.br_phys).create()
|
||||
|
@ -212,13 +212,19 @@ class TestOVSAgent(base.OVSAgentTestFramework):
|
||||
self.wait_until_ports_state(self.ports, up=True)
|
||||
self.assert_vlan_tags(self.ports, self.agent)
|
||||
|
||||
def test_assert_bridges_ports_vxlan(self):
|
||||
agent = self.create_agent()
|
||||
def _test_assert_bridges_ports_vxlan(self, local_ip=None):
|
||||
agent = self.create_agent(local_ip=local_ip)
|
||||
self.assertTrue(self.ovs.bridge_exists(self.br_int))
|
||||
self.assertTrue(self.ovs.bridge_exists(self.br_tun))
|
||||
self.assert_bridge_ports()
|
||||
self.assert_patch_ports(agent)
|
||||
|
||||
def test_assert_bridges_ports_vxlan_ipv4(self):
|
||||
self._test_assert_bridges_ports_vxlan()
|
||||
|
||||
def test_assert_bridges_ports_vxlan_ipv6(self):
|
||||
self._test_assert_bridges_ports_vxlan(local_ip='2001:db8:100::1')
|
||||
|
||||
def test_assert_bridges_ports_no_tunnel(self):
|
||||
self.create_agent(create_tunnels=False)
|
||||
self.assertTrue(self.ovs.bridge_exists(self.br_int))
|
||||
|
@ -147,19 +147,29 @@ class OVSBridgeTestCase(OVSBridgeTestBase):
|
||||
self.br.br_name, 'datapath_id', dpid)
|
||||
self.assertIn(dpid, self.br.get_datapath_id())
|
||||
|
||||
def test_add_tunnel_port(self):
|
||||
def _test_add_tunnel_port(self, attrs):
|
||||
port_name = tests_base.get_rand_device_name(net_helpers.PORT_PREFIX)
|
||||
self.br.add_tunnel_port(port_name, attrs['remote_ip'],
|
||||
attrs['local_ip'])
|
||||
self.assertEqual('gre',
|
||||
self.ovs.db_get_val('Interface', port_name, 'type'))
|
||||
options = self.ovs.db_get_val('Interface', port_name, 'options')
|
||||
for attr, val in attrs.items():
|
||||
self.assertEqual(val, options[attr])
|
||||
|
||||
def test_add_tunnel_port_ipv4(self):
|
||||
attrs = {
|
||||
'remote_ip': '192.0.2.1', # RFC 5737 TEST-NET-1
|
||||
'local_ip': '198.51.100.1', # RFC 5737 TEST-NET-2
|
||||
}
|
||||
port_name = tests_base.get_rand_device_name(net_helpers.PORT_PREFIX)
|
||||
self.br.add_tunnel_port(port_name, attrs['remote_ip'],
|
||||
attrs['local_ip'])
|
||||
self.assertEqual(self.ovs.db_get_val('Interface', port_name, 'type'),
|
||||
'gre')
|
||||
options = self.ovs.db_get_val('Interface', port_name, 'options')
|
||||
for attr, val in attrs.items():
|
||||
self.assertEqual(val, options[attr])
|
||||
self._test_add_tunnel_port(attrs)
|
||||
|
||||
def test_add_tunnel_port_ipv6(self):
|
||||
attrs = {
|
||||
'remote_ip': '2001:db8:200::1',
|
||||
'local_ip': '2001:db8:100::1',
|
||||
}
|
||||
self._test_add_tunnel_port(attrs)
|
||||
|
||||
def test_add_patch_port(self):
|
||||
local = tests_base.get_rand_device_name(net_helpers.PORT_PREFIX)
|
||||
|
@ -44,6 +44,7 @@ OVS_LINUX_KERN_VERS_WITHOUT_VXLAN = "3.12.0"
|
||||
FAKE_MAC = '00:11:22:33:44:55'
|
||||
FAKE_IP1 = '10.0.0.1'
|
||||
FAKE_IP2 = '10.0.0.2'
|
||||
FAKE_IP6 = '2001:db8:42:42::10'
|
||||
|
||||
TEST_PORT_ID1 = 'port-id-1'
|
||||
TEST_PORT_ID2 = 'port-id-2'
|
||||
@ -1243,6 +1244,7 @@ class TestOvsNeutronAgent(object):
|
||||
self.agent.l2_pop = False
|
||||
self.agent.udp_vxlan_port = 8472
|
||||
self.agent.tun_br_ofports['vxlan'] = {}
|
||||
self.agent.local_ip = '2.3.4.5'
|
||||
with mock.patch.object(self.agent.tun_br,
|
||||
"add_tunnel_port",
|
||||
return_value='6') as add_tun_port_fn,\
|
||||
@ -1460,23 +1462,50 @@ class TestOvsNeutronAgent(object):
|
||||
mock_loop.assert_called_once_with(polling_manager=mock.ANY)
|
||||
|
||||
def test_setup_tunnel_port_invalid_ofport(self):
|
||||
remote_ip = '1.2.3.4'
|
||||
with mock.patch.object(
|
||||
self.agent.tun_br,
|
||||
'add_tunnel_port',
|
||||
return_value=ovs_lib.INVALID_OFPORT) as add_tunnel_port_fn,\
|
||||
mock.patch.object(self.mod_agent.LOG, 'error') as log_error_fn:
|
||||
self.agent.local_ip = '1.2.3.4'
|
||||
ofport = self.agent._setup_tunnel_port(
|
||||
self.agent.tun_br, 'gre-1', 'remote_ip', p_const.TYPE_GRE)
|
||||
self.agent.tun_br, 'gre-1', remote_ip, p_const.TYPE_GRE)
|
||||
add_tunnel_port_fn.assert_called_once_with(
|
||||
'gre-1', 'remote_ip', self.agent.local_ip, p_const.TYPE_GRE,
|
||||
'gre-1', remote_ip, self.agent.local_ip, p_const.TYPE_GRE,
|
||||
self.agent.vxlan_udp_port, self.agent.dont_fragment,
|
||||
self.agent.tunnel_csum)
|
||||
log_error_fn.assert_called_once_with(
|
||||
_("Failed to set-up %(type)s tunnel port to %(ip)s"),
|
||||
{'type': p_const.TYPE_GRE, 'ip': 'remote_ip'})
|
||||
{'type': p_const.TYPE_GRE, 'ip': remote_ip})
|
||||
self.assertEqual(0, ofport)
|
||||
|
||||
def test_setup_tunnel_port_invalid_address_mismatch(self):
|
||||
remote_ip = '2001:db8::2'
|
||||
with mock.patch.object(self.mod_agent.LOG, 'error') as log_error_fn:
|
||||
self.agent.local_ip = '1.2.3.4'
|
||||
ofport = self.agent._setup_tunnel_port(
|
||||
self.agent.tun_br, 'gre-1', remote_ip, p_const.TYPE_GRE)
|
||||
log_error_fn.assert_called_once_with(
|
||||
_("IP version mismatch, cannot create tunnel: "
|
||||
"local_ip=%(lip)s remote_ip=%(rip)s"),
|
||||
{'lip': self.agent.local_ip, 'rip': remote_ip})
|
||||
self.assertEqual(0, ofport)
|
||||
|
||||
def test_setup_tunnel_port_invalid_netaddr_exception(self):
|
||||
remote_ip = '2001:db8::2'
|
||||
with mock.patch.object(self.mod_agent.LOG, 'error') as log_error_fn:
|
||||
self.agent.local_ip = '1.2.3.4.5'
|
||||
ofport = self.agent._setup_tunnel_port(
|
||||
self.agent.tun_br, 'gre-1', remote_ip, p_const.TYPE_GRE)
|
||||
log_error_fn.assert_called_once_with(
|
||||
_("Invalid local or remote IP, cannot create tunnel: "
|
||||
"local_ip=%(lip)s remote_ip=%(rip)s"),
|
||||
{'lip': self.agent.local_ip, 'rip': remote_ip})
|
||||
self.assertEqual(0, ofport)
|
||||
|
||||
def test_setup_tunnel_port_error_negative_df_disabled(self):
|
||||
remote_ip = '1.2.3.4'
|
||||
with mock.patch.object(
|
||||
self.agent.tun_br,
|
||||
'add_tunnel_port',
|
||||
@ -1484,18 +1513,20 @@ class TestOvsNeutronAgent(object):
|
||||
mock.patch.object(self.mod_agent.LOG, 'error') as log_error_fn:
|
||||
self.agent.dont_fragment = False
|
||||
self.agent.tunnel_csum = False
|
||||
self.agent.local_ip = '2.3.4.5'
|
||||
ofport = self.agent._setup_tunnel_port(
|
||||
self.agent.tun_br, 'gre-1', 'remote_ip', p_const.TYPE_GRE)
|
||||
self.agent.tun_br, 'gre-1', remote_ip, p_const.TYPE_GRE)
|
||||
add_tunnel_port_fn.assert_called_once_with(
|
||||
'gre-1', 'remote_ip', self.agent.local_ip, p_const.TYPE_GRE,
|
||||
'gre-1', remote_ip, self.agent.local_ip, p_const.TYPE_GRE,
|
||||
self.agent.vxlan_udp_port, self.agent.dont_fragment,
|
||||
self.agent.tunnel_csum)
|
||||
log_error_fn.assert_called_once_with(
|
||||
_("Failed to set-up %(type)s tunnel port to %(ip)s"),
|
||||
{'type': p_const.TYPE_GRE, 'ip': 'remote_ip'})
|
||||
{'type': p_const.TYPE_GRE, 'ip': remote_ip})
|
||||
self.assertEqual(0, ofport)
|
||||
|
||||
def test_setup_tunnel_port_error_negative_tunnel_csum(self):
|
||||
remote_ip = '1.2.3.4'
|
||||
with mock.patch.object(
|
||||
self.agent.tun_br,
|
||||
'add_tunnel_port',
|
||||
@ -1503,15 +1534,16 @@ class TestOvsNeutronAgent(object):
|
||||
mock.patch.object(self.mod_agent.LOG, 'error') as log_error_fn:
|
||||
self.agent.dont_fragment = True
|
||||
self.agent.tunnel_csum = True
|
||||
self.agent.local_ip = '2.3.4.5'
|
||||
ofport = self.agent._setup_tunnel_port(
|
||||
self.agent.tun_br, 'gre-1', 'remote_ip', p_const.TYPE_GRE)
|
||||
self.agent.tun_br, 'gre-1', remote_ip, p_const.TYPE_GRE)
|
||||
add_tunnel_port_fn.assert_called_once_with(
|
||||
'gre-1', 'remote_ip', self.agent.local_ip, p_const.TYPE_GRE,
|
||||
'gre-1', remote_ip, self.agent.local_ip, p_const.TYPE_GRE,
|
||||
self.agent.vxlan_udp_port, self.agent.dont_fragment,
|
||||
self.agent.tunnel_csum)
|
||||
log_error_fn.assert_called_once_with(
|
||||
_("Failed to set-up %(type)s tunnel port to %(ip)s"),
|
||||
{'type': p_const.TYPE_GRE, 'ip': 'remote_ip'})
|
||||
{'type': p_const.TYPE_GRE, 'ip': remote_ip})
|
||||
self.assertEqual(0, ofport)
|
||||
|
||||
def test_tunnel_sync_with_ml2_plugin(self):
|
||||
@ -1862,8 +1894,10 @@ class TestOvsNeutronAgent(object):
|
||||
self.agent.l2_pop = False
|
||||
self.agent.local_vlan_map = {
|
||||
'foo': self.mod_agent.LocalVLANMapping(4, tunnel_type, 2, 1)}
|
||||
self.agent.local_ip = '2.3.4.5'
|
||||
bridge.install_flood_to_tun.side_effect = add_new_vlan_mapping
|
||||
self.agent._setup_tunnel_port(bridge, 1, 2, tunnel_type=tunnel_type)
|
||||
self.agent._setup_tunnel_port(bridge, 1, '1.2.3.4',
|
||||
tunnel_type=tunnel_type)
|
||||
self.assertIn('bar', self.agent.local_vlan_map)
|
||||
|
||||
def test_setup_entry_for_arp_reply_ignores_ipv6_addresses(self):
|
||||
@ -3011,6 +3045,12 @@ class TestValidateTunnelLocalIP(base.BaseTestCase):
|
||||
ovs_agent.validate_local_ip(FAKE_IP1)
|
||||
mock_get_device_by_ip.assert_called_once_with(FAKE_IP1)
|
||||
|
||||
def test_validate_local_ip_with_valid_ipv6(self):
|
||||
mock_get_device_by_ip = mock.patch.object(
|
||||
ip_lib.IPWrapper, 'get_device_by_ip').start()
|
||||
ovs_agent.validate_local_ip(FAKE_IP6)
|
||||
mock_get_device_by_ip.assert_called_once_with(FAKE_IP6)
|
||||
|
||||
def test_validate_local_ip_with_none_ip(self):
|
||||
with testtools.ExpectedException(SystemExit):
|
||||
ovs_agent.validate_local_ip(None)
|
||||
@ -3023,11 +3063,20 @@ class TestValidateTunnelLocalIP(base.BaseTestCase):
|
||||
ovs_agent.validate_local_ip(FAKE_IP1)
|
||||
mock_get_device_by_ip.assert_called_once_with(FAKE_IP1)
|
||||
|
||||
def test_validate_local_ip_with_invalid_ipv6(self):
|
||||
mock_get_device_by_ip = mock.patch.object(
|
||||
ip_lib.IPWrapper, 'get_device_by_ip').start()
|
||||
mock_get_device_by_ip.return_value = None
|
||||
with testtools.ExpectedException(SystemExit):
|
||||
ovs_agent.validate_local_ip(FAKE_IP6)
|
||||
mock_get_device_by_ip.assert_called_once_with(FAKE_IP6)
|
||||
|
||||
|
||||
class TestOvsAgentTunnelName(base.BaseTestCase):
|
||||
def test_get_ip_in_hex_invalid_address(self):
|
||||
def test_get_tunnel_hash_invalid_address(self):
|
||||
hashlen = n_const.DEVICE_NAME_MAX_LEN
|
||||
self.assertIsNone(
|
||||
ovs_agent.OVSNeutronAgent.get_ip_in_hex('a.b.c.d'))
|
||||
ovs_agent.OVSNeutronAgent.get_tunnel_hash('a.b.c.d', hashlen))
|
||||
|
||||
def test_get_tunnel_name_vxlan(self):
|
||||
self.assertEqual(
|
||||
@ -3040,3 +3089,15 @@ class TestOvsAgentTunnelName(base.BaseTestCase):
|
||||
'gre-7f000002',
|
||||
ovs_agent.OVSNeutronAgent.get_tunnel_name(
|
||||
'gre', '127.0.0.1', '127.0.0.2'))
|
||||
|
||||
def test_get_tunnel_name_vxlan_ipv6(self):
|
||||
self.assertEqual(
|
||||
'vxlan-pehtjzksi',
|
||||
ovs_agent.OVSNeutronAgent.get_tunnel_name(
|
||||
'vxlan', '2001:db8::1', '2001:db8::2'))
|
||||
|
||||
def test_get_tunnel_name_gre_ipv6(self):
|
||||
self.assertEqual(
|
||||
'gre-pehtjzksiqr',
|
||||
ovs_agent.OVSNeutronAgent.get_tunnel_name(
|
||||
'gre', '2001:db8::1', '2001:db8::2'))
|
||||
|
@ -0,0 +1,10 @@
|
||||
---
|
||||
prelude: >
|
||||
Support for IPv6 addresses as tunnel endpoints in OVS.
|
||||
features:
|
||||
- The local_ip value in ml2_conf.ini can now be set to
|
||||
an IPv6 address configured on the system.
|
||||
other:
|
||||
- Requires OVS 2.5+ version or higher with linux kernel
|
||||
4.3 or higher. More info at
|
||||
`OVS github page <https://github.com/openvswitch/ovs/blob/master/FAQ.md>`_.
|
Loading…
Reference in New Issue
Block a user