]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
DHCP agent: allow using gateway IPs instead of uniquely allocated
authorNeil Jerram <Neil.Jerram@metaswitch.com>
Mon, 27 Jul 2015 13:41:29 +0000 (14:41 +0100)
committerNeil Jerram <Neil.Jerram@metaswitch.com>
Tue, 1 Sep 2015 22:46:15 +0000 (23:46 +0100)
In each place where the DHCP agent runs, and for each subnet for which
DHCP is handing out IP addresses, the DHCP port needs - at the Linux
level - to have an IP address within that subnet.  Generally this
needs to be a unique Neutron-allocated IP address, because the
subnet's underlying L2 domain is bridged across multiple compute hosts
and network nodes, and for HA there may be multiple DHCP agents
running on that same bridged L2 domain.

However, if the DHCP ports - on multiple compute/network nodes but for
the same network - are _not_ bridged to each other, they do not need
each to have a unique IP address.  Instead they can all share the same
address from the relevant subnet.  This works, without creating any
ambiguity, because those ports are not all present on the same L2
domain, and because no data within the network is ever sent to that
address.  (DHCP requests are broadcast, and it is the network's job to
ensure that such a broadcast will reach at least one of the available
DHCP servers.  DHCP responses will be sent _from_ the DHCP port
address.)

Specifically, for some networking backends it makes sense to allow all
DHCP ports to use the subnet's gateway IP address, and thereby to
completely avoid any unique IP address allocation.

This change therefore enhances the DHCP agent code to be able to use
gateway IPs as an alternative to uniquely allocated ones, with the
choice between those being made by a new interface driver property,
'use_gateway_ips'.  The back-compatible default is to use unique IPs.
An interface driver that wants the DHCP agent to use gateway IPs can
achieve that by overriding as follows:

    @property
    def use_gateway_ips(self):
        return True

Partial-Bug: #1486649
Change-Id: I17e1dc9231a5ec35bd6f84c4c7aca6350d76e8ec

neutron/agent/linux/dhcp.py
neutron/agent/linux/interface.py
neutron/tests/unit/agent/dhcp/test_agent.py
neutron/tests/unit/agent/linux/test_dhcp.py

index e562ab36db1effcf44a34227dd64971399054e64..e5cb8e8dbc7936570f82212c83a487127b11a107 100644 (file)
@@ -1003,10 +1003,18 @@ class DeviceManager(object):
         # the following loop...
         port = None
 
-        # Look for an existing DHCP for this network.
+        # Look for an existing DHCP port for this network.
         for port in network.ports:
             port_device_id = getattr(port, 'device_id', None)
             if port_device_id == device_id:
+                # If using gateway IPs on this port, we can skip the
+                # following code, whose purpose is just to review and
+                # update the Neutron-allocated IP addresses for the
+                # port.
+                if self.driver.use_gateway_ips:
+                    return port
+                # Otherwise break out, as we now have the DHCP port
+                # whose subnets and addresses we need to review.
                 break
         else:
             return None
@@ -1063,13 +1071,21 @@ class DeviceManager(object):
         LOG.debug('DHCP port %(device_id)s on network %(network_id)s'
                   ' does not yet exist. Creating new one.',
                   {'device_id': device_id, 'network_id': network.id})
+
+        # Make a list of the subnets that need a unique IP address for
+        # this DHCP port.
+        if self.driver.use_gateway_ips:
+            unique_ip_subnets = []
+        else:
+            unique_ip_subnets = [dict(subnet_id=s) for s in dhcp_subnets]
+
         port_dict = dict(
             name='',
             admin_state_up=True,
             device_id=device_id,
             network_id=network.id,
             tenant_id=network.tenant_id,
-            fixed_ips=[dict(subnet_id=s) for s in dhcp_subnets])
+            fixed_ips=unique_ip_subnets)
         return self.plugin.create_dhcp_port({'port': port_dict})
 
     def setup_dhcp_port(self, network):
@@ -1141,6 +1157,17 @@ class DeviceManager(object):
                 ip_cidr = '%s/%s' % (fixed_ip.ip_address, net.prefixlen)
                 ip_cidrs.append(ip_cidr)
 
+        if self.driver.use_gateway_ips:
+            # For each DHCP-enabled subnet, add that subnet's gateway
+            # IP address to the Linux device for the DHCP port.
+            for subnet in network.subnets:
+                if not subnet.enable_dhcp:
+                    continue
+                gateway = subnet.gateway_ip
+                if gateway:
+                    net = netaddr.IPNetwork(subnet.cidr)
+                    ip_cidrs.append('%s/%s' % (gateway, net.prefixlen))
+
         if (self.conf.enable_isolated_metadata and
             self.conf.use_namespaces):
             ip_cidrs.append(METADATA_DEFAULT_CIDR)
index 9207503e7ac4dfcd79038d88f9d109b20747ac7b..5ac117e67d27b50e554d4189e76bca6675af0c8f 100644 (file)
@@ -52,6 +52,46 @@ class LinuxInterfaceDriver(object):
     def __init__(self, conf):
         self.conf = conf
 
+    @property
+    def use_gateway_ips(self):
+        """Whether to use gateway IPs instead of unique IP allocations.
+
+        In each place where the DHCP agent runs, and for each subnet for
+        which DHCP is handling out IP addresses, the DHCP port needs -
+        at the Linux level - to have an IP address within that subnet.
+        Generally this needs to be a unique Neutron-allocated IP
+        address, because the subnet's underlying L2 domain is bridged
+        across multiple compute hosts and network nodes, and for HA
+        there may be multiple DHCP agents running on that same bridged
+        L2 domain.
+
+        However, if the DHCP ports - on multiple compute/network nodes
+        but for the same network - are _not_ bridged to each other,
+        they do not need each to have a unique IP address.  Instead
+        they can all share the same address from the relevant subnet.
+        This works, without creating any ambiguity, because those
+        ports are not all present on the same L2 domain, and because
+        no data within the network is ever sent to that address.
+        (DHCP requests are broadcast, and it is the network's job to
+        ensure that such a broadcast will reach at least one of the
+        available DHCP servers.  DHCP responses will be sent _from_
+        the DHCP port address.)
+
+        Specifically, for networking backends where it makes sense,
+        the DHCP agent allows all DHCP ports to use the subnet's
+        gateway IP address, and thereby to completely avoid any unique
+        IP address allocation.  This behaviour is selected by running
+        the DHCP agent with a configured interface driver whose
+        'use_gateway_ips' property is True.
+
+        When an operator deploys Neutron with an interface driver that
+        makes use_gateway_ips True, they should also ensure that a
+        gateway IP address is defined for each DHCP-enabled subnet,
+        and that the gateway IP address doesn't change during the
+        subnet's lifetime.
+        """
+        return False
+
     def init_l3(self, device_name, ip_cidrs, namespace=None,
                 preserve_ips=[], gateway_ips=None,
                 clean_connections=False):
index d1c7226d8404ddd4adb62c2d5262d285b45f4f0c..74b7f3b1089723eef9b9a47e4f78f3e37bed12ad 100644 (file)
@@ -1193,6 +1193,7 @@ class TestDeviceManager(base.BaseTestCase):
         self.mock_driver = mock.MagicMock()
         self.mock_driver.DEV_NAME_LEN = (
             interface.LinuxInterfaceDriver.DEV_NAME_LEN)
+        self.mock_driver.use_gateway_ips = False
         self.mock_iproute = mock.MagicMock()
         driver_cls.return_value = self.mock_driver
         iproute_cls.return_value = self.mock_iproute
index 60c241d8aede19bc90679682bac4cbcee97e7546..5508b01d21d33908acd3cc88f3d816a4a469ec16 100644 (file)
@@ -504,10 +504,17 @@ class FakeDualNetwork(object):
 
 
 class FakeDeviceManagerNetwork(object):
-    id = 'cccccccc-cccc-cccc-cccc-cccccccccccc'
-    subnets = [FakeV4Subnet(), FakeV6SubnetDHCPStateful()]
-    ports = [FakePort1(), FakeV6Port(), FakeDualPort(), FakeRouterPort()]
-    namespace = 'qdhcp-ns'
+    # Use instance rather than class attributes here, so that we get
+    # an independent set of ports each time FakeDeviceManagerNetwork()
+    # is used.
+    def __init__(self):
+        self.id = 'cccccccc-cccc-cccc-cccc-cccccccccccc'
+        self.subnets = [FakeV4Subnet(), FakeV6SubnetDHCPStateful()]
+        self.ports = [FakePort1(),
+                      FakeV6Port(),
+                      FakeDualPort(),
+                      FakeRouterPort()]
+        self.namespace = 'qdhcp-ns'
 
 
 class FakeDualNetworkReserved(object):
@@ -1887,7 +1894,17 @@ class TestDeviceManager(TestConfBase):
         """Test new and existing cases of DeviceManager's DHCP port setup
         logic.
         """
+        self._test_setup(load_interface_driver, ip_lib, False)
 
+    @mock.patch('neutron.agent.linux.dhcp.ip_lib')
+    @mock.patch('neutron.agent.linux.dhcp.common_utils.load_interface_driver')
+    def test_setup_gateway_ips(self, load_interface_driver, ip_lib):
+        """Test new and existing cases of DeviceManager's DHCP port setup
+        logic.
+        """
+        self._test_setup(load_interface_driver, ip_lib, True)
+
+    def _test_setup(self, load_interface_driver, ip_lib, use_gateway_ips):
         # Create DeviceManager.
         self.conf.register_opt(cfg.BoolOpt('enable_isolated_metadata',
                                            default=False))
@@ -1913,6 +1930,7 @@ class TestDeviceManager(TestConfBase):
 
         plugin.create_dhcp_port.side_effect = mock_create
         mgr.driver.get_device_name.return_value = 'ns-XXX'
+        mgr.driver.use_gateway_ips = use_gateway_ips
         ip_lib.ensure_device_is_ready.return_value = True
         mgr.setup(network)
         plugin.create_dhcp_port.assert_called_with(mock.ANY)
@@ -1921,8 +1939,13 @@ class TestDeviceManager(TestConfBase):
                                               mock.ANY,
                                               namespace='qdhcp-ns')
         cidrs = set(mgr.driver.init_l3.call_args[0][1])
-        self.assertEqual(cidrs, set(['unique-IP-address/24',
-                                     'unique-IP-address/64']))
+        if use_gateway_ips:
+            self.assertEqual(cidrs, set(['%s/%s' % (s.gateway_ip,
+                                                    s.cidr.split('/')[1])
+                                         for s in network.subnets]))
+        else:
+            self.assertEqual(cidrs, set(['unique-IP-address/24',
+                                         'unique-IP-address/64']))
 
         # Now call setup again.  This time we go through the existing
         # port code path, and the driver's init_l3 method is called
@@ -1934,8 +1957,13 @@ class TestDeviceManager(TestConfBase):
                                               mock.ANY,
                                               namespace='qdhcp-ns')
         cidrs = set(mgr.driver.init_l3.call_args[0][1])
-        self.assertEqual(cidrs, set(['unique-IP-address/24',
-                                     'unique-IP-address/64']))
+        if use_gateway_ips:
+            self.assertEqual(cidrs, set(['%s/%s' % (s.gateway_ip,
+                                                    s.cidr.split('/')[1])
+                                         for s in network.subnets]))
+        else:
+            self.assertEqual(cidrs, set(['unique-IP-address/24',
+                                         'unique-IP-address/64']))
         self.assertFalse(plugin.create_dhcp_port.called)
 
     @mock.patch('neutron.agent.linux.dhcp.ip_lib')
@@ -1965,6 +1993,7 @@ class TestDeviceManager(TestConfBase):
 
         plugin.update_dhcp_port.side_effect = mock_update
         mgr.driver.get_device_name.return_value = 'ns-XXX'
+        mgr.driver.use_gateway_ips = False
         ip_lib.ensure_device_is_ready.return_value = True
         mgr.setup(network)
         plugin.update_dhcp_port.assert_called_with(reserved_port.id, mock.ANY)
@@ -2004,6 +2033,7 @@ class TestDeviceManager(TestConfBase):
 
         plugin.update_dhcp_port.side_effect = mock_update
         mgr.driver.get_device_name.return_value = 'ns-XXX'
+        mgr.driver.use_gateway_ips = False
         ip_lib.ensure_device_is_ready.return_value = True
         mgr.setup(network)
         plugin.update_dhcp_port.assert_called_with(reserved_port_2.id,