Add lease expiration script support for dnsmasq

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
This commit is contained in:
Mark McClain 2012-08-16 17:34:44 -04:00
parent 9aacc68fa9
commit ed263e68e2
11 changed files with 491 additions and 26 deletions

View File

@ -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()

View 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')

View 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.

View 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)

View 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)

View 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)

View 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))

View 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:
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)

View 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'
)
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')
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)

View 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):

View File

@ -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',