diff --git a/bin/quantum-dhcp-agent-dnsmasq-lease-update b/bin/quantum-dhcp-agent-dnsmasq-lease-update new file mode 100755 index 000000000..d054fa8c6 --- /dev/null +++ b/bin/quantum-dhcp-agent-dnsmasq-lease-update @@ -0,0 +1,20 @@ +#!/usr/bin/env python +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2012 Openstack, LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from quantum.agent.linux import dhcp +dhcp.Dnsmasq.lease_update() diff --git a/quantum/agent/dhcp_agent.py b/quantum/agent/dhcp_agent.py index bf6229bbf..6b3e30bfc 100644 --- a/quantum/agent/dhcp_agent.py +++ b/quantum/agent/dhcp_agent.py @@ -16,6 +16,8 @@ # under the License. import logging +import os +import re import socket import sys import uuid @@ -28,11 +30,13 @@ from quantum.agent.common import config from quantum.agent.linux import dhcp from quantum.agent.linux import interface from quantum.agent.linux import ip_lib +from quantum.api.v2 import attributes from quantum.common import exceptions from quantum.common import topics from quantum.openstack.common import cfg from quantum.openstack.common import context from quantum.openstack.common import importutils +from quantum.openstack.common import jsonutils from quantum.openstack.common.rpc import proxy from quantum.version import version_string @@ -59,6 +63,7 @@ class DhcpAgent(object): self.device_manager = DeviceManager(self.conf, self.plugin_rpc) self.notifications = agent_rpc.NotificationDispatcher() + self.lease_relay = DhcpLeaseRelay(self.update_lease) def run(self): """Activate the DHCP agent.""" @@ -66,6 +71,7 @@ class DhcpAgent(object): for network_id in self.plugin_rpc.get_active_networks(): self.enable_dhcp_helper(network_id) + self.lease_relay.start() self.notifications.run_dispatch(self) def call_driver(self, action, network): @@ -82,6 +88,10 @@ class DhcpAgent(object): except Exception, e: LOG.warn('Unable to %s dhcp. Exception: %s' % (action, e)) + def update_lease(self, network_id, ip_address, time_remaining): + self.plugin_rpc.update_lease_expiration(network_id, ip_address, + time_remaining) + def enable_dhcp_helper(self, network_id): """Enable DHCP for a network that meets enabling criteria.""" network = self.plugin_rpc.get_network_info(network_id) @@ -236,6 +246,16 @@ class DhcpPluginApi(proxy.RpcProxy): host=self.host), topic=self.topic) + def update_lease_expiration(self, network_id, ip_address, lease_remaining): + """Make a remote process call to update the ip lease expiration.""" + self.cast(self.context, + self.make_msg('update_lease_expiration', + network_id=network_id, + ip_address=ip_address, + lease_remaining=lease_remaining, + host=self.host), + topic=self.topic) + class NetworkCache(object): """Agent cache of the current network state.""" @@ -401,10 +421,75 @@ class DictModel(object): setattr(self, key, value) +class DhcpLeaseRelay(object): + """UNIX domain socket server for processing lease updates. + + Network namespace isolation prevents the DHCP process from notifying + Quantum directly. This class works around the limitation by using the + domain socket to pass the information. This class handles message. + receiving and then calls the callback method. + """ + + OPTS = [ + cfg.StrOpt('dhcp_lease_relay_socket', + default='$state_path/dhcp/lease_relay', + help='Location to DHCP lease relay UNIX domain socket') + ] + + def __init__(self, lease_update_callback): + self.callback = lease_update_callback + + try: + os.unlink(cfg.CONF.dhcp_lease_relay_socket) + except OSError: + if os.path.exists(cfg.CONF.dhcp_lease_relay_socket): + raise + + def _validate_field(self, value, regex): + """Validate value against a regular expression and return if valid.""" + match = re.match(regex, value) + + if match: + return value + raise ValueError(_("Value %s does not match regex: %s") % + (value, regex)) + + def _handler(self, client_sock, client_addr): + """Handle incoming lease relay stream connection. + + This method will only read the first 1024 bytes and then close the + connection. The limit exists to limit the impact of misbehaving + clients. + """ + try: + msg = client_sock.recv(1024) + data = jsonutils.loads(msg) + client_sock.close() + + network_id = self._validate_field(data['network_id'], + attributes.UUID_PATTERN) + ip_address = str(netaddr.IPAddress(data['ip_address'])) + lease_remaining = int(data['lease_remaining']) + self.callback(network_id, ip_address, lease_remaining) + except ValueError, e: + LOG.warn(_('Unable to parse lease relay msg to dict.')) + LOG.warn(_('Exception value: %s') % e) + LOG.warn(_('Message representation: %s') % repr(msg)) + except Exception, e: + LOG.exception(_('Unable update lease. Exception')) + + def start(self): + """Spawn a green thread to run the lease relay unix socket server.""" + listener = eventlet.listen(cfg.CONF.dhcp_lease_relay_socket, + family=socket.AF_UNIX) + eventlet.spawn(eventlet.serve, listener, self._handler) + + def main(): eventlet.monkey_patch() cfg.CONF.register_opts(DhcpAgent.OPTS) cfg.CONF.register_opts(DeviceManager.OPTS) + cfg.CONF.register_opts(DhcpLeaseRelay.OPTS) cfg.CONF.register_opts(dhcp.OPTS) cfg.CONF.register_opts(interface.OPTS) cfg.CONF(args=sys.argv, project='quantum') diff --git a/quantum/agent/linux/dhcp.py b/quantum/agent/linux/dhcp.py index 2ed335d09..720e732de 100644 --- a/quantum/agent/linux/dhcp.py +++ b/quantum/agent/linux/dhcp.py @@ -19,8 +19,11 @@ import abc import logging import os import re +import socket import StringIO +import sys import tempfile +import textwrap import netaddr @@ -28,6 +31,7 @@ from quantum.agent.linux import ip_lib from quantum.agent.linux import utils from quantum.openstack.common import cfg from quantum.openstack.common import importutils +from quantum.openstack.common import jsonutils LOG = logging.getLogger(__name__) @@ -190,12 +194,20 @@ class Dnsmasq(DhcpLocalProcess): _TAG_PREFIX = 'tag%d' + QUANTUM_NETWORK_ID_KEY = 'QUANTUM_NETWORK_ID' + QUANTUM_RELAY_SOCKET_PATH_KEY = 'QUANTUM_RELAY_SOCKET_PATH' + def spawn_process(self): """Spawns a Dnsmasq process for the network.""" interface_name = self.device_delegate.get_interface_name(self.network) + + env = { + self.QUANTUM_NETWORK_ID_KEY: self.network.id, + self.QUANTUM_RELAY_SOCKET_PATH_KEY: + self.conf.dhcp_lease_relay_socket + } + cmd = [ - # TODO (mark): this is dhcpbridge script we'll need to know - # when an IP address has been released 'dnsmasq', '--no-hosts', '--no-resolv', @@ -210,6 +222,7 @@ class Dnsmasq(DhcpLocalProcess): #'--dhcp-lease-max=%s' % ?, '--dhcp-hostsfile=%s' % self._output_hosts_file(), '--dhcp-optsfile=%s' % self._output_opts_file(), + '--dhcp-script=%s' % self._lease_relay_script_path(), '--leasefile-ro', ] @@ -237,8 +250,10 @@ class Dnsmasq(DhcpLocalProcess): if self.conf.use_namespaces: ip_wrapper = ip_lib.IPWrapper(self.root_helper, namespace=self.network.id) - ip_wrapper.netns.execute(cmd) + ip_wrapper.netns.execute(cmd, addl_env=env) else: + # For normal sudo prepend the env vars before command + cmd = ['%s=%s' % pair for pair in env.items()] + cmd utils.execute(cmd, self.root_helper) def reload_allocations(self): @@ -298,6 +313,36 @@ class Dnsmasq(DhcpLocalProcess): replace_file(name, '\n'.join(['tag:%s,%s:%s,%s' % o for o in options])) return name + def _lease_relay_script_path(self): + return os.path.join(os.path.dirname(sys.argv[0]), + 'quantum-dhcp-agent-dnsmasq-lease-update') + + @classmethod + def lease_update(cls): + network_id = os.environ.get(cls.QUANTUM_NETWORK_ID_KEY) + dhcp_relay_socket = os.environ.get(cls.QUANTUM_RELAY_SOCKET_PATH_KEY) + + action = sys.argv[1] + if action not in ('add', 'del', 'old'): + sys.exit() + + mac_address = sys.argv[2] + ip_address = sys.argv[3] + + if action == 'del': + lease_remaining = 0 + else: + lease_remaining = int(os.environ.get('DNSMASQ_TIME_REMAINING', 0)) + + data = dict(network_id=network_id, mac_address=mac_address, + ip_address=ip_address, lease_remaining=lease_remaining) + + if os.path.exists(dhcp_relay_socket): + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.connect(dhcp_relay_socket) + sock.send(jsonutils.dumps(data)) + sock.close() + def replace_file(file_name, data): """Replaces the contents of file_name with data in a safe manner. diff --git a/quantum/agent/linux/ip_lib.py b/quantum/agent/linux/ip_lib.py index 9d548b9d9..006ad51b4 100644 --- a/quantum/agent/linux/ip_lib.py +++ b/quantum/agent/linux/ip_lib.py @@ -269,13 +269,14 @@ class IpNetnsCommand(IpCommandBase): ['ip', 'netns', 'delete', name], root_helper=self._parent.root_helper) - def execute(self, cmds): + def execute(self, cmds, addl_env={}): if not self._parent.root_helper: raise exceptions.SudoRequired() elif not self._parent.namespace: raise Exception(_('No namespace defined for parent')) else: return utils.execute( + ['%s=%s' % pair for pair in addl_env.items()] + ['ip', 'netns', 'exec', self._parent.namespace] + list(cmds), root_helper=self._parent.root_helper) diff --git a/quantum/db/db_base_plugin_v2.py b/quantum/db/db_base_plugin_v2.py index 8750a26eb..ff2b08f3d 100644 --- a/quantum/db/db_base_plugin_v2.py +++ b/quantum/db/db_base_plugin_v2.py @@ -281,6 +281,21 @@ class QuantumDbPluginV2(quantum_plugin_base_v2.QuantumPluginBaseV2): return (timeutils.utcnow() + datetime.timedelta(seconds=cfg.CONF.dhcp_lease_duration)) + def update_fixed_ip_lease_expiration(self, context, network_id, + ip_address, lease_remaining): + + expiration = timeutils.utcnow() + datetime.timedelta(lease_remaining) + + query = context.session.query(models_v2.IPAllocation) + query = query.filter_by(network_id=network_id, ip_address=ip_address) + + try: + fixed_ip = query.one() + fixed_ip.expiration = expiration + except exc.NoResultFound: + LOG.debug("No fixed IP found that matches the network %s and " + "ip address %s.", network_id, ip_address) + @staticmethod def _delete_ip_allocation(context, network_id, subnet_id, port_id, ip_address): @@ -1014,7 +1029,9 @@ class QuantumDbPluginV2(quantum_plugin_base_v2.QuantumPluginBaseV2): network_id=port['network_id'], port_id=port.id, ip_address=ip['ip_address'], - subnet_id=ip['subnet_id']) + subnet_id=ip['subnet_id'], + expiration=self._default_allocation_expiration() + ) context.session.add(allocated) return self._make_port_dict(port) diff --git a/quantum/db/dhcp_rpc_base.py b/quantum/db/dhcp_rpc_base.py index 0969161d1..de4dba5f9 100644 --- a/quantum/db/dhcp_rpc_base.py +++ b/quantum/db/dhcp_rpc_base.py @@ -171,3 +171,19 @@ class DhcpRpcCallbackMixin(object): del fixed_ips[i] break plugin.update_port(context, port['id'], dict(port=port)) + + def update_lease_expiration(self, context, **kwargs): + """Release the fixed_ip associated the subnet on a port.""" + host = kwargs.get('host') + network_id = kwargs.get('network_id') + ip_address = kwargs.get('ip_address') + lease_remaining = kwargs.get('lease_remaining') + + LOG.debug('Updating lease expiration for %s on network %s from %s.', + ip_address, network_id, host) + + context = augment_context(context) + plugin = manager.QuantumManager.get_plugin() + + plugin.update_fixed_ip_lease_expiration(context, network_id, + ip_address, lease_remaining) diff --git a/quantum/tests/unit/test_db_plugin.py b/quantum/tests/unit/test_db_plugin.py index 5bdbeb428..fa265134d 100644 --- a/quantum/tests/unit/test_db_plugin.py +++ b/quantum/tests/unit/test_db_plugin.py @@ -33,6 +33,7 @@ from quantum.common.test_lib import test_config from quantum import context from quantum.db import api as db from quantum.db import db_base_plugin_v2 +from quantum.db import models_v2 from quantum.manager import QuantumManager from quantum.openstack.common import cfg from quantum.openstack.common import timeutils @@ -1210,6 +1211,57 @@ class TestPortsV2(QuantumDbPluginV2TestCase): res = port_req.get_response(self.api) self.assertEquals(res.status_int, 422) + def test_default_allocation_expiration(self): + reference = datetime.datetime(2012, 8, 13, 23, 11, 0) + timeutils.utcnow.override_time = reference + + cfg.CONF.set_override('dhcp_lease_duration', 120) + expires = QuantumManager.get_plugin()._default_allocation_expiration() + timeutils.utcnow + cfg.CONF.reset() + timeutils.utcnow.override_time = None + self.assertEqual(expires, reference + datetime.timedelta(seconds=120)) + + def test_update_fixed_ip_lease_expiration(self): + cfg.CONF.set_override('dhcp_lease_duration', 10) + plugin = QuantumManager.get_plugin() + with self.subnet() as subnet: + with self.port(subnet=subnet) as port: + update_context = context.Context('', port['port']['tenant_id']) + plugin.update_fixed_ip_lease_expiration( + update_context, + subnet['subnet']['network_id'], + port['port']['fixed_ips'][0]['ip_address'], + 500) + + q = update_context.session.query(models_v2.IPAllocation) + q = q.filter_by( + port_id=port['port']['id'], + ip_address=port['port']['fixed_ips'][0]['ip_address']) + + ip_allocation = q.one() + + self.assertGreater( + ip_allocation.expiration - timeutils.utcnow(), + datetime.timedelta(seconds=10)) + + cfg.CONF.reset() + + def test_update_fixed_ip_lease_expiration_invalid_address(self): + cfg.CONF.set_override('dhcp_lease_duration', 10) + plugin = QuantumManager.get_plugin() + with self.subnet() as subnet: + with self.port(subnet=subnet) as port: + update_context = context.Context('', port['port']['tenant_id']) + with mock.patch.object(db_base_plugin_v2, 'LOG') as log: + plugin.update_fixed_ip_lease_expiration( + update_context, + subnet['subnet']['network_id'], + '255.255.255.0', + 120) + self.assertTrue(log.mock_calls) + cfg.CONF.reset() + class TestNetworksV2(QuantumDbPluginV2TestCase): # NOTE(cerberus): successful network update and delete are @@ -2107,14 +2159,3 @@ class TestSubnetsV2(QuantumDbPluginV2TestCase): req = self.new_delete_request('subnets', subnet['subnet']['id']) res = req.get_response(self.api) self.assertEquals(res.status_int, 204) - - def test_default_allocation_expiration(self): - reference = datetime.datetime(2012, 8, 13, 23, 11, 0) - timeutils.utcnow.override_time = reference - - cfg.CONF.set_override('dhcp_lease_duration', 120) - expires = QuantumManager.get_plugin()._default_allocation_expiration() - timeutils.utcnow - cfg.CONF.reset() - timeutils.utcnow.override_time = None - self.assertEqual(expires, reference + datetime.timedelta(seconds=120)) diff --git a/quantum/tests/unit/test_dhcp_agent.py b/quantum/tests/unit/test_dhcp_agent.py index a88b23d52..c6cd495b9 100644 --- a/quantum/tests/unit/test_dhcp_agent.py +++ b/quantum/tests/unit/test_dhcp_agent.py @@ -15,6 +15,7 @@ # License for the specific language governing permissions and limitations # under the License. +import socket import uuid import mock @@ -25,6 +26,7 @@ from quantum.agent.common import config from quantum.agent.linux import interface from quantum.common import exceptions from quantum.openstack.common import cfg +from quantum.openstack.common import jsonutils class FakeModel: @@ -71,6 +73,7 @@ fake_down_network = FakeModel('12345678-dddd-dddd-1234567890ab', class TestDhcpAgent(unittest.TestCase): def setUp(self): cfg.CONF.register_opts(dhcp_agent.DhcpAgent.OPTS) + cfg.CONF.register_opts(dhcp_agent.DhcpLeaseRelay.OPTS) self.driver_cls_p = mock.patch( 'quantum.agent.dhcp_agent.importutils.import_class') self.driver = mock.Mock(name='driver') @@ -104,11 +107,13 @@ class TestDhcpAgent(unittest.TestCase): dhcp = dhcp_agent.DhcpAgent(cfg.CONF) with mock.patch.object(dhcp, 'enable_dhcp_helper') as enable: - dhcp.run() - enable.assert_called_once_with('a') - plug.assert_called_once_with('q-plugin', mock.ANY) - mock_plugin.assert_has_calls( - [mock.call.get_active_networks()]) + with mock.patch.object(dhcp, 'lease_relay') as relay: + dhcp.run() + enable.assert_called_once_with('a') + plug.assert_called_once_with('q-plugin', mock.ANY) + mock_plugin.assert_has_calls( + [mock.call.get_active_networks()]) + relay.assert_has_mock_calls([mock.call.run()]) self.notification.assert_has_calls([mock.call.run_dispatch()]) @@ -348,6 +353,16 @@ class TestDhcpPluginApiProxy(unittest.TestCase): device_id='devid', host='foo') + def test_update_lease_expiration(self): + with mock.patch.object(self.proxy, 'cast') as mock_cast: + self.proxy.update_lease_expiration('netid', 'ipaddr', 1) + mock_cast.assert_called() + self.make_msg.assert_called_once_with('update_lease_expiration', + network_id='netid', + ip_address='ipaddr', + lease_remaining=1, + host='foo') + class TestNetworkCache(unittest.TestCase): def test_put_network(self): @@ -625,6 +640,138 @@ class TestDeviceManager(unittest.TestCase): self.assertEqual(dh.get_device_id(fake_network), expected) +class TestDhcpLeaseRelay(unittest.TestCase): + def setUp(self): + cfg.CONF.register_opts(dhcp_agent.DhcpLeaseRelay.OPTS) + self.unlink_p = mock.patch('os.unlink') + self.unlink = self.unlink_p.start() + + def tearDown(self): + self.unlink_p.stop() + + def test_init_relay_socket_path_no_prev_socket(self): + with mock.patch('os.path.exists') as exists: + exists.return_value = False + self.unlink.side_effect = OSError + + relay = dhcp_agent.DhcpLeaseRelay(None) + + self.unlink.assert_called_once_with( + cfg.CONF.dhcp_lease_relay_socket) + exists.assert_called_once_with(cfg.CONF.dhcp_lease_relay_socket) + + def test_init_relay_socket_path_prev_socket_exists(self): + with mock.patch('os.path.exists') as exists: + exists.return_value = False + + relay = dhcp_agent.DhcpLeaseRelay(None) + + self.unlink.assert_called_once_with( + cfg.CONF.dhcp_lease_relay_socket) + self.assertFalse(exists.called) + + def test_init_relay_socket_path_prev_socket_unlink_failure(self): + self.unlink.side_effect = OSError + with mock.patch('os.path.exists') as exists: + exists.return_value = True + with self.assertRaises(OSError): + relay = dhcp_agent.DhcpLeaseRelay(None) + + self.unlink.assert_called_once_with( + cfg.CONF.dhcp_lease_relay_socket) + exists.assert_called_once_with( + cfg.CONF.dhcp_lease_relay_socket) + + def test_validate_field_valid(self): + relay = dhcp_agent.DhcpLeaseRelay(None) + retval = relay._validate_field('1b', '\d[a-f]') + self.assertEqual(retval, '1b') + + def test_validate_field_invalid(self): + relay = dhcp_agent.DhcpLeaseRelay(None) + with self.assertRaises(ValueError): + retval = relay._validate_field('zz', '\d[a-f]') + + def test_handler_valid_data(self): + network_id = 'cccccccc-cccc-cccc-cccc-cccccccccccc' + ip_address = '192.168.1.9' + lease_remaining = 120 + + json_rep = jsonutils.dumps(dict(network_id=network_id, + lease_remaining=lease_remaining, + ip_address=ip_address)) + handler = mock.Mock() + mock_sock = mock.Mock() + mock_sock.recv.return_value = json_rep + + relay = dhcp_agent.DhcpLeaseRelay(handler) + + relay._handler(mock_sock, mock.Mock()) + mock_sock.assert_has_calls([mock.call.recv(1024), mock.call.close()]) + handler.called_once_with(network_id, ip_address, lease_remaining) + + def test_handler_invalid_data(self): + network_id = 'cccccccc-cccc-cccc-cccc-cccccccccccc' + ip_address = '192.168.x.x' + lease_remaining = 120 + + json_rep = jsonutils.dumps( + dict(network_id=network_id, + lease_remaining=lease_remaining, + ip_address=ip_address)) + + handler = mock.Mock() + mock_sock = mock.Mock() + mock_sock.recv.return_value = json_rep + + relay = dhcp_agent.DhcpLeaseRelay(handler) + + with mock.patch.object(relay, '_validate_field') as validate: + validate.side_effect = ValueError + + with mock.patch.object(dhcp_agent.LOG, 'warn') as log: + + relay._handler(mock_sock, mock.Mock()) + mock_sock.assert_has_calls( + [mock.call.recv(1024), mock.call.close()]) + self.assertFalse(handler.called) + self.assertTrue(log.called) + + def test_handler_other_exception(self): + network_id = 'cccccccc-cccc-cccc-cccc-cccccccccccc' + ip_address = '192.168.x.x' + lease_remaining = 120 + + json_rep = jsonutils.dumps( + dict(network_id=network_id, + lease_remaining=lease_remaining, + ip_address=ip_address)) + handler = mock.Mock() + mock_sock = mock.Mock() + mock_sock.recv.side_effect = Exception + + relay = dhcp_agent.DhcpLeaseRelay(handler) + + with mock.patch.object(dhcp_agent.LOG, 'exception') as log: + relay._handler(mock_sock, mock.Mock()) + mock_sock.assert_has_calls([mock.call.recv(1024)]) + self.assertFalse(handler.called) + self.assertTrue(log.called) + + def test_start(self): + with mock.patch.object(dhcp_agent, 'eventlet') as mock_eventlet: + handler = mock.Mock() + relay = dhcp_agent.DhcpLeaseRelay(handler) + relay.start() + + mock_eventlet.assert_has_calls( + [mock.call.listen(cfg.CONF.dhcp_lease_relay_socket, + family=socket.AF_UNIX), + mock.call.spawn(mock_eventlet.serve, + mock.call.listen.return_value, + relay._handler)]) + + class TestDictModel(unittest.TestCase): def test_basic_dict(self): d = dict(a=1, b=2) diff --git a/quantum/tests/unit/test_linux_dhcp.py b/quantum/tests/unit/test_linux_dhcp.py index 8bda152cd..20ca18d7c 100644 --- a/quantum/tests/unit/test_linux_dhcp.py +++ b/quantum/tests/unit/test_linux_dhcp.py @@ -16,6 +16,7 @@ # under the License. import os +import socket import tempfile import unittest2 as unittest @@ -24,6 +25,7 @@ import mock from quantum.agent.linux import dhcp from quantum.agent.common import config from quantum.openstack.common import cfg +from quantum.openstack.common import jsonutils class FakeIPAllocation: @@ -169,6 +171,8 @@ 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(args=args) self.conf.set_override('state_path', '') self.conf.use_namespaces = True @@ -330,7 +334,15 @@ class TestDnsmasq(TestBase): def mock_get_conf_file_name(kind, ensure_conf_dir=False): return '/dhcp/cccccccc-cccc-cccc-cccc-cccccccccccc/%s' % kind + def fake_argv(index): + if index == 0: + return '/usr/local/bin/quantum-dhcp-agent' + else: + raise IndexError + expected = [ + 'QUANTUM_RELAY_SOCKET_PATH=/dhcp/lease_relay', + 'QUANTUM_NETWORK_ID=cccccccc-cccc-cccc-cccc-cccccccccccc', 'ip', 'netns', 'exec', @@ -346,6 +358,8 @@ class TestDnsmasq(TestBase): '--pid-file=/dhcp/cccccccc-cccc-cccc-cccc-cccccccccccc/pid', '--dhcp-hostsfile=/dhcp/cccccccc-cccc-cccc-cccc-cccccccccccc/host', '--dhcp-optsfile=/dhcp/cccccccc-cccc-cccc-cccc-cccccccccccc/opts', + ('--dhcp-script=/usr/local/bin/quantum-dhcp-agent-' + 'dnsmasq-lease-update'), '--leasefile-ro', '--dhcp-range=set:tag0,192.168.0.0,static,120s', '--dhcp-range=set:tag1,fdca:3ba5:a17a:4ba3::,static,120s' @@ -366,11 +380,15 @@ class TestDnsmasq(TestBase): mocks['_output_opts_file'].return_value = ( '/dhcp/cccccccc-cccc-cccc-cccc-cccccccccccc/opts' ) - dm = dhcp.Dnsmasq(self.conf, FakeDualNetwork(), - device_delegate=delegate) - dm.spawn_process() - self.assertTrue(mocks['_output_opts_file'].called) - self.execute.assert_called_once_with(expected, root_helper='sudo') + + with mock.patch.object(dhcp.sys, 'argv') as argv: + argv.__getitem__.side_effect = fake_argv + dm = dhcp.Dnsmasq(self.conf, FakeDualNetwork(), + device_delegate=delegate) + dm.spawn_process() + self.assertTrue(mocks['_output_opts_file'].called) + self.execute.assert_called_once_with(expected, + root_helper='sudo') def test_spawn(self): self._test_spawn([]) @@ -424,3 +442,66 @@ class TestDnsmasq(TestBase): 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') + + def _test_lease_relay_script_helper(self, action, lease_remaining, + path_exists=True): + relay_path = '/dhcp/relay_socket' + network_id = 'cccccccc-cccc-cccc-cccc-cccccccccccc' + mac_address = 'aa:bb:cc:dd:ee:ff' + ip_address = '192.168.1.9' + + json_rep = jsonutils.dumps(dict(network_id=network_id, + lease_remaining=lease_remaining, + mac_address=mac_address, + ip_address=ip_address)) + + environ = { + 'QUANTUM_NETWORK_ID': network_id, + 'QUANTUM_RELAY_SOCKET_PATH': relay_path, + 'DNSMASQ_TIME_REMAINING': '120', + } + + def fake_environ(name, default=None): + return environ.get(name, default) + + with mock.patch('os.environ') as mock_environ: + mock_environ.get.side_effect = fake_environ + + with mock.patch.object(dhcp, 'sys') as mock_sys: + mock_sys.argv = [ + 'lease-update', + action, + mac_address, + ip_address, + ] + + with mock.patch('socket.socket') as mock_socket: + mock_conn = mock.Mock() + mock_socket.return_value = mock_conn + + with mock.patch('os.path.exists') as mock_exists: + mock_exists.return_value = path_exists + + dhcp.Dnsmasq.lease_update() + + mock_exists.assert_called_once_with(relay_path) + if path_exists: + mock_socket.assert_called_once_with( + socket.AF_UNIX, socket.SOCK_STREAM) + + mock_conn.assert_has_calls( + [mock.call.connect(relay_path), + mock.call.send(json_rep), + mock.call.close()]) + + def test_lease_relay_script_add(self): + self._test_lease_relay_script_helper('add', 120) + + def test_lease_relay_script_old(self): + self._test_lease_relay_script_helper('old', 120) + + def test_lease_relay_script_del(self): + self._test_lease_relay_script_helper('del', 0) + + def test_lease_relay_script_add_socket_missing(self): + self._test_lease_relay_script_helper('add', 120, False) diff --git a/quantum/tests/unit/test_linux_ip_lib.py b/quantum/tests/unit/test_linux_ip_lib.py index cb432e7f0..f39eb96d7 100644 --- a/quantum/tests/unit/test_linux_ip_lib.py +++ b/quantum/tests/unit/test_linux_ip_lib.py @@ -450,6 +450,16 @@ class TestIpNetnsCommand(TestIPCmdBase): 'link', 'list'], root_helper='sudo') + def test_execute_env_var_prepend(self): + self.parent.namespace = 'ns' + with mock.patch('quantum.agent.linux.utils.execute') as execute: + env = dict(FOO=1, BAR=2) + self.netns_cmd.execute(['ip', 'link', 'list'], env) + execute.assert_called_once_with( + ['FOO=1', 'BAR=2', 'ip', 'netns', 'exec', 'ns', 'ip', 'link', + 'list'], + root_helper='sudo') + class TestDeviceExists(unittest.TestCase): def test_device_exists(self): diff --git a/setup.py b/setup.py index 04664f2f1..1a59f0c63 100644 --- a/setup.py +++ b/setup.py @@ -98,6 +98,8 @@ setuptools.setup( entry_points={ 'console_scripts': [ 'quantum-dhcp-agent = quantum.agent.dhcp_agent:main', + 'quantum-dhcp-agent-dnsmasq-lease-update =' + 'quantum.agent.linux.dhcp:Dnsmasq.lease_update', 'quantum-l3-agent = quantum.agent.l3_nat_agent:main', 'quantum-linuxbridge-agent =' 'quantum.plugins.linuxbridge.agent.linuxbridge_quantum_agent:main',