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'