Merge "add non-routed subnet metadata support"
This commit is contained in:
commit
c2886fce30
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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')
|
||||
|
@ -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:
|
||||
|
@ -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'
|
||||
|
Loading…
Reference in New Issue
Block a user