implement dhcp agent for quantum
blueprint: quantum-dhcp This change adds an agent to manage DHCP for Quantum networks Change-Id: If3c62965550dc0b0a7982b01d3468e2e07e2b775
This commit is contained in:
parent
02f7d25f49
commit
37d09064ff
20
bin/quantum-dhcp-agent
Executable file
20
bin/quantum-dhcp-agent
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.dhcp_agent import main
|
||||
main()
|
38
etc/dhcp_agent.ini
Normal file
38
etc/dhcp_agent.ini
Normal file
@ -0,0 +1,38 @@
|
||||
[DEFAULT]
|
||||
# Show debugging output in log (sets DEBUG log level output)
|
||||
# debug = true
|
||||
|
||||
# Where to store dnsmasq state files. This directory must be writable by the
|
||||
# user executing the agent. The value below is compatible with a default
|
||||
# devstack installation.
|
||||
state_path = /opt/stack/data
|
||||
|
||||
|
||||
# The DHCP requires that an inteface driver be set. Choose the one that best
|
||||
# matches you plugin.
|
||||
|
||||
# OVS
|
||||
interface_driver = quantum.agent.linux.interface.OVSInterfaceDriver
|
||||
# LinuxBridge
|
||||
#interface_driver = quantum.agent.linux.interface.BridgeInterfaceDriver
|
||||
|
||||
# The agent can use other DHCP drivers. Dnsmasq is the simplest and requires
|
||||
# no additional setup of the DHCP server.
|
||||
dhcp_driver = quantum.agent.linux.dhcp.Dnsmasq
|
||||
|
||||
#
|
||||
# Temporary F2 variables until the Agent <> Quantum Server is reworked in F3
|
||||
#
|
||||
# The database used by the OVS Quantum plugin
|
||||
db_connection = mysql://root:password@localhost/ovs_quantum?charset=utf8
|
||||
|
||||
# The database used by the LinuxBridge Quantum plugin
|
||||
#db_connection = mysql://root:password@localhost/quantum_linux_bridge
|
||||
|
||||
# The Quantum user information for accessing the Quantum API.
|
||||
auth_url = http://localhost:35357/v2.0
|
||||
auth_region = RegionOne
|
||||
admin_tenant_name = service
|
||||
admin_user = quantum
|
||||
admin_password = password
|
||||
|
16
quantum/agent/common/__init__.py
Normal file
16
quantum/agent/common/__init__.py
Normal file
@ -0,0 +1,16 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 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.
|
36
quantum/agent/common/config.py
Normal file
36
quantum/agent/common/config.py
Normal file
@ -0,0 +1,36 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 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.
|
||||
|
||||
import os
|
||||
|
||||
from quantum.common import config
|
||||
from quantum.openstack.common import cfg
|
||||
|
||||
|
||||
def setup_conf():
|
||||
bind_opts = [
|
||||
cfg.StrOpt('state_path',
|
||||
default='/var/lib/quantum',
|
||||
help='Top-level directory for maintaining dhcp state'),
|
||||
]
|
||||
|
||||
conf = cfg.CommonConfigOpts()
|
||||
conf.register_opts(bind_opts)
|
||||
return conf
|
||||
|
||||
# add a logging setup method here for convenience
|
||||
setup_logging = config.setup_logging
|
361
quantum/agent/dhcp_agent.py
Normal file
361
quantum/agent/dhcp_agent.py
Normal file
@ -0,0 +1,361 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 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.
|
||||
|
||||
import collections
|
||||
import logging
|
||||
import socket
|
||||
import sys
|
||||
import time
|
||||
import uuid
|
||||
|
||||
from sqlalchemy.ext import sqlsoup
|
||||
|
||||
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.common import exceptions
|
||||
from quantum.openstack.common import cfg
|
||||
from quantum.openstack.common import importutils
|
||||
from quantum.version import version_string
|
||||
from quantumclient.v2_0 import client
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
State = collections.namedtuple('State',
|
||||
['networks', 'subnet_hashes', 'ipalloc_hashes'])
|
||||
|
||||
|
||||
class DhcpAgent(object):
|
||||
OPTS = [
|
||||
cfg.StrOpt('db_connection', default=''),
|
||||
cfg.StrOpt('root_helper', default='sudo'),
|
||||
cfg.StrOpt('dhcp_driver',
|
||||
default='quantum.agent.linux.dhcp.Dnsmasq',
|
||||
help="The driver used to manage the DHCP server."),
|
||||
cfg.IntOpt('polling_interval',
|
||||
default=3,
|
||||
help="The time in seconds between state poll requests."),
|
||||
cfg.IntOpt('reconnect_interval',
|
||||
default=5,
|
||||
help="The time in seconds between db reconnect attempts.")
|
||||
]
|
||||
|
||||
def __init__(self, conf):
|
||||
self.conf = conf
|
||||
self.dhcp_driver_cls = importutils.import_class(conf.dhcp_driver)
|
||||
self.db = None
|
||||
self.polling_interval = conf.polling_interval
|
||||
self.reconnect_interval = conf.reconnect_interval
|
||||
self._run = True
|
||||
self.prev_state = State(set(), set(), set())
|
||||
|
||||
def daemon_loop(self):
|
||||
while self._run:
|
||||
delta = self.get_network_state_delta()
|
||||
if delta is None:
|
||||
continue
|
||||
|
||||
for network in delta.get('new', []):
|
||||
self.call_driver('enable', network)
|
||||
for network in delta.get('updated', []):
|
||||
self.call_driver('reload_allocations', network)
|
||||
for network in delta.get('deleted', []):
|
||||
self.call_driver('disable', network)
|
||||
|
||||
time.sleep(self.polling_interval)
|
||||
|
||||
def _state_builder(self):
|
||||
"""Polls the Quantum database and returns a represenation
|
||||
of the network state.
|
||||
|
||||
The value returned is a State tuple that contains three sets:
|
||||
networks, subnet_hashes, and ipalloc_hashes.
|
||||
|
||||
The hash sets are a tuple that contains the computed signature of the
|
||||
obejct's metadata and the network that owns it. Signatures are used
|
||||
because the objects metadata can change. Python's built-in hash
|
||||
function is used on the string repr to compute the metadata signature.
|
||||
"""
|
||||
try:
|
||||
if self.db is None:
|
||||
time.sleep(self.reconnect_interval)
|
||||
self.db = sqlsoup.SqlSoup(self.conf.db_connection)
|
||||
LOG.info("Connecting to database \"%s\" on %s" %
|
||||
(self.db.engine.url.database,
|
||||
self.db.engine.url.host))
|
||||
else:
|
||||
# we have to commit to get the latest view
|
||||
self.db.commit()
|
||||
|
||||
subnets = {}
|
||||
subnet_hashes = set()
|
||||
for subnet in self.db.subnets.all():
|
||||
subnet_hashes.add((hash(str(subnet)), subnet.network_id))
|
||||
subnets[subnet.id] = subnet.network_id
|
||||
|
||||
ipalloc_hashes = set([(hash(str(a)), subnets[a.subnet_id])
|
||||
for a in self.db.ipallocations.all()])
|
||||
|
||||
networks = set(subnets.itervalues())
|
||||
|
||||
return State(networks, subnet_hashes, ipalloc_hashes)
|
||||
|
||||
except Exception, e:
|
||||
LOG.warn('Unable to get network state delta. Exception: %s' % e)
|
||||
self.db = None
|
||||
return None
|
||||
|
||||
def get_network_state_delta(self):
|
||||
"""Return a dict containing the sets of networks that are new,
|
||||
updated, and deleted."""
|
||||
delta = {}
|
||||
state = self._state_builder()
|
||||
|
||||
if state is None:
|
||||
return None
|
||||
|
||||
# determine the new/deleted networks
|
||||
delta['deleted'] = self.prev_state.networks - state.networks
|
||||
delta['new'] = state.networks - self.prev_state.networks
|
||||
|
||||
# Get the networks that have subnets added or deleted.
|
||||
# The change candidates are the net_id portion of the symmetric diff
|
||||
# between the sets of (subnet_hash,net_id)
|
||||
candidates = set(
|
||||
[h[1] for h in
|
||||
(state.subnet_hashes ^ self.prev_state.subnet_hashes)]
|
||||
)
|
||||
|
||||
# Update with the networks that have had allocations added/deleted.
|
||||
# change candidates are the net_id portion of the symmetric diff
|
||||
# between the sets of (alloc_hash,net_id)
|
||||
candidates.update(
|
||||
[h[1] for h in
|
||||
(state.ipalloc_hashes ^ self.prev_state.ipalloc_hashes)]
|
||||
)
|
||||
|
||||
# the updated set will contain new and deleted networks, so remove them
|
||||
delta['updated'] = candidates - delta['new'] - delta['deleted']
|
||||
|
||||
self.prev_state = state
|
||||
|
||||
return delta
|
||||
|
||||
def call_driver(self, action, network_id):
|
||||
"""Invoke an action on a DHCP driver instance."""
|
||||
try:
|
||||
# the Driver expects something that is duck typed similar to
|
||||
# the base models. Augmenting will add support to the SqlSoup
|
||||
# result, so that the Driver does have to concern itself with our
|
||||
# db schema.
|
||||
network = AugmentingWrapper(
|
||||
self.db.networks.filter_by(id=network_id).one(),
|
||||
self.db
|
||||
)
|
||||
driver = self.dhcp_driver_cls(self.conf,
|
||||
network,
|
||||
self.conf.root_helper,
|
||||
DeviceManager(self.conf, self.db))
|
||||
getattr(driver, action)()
|
||||
|
||||
except Exception, e:
|
||||
LOG.warn('Unable to %s dhcp. Exception: %s' % (action, e))
|
||||
|
||||
# Manipulate the state so the action will be attempted on next
|
||||
# loop iteration.
|
||||
if action == 'disable':
|
||||
# adding to prev state means we'll try to delete it next time
|
||||
self.prev_state.networks.add(network_id)
|
||||
else:
|
||||
# removing means it will look like new next time
|
||||
self.prev_state.networks.remove(network_id)
|
||||
|
||||
|
||||
class DeviceManager(object):
|
||||
OPTS = [
|
||||
cfg.StrOpt('admin_user'),
|
||||
cfg.StrOpt('admin_password'),
|
||||
cfg.StrOpt('admin_tenant_name'),
|
||||
cfg.StrOpt('auth_url'),
|
||||
cfg.StrOpt('auth_strategy', default='keystone'),
|
||||
cfg.StrOpt('auth_region'),
|
||||
cfg.StrOpt('interface_driver',
|
||||
help="The driver used to manage the virtual interface.")
|
||||
]
|
||||
|
||||
def __init__(self, conf, db):
|
||||
self.conf = conf
|
||||
self.db = db
|
||||
|
||||
if not conf.interface_driver:
|
||||
LOG.error(_('You must specify an interface driver'))
|
||||
self.driver = importutils.import_object(conf.interface_driver, conf)
|
||||
|
||||
def get_interface_name(self, network):
|
||||
return ('tap' + network.id)[:self.driver.DEV_NAME_LEN]
|
||||
|
||||
def get_device_id(self, network):
|
||||
# There could be more than one dhcp server per network, so create
|
||||
# a device id that combines host and network ids
|
||||
|
||||
host_uuid = uuid.uuid5(uuid.NAMESPACE_DNS, socket.gethostname())
|
||||
return 'dhcp%s-%s' % (host_uuid, network.id)
|
||||
|
||||
def setup(self, network, reuse_existing=False):
|
||||
interface_name = self.get_interface_name(network)
|
||||
port = self._get_or_create_port(network)
|
||||
|
||||
if ip_lib.device_exists(interface_name):
|
||||
if not reuse_existing:
|
||||
raise exceptions.PreexistingDeviceFailure(
|
||||
dev_name=interface_name)
|
||||
|
||||
LOG.debug(_('Reusing existing device: %s.') % interface_name)
|
||||
else:
|
||||
self.driver.plug(network.id,
|
||||
port.id,
|
||||
interface_name,
|
||||
port.mac_address)
|
||||
self.driver.init_l3(port, interface_name)
|
||||
|
||||
def destroy(self, network):
|
||||
self.driver.unplug(self.get_interface_name(network))
|
||||
|
||||
def _get_or_create_port(self, network):
|
||||
# todo (mark): reimplement using RPC
|
||||
# Usage of client lib is a temporary measure.
|
||||
|
||||
try:
|
||||
device_id = self.get_device_id(network)
|
||||
port_obj = self.db.ports.filter_by(device_id=device_id).one()
|
||||
port = AugmentingWrapper(port_obj, self.db)
|
||||
except sqlsoup.SQLAlchemyError, e:
|
||||
port = self._create_port(network)
|
||||
|
||||
return port
|
||||
|
||||
def _create_port(self, network):
|
||||
# todo (mark): reimplement using RPC
|
||||
# Usage of client lib is a temporary measure.
|
||||
|
||||
quantum = client.Client(
|
||||
username=self.conf.admin_user,
|
||||
password=self.conf.admin_password,
|
||||
tenant_name=self.conf.admin_tenant_name,
|
||||
auth_url=self.conf.auth_url,
|
||||
auth_strategy=self.conf.auth_strategy,
|
||||
auth_region=self.conf.auth_region
|
||||
)
|
||||
|
||||
body = dict(port=dict(
|
||||
admin_state_up=True,
|
||||
device_id=self.get_device_id(network),
|
||||
network_id=network.id,
|
||||
tenant_id=network.tenant_id,
|
||||
fixed_ips=[dict(subnet_id=s.id) for s in network.subnets]))
|
||||
port_dict = quantum.create_port(body)['port']
|
||||
|
||||
# we have to call commit since the port was created in outside of
|
||||
# our current transaction
|
||||
self.db.commit()
|
||||
|
||||
port = AugmentingWrapper(
|
||||
self.db.ports.filter_by(id=port_dict['id']).one(),
|
||||
self.db)
|
||||
return port
|
||||
|
||||
|
||||
class PortModel(object):
|
||||
def __init__(self, port_dict):
|
||||
self.__dict__.update(port_dict)
|
||||
|
||||
|
||||
class AugmentingWrapper(object):
|
||||
"""A wrapper that augments Sqlsoup results so that they look like the
|
||||
base v2 db model.
|
||||
"""
|
||||
|
||||
MAPPING = {
|
||||
'networks': {'subnets': 'subnets', 'ports': 'ports'},
|
||||
'subnets': {'allocations': 'ipallocations'},
|
||||
'ports': {'fixed_ips': 'ipallocations'},
|
||||
|
||||
}
|
||||
|
||||
def __init__(self, obj, db):
|
||||
self.obj = obj
|
||||
self.db = db
|
||||
|
||||
def __repr__(self):
|
||||
return repr(self.obj)
|
||||
|
||||
def __getattr__(self, name):
|
||||
"""Executes a dynamic lookup of attributes to make SqlSoup results
|
||||
mimic the same structure as the v2 db models.
|
||||
|
||||
The actual models could not be used because they're dependent on the
|
||||
plugin and the agent is not tied to any plugin structure.
|
||||
|
||||
If .subnet, is accessed, the wrapper will return a subnet
|
||||
object if this instance has a subnet_id attribute.
|
||||
|
||||
If the _id attribute does not exists then wrapper will check MAPPING
|
||||
to see if a reverse relationship exists. If so, a wrapped result set
|
||||
will be returned.
|
||||
"""
|
||||
|
||||
try:
|
||||
return getattr(self.obj, name)
|
||||
except:
|
||||
pass
|
||||
|
||||
id_attr = '%s_id' % name
|
||||
if hasattr(self.obj, id_attr):
|
||||
args = {'id': getattr(self.obj, id_attr)}
|
||||
return AugmentingWrapper(
|
||||
getattr(self.db, '%ss' % name).filter_by(**args).one(),
|
||||
self.db
|
||||
)
|
||||
try:
|
||||
attr_name = self.MAPPING[self.obj._table.name][name]
|
||||
arg_name = '%s_id' % self.obj._table.name[:-1]
|
||||
args = {arg_name: self.obj.id}
|
||||
|
||||
return [AugmentingWrapper(o, self.db) for o in
|
||||
getattr(self.db, attr_name).filter_by(**args).all()]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
raise AttributeError
|
||||
|
||||
|
||||
def main():
|
||||
conf = config.setup_conf()
|
||||
conf.register_opts(DhcpAgent.OPTS)
|
||||
conf.register_opts(DeviceManager.OPTS)
|
||||
conf.register_opts(dhcp.OPTS)
|
||||
conf.register_opts(interface.OPTS)
|
||||
conf(sys.argv)
|
||||
config.setup_logging(conf)
|
||||
|
||||
mgr = DhcpAgent(conf)
|
||||
mgr.daemon_loop()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
270
quantum/agent/linux/dhcp.py
Normal file
270
quantum/agent/linux/dhcp.py
Normal file
@ -0,0 +1,270 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 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.
|
||||
|
||||
import abc
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import StringIO
|
||||
import tempfile
|
||||
|
||||
import netaddr
|
||||
|
||||
from quantum.agent.linux import utils
|
||||
from quantum.openstack.common import cfg
|
||||
from quantum.openstack.common import importutils
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
OPTS = [
|
||||
cfg.StrOpt('dhcp_confs',
|
||||
default='$state_path/dhcp',
|
||||
help='Location to store DHCP server config files'),
|
||||
cfg.IntOpt('dhcp_lease_time',
|
||||
default=120,
|
||||
help='Lifetime of a DHCP lease in seconds'),
|
||||
cfg.StrOpt('dhcp_domain',
|
||||
default='openstacklocal',
|
||||
help='Domain to use for building the hostnames'),
|
||||
cfg.StrOpt('dnsmasq_config_file',
|
||||
help='Override the default dnsmasq settings with this file'),
|
||||
cfg.StrOpt('dnsmasq_dns_server',
|
||||
help='Use another DNS server before any in /etc/resolv.conf.'),
|
||||
]
|
||||
|
||||
IPV4 = 4
|
||||
IPV6 = 6
|
||||
UDP = 'udp'
|
||||
TCP = 'tcp'
|
||||
DNS_PORT = 53
|
||||
DHCPV4_PORT = 67
|
||||
DHCPV6_PORT = 467
|
||||
|
||||
|
||||
class DhcpBase(object):
|
||||
__metaclass__ = abc.ABCMeta
|
||||
|
||||
def __init__(self, conf, network, root_helper='sudo',
|
||||
device_delegate=None):
|
||||
self.conf = conf
|
||||
self.network = network
|
||||
self.root_helper = root_helper
|
||||
self.device_delegate = device_delegate
|
||||
|
||||
@abc.abstractmethod
|
||||
def enable(self):
|
||||
"""Enables DHCP for this network."""
|
||||
|
||||
@abc.abstractmethod
|
||||
def disable(self):
|
||||
"""Disable dhcp for this network."""
|
||||
|
||||
def restart(self):
|
||||
"""Restart the dhcp service for the network."""
|
||||
self.disable()
|
||||
self.enable()
|
||||
|
||||
@abc.abstractproperty
|
||||
def active(self):
|
||||
"""Boolean representing the running state of the DHCP server."""
|
||||
|
||||
@abc.abstractmethod
|
||||
def reload_allocations(self):
|
||||
"""Force the DHCP server to reload the assignment database."""
|
||||
|
||||
|
||||
class DhcpLocalProcess(DhcpBase):
|
||||
PORTS = []
|
||||
|
||||
def enable(self):
|
||||
"""Enables DHCP for this network by spawning a local process."""
|
||||
if self.active:
|
||||
self.reload_allocations()
|
||||
return
|
||||
|
||||
self.device_delegate.setup(self.network, reuse_existing=True)
|
||||
self.spawn_process()
|
||||
|
||||
def disable(self):
|
||||
"""Disable DHCP for this network by killing the local process."""
|
||||
pid = self.pid
|
||||
|
||||
if self.active:
|
||||
utils.execute(['kill', '-9', pid], self.root_helper)
|
||||
self.device_delegate.destroy(self.network)
|
||||
elif pid:
|
||||
LOG.debug(_('DHCP for %s pid %d is stale, ignoring command') %
|
||||
(self.network.id, pid))
|
||||
else:
|
||||
LOG.debug(_('No DHCP started for %s') % self.network.id)
|
||||
|
||||
def get_conf_file_name(self, kind, ensure_conf_dir=False):
|
||||
"""Returns the file name for a given kind of config file."""
|
||||
confs_dir = os.path.abspath(os.path.normpath(self.conf.dhcp_confs))
|
||||
conf_dir = os.path.join(confs_dir, self.network.id)
|
||||
if ensure_conf_dir:
|
||||
if not os.path.isdir(conf_dir):
|
||||
os.makedirs(conf_dir, 0755)
|
||||
|
||||
return os.path.join(conf_dir, kind)
|
||||
|
||||
def _get_value_from_conf_file(self, kind, converter=None):
|
||||
"""A helper function to read a value from one of the state files."""
|
||||
file_name = self.get_conf_file_name(kind)
|
||||
msg = _('Error while reading %s')
|
||||
|
||||
try:
|
||||
with open(file_name, 'r') as f:
|
||||
try:
|
||||
return converter and converter(f.read()) or f.read()
|
||||
except ValueError, e:
|
||||
msg = _('Unable to convert value in %s')
|
||||
except IOError, e:
|
||||
msg = _('Unable to access %s')
|
||||
|
||||
LOG.debug(msg % file_name)
|
||||
return None
|
||||
|
||||
@property
|
||||
def pid(self):
|
||||
"""Last known pid for the DHCP process spawned for this network."""
|
||||
return self._get_value_from_conf_file('pid', int)
|
||||
|
||||
@property
|
||||
def active(self):
|
||||
pid = self.pid
|
||||
cmd = ['cat', '/proc/%s/cmdline' % pid]
|
||||
try:
|
||||
return self.network.id in utils.execute(cmd, self.root_helper)
|
||||
except RuntimeError, e:
|
||||
return False
|
||||
|
||||
@abc.abstractmethod
|
||||
def spawn_process(self):
|
||||
pass
|
||||
|
||||
|
||||
class Dnsmasq(DhcpLocalProcess):
|
||||
# The ports that need to be opened when security policies are active
|
||||
# on the Quantum port used for DHCP. These are provided as a convenience
|
||||
# for users of this class.
|
||||
PORTS = {IPV4: [(UDP, DNS_PORT), (TCP, DNS_PORT), (UDP, DHCPV4_PORT)],
|
||||
IPV6: [(UDP, DNS_PORT), (TCP, DNS_PORT), (UDP, DHCPV6_PORT)],
|
||||
}
|
||||
|
||||
_TAG_PREFIX = 'tag%d'
|
||||
|
||||
def spawn_process(self):
|
||||
"""Spawns a Dnsmasq process for the network."""
|
||||
interface_name = self.device_delegate.get_interface_name(self.network)
|
||||
cmd = [
|
||||
'NETWORK_ID=%s' % self.network.id,
|
||||
# TODO (mark): this is dhcpbridge script we'll need to know
|
||||
# when an IP address has been released
|
||||
'dnsmasq',
|
||||
'--no-hosts',
|
||||
'--no-resolv',
|
||||
'--strict-order',
|
||||
'--bind-interfaces',
|
||||
'--interface=%s' % interface_name,
|
||||
'--except-interface=lo',
|
||||
'--domain=%s' % self.conf.dhcp_domain,
|
||||
'--pid-file=%s' % self.get_conf_file_name('pid',
|
||||
ensure_conf_dir=True),
|
||||
#TODO (mark): calculate value from cidr (defaults to 150)
|
||||
#'--dhcp-lease-max=%s' % ?,
|
||||
'--dhcp-hostsfile=%s' % self._output_hosts_file(),
|
||||
'--dhcp-optsfile=%s' % self._output_opts_file(),
|
||||
'--leasefile-ro',
|
||||
]
|
||||
|
||||
for i, subnet in enumerate(self.network.subnets):
|
||||
if subnet.ip_version == 4:
|
||||
mode = 'static'
|
||||
else:
|
||||
# TODO (mark): how do we indicate other options
|
||||
# ra-only, slaac, ra-nameservers, and ra-stateless.
|
||||
mode = 'static'
|
||||
cmd.append('--dhcp-range=set:%s,%s,%s,%ss' %
|
||||
(self._TAG_PREFIX % i,
|
||||
netaddr.IPNetwork(subnet.cidr).network,
|
||||
mode,
|
||||
self.conf.dhcp_lease_time))
|
||||
|
||||
if self.conf.dnsmasq_config_file:
|
||||
cmd.append('--conf-file=%s' % self.conf.dnsmasq_config_file)
|
||||
if self.conf.dnsmasq_dns_server:
|
||||
cmd.append('--server=%s' % self.conf.dnsmasq_dns_server)
|
||||
|
||||
utils.execute(cmd, self.root_helper)
|
||||
|
||||
def reload_allocations(self):
|
||||
"""Rebuilds the dnsmasq config and signal the dnsmasq to reload."""
|
||||
self._output_hosts_file()
|
||||
self._output_opts_file()
|
||||
utils.execute(['kill', '-HUP', self.pid], self.root_helper)
|
||||
LOG.debug(_('Reloading allocations for network: %s') % self.network.id)
|
||||
|
||||
def _output_hosts_file(self):
|
||||
"""Writes a dnsmasq compatible hosts file."""
|
||||
r = re.compile('[:.]')
|
||||
buf = StringIO.StringIO()
|
||||
|
||||
for port in self.network.ports:
|
||||
for alloc in port.fixed_ips:
|
||||
name = '%s.%s' % (r.sub('-', alloc.ip_address),
|
||||
self.conf.dhcp_domain)
|
||||
buf.write('%s,%s,%s\n' %
|
||||
(port.mac_address, name, alloc.ip_address))
|
||||
|
||||
name = self.get_conf_file_name('host')
|
||||
replace_file(name, buf.getvalue())
|
||||
return name
|
||||
|
||||
def _output_opts_file(self):
|
||||
"""Write a dnsmasq compatible options file."""
|
||||
# TODO (mark): add support for nameservers
|
||||
options = []
|
||||
for i, subnet in enumerate(self.network.subnets):
|
||||
if subnet.ip_version == 6:
|
||||
continue
|
||||
else:
|
||||
options.append((self._TAG_PREFIX % i,
|
||||
'option',
|
||||
'router',
|
||||
subnet.gateway_ip))
|
||||
|
||||
name = self.get_conf_file_name('opts')
|
||||
replace_file(name, '\n'.join(['tag:%s,%s:%s,%s' % o for o in options]))
|
||||
return name
|
||||
|
||||
|
||||
def replace_file(file_name, data):
|
||||
"""Replaces the contents of file_name with data in a safe manner.
|
||||
|
||||
First write to a temp file and then rename. Since POSIX renames are
|
||||
atomic, the file is unlikely to be corrupted by competing writes.
|
||||
|
||||
We create the tempfile on the same device to ensure that it can be renamed.
|
||||
"""
|
||||
|
||||
base_dir = os.path.dirname(os.path.abspath(file_name))
|
||||
tmp_file = tempfile.NamedTemporaryFile('w+', dir=base_dir, delete=False)
|
||||
tmp_file.write(data)
|
||||
tmp_file.close()
|
||||
os.chmod(tmp_file.name, 0644)
|
||||
os.rename(tmp_file.name, file_name)
|
174
quantum/agent/linux/interface.py
Normal file
174
quantum/agent/linux/interface.py
Normal file
@ -0,0 +1,174 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 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.
|
||||
|
||||
import abc
|
||||
import logging
|
||||
|
||||
import netaddr
|
||||
|
||||
from quantum.agent.linux import ip_lib
|
||||
from quantum.agent.linux import ovs_lib
|
||||
from quantum.agent.linux import utils
|
||||
from quantum.common import exceptions
|
||||
from quantum.openstack.common import cfg
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
OPTS = [
|
||||
cfg.StrOpt('ovs_integration_bridge',
|
||||
default='br-int',
|
||||
help='Name of Open vSwitch bridge to use'),
|
||||
cfg.StrOpt('network_device_mtu',
|
||||
help='MTU setting for device.'),
|
||||
]
|
||||
|
||||
|
||||
class LinuxInterfaceDriver(object):
|
||||
__metaclass__ = abc.ABCMeta
|
||||
|
||||
# from linux IF_NAMESIZE
|
||||
DEV_NAME_LEN = 14
|
||||
|
||||
def __init__(self, conf):
|
||||
self.conf = conf
|
||||
|
||||
def init_l3(self, port, device_name):
|
||||
"""Set the L3 settings for the interface using data from the port."""
|
||||
device = ip_lib.IPDevice(device_name, self.conf.root_helper)
|
||||
|
||||
previous = {}
|
||||
for address in device.addr.list(scope='global', filters=['permanent']):
|
||||
previous[address['cidr']] = address['ip_version']
|
||||
|
||||
# add new addresses
|
||||
for fixed_ip in port.fixed_ips:
|
||||
subnet = fixed_ip.subnet
|
||||
net = netaddr.IPNetwork(subnet.cidr)
|
||||
ip_cidr = '%s/%s' % (fixed_ip.ip_address, net.prefixlen)
|
||||
|
||||
if ip_cidr in previous:
|
||||
del previous[ip_cidr]
|
||||
continue
|
||||
|
||||
device.addr.add(net.version, ip_cidr, str(net.broadcast))
|
||||
|
||||
# clean up any old addresses
|
||||
for ip_cidr, ip_version in previous.items():
|
||||
device.addr.delete(ip_version, ip_cidr)
|
||||
|
||||
def check_bridge_exists(self, bridge):
|
||||
if not ip_lib.device_exists(bridge):
|
||||
raise exception.BridgeDoesNotExist(bridge=bridge)
|
||||
|
||||
@abc.abstractmethod
|
||||
def plug(self, network_id, port_id, device_name, mac_address):
|
||||
"""Plug in the interface."""
|
||||
|
||||
@abc.abstractmethod
|
||||
def unplug(self, device_name):
|
||||
"""Unplug the interface."""
|
||||
|
||||
|
||||
class NullDriver(LinuxInterfaceDriver):
|
||||
def plug(self, network_id, port_id, device_name, mac_address):
|
||||
pass
|
||||
|
||||
def unplug(self, device_name):
|
||||
pass
|
||||
|
||||
|
||||
class OVSInterfaceDriver(LinuxInterfaceDriver):
|
||||
"""Driver for creating an OVS interface."""
|
||||
|
||||
def plug(self, network_id, port_id, device_name, mac_address):
|
||||
"""Plug in the interface."""
|
||||
bridge = self.conf.ovs_integration_bridge
|
||||
|
||||
self.check_bridge_exists(bridge)
|
||||
|
||||
if not ip_lib.device_exists(device_name):
|
||||
utils.execute(['ovs-vsctl',
|
||||
'--', '--may-exist', 'add-port', bridge,
|
||||
device_name,
|
||||
'--', 'set', 'Interface', device_name,
|
||||
'type=internal',
|
||||
'--', 'set', 'Interface', device_name,
|
||||
'external-ids:iface-id=%s' % port_id,
|
||||
'--', 'set', 'Interface', device_name,
|
||||
'external-ids:iface-status=active',
|
||||
'--', 'set', 'Interface', device_name,
|
||||
'external-ids:attached-mac=%s' %
|
||||
mac_address],
|
||||
self.conf.root_helper)
|
||||
|
||||
device = ip_lib.IPDevice(device_name, self.conf.root_helper)
|
||||
device.link.set_address(mac_address)
|
||||
if self.conf.network_device_mtu:
|
||||
device.link.set_mtu(self.conf.network_device_mtu)
|
||||
device.link.set_up()
|
||||
else:
|
||||
LOG.error(_('Device %s already exists') % device)
|
||||
|
||||
def unplug(self, device_name):
|
||||
"""Unplug the interface."""
|
||||
bridge_name = self.conf.ovs_integration_bridge
|
||||
|
||||
self.check_bridge_exists(bridge_name)
|
||||
bridge = ovs_lib.OVSBridge(bridge_name, self.conf.root_helper)
|
||||
bridge.delete_port(device_name)
|
||||
|
||||
|
||||
class BridgeInterfaceDriver(LinuxInterfaceDriver):
|
||||
"""Driver for creating bridge interfaces."""
|
||||
|
||||
BRIDGE_NAME_PREFIX = 'brq'
|
||||
|
||||
def plug(self, network_id, port_id, device_name, mac_address):
|
||||
"""Plugin the interface."""
|
||||
bridge = self.get_bridge(network_id)
|
||||
|
||||
self.check_bridge_exists(bridge)
|
||||
|
||||
if not ip_lib.device_exists(device_name):
|
||||
device = ip_lib.IPDevice(device_name, self.conf.root_helper)
|
||||
try:
|
||||
# First, try with 'ip'
|
||||
device.tuntap.add()
|
||||
except RuntimeError, e:
|
||||
# Second option: tunctl
|
||||
utils.execute(['tunctl', '-b', '-t', device_name],
|
||||
self.conf.root_helper)
|
||||
|
||||
device.link.set_address(mac_address)
|
||||
device.link.set_up()
|
||||
else:
|
||||
LOG.warn(_("Device %s already exists") % device_name)
|
||||
|
||||
def unplug(self, device_name):
|
||||
"""Unplug the interface."""
|
||||
device = ip_lib.IPDevice(device_name, self.conf.root_helper)
|
||||
try:
|
||||
device.link.delete()
|
||||
LOG.debug(_("Unplugged interface '%s'") % device_name)
|
||||
except RuntimeError:
|
||||
LOG.error(_("Failed unplugging interface '%s'") %
|
||||
device_name)
|
||||
|
||||
def get_bridge(self, network_id):
|
||||
"""Returns the name of the bridge interface."""
|
||||
bridge = self.BRIDGE_NAME_PREFIX + network_id[0:11]
|
||||
return bridge
|
191
quantum/agent/linux/ip_lib.py
Normal file
191
quantum/agent/linux/ip_lib.py
Normal file
@ -0,0 +1,191 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
# Copyright 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 utils
|
||||
|
||||
|
||||
class IPDevice(object):
|
||||
def __init__(self, name, root_helper=None):
|
||||
self.name = name
|
||||
self.root_helper = root_helper
|
||||
self._commands = {}
|
||||
|
||||
self.link = IpLinkCommand(self)
|
||||
self.tuntap = IpTuntapCommand(self)
|
||||
self.addr = IpAddrCommand(self)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.name == other.name
|
||||
|
||||
@classmethod
|
||||
def _execute(cls, options, command, args, root_helper=None):
|
||||
opt_list = ['-%s' % o for o in options]
|
||||
return utils.execute(['ip'] + opt_list + [command] + list(args),
|
||||
root_helper=root_helper)
|
||||
|
||||
@classmethod
|
||||
def get_devices(cls):
|
||||
retval = []
|
||||
for line in cls._execute('o', 'link', ('list',)).split('\n'):
|
||||
if '<' not in line:
|
||||
continue
|
||||
index, name, attrs = line.split(':', 2)
|
||||
retval.append(IPDevice(name.strip()))
|
||||
return retval
|
||||
|
||||
|
||||
class IpCommandBase(object):
|
||||
COMMAND = ''
|
||||
|
||||
def __init__(self, parent):
|
||||
self._parent = parent
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self._parent.name
|
||||
|
||||
def _run(self, *args, **kwargs):
|
||||
return self._parent._execute(kwargs.get('options', []),
|
||||
self.COMMAND,
|
||||
args)
|
||||
|
||||
def _as_root(self, *args, **kwargs):
|
||||
if not self._parent.root_helper:
|
||||
raise exceptions.SudoRequired()
|
||||
return self._parent._execute(kwargs.get('options', []),
|
||||
self.COMMAND,
|
||||
args,
|
||||
self._parent.root_helper)
|
||||
|
||||
|
||||
class IpLinkCommand(IpCommandBase):
|
||||
COMMAND = 'link'
|
||||
|
||||
def set_address(self, mac_address):
|
||||
self._as_root('set', self.name, 'address', mac_address)
|
||||
|
||||
def set_mtu(self, mtu_size):
|
||||
self._as_root('set', self.name, 'mtu', mtu_size)
|
||||
|
||||
def set_up(self):
|
||||
self._as_root('set', self.name, 'up')
|
||||
|
||||
def set_down(self):
|
||||
self._as_root('set', self.name, 'down')
|
||||
|
||||
def delete(self):
|
||||
self._as_root('delete', self.name)
|
||||
|
||||
@property
|
||||
def address(self):
|
||||
return self.attributes.get('link/ether')
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
return self.attributes.get('state')
|
||||
|
||||
@property
|
||||
def mtu(self):
|
||||
return self.attributes.get('mtu')
|
||||
|
||||
@property
|
||||
def qdisc(self):
|
||||
return self.attributes.get('qdisc')
|
||||
|
||||
@property
|
||||
def qlen(self):
|
||||
return self.attributes.get('qlen')
|
||||
|
||||
@property
|
||||
def attributes(self):
|
||||
return self._parse_line(self._run('show', self.name, options='o'))
|
||||
|
||||
def _parse_line(self, value):
|
||||
device_name, settings = value.replace("\\", '').split('>', 1)
|
||||
|
||||
tokens = settings.split()
|
||||
keys = tokens[::2]
|
||||
values = [int(v) if v.isdigit() else v for v in tokens[1::2]]
|
||||
|
||||
retval = dict(zip(keys, values))
|
||||
return retval
|
||||
|
||||
|
||||
class IpTuntapCommand(IpCommandBase):
|
||||
COMMAND = 'tuntap'
|
||||
|
||||
def add(self):
|
||||
self._as_root('add', self.name, 'mode', 'tap')
|
||||
|
||||
|
||||
class IpAddrCommand(IpCommandBase):
|
||||
COMMAND = 'addr'
|
||||
|
||||
def add(self, ip_version, cidr, broadcast, scope='global'):
|
||||
self._as_root('add',
|
||||
cidr,
|
||||
'brd',
|
||||
broadcast,
|
||||
'scope',
|
||||
scope,
|
||||
'dev',
|
||||
self.name,
|
||||
options=[ip_version])
|
||||
|
||||
def delete(self, ip_version, cidr):
|
||||
self._as_root('del',
|
||||
cidr,
|
||||
'dev',
|
||||
self.name,
|
||||
options=[ip_version])
|
||||
|
||||
def flush(self):
|
||||
self._as_root('flush', self.name)
|
||||
|
||||
def list(self, scope=None, to=None, filters=[]):
|
||||
retval = []
|
||||
|
||||
if scope:
|
||||
filters += ['scope', scope]
|
||||
if to:
|
||||
filters += ['to', to]
|
||||
|
||||
for line in self._run('show', self.name, *filters).split('\n'):
|
||||
line = line.strip()
|
||||
if not line.startswith('inet'):
|
||||
continue
|
||||
parts = line.split()
|
||||
if parts[0] == 'inet6':
|
||||
version = 6
|
||||
scope = parts[3]
|
||||
else:
|
||||
version = 4
|
||||
scope = parts[5]
|
||||
|
||||
retval.append(dict(cidr=parts[1],
|
||||
scope=scope,
|
||||
ip_version=version,
|
||||
dynamic=('dynamic' == parts[-1])))
|
||||
return retval
|
||||
|
||||
|
||||
def device_exists(device_name):
|
||||
try:
|
||||
address = IPDevice(device_name).link.address
|
||||
except RuntimeError:
|
||||
return False
|
||||
|
||||
return True
|
@ -156,3 +156,15 @@ class MacAddressGenerationFailure(QuantumException):
|
||||
|
||||
class IpAddressGenerationFailure(QuantumException):
|
||||
message = _("No more IP addresses available on network %(net_id)s.")
|
||||
|
||||
|
||||
class BridgeDoesNotExist(QuantumException):
|
||||
message = _("Bridge %(bridge)s does not exist.")
|
||||
|
||||
|
||||
class PreexistingDeviceFailure(QuantumException):
|
||||
message = _("Creation failed. %(dev_name)s already exists.")
|
||||
|
||||
|
||||
class SudoRequired(QuantumException):
|
||||
message = _("Sudo priviledge is required to run this command.")
|
||||
|
26
quantum/rootwrap/dhcp-agent.py
Normal file
26
quantum/rootwrap/dhcp-agent.py
Normal file
@ -0,0 +1,26 @@
|
||||
# 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.rootwrap import filters
|
||||
|
||||
filterlist = [
|
||||
# quantum/agent/linux/dhcp.py:
|
||||
# "dnsmasq", "--no-hosts", ...
|
||||
filters.CommandFilter("/usr/sbin/dnsmasq", "root"),
|
||||
filters.KillFilter("/bin/kill", "root", [''], ['/usr/sbin/dnsmasq']),
|
||||
]
|
23
quantum/tests/unit/test_agent_config.py
Normal file
23
quantum/tests/unit/test_agent_config.py
Normal file
@ -0,0 +1,23 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 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.common import config
|
||||
|
||||
|
||||
def test_setup_conf():
|
||||
conf = config.setup_conf()
|
||||
assert conf.state_path.endswith('/var/lib/quantum')
|
@ -17,6 +17,8 @@
|
||||
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
|
||||
from quantum.agent.linux import utils
|
||||
|
||||
|
||||
|
318
quantum/tests/unit/test_dhcp_agent.py
Normal file
318
quantum/tests/unit/test_dhcp_agent.py
Normal file
@ -0,0 +1,318 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 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.
|
||||
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
from sqlalchemy.ext import sqlsoup
|
||||
|
||||
from quantum.agent import dhcp_agent
|
||||
from quantum.agent.common import config
|
||||
from quantum.agent.linux import interface
|
||||
|
||||
|
||||
class FakeModel:
|
||||
def __init__(self, id_, **kwargs):
|
||||
self.id = id_
|
||||
self.__dict__.update(kwargs)
|
||||
|
||||
def __str__(self):
|
||||
return str(self.__dict__)
|
||||
|
||||
|
||||
class TestDhcpAgent(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.conf = config.setup_conf()
|
||||
self.conf.register_opts(dhcp_agent.DhcpAgent.OPTS)
|
||||
self.driver_cls_p = mock.patch(
|
||||
'quantum.agent.dhcp_agent.importutils.import_class')
|
||||
self.driver = mock.Mock(name='driver')
|
||||
self.driver_cls = self.driver_cls_p.start()
|
||||
self.driver_cls.return_value = self.driver
|
||||
self.dhcp = dhcp_agent.DhcpAgent(self.conf)
|
||||
self.dhcp.polling_interval = 0
|
||||
|
||||
def tearDown(self):
|
||||
self.driver_cls_p.stop()
|
||||
|
||||
def test_daemon_loop_survives_get_network_state_delta_failure(self):
|
||||
def stop_loop(*args):
|
||||
self.dhcp._run = False
|
||||
return None
|
||||
|
||||
with mock.patch.object(self.dhcp, 'get_network_state_delta') as state:
|
||||
state.side_effect = stop_loop
|
||||
self.dhcp.daemon_loop()
|
||||
|
||||
def test_daemon_loop_completes_single_pass(self):
|
||||
with mock.patch.object(self.dhcp, 'get_network_state_delta') as state:
|
||||
with mock.patch.object(self.dhcp, 'call_driver') as call_driver:
|
||||
with mock.patch('quantum.agent.dhcp_agent.time') as time:
|
||||
time.sleep = mock.Mock(side_effect=RuntimeError('stop'))
|
||||
state.return_value = dict(new=['new_net'],
|
||||
updated=['updated_net'],
|
||||
deleted=['deleted_net'])
|
||||
|
||||
self.assertRaises(RuntimeError, self.dhcp.daemon_loop)
|
||||
call_driver.assert_has_calls(
|
||||
[mock.call('enable', 'new_net'),
|
||||
mock.call('reload_allocations', 'updated_net'),
|
||||
mock.call('disable', 'deleted_net')])
|
||||
|
||||
def test_state_builder(self):
|
||||
fake_subnet = [
|
||||
FakeModel(1, network_id=1),
|
||||
FakeModel(2, network_id=2),
|
||||
]
|
||||
|
||||
fake_allocation = [
|
||||
FakeModel(2, subnet_id=1)
|
||||
]
|
||||
|
||||
db = mock.Mock()
|
||||
db.subnets.all = mock.Mock(return_value=fake_subnet)
|
||||
db.ipallocations.all = mock.Mock(return_value=fake_allocation)
|
||||
self.dhcp.db = db
|
||||
state = self.dhcp._state_builder()
|
||||
|
||||
self.assertEquals(state.networks, set([1, 2]))
|
||||
|
||||
expected_subnets = set([
|
||||
(hash(str(fake_subnet[0])), 1),
|
||||
(hash(str(fake_subnet[1])), 2)
|
||||
])
|
||||
self.assertEquals(state.subnet_hashes, expected_subnets)
|
||||
|
||||
expected_ipalloc = set([
|
||||
(hash(str(fake_allocation[0])), 1),
|
||||
])
|
||||
self.assertEquals(state.ipalloc_hashes, expected_ipalloc)
|
||||
|
||||
def _network_state_helper(self, before, after):
|
||||
with mock.patch.object(self.dhcp, '_state_builder') as state_builder:
|
||||
state_builder.return_value = after
|
||||
self.dhcp.prev_state = before
|
||||
return self.dhcp.get_network_state_delta()
|
||||
|
||||
def test_get_network_state_fresh(self):
|
||||
new_state = dhcp_agent.State(set([1]), set([(3, 1)]), set([(11, 1)]))
|
||||
|
||||
delta = self._network_state_helper(self.dhcp.prev_state, new_state)
|
||||
self.assertEqual(delta,
|
||||
dict(new=set([1]), deleted=set(), updated=set()))
|
||||
|
||||
def test_get_network_state_new_subnet_on_known_network(self):
|
||||
prev_state = dhcp_agent.State(set([1]), set([(3, 1)]), set([(11, 1)]))
|
||||
new_state = dhcp_agent.State(set([1]),
|
||||
set([(3, 1), (4, 1)]),
|
||||
set([(11, 1)]))
|
||||
|
||||
delta = self._network_state_helper(prev_state, new_state)
|
||||
self.assertEqual(delta,
|
||||
dict(new=set(), deleted=set(), updated=set([1])))
|
||||
|
||||
def test_get_network_state_new_ipallocation(self):
|
||||
prev_state = dhcp_agent.State(set([1]),
|
||||
set([(3, 1)]),
|
||||
set([(11, 1)]))
|
||||
new_state = dhcp_agent.State(set([1]),
|
||||
set([(3, 1)]),
|
||||
set([(11, 1), (12, 1)]))
|
||||
|
||||
delta = self._network_state_helper(prev_state, new_state)
|
||||
self.assertEqual(delta,
|
||||
dict(new=set(), deleted=set(), updated=set([1])))
|
||||
|
||||
def test_get_network_state_delete_subnet_on_known_network(self):
|
||||
prev_state = dhcp_agent.State(set([1]),
|
||||
set([(3, 1), (4, 1)]),
|
||||
set([(11, 1)]))
|
||||
new_state = dhcp_agent.State(set([1]),
|
||||
set([(3, 1)]),
|
||||
set([(11, 1)]))
|
||||
|
||||
delta = self._network_state_helper(prev_state, new_state)
|
||||
self.assertEqual(delta,
|
||||
dict(new=set(), deleted=set(), updated=set([1])))
|
||||
|
||||
def test_get_network_state_deleted_ipallocation(self):
|
||||
prev_state = dhcp_agent.State(set([1]),
|
||||
set([(3, 1)]),
|
||||
set([(11, 1), (12, 1)]))
|
||||
new_state = dhcp_agent.State(set([1]),
|
||||
set([(3, 1)]),
|
||||
set([(11, 1)]))
|
||||
|
||||
delta = self._network_state_helper(prev_state, new_state)
|
||||
self.assertEqual(delta,
|
||||
dict(new=set(), deleted=set(), updated=set([1])))
|
||||
|
||||
def test_get_network_state_deleted_network(self):
|
||||
prev_state = dhcp_agent.State(set([1]),
|
||||
set([(3, 1)]),
|
||||
set([(11, 1), (12, 1)]))
|
||||
new_state = dhcp_agent.State(set(), set(), set())
|
||||
|
||||
delta = self._network_state_helper(prev_state, new_state)
|
||||
self.assertEqual(delta,
|
||||
dict(new=set(), deleted=set([1]), updated=set()))
|
||||
|
||||
def test_get_network_state_changed_subnet_and_deleted_network(self):
|
||||
prev_state = dhcp_agent.State(set([1, 2]),
|
||||
set([(3, 1), (2, 2)]),
|
||||
set([(11, 1), (12, 1)]))
|
||||
new_state = dhcp_agent.State(set([1]),
|
||||
set([(4, 1)]),
|
||||
set([(11, 1), (12, 1)]))
|
||||
|
||||
delta = self._network_state_helper(prev_state, new_state)
|
||||
self.assertEqual(delta,
|
||||
dict(new=set(), deleted=set([2]), updated=set([1])))
|
||||
|
||||
def test_call_driver(self):
|
||||
with mock.patch.object(self.dhcp, 'db') as db:
|
||||
db.networks = mock.Mock()
|
||||
db.networks.filter_by = mock.Mock(
|
||||
return_value=mock.Mock(return_value=FakeModel('1')))
|
||||
with mock.patch.object(dhcp_agent, 'DeviceManager') as dev_mgr:
|
||||
self.dhcp.call_driver('foo', '1')
|
||||
dev_mgr.assert_called()
|
||||
self.driver.assert_called_once_with(self.conf,
|
||||
mock.ANY,
|
||||
'sudo',
|
||||
mock.ANY)
|
||||
|
||||
|
||||
class TestDeviceManager(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.conf = config.setup_conf()
|
||||
self.conf.register_opts(dhcp_agent.DeviceManager.OPTS)
|
||||
self.conf.set_override('interface_driver',
|
||||
'quantum.agent.linux.interface.NullDriver')
|
||||
|
||||
self.client_cls_p = mock.patch('quantumclient.v2_0.client.Client')
|
||||
client_cls = self.client_cls_p.start()
|
||||
self.client_inst = mock.Mock()
|
||||
client_cls.return_value = self.client_inst
|
||||
|
||||
self.device_exists_p = mock.patch(
|
||||
'quantum.agent.linux.ip_lib.device_exists')
|
||||
self.device_exists = self.device_exists_p.start()
|
||||
|
||||
self.dvr_cls_p = mock.patch('quantum.agent.linux.interface.NullDriver')
|
||||
driver_cls = self.dvr_cls_p.start()
|
||||
self.mock_driver = mock.MagicMock()
|
||||
self.mock_driver.DEV_NAME_LEN = (
|
||||
interface.LinuxInterfaceDriver.DEV_NAME_LEN)
|
||||
driver_cls.return_value = self.mock_driver
|
||||
|
||||
def tearDown(self):
|
||||
self.dvr_cls_p.stop()
|
||||
self.device_exists_p.stop()
|
||||
self.client_cls_p.stop()
|
||||
|
||||
def test_setup(self):
|
||||
fake_subnets = [FakeModel('12345678-aaaa-aaaa-1234567890ab'),
|
||||
FakeModel('12345678-bbbb-bbbb-1234567890ab')]
|
||||
|
||||
fake_network = FakeModel('12345678-1234-5678-1234567890ab',
|
||||
tenant_id='aaaaaaaa-aaaa-aaaa-aaaaaaaaaaaa',
|
||||
subnets=fake_subnets)
|
||||
|
||||
fake_port = FakeModel('12345678-aaaa-aaaa-1234567890ab',
|
||||
mac_address='aa:bb:cc:dd:ee:ff')
|
||||
|
||||
port_dict = dict(mac_address='aa:bb:cc:dd:ee:ff', allocations=[], id=1)
|
||||
|
||||
self.client_inst.create_port.return_value = dict(port=port_dict)
|
||||
self.device_exists.return_value = False
|
||||
|
||||
# fake the db
|
||||
filter_by_result = mock.Mock()
|
||||
filter_by_result.one = mock.Mock(return_value=fake_port)
|
||||
|
||||
self.filter_called = False
|
||||
|
||||
def get_filter_results(*args, **kwargs):
|
||||
if self.filter_called:
|
||||
return filter_by_result
|
||||
else:
|
||||
self.filter_called = True
|
||||
raise sqlsoup.SQLAlchemyError()
|
||||
|
||||
return filter_results.pop(0)
|
||||
|
||||
mock_db = mock.Mock()
|
||||
mock_db.ports = mock.Mock(name='ports2')
|
||||
mock_db.ports.filter_by = mock.Mock(
|
||||
name='filter_by',
|
||||
side_effect=get_filter_results)
|
||||
|
||||
dh = dhcp_agent.DeviceManager(self.conf, mock_db)
|
||||
dh.setup(fake_network)
|
||||
|
||||
self.client_inst.assert_has_calls([
|
||||
mock.call.create_port(mock.ANY)])
|
||||
|
||||
self.mock_driver.assert_has_calls([
|
||||
mock.call.plug('12345678-1234-5678-1234567890ab',
|
||||
'12345678-aaaa-aaaa-1234567890ab',
|
||||
'tap12345678-12',
|
||||
'aa:bb:cc:dd:ee:ff'),
|
||||
mock.call.init_l3(mock.ANY, 'tap12345678-12')]
|
||||
)
|
||||
|
||||
def test_destroy(self):
|
||||
fake_subnets = [FakeModel('12345678-aaaa-aaaa-1234567890ab'),
|
||||
FakeModel('12345678-bbbb-bbbb-1234567890ab')]
|
||||
|
||||
fake_network = FakeModel('12345678-1234-5678-1234567890ab',
|
||||
tenant_id='aaaaaaaa-aaaa-aaaa-aaaaaaaaaaaa',
|
||||
subnets=fake_subnets)
|
||||
|
||||
with mock.patch('quantum.agent.linux.interface.NullDriver') as dvr_cls:
|
||||
mock_driver = mock.MagicMock()
|
||||
mock_driver.DEV_NAME_LEN = (
|
||||
interface.LinuxInterfaceDriver.DEV_NAME_LEN)
|
||||
dvr_cls.return_value = mock_driver
|
||||
|
||||
dh = dhcp_agent.DeviceManager(self.conf, None)
|
||||
dh.destroy(fake_network)
|
||||
|
||||
dvr_cls.assert_called_once_with(self.conf)
|
||||
mock_driver.assert_has_calls(
|
||||
[mock.call.unplug('tap12345678-12')])
|
||||
|
||||
|
||||
class TestAugmentingWrapper(unittest.TestCase):
|
||||
def test_simple_wrap(self):
|
||||
net = mock.Mock()
|
||||
db = mock.Mock()
|
||||
net.name = 'foo'
|
||||
wrapped = dhcp_agent.AugmentingWrapper(net, db)
|
||||
self.assertEqual(wrapped.name, 'foo')
|
||||
self.assertEqual(repr(net), repr(wrapped))
|
||||
|
||||
|
||||
def test_dhcp_agent_main():
|
||||
with mock.patch('quantum.agent.dhcp_agent.DeviceManager') as dev_mgr:
|
||||
with mock.patch('quantum.agent.dhcp_agent.DhcpAgent') as dhcp:
|
||||
dhcp_agent.main()
|
||||
dev_mgr.assert_called_once(mock.ANY, 'sudo')
|
||||
dhcp.assert_has_calls([
|
||||
mock.call(mock.ANY),
|
||||
mock.call().daemon_loop()])
|
376
quantum/tests/unit/test_linux_dhcp.py
Normal file
376
quantum/tests/unit/test_linux_dhcp.py
Normal file
@ -0,0 +1,376 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 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.
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import unittest2 as unittest
|
||||
|
||||
import mock
|
||||
|
||||
from quantum.agent.linux import dhcp
|
||||
from quantum.agent.common import config
|
||||
from quantum.openstack.common import cfg
|
||||
|
||||
|
||||
class FakeIPAllocation:
|
||||
def __init__(self, address):
|
||||
self.ip_address = address
|
||||
|
||||
|
||||
class FakePort1:
|
||||
id = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee'
|
||||
admin_state_up = True
|
||||
fixed_ips = [FakeIPAllocation('192.168.0.2')]
|
||||
mac_address = '00:00:80:aa:bb:cc'
|
||||
|
||||
|
||||
class FakePort2:
|
||||
id = 'ffffffff-ffff-ffff-ffff-ffffffffffff'
|
||||
admin_state_up = False
|
||||
fixed_ips = [FakeIPAllocation('fdca:3ba5:a17a:4ba3::2')]
|
||||
mac_address = '00:00:f3:aa:bb:cc'
|
||||
|
||||
|
||||
class FakePort3:
|
||||
id = '44444444-4444-4444-4444-444444444444'
|
||||
admin_state_up = True
|
||||
fixed_ips = [FakeIPAllocation('192.168.0.3'),
|
||||
FakeIPAllocation('fdca:3ba5:a17a:4ba3::3')]
|
||||
mac_address = '00:00:0f:aa:bb:cc'
|
||||
|
||||
|
||||
class FakeV4Subnet:
|
||||
id = 'dddddddd-dddd-dddd-dddd-dddddddddddd'
|
||||
ip_version = 4
|
||||
cidr = '192.168.0.0/24'
|
||||
gateway_ip = '192.168.0.1'
|
||||
|
||||
|
||||
class FakeV6Subnet:
|
||||
id = 'ffffffff-ffff-ffff-ffff-ffffffffffff'
|
||||
ip_version = 6
|
||||
cidr = 'fdca:3ba5:a17a:4ba3::/64'
|
||||
gateway_ip = 'fdca:3ba5:a17a:4ba3::1'
|
||||
|
||||
|
||||
class FakeV4Network:
|
||||
id = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
|
||||
subnets = [FakeV4Subnet()]
|
||||
ports = [FakePort1()]
|
||||
|
||||
|
||||
class FakeV6Network:
|
||||
id = 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'
|
||||
subnets = [FakeV6Subnet()]
|
||||
ports = [FakePort2()]
|
||||
|
||||
|
||||
class FakeDualNetwork:
|
||||
id = 'cccccccc-cccc-cccc-cccc-cccccccccccc'
|
||||
subnets = [FakeV4Subnet(), FakeV6Subnet()]
|
||||
ports = [FakePort1(), FakePort2(), FakePort3()]
|
||||
|
||||
|
||||
class TestDhcpBase(unittest.TestCase):
|
||||
def test_base_abc_error(self):
|
||||
self.assertRaises(TypeError, dhcp.DhcpBase, None)
|
||||
|
||||
def test_replace_file(self):
|
||||
# make file to replace
|
||||
with mock.patch('tempfile.NamedTemporaryFile') as ntf:
|
||||
ntf.return_value.name = '/baz'
|
||||
with mock.patch('os.chmod') as chmod:
|
||||
with mock.patch('os.rename') as rename:
|
||||
dhcp.replace_file('/foo', 'bar')
|
||||
|
||||
expected = [mock.call('w+', dir='/', delete=False),
|
||||
mock.call().write('bar'),
|
||||
mock.call().close()]
|
||||
|
||||
ntf.assert_has_calls(expected)
|
||||
chmod.assert_called_once_with('/baz', 0644)
|
||||
rename.assert_called_once_with('/baz', '/foo')
|
||||
|
||||
def test_restart(self):
|
||||
class SubClass(dhcp.DhcpBase):
|
||||
def __init__(self):
|
||||
dhcp.DhcpBase.__init__(self, None, None)
|
||||
self.called = []
|
||||
|
||||
def enable(self):
|
||||
self.called.append('enable')
|
||||
|
||||
def disable(self):
|
||||
self.called.append('disable')
|
||||
|
||||
def reload_allocations(self):
|
||||
pass
|
||||
|
||||
@property
|
||||
def active(self):
|
||||
return True
|
||||
|
||||
c = SubClass()
|
||||
c.restart()
|
||||
self.assertEquals(c.called, ['disable', 'enable'])
|
||||
|
||||
|
||||
class LocalChild(dhcp.DhcpLocalProcess):
|
||||
PORTS = {4: [4], 6: [6]}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(LocalChild, self).__init__(*args, **kwargs)
|
||||
self.called = []
|
||||
|
||||
def reload_allocations(self):
|
||||
self.called.append('reload')
|
||||
|
||||
def spawn_process(self):
|
||||
self.called.append('spawn')
|
||||
|
||||
|
||||
class TestBase(unittest.TestCase):
|
||||
def setUp(self):
|
||||
root = os.path.dirname(os.path.dirname(__file__))
|
||||
args = ['--config-file',
|
||||
os.path.join(root, 'etc', 'quantum.conf.test')]
|
||||
self.conf = config.setup_conf()
|
||||
self.conf.register_opts(dhcp.OPTS)
|
||||
self.conf(args=args)
|
||||
self.conf.set_override('state_path', '')
|
||||
|
||||
self.replace_p = mock.patch('quantum.agent.linux.dhcp.replace_file')
|
||||
self.execute_p = mock.patch('quantum.agent.linux.utils.execute')
|
||||
self.safe = self.replace_p.start()
|
||||
self.execute = self.execute_p.start()
|
||||
|
||||
def tearDown(self):
|
||||
self.execute_p.stop()
|
||||
self.replace_p.stop()
|
||||
|
||||
|
||||
class TestDhcpLocalProcess(TestBase):
|
||||
def test_active(self):
|
||||
dummy_cmd_line = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
|
||||
self.execute.return_value = (dummy_cmd_line, '')
|
||||
with mock.patch.object(LocalChild, 'pid') as pid:
|
||||
pid.__get__ = mock.Mock(return_value=4)
|
||||
lp = LocalChild(self.conf, FakeV4Network())
|
||||
self.assertTrue(lp.active)
|
||||
self.execute.assert_called_once_with(['cat', '/proc/4/cmdline'],
|
||||
'sudo')
|
||||
|
||||
def test_active_cmd_mismatch(self):
|
||||
dummy_cmd_line = 'bbbbbbbb-bbbb-bbbb-aaaa-aaaaaaaaaaaa'
|
||||
self.execute.return_value = (dummy_cmd_line, '')
|
||||
with mock.patch.object(LocalChild, 'pid') as pid:
|
||||
pid.__get__ = mock.Mock(return_value=4)
|
||||
lp = LocalChild(self.conf, FakeV4Network())
|
||||
self.assertFalse(lp.active)
|
||||
self.execute.assert_called_once_with(['cat', '/proc/4/cmdline'],
|
||||
'sudo')
|
||||
|
||||
def test_get_conf_file_name(self):
|
||||
tpl = '/dhcp/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa/dev'
|
||||
with mock.patch('os.path.isdir') as isdir:
|
||||
isdir.return_value = False
|
||||
with mock.patch('os.makedirs') as makedirs:
|
||||
lp = LocalChild(self.conf, FakeV4Network())
|
||||
self.assertEqual(lp.get_conf_file_name('dev'), tpl)
|
||||
self.assertFalse(makedirs.called)
|
||||
|
||||
def test_get_conf_file_name_ensure_dir(self):
|
||||
tpl = '/dhcp/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa/dev'
|
||||
with mock.patch('os.path.isdir') as isdir:
|
||||
isdir.return_value = False
|
||||
with mock.patch('os.makedirs') as makedirs:
|
||||
lp = LocalChild(self.conf, FakeV4Network())
|
||||
self.assertEqual(lp.get_conf_file_name('dev', True), tpl)
|
||||
self.assertTrue(makedirs.called)
|
||||
|
||||
def test_enable_already_active(self):
|
||||
with mock.patch.object(LocalChild, 'active') as patched:
|
||||
patched.__get__ = mock.Mock(return_value=True)
|
||||
lp = LocalChild(self.conf, FakeV4Network())
|
||||
lp.enable()
|
||||
|
||||
self.assertEqual(lp.called, ['reload'])
|
||||
|
||||
def test_enable(self):
|
||||
delegate = mock.Mock(return_value='tap0')
|
||||
attrs_to_mock = dict(
|
||||
[(a, mock.DEFAULT) for a in
|
||||
['active', 'get_conf_file_name']]
|
||||
)
|
||||
|
||||
with mock.patch.multiple(LocalChild, **attrs_to_mock) as mocks:
|
||||
mocks['active'].__get__ = mock.Mock(return_value=False)
|
||||
mocks['get_conf_file_name'].return_value = '/dir'
|
||||
lp = LocalChild(self.conf,
|
||||
FakeDualNetwork(),
|
||||
device_delegate=delegate)
|
||||
lp.enable()
|
||||
|
||||
delegate.assert_has_calls(
|
||||
[mock.call.setup(mock.ANY, reuse_existing=True)])
|
||||
self.assertEqual(lp.called, ['spawn'])
|
||||
|
||||
def test_disable_not_active(self):
|
||||
attrs_to_mock = dict([(a, mock.DEFAULT) for a in ['active', 'pid']])
|
||||
with mock.patch.multiple(LocalChild, **attrs_to_mock) as mocks:
|
||||
mocks['active'].__get__ = mock.Mock(return_value=False)
|
||||
mocks['pid'].__get__ = mock.Mock(return_value=5)
|
||||
with mock.patch.object(dhcp.LOG, 'debug') as log:
|
||||
lp = LocalChild(self.conf, FakeDualNetwork())
|
||||
lp.disable()
|
||||
msg = log.call_args[0][0]
|
||||
self.assertIn('stale', msg)
|
||||
|
||||
def test_disable_unknown_network(self):
|
||||
attrs_to_mock = dict([(a, mock.DEFAULT) for a in ['active', 'pid']])
|
||||
with mock.patch.multiple(LocalChild, **attrs_to_mock) as mocks:
|
||||
mocks['active'].__get__ = mock.Mock(return_value=False)
|
||||
mocks['pid'].__get__ = mock.Mock(return_value=None)
|
||||
with mock.patch.object(dhcp.LOG, 'debug') as log:
|
||||
lp = LocalChild(self.conf, FakeDualNetwork())
|
||||
lp.disable()
|
||||
msg = log.call_args[0][0]
|
||||
self.assertIn('No DHCP', msg)
|
||||
|
||||
def test_disable(self):
|
||||
attrs_to_mock = dict([(a, mock.DEFAULT) for a in
|
||||
['active', 'pid']])
|
||||
delegate = mock.Mock()
|
||||
delegate.intreface_name = 'tap0'
|
||||
network = FakeDualNetwork()
|
||||
with mock.patch.multiple(LocalChild, **attrs_to_mock) as mocks:
|
||||
mocks['active'].__get__ = mock.Mock(return_value=True)
|
||||
mocks['pid'].__get__ = mock.Mock(return_value=5)
|
||||
lp = LocalChild(self.conf, network, device_delegate=delegate)
|
||||
lp.disable()
|
||||
|
||||
delegate.assert_has_calls([mock.call.destroy(network)])
|
||||
self.execute.assert_called_once_with(['kill', '-9', 5], 'sudo')
|
||||
|
||||
def test_pid(self):
|
||||
with mock.patch('__builtin__.open') as mock_open:
|
||||
mock_open.return_value.__enter__ = lambda s: s
|
||||
mock_open.return_value.__exit__ = mock.Mock()
|
||||
mock_open.return_value.read.return_value = '5'
|
||||
lp = LocalChild(self.conf, FakeDualNetwork())
|
||||
self.assertEqual(lp.pid, 5)
|
||||
|
||||
def test_pid_no_an_int(self):
|
||||
with mock.patch('__builtin__.open') as mock_open:
|
||||
mock_open.return_value.__enter__ = lambda s: s
|
||||
mock_open.return_value.__exit__ = mock.Mock()
|
||||
mock_open.return_value.read.return_value = 'foo'
|
||||
lp = LocalChild(self.conf, FakeDualNetwork())
|
||||
self.assertIsNone(lp.pid)
|
||||
|
||||
def test_pid_invalid_file(self):
|
||||
with mock.patch.object(LocalChild, 'get_conf_file_name') as conf_file:
|
||||
conf_file.return_value = '.doesnotexist/pid'
|
||||
lp = LocalChild(self.conf, FakeDualNetwork())
|
||||
self.assertIsNone(lp.pid)
|
||||
|
||||
|
||||
class TestDnsmasq(TestBase):
|
||||
def _test_spawn(self, extra_options):
|
||||
def mock_get_conf_file_name(kind, ensure_conf_dir=False):
|
||||
return '/dhcp/cccccccc-cccc-cccc-cccc-cccccccccccc/%s' % kind
|
||||
|
||||
expected = [
|
||||
'NETWORK_ID=cccccccc-cccc-cccc-cccc-cccccccccccc',
|
||||
'dnsmasq',
|
||||
'--no-hosts',
|
||||
'--no-resolv',
|
||||
'--strict-order',
|
||||
'--bind-interfaces',
|
||||
'--interface=tap0',
|
||||
'--except-interface=lo',
|
||||
'--domain=openstacklocal',
|
||||
'--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',
|
||||
'--leasefile-ro',
|
||||
'--dhcp-range=set:tag0,192.168.0.0,static,120s',
|
||||
'--dhcp-range=set:tag1,fdca:3ba5:a17a:4ba3::,static,120s'
|
||||
]
|
||||
expected.extend(extra_options)
|
||||
|
||||
self.execute.return_value = ('', '')
|
||||
delegate = mock.Mock()
|
||||
delegate.get_interface_name.return_value = 'tap0'
|
||||
|
||||
attrs_to_mock = dict(
|
||||
[(a, mock.DEFAULT) for a in
|
||||
['_output_opts_file', 'get_conf_file_name']]
|
||||
)
|
||||
|
||||
with mock.patch.multiple(dhcp.Dnsmasq, **attrs_to_mock) as mocks:
|
||||
mocks['get_conf_file_name'].side_effect = mock_get_conf_file_name
|
||||
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, 'sudo')
|
||||
|
||||
def test_spawn(self):
|
||||
self._test_spawn([])
|
||||
|
||||
def test_spawn_cfg_config_file(self):
|
||||
self.conf.set_override('dnsmasq_config_file', '/foo')
|
||||
self._test_spawn(['--conf-file=/foo'])
|
||||
|
||||
def test_spawn_cfg_dns_server(self):
|
||||
self.conf.set_override('dnsmasq_dns_server', '8.8.8.8')
|
||||
self._test_spawn(['--server=8.8.8.8'])
|
||||
|
||||
def test_output_opts_file(self):
|
||||
expected = 'tag:tag0,option:router,192.168.0.1'
|
||||
with mock.patch.object(dhcp.Dnsmasq, 'get_conf_file_name') as conf_fn:
|
||||
conf_fn.return_value = '/foo/opts'
|
||||
dm = dhcp.Dnsmasq(self.conf, FakeDualNetwork())
|
||||
dm._output_opts_file()
|
||||
|
||||
self.safe.assert_called_once_with('/foo/opts', expected)
|
||||
|
||||
def test_reload_allocations(self):
|
||||
exp_host_name = '/dhcp/cccccccc-cccc-cccc-cccc-cccccccccccc/host'
|
||||
exp_host_data = """
|
||||
00:00:80:aa:bb:cc,192-168-0-2.openstacklocal,192.168.0.2
|
||||
00:00:f3:aa:bb:cc,fdca-3ba5-a17a-4ba3--2.openstacklocal,fdca:3ba5:a17a:4ba3::2
|
||||
00:00:0f:aa:bb:cc,192-168-0-3.openstacklocal,192.168.0.3
|
||||
00:00:0f:aa:bb:cc,fdca-3ba5-a17a-4ba3--3.openstacklocal,fdca:3ba5:a17a:4ba3::3
|
||||
""".lstrip()
|
||||
exp_opt_name = '/dhcp/cccccccc-cccc-cccc-cccc-cccccccccccc/opts'
|
||||
exp_opt_data = "tag:tag0,option:router,192.168.0.1"
|
||||
|
||||
with mock.patch('os.path.isdir') as isdir:
|
||||
isdir.return_value = True
|
||||
with mock.patch.object(dhcp.Dnsmasq, 'pid') as pid:
|
||||
pid.__get__ = mock.Mock(return_value=5)
|
||||
dm = dhcp.Dnsmasq(self.conf, FakeDualNetwork())
|
||||
dm.reload_allocations()
|
||||
|
||||
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(['kill', '-HUP', 5], 'sudo')
|
224
quantum/tests/unit/test_linux_interface.py
Normal file
224
quantum/tests/unit/test_linux_interface.py
Normal file
@ -0,0 +1,224 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 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.
|
||||
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
|
||||
from quantum.agent.common import config
|
||||
from quantum.agent.linux import interface
|
||||
from quantum.agent.linux import ip_lib
|
||||
from quantum.agent.linux import utils
|
||||
from quantum.openstack.common import cfg
|
||||
|
||||
|
||||
class BaseChild(interface.LinuxInterfaceDriver):
|
||||
def plug(*args):
|
||||
pass
|
||||
|
||||
def unplug(*args):
|
||||
pass
|
||||
|
||||
|
||||
class FakeSubnet:
|
||||
cidr = '192.168.1.1/24'
|
||||
|
||||
|
||||
class FakeAllocation:
|
||||
subnet = FakeSubnet()
|
||||
ip_address = '192.168.1.2'
|
||||
ip_version = 4
|
||||
|
||||
|
||||
class FakePort(object):
|
||||
fixed_ips = [FakeAllocation]
|
||||
device_id = 'cccccccc-cccc-cccc-cccc-cccccccccccc'
|
||||
|
||||
|
||||
class TestBase(unittest.TestCase):
|
||||
def setUp(self):
|
||||
root_helper_opt = [
|
||||
cfg.StrOpt('root_helper', default='sudo'),
|
||||
]
|
||||
self.conf = config.setup_conf()
|
||||
self.conf.register_opts(interface.OPTS)
|
||||
self.conf.register_opts(root_helper_opt)
|
||||
self.ip_dev_p = mock.patch.object(ip_lib, 'IPDevice')
|
||||
self.ip_dev = self.ip_dev_p.start()
|
||||
self.device_exists_p = mock.patch.object(ip_lib, 'device_exists')
|
||||
self.device_exists = self.device_exists_p.start()
|
||||
|
||||
def tearDown(self):
|
||||
# sometimes a test may turn this off
|
||||
try:
|
||||
self.device_exists_p.stop()
|
||||
except RuntimeError, e:
|
||||
pass
|
||||
self.ip_dev_p.stop()
|
||||
|
||||
|
||||
class TestABCDriver(TestBase):
|
||||
def test_l3_init(self):
|
||||
addresses = [dict(ip_version=4, scope='global',
|
||||
dynamic=False, cidr='172.16.77.240/24')]
|
||||
self.ip_dev().addr.list = mock.Mock(return_value=addresses)
|
||||
|
||||
bc = BaseChild(self.conf)
|
||||
bc.init_l3(FakePort(), 'tap0')
|
||||
self.ip_dev.assert_has_calls(
|
||||
[mock.call('tap0', 'sudo'),
|
||||
mock.call().addr.list(scope='global', filters=['permanent']),
|
||||
mock.call().addr.add(4, '192.168.1.2/24', '192.168.1.255'),
|
||||
mock.call().addr.delete(4, '172.16.77.240/24')])
|
||||
|
||||
|
||||
class TestOVSInterfaceDriver(TestBase):
|
||||
def test_plug(self, additional_expectation=[]):
|
||||
def device_exists(dev, root_helper=None):
|
||||
return dev == 'br-int'
|
||||
|
||||
vsctl_cmd = ['ovs-vsctl', '--', '--may-exist', 'add-port',
|
||||
'br-int', 'tap0', '--', 'set', 'Interface', 'tap0',
|
||||
'type=internal', '--', 'set', 'Interface', 'tap0',
|
||||
'external-ids:iface-id=port-1234', '--', 'set',
|
||||
'Interface', 'tap0',
|
||||
'external-ids:iface-status=active', '--', 'set',
|
||||
'Interface', 'tap0',
|
||||
'external-ids:attached-mac=aa:bb:cc:dd:ee:ff']
|
||||
|
||||
with mock.patch.object(utils, 'execute') as execute:
|
||||
ovs = interface.OVSInterfaceDriver(self.conf)
|
||||
self.device_exists.side_effect = device_exists
|
||||
ovs.plug('01234567-1234-1234-99',
|
||||
'port-1234',
|
||||
'tap0',
|
||||
'aa:bb:cc:dd:ee:ff')
|
||||
execute.assert_called_once_with(vsctl_cmd, 'sudo')
|
||||
|
||||
expected = [mock.call('tap0', 'sudo'),
|
||||
mock.call().link.set_address('aa:bb:cc:dd:ee:ff')]
|
||||
|
||||
expected.extend(additional_expectation)
|
||||
expected.append(mock.call().link.set_up())
|
||||
self.ip_dev.assert_has_calls(expected)
|
||||
|
||||
def test_plug_mtu(self):
|
||||
self.conf.set_override('network_device_mtu', 9000)
|
||||
self.test_plug([mock.call().link.set_mtu(9000)])
|
||||
|
||||
def test_unplug(self):
|
||||
with mock.patch('quantum.agent.linux.ovs_lib.OVSBridge') as ovs_br:
|
||||
ovs = interface.OVSInterfaceDriver(self.conf)
|
||||
ovs.unplug('tap0')
|
||||
ovs_br.assert_has_calls([mock.call('br-int', 'sudo'),
|
||||
mock.call().delete_port('tap0')])
|
||||
|
||||
|
||||
class TestBridgeInterfaceDriver(TestBase):
|
||||
def test_get_bridge(self):
|
||||
br = interface.BridgeInterfaceDriver(self.conf)
|
||||
self.assertEqual('brq12345678-11', br.get_bridge('12345678-1122-3344'))
|
||||
|
||||
def test_plug(self):
|
||||
def device_exists(device, root_helper=None):
|
||||
return device.startswith('brq')
|
||||
|
||||
expected = [mock.call(c, 'sudo') for c in [
|
||||
['ip', 'tuntap', 'add', 'tap0', 'mode', 'tap'],
|
||||
['ip', 'link', 'set', 'tap0', 'address', 'aa:bb:cc:dd:ee:ff'],
|
||||
['ip', 'link', 'set', 'tap0', 'up']]
|
||||
]
|
||||
|
||||
self.device_exists.side_effect = device_exists
|
||||
br = interface.BridgeInterfaceDriver(self.conf)
|
||||
br.plug('01234567-1234-1234-99',
|
||||
'port-1234',
|
||||
'tap0',
|
||||
'aa:bb:cc:dd:ee:ff')
|
||||
|
||||
self.ip_dev.assert_has_calls(
|
||||
[mock.call('tap0', 'sudo'),
|
||||
mock.call().tuntap.add(),
|
||||
mock.call().link.set_address('aa:bb:cc:dd:ee:ff'),
|
||||
mock.call().link.set_up()])
|
||||
|
||||
def test_plug_dev_exists(self):
|
||||
self.device_exists.return_value = True
|
||||
with mock.patch('quantum.agent.linux.interface.LOG.warn') as log:
|
||||
br = interface.BridgeInterfaceDriver(self.conf)
|
||||
br.plug('01234567-1234-1234-99',
|
||||
'port-1234',
|
||||
'tap0',
|
||||
'aa:bb:cc:dd:ee:ff')
|
||||
self.ip_dev.assert_has_calls([])
|
||||
self.assertEquals(log.call_count, 1)
|
||||
|
||||
def test_tunctl_failback(self):
|
||||
def device_exists(dev, root_helper=None):
|
||||
return dev.startswith('brq')
|
||||
|
||||
expected = [mock.call(c, 'sudo') for c in [
|
||||
['ip', 'tuntap', 'add', 'tap0', 'mode', 'tap'],
|
||||
['tunctl', '-b', '-t', 'tap0'],
|
||||
['ip', 'link', 'set', 'tap0', 'address', 'aa:bb:cc:dd:ee:ff'],
|
||||
['ip', 'link', 'set', 'tap0', 'up']]
|
||||
]
|
||||
|
||||
self.device_exists.side_effect = device_exists
|
||||
self.ip_dev().tuntap.add.side_effect = RuntimeError
|
||||
self.ip_dev.reset_calls()
|
||||
with mock.patch.object(utils, 'execute') as execute:
|
||||
br = interface.BridgeInterfaceDriver(self.conf)
|
||||
br.plug('01234567-1234-1234-99',
|
||||
'port-1234',
|
||||
'tap0',
|
||||
'aa:bb:cc:dd:ee:ff')
|
||||
execute.assert_called_once_with(['tunctl', '-b', '-t', 'tap0'],
|
||||
'sudo')
|
||||
self.ip_dev.assert_has_calls(
|
||||
[mock.call('tap0', 'sudo'),
|
||||
mock.call().tuntap.add(),
|
||||
mock.call().link.set_address('aa:bb:cc:dd:ee:ff'),
|
||||
mock.call().link.set_up()])
|
||||
|
||||
def test_unplug(self):
|
||||
self.device_exists.return_value = True
|
||||
with mock.patch('quantum.agent.linux.interface.LOG.debug') as log:
|
||||
br = interface.BridgeInterfaceDriver(self.conf)
|
||||
br.unplug('tap0')
|
||||
log.assert_called_once()
|
||||
self.execute.assert_has_calls(
|
||||
[mock.call(['ip', 'link', 'delete', 'tap0'], 'sudo')])
|
||||
|
||||
def test_unplug_no_device(self):
|
||||
self.device_exists.return_value = False
|
||||
self.ip_dev().link.delete.side_effect = RuntimeError
|
||||
with mock.patch('quantum.agent.linux.interface.LOG') as log:
|
||||
br = interface.BridgeInterfaceDriver(self.conf)
|
||||
br.unplug('tap0')
|
||||
[mock.call(), mock.call('tap0', 'sudo'), mock.call().link.delete()]
|
||||
self.assertEqual(log.error.call_count, 1)
|
||||
|
||||
def test_unplug(self):
|
||||
self.device_exists.return_value = True
|
||||
with mock.patch('quantum.agent.linux.interface.LOG.debug') as log:
|
||||
br = interface.BridgeInterfaceDriver(self.conf)
|
||||
br.unplug('tap0')
|
||||
log.assert_called_once()
|
||||
|
||||
self.ip_dev.assert_has_calls([mock.call('tap0', 'sudo'),
|
||||
mock.call().link.delete()])
|
274
quantum/tests/unit/test_linux_ip_lib.py
Normal file
274
quantum/tests/unit/test_linux_ip_lib.py
Normal file
@ -0,0 +1,274 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 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.
|
||||
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
|
||||
from quantum.agent.linux import ip_lib
|
||||
from quantum.agent.linux import utils
|
||||
|
||||
|
||||
LINK_SAMPLE = [
|
||||
'1: lo: <LOOPBACK,UP,LOWER_UP> mtu 16436 qdisc noqueue state UNKNOWN \\'
|
||||
'link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00',
|
||||
'2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP '
|
||||
'qlen 1000\ link/ether cc:dd:ee:ff:ab:cd brd ff:ff:ff:ff:ff:ff',
|
||||
'3: br-int: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN '
|
||||
'\ link/ether aa:bb:cc:dd:ee:ff brd ff:ff:ff:ff:ff:ff',
|
||||
'4: gw-ddc717df-49: <BROADCAST,MULTICAST> mtu 1500 qdisc noop '
|
||||
'state DOWN \ link/ether fe:dc:ba:fe:dc:ba brd ff:ff:ff:ff:ff:ff']
|
||||
|
||||
ADDR_SAMPLE = ("""
|
||||
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP qlen 1000
|
||||
link/ether dd:cc:aa:b9:76:ce brd ff:ff:ff:ff:ff:ff
|
||||
inet 172.16.77.240/24 brd 172.16.77.255 scope global eth0
|
||||
inet6 2001:470:9:1224:5595:dd51:6ba2:e788/64 scope global temporary dynamic
|
||||
valid_lft 14187sec preferred_lft 3387sec
|
||||
inet6 2001:470:9:1224:fd91:272:581e:3a32/64 scope global temporary """
|
||||
"""deprecated dynamic
|
||||
valid_lft 14187sec preferred_lft 0sec
|
||||
inet6 2001:470:9:1224:4508:b885:5fb:740b/64 scope global temporary """
|
||||
"""deprecated dynamic
|
||||
valid_lft 14187sec preferred_lft 0sec
|
||||
inet6 2001:470:9:1224:dfcc:aaff:feb9:76ce/64 scope global dynamic
|
||||
valid_lft 14187sec preferred_lft 3387sec
|
||||
inet6 fe80::dfcc:aaff:feb9:76ce/64 scope link
|
||||
valid_lft forever preferred_lft forever
|
||||
""")
|
||||
|
||||
|
||||
class TestIPDevice(unittest.TestCase):
|
||||
def test_execute_wrapper(self):
|
||||
with mock.patch('quantum.agent.linux.utils.execute') as execute:
|
||||
ip_lib.IPDevice._execute('o', 'link', ('list',), 'sudo')
|
||||
|
||||
execute.assert_called_once_with(['ip', '-o', 'link', 'list'],
|
||||
root_helper='sudo')
|
||||
|
||||
def test_execute_wrapper_int_options(self):
|
||||
with mock.patch('quantum.agent.linux.utils.execute') as execute:
|
||||
ip_lib.IPDevice._execute([4], 'link', ('list',))
|
||||
|
||||
execute.assert_called_once_with(['ip', '-4', 'link', 'list'],
|
||||
root_helper=None)
|
||||
|
||||
def test_execute_wrapper_no_options(self):
|
||||
with mock.patch('quantum.agent.linux.utils.execute') as execute:
|
||||
ip_lib.IPDevice._execute([], 'link', ('list',))
|
||||
|
||||
execute.assert_called_once_with(['ip', 'link', 'list'],
|
||||
root_helper=None)
|
||||
|
||||
def test_get_devices(self):
|
||||
with mock.patch.object(ip_lib.IPDevice, '_execute') as _execute:
|
||||
_execute.return_value = '\n'.join(LINK_SAMPLE)
|
||||
retval = ip_lib.IPDevice.get_devices()
|
||||
self.assertEquals(retval,
|
||||
[ip_lib.IPDevice('lo'),
|
||||
ip_lib.IPDevice('eth0'),
|
||||
ip_lib.IPDevice('br-int'),
|
||||
ip_lib.IPDevice('gw-ddc717df-49')])
|
||||
|
||||
_execute.assert_called_once_with('o', 'link', ('list',))
|
||||
|
||||
|
||||
class TestIPCommandBase(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.ip_dev = mock.Mock()
|
||||
self.ip_dev.name = 'eth0'
|
||||
self.ip_dev.root_helper = 'sudo'
|
||||
self.ip_dev._execute = mock.Mock(return_value='executed')
|
||||
self.ip_cmd = ip_lib.IpCommandBase(self.ip_dev)
|
||||
self.ip_cmd.COMMAND = 'foo'
|
||||
|
||||
def test_run(self):
|
||||
self.assertEqual(self.ip_cmd._run('link', 'show'), 'executed')
|
||||
self.ip_dev._execute.assert_called_once_with([], 'foo',
|
||||
('link', 'show'))
|
||||
|
||||
def test_run_with_options(self):
|
||||
self.assertEqual(self.ip_cmd._run('link', options='o'), 'executed')
|
||||
self.ip_dev._execute.assert_called_once_with('o', 'foo', ('link',))
|
||||
|
||||
def test_as_root(self):
|
||||
self.assertEqual(self.ip_cmd._as_root('link'), 'executed')
|
||||
self.ip_dev._execute.assert_called_once_with([], 'foo',
|
||||
('link',), 'sudo')
|
||||
|
||||
def test_as_root_with_options(self):
|
||||
self.assertEqual(self.ip_cmd._as_root('link', options='o'), 'executed')
|
||||
self.ip_dev._execute.assert_called_once_with('o', 'foo',
|
||||
('link',), 'sudo')
|
||||
|
||||
def test_name_property(self):
|
||||
self.assertEqual(self.ip_cmd.name, 'eth0')
|
||||
|
||||
|
||||
class TestIPCmdBase(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.parent = mock.Mock()
|
||||
self.parent.name = 'eth0'
|
||||
self.parent.root_helper = 'sudo'
|
||||
|
||||
def _assert_call(self, options, args):
|
||||
self.parent.assert_has_calls([
|
||||
mock.call._execute(options, self.command, args)])
|
||||
|
||||
def _assert_sudo(self, options, args):
|
||||
self.parent.assert_has_calls([
|
||||
mock.call._execute(options, self.command, args, 'sudo')])
|
||||
|
||||
|
||||
class TestIpLinkCommand(TestIPCmdBase):
|
||||
def setUp(self):
|
||||
super(TestIpLinkCommand, self).setUp()
|
||||
self.command = 'link'
|
||||
self.link_cmd = ip_lib.IpLinkCommand(self.parent)
|
||||
|
||||
def test_set_address(self):
|
||||
self.link_cmd.set_address('aa:bb:cc:dd:ee:ff')
|
||||
self._assert_sudo([], ('set', 'eth0', 'address', 'aa:bb:cc:dd:ee:ff'))
|
||||
|
||||
def test_set_mtu(self):
|
||||
self.link_cmd.set_mtu(1500)
|
||||
self._assert_sudo([], ('set', 'eth0', 'mtu', 1500))
|
||||
|
||||
def test_set_up(self):
|
||||
self.link_cmd.set_up()
|
||||
self._assert_sudo([], ('set', 'eth0', 'up'))
|
||||
|
||||
def test_set_down(self):
|
||||
self.link_cmd.set_down()
|
||||
self._assert_sudo([], ('set', 'eth0', 'down'))
|
||||
|
||||
def test_delete(self):
|
||||
self.link_cmd.delete()
|
||||
self._assert_sudo([], ('delete', 'eth0'))
|
||||
|
||||
def test_address_property(self):
|
||||
self.parent._execute = mock.Mock(return_value=LINK_SAMPLE[1])
|
||||
self.assertEqual(self.link_cmd.address, 'cc:dd:ee:ff:ab:cd')
|
||||
|
||||
def test_mtu_property(self):
|
||||
self.parent._execute = mock.Mock(return_value=LINK_SAMPLE[1])
|
||||
self.assertEqual(self.link_cmd.mtu, 1500)
|
||||
|
||||
def test_qdisc_property(self):
|
||||
self.parent._execute = mock.Mock(return_value=LINK_SAMPLE[1])
|
||||
self.assertEqual(self.link_cmd.qdisc, 'mq')
|
||||
|
||||
def test_qlen_property(self):
|
||||
self.parent._execute = mock.Mock(return_value=LINK_SAMPLE[1])
|
||||
self.assertEqual(self.link_cmd.qlen, 1000)
|
||||
|
||||
def test_settings_property(self):
|
||||
expected = {'mtu': 1500,
|
||||
'qlen': 1000,
|
||||
'state': 'UP',
|
||||
'qdisc': 'mq',
|
||||
'brd': 'ff:ff:ff:ff:ff:ff',
|
||||
'link/ether': 'cc:dd:ee:ff:ab:cd'}
|
||||
self.parent._execute = mock.Mock(return_value=LINK_SAMPLE[1])
|
||||
self.assertEquals(self.link_cmd.attributes, expected)
|
||||
self._assert_call('o', ('show', 'eth0'))
|
||||
|
||||
|
||||
class TestIpTuntapCommand(TestIPCmdBase):
|
||||
def setUp(self):
|
||||
super(TestIpTuntapCommand, self).setUp()
|
||||
self.parent.name = 'tap0'
|
||||
self.command = 'tuntap'
|
||||
self.tuntap_cmd = ip_lib.IpTuntapCommand(self.parent)
|
||||
|
||||
def test_add_tap(self):
|
||||
self.tuntap_cmd.add()
|
||||
self._assert_sudo([], ('add', 'tap0', 'mode', 'tap'))
|
||||
|
||||
|
||||
class TestIpAddrCommand(TestIPCmdBase):
|
||||
def setUp(self):
|
||||
super(TestIpAddrCommand, self).setUp()
|
||||
self.parent.name = 'tap0'
|
||||
self.command = 'addr'
|
||||
self.addr_cmd = ip_lib.IpAddrCommand(self.parent)
|
||||
|
||||
def test_add_address(self):
|
||||
self.addr_cmd.add(4, '192.168.45.100/24', '192.168.45.255')
|
||||
self._assert_sudo([4],
|
||||
('add', '192.168.45.100/24', 'brd', '192.168.45.255',
|
||||
'scope', 'global', 'dev', 'tap0'))
|
||||
|
||||
def test_add_address_scoped(self):
|
||||
self.addr_cmd.add(4, '192.168.45.100/24', '192.168.45.255',
|
||||
scope='link')
|
||||
self._assert_sudo([4],
|
||||
('add', '192.168.45.100/24', 'brd', '192.168.45.255',
|
||||
'scope', 'link', 'dev', 'tap0'))
|
||||
|
||||
def test_del_address(self):
|
||||
self.addr_cmd.delete(4, '192.168.45.100/24')
|
||||
self._assert_sudo([4],
|
||||
('del', '192.168.45.100/24', 'dev', 'tap0'))
|
||||
|
||||
def test_flush(self):
|
||||
self.addr_cmd.flush()
|
||||
self._assert_sudo([], ('flush', 'tap0'))
|
||||
|
||||
def test_list(self):
|
||||
expected = [
|
||||
dict(ip_version=4, scope='global',
|
||||
dynamic=False, cidr='172.16.77.240/24'),
|
||||
dict(ip_version=6, scope='global',
|
||||
dynamic=True, cidr='2001:470:9:1224:5595:dd51:6ba2:e788/64'),
|
||||
dict(ip_version=6, scope='global',
|
||||
dynamic=True, cidr='2001:470:9:1224:fd91:272:581e:3a32/64'),
|
||||
dict(ip_version=6, scope='global',
|
||||
dynamic=True, cidr='2001:470:9:1224:4508:b885:5fb:740b/64'),
|
||||
dict(ip_version=6, scope='global',
|
||||
dynamic=True, cidr='2001:470:9:1224:dfcc:aaff:feb9:76ce/64'),
|
||||
dict(ip_version=6, scope='link',
|
||||
dynamic=False, cidr='fe80::dfcc:aaff:feb9:76ce/64')]
|
||||
|
||||
self.parent._execute = mock.Mock(return_value=ADDR_SAMPLE)
|
||||
self.assertEquals(self.addr_cmd.list(), expected)
|
||||
self._assert_call([], ('show', 'tap0'))
|
||||
|
||||
def test_list_filtered(self):
|
||||
expected = [
|
||||
dict(ip_version=4, scope='global',
|
||||
dynamic=False, cidr='172.16.77.240/24')]
|
||||
|
||||
output = '\n'.join(ADDR_SAMPLE.split('\n')[0:4])
|
||||
self.parent._execute = mock.Mock(return_value=output)
|
||||
self.assertEquals(self.addr_cmd.list('global', filters=['permanent']),
|
||||
expected)
|
||||
self._assert_call([], ('show', 'tap0', 'permanent', 'scope', 'global'))
|
||||
|
||||
|
||||
class TestDeviceExists(unittest.TestCase):
|
||||
def test_device_exists(self):
|
||||
with mock.patch.object(ip_lib.IPDevice, '_execute') as _execute:
|
||||
_execute.return_value = LINK_SAMPLE[1]
|
||||
self.assertTrue(ip_lib.device_exists('eth0'))
|
||||
_execute.assert_called_once_with('o', 'link', ('show', 'eth0'))
|
||||
|
||||
def test_device_does_not_exist(self):
|
||||
with mock.patch.object(ip_lib.IPDevice, '_execute') as _execute:
|
||||
_execute.return_value = ''
|
||||
_execute.side_effect = RuntimeError
|
||||
self.assertFalse(ip_lib.device_exists('eth0'))
|
6
setup.py
6
setup.py
@ -51,7 +51,10 @@ ryu_plugin_config_path = 'etc/quantum/plugins/ryu'
|
||||
|
||||
DataFiles = [
|
||||
(config_path,
|
||||
['etc/quantum.conf', 'etc/api-paste.ini', 'etc/policy.json']),
|
||||
['etc/quantum.conf',
|
||||
'etc/api-paste.ini',
|
||||
'etc/policy.json',
|
||||
'etc/dhcp_agent.ini']),
|
||||
(init_path, ['etc/init.d/quantum-server']),
|
||||
(ovs_plugin_config_path,
|
||||
['etc/quantum/plugins/openvswitch/ovs_quantum_plugin.ini']),
|
||||
@ -89,6 +92,7 @@ setuptools.setup(
|
||||
eager_resources=EagerResources,
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'quantum-dhcp-agent = quantum.agent.dhcp_agent:main',
|
||||
'quantum-linuxbridge-agent ='
|
||||
'quantum.plugins.linuxbridge.agent.linuxbridge_quantum_agent:main',
|
||||
'quantum-openvswitch-agent ='
|
||||
|
@ -2,8 +2,10 @@ Paste
|
||||
PasteDeploy==1.5.0
|
||||
Routes>=1.12.3
|
||||
eventlet>=0.9.12
|
||||
httplib2
|
||||
lxml
|
||||
netaddr
|
||||
python-gflags==1.3
|
||||
python-quantumclient>=0.1,<0.2
|
||||
sqlalchemy>0.6.4
|
||||
webob==1.2.0
|
||||
|
Loading…
Reference in New Issue
Block a user