diff --git a/neutron/agent/linux/dhcp.py b/neutron/agent/linux/dhcp.py index 8d4d90a3593..951a4bf1543 100644 --- a/neutron/agent/linux/dhcp.py +++ b/neutron/agent/linux/dhcp.py @@ -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.""" diff --git a/neutron/tests/functional/agent/test_dhcp_agent.py b/neutron/tests/functional/agent/test_dhcp_agent.py index 86780202d5a..5063be0a19a 100644 --- a/neutron/tests/functional/agent/test_dhcp_agent.py +++ b/neutron/tests/functional/agent/test_dhcp_agent.py @@ -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() diff --git a/neutron/tests/unit/agent/dhcp/test_agent.py b/neutron/tests/unit/agent/dhcp/test_agent.py index 35cc7d12b21..9f21b785e26 100644 --- a/neutron/tests/unit/agent/dhcp/test_agent.py +++ b/neutron/tests/unit/agent/dhcp/test_agent.py @@ -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):