From cb6b9994e29f619fa9da08c857ede87c225f51ee Mon Sep 17 00:00:00 2001 From: Mark McClain Date: Mon, 4 Feb 2013 23:59:59 -0500 Subject: [PATCH] add non-routed subnet metadata support 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 | 7 +++ quantum/agent/dhcp_agent.py | 47 +++++++++++++-- quantum/agent/linux/dhcp.py | 45 +++++++++++++- quantum/tests/unit/test_dhcp_agent.py | 86 +++++++++++++++++++++++++-- quantum/tests/unit/test_linux_dhcp.py | 39 ++++++++++-- 5 files changed, 207 insertions(+), 17 deletions(-) diff --git a/etc/dhcp_agent.ini b/etc/dhcp_agent.ini index 3ec8a82fbc..1d3eef0d35 100644 --- a/etc/dhcp_agent.ini +++ b/etc/dhcp_agent.ini @@ -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 diff --git a/quantum/agent/dhcp_agent.py b/quantum/agent/dhcp_agent.py index b3ef9d7d5f..5631f4235f 100644 --- a/quantum/agent/dhcp_agent.py +++ b/quantum/agent/dhcp_agent.py @@ -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) diff --git a/quantum/agent/linux/dhcp.py b/quantum/agent/linux/dhcp.py index cc2139b0f1..7d9c4b02a9 100644 --- a/quantum/agent/linux/dhcp.py +++ b/quantum/agent/linux/dhcp.py @@ -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') diff --git a/quantum/tests/unit/test_dhcp_agent.py b/quantum/tests/unit/test_dhcp_agent.py index 46cff19f37..d2c496a970 100644 --- a/quantum/tests/unit/test_dhcp_agent.py +++ b/quantum/tests/unit/test_dhcp_agent.py @@ -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: diff --git a/quantum/tests/unit/test_linux_dhcp.py b/quantum/tests/unit/test_linux_dhcp.py index 7ad4444a7b..95ecd892c3 100644 --- a/quantum/tests/unit/test_linux_dhcp.py +++ b/quantum/tests/unit/test_linux_dhcp.py @@ -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'