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
This commit is contained in:
parent
af1b0d19fc
commit
cb6b9994e2
@ -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