]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
Add lease expiration script support for dnsmasq
authorMark McClain <mark.mcclain@dreamhost.com>
Thu, 16 Aug 2012 21:34:44 +0000 (17:34 -0400)
committerMark McClain <mark.mcclain@dreamhost.com>
Fri, 24 Aug 2012 16:14:45 +0000 (12:14 -0400)
Fixes bug 1022804

This is phase 2 of the bug fix.  This changeset adds support for dnsmasq
 --dhcp-script to notify Quantum of lease renewals.  Communication between
dnsmasq and the Quantum DHCP agent occurs via UNIX domain socket since dnsmasq
may run in a network namespace.  The DHCP agent is responsible for
relaying the updated lease expiration back the Quantum server.

Change-Id: If42b76bbb9ec7543e681e26b9add8eb1d7054eeb

bin/quantum-dhcp-agent-dnsmasq-lease-update [new file with mode: 0755]
quantum/agent/dhcp_agent.py
quantum/agent/linux/dhcp.py
quantum/agent/linux/ip_lib.py
quantum/db/db_base_plugin_v2.py
quantum/db/dhcp_rpc_base.py
quantum/tests/unit/test_db_plugin.py
quantum/tests/unit/test_dhcp_agent.py
quantum/tests/unit/test_linux_dhcp.py
quantum/tests/unit/test_linux_ip_lib.py
setup.py

diff --git a/bin/quantum-dhcp-agent-dnsmasq-lease-update b/bin/quantum-dhcp-agent-dnsmasq-lease-update
new file mode 100755 (executable)
index 0000000..d054fa8
--- /dev/null
@@ -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()
index bf6229bbf0873f61d5ea0c75b6b8ff0e4a896d08..6b3e30bfc724ecd19d6d31b002a2277cd96964fa 100644 (file)
@@ -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')
index 2ed335d09f86100f380917b053f3c507e2367fdf..720e732de74d7851bf006ce1cec17ecead66a27e 100644 (file)
@@ -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.
index 9d548b9d9537a2ed5bced3820b4577f7e82c0b25..006ad51b40a2012bd452e6c9951af954b3a4492f 100644 (file)
@@ -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)
 
index 8750a26ebe57e44ac21550227c02b5c0a3f13abc..ff2b08f3d626f089da2e4d94d5dada449d947693 100644 (file)
@@ -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)
index 0969161d1f19528a6f65862bee306ef997c21777..de4dba5f93a0b6f077ed9a27fb19ed3f6f9dfe60 100644 (file)
@@ -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)
index 5bdbeb4280dbdea2f1b3e8359b4cb5ca56299622..fa265134d827c2ce18201f36bb4487bd3ef8c066 100644 (file)
@@ -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))
index a88b23d52b46f81d47a3bff180624f5dc2fcf03d..c6cd495b907092d018be5446b21095828093565c 100644 (file)
@@ -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)
index 8bda152cd177def020be77807882c189c4a099f5..20ca18d7c2d68e3006d6d55ca81f34b4750ede62 100644 (file)
@@ -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)
index cb432e7f0f0836febb9201bd9926ad4320c3b34a..f39eb96d77d21885b854e7f1e55293ab1c9edf93 100644 (file)
@@ -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):
index 04664f2f1c35e4b5114705e975dac98bc0e83ecc..1a59f0c638133ed85c0b792ba92defcca6cc932b 100644 (file)
--- 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',