--- /dev/null
+#!/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()
--- /dev/null
+[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
+
--- /dev/null
+# 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.
--- /dev/null
+# 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
--- /dev/null
+# 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()
--- /dev/null
+# 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)
--- /dev/null
+# 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
--- /dev/null
+# 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
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.")
--- /dev/null
+# 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']),
+]
--- /dev/null
+# 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')
import unittest
+import mock
+
from quantum.agent.linux import utils
--- /dev/null
+# 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()])
--- /dev/null
+# 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')
--- /dev/null
+# 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()])
--- /dev/null
+# 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'))
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']),
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 ='
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