Merge "add non-routed subnet metadata support"

This commit is contained in:
Jenkins 2013-02-08 22:39:32 +00:00 committed by Gerrit Code Review
commit c2886fce30
5 changed files with 207 additions and 17 deletions

View 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 # Allow overlapping IP (Must have kernel build with CONFIG_NET_NS=y and
# iproute2 package that supports namespaces). # iproute2 package that supports namespaces).
# use_namespaces = True # 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

View File

@ -24,6 +24,7 @@ import netaddr
from quantum.agent.common import config from quantum.agent.common import config
from quantum.agent.linux import dhcp from quantum.agent.linux import dhcp
from quantum.agent.linux import external_process
from quantum.agent.linux import interface from quantum.agent.linux import interface
from quantum.agent.linux import ip_lib from quantum.agent.linux import ip_lib
from quantum.agent import rpc as agent_rpc from quantum.agent import rpc as agent_rpc
@ -39,6 +40,8 @@ from quantum.openstack.common import uuidutils
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
NS_PREFIX = 'qdhcp-' NS_PREFIX = 'qdhcp-'
METADATA_DEFAULT_IP = '169.254.169.254/16'
METADATA_PORT = 80
class DhcpAgent(object): class DhcpAgent(object):
@ -49,7 +52,9 @@ class DhcpAgent(object):
default='quantum.agent.linux.dhcp.Dnsmasq', default='quantum.agent.linux.dhcp.Dnsmasq',
help=_("The driver used to manage the DHCP server.")), help=_("The driver used to manage the DHCP server.")),
cfg.BoolOpt('use_namespaces', default=True, 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): def __init__(self, conf):
@ -73,12 +78,12 @@ class DhcpAgent(object):
self.lease_relay.start() self.lease_relay.start()
self.notifications.run_dispatch(self) 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): def call_driver(self, action, network):
"""Invoke an action on a DHCP driver instance.""" """Invoke an action on a DHCP driver instance."""
if self.conf.use_namespaces:
namespace = NS_PREFIX + network.id
else:
namespace = None
try: try:
# the Driver expects something that is duck typed similar to # the Driver expects something that is duck typed similar to
# the base models. # the base models.
@ -86,7 +91,7 @@ class DhcpAgent(object):
network, network,
self.root_helper, self.root_helper,
self.device_manager, self.device_manager,
namespace) self._ns_name(network))
getattr(driver, action)() getattr(driver, action)()
return True return True
@ -145,6 +150,8 @@ class DhcpAgent(object):
for subnet in network.subnets: for subnet in network.subnets:
if subnet.enable_dhcp: if subnet.enable_dhcp:
if self.call_driver('enable', network): if self.call_driver('enable', network):
if self.conf.use_namespaces:
self.enable_isolated_metadata_proxy(network)
self.cache.put(network) self.cache.put(network)
break break
@ -152,6 +159,8 @@ class DhcpAgent(object):
"""Disable DHCP for a network known to the agent.""" """Disable DHCP for a network known to the agent."""
network = self.cache.get_network_by_id(network_id) network = self.cache.get_network_by_id(network_id)
if network: if network:
if self.conf.use_namespaces:
self.disable_isolated_metadata_proxy(network)
if self.call_driver('disable', network): if self.call_driver('disable', network):
self.cache.remove(network) self.cache.remove(network)
@ -235,6 +244,29 @@ class DhcpAgent(object):
self.cache.remove_port(port) self.cache.remove_port(port)
self.call_driver('reload_allocations', network) 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): class DhcpPluginApi(proxy.RpcProxy):
"""Agent side of the dhcp rpc API. """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_cidr = '%s/%s' % (fixed_ip.ip_address, net.prefixlen)
ip_cidrs.append(ip_cidr) 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, self.driver.init_l3(interface_name, ip_cidrs,
namespace=namespace) namespace=namespace)

View File

@ -58,6 +58,7 @@ TCP = 'tcp'
DNS_PORT = 53 DNS_PORT = 53
DHCPV4_PORT = 67 DHCPV4_PORT = 67
DHCPV6_PORT = 467 DHCPV6_PORT = 467
METADATA_DEFAULT_IP = '169.254.169.254'
class DhcpBase(object): class DhcpBase(object):
@ -264,14 +265,15 @@ class Dnsmasq(DhcpLocalProcess):
utils.execute(cmd, self.root_helper) utils.execute(cmd, self.root_helper)
def reload_allocations(self): 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(): if not self._enable_dhcp():
self.disable() self.disable()
LOG.debug(_('Killing dhcpmasq for network since all subnets have ' LOG.debug(_('Killing dhcpmasq for network since all subnets have '
'turned off DHCP: %s'), self.network.id) 'turned off DHCP: %s'), self.network.id)
return return
"""Rebuilds the dnsmasq config and signal the dnsmasq to reload."""
self._output_hosts_file() self._output_hosts_file()
self._output_opts_file() self._output_opts_file()
cmd = ['kill', '-HUP', self.pid] cmd = ['kill', '-HUP', self.pid]
@ -301,6 +303,10 @@ class Dnsmasq(DhcpLocalProcess):
def _output_opts_file(self): def _output_opts_file(self):
"""Write a dnsmasq compatible options file.""" """Write a dnsmasq compatible options file."""
if self.conf.enable_isolated_metadata:
subnet_to_interface_ip = self._make_subnet_interface_ip_map()
options = [] options = []
for i, subnet in enumerate(self.network.subnets): for i, subnet in enumerate(self.network.subnets):
if not subnet.enable_dhcp: if not subnet.enable_dhcp:
@ -312,6 +318,19 @@ class Dnsmasq(DhcpLocalProcess):
host_routes = ["%s,%s" % (hr.destination, hr.nexthop) host_routes = ["%s,%s" % (hr.destination, hr.nexthop)
for hr in subnet.host_routes] 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: if host_routes:
options.append( options.append(
self._format_option(i, 'classless-static-route', self._format_option(i, 'classless-static-route',
@ -328,6 +347,28 @@ class Dnsmasq(DhcpLocalProcess):
replace_file(name, '\n'.join(options)) replace_file(name, '\n'.join(options))
return name 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): def _lease_relay_script_path(self):
return os.path.join(os.path.dirname(sys.argv[0]), return os.path.join(os.path.dirname(sys.argv[0]),
'quantum-dhcp-agent-dnsmasq-lease-update') 'quantum-dhcp-agent-dnsmasq-lease-update')

View File

@ -100,6 +100,7 @@ class TestDhcpAgent(unittest.TestCase):
def tearDown(self): def tearDown(self):
self.notification_p.stop() self.notification_p.stop()
self.driver_cls_p.stop() self.driver_cls_p.stop()
cfg.CONF.reset()
def test_dhcp_agent_main(self): def test_dhcp_agent_main(self):
logging_str = 'quantum.agent.common.config.setup_logging' logging_str = 'quantum.agent.common.config.setup_logging'
@ -131,6 +132,19 @@ class TestDhcpAgent(unittest.TestCase):
[mock.call.start()]) [mock.call.start()])
self.notification.assert_has_calls([mock.call.run_dispatch()]) 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): def test_call_driver(self):
network = mock.Mock() network = mock.Mock()
network.id = '1' 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_p = mock.patch.object(self.dhcp, 'call_driver')
self.call_driver = self.call_driver_p.start() 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): def tearDown(self):
self.external_process_p.stop()
self.call_driver_p.stop() self.call_driver_p.stop()
self.cache_p.stop() self.cache_p.stop()
self.plugin_p.stop() self.plugin_p.stop()
@ -289,6 +308,14 @@ class TestDhcpAgentEventHandler(unittest.TestCase):
[mock.call.get_network_info(fake_network.id)]) [mock.call.get_network_info(fake_network.id)])
self.call_driver.assert_called_once_with('enable', fake_network) self.call_driver.assert_called_once_with('enable', fake_network)
self.cache.assert_has_calls([mock.call.put(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): def test_enable_dhcp_helper_down_network(self):
self.plugin.get_network_info.return_value = fake_down_network 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)]) [mock.call.get_network_info(fake_down_network.id)])
self.assertFalse(self.call_driver.called) self.assertFalse(self.call_driver.called)
self.assertFalse(self.cache.called) self.assertFalse(self.cache.called)
self.assertFalse(self.external_process.called)
def test_enable_dhcp_helper_exception_during_rpc(self): def test_enable_dhcp_helper_exception_during_rpc(self):
self.plugin.get_network_info.side_effect = Exception self.plugin.get_network_info.side_effect = Exception
@ -308,15 +336,17 @@ class TestDhcpAgentEventHandler(unittest.TestCase):
self.assertTrue(log.called) self.assertTrue(log.called)
self.assertTrue(self.dhcp.needs_resync) self.assertTrue(self.dhcp.needs_resync)
self.assertFalse(self.cache.called) self.assertFalse(self.cache.called)
self.assertFalse(self.external_process.called)
def test_enable_dhcp_helper_driver_failure(self): def test_enable_dhcp_helper_driver_failure(self):
self.plugin.get_network_info.return_value = fake_network self.plugin.get_network_info.return_value = fake_network
self.call_driver.return_value = False
self.dhcp.enable_dhcp_helper(fake_network.id) self.dhcp.enable_dhcp_helper(fake_network.id)
self.call_driver.enable.return_value = False
self.plugin.assert_has_calls( self.plugin.assert_has_calls(
[mock.call.get_network_info(fake_network.id)]) [mock.call.get_network_info(fake_network.id)])
self.call_driver.assert_called_once_with('enable', fake_network) self.call_driver.assert_called_once_with('enable', fake_network)
self.assertFalse(self.cache.called) self.assertFalse(self.cache.called)
self.assertFalse(self.external_process.called)
def test_disable_dhcp_helper_known_network(self): def test_disable_dhcp_helper_known_network(self):
self.cache.get_network_by_id.return_value = fake_network self.cache.get_network_by_id.return_value = fake_network
@ -324,6 +354,14 @@ class TestDhcpAgentEventHandler(unittest.TestCase):
self.cache.assert_has_calls( self.cache.assert_has_calls(
[mock.call.get_network_by_id(fake_network.id)]) [mock.call.get_network_by_id(fake_network.id)])
self.call_driver.assert_called_once_with('disable', fake_network) 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): def test_disable_dhcp_helper_unknown_network(self):
self.cache.get_network_by_id.return_value = None self.cache.get_network_by_id.return_value = None
@ -331,16 +369,51 @@ class TestDhcpAgentEventHandler(unittest.TestCase):
self.cache.assert_has_calls( self.cache.assert_has_calls(
[mock.call.get_network_by_id('abcdef')]) [mock.call.get_network_by_id('abcdef')])
self.assertEqual(self.call_driver.call_count, 0) self.assertEqual(self.call_driver.call_count, 0)
self.assertFalse(self.external_process.called)
def test_disable_dhcp_helper_driver_failure(self): def test_disable_dhcp_helper_driver_failure(self):
self.cache.get_network_by_id.return_value = fake_network 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.dhcp.disable_dhcp_helper(fake_network.id)
self.call_driver.disable.return_value = False
self.cache.assert_has_calls( self.cache.assert_has_calls(
[mock.call.get_network_by_id(fake_network.id)]) [mock.call.get_network_by_id(fake_network.id)])
self.call_driver.assert_called_once_with('disable', fake_network) self.call_driver.assert_called_once_with('disable', fake_network)
self.cache.assert_has_calls( self.cache.assert_has_calls(
[mock.call.get_network_by_id(fake_network.id)]) [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): def test_network_create_end(self):
payload = dict(network=dict(id=fake_network.id)) payload = dict(network=dict(id=fake_network.id))
@ -668,6 +741,8 @@ class TestDeviceManager(unittest.TestCase):
cfg.CONF.set_override('interface_driver', cfg.CONF.set_override('interface_driver',
'quantum.agent.linux.interface.NullDriver') 'quantum.agent.linux.interface.NullDriver')
config.register_root_helper(cfg.CONF) 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( self.device_exists_p = mock.patch(
'quantum.agent.linux.ip_lib.device_exists') 'quantum.agent.linux.ip_lib.device_exists')
@ -683,6 +758,7 @@ class TestDeviceManager(unittest.TestCase):
def tearDown(self): def tearDown(self):
self.dvr_cls_p.stop() self.dvr_cls_p.stop()
self.device_exists_p.stop() self.device_exists_p.stop()
cfg.CONF.reset()
def _test_setup_helper(self, device_exists, reuse_existing=False): def _test_setup_helper(self, device_exists, reuse_existing=False):
plugin = mock.Mock() plugin = mock.Mock()
@ -691,7 +767,9 @@ class TestDeviceManager(unittest.TestCase):
self.mock_driver.get_device_name.return_value = 'tap12345678-12' self.mock_driver.get_device_name.return_value = 'tap12345678-12'
dh = dhcp_agent.DeviceManager(cfg.CONF, plugin) 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([ plugin.assert_has_calls([
mock.call.get_dhcp_port(fake_network.id, mock.ANY)]) 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 namespace = dhcp_agent.NS_PREFIX + fake_network.id
expected = [mock.call.init_l3('tap12345678-12', expected = [mock.call.init_l3('tap12345678-12',
['172.9.9.9/24'], ['172.9.9.9/24', '169.254.169.254/16'],
namespace=namespace)] namespace=namespace)]
if not reuse_existing: if not reuse_existing:

View File

@ -202,8 +202,11 @@ class TestBase(unittest.TestCase):
os.path.join(root, 'etc', 'quantum.conf.test')] os.path.join(root, 'etc', 'quantum.conf.test')]
self.conf = config.setup_conf() self.conf = config.setup_conf()
self.conf.register_opts(dhcp.OPTS) self.conf.register_opts(dhcp.OPTS)
self.conf.register_opt(cfg.StrOpt('dhcp_lease_relay_socket', self.conf.register_opt(
default='$state_path/dhcp/lease_relay')) 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(args=args)
self.conf.set_override('state_path', '') self.conf.set_override('state_path', '')
self.conf.use_namespaces = True 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) self.safe.assert_called_once_with('/foo/opts', expected)
def test_output_opts_file_no_gateway(self): 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: with mock.patch.object(dhcp.Dnsmasq, 'get_conf_file_name') as conf_fn:
conf_fn.return_value = '/foo/opts' conf_fn.return_value = '/foo/opts'
dm = dhcp.Dnsmasq(self.conf, FakeV4NoGatewayNetwork()) 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) 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) pid.__get__ = mock.Mock(return_value=5)
dm = dhcp.Dnsmasq(self.conf, FakeDualNetwork(), dm = dhcp.Dnsmasq(self.conf, FakeDualNetwork(),
namespace='qdhcp-ns') 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), self.safe.assert_has_calls([mock.call(exp_host_name, exp_host_data),
mock.call(exp_opt_name, exp_opt_data)]) mock.call(exp_opt_name, exp_opt_data)])
self.execute.assert_called_once_with(exp_args, root_helper='sudo', self.execute.assert_called_once_with(exp_args, root_helper='sudo',
check_exit_code=True) 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, def _test_lease_relay_script_helper(self, action, lease_remaining,
path_exists=True): path_exists=True):
relay_path = '/dhcp/relay_socket' relay_path = '/dhcp/relay_socket'