Add IPv6 default route to DHCP namespace
The DHCP namespace used to always have its IPv6 default
route configured from a received Router Advertisement (RA).
A recent change [1] disabled receipt of RAs, instead
relying on the network topology to configure the namespace.
Unfortunately the code only added an IPv4 default route,
which caused a regression with DNS resolution in some
circumstances where IPv6 was being used.
A default route is now added for both IP versions.
[1] https://review.openstack.org/#/c/386687/
Change-Id: I7c388f64c0aa9feb002f7a2faf76e7ccca30a3e7
Closes-bug: 1684682
(cherry picked from commit 7ad7584ce1
)
This commit is contained in:
parent
66073fa1f5
commit
afdc38f7c6
|
@ -1109,31 +1109,39 @@ class DeviceManager(object):
|
|||
return common_utils.get_dhcp_agent_device_id(network.id,
|
||||
self.conf.host)
|
||||
|
||||
def _set_default_route(self, network, device_name):
|
||||
"""Sets the default gateway for this dhcp namespace.
|
||||
|
||||
This method is idempotent and will only adjust the route if adjusting
|
||||
it would change it from what it already is. This makes it safe to call
|
||||
and avoids unnecessary perturbation of the system.
|
||||
"""
|
||||
def _set_default_route_ip_version(self, network, device_name, ip_version):
|
||||
device = ip_lib.IPDevice(device_name, namespace=network.namespace)
|
||||
gateway = device.route.get_gateway()
|
||||
gateway = device.route.get_gateway(ip_version=ip_version)
|
||||
if gateway:
|
||||
gateway = gateway.get('gateway')
|
||||
|
||||
for subnet in network.subnets:
|
||||
skip_subnet = (
|
||||
subnet.ip_version != 4
|
||||
subnet.ip_version != ip_version
|
||||
or not subnet.enable_dhcp
|
||||
or subnet.gateway_ip is None)
|
||||
|
||||
if skip_subnet:
|
||||
continue
|
||||
|
||||
if subnet.ip_version == constants.IP_VERSION_6:
|
||||
# This is duplicating some of the API checks already done,
|
||||
# but some of the functional tests call directly
|
||||
prefixlen = netaddr.IPNetwork(subnet.cidr).prefixlen
|
||||
if prefixlen == 0 or prefixlen > 126:
|
||||
continue
|
||||
modes = [constants.IPV6_SLAAC, constants.DHCPV6_STATELESS]
|
||||
addr_mode = getattr(subnet, 'ipv6_address_mode', None)
|
||||
ra_mode = getattr(subnet, 'ipv6_ra_mode', None)
|
||||
if (prefixlen != 64 and
|
||||
(addr_mode in modes or ra_mode in modes)):
|
||||
continue
|
||||
|
||||
if gateway != subnet.gateway_ip:
|
||||
LOG.debug('Setting gateway for dhcp netns on net %(n)s to '
|
||||
'%(ip)s',
|
||||
{'n': network.id, 'ip': subnet.gateway_ip})
|
||||
LOG.debug('Setting IPv%(version)s gateway for dhcp netns '
|
||||
'on net %(n)s to %(ip)s',
|
||||
{'n': network.id, 'ip': subnet.gateway_ip,
|
||||
'version': ip_version})
|
||||
|
||||
# Check for and remove the on-link route for the old
|
||||
# gateway being replaced, if it is outside the subnet
|
||||
|
@ -1141,12 +1149,8 @@ class DeviceManager(object):
|
|||
not ipam_utils.check_subnet_ip(
|
||||
subnet.cidr, gateway))
|
||||
if is_old_gateway_not_in_subnet:
|
||||
v4_onlink = device.route.list_onlink_routes(
|
||||
constants.IP_VERSION_4)
|
||||
v6_onlink = device.route.list_onlink_routes(
|
||||
constants.IP_VERSION_6)
|
||||
existing_onlink_routes = set(
|
||||
r['cidr'] for r in v4_onlink + v6_onlink)
|
||||
onlink = device.route.list_onlink_routes(ip_version)
|
||||
existing_onlink_routes = set(r['cidr'] for r in onlink)
|
||||
if gateway in existing_onlink_routes:
|
||||
device.route.delete_route(gateway, scope='link')
|
||||
|
||||
|
@ -1163,10 +1167,23 @@ class DeviceManager(object):
|
|||
# No subnets on the network have a valid gateway. Clean it up to avoid
|
||||
# confusion from seeing an invalid gateway here.
|
||||
if gateway is not None:
|
||||
LOG.debug('Removing gateway for dhcp netns on net %s', network.id)
|
||||
LOG.debug('Removing IPv%(version)s gateway for dhcp netns on '
|
||||
'net %(n)s',
|
||||
{'n': network.id, 'version': ip_version})
|
||||
|
||||
device.route.delete_gateway(gateway)
|
||||
|
||||
def _set_default_route(self, network, device_name):
|
||||
"""Sets the default gateway for this dhcp namespace.
|
||||
|
||||
This method is idempotent and will only adjust the route if adjusting
|
||||
it would change it from what it already is. This makes it safe to call
|
||||
and avoids unnecessary perturbation of the system.
|
||||
"""
|
||||
for ip_version in (constants.IP_VERSION_4, constants.IP_VERSION_6):
|
||||
self._set_default_route_ip_version(network, device_name,
|
||||
ip_version)
|
||||
|
||||
def _setup_existing_dhcp_port(self, network, device_id, dhcp_subnets):
|
||||
"""Set up the existing DHCP port, if there is one."""
|
||||
|
||||
|
|
|
@ -50,9 +50,9 @@ class DHCPAgentOVSTestFramework(base.BaseSudoTestCase):
|
|||
4: {'addr': '192.168.10.11',
|
||||
'cidr': '192.168.10.0/24',
|
||||
'gateway': '192.168.10.1'},
|
||||
6: {'addr': '0:0:0:0:0:ffff:c0a8:a0b',
|
||||
'cidr': '0:0:0:0:0:ffff:c0a8:a00/120',
|
||||
'gateway': '0:0:0:0:0:ffff:c0a8:a01'}, }
|
||||
6: {'addr': '2001:db8:0:1::c0a8:a0b',
|
||||
'cidr': '2001:db8:0:1::c0a8:a00/120',
|
||||
'gateway': '2001:db8:0:1::c0a8:a01'}, }
|
||||
|
||||
def setUp(self):
|
||||
super(DHCPAgentOVSTestFramework, self).setUp()
|
||||
|
|
|
@ -1342,6 +1342,11 @@ class FakePort1(object):
|
|||
self.id = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee'
|
||||
|
||||
|
||||
class FakePort2(object):
|
||||
def __init__(self):
|
||||
self.id = 'ffffffff-ffff-ffff-ffff-ffffffffffff'
|
||||
|
||||
|
||||
class FakeV4Subnet(object):
|
||||
def __init__(self):
|
||||
self.id = 'dddddddd-dddd-dddd-dddd-dddddddddddd'
|
||||
|
@ -1351,12 +1356,27 @@ class FakeV4Subnet(object):
|
|||
self.enable_dhcp = True
|
||||
|
||||
|
||||
class FakeV6Subnet(object):
|
||||
def __init__(self):
|
||||
self.id = 'ffffffff-ffff-ffff-ffff-ffffffffffff'
|
||||
self.ip_version = 6
|
||||
self.cidr = '2001:db8:0:1::/64'
|
||||
self.gateway_ip = '2001:db8:0:1::1'
|
||||
self.enable_dhcp = True
|
||||
|
||||
|
||||
class FakeV4SubnetOutsideGateway(FakeV4Subnet):
|
||||
def __init__(self):
|
||||
super(FakeV4SubnetOutsideGateway, self).__init__()
|
||||
self.gateway_ip = '192.168.1.1'
|
||||
|
||||
|
||||
class FakeV6SubnetOutsideGateway(FakeV6Subnet):
|
||||
def __init__(self):
|
||||
super(FakeV6SubnetOutsideGateway, self).__init__()
|
||||
self.gateway_ip = '2001:db8:1:1::1'
|
||||
|
||||
|
||||
class FakeV4SubnetNoGateway(object):
|
||||
def __init__(self):
|
||||
self.id = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee'
|
||||
|
@ -1366,6 +1386,15 @@ class FakeV4SubnetNoGateway(object):
|
|||
self.enable_dhcp = True
|
||||
|
||||
|
||||
class FakeV6SubnetNoGateway(object):
|
||||
def __init__(self):
|
||||
self.id = 'ffffffff-ffff-ffff-ffff-ffffffffffff'
|
||||
self.ip_version = 6
|
||||
self.cidr = '2001:db8:1:0::/64'
|
||||
self.gateway_ip = None
|
||||
self.enable_dhcp = True
|
||||
|
||||
|
||||
class FakeV4Network(object):
|
||||
def __init__(self):
|
||||
self.id = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
|
||||
|
@ -1374,24 +1403,39 @@ class FakeV4Network(object):
|
|||
self.namespace = 'qdhcp-aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
|
||||
|
||||
|
||||
class FakeDualNetwork(object):
|
||||
def __init__(self):
|
||||
self.id = 'dddddddd-dddd-dddd-dddd-dddddddddddd'
|
||||
self.subnets = [FakeV4Subnet(), FakeV6Subnet()]
|
||||
self.ports = [FakePort1(), FakePort2()]
|
||||
self.namespace = 'qdhcp-dddddddd-dddd-dddd-dddd-dddddddddddd'
|
||||
|
||||
|
||||
class FakeV4NetworkOutsideGateway(FakeV4Network):
|
||||
def __init__(self):
|
||||
super(FakeV4NetworkOutsideGateway, self).__init__()
|
||||
self.subnets = [FakeV4SubnetOutsideGateway()]
|
||||
|
||||
|
||||
class FakeV4NetworkNoSubnet(object):
|
||||
class FakeDualNetworkOutsideGateway(FakeDualNetwork):
|
||||
def __init__(self):
|
||||
super(FakeDualNetworkOutsideGateway, self).__init__()
|
||||
self.subnets = [FakeV4SubnetOutsideGateway(),
|
||||
FakeV6SubnetOutsideGateway()]
|
||||
|
||||
|
||||
class FakeDualNetworkNoSubnet(object):
|
||||
def __init__(self):
|
||||
self.id = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
|
||||
self.subnets = []
|
||||
self.ports = []
|
||||
|
||||
|
||||
class FakeV4NetworkNoGateway(object):
|
||||
class FakeDualNetworkNoGateway(object):
|
||||
def __init__(self):
|
||||
self.id = 'cccccccc-cccc-cccc-cccc-cccccccccccc'
|
||||
self.subnets = [FakeV4SubnetNoGateway()]
|
||||
self.ports = [FakePort1()]
|
||||
self.subnets = [FakeV4SubnetNoGateway(), FakeV6SubnetNoGateway()]
|
||||
self.ports = [FakePort1(), FakePort2()]
|
||||
|
||||
|
||||
class TestDeviceManager(base.BaseTestCase):
|
||||
|
@ -1744,7 +1788,7 @@ class TestDeviceManager(base.BaseTestCase):
|
|||
network = FakeV4Network()
|
||||
dh._set_default_route(network, 'tap-name')
|
||||
|
||||
self.assertEqual(1, device.route.get_gateway.call_count)
|
||||
self.assertEqual(2, device.route.get_gateway.call_count)
|
||||
self.assertFalse(device.route.delete_gateway.called)
|
||||
device.route.add_gateway.assert_called_once_with('192.168.0.1')
|
||||
|
||||
|
@ -1758,7 +1802,7 @@ class TestDeviceManager(base.BaseTestCase):
|
|||
network = FakeV4NetworkOutsideGateway()
|
||||
dh._set_default_route(network, 'tap-name')
|
||||
|
||||
self.assertEqual(1, device.route.get_gateway.call_count)
|
||||
self.assertEqual(2, device.route.get_gateway.call_count)
|
||||
self.assertFalse(device.route.delete_gateway.called)
|
||||
device.route.add_route.assert_called_once_with('192.168.1.1',
|
||||
scope='link')
|
||||
|
@ -1770,104 +1814,141 @@ class TestDeviceManager(base.BaseTestCase):
|
|||
device = mock.Mock()
|
||||
mock_IPDevice.return_value = device
|
||||
device.route.get_gateway.return_value = None
|
||||
network = FakeV4NetworkNoSubnet()
|
||||
network = FakeDualNetworkNoSubnet()
|
||||
network.namespace = 'qdhcp-1234'
|
||||
dh._set_default_route(network, 'tap-name')
|
||||
|
||||
self.assertEqual(1, device.route.get_gateway.call_count)
|
||||
self.assertEqual(2, device.route.get_gateway.call_count)
|
||||
self.assertFalse(device.route.delete_gateway.called)
|
||||
self.assertFalse(device.route.add_gateway.called)
|
||||
|
||||
def test_set_default_route_no_subnet_delete_gateway(self):
|
||||
dh = dhcp.DeviceManager(cfg.CONF, None)
|
||||
v4_gateway = '192.168.0.1'
|
||||
v6_gateway = '2001:db8:0:1::1'
|
||||
expected = [mock.call(v4_gateway),
|
||||
mock.call(v6_gateway)]
|
||||
with mock.patch.object(dhcp.ip_lib, 'IPDevice') as mock_IPDevice:
|
||||
device = mock.Mock()
|
||||
mock_IPDevice.return_value = device
|
||||
device.route.get_gateway.return_value = dict(gateway='192.168.0.1')
|
||||
network = FakeV4NetworkNoSubnet()
|
||||
device.route.get_gateway.side_effect = [
|
||||
dict(gateway=v4_gateway), dict(gateway=v6_gateway)]
|
||||
network = FakeDualNetworkNoSubnet()
|
||||
network.namespace = 'qdhcp-1234'
|
||||
dh._set_default_route(network, 'tap-name')
|
||||
|
||||
self.assertEqual(1, device.route.get_gateway.call_count)
|
||||
device.route.delete_gateway.assert_called_once_with('192.168.0.1')
|
||||
self.assertEqual(2, device.route.get_gateway.call_count)
|
||||
self.assertEqual(2, device.route.delete_gateway.call_count)
|
||||
device.route.delete_gateway.assert_has_calls(expected)
|
||||
self.assertFalse(device.route.add_gateway.called)
|
||||
|
||||
def test_set_default_route_no_gateway(self):
|
||||
dh = dhcp.DeviceManager(cfg.CONF, None)
|
||||
v4_gateway = '192.168.0.1'
|
||||
v6_gateway = '2001:db8:0:1::1'
|
||||
expected = [mock.call(v4_gateway),
|
||||
mock.call(v6_gateway)]
|
||||
with mock.patch.object(dhcp.ip_lib, 'IPDevice') as mock_IPDevice:
|
||||
device = mock.Mock()
|
||||
mock_IPDevice.return_value = device
|
||||
device.route.get_gateway.return_value = dict(gateway='192.168.0.1')
|
||||
network = FakeV4NetworkNoGateway()
|
||||
device.route.get_gateway.side_effect = [
|
||||
dict(gateway=v4_gateway), dict(gateway=v6_gateway)]
|
||||
network = FakeDualNetworkNoGateway()
|
||||
network.namespace = 'qdhcp-1234'
|
||||
dh._set_default_route(network, 'tap-name')
|
||||
|
||||
self.assertEqual(1, device.route.get_gateway.call_count)
|
||||
device.route.delete_gateway.assert_called_once_with('192.168.0.1')
|
||||
self.assertEqual(2, device.route.get_gateway.call_count)
|
||||
self.assertEqual(2, device.route.delete_gateway.call_count)
|
||||
device.route.delete_gateway.assert_has_calls(expected)
|
||||
self.assertFalse(device.route.add_gateway.called)
|
||||
|
||||
def test_set_default_route_do_nothing(self):
|
||||
dh = dhcp.DeviceManager(cfg.CONF, None)
|
||||
v4_gateway = '192.168.0.1'
|
||||
v6_gateway = '2001:db8:0:1::1'
|
||||
with mock.patch.object(dhcp.ip_lib, 'IPDevice') as mock_IPDevice:
|
||||
device = mock.Mock()
|
||||
mock_IPDevice.return_value = device
|
||||
device.route.get_gateway.return_value = dict(gateway='192.168.0.1')
|
||||
network = FakeV4Network()
|
||||
device.route.get_gateway.side_effect = [
|
||||
dict(gateway=v4_gateway), dict(gateway=v6_gateway)]
|
||||
network = FakeDualNetwork()
|
||||
dh._set_default_route(network, 'tap-name')
|
||||
|
||||
self.assertEqual(1, device.route.get_gateway.call_count)
|
||||
self.assertEqual(2, device.route.get_gateway.call_count)
|
||||
self.assertFalse(device.route.delete_gateway.called)
|
||||
self.assertFalse(device.route.add_gateway.called)
|
||||
|
||||
def test_set_default_route_change_gateway(self):
|
||||
dh = dhcp.DeviceManager(cfg.CONF, None)
|
||||
v4_gateway = '192.168.0.1'
|
||||
old_v4_gateway = '192.168.0.2'
|
||||
v6_gateway = '2001:db8:0:1::1'
|
||||
old_v6_gateway = '2001:db8:0:1::2'
|
||||
expected = [mock.call(v4_gateway),
|
||||
mock.call(v6_gateway)]
|
||||
with mock.patch.object(dhcp.ip_lib, 'IPDevice') as mock_IPDevice:
|
||||
device = mock.Mock()
|
||||
mock_IPDevice.return_value = device
|
||||
device.route.get_gateway.return_value = dict(gateway='192.168.0.2')
|
||||
network = FakeV4Network()
|
||||
device.route.get_gateway.side_effect = [
|
||||
dict(gateway=old_v4_gateway), dict(gateway=old_v6_gateway)]
|
||||
network = FakeDualNetwork()
|
||||
dh._set_default_route(network, 'tap-name')
|
||||
|
||||
self.assertEqual(1, device.route.get_gateway.call_count)
|
||||
self.assertEqual(2, device.route.get_gateway.call_count)
|
||||
self.assertFalse(device.route.delete_gateway.called)
|
||||
device.route.add_gateway.assert_called_once_with('192.168.0.1')
|
||||
device.route.add_gateway.assert_has_calls(expected)
|
||||
|
||||
def test_set_default_route_change_gateway_outside_subnet(self):
|
||||
dh = dhcp.DeviceManager(cfg.CONF, None)
|
||||
v4_gateway = '192.168.1.1'
|
||||
old_v4_gateway = '192.168.2.1'
|
||||
v6_gateway = '2001:db8:1:1::1'
|
||||
old_v6_gateway = '2001:db8:2:0::1'
|
||||
add_route_expected = [mock.call(v4_gateway, scope='link'),
|
||||
mock.call(v6_gateway, scope='link')]
|
||||
add_gw_expected = [mock.call(v4_gateway),
|
||||
mock.call(v6_gateway)]
|
||||
with mock.patch.object(dhcp.ip_lib, 'IPDevice') as mock_IPDevice:
|
||||
device = mock.Mock()
|
||||
mock_IPDevice.return_value = device
|
||||
device.route.list_onlink_routes.return_value = (
|
||||
[{'cidr': '192.168.2.1'}])
|
||||
device.route.get_gateway.return_value = dict(gateway='192.168.2.1')
|
||||
network = FakeV4NetworkOutsideGateway()
|
||||
device.route.list_onlink_routes.side_effect = [
|
||||
[{'cidr': old_v4_gateway}], []]
|
||||
device.route.get_gateway.side_effect = [
|
||||
dict(gateway=old_v4_gateway), dict(gateway=old_v6_gateway)]
|
||||
network = FakeDualNetworkOutsideGateway()
|
||||
dh._set_default_route(network, 'tap-name')
|
||||
|
||||
self.assertEqual(1, device.route.get_gateway.call_count)
|
||||
self.assertEqual(2, device.route.get_gateway.call_count)
|
||||
self.assertEqual(2, device.route.list_onlink_routes.call_count)
|
||||
self.assertFalse(device.route.delete_gateway.called)
|
||||
device.route.delete_route.assert_called_once_with('192.168.2.1',
|
||||
device.route.delete_route.assert_called_once_with(old_v4_gateway,
|
||||
scope='link')
|
||||
device.route.add_route.assert_called_once_with('192.168.1.1',
|
||||
scope='link')
|
||||
device.route.add_gateway.assert_called_once_with('192.168.1.1')
|
||||
device.route.add_route.assert_has_calls(add_route_expected)
|
||||
device.route.add_gateway.assert_has_calls(add_gw_expected)
|
||||
|
||||
def test_set_default_route_two_subnets(self):
|
||||
# Try two subnets. Should set gateway from the first.
|
||||
dh = dhcp.DeviceManager(cfg.CONF, None)
|
||||
v4_gateway = '192.168.1.1'
|
||||
v6_gateway = '2001:db8:1:1::1'
|
||||
expected = [mock.call(v4_gateway),
|
||||
mock.call(v6_gateway)]
|
||||
with mock.patch.object(dhcp.ip_lib, 'IPDevice') as mock_IPDevice:
|
||||
device = mock.Mock()
|
||||
mock_IPDevice.return_value = device
|
||||
device.route.get_gateway.return_value = None
|
||||
network = FakeV4Network()
|
||||
network = FakeDualNetwork()
|
||||
subnet2 = FakeV4Subnet()
|
||||
subnet2.gateway_ip = '192.168.1.1'
|
||||
network.subnets = [subnet2, FakeV4Subnet()]
|
||||
subnet2.gateway_ip = v4_gateway
|
||||
subnet3 = FakeV6Subnet()
|
||||
subnet3.gateway_ip = v6_gateway
|
||||
network.subnets = [subnet2, FakeV4Subnet(),
|
||||
subnet3, FakeV6Subnet()]
|
||||
dh._set_default_route(network, 'tap-name')
|
||||
|
||||
self.assertEqual(1, device.route.get_gateway.call_count)
|
||||
self.assertEqual(2, device.route.get_gateway.call_count)
|
||||
self.assertFalse(device.route.delete_gateway.called)
|
||||
device.route.add_gateway.assert_called_once_with('192.168.1.1')
|
||||
device.route.add_gateway.assert_has_calls(expected)
|
||||
|
||||
|
||||
class TestDictModel(base.BaseTestCase):
|
||||
|
|
Loading…
Reference in New Issue