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:
parent
9aacc68fa9
commit
ed263e68e2
20
bin/quantum-dhcp-agent-dnsmasq-lease-update
Executable file
20
bin/quantum-dhcp-agent-dnsmasq-lease-update
Executable 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()
|
@ -16,6 +16,8 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
import re
|
||||||
import socket
|
import socket
|
||||||
import sys
|
import sys
|
||||||
import uuid
|
import uuid
|
||||||
@ -28,11 +30,13 @@ from quantum.agent.common import config
|
|||||||
from quantum.agent.linux import dhcp
|
from quantum.agent.linux import dhcp
|
||||||
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.api.v2 import attributes
|
||||||
from quantum.common import exceptions
|
from quantum.common import exceptions
|
||||||
from quantum.common import topics
|
from quantum.common import topics
|
||||||
from quantum.openstack.common import cfg
|
from quantum.openstack.common import cfg
|
||||||
from quantum.openstack.common import context
|
from quantum.openstack.common import context
|
||||||
from quantum.openstack.common import importutils
|
from quantum.openstack.common import importutils
|
||||||
|
from quantum.openstack.common import jsonutils
|
||||||
from quantum.openstack.common.rpc import proxy
|
from quantum.openstack.common.rpc import proxy
|
||||||
from quantum.version import version_string
|
from quantum.version import version_string
|
||||||
|
|
||||||
@ -59,6 +63,7 @@ class DhcpAgent(object):
|
|||||||
|
|
||||||
self.device_manager = DeviceManager(self.conf, self.plugin_rpc)
|
self.device_manager = DeviceManager(self.conf, self.plugin_rpc)
|
||||||
self.notifications = agent_rpc.NotificationDispatcher()
|
self.notifications = agent_rpc.NotificationDispatcher()
|
||||||
|
self.lease_relay = DhcpLeaseRelay(self.update_lease)
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
"""Activate the DHCP agent."""
|
"""Activate the DHCP agent."""
|
||||||
@ -66,6 +71,7 @@ class DhcpAgent(object):
|
|||||||
for network_id in self.plugin_rpc.get_active_networks():
|
for network_id in self.plugin_rpc.get_active_networks():
|
||||||
self.enable_dhcp_helper(network_id)
|
self.enable_dhcp_helper(network_id)
|
||||||
|
|
||||||
|
self.lease_relay.start()
|
||||||
self.notifications.run_dispatch(self)
|
self.notifications.run_dispatch(self)
|
||||||
|
|
||||||
def call_driver(self, action, network):
|
def call_driver(self, action, network):
|
||||||
@ -82,6 +88,10 @@ class DhcpAgent(object):
|
|||||||
except Exception, e:
|
except Exception, e:
|
||||||
LOG.warn('Unable to %s dhcp. Exception: %s' % (action, 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):
|
def enable_dhcp_helper(self, network_id):
|
||||||
"""Enable DHCP for a network that meets enabling criteria."""
|
"""Enable DHCP for a network that meets enabling criteria."""
|
||||||
network = self.plugin_rpc.get_network_info(network_id)
|
network = self.plugin_rpc.get_network_info(network_id)
|
||||||
@ -236,6 +246,16 @@ class DhcpPluginApi(proxy.RpcProxy):
|
|||||||
host=self.host),
|
host=self.host),
|
||||||
topic=self.topic)
|
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):
|
class NetworkCache(object):
|
||||||
"""Agent cache of the current network state."""
|
"""Agent cache of the current network state."""
|
||||||
@ -401,10 +421,75 @@ class DictModel(object):
|
|||||||
setattr(self, key, value)
|
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():
|
def main():
|
||||||
eventlet.monkey_patch()
|
eventlet.monkey_patch()
|
||||||
cfg.CONF.register_opts(DhcpAgent.OPTS)
|
cfg.CONF.register_opts(DhcpAgent.OPTS)
|
||||||
cfg.CONF.register_opts(DeviceManager.OPTS)
|
cfg.CONF.register_opts(DeviceManager.OPTS)
|
||||||
|
cfg.CONF.register_opts(DhcpLeaseRelay.OPTS)
|
||||||
cfg.CONF.register_opts(dhcp.OPTS)
|
cfg.CONF.register_opts(dhcp.OPTS)
|
||||||
cfg.CONF.register_opts(interface.OPTS)
|
cfg.CONF.register_opts(interface.OPTS)
|
||||||
cfg.CONF(args=sys.argv, project='quantum')
|
cfg.CONF(args=sys.argv, project='quantum')
|
||||||
|
@ -19,8 +19,11 @@ import abc
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import socket
|
||||||
import StringIO
|
import StringIO
|
||||||
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
import textwrap
|
||||||
|
|
||||||
import netaddr
|
import netaddr
|
||||||
|
|
||||||
@ -28,6 +31,7 @@ from quantum.agent.linux import ip_lib
|
|||||||
from quantum.agent.linux import utils
|
from quantum.agent.linux import utils
|
||||||
from quantum.openstack.common import cfg
|
from quantum.openstack.common import cfg
|
||||||
from quantum.openstack.common import importutils
|
from quantum.openstack.common import importutils
|
||||||
|
from quantum.openstack.common import jsonutils
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -190,12 +194,20 @@ class Dnsmasq(DhcpLocalProcess):
|
|||||||
|
|
||||||
_TAG_PREFIX = 'tag%d'
|
_TAG_PREFIX = 'tag%d'
|
||||||
|
|
||||||
|
QUANTUM_NETWORK_ID_KEY = 'QUANTUM_NETWORK_ID'
|
||||||
|
QUANTUM_RELAY_SOCKET_PATH_KEY = 'QUANTUM_RELAY_SOCKET_PATH'
|
||||||
|
|
||||||
def spawn_process(self):
|
def spawn_process(self):
|
||||||
"""Spawns a Dnsmasq process for the network."""
|
"""Spawns a Dnsmasq process for the network."""
|
||||||
interface_name = self.device_delegate.get_interface_name(self.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 = [
|
cmd = [
|
||||||
# TODO (mark): this is dhcpbridge script we'll need to know
|
|
||||||
# when an IP address has been released
|
|
||||||
'dnsmasq',
|
'dnsmasq',
|
||||||
'--no-hosts',
|
'--no-hosts',
|
||||||
'--no-resolv',
|
'--no-resolv',
|
||||||
@ -210,6 +222,7 @@ class Dnsmasq(DhcpLocalProcess):
|
|||||||
#'--dhcp-lease-max=%s' % ?,
|
#'--dhcp-lease-max=%s' % ?,
|
||||||
'--dhcp-hostsfile=%s' % self._output_hosts_file(),
|
'--dhcp-hostsfile=%s' % self._output_hosts_file(),
|
||||||
'--dhcp-optsfile=%s' % self._output_opts_file(),
|
'--dhcp-optsfile=%s' % self._output_opts_file(),
|
||||||
|
'--dhcp-script=%s' % self._lease_relay_script_path(),
|
||||||
'--leasefile-ro',
|
'--leasefile-ro',
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -237,8 +250,10 @@ class Dnsmasq(DhcpLocalProcess):
|
|||||||
if self.conf.use_namespaces:
|
if self.conf.use_namespaces:
|
||||||
ip_wrapper = ip_lib.IPWrapper(self.root_helper,
|
ip_wrapper = ip_lib.IPWrapper(self.root_helper,
|
||||||
namespace=self.network.id)
|
namespace=self.network.id)
|
||||||
ip_wrapper.netns.execute(cmd)
|
ip_wrapper.netns.execute(cmd, addl_env=env)
|
||||||
else:
|
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)
|
utils.execute(cmd, self.root_helper)
|
||||||
|
|
||||||
def reload_allocations(self):
|
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]))
|
replace_file(name, '\n'.join(['tag:%s,%s:%s,%s' % o for o in options]))
|
||||||
return name
|
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):
|
def replace_file(file_name, data):
|
||||||
"""Replaces the contents of file_name with data in a safe manner.
|
"""Replaces the contents of file_name with data in a safe manner.
|
||||||
|
@ -269,13 +269,14 @@ class IpNetnsCommand(IpCommandBase):
|
|||||||
['ip', 'netns', 'delete', name],
|
['ip', 'netns', 'delete', name],
|
||||||
root_helper=self._parent.root_helper)
|
root_helper=self._parent.root_helper)
|
||||||
|
|
||||||
def execute(self, cmds):
|
def execute(self, cmds, addl_env={}):
|
||||||
if not self._parent.root_helper:
|
if not self._parent.root_helper:
|
||||||
raise exceptions.SudoRequired()
|
raise exceptions.SudoRequired()
|
||||||
elif not self._parent.namespace:
|
elif not self._parent.namespace:
|
||||||
raise Exception(_('No namespace defined for parent'))
|
raise Exception(_('No namespace defined for parent'))
|
||||||
else:
|
else:
|
||||||
return utils.execute(
|
return utils.execute(
|
||||||
|
['%s=%s' % pair for pair in addl_env.items()] +
|
||||||
['ip', 'netns', 'exec', self._parent.namespace] + list(cmds),
|
['ip', 'netns', 'exec', self._parent.namespace] + list(cmds),
|
||||||
root_helper=self._parent.root_helper)
|
root_helper=self._parent.root_helper)
|
||||||
|
|
||||||
|
@ -281,6 +281,21 @@ class QuantumDbPluginV2(quantum_plugin_base_v2.QuantumPluginBaseV2):
|
|||||||
return (timeutils.utcnow() +
|
return (timeutils.utcnow() +
|
||||||
datetime.timedelta(seconds=cfg.CONF.dhcp_lease_duration))
|
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
|
@staticmethod
|
||||||
def _delete_ip_allocation(context, network_id, subnet_id, port_id,
|
def _delete_ip_allocation(context, network_id, subnet_id, port_id,
|
||||||
ip_address):
|
ip_address):
|
||||||
@ -1014,7 +1029,9 @@ class QuantumDbPluginV2(quantum_plugin_base_v2.QuantumPluginBaseV2):
|
|||||||
network_id=port['network_id'],
|
network_id=port['network_id'],
|
||||||
port_id=port.id,
|
port_id=port.id,
|
||||||
ip_address=ip['ip_address'],
|
ip_address=ip['ip_address'],
|
||||||
subnet_id=ip['subnet_id'])
|
subnet_id=ip['subnet_id'],
|
||||||
|
expiration=self._default_allocation_expiration()
|
||||||
|
)
|
||||||
context.session.add(allocated)
|
context.session.add(allocated)
|
||||||
|
|
||||||
return self._make_port_dict(port)
|
return self._make_port_dict(port)
|
||||||
|
@ -171,3 +171,19 @@ class DhcpRpcCallbackMixin(object):
|
|||||||
del fixed_ips[i]
|
del fixed_ips[i]
|
||||||
break
|
break
|
||||||
plugin.update_port(context, port['id'], dict(port=port))
|
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)
|
||||||
|
@ -33,6 +33,7 @@ from quantum.common.test_lib import test_config
|
|||||||
from quantum import context
|
from quantum import context
|
||||||
from quantum.db import api as db
|
from quantum.db import api as db
|
||||||
from quantum.db import db_base_plugin_v2
|
from quantum.db import db_base_plugin_v2
|
||||||
|
from quantum.db import models_v2
|
||||||
from quantum.manager import QuantumManager
|
from quantum.manager import QuantumManager
|
||||||
from quantum.openstack.common import cfg
|
from quantum.openstack.common import cfg
|
||||||
from quantum.openstack.common import timeutils
|
from quantum.openstack.common import timeutils
|
||||||
@ -1210,6 +1211,57 @@ class TestPortsV2(QuantumDbPluginV2TestCase):
|
|||||||
res = port_req.get_response(self.api)
|
res = port_req.get_response(self.api)
|
||||||
self.assertEquals(res.status_int, 422)
|
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):
|
class TestNetworksV2(QuantumDbPluginV2TestCase):
|
||||||
# NOTE(cerberus): successful network update and delete are
|
# NOTE(cerberus): successful network update and delete are
|
||||||
@ -2107,14 +2159,3 @@ class TestSubnetsV2(QuantumDbPluginV2TestCase):
|
|||||||
req = self.new_delete_request('subnets', subnet['subnet']['id'])
|
req = self.new_delete_request('subnets', subnet['subnet']['id'])
|
||||||
res = req.get_response(self.api)
|
res = req.get_response(self.api)
|
||||||
self.assertEquals(res.status_int, 204)
|
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))
|
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import socket
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
import mock
|
import mock
|
||||||
@ -25,6 +26,7 @@ from quantum.agent.common import config
|
|||||||
from quantum.agent.linux import interface
|
from quantum.agent.linux import interface
|
||||||
from quantum.common import exceptions
|
from quantum.common import exceptions
|
||||||
from quantum.openstack.common import cfg
|
from quantum.openstack.common import cfg
|
||||||
|
from quantum.openstack.common import jsonutils
|
||||||
|
|
||||||
|
|
||||||
class FakeModel:
|
class FakeModel:
|
||||||
@ -71,6 +73,7 @@ fake_down_network = FakeModel('12345678-dddd-dddd-1234567890ab',
|
|||||||
class TestDhcpAgent(unittest.TestCase):
|
class TestDhcpAgent(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
cfg.CONF.register_opts(dhcp_agent.DhcpAgent.OPTS)
|
cfg.CONF.register_opts(dhcp_agent.DhcpAgent.OPTS)
|
||||||
|
cfg.CONF.register_opts(dhcp_agent.DhcpLeaseRelay.OPTS)
|
||||||
self.driver_cls_p = mock.patch(
|
self.driver_cls_p = mock.patch(
|
||||||
'quantum.agent.dhcp_agent.importutils.import_class')
|
'quantum.agent.dhcp_agent.importutils.import_class')
|
||||||
self.driver = mock.Mock(name='driver')
|
self.driver = mock.Mock(name='driver')
|
||||||
@ -104,11 +107,13 @@ class TestDhcpAgent(unittest.TestCase):
|
|||||||
|
|
||||||
dhcp = dhcp_agent.DhcpAgent(cfg.CONF)
|
dhcp = dhcp_agent.DhcpAgent(cfg.CONF)
|
||||||
with mock.patch.object(dhcp, 'enable_dhcp_helper') as enable:
|
with mock.patch.object(dhcp, 'enable_dhcp_helper') as enable:
|
||||||
|
with mock.patch.object(dhcp, 'lease_relay') as relay:
|
||||||
dhcp.run()
|
dhcp.run()
|
||||||
enable.assert_called_once_with('a')
|
enable.assert_called_once_with('a')
|
||||||
plug.assert_called_once_with('q-plugin', mock.ANY)
|
plug.assert_called_once_with('q-plugin', mock.ANY)
|
||||||
mock_plugin.assert_has_calls(
|
mock_plugin.assert_has_calls(
|
||||||
[mock.call.get_active_networks()])
|
[mock.call.get_active_networks()])
|
||||||
|
relay.assert_has_mock_calls([mock.call.run()])
|
||||||
|
|
||||||
self.notification.assert_has_calls([mock.call.run_dispatch()])
|
self.notification.assert_has_calls([mock.call.run_dispatch()])
|
||||||
|
|
||||||
@ -348,6 +353,16 @@ class TestDhcpPluginApiProxy(unittest.TestCase):
|
|||||||
device_id='devid',
|
device_id='devid',
|
||||||
host='foo')
|
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):
|
class TestNetworkCache(unittest.TestCase):
|
||||||
def test_put_network(self):
|
def test_put_network(self):
|
||||||
@ -625,6 +640,138 @@ class TestDeviceManager(unittest.TestCase):
|
|||||||
self.assertEqual(dh.get_device_id(fake_network), expected)
|
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):
|
class TestDictModel(unittest.TestCase):
|
||||||
def test_basic_dict(self):
|
def test_basic_dict(self):
|
||||||
d = dict(a=1, b=2)
|
d = dict(a=1, b=2)
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import socket
|
||||||
import tempfile
|
import tempfile
|
||||||
import unittest2 as unittest
|
import unittest2 as unittest
|
||||||
|
|
||||||
@ -24,6 +25,7 @@ import mock
|
|||||||
from quantum.agent.linux import dhcp
|
from quantum.agent.linux import dhcp
|
||||||
from quantum.agent.common import config
|
from quantum.agent.common import config
|
||||||
from quantum.openstack.common import cfg
|
from quantum.openstack.common import cfg
|
||||||
|
from quantum.openstack.common import jsonutils
|
||||||
|
|
||||||
|
|
||||||
class FakeIPAllocation:
|
class FakeIPAllocation:
|
||||||
@ -169,6 +171,8 @@ 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',
|
||||||
|
default='$state_path/dhcp/lease_relay'))
|
||||||
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
|
||||||
@ -330,7 +334,15 @@ class TestDnsmasq(TestBase):
|
|||||||
def mock_get_conf_file_name(kind, ensure_conf_dir=False):
|
def mock_get_conf_file_name(kind, ensure_conf_dir=False):
|
||||||
return '/dhcp/cccccccc-cccc-cccc-cccc-cccccccccccc/%s' % kind
|
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 = [
|
expected = [
|
||||||
|
'QUANTUM_RELAY_SOCKET_PATH=/dhcp/lease_relay',
|
||||||
|
'QUANTUM_NETWORK_ID=cccccccc-cccc-cccc-cccc-cccccccccccc',
|
||||||
'ip',
|
'ip',
|
||||||
'netns',
|
'netns',
|
||||||
'exec',
|
'exec',
|
||||||
@ -346,6 +358,8 @@ class TestDnsmasq(TestBase):
|
|||||||
'--pid-file=/dhcp/cccccccc-cccc-cccc-cccc-cccccccccccc/pid',
|
'--pid-file=/dhcp/cccccccc-cccc-cccc-cccc-cccccccccccc/pid',
|
||||||
'--dhcp-hostsfile=/dhcp/cccccccc-cccc-cccc-cccc-cccccccccccc/host',
|
'--dhcp-hostsfile=/dhcp/cccccccc-cccc-cccc-cccc-cccccccccccc/host',
|
||||||
'--dhcp-optsfile=/dhcp/cccccccc-cccc-cccc-cccc-cccccccccccc/opts',
|
'--dhcp-optsfile=/dhcp/cccccccc-cccc-cccc-cccc-cccccccccccc/opts',
|
||||||
|
('--dhcp-script=/usr/local/bin/quantum-dhcp-agent-'
|
||||||
|
'dnsmasq-lease-update'),
|
||||||
'--leasefile-ro',
|
'--leasefile-ro',
|
||||||
'--dhcp-range=set:tag0,192.168.0.0,static,120s',
|
'--dhcp-range=set:tag0,192.168.0.0,static,120s',
|
||||||
'--dhcp-range=set:tag1,fdca:3ba5:a17a:4ba3::,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 = (
|
mocks['_output_opts_file'].return_value = (
|
||||||
'/dhcp/cccccccc-cccc-cccc-cccc-cccccccccccc/opts'
|
'/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(),
|
dm = dhcp.Dnsmasq(self.conf, FakeDualNetwork(),
|
||||||
device_delegate=delegate)
|
device_delegate=delegate)
|
||||||
dm.spawn_process()
|
dm.spawn_process()
|
||||||
self.assertTrue(mocks['_output_opts_file'].called)
|
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):
|
def test_spawn(self):
|
||||||
self._test_spawn([])
|
self._test_spawn([])
|
||||||
@ -424,3 +442,66 @@ class TestDnsmasq(TestBase):
|
|||||||
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')
|
||||||
|
|
||||||
|
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)
|
||||||
|
@ -450,6 +450,16 @@ class TestIpNetnsCommand(TestIPCmdBase):
|
|||||||
'link', 'list'],
|
'link', 'list'],
|
||||||
root_helper='sudo')
|
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):
|
class TestDeviceExists(unittest.TestCase):
|
||||||
def test_device_exists(self):
|
def test_device_exists(self):
|
||||||
|
2
setup.py
2
setup.py
@ -98,6 +98,8 @@ setuptools.setup(
|
|||||||
entry_points={
|
entry_points={
|
||||||
'console_scripts': [
|
'console_scripts': [
|
||||||
'quantum-dhcp-agent = quantum.agent.dhcp_agent:main',
|
'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-l3-agent = quantum.agent.l3_nat_agent:main',
|
||||||
'quantum-linuxbridge-agent ='
|
'quantum-linuxbridge-agent ='
|
||||||
'quantum.plugins.linuxbridge.agent.linuxbridge_quantum_agent:main',
|
'quantum.plugins.linuxbridge.agent.linuxbridge_quantum_agent:main',
|
||||||
|
Loading…
Reference in New Issue
Block a user