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:
Mark McClain 2012-06-27 14:15:53 -04:00
parent bb2b244fe3
commit 43960ee448
18 changed files with 2368 additions and 1 deletions

20
bin/quantum-dhcp-agent Executable file
View File

@ -0,0 +1,20 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 2012 Openstack, LLC.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from quantum.agent.dhcp_agent import main
main()

38
etc/dhcp_agent.ini Normal file
View 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

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

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

View 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

View 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

View File

@ -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.")

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

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

View File

@ -17,6 +17,8 @@
import unittest
import mock
from quantum.agent.linux import utils

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

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

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

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

View File

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

View File

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