]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
add non-routed subnet metadata support
authorMark McClain <mark.mcclain@dreamhost.com>
Tue, 5 Feb 2013 04:59:59 +0000 (23:59 -0500)
committerMark McClain <mark.mcclain@dreamhost.com>
Fri, 8 Feb 2013 05:05:51 +0000 (00:05 -0500)
implements blueprint metadata-non-routed

This patchset completes Quantum metadata support by adding metadata
proxy support for isolated network segments.  The support requires that
the guest instance request host routes, so that the DHCP port can be
used to proxy metadata requests. NOTE: The cirros image does not support
host router, so the UEC or equivalent required for testing and usage.

Change-Id: I962deef7c164ecb2a93b7af326ef8dca6e2b183a

etc/dhcp_agent.ini
quantum/agent/dhcp_agent.py
quantum/agent/linux/dhcp.py
quantum/tests/unit/test_dhcp_agent.py
quantum/tests/unit/test_linux_dhcp.py

index 3ec8a82fbcdfff4db0a2052bf2530dbf8d86d32c..1d3eef0d3574bf2ab0644628a96d8aa3e928aa9e 100644 (file)
@@ -29,3 +29,10 @@ dhcp_driver = quantum.agent.linux.dhcp.Dnsmasq
 # Allow overlapping IP (Must have kernel build with CONFIG_NET_NS=y and
 # iproute2 package that supports namespaces).
 # use_namespaces = True
+
+#  The DHCP server can assist with providing metadata support on isolated
+#  networks. Setting this value to True will cause the DHCP server to append
+#  specific host routes to the DHCP request.  The metadata service will only
+#  be activated when the subnet gateway_ip is None.  The guest instance must
+#  be configured to request host routes via DHCP (Option 121).
+# enable_isolated_metadata = False
index b3ef9d7d5f557d05b3c5ee63a373140c40976701..5631f4235f92005444bd2a1f4c8513b5d575f120 100644 (file)
@@ -24,6 +24,7 @@ import netaddr
 
 from quantum.agent.common import config
 from quantum.agent.linux import dhcp
+from quantum.agent.linux import external_process
 from quantum.agent.linux import interface
 from quantum.agent.linux import ip_lib
 from quantum.agent import rpc as agent_rpc
@@ -39,6 +40,8 @@ from quantum.openstack.common import uuidutils
 
 LOG = logging.getLogger(__name__)
 NS_PREFIX = 'qdhcp-'
+METADATA_DEFAULT_IP = '169.254.169.254/16'
+METADATA_PORT = 80
 
 
 class DhcpAgent(object):
@@ -49,7 +52,9 @@ class DhcpAgent(object):
                    default='quantum.agent.linux.dhcp.Dnsmasq',
                    help=_("The driver used to manage the DHCP server.")),
         cfg.BoolOpt('use_namespaces', default=True,
-                    help=_("Allow overlapping IP."))
+                    help=_("Allow overlapping IP.")),
+        cfg.BoolOpt('enable_isolated_metadata', default=False,
+                    help=_("Support Metadata requests on isolated networks."))
     ]
 
     def __init__(self, conf):
@@ -73,12 +78,12 @@ class DhcpAgent(object):
         self.lease_relay.start()
         self.notifications.run_dispatch(self)
 
+    def _ns_name(self, network):
+        if self.conf.use_namespaces:
+            return NS_PREFIX + network.id
+
     def call_driver(self, action, network):
         """Invoke an action on a DHCP driver instance."""
-        if self.conf.use_namespaces:
-            namespace = NS_PREFIX + network.id
-        else:
-            namespace = None
         try:
             # the Driver expects something that is duck typed similar to
             # the base models.
@@ -86,7 +91,7 @@ class DhcpAgent(object):
                                           network,
                                           self.root_helper,
                                           self.device_manager,
-                                          namespace)
+                                          self._ns_name(network))
             getattr(driver, action)()
             return True
 
@@ -145,6 +150,8 @@ class DhcpAgent(object):
         for subnet in network.subnets:
             if subnet.enable_dhcp:
                 if self.call_driver('enable', network):
+                    if self.conf.use_namespaces:
+                        self.enable_isolated_metadata_proxy(network)
                     self.cache.put(network)
                 break
 
@@ -152,6 +159,8 @@ class DhcpAgent(object):
         """Disable DHCP for a network known to the agent."""
         network = self.cache.get_network_by_id(network_id)
         if network:
+            if self.conf.use_namespaces:
+                self.disable_isolated_metadata_proxy(network)
             if self.call_driver('disable', network):
                 self.cache.remove(network)
 
@@ -235,6 +244,29 @@ class DhcpAgent(object):
             self.cache.remove_port(port)
             self.call_driver('reload_allocations', network)
 
+    def enable_isolated_metadata_proxy(self, network):
+        def callback(pid_file):
+            return ['quantum-ns-metadata-proxy',
+                    '--pid_file=%s' % pid_file,
+                    '--network_id=%s' % network.id,
+                    '--state_path=%s' % self.conf.state_path,
+                    '--metadata_port=%d' % METADATA_PORT]
+
+        pm = external_process.ProcessManager(
+            self.conf,
+            network.id,
+            self.conf.root_helper,
+            self._ns_name(network))
+        pm.enable(callback)
+
+    def disable_isolated_metadata_proxy(self, network):
+        pm = external_process.ProcessManager(
+            self.conf,
+            network.id,
+            self.conf.root_helper,
+            self._ns_name(network))
+        pm.disable()
+
 
 class DhcpPluginApi(proxy.RpcProxy):
     """Agent side of the dhcp rpc API.
@@ -447,6 +479,9 @@ class DeviceManager(object):
             ip_cidr = '%s/%s' % (fixed_ip.ip_address, net.prefixlen)
             ip_cidrs.append(ip_cidr)
 
+        if self.conf.enable_isolated_metadata and self.conf.use_namespaces:
+            ip_cidrs.append(METADATA_DEFAULT_IP)
+
         self.driver.init_l3(interface_name, ip_cidrs,
                             namespace=namespace)
 
index cc2139b0f1def093d04ea9bc0a49d3a337876a55..7d9c4b02a9d31ea1385bc8bba04020c7ed8ef2b8 100644 (file)
@@ -58,6 +58,7 @@ TCP = 'tcp'
 DNS_PORT = 53
 DHCPV4_PORT = 67
 DHCPV6_PORT = 467
+METADATA_DEFAULT_IP = '169.254.169.254'
 
 
 class DhcpBase(object):
@@ -264,14 +265,15 @@ class Dnsmasq(DhcpLocalProcess):
             utils.execute(cmd, self.root_helper)
 
     def reload_allocations(self):
-        """If all subnets turn off dhcp, kill the process."""
+        """Rebuild the dnsmasq config and signal the dnsmasq to reload."""
+
+        # If all subnets turn off dhcp, kill the process.
         if not self._enable_dhcp():
             self.disable()
             LOG.debug(_('Killing dhcpmasq for network since all subnets have '
                         'turned off DHCP: %s'), self.network.id)
             return
 
-        """Rebuilds the dnsmasq config and signal the dnsmasq to reload."""
         self._output_hosts_file()
         self._output_opts_file()
         cmd = ['kill', '-HUP', self.pid]
@@ -301,6 +303,10 @@ class Dnsmasq(DhcpLocalProcess):
 
     def _output_opts_file(self):
         """Write a dnsmasq compatible options file."""
+
+        if self.conf.enable_isolated_metadata:
+            subnet_to_interface_ip = self._make_subnet_interface_ip_map()
+
         options = []
         for i, subnet in enumerate(self.network.subnets):
             if not subnet.enable_dhcp:
@@ -312,6 +318,19 @@ class Dnsmasq(DhcpLocalProcess):
 
             host_routes = ["%s,%s" % (hr.destination, hr.nexthop)
                            for hr in subnet.host_routes]
+
+            # Add host routes for isolated network segments
+            enable_metadata = (
+                self.conf.enable_isolated_metadata
+                and not subnet.gateway_ip
+                and subnet.ip_version == 4)
+
+            if enable_metadata:
+                subnet_dhcp_ip = subnet_to_interface_ip[subnet.id]
+                host_routes.append(
+                    '%s/32,%s' % (METADATA_DEFAULT_IP, subnet_dhcp_ip)
+                )
+
             if host_routes:
                 options.append(
                     self._format_option(i, 'classless-static-route',
@@ -328,6 +347,28 @@ class Dnsmasq(DhcpLocalProcess):
         replace_file(name, '\n'.join(options))
         return name
 
+    def _make_subnet_interface_ip_map(self):
+        ip_dev = ip_lib.IPDevice(
+            self.interface_name,
+            self.root_helper,
+            self.namespace
+        )
+
+        subnet_lookup = dict(
+            (netaddr.IPNetwork(subnet.cidr), subnet.id)
+            for subnet in self.network.subnets
+        )
+
+        retval = {}
+
+        for addr in ip_dev.addr.list():
+            ip_net = netaddr.IPNetwork(addr['cidr'])
+
+            if ip_net in subnet_lookup:
+                retval[subnet_lookup[ip_net]] = addr['cidr'].split('/')[0]
+
+        return retval
+
     def _lease_relay_script_path(self):
         return os.path.join(os.path.dirname(sys.argv[0]),
                             'quantum-dhcp-agent-dnsmasq-lease-update')
index 46cff19f3714cc420a5b7cf021b702d72b295f8f..d2c496a970a504a4a58bcff666a170a594479948 100644 (file)
@@ -100,6 +100,7 @@ class TestDhcpAgent(unittest.TestCase):
     def tearDown(self):
         self.notification_p.stop()
         self.driver_cls_p.stop()
+        cfg.CONF.reset()
 
     def test_dhcp_agent_main(self):
         logging_str = 'quantum.agent.common.config.setup_logging'
@@ -131,6 +132,19 @@ class TestDhcpAgent(unittest.TestCase):
                     [mock.call.start()])
                 self.notification.assert_has_calls([mock.call.run_dispatch()])
 
+    def test_ns_name(self):
+        with mock.patch('quantum.agent.dhcp_agent.DeviceManager') as dev_mgr:
+            mock_net = mock.Mock(id='foo')
+            dhcp = dhcp_agent.DhcpAgent(cfg.CONF)
+            self.assertTrue(dhcp._ns_name(mock_net), 'qdhcp-foo')
+
+    def test_ns_name_disabled_namespace(self):
+        with mock.patch('quantum.agent.dhcp_agent.DeviceManager') as dev_mgr:
+            cfg.CONF.set_override('use_namespaces', False)
+            mock_net = mock.Mock(id='foo')
+            dhcp = dhcp_agent.DhcpAgent(cfg.CONF)
+            self.assertIsNone(dhcp._ns_name(mock_net))
+
     def test_call_driver(self):
         network = mock.Mock()
         network.id = '1'
@@ -275,8 +289,13 @@ class TestDhcpAgentEventHandler(unittest.TestCase):
         self.call_driver_p = mock.patch.object(self.dhcp, 'call_driver')
 
         self.call_driver = self.call_driver_p.start()
+        self.external_process_p = mock.patch(
+            'quantum.agent.linux.external_process.ProcessManager'
+        )
+        self.external_process = self.external_process_p.start()
 
     def tearDown(self):
+        self.external_process_p.stop()
         self.call_driver_p.stop()
         self.cache_p.stop()
         self.plugin_p.stop()
@@ -289,6 +308,14 @@ class TestDhcpAgentEventHandler(unittest.TestCase):
             [mock.call.get_network_info(fake_network.id)])
         self.call_driver.assert_called_once_with('enable', fake_network)
         self.cache.assert_has_calls([mock.call.put(fake_network)])
+        self.external_process.assert_has_calls([
+            mock.call(
+                cfg.CONF,
+                '12345678-1234-5678-1234567890ab',
+                'sudo',
+                'qdhcp-12345678-1234-5678-1234567890ab'),
+            mock.call().enable(mock.ANY)
+        ])
 
     def test_enable_dhcp_helper_down_network(self):
         self.plugin.get_network_info.return_value = fake_down_network
@@ -297,6 +324,7 @@ class TestDhcpAgentEventHandler(unittest.TestCase):
             [mock.call.get_network_info(fake_down_network.id)])
         self.assertFalse(self.call_driver.called)
         self.assertFalse(self.cache.called)
+        self.assertFalse(self.external_process.called)
 
     def test_enable_dhcp_helper_exception_during_rpc(self):
         self.plugin.get_network_info.side_effect = Exception
@@ -308,15 +336,17 @@ class TestDhcpAgentEventHandler(unittest.TestCase):
             self.assertTrue(log.called)
             self.assertTrue(self.dhcp.needs_resync)
             self.assertFalse(self.cache.called)
+            self.assertFalse(self.external_process.called)
 
     def test_enable_dhcp_helper_driver_failure(self):
         self.plugin.get_network_info.return_value = fake_network
+        self.call_driver.return_value = False
         self.dhcp.enable_dhcp_helper(fake_network.id)
-        self.call_driver.enable.return_value = False
         self.plugin.assert_has_calls(
             [mock.call.get_network_info(fake_network.id)])
         self.call_driver.assert_called_once_with('enable', fake_network)
         self.assertFalse(self.cache.called)
+        self.assertFalse(self.external_process.called)
 
     def test_disable_dhcp_helper_known_network(self):
         self.cache.get_network_by_id.return_value = fake_network
@@ -324,6 +354,14 @@ class TestDhcpAgentEventHandler(unittest.TestCase):
         self.cache.assert_has_calls(
             [mock.call.get_network_by_id(fake_network.id)])
         self.call_driver.assert_called_once_with('disable', fake_network)
+        self.external_process.assert_has_calls([
+            mock.call(
+                cfg.CONF,
+                '12345678-1234-5678-1234567890ab',
+                'sudo',
+                'qdhcp-12345678-1234-5678-1234567890ab'),
+            mock.call().disable()
+        ])
 
     def test_disable_dhcp_helper_unknown_network(self):
         self.cache.get_network_by_id.return_value = None
@@ -331,16 +369,51 @@ class TestDhcpAgentEventHandler(unittest.TestCase):
         self.cache.assert_has_calls(
             [mock.call.get_network_by_id('abcdef')])
         self.assertEqual(self.call_driver.call_count, 0)
+        self.assertFalse(self.external_process.called)
 
     def test_disable_dhcp_helper_driver_failure(self):
         self.cache.get_network_by_id.return_value = fake_network
+        self.call_driver.return_value = False
         self.dhcp.disable_dhcp_helper(fake_network.id)
-        self.call_driver.disable.return_value = False
         self.cache.assert_has_calls(
             [mock.call.get_network_by_id(fake_network.id)])
         self.call_driver.assert_called_once_with('disable', fake_network)
         self.cache.assert_has_calls(
             [mock.call.get_network_by_id(fake_network.id)])
+        self.external_process.assert_has_calls([
+            mock.call(
+                cfg.CONF,
+                '12345678-1234-5678-1234567890ab',
+                'sudo',
+                'qdhcp-12345678-1234-5678-1234567890ab'),
+            mock.call().disable()
+        ])
+
+    def test_enable_isolated_metadata_proxy(self):
+        class_path = 'quantum.agent.linux.external_process.ProcessManager'
+        with mock.patch(class_path) as ext_process:
+            self.dhcp.enable_isolated_metadata_proxy(fake_network)
+            ext_process.assert_has_calls([
+                mock.call(
+                    cfg.CONF,
+                    '12345678-1234-5678-1234567890ab',
+                    'sudo',
+                    'qdhcp-12345678-1234-5678-1234567890ab'),
+                mock.call().enable(mock.ANY)
+            ])
+
+    def test_disable_isolated_metadata_proxy(self):
+        class_path = 'quantum.agent.linux.external_process.ProcessManager'
+        with mock.patch(class_path) as ext_process:
+            self.dhcp.disable_isolated_metadata_proxy(fake_network)
+            ext_process.assert_has_calls([
+                mock.call(
+                    cfg.CONF,
+                    '12345678-1234-5678-1234567890ab',
+                    'sudo',
+                    'qdhcp-12345678-1234-5678-1234567890ab'),
+                mock.call().disable()
+            ])
 
     def test_network_create_end(self):
         payload = dict(network=dict(id=fake_network.id))
@@ -668,6 +741,8 @@ class TestDeviceManager(unittest.TestCase):
         cfg.CONF.set_override('interface_driver',
                               'quantum.agent.linux.interface.NullDriver')
         config.register_root_helper(cfg.CONF)
+        cfg.CONF.set_override('use_namespaces', True)
+        cfg.CONF.set_override('enable_isolated_metadata', True)
 
         self.device_exists_p = mock.patch(
             'quantum.agent.linux.ip_lib.device_exists')
@@ -683,6 +758,7 @@ class TestDeviceManager(unittest.TestCase):
     def tearDown(self):
         self.dvr_cls_p.stop()
         self.device_exists_p.stop()
+        cfg.CONF.reset()
 
     def _test_setup_helper(self, device_exists, reuse_existing=False):
         plugin = mock.Mock()
@@ -691,7 +767,9 @@ class TestDeviceManager(unittest.TestCase):
         self.mock_driver.get_device_name.return_value = 'tap12345678-12'
 
         dh = dhcp_agent.DeviceManager(cfg.CONF, plugin)
-        dh.setup(fake_network, reuse_existing)
+        interface_name = dh.setup(fake_network, reuse_existing)
+
+        self.assertEqual(interface_name, 'tap12345678-12')
 
         plugin.assert_has_calls([
             mock.call.get_dhcp_port(fake_network.id, mock.ANY)])
@@ -699,7 +777,7 @@ class TestDeviceManager(unittest.TestCase):
         namespace = dhcp_agent.NS_PREFIX + fake_network.id
 
         expected = [mock.call.init_l3('tap12345678-12',
-                                      ['172.9.9.9/24'],
+                                      ['172.9.9.9/24', '169.254.169.254/16'],
                                       namespace=namespace)]
 
         if not reuse_existing:
index 7ad4444a7b91788dc1f071259ad4ad2b86703728..95ecd892c3b3e37807fbe3a2d2a77965e4e294ff 100644 (file)
@@ -202,8 +202,11 @@ class TestBase(unittest.TestCase):
                 os.path.join(root, 'etc', 'quantum.conf.test')]
         self.conf = config.setup_conf()
         self.conf.register_opts(dhcp.OPTS)
-        self.conf.register_opt(cfg.StrOpt('dhcp_lease_relay_socket',
-                               default='$state_path/dhcp/lease_relay'))
+        self.conf.register_opt(
+            cfg.StrOpt('dhcp_lease_relay_socket',
+                       default='$state_path/dhcp/lease_relay'))
+        self.conf.register_opt(cfg.BoolOpt('enable_isolated_metadata',
+                                           default=True))
         self.conf(args=args)
         self.conf.set_override('state_path', '')
         self.conf.use_namespaces = True
@@ -511,12 +514,18 @@ tag:tag0,option:router,192.168.0.1""".lstrip()
         self.safe.assert_called_once_with('/foo/opts', expected)
 
     def test_output_opts_file_no_gateway(self):
-        expected = "tag:tag0,option:router"
+        expected = """
+tag:tag0,option:classless-static-route,169.254.169.254/32,192.168.1.1
+tag:tag0,option:router""".lstrip()
 
         with mock.patch.object(dhcp.Dnsmasq, 'get_conf_file_name') as conf_fn:
             conf_fn.return_value = '/foo/opts'
             dm = dhcp.Dnsmasq(self.conf, FakeV4NoGatewayNetwork())
-            dm._output_opts_file()
+            with mock.patch.object(dm, '_make_subnet_interface_ip_map') as ipm:
+                ipm.return_value = {FakeV4SubnetNoGateway.id: '192.168.1.1'}
+
+                dm._output_opts_file()
+                self.assertTrue(ipm.called)
 
         self.safe.assert_called_once_with('/foo/opts', expected)
 
@@ -549,13 +558,33 @@ tag:tag1,option:classless-static-route,%s,%s""".lstrip() % (fake_v6,
                 pid.__get__ = mock.Mock(return_value=5)
                 dm = dhcp.Dnsmasq(self.conf, FakeDualNetwork(),
                                   namespace='qdhcp-ns')
-                dm.reload_allocations()
+
+                method_name = '_make_subnet_interface_ip_map'
+                with mock.patch.object(dhcp.Dnsmasq, method_name) as ip_map:
+                    ip_map.return_value = {}
+                    dm.reload_allocations()
+                    self.assertTrue(ip_map.called)
 
         self.safe.assert_has_calls([mock.call(exp_host_name, exp_host_data),
                                     mock.call(exp_opt_name, exp_opt_data)])
         self.execute.assert_called_once_with(exp_args, root_helper='sudo',
                                              check_exit_code=True)
 
+    def test_make_subnet_interface_ip_map(self):
+        with mock.patch('quantum.agent.linux.ip_lib.IPDevice') as ip_dev:
+            ip_dev.return_value.addr.list.return_value = [
+                {'cidr': '192.168.0.1/24'}
+            ]
+
+            dm = dhcp.Dnsmasq(self.conf,
+                              FakeDualNetwork(),
+                              namespace='qdhcp-ns')
+
+            self.assertEqual(
+                dm._make_subnet_interface_ip_map(),
+                {FakeV4Subnet.id: '192.168.0.1'}
+            )
+
     def _test_lease_relay_script_helper(self, action, lease_remaining,
                                         path_exists=True):
         relay_path = '/dhcp/relay_socket'