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:
Mark McClain 2013-02-04 23:59:59 -05:00
parent af1b0d19fc
commit cb6b9994e2
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'