]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
DHCP agent: clarify logic of setup_dhcp_port
authorNeil Jerram <Neil.Jerram@metaswitch.com>
Thu, 23 Jul 2015 17:17:12 +0000 (18:17 +0100)
committerNeil Jerram <Neil.Jerram@metaswitch.com>
Fri, 21 Aug 2015 11:54:33 +0000 (12:54 +0100)
When the DHCP port already exists, the code for finding it is
unhelpfully mixed up with the code for updating its subnet IDs and
fixed IP addresses.  Clarify that area by splitting setup_dhcp_port
into 3 subroutines, for each of the existing, reserved and new port
cases.

Related-Bug: #1486649
Change-Id: I2a537560dc7a37299f4b7b4cd508d9309bbe1209

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

index 0ac27b241a372021d8fbbb66fc7b18fbcbf4ac96..e562ab36db1effcf44a34227dd64971399054e64 100644 (file)
@@ -996,77 +996,111 @@ class DeviceManager(object):
 
             device.route.delete_gateway(gateway)
 
-    def setup_dhcp_port(self, network):
-        """Create/update DHCP port for the host if needed and return port."""
+    def _setup_existing_dhcp_port(self, network, device_id, dhcp_subnets):
+        """Set up the existing DHCP port, if there is one."""
 
-        device_id = self.get_device_id(network)
-        subnets = {subnet.id: subnet for subnet in network.subnets
-                   if subnet.enable_dhcp}
+        # To avoid pylint thinking that port might be undefined after
+        # the following loop...
+        port = None
 
-        dhcp_port = None
+        # Look for an existing DHCP for this network.
         for port in network.ports:
             port_device_id = getattr(port, 'device_id', None)
             if port_device_id == device_id:
-                dhcp_enabled_subnet_ids = set(subnets)
-                port_fixed_ips = []
-                for fixed_ip in port.fixed_ips:
-                    if fixed_ip.subnet_id in dhcp_enabled_subnet_ids:
-                        port_fixed_ips.append(
-                            {'subnet_id': fixed_ip.subnet_id,
-                             'ip_address': fixed_ip.ip_address})
-
-                port_subnet_ids = set(ip.subnet_id for ip in port.fixed_ips)
-                # If there is a new dhcp enabled subnet or a port that is no
-                # longer on a dhcp enabled subnet, we need to call update.
-                if dhcp_enabled_subnet_ids != port_subnet_ids:
-                    port_fixed_ips.extend(
-                        dict(subnet_id=s)
-                        for s in dhcp_enabled_subnet_ids - port_subnet_ids)
-                    dhcp_port = self.plugin.update_dhcp_port(
-                        port.id, {'port': {'network_id': network.id,
-                                           'fixed_ips': port_fixed_ips}})
-                    if not dhcp_port:
-                        raise exceptions.Conflict()
-                else:
-                    dhcp_port = port
-                # break since we found port that matches device_id
                 break
+        else:
+            return None
+
+        # Compare what the subnets should be against what is already
+        # on the port.
+        dhcp_enabled_subnet_ids = set(dhcp_subnets)
+        port_subnet_ids = set(ip.subnet_id for ip in port.fixed_ips)
+
+        # If those differ, we need to call update.
+        if dhcp_enabled_subnet_ids != port_subnet_ids:
+            # Collect the subnets and fixed IPs that the port already
+            # has, for subnets that are still in the DHCP-enabled set.
+            wanted_fixed_ips = []
+            for fixed_ip in port.fixed_ips:
+                if fixed_ip.subnet_id in dhcp_enabled_subnet_ids:
+                    wanted_fixed_ips.append(
+                        {'subnet_id': fixed_ip.subnet_id,
+                         'ip_address': fixed_ip.ip_address})
+
+            # Add subnet IDs for new DHCP-enabled subnets.
+            wanted_fixed_ips.extend(
+                dict(subnet_id=s)
+                for s in dhcp_enabled_subnet_ids - port_subnet_ids)
+
+            # Update the port to have the calculated subnets and fixed
+            # IPs.  The Neutron server will allocate a fresh IP for
+            # each subnet that doesn't already have one.
+            port = self.plugin.update_dhcp_port(
+                port.id,
+                {'port': {'network_id': network.id,
+                          'fixed_ips': wanted_fixed_ips}})
+            if not port:
+                raise exceptions.Conflict()
+
+        return port
+
+    def _setup_reserved_dhcp_port(self, network, device_id, dhcp_subnets):
+        """Setup the reserved DHCP port, if there is one."""
+        LOG.debug('DHCP port %(device_id)s on network %(network_id)s'
+                  ' does not yet exist. Checking for a reserved port.',
+                  {'device_id': device_id, 'network_id': network.id})
+        for port in network.ports:
+            port_device_id = getattr(port, 'device_id', None)
+            if port_device_id == constants.DEVICE_ID_RESERVED_DHCP_PORT:
+                port = self.plugin.update_dhcp_port(
+                    port.id, {'port': {'network_id': network.id,
+                                       'device_id': device_id}})
+                if port:
+                    return port
+
+    def _setup_new_dhcp_port(self, network, device_id, dhcp_subnets):
+        """Create and set up new DHCP port for the specified network."""
+        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})
+        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])
+        return self.plugin.create_dhcp_port({'port': port_dict})
 
-        # check for a reserved DHCP port
-        if dhcp_port is None:
-            LOG.debug('DHCP port %(device_id)s on network %(network_id)s'
-                      ' does not yet exist. Checking for a reserved port.',
-                      {'device_id': device_id, 'network_id': network.id})
-            for port in network.ports:
-                port_device_id = getattr(port, 'device_id', None)
-                if port_device_id == constants.DEVICE_ID_RESERVED_DHCP_PORT:
-                    dhcp_port = self.plugin.update_dhcp_port(
-                        port.id, {'port': {'network_id': network.id,
-                                           'device_id': device_id}})
-                    if dhcp_port:
-                        break
-
-        # DHCP port has not yet been created.
-        if dhcp_port is None:
-            LOG.debug('DHCP port %(device_id)s on network %(network_id)s'
-                      ' does not yet exist.', {'device_id': device_id,
-                                               'network_id': network.id})
-            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 subnets])
-            dhcp_port = self.plugin.create_dhcp_port({'port': port_dict})
-
-        if not dhcp_port:
+    def setup_dhcp_port(self, network):
+        """Create/update DHCP port for the host if needed and return port."""
+
+        # The ID that the DHCP port will have (or already has).
+        device_id = self.get_device_id(network)
+
+        # Get the set of DHCP-enabled subnets on this network.
+        dhcp_subnets = {subnet.id: subnet for subnet in network.subnets
+                        if subnet.enable_dhcp}
+
+        # There are 3 cases: either the DHCP port already exists (but
+        # might need to be updated for a changed set of subnets); or
+        # some other code has already prepared a 'reserved' DHCP port,
+        # and we just need to adopt that; or we need to create a new
+        # DHCP port.  Try each of those in turn until we have a DHCP
+        # port.
+        for setup_method in (self._setup_existing_dhcp_port,
+                             self._setup_reserved_dhcp_port,
+                             self._setup_new_dhcp_port):
+            dhcp_port = setup_method(network, device_id, dhcp_subnets)
+            if dhcp_port:
+                break
+        else:
             raise exceptions.Conflict()
 
         # Convert subnet_id to subnet dict
         fixed_ips = [dict(subnet_id=fixed_ip.subnet_id,
                           ip_address=fixed_ip.ip_address,
-                          subnet=subnets[fixed_ip.subnet_id])
+                          subnet=dhcp_subnets[fixed_ip.subnet_id])
                      for fixed_ip in dhcp_port.fixed_ips]
 
         ips = [DictModel(item) if isinstance(item, dict) else item
index 1e2631dae4d70decf7cdf8684139369d51cc765e..60c241d8aede19bc90679682bac4cbcee97e7546 100644 (file)
@@ -48,6 +48,13 @@ class DhcpOpt(object):
         return str(self.__dict__)
 
 
+# A base class where class attributes can also be accessed by treating
+# an instance as a dict.
+class Dictable(object):
+    def __getitem__(self, k):
+        return self.__class__.__dict__.get(k)
+
+
 class FakeDhcpPort(object):
     id = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa'
     admin_state_up = True
@@ -61,6 +68,19 @@ class FakeDhcpPort(object):
         self.extra_dhcp_opts = []
 
 
+class FakeReservedPort(object):
+    admin_state_up = True
+    device_owner = 'network:dhcp'
+    fixed_ips = [FakeIPAllocation('192.168.0.6',
+                                  'dddddddd-dddd-dddd-dddd-dddddddddddd')]
+    mac_address = '00:00:80:aa:bb:ee'
+    device_id = constants.DEVICE_ID_RESERVED_DHCP_PORT
+
+    def __init__(self, id='reserved-aaaa-aaaa-aaaa-aaaaaaaaaaa'):
+        self.extra_dhcp_opts = []
+        self.id = id
+
+
 class FakePort1(object):
     id = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee'
     admin_state_up = True
@@ -283,7 +303,7 @@ class FakeV6HostRoute(object):
     nexthop = '2001:0200:feed:7ac0::1'
 
 
-class FakeV4Subnet(object):
+class FakeV4Subnet(Dictable):
     id = 'dddddddd-dddd-dddd-dddd-dddddddddddd'
     ip_version = 4
     cidr = '192.168.0.0/24'
@@ -400,7 +420,7 @@ class FakeV4SubnetNoDHCP(object):
     dns_nameservers = []
 
 
-class FakeV6SubnetDHCPStateful(object):
+class FakeV6SubnetDHCPStateful(Dictable):
     id = 'ffffffff-ffff-ffff-ffff-ffffffffffff'
     ip_version = 6
     cidr = 'fdca:3ba5:a17a:4ba3::/64'
@@ -483,6 +503,29 @@ class FakeDualNetwork(object):
     namespace = 'qdhcp-ns'
 
 
+class FakeDeviceManagerNetwork(object):
+    id = 'cccccccc-cccc-cccc-cccc-cccccccccccc'
+    subnets = [FakeV4Subnet(), FakeV6SubnetDHCPStateful()]
+    ports = [FakePort1(), FakeV6Port(), FakeDualPort(), FakeRouterPort()]
+    namespace = 'qdhcp-ns'
+
+
+class FakeDualNetworkReserved(object):
+    id = 'cccccccc-cccc-cccc-cccc-cccccccccccc'
+    subnets = [FakeV4Subnet(), FakeV6SubnetDHCPStateful()]
+    ports = [FakePort1(), FakeV6Port(), FakeDualPort(), FakeRouterPort(),
+             FakeReservedPort()]
+    namespace = 'qdhcp-ns'
+
+
+class FakeDualNetworkReserved2(object):
+    id = 'cccccccc-cccc-cccc-cccc-cccccccccccc'
+    subnets = [FakeV4Subnet(), FakeV6SubnetDHCPStateful()]
+    ports = [FakePort1(), FakeV6Port(), FakeDualPort(), FakeRouterPort(),
+             FakeReservedPort(), FakeReservedPort(id='reserved-2')]
+    namespace = 'qdhcp-ns'
+
+
 class FakeNetworkDhcpPort(object):
     id = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
     subnets = [FakeV4Subnet()]
@@ -714,9 +757,9 @@ class LocalChild(dhcp.DhcpLocalProcess):
         self.called.append('spawn')
 
 
-class TestBase(base.BaseTestCase):
+class TestConfBase(base.BaseTestCase):
     def setUp(self):
-        super(TestBase, self).setUp()
+        super(TestConfBase, self).setUp()
         self.conf = config.setup_conf()
         self.conf.register_opts(base_config.core_opts)
         self.conf.register_opts(dhcp_config.DHCP_OPTS)
@@ -724,6 +767,11 @@ class TestBase(base.BaseTestCase):
         self.conf.register_opts(external_process.OPTS)
         config.register_interface_driver_opts_helper(self.conf)
         config.register_use_namespaces_opts_helper(self.conf)
+
+
+class TestBase(TestConfBase):
+    def setUp(self):
+        super(TestBase, self).setUp()
         instance = mock.patch("neutron.agent.linux.dhcp.DeviceManager")
         self.mock_mgr = instance.start()
         self.conf.register_opt(cfg.BoolOpt('enable_isolated_metadata',
@@ -1829,3 +1877,138 @@ class TestDnsmasq(TestBase):
         self.conf.set_override('enable_metadata_network', True)
         self.assertTrue(dhcp.Dnsmasq.should_enable_metadata(
             self.conf, FakeV4MetadataNetwork()))
+
+
+class TestDeviceManager(TestConfBase):
+
+    @mock.patch('neutron.agent.linux.dhcp.ip_lib')
+    @mock.patch('neutron.agent.linux.dhcp.common_utils.load_interface_driver')
+    def test_setup(self, load_interface_driver, ip_lib):
+        """Test new and existing cases of DeviceManager's DHCP port setup
+        logic.
+        """
+
+        # Create DeviceManager.
+        self.conf.register_opt(cfg.BoolOpt('enable_isolated_metadata',
+                                           default=False))
+        plugin = mock.Mock()
+        mgr = dhcp.DeviceManager(self.conf, plugin)
+        load_interface_driver.assert_called_with(self.conf)
+
+        # Setup with no existing DHCP port - expect a new DHCP port to
+        # be created.
+        network = FakeDeviceManagerNetwork()
+        network.tenant_id = 'Tenant A'
+
+        def mock_create(dict):
+            port = dhcp.DictModel(dict['port'])
+            port.id = 'abcd-123456789'
+            port.mac_address = '00-12-34-56-78-90'
+            port.fixed_ips = [
+                dhcp.DictModel({'subnet_id': ip['subnet_id'],
+                                'ip_address': 'unique-IP-address'})
+                for ip in port.fixed_ips
+            ]
+            return port
+
+        plugin.create_dhcp_port.side_effect = mock_create
+        mgr.driver.get_device_name.return_value = 'ns-XXX'
+        ip_lib.ensure_device_is_ready.return_value = True
+        mgr.setup(network)
+        plugin.create_dhcp_port.assert_called_with(mock.ANY)
+
+        mgr.driver.init_l3.assert_called_with('ns-XXX',
+                                              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']))
+
+        # Now call setup again.  This time we go through the existing
+        # port code path, and the driver's init_l3 method is called
+        # again.
+        plugin.create_dhcp_port.reset_mock()
+        mgr.driver.init_l3.reset_mock()
+        mgr.setup(network)
+        mgr.driver.init_l3.assert_called_with('ns-XXX',
+                                              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']))
+        self.assertFalse(plugin.create_dhcp_port.called)
+
+    @mock.patch('neutron.agent.linux.dhcp.ip_lib')
+    @mock.patch('neutron.agent.linux.dhcp.common_utils.load_interface_driver')
+    def test_setup_reserved(self, load_interface_driver, ip_lib):
+        """Test reserved port case of DeviceManager's DHCP port setup
+        logic.
+        """
+
+        # Create DeviceManager.
+        self.conf.register_opt(cfg.BoolOpt('enable_isolated_metadata',
+                                           default=False))
+        plugin = mock.Mock()
+        mgr = dhcp.DeviceManager(self.conf, plugin)
+        load_interface_driver.assert_called_with(self.conf)
+
+        # Setup with a reserved DHCP port.
+        network = FakeDualNetworkReserved()
+        network.tenant_id = 'Tenant A'
+        reserved_port = network.ports[-1]
+
+        def mock_update(port_id, dict):
+            port = reserved_port
+            port.network_id = dict['port']['network_id']
+            port.device_id = dict['port']['device_id']
+            return port
+
+        plugin.update_dhcp_port.side_effect = mock_update
+        mgr.driver.get_device_name.return_value = 'ns-XXX'
+        ip_lib.ensure_device_is_ready.return_value = True
+        mgr.setup(network)
+        plugin.update_dhcp_port.assert_called_with(reserved_port.id, mock.ANY)
+
+        mgr.driver.init_l3.assert_called_with('ns-XXX',
+                                              ['192.168.0.6/24'],
+                                              namespace='qdhcp-ns')
+
+    @mock.patch('neutron.agent.linux.dhcp.ip_lib')
+    @mock.patch('neutron.agent.linux.dhcp.common_utils.load_interface_driver')
+    def test_setup_reserved_2(self, load_interface_driver, ip_lib):
+        """Test scenario where a network has two reserved ports, and
+        update_dhcp_port fails for the first of those.
+        """
+
+        # Create DeviceManager.
+        self.conf.register_opt(cfg.BoolOpt('enable_isolated_metadata',
+                                           default=False))
+        plugin = mock.Mock()
+        mgr = dhcp.DeviceManager(self.conf, plugin)
+        load_interface_driver.assert_called_with(self.conf)
+
+        # Setup with a reserved DHCP port.
+        network = FakeDualNetworkReserved2()
+        network.tenant_id = 'Tenant A'
+        reserved_port_1 = network.ports[-2]
+        reserved_port_2 = network.ports[-1]
+
+        def mock_update(port_id, dict):
+            if port_id == reserved_port_1.id:
+                return None
+
+            port = reserved_port_2
+            port.network_id = dict['port']['network_id']
+            port.device_id = dict['port']['device_id']
+            return port
+
+        plugin.update_dhcp_port.side_effect = mock_update
+        mgr.driver.get_device_name.return_value = 'ns-XXX'
+        ip_lib.ensure_device_is_ready.return_value = True
+        mgr.setup(network)
+        plugin.update_dhcp_port.assert_called_with(reserved_port_2.id,
+                                                   mock.ANY)
+
+        mgr.driver.init_l3.assert_called_with('ns-XXX',
+                                              ['192.168.0.6/24'],
+                                              namespace='qdhcp-ns')