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:
Brian Haley 2017-05-02 15:25:17 -04:00 committed by Brian Haley
parent 5832b4cc41
commit b124e6835e
3 changed files with 157 additions and 59 deletions

View File

@ -1111,31 +1111,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
@ -1143,12 +1151,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')
@ -1165,10 +1169,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."""

View File

@ -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()

View File

@ -1343,6 +1343,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'
@ -1352,12 +1357,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'
@ -1367,6 +1387,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'
@ -1375,24 +1404,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):
@ -1747,7 +1791,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')
@ -1761,7 +1805,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')
@ -1773,104 +1817,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):