From 773394a1887bec6ab4c2ff0308f0e830e9a9089f Mon Sep 17 00:00:00 2001 From: Frode Nordahl Date: Mon, 14 Dec 2015 13:51:48 +0100 Subject: [PATCH] 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 --- .../openvswitch/agent/common/config.py | 5 +- .../openvswitch/agent/ovs_neutron_agent.py | 41 +++++++-- neutron/tests/common/agents/ovs_agent.py | 19 +++-- neutron/tests/functional/agent/l2/base.py | 5 +- .../functional/agent/test_l2_ovs_agent.py | 10 ++- .../tests/functional/agent/test_ovs_lib.py | 28 ++++-- .../agent/test_ovs_neutron_agent.py | 85 ++++++++++++++++--- ...pv6-tunnel-endpoints-f41b4954a04c43f6.yaml | 10 +++ 8 files changed, 162 insertions(+), 41 deletions(-) create mode 100644 releasenotes/notes/ovs-ipv6-tunnel-endpoints-f41b4954a04c43f6.yaml diff --git a/neutron/plugins/ml2/drivers/openvswitch/agent/common/config.py b/neutron/plugins/ml2/drivers/openvswitch/agent/common/config.py index bc442ec4bc5..8ef87353c33 100644 --- a/neutron/plugins/ml2/drivers/openvswitch/agent/common/config.py +++ b/neutron/plugins/ml2/drivers/openvswitch/agent/common/config.py @@ -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 : " diff --git a/neutron/plugins/ml2/drivers/openvswitch/agent/ovs_neutron_agent.py b/neutron/plugins/ml2/drivers/openvswitch/agent/ovs_neutron_agent.py index fdf6998afa5..cafc221750b 100644 --- a/neutron/plugins/ml2/drivers/openvswitch/agent/ovs_neutron_agent.py +++ b/neutron/plugins/ml2/drivers/openvswitch/agent/ovs_neutron_agent.py @@ -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 diff --git a/neutron/tests/common/agents/ovs_agent.py b/neutron/tests/common/agents/ovs_agent.py index b93bc3861e0..a0a4deeea34 100755 --- a/neutron/tests/common/agents/ovs_agent.py +++ b/neutron/tests/common/agents/ovs_agent.py @@ -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) diff --git a/neutron/tests/functional/agent/l2/base.py b/neutron/tests/functional/agent/l2/base.py index 37bf76ab9f6..955fd67415e 100644 --- a/neutron/tests/functional/agent/l2/base.py +++ b/neutron/tests/functional/agent/l2/base.py @@ -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() diff --git a/neutron/tests/functional/agent/test_l2_ovs_agent.py b/neutron/tests/functional/agent/test_l2_ovs_agent.py index 67fb48a97d9..15efcc269dd 100644 --- a/neutron/tests/functional/agent/test_l2_ovs_agent.py +++ b/neutron/tests/functional/agent/test_l2_ovs_agent.py @@ -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)) diff --git a/neutron/tests/functional/agent/test_ovs_lib.py b/neutron/tests/functional/agent/test_ovs_lib.py index e618c8fa262..07c9dd9fb76 100644 --- a/neutron/tests/functional/agent/test_ovs_lib.py +++ b/neutron/tests/functional/agent/test_ovs_lib.py @@ -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) diff --git a/neutron/tests/unit/plugins/ml2/drivers/openvswitch/agent/test_ovs_neutron_agent.py b/neutron/tests/unit/plugins/ml2/drivers/openvswitch/agent/test_ovs_neutron_agent.py index 8ec60b4d93b..298b239f85c 100644 --- a/neutron/tests/unit/plugins/ml2/drivers/openvswitch/agent/test_ovs_neutron_agent.py +++ b/neutron/tests/unit/plugins/ml2/drivers/openvswitch/agent/test_ovs_neutron_agent.py @@ -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')) diff --git a/releasenotes/notes/ovs-ipv6-tunnel-endpoints-f41b4954a04c43f6.yaml b/releasenotes/notes/ovs-ipv6-tunnel-endpoints-f41b4954a04c43f6.yaml new file mode 100644 index 00000000000..a31f7a8e53f --- /dev/null +++ b/releasenotes/notes/ovs-ipv6-tunnel-endpoints-f41b4954a04c43f6.yaml @@ -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 `_.