# 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
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
LOG = logging.getLogger(__name__)
NS_PREFIX = 'qdhcp-'
+METADATA_DEFAULT_IP = '169.254.169.254/16'
+METADATA_PORT = 80
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):
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.
network,
self.root_helper,
self.device_manager,
- namespace)
+ self._ns_name(network))
getattr(driver, action)()
return True
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
"""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)
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.
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)
DNS_PORT = 53
DHCPV4_PORT = 67
DHCPV6_PORT = 467
+METADATA_DEFAULT_IP = '169.254.169.254'
class DhcpBase(object):
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]
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:
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',
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')
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'
[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'
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()
[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
[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
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
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
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))
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')
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()
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)])
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:
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
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)
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'