]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
Convert DHCP from polling to RPC
authorMark McClain <mark.mcclain@dreamhost.com>
Tue, 7 Aug 2012 20:23:05 +0000 (16:23 -0400)
committerMark McClain <mark.mcclain@dreamhost.com>
Tue, 14 Aug 2012 16:28:54 +0000 (12:28 -0400)
blueprint non-polling-dhcp-impl

This requires a change to devstack/stack.sh.
See https://review.openstack.org/#/c/11278/ for the required changes.

Change-Id: I1ea22c8e1b80e630bcb83f27a31aaeef482aff6c

17 files changed:
etc/dhcp_agent.ini
etc/quantum.conf
quantum/agent/dhcp_agent.py
quantum/agent/linux/dhcp.py
quantum/agent/linux/interface.py
quantum/agent/linux/ip_lib.py
quantum/agent/rpc.py
quantum/common/topics.py
quantum/db/dhcp_rpc_base.py [new file with mode: 0644]
quantum/plugins/linuxbridge/lb_quantum_plugin.py
quantum/plugins/openvswitch/ovs_quantum_plugin.py
quantum/tests/unit/test_agent_rpc.py [new file with mode: 0644]
quantum/tests/unit/test_db_plugin.py
quantum/tests/unit/test_db_rpc_base.py [new file with mode: 0644]
quantum/tests/unit/test_dhcp_agent.py
quantum/tests/unit/test_linux_dhcp.py
quantum/tests/unit/test_linux_interface.py

index 377c83b649f1010dd0ef2ed586a186961e9181aa..8ecc9d0f9684a6a02f213889395a1f910dfdeda1 100644 (file)
@@ -24,24 +24,4 @@ dhcp_driver = quantum.agent.linux.dhcp.Dnsmasq
 
 # Allow overlapping IP (Must have kernel build with CONFIG_NET_NS=y and
 # iproute2 package that supports namespaces).
-use_namespaces = True
-
-#
-# 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 database used by the Ryu Quantum plugin
-#db_connection = mysql://root:password@localhost/ryu_quantum
-
-# 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
-
+# use_namespaces = True
index 262f9bf5d641208597611443c97f9d40b8056d03..39f18de53e1d939c28775c91fe7056a2b966efa5 100644 (file)
@@ -124,6 +124,25 @@ control_exchange = quantum
 # The "host" option should point or resolve to this address.
 # rpc_zmq_bind_address = *
 
+# ============ Notification System Options =====================
+
+# Notifications can be sent when network/subnet/port are create, updated or deleted.
+# There are four methods of sending notifications, logging (via the
+# log_file directive), rpc (via a message queue),
+# noop (no notifications sent, the default) or list of them
+
+# Defined in notifier api
+notification_driver = quantum.openstack.common.notifier.list_notifier
+# default_notification_level = INFO
+# myhost = myhost.com
+# default_publisher_id = $myhost
+
+# Defined in rabbit_notifier for rpc way
+# notification_topics = notifications
+
+# Defined in list_notifier
+list_notifier_drivers = quantum.openstack.common.notifier.rabbit_notifier
+
 [QUOTAS]
 # resource name(s) that are supported in quota features
 # quota_items = network,subnet,port
@@ -142,22 +161,3 @@ control_exchange = quantum
 
 # default driver to use for quota checks
 # quota_driver = quantum.quota.ConfDriver
-
-# ============ Notification System Options =====================
-
-# Notifications can be sent when network/subnet/port are create, updated or deleted.
-# There are four methods of sending notifications, logging (via the
-# log_file directive), rpc (via a message queue),
-# noop (no notifications sent, the default) or list of them
-
-# Defined in notifier api
-# notification_driver = quantum.openstack.common.notifier.no_op_notifier
-# default_notification_level = INFO
-# myhost = myhost.com
-# default_publisher_id = $myhost
-
-# Defined in rabbit_notifier for rpc way
-# notification_topics = notifications
-
-# Defined in list_notifier
-# list_notifier_drivers = quantum.openstack.common.notifier.no_op_notifier
index 5c4409cbc419ca4b12776d2e5701070a58e6964d..bf6229bbf0873f61d5ea0c75b6b8ff0e4a896d08 100644 (file)
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
-import collections
 import logging
 import socket
 import sys
-import time
 import uuid
 
+import eventlet
 import netaddr
-from sqlalchemy.ext import sqlsoup
 
+from quantum.agent import rpc as agent_rpc
 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.common import topics
 from quantum.openstack.common import cfg
+from quantum.openstack.common import context
 from quantum.openstack.common import importutils
+from quantum.openstack.common.rpc import proxy
 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."),
         cfg.BoolOpt('use_namespaces', default=True,
                     help="Allow overlapping IP.")
     ]
 
     def __init__(self, conf):
         self.conf = conf
+        self.cache = NetworkCache()
+
         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()
-
-            network_admin_up = {}
-            for network in self.db.networks.all():
-                network_admin_up[network.id] = network.admin_state_up
-
-            for subnet in self.db.subnets.all():
-                if (not subnet.enable_dhcp or
-                        not network_admin_up[subnet.network_id]):
-                    continue
-                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()
-                                 if a.subnet_id in subnets])
-
-            networks = set(subnets.itervalues())
-
-            return State(networks, subnet_hashes, ipalloc_hashes)
+        ctx = context.RequestContext('quantum', 'quantum', is_admin=True)
+        self.plugin_rpc = DhcpPluginApi(topics.PLUGIN, ctx)
 
-        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):
+        self.device_manager = DeviceManager(self.conf, self.plugin_rpc)
+        self.notifications = agent_rpc.NotificationDispatcher()
+
+    def run(self):
+        """Activate the DHCP agent."""
+        # enable DHCP for current networks
+        for network_id in self.plugin_rpc.get_active_networks():
+            self.enable_dhcp_helper(network_id)
+
+        self.notifications.run_dispatch(self)
+
+    def call_driver(self, action, network):
         """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
-            )
+            # the base models.
             driver = self.dhcp_driver_cls(self.conf,
                                           network,
                                           self.conf.root_helper,
-                                          DeviceManager(self.conf,
-                                                        self.db,
-                                                        'network:dhcp'))
+                                          self.device_manager)
             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)
+    def enable_dhcp_helper(self, network_id):
+        """Enable DHCP for a network that meets enabling criteria."""
+        network = self.plugin_rpc.get_network_info(network_id)
+        for subnet in network.subnets:
+            if subnet.enable_dhcp:
+                if network.admin_state_up:
+                    self.call_driver('enable', network)
+                    self.cache.put(network)
+                break
+
+    def disable_dhcp_helper(self, network_id):
+        """Disable DHCP for a network known to the agent."""
+        network = self.cache.get_network_by_id(network_id)
+        if network:
+            self.call_driver('disable', network)
+            self.cache.remove(network)
+
+    def refresh_dhcp_helper(self, network_id):
+        """Refresh or disable DHCP for a network depending on the current state
+        of the network.
+
+        """
+        if not self.cache.get_network_by_id(network_id):
+            # DHCP current not running for network.
+            self.enable_dhcp_helper(network_id)
+
+        network = self.plugin_rpc.get_network_info(network_id)
+        for subnet in network.subnets:
+            if subnet.enable_dhcp:
+                self.cache.put(network)
+                self.call_driver('update_l3', network)
+                break
+        else:
+            self.disable_dhcp_helper(network.id)
+
+    def network_create_end(self, payload):
+        """Handle the network.create.end notification event."""
+        network_id = payload['network']['id']
+        self.enable_dhcp_helper(network_id)
+
+    def network_update_end(self, payload):
+        """Handle the network.update.end notification event."""
+        network_id = payload['network']['id']
+        if payload['network']['admin_state_up']:
+            self.enable_dhcp_helper(network_id)
+        else:
+            self.disable_dhcp_helper(network_id)
+
+    def network_delete_start(self, payload):
+        """Handle the network.detete.start notification event."""
+        self.disable_dhcp_helper(payload['network_id'])
+
+    def subnet_delete_start(self, payload):
+        """Handle the subnet.detete.start notification event."""
+        subnet_id = payload['subnet_id']
+        network = self.cache.get_network_by_subnet_id(subnet_id)
+        if network:
+            device_id = self.device_manager.get_device_id(network)
+            self.plugin_rpc.release_port_fixed_ip(network.id, device_id,
+                                                  subnet_id)
+
+    def subnet_update_end(self, payload):
+        """Handle the subnet.update.end notification event."""
+        network_id = payload['subnet']['network_id']
+        self.refresh_dhcp_helper(network_id)
+
+    # Use the update handler for the subnet create event.
+    subnet_create_end = subnet_update_end
+
+    def subnet_delete_end(self, payload):
+        """Handle the subnet.delete.end notification event."""
+        subnet_id = payload['subnet_id']
+        network = self.cache.get_network_by_subnet_id(subnet_id)
+        if network:
+            self.refresh_dhcp_helper(network.id)
+
+    def port_update_end(self, payload):
+        """Handle the port.update.end notification event."""
+        port = DictModel(payload['port'])
+        network = self.cache.get_network_by_id(port.network_id)
+        if network:
+            self.cache.put_port(port)
+            self.call_driver('reload_allocations', network)
+
+    # Use the update handler for the port create event.
+    port_create_end = port_update_end
+
+    def port_delete_end(self, payload):
+        """Handle the port.delete.end notification event."""
+        port = self.cache.get_port_by_id(payload['port_id'])
+        if port:
+            network = self.cache.get_network_by_id(port.network_id)
+            self.cache.remove_port(port)
+            self.call_driver('reload_allocations', network)
+
+
+class DhcpPluginApi(proxy.RpcProxy):
+    """Agent side of the dhcp rpc API.
+
+    API version history:
+        1.0 - Initial version.
+
+    """
+
+    BASE_RPC_API_VERSION = '1.0'
+
+    def __init__(self, topic, context):
+        super(DhcpPluginApi, self).__init__(
+            topic=topic, default_version=self.BASE_RPC_API_VERSION)
+        self.context = context
+        self.host = socket.gethostname()
+
+    def get_active_networks(self):
+        """Make a remote process call to retrieve the active networks."""
+        return self.call(self.context,
+                         self.make_msg('get_active_networks', host=self.host),
+                         topic=self.topic)
+
+    def get_network_info(self, network_id):
+        """Make a remote process call to retrieve network info."""
+        return DictModel(self.call(self.context,
+                                   self.make_msg('get_network_info',
+                                                 network_id=network_id,
+                                                 host=self.host),
+                                   topic=self.topic))
+
+    def get_dhcp_port(self, network_id, device_id):
+        """Make a remote process call to create the dhcp port."""
+        return DictModel(self.call(self.context,
+                                   self.make_msg('get_dhcp_port',
+                                                 network_id=network_id,
+                                                 device_id=device_id,
+                                                 host=self.host),
+                                   topic=self.topic))
+
+    def release_dhcp_port(self, network_id, device_id):
+        """Make a remote process call to release the dhcp port."""
+        return self.call(self.context,
+                         self.make_msg('release_dhcp_port',
+                                       network_id=network_id,
+                                       device_id=device_id,
+                                       host=self.host),
+                         topic=self.topic)
+
+    def release_port_fixed_ip(self, network_id, device_id, subnet_id):
+        """Make a remote process call to release a fixed_ip on the port."""
+        return self.call(self.context,
+                         self.make_msg('release_port_fixed_ip',
+                                       network_id=network_id,
+                                       subnet_id=subnet_id,
+                                       device_id=device_id,
+                                       host=self.host),
+                         topic=self.topic)
+
+
+class NetworkCache(object):
+    """Agent cache of the current network state."""
+    def __init__(self):
+        self.cache = {}
+        self.subnet_lookup = {}
+        self.port_lookup = {}
+
+    def get_network_by_id(self, network_id):
+        return self.cache.get(network_id)
+
+    def get_network_by_subnet_id(self, subnet_id):
+        return self.cache.get(self.subnet_lookup.get(subnet_id))
+
+    def get_network_by_port_id(self, port_id):
+        return self.cache.get(self.port_lookup.get(port_id))
+
+    def put(self, network):
+        if network.id in self.cache:
+            self.remove(self.cache[network.id])
+
+        self.cache[network.id] = network
+
+        for subnet in network.subnets:
+            self.subnet_lookup[subnet.id] = network.id
+
+        for port in network.ports:
+            self.port_lookup[port.id] = network.id
+
+    def remove(self, network):
+        del self.cache[network.id]
+
+        for subnet in network.subnets:
+            del self.subnet_lookup[subnet.id]
+
+        for port in network.ports:
+            del self.port_lookup[port.id]
+
+    def put_port(self, port):
+        network = self.get_network_by_id(port.network_id)
+        for index in range(len(network.ports)):
+            if network.ports[index].id == port.id:
+                network.ports[index] = port
+                break
+        else:
+            network.ports.append(port)
+
+        self.port_lookup[port.id] = network.id
+
+    def remove_port(self, port):
+        network = self.get_network_by_port_id(port.id)
+
+        for index in range(len(network.ports)):
+            if network.ports[index] == port:
+                del network.ports[index]
+                del self.port_lookup[port.id]
+                break
+
+    def get_port_by_id(self, port_id):
+        network = self.get_network_by_port_id(port_id)
+        if network:
+            for port in network.ports:
+                if port.id == port_id:
+                    return port
 
 
 class DeviceManager(object):
@@ -212,20 +314,22 @@ class DeviceManager(object):
                    help="The driver used to manage the virtual interface.")
     ]
 
-    def __init__(self, conf, db, device_owner=''):
+    def __init__(self, conf, plugin):
         self.conf = conf
-        self.db = db
-        self.device_owner = device_owner
+        self.plugin = plugin
         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, port=None):
+        """Return interface(device) name for use by the DHCP process."""
         if not port:
-            port = self._get_or_create_port(network)
+            device_id = self.get_device_id(network)
+            port = self.plugin.get_dhcp_port(network.id, device_id)
         return self.driver.get_device_name(port)
 
     def get_device_id(self, network):
+        """Return a unique DHCP device ID for this host on the network."""
         # There could be more than one dhcp server per network, so create
         # a device id that combines host and network ids
 
@@ -233,7 +337,10 @@ class DeviceManager(object):
         return 'dhcp%s-%s' % (host_uuid, network.id)
 
     def setup(self, network, reuse_existing=False):
-        port = self._get_or_create_port(network)
+        """Create and initialize a device for network's DHCP on this host."""
+        device_id = self.get_device_id(network)
+        port = self.plugin.get_dhcp_port(network.id, device_id)
+
         interface_name = self.get_interface_name(network, port)
 
         if self.conf.use_namespaces:
@@ -266,128 +373,45 @@ class DeviceManager(object):
                             namespace=namespace)
 
     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),
-            device_owner=self.device_owner,
-            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.
+        """Destroy the device used for the network's DHCP on this host."""
+        if self.conf.use_namespaces:
+            namespace = network.id
+        else:
+            namespace = None
 
-        If .subnet, is accessed, the wrapper will return a subnet
-        object if this instance has a subnet_id attribute.
+        self.driver.unplug(self.get_interface_name(network),
+                           namespace=namespace)
+        self.plugin.release_dhcp_port(network.id, self.get_device_id(network))
 
-        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.
-        """
+    def update_l3(self, network):
+        """Update the L3 attributes for the current network's DHCP device."""
+        self.setup(network, reuse_existing=True)
 
-        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
+class DictModel(object):
+    """Convert dict into an object that provides attribute access to values."""
+    def __init__(self, d):
+        for key, value in d.iteritems():
+            if isinstance(value, list):
+                value = [DictModel(item) if isinstance(item, dict) else item
+                         for item in value]
+            elif isinstance(value, dict):
+                value = DictModel(value)
 
-        raise AttributeError
+            setattr(self, key, value)
 
 
 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()
+    eventlet.monkey_patch()
+    cfg.CONF.register_opts(DhcpAgent.OPTS)
+    cfg.CONF.register_opts(DeviceManager.OPTS)
+    cfg.CONF.register_opts(dhcp.OPTS)
+    cfg.CONF.register_opts(interface.OPTS)
+    cfg.CONF(args=sys.argv, project='quantum')
+    config.setup_logging(cfg.CONF)
+
+    mgr = DhcpAgent(cfg.CONF)
+    mgr.run()
 
 
 if __name__ == '__main__':
index 270a2c4ffe5f292f1cdb34fa4193b1103480a81f..2ed335d09f86100f380917b053f3c507e2367fdf 100644 (file)
@@ -74,6 +74,10 @@ class DhcpBase(object):
     def disable(self):
         """Disable dhcp for this network."""
 
+    @abc.abstractmethod
+    def update_l3(self, subnet, reason):
+        """Alert the driver that a subnet has changed."""
+
     def restart(self):
         """Restart the dhcp service for the network."""
         self.disable()
@@ -125,6 +129,11 @@ class DhcpLocalProcess(DhcpBase):
         else:
             LOG.debug(_('No DHCP started for %s') % self.network.id)
 
+    def update_l3(self):
+        """Update the L3 settings for the interface and reload settings."""
+        self.device_delegate.update_l3(self.network)
+        self.reload_allocations()
+
     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))
index 07d2d6234e09a796d477fc7e92fcb78003e6ef16..af9193c67e752c6ff22f41ee28703169d8c97f7d 100644 (file)
@@ -90,7 +90,7 @@ class LinuxInterfaceDriver(object):
         """Plug in the interface."""
 
     @abc.abstractmethod
-    def unplug(self, device_name, bridge=None):
+    def unplug(self, device_name, bridge=None, namespace=None):
         """Unplug the interface."""
 
 
@@ -99,7 +99,7 @@ class NullDriver(LinuxInterfaceDriver):
              bridge=None, namespace=None):
         pass
 
-    def unplug(self, device_name, bridge=None):
+    def unplug(self, device_name, bridge=None, namespace=None):
         pass
 
 
@@ -143,7 +143,7 @@ class OVSInterfaceDriver(LinuxInterfaceDriver):
             namespace_obj.add_device_to_namespace(device)
         device.link.set_up()
 
-    def unplug(self, device_name, bridge=None):
+    def unplug(self, device_name, bridge=None, namespace=None):
         """Unplug the interface."""
         if not bridge:
             bridge = self.conf.ovs_integration_bridge
@@ -180,9 +180,9 @@ class BridgeInterfaceDriver(LinuxInterfaceDriver):
         else:
             LOG.warn(_("Device %s already exists") % device_name)
 
-    def unplug(self, device_name, bridge=None):
+    def unplug(self, device_name, bridge=None, namespace=None):
         """Unplug the interface."""
-        device = ip_lib.IPDevice(device_name, self.conf.root_helper)
+        device = ip_lib.IPDevice(device_name, self.conf.root_helper, namespace)
         try:
             device.link.delete()
             LOG.debug(_("Unplugged interface '%s'") % device_name)
index f03cc66e98c80178f003ebb93942e21cfd34191c..3670fccf58daac7c82949c404fc8af30ef2090a4 100644 (file)
@@ -191,8 +191,10 @@ class IpLinkCommand(IpDeviceCommandBase):
         return self._parse_line(self._run('show', self.name, options='o'))
 
     def _parse_line(self, value):
-        device_name, settings = value.replace("\\", '').split('>', 1)
+        if not value:
+            return {}
 
+        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]]
@@ -286,4 +288,4 @@ def device_exists(device_name, root_helper=None, namespace=None):
         address = IPDevice(device_name, root_helper, namespace).link.address
     except RuntimeError:
         return False
-    return True
+    return bool(address)
index 7e7fe791f0f784df4cc01962539ee7a562c6553d..ff7aa50498ab27d062d4e34d0489dd3c36b1d64d 100644 (file)
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import logging
+
+import eventlet
+
 from quantum.common import topics
+
 from quantum.openstack.common import rpc
 from quantum.openstack.common.rpc import proxy
+from quantum.openstack.common.notifier import api
+from quantum.openstack.common.notifier import rabbit_notifier
+
+
+LOG = logging.getLogger(__name__)
 
 
 def create_consumers(dispatcher, prefix, topic_details):
@@ -67,3 +77,32 @@ class PluginApi(proxy.RpcProxy):
         return self.call(context,
                          self.make_msg('tunnel_sync', tunnel_ip=tunnel_ip),
                          topic=self.topic)
+
+
+class NotificationDispatcher(object):
+    def __init__(self):
+        # Set the Queue size to 1 so that messages stay on server rather than
+        # being buffered in the process.
+        self.queue = eventlet.queue.Queue(1)
+        self.connection = rpc.create_connection(new=True)
+        topic = '%s.%s' % (rabbit_notifier.CONF.notification_topics[0],
+                           api.CONF.default_notification_level.lower())
+        self.connection.declare_topic_consumer(topic=topic,
+                                               callback=self._add_to_queue)
+        self.connection.consume_in_thread()
+
+    def _add_to_queue(self, msg):
+        self.queue.put(msg)
+
+    def run_dispatch(self, handler):
+        while True:
+            msg = self.queue.get()
+            name = msg['event_type'].replace('.', '_')
+
+            try:
+                if hasattr(handler, name):
+                    getattr(handler, name)(msg['payload'])
+                else:
+                    LOG.debug('Unknown event_type: %s.' % msg['event_type'])
+            except Exception, e:
+                LOG.warn('Error processing message. Exception: %s' % e)
index a5521c09cba4334191dd6a11919dda969fe94115..d46769b6de59cef3da1c002b06a6d6edd67fc1d0 100644 (file)
@@ -23,6 +23,7 @@ UPDATE = 'update'
 
 AGENT = 'q-agent-notifier'
 PLUGIN = 'q-plugin'
+DHCP = 'q-dhcp-notifer'
 
 
 def get_topic_name(prefix, table, operation):
diff --git a/quantum/db/dhcp_rpc_base.py b/quantum/db/dhcp_rpc_base.py
new file mode 100644 (file)
index 0000000..e32203d
--- /dev/null
@@ -0,0 +1,173 @@
+# Copyright (c) 2012 OpenStack, LLC.
+#
+# 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 logging
+
+from sqlalchemy.orm import exc
+
+from quantum import context as quantum_context
+from quantum import manager
+from quantum.api.v2 import attributes
+from quantum.db import api as db
+from quantum.openstack.common import context
+
+
+LOG = logging.getLogger(__name__)
+
+
+def augment_context(context):
+    """Augments RPC with additional attributes, so that plugin calls work."""
+    return quantum_context.Context(context.user, None, is_admin=True,
+                                   roles=['admin'])
+
+
+class DhcpRpcCallbackMixin(object):
+    """A mix-in that enable DHCP agent support in plugin implementations."""
+
+    def get_active_networks(self, context, **kwargs):
+        """Retrieve and return a list of the active network ids."""
+        host = kwargs.get('host')
+        LOG.debug('Network list requested from %s', host)
+        plugin = manager.QuantumManager.get_plugin()
+        context = augment_context(context)
+        filters = dict(admin_state_up=[True])
+
+        return [net['id'] for net in
+                plugin.get_networks(context, filters=filters)]
+
+    def get_network_info(self, context, **kwargs):
+        """Retrieve and return a extended information about a network."""
+        network_id = kwargs.get('network_id')
+        context = augment_context(context)
+        plugin = manager.QuantumManager.get_plugin()
+        network = plugin.get_network(context, network_id)
+
+        filters = dict(network_id=[network_id])
+        network['subnets'] = plugin.get_subnets(context, filters=filters)
+        network['ports'] = plugin.get_ports(context, filters=filters)
+        return network
+
+    def get_dhcp_port(self, context, **kwargs):
+        """Allocate a DHCP port for the host and return port information.
+
+        This method will re-use an existing port if one already exists.  When a
+        port is re-used, the fixed_ip allocation will be updated to the current
+        network state.
+
+        """
+        host = kwargs.get('host')
+        network_id = kwargs.get('network_id')
+        device_id = kwargs.get('device_id')
+         # There could be more than one dhcp server per network, so create
+         # a device id that combines host and network ids
+
+        LOG.debug('Port %s for %s requested from %s', device_id, network_id,
+                  host)
+        context = augment_context(context)
+        plugin = manager.QuantumManager.get_plugin()
+        retval = None
+
+        filters = dict(network_id=[network_id])
+        subnets = dict([(s['id'], s) for s in
+                        plugin.get_subnets(context, filters=filters)])
+
+        dhcp_enabled_subnet_ids = [s['id'] for s in
+                                   subnets.values() if s['enable_dhcp']]
+
+        try:
+            filters = dict(network_id=[network_id], device_id=[device_id])
+            ports = plugin.get_ports(context, filters=filters)
+            if len(ports):
+                # Ensure that fixed_ips cover all dhcp_enabled subnets.
+                port = ports[0]
+                for fixed_ip in port['fixed_ips']:
+                    if fixed_ip['subnet_id'] in dhcp_enabled_subnet_ids:
+                        dhcp_enabled_subnet_ids.remove(fixed_ip['subnet_id'])
+                port['fixed_ips'].extend(
+                    [dict(subnet_id=s) for s in dhcp_enabled_subnet_ids])
+
+                retval = plugin.update_port(context, port['id'],
+                                            dict(port=port))
+
+        except exc.NoResultFound:
+            pass
+
+        if retval is None:
+            # No previous port exists, so create a new one.
+            LOG.debug('DHCP port %s for %s created', device_id, network_id,
+                      host)
+
+            network = plugin.get_network(context, network_id)
+
+            port_dict = dict(
+                admin_state_up=True,
+                device_id=device_id,
+                network_id=network_id,
+                tenant_id=network['tenant_id'],
+                mac_address=attributes.ATTR_NOT_SPECIFIED,
+                name='DHCP Agent',
+                device_owner='network:dhcp',
+                fixed_ips=[dict(subnet_id=s) for s in dhcp_enabled_subnet_ids])
+
+            retval = plugin.create_port(context, dict(port=port_dict))
+
+        # Convert subnet_id to subnet dict
+        for fixed_ip in retval['fixed_ips']:
+            subnet_id = fixed_ip.pop('subnet_id')
+            fixed_ip['subnet'] = subnets[subnet_id]
+
+        return retval
+
+    def release_dhcp_port(self, context, **kwargs):
+        """Release the port currently being used by a DHCP agent."""
+        host = kwargs.get('host')
+        network_id = kwargs.get('network_id')
+        device_id = kwargs.get('device_id')
+
+        LOG.debug('DHCP port deletion for %s d request from %s', network_id,
+                  host)
+        context = augment_context(context)
+        plugin = manager.QuantumManager.get_plugin()
+        filters = dict(network_id=[network_id], device_id=[device_id])
+        ports = plugin.get_ports(context, filters=filters)
+
+        if len(ports):
+            plugin.delete_port(context, ports[0]['id'])
+
+    def release_port_fixed_ip(self, context, **kwargs):
+        """Release the fixed_ip associated the subnet on a port."""
+        host = kwargs.get('host')
+        network_id = kwargs.get('network_id')
+        device_id = kwargs.get('device_id')
+        subnet_id = kwargs.get('subnet_id')
+
+        LOG.debug('DHCP port remove fixed_ip for %s d request from %s',
+                  subnet_id,
+                  host)
+
+        context = augment_context(context)
+        plugin = manager.QuantumManager.get_plugin()
+        filters = dict(network_id=[network_id], device_id=[device_id])
+        ports = plugin.get_ports(context, filters=filters)
+
+        if len(ports):
+            port = ports[0]
+
+            fixed_ips = port.get('fixed_ips', [])
+            for i in range(len(fixed_ips)):
+                if fixed_ips[i]['subnet_id'] == subnet_id:
+                    del fixed_ips[i]
+                    break
+            plugin.update_port(context, port['id'], dict(port=port))
index 534a1e1c0a87abb7791281ae502733fb020ca2e8..30ddbf831ea067890694d51e1201110a3f1783ab 100644 (file)
@@ -22,6 +22,7 @@ from quantum.common import exceptions as q_exc
 from quantum.common import topics
 from quantum.db import api as db_api
 from quantum.db import db_base_plugin_v2
+from quantum.db import dhcp_rpc_base
 from quantum.db import models_v2
 from quantum.openstack.common import context
 from quantum.openstack.common import cfg
@@ -36,7 +37,7 @@ from quantum import policy
 LOG = logging.getLogger(__name__)
 
 
-class LinuxBridgeRpcCallbacks():
+class LinuxBridgeRpcCallbacks(dhcp_rpc_base.DhcpRpcCallbackMixin):
 
     # Set RPC API version to 1.0 by default.
     RPC_API_VERSION = '1.0'
index 9bc15886ba37ec5998f86203cfdd5ac340cafb88..0e47963c527c060b1099ed708ae90fb4d48abcf3 100644 (file)
@@ -30,6 +30,7 @@ from quantum.common import topics
 from quantum.common.utils import find_config_file
 from quantum.db import api as db
 from quantum.db import db_base_plugin_v2
+from quantum.db import dhcp_rpc_base
 from quantum.db import models_v2
 from quantum.openstack.common import context
 from quantum.openstack.common import cfg
@@ -44,7 +45,7 @@ from quantum import policy
 LOG = logging.getLogger(__name__)
 
 
-class OVSRpcCallbacks():
+class OVSRpcCallbacks(dhcp_rpc_base.DhcpRpcCallbackMixin):
 
     # Set RPC API version to 1.0 by default.
     RPC_API_VERSION = '1.0'
diff --git a/quantum/tests/unit/test_agent_rpc.py b/quantum/tests/unit/test_agent_rpc.py
new file mode 100644 (file)
index 0000000..8dae30b
--- /dev/null
@@ -0,0 +1,119 @@
+# 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 import rpc
+from quantum.openstack.common import cfg
+
+
+class AgentRPCMethods(unittest.TestCase):
+    def test_create_consumers(self):
+        dispatcher = mock.Mock()
+        expected = [
+            mock.call(new=True),
+            mock.call().create_consumer('foo-topic-op', dispatcher,
+                                        fanout=True),
+            mock.call().consume_in_thread()
+        ]
+
+        call_to_patch = 'quantum.openstack.common.rpc.create_connection'
+        with mock.patch(call_to_patch) as create_connection:
+            conn = rpc.create_consumers(dispatcher, 'foo', [('topic', 'op')])
+            create_connection.assert_has_calls(expected)
+
+
+class AgentRPCNotificationDispatcher(unittest.TestCase):
+    def setUp(self):
+        self.create_connection_p = mock.patch(
+            'quantum.openstack.common.rpc.create_connection')
+        self.create_connection = self.create_connection_p.start()
+        cfg.CONF.set_override('default_notification_level', 'INFO')
+        cfg.CONF.set_override('notification_topics', ['notifications'])
+
+    def tearDown(self):
+        self.create_connection_p.stop()
+        cfg.CONF.reset()
+
+    def test_init(self):
+        nd = rpc.NotificationDispatcher()
+
+        expected = [
+            mock.call(new=True),
+            mock.call().declare_topic_consumer(topic='notifications.info',
+                                               callback=nd._add_to_queue),
+            mock.call().consume_in_thread()
+        ]
+        self.create_connection.assert_has_calls(expected)
+
+    def test_add_to_queue(self):
+        nd = rpc.NotificationDispatcher()
+        nd._add_to_queue('foo')
+        self.assertEqual(nd.queue.get(), 'foo')
+
+    def _test_run_dispatch_helper(self, msg, handler):
+        msgs = [msg]
+
+        def side_effect(*args):
+            return msgs.pop(0)
+
+        with mock.patch('eventlet.Queue.get') as queue_get:
+            queue_get.side_effect = side_effect
+            nd = rpc.NotificationDispatcher()
+            # catch the assertion so that the loop runs once
+            self.assertRaises(IndexError, nd.run_dispatch, handler)
+
+    def test_run_dispatch_once(self):
+        class SimpleHandler:
+            def __init__(self):
+                self.network_delete_end = mock.Mock()
+
+        msg = dict(event_type='network.delete.end',
+                   payload=dict(network_id='a'))
+
+        handler = SimpleHandler()
+        self._test_run_dispatch_helper(msg, handler)
+        handler.network_delete_end.called_once_with(msg['payload'])
+
+    def test_run_dispatch_missing_handler(self):
+        class SimpleHandler:
+            self.subnet_create_start = mock.Mock()
+
+        msg = dict(event_type='network.delete.end',
+                   payload=dict(network_id='a'))
+
+        handler = SimpleHandler()
+
+        with mock.patch('quantum.agent.rpc.LOG') as log:
+            self._test_run_dispatch_helper(msg, handler)
+            log.assert_has_calls([mock.call.debug(mock.ANY)])
+
+    def test_run_dispatch_handler_raises(self):
+        class SimpleHandler:
+            def network_delete_end(self, payload):
+                raise Exception('foo')
+
+        msg = dict(event_type='network.delete.end',
+                   payload=dict(network_id='a'))
+
+        handler = SimpleHandler()
+
+        with mock.patch('quantum.agent.rpc.LOG') as log:
+            self._test_run_dispatch_helper(msg, handler)
+            log.assert_has_calls([mock.call.warn(mock.ANY)])
index 52c4f3fe5baa140f5b9e022b0bef13eac99289f6..b383168f4dd825c9f8e9827d9f2eaa2586b7f9b9 100644 (file)
@@ -31,6 +31,7 @@ from quantum.common import exceptions as q_exc
 from quantum.common.test_lib import test_config
 from quantum import context
 from quantum.db import api as db
+from quantum.db import db_base_plugin_v2
 from quantum.manager import QuantumManager
 from quantum.openstack.common import cfg
 from quantum.tests.unit import test_extensions
diff --git a/quantum/tests/unit/test_db_rpc_base.py b/quantum/tests/unit/test_db_rpc_base.py
new file mode 100644 (file)
index 0000000..279079f
--- /dev/null
@@ -0,0 +1,165 @@
+# Copyright (c) 2012 OpenStack, LLC.
+#
+# 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.db import dhcp_rpc_base
+
+
+class TestDhcpAugmentContext(unittest.TestCase):
+    def test_augment_context(self):
+        context = mock.Mock()
+        context.user = 'quantum'
+        context.tenant = None
+        context.is_admin = True
+
+        new_context = dhcp_rpc_base.augment_context(context)
+
+        self.assertEqual(new_context.user_id, context.user)
+        self.assertEqual(new_context.roles, ['admin'])
+
+
+class TestDhcpRpcCallackMixin(unittest.TestCase):
+    def setUp(self):
+        self.context_p = mock.patch('quantum.db.dhcp_rpc_base.augment_context')
+        self.context_p.start()
+
+        self.plugin_p = mock.patch('quantum.manager.QuantumManager.get_plugin')
+        get_plugin = self.plugin_p.start()
+        self.plugin = mock.Mock()
+        get_plugin.return_value = self.plugin
+        self.callbacks = dhcp_rpc_base.DhcpRpcCallbackMixin()
+        self.log_p = mock.patch('quantum.db.dhcp_rpc_base.LOG')
+        self.log = self.log_p.start()
+
+    def tearDown(self):
+        self.log_p.stop()
+        self.plugin_p.stop()
+        self.context_p.stop()
+
+    def test_get_active_networks(self):
+        plugin_retval = [dict(id='a'), dict(id='b')]
+        self.plugin.get_networks.return_value = plugin_retval
+
+        networks = self.callbacks.get_active_networks(mock.Mock(), host='host')
+
+        self.assertEqual(networks, ['a', 'b'])
+        self.plugin.assert_has_calls(
+            [mock.call.get_networks(mock.ANY,
+                                    filters=dict(admin_state_up=[True]))])
+
+        self.assertEqual(len(self.log.mock_calls), 1)
+
+    def test_get_network_info(self):
+        network_retval = dict(id='a')
+
+        subnet_retval = mock.Mock()
+        port_retval = mock.Mock()
+
+        self.plugin.get_network.return_value = network_retval
+        self.plugin.get_subnets.return_value = subnet_retval
+        self.plugin.get_ports.return_value = port_retval
+
+        retval = self.callbacks.get_network_info(mock.Mock(), network_id='a')
+        self.assertEquals(retval, network_retval)
+        self.assertEqual(retval['subnets'], subnet_retval)
+        self.assertEqual(retval['ports'], port_retval)
+
+    def _test_get_dhcp_port_helper(self, port_retval, other_expectations=[],
+                                   update_port=None, create_port=None):
+        subnets_retval = [dict(id='a', enable_dhcp=True),
+                          dict(id='b', enable_dhcp=False)]
+
+        self.plugin.get_subnets.return_value = subnets_retval
+        if port_retval:
+            self.plugin.get_ports.return_value = [port_retval]
+        else:
+            self.plugin.get_ports.return_value = []
+        self.plugin.update_port.return_value = update_port
+        self.plugin.create_port.return_value = create_port
+
+        retval = self.callbacks.get_dhcp_port(mock.Mock(),
+                                              network_id='netid',
+                                              device_id='devid',
+                                              host='host')
+
+        expected = [mock.call.get_subnets(mock.ANY,
+                                          filters=dict(network_id=['netid'])),
+                    mock.call.get_ports(mock.ANY,
+                                        filters=dict(network_id=['netid'],
+                                                     device_id=['devid']))]
+
+        expected.extend(other_expectations)
+        self.plugin.assert_has_calls(expected)
+        return retval
+
+    def test_get_dhcp_port_existing(self):
+        port_retval = dict(id='port_id', fixed_ips=[dict(subnet_id='a')])
+        expectations = [
+            mock.call.update_port(mock.ANY, 'port_id', dict(port=port_retval))]
+
+        retval = self._test_get_dhcp_port_helper(port_retval, expectations,
+                                                 update_port=port_retval)
+        self.assertEqual(len(self.log.mock_calls), 1)
+
+    def test_get_dhcp_port_create_new(self):
+        self.plugin.get_network.return_value = dict(tenant_id='tenantid')
+        create_spec = dict(tenant_id='tenantid', device_id='devid',
+                           network_id='netid', name='DHCP Agent',
+                           admin_state_up=True,
+                           device_owner='network:dhcp',
+                           mac_address=mock.ANY)
+        create_retval = create_spec.copy()
+        create_retval['id'] = 'port_id'
+        create_retval['fixed_ips'] = [dict(subnet_id='a', enable_dhcp=True)]
+
+        create_spec['fixed_ips'] = [dict(subnet_id='a')]
+
+        expectations = [
+            mock.call.get_network(mock.ANY, 'netid'),
+            mock.call.create_port(mock.ANY, dict(port=create_spec))]
+
+        retval = self._test_get_dhcp_port_helper(None, expectations,
+                                                 create_port=create_retval)
+        self.assertEqual(create_retval, retval)
+        self.assertEqual(len(self.log.mock_calls), 2)
+
+    def test_release_dhcp_port(self):
+        port_retval = dict(id='port_id', fixed_ips=[dict(subnet_id='a')])
+        self.plugin.get_ports.return_value = [port_retval]
+
+        self.callbacks.release_dhcp_port(mock.ANY, network_id='netid',
+                                         device_id='devid')
+
+        self.plugin.assert_has_calls([
+            mock.call.get_ports(mock.ANY, filters=dict(network_id=['netid'],
+                                                       device_id=['devid'])),
+            mock.call.delete_port(mock.ANY, 'port_id')])
+
+    def test_release_port_fixed_ip(self):
+        port_retval = dict(id='port_id', fixed_ips=[dict(subnet_id='a')])
+        port_update = dict(id='port_id', fixed_ips=[])
+        self.plugin.get_ports.return_value = [port_retval]
+
+        self.callbacks.release_port_fixed_ip(mock.ANY, network_id='netid',
+                                             device_id='devid', subnet_id='a')
+
+        self.plugin.assert_has_calls([
+            mock.call.get_ports(mock.ANY, filters=dict(network_id=['netid'],
+                                                       device_id=['devid'])),
+            mock.call.update_port(mock.ANY, 'port_id',
+                                  dict(port=port_update))])
index d42b9373037bd1d340e10998a3d54a963e6a5e1f..a88b23d52b46f81d47a3bff180624f5dc2fcf03d 100644 (file)
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
-import unittest
+import uuid
 
 import mock
-from sqlalchemy.ext import sqlsoup
+import unittest2 as unittest
 
 from quantum.agent import dhcp_agent
 from quantum.agent.common import config
 from quantum.agent.linux import interface
+from quantum.common import exceptions
+from quantum.openstack.common import cfg
 
 
 class FakeModel:
@@ -34,36 +36,52 @@ class FakeModel:
         return str(self.__dict__)
 
 
-class FakePortModel(FakeModel):
-    fixed_ips = []
+fake_subnet1 = FakeModel('bbbbbbbb-bbbb-bbbb-bbbbbbbbbbbb',
+                         network_id='12345678-1234-5678-1234567890ab',
+                         cidr='172.9.9.0/24', enable_dhcp=True)
 
+fake_subnet2 = FakeModel('dddddddd-dddd-dddd-dddddddddddd',
+                         network_id='12345678-1234-5678-1234567890ab',
+                         enable_dhcp=False)
 
-class FakeFixedIPModel(object):
+fake_fixed_ip = FakeModel('', subnet=fake_subnet1, ip_address='172.9.9.9')
 
-    def __init__(self, ip_address, cidr):
-        self.subnet = FakeSubnetModel(cidr)
-        self.ip_address = ip_address
+fake_port1 = FakeModel('12345678-1234-aaaa-1234567890ab',
+                       mac_address='aa:bb:cc:dd:ee:ff',
+                       network_id='12345678-1234-5678-1234567890ab',
+                       fixed_ips=[fake_fixed_ip])
 
+fake_port2 = FakeModel('12345678-1234-aaaa-123456789000',
+                       mac_address='aa:bb:cc:dd:ee:99',
+                       network_id='12345678-1234-5678-1234567890ab')
 
-class FakeSubnetModel(object):
+fake_network = FakeModel('12345678-1234-5678-1234567890ab',
+                         tenant_id='aaaaaaaa-aaaa-aaaa-aaaaaaaaaaaa',
+                         admin_state_up=True,
+                         subnets=[fake_subnet1, fake_subnet2],
+                         ports=[fake_port1])
 
-    def __init__(self, cidr):
-        self.cidr = cidr
+fake_down_network = FakeModel('12345678-dddd-dddd-1234567890ab',
+                              tenant_id='aaaaaaaa-aaaa-aaaa-aaaaaaaaaaaa',
+                              admin_state_up=False,
+                              subnets=[],
+                              ports=[])
 
 
 class TestDhcpAgent(unittest.TestCase):
     def setUp(self):
-        self.conf = config.setup_conf()
-        self.conf.register_opts(dhcp_agent.DhcpAgent.OPTS)
+        cfg.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
+        self.notification_p = mock.patch(
+            'quantum.agent.rpc.NotificationDispatcher')
+        self.notification = self.notification_p.start()
 
     def tearDown(self):
+        self.notification_p.stop()
         self.driver_cls_p.stop()
 
     def test_dhcp_agent_main(self):
@@ -75,315 +93,371 @@ class TestDhcpAgent(unittest.TestCase):
                     dev_mgr.assert_called_once(mock.ANY, 'sudo')
                     dhcp.assert_has_calls([
                         mock.call(mock.ANY),
-                        mock.call().daemon_loop()])
-
-    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):
-        self.dhcp._network_dhcp_enable = mock.Mock(return_value=True)
-        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_network_admin_down(self):
-        fake_network1 = FakeModel(1, admin_state_up=True)
-        fake_network2 = FakeModel(2, admin_state_up=False)
-
-        fake_subnet1 = FakeModel(1, network_id=1, enable_dhcp=True)
-        fake_subnet2 = FakeModel(2, network_id=2, enable_dhcp=True)
-        fake_subnet3 = FakeModel(3, network_id=2, enable_dhcp=True)
-
-        fake_network1.subnets = [fake_subnet1]
-        fake_network2.subnets = [fake_subnet2, fake_subnet3]
-
-        fake_subnet1.network = fake_network1
-        fake_subnet2.network = fake_network2
-        fake_subnet3.network = fake_network2
-
-        fake_allocation = [
-            FakeModel(2, subnet_id=1),
-            FakeModel(3, subnet_id=2)
-        ]
-
-        fake_subnets = [fake_subnet1, fake_subnet2, fake_subnet3]
-        fake_networks = [fake_network1, fake_network2]
-
-        db = mock.Mock()
-        db.subnets.all = mock.Mock(return_value=fake_subnets)
-        db.networks.all = mock.Mock(return_value=fake_networks)
-        db.ipallocations.all = mock.Mock(return_value=fake_allocation)
-        self.dhcp.db = db
-        state = self.dhcp._state_builder()
-
-        self.assertEquals(state.networks, set([1]))
-
-        expected_subnets = set([
-            (hash(str(fake_subnets[0])), 1),
-        ])
-        self.assertEquals(state.subnet_hashes, expected_subnets)
-
-        expected_ipalloc = set([
-            (hash(str(fake_allocation[0])), 1),
-        ])
-        self.assertEquals(state.ipalloc_hashes, expected_ipalloc)
-
-    def test_state_builder_network_dhcp_partial_disable(self):
-        fake_network1 = FakeModel(1, admin_state_up=True)
-        fake_network2 = FakeModel(2, admin_state_up=True)
-
-        fake_subnet1 = FakeModel(1, network_id=1, enable_dhcp=True)
-        fake_subnet2 = FakeModel(2, network_id=2, enable_dhcp=False)
-        fake_subnet3 = FakeModel(3, network_id=2, enable_dhcp=True)
-
-        fake_network1.subnets = [fake_subnet1]
-        fake_network2.subnets = [fake_subnet2, fake_subnet3]
-
-        fake_subnet1.network = fake_network1
-        fake_subnet2.network = fake_network2
-        fake_subnet3.network = fake_network2
-
-        fake_allocation = [
-            FakeModel(2, subnet_id=1),
-            FakeModel(3, subnet_id=2),
-            FakeModel(4, subnet_id=3),
-        ]
-
-        fake_subnets = [fake_subnet1, fake_subnet2, fake_subnet3]
-        fake_networks = [fake_network1, fake_network2]
-
-        db = mock.Mock()
-        db.subnets.all = mock.Mock(return_value=fake_subnets)
-        db.networks.all = mock.Mock(return_value=fake_networks)
-        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_subnets[0])), 1),
-            (hash(str(fake_subnets[2])), 2),
-        ])
-        self.assertEquals(state.subnet_hashes, expected_subnets)
-
-        expected_ipalloc = set([
-            (hash(str(fake_allocation[0])), 1),
-            (hash(str(fake_allocation[2])), 2),
-        ])
-        self.assertEquals(state.ipalloc_hashes, expected_ipalloc)
-
-    def test_state_builder_network_dhcp_all_disable(self):
-        fake_network1 = FakeModel(1, admin_state_up=True)
-        fake_network2 = FakeModel(2, admin_state_up=True)
-
-        fake_subnet1 = FakeModel(1, network_id=1, enable_dhcp=True)
-        fake_subnet2 = FakeModel(2, network_id=2, enable_dhcp=False)
-        fake_subnet3 = FakeModel(3, network_id=2, enable_dhcp=False)
-
-        fake_network1.subnets = [fake_subnet1]
-        fake_network2.subnets = [fake_subnet2, fake_subnet3]
-
-        fake_subnet1.network = fake_network1
-        fake_subnet2.network = fake_network2
-        fake_subnet3.network = fake_network2
-
-        fake_allocation = [
-            FakeModel(2, subnet_id=1),
-            FakeModel(3, subnet_id=2),
-            FakeModel(4, subnet_id=3),
-        ]
-
-        fake_subnets = [fake_subnet1, fake_subnet2, fake_subnet3]
-        fake_networks = [fake_network1, fake_network2]
-
-        db = mock.Mock()
-        db.subnets.all = mock.Mock(return_value=fake_subnets)
-        db.networks.all = mock.Mock(return_value=fake_networks)
-        db.ipallocations.all = mock.Mock(return_value=fake_allocation)
-        self.dhcp.db = db
-        state = self.dhcp._state_builder()
-
-        self.assertEquals(state.networks, set([1]))
-
-        expected_subnets = set([
-            (hash(str(fake_subnets[0])), 1)
-        ])
-        self.assertEquals(state.subnet_hashes, expected_subnets)
-
-        expected_ipalloc = set([
-            (hash(str(fake_allocation[0])), 1)
-        ])
-        self.assertEquals(state.ipalloc_hashes, expected_ipalloc)
-
-    def test_state_builder_mixed(self):
-        fake_network1 = FakeModel(1, admin_state_up=True)
-        fake_network2 = FakeModel(2, admin_state_up=True)
-        fake_network3 = FakeModel(3, admin_state_up=False)
-
-        fake_subnet1 = FakeModel(1, network_id=1, enable_dhcp=True)
-        fake_subnet2 = FakeModel(2, network_id=2, enable_dhcp=False)
-        fake_subnet3 = FakeModel(3, network_id=3, enable_dhcp=True)
-
-        fake_network1.subnets = [fake_subnet1]
-        fake_network2.subnets = [fake_subnet2]
-        fake_network3.subnets = [fake_subnet3]
-
-        fake_subnet1.network = fake_network1
-        fake_subnet2.network = fake_network2
-        fake_subnet3.network = fake_network3
-
-        fake_allocation = [
-            FakeModel(2, subnet_id=1)
-        ]
-
-        fake_subnets = [fake_subnet1, fake_subnet2, fake_subnet3]
-        fake_networks = [fake_network1, fake_network2, fake_network3]
-
-        db = mock.Mock()
-        db.subnets.all = mock.Mock(return_value=fake_subnets)
-        db.networks.all = mock.Mock(return_value=fake_networks)
-        db.ipallocations.all = mock.Mock(return_value=fake_allocation)
-        self.dhcp.db = db
-        state = self.dhcp._state_builder()
-
-        self.assertEquals(state.networks, set([1]))
-
-        expected_subnets = set([
-            (hash(str(fake_subnets[0])), 1),
-        ])
-        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])))
+                        mock.call().run()])
+
+    def test_run_completes_single_pass(self):
+        with mock.patch('quantum.agent.dhcp_agent.DeviceManager') as dev_mgr:
+            with mock.patch('quantum.agent.dhcp_agent.DhcpPluginApi') as plug:
+                mock_plugin = mock.Mock()
+                mock_plugin.get_active_networks.return_value = ['a']
+                plug.return_value = mock_plugin
+
+                dhcp = dhcp_agent.DhcpAgent(cfg.CONF)
+                with mock.patch.object(dhcp, 'enable_dhcp_helper') as enable:
+                    dhcp.run()
+                    enable.assert_called_once_with('a')
+                    plug.assert_called_once_with('q-plugin', mock.ANY)
+                    mock_plugin.assert_has_calls(
+                        [mock.call.get_active_networks()])
+
+        self.notification.assert_has_calls([mock.call.run_dispatch()])
 
     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)
+        with mock.patch('quantum.agent.dhcp_agent.DeviceManager') as dev_mgr:
+            dhcp = dhcp_agent.DhcpAgent(cfg.CONF)
+            dhcp.call_driver('foo', '1')
+            dev_mgr.assert_called()
+            self.driver.assert_called_once_with(cfg.CONF,
+                                                mock.ANY,
+                                                'sudo',
+                                                mock.ANY)
+
+
+class TestDhcpAgentEventHandler(unittest.TestCase):
+    def setUp(self):
+        cfg.CONF.register_opts(dhcp_agent.DeviceManager.OPTS)
+        cfg.CONF.set_override('interface_driver',
+                              'quantum.agent.linux.interface.NullDriver')
+        cfg.CONF.root_helper = 'sudo'
+        cfg.CONF.register_opts(dhcp_agent.DhcpAgent.OPTS)
+        self.notification_p = mock.patch(
+            'quantum.agent.rpc.NotificationDispatcher')
+        self.notification = self.notification_p.start()
+
+        self.plugin_p = mock.patch('quantum.agent.dhcp_agent.DhcpPluginApi')
+        plugin_cls = self.plugin_p.start()
+        self.plugin = mock.Mock()
+        plugin_cls.return_value = self.plugin
+
+        self.cache_p = mock.patch('quantum.agent.dhcp_agent.NetworkCache')
+        cache_cls = self.cache_p.start()
+        self.cache = mock.Mock()
+        cache_cls.return_value = self.cache
+
+        self.dhcp = dhcp_agent.DhcpAgent(cfg.CONF)
+        self.call_driver_p = mock.patch.object(self.dhcp, 'call_driver')
+
+        self.call_driver = self.call_driver_p.start()
+
+    def tearDown(self):
+        self.call_driver_p.stop()
+        self.cache_p.stop()
+        self.plugin_p.stop()
+        self.notification_p.stop()
+
+    def test_enable_dhcp_helper(self):
+        self.plugin.get_network_info.return_value = fake_network
+        self.dhcp.enable_dhcp_helper(fake_network.id)
+        self.plugin.assert_has_calls(
+            [mock.call.get_network_info(fake_network.id)])
+        self.call_driver.assert_called_once_with('enable', fake_network)
+
+    def test_enable_dhcp_helper_down_network(self):
+        self.plugin.get_network_info.return_value = fake_down_network
+        self.dhcp.enable_dhcp_helper(fake_down_network.id)
+        self.plugin.assert_has_calls(
+            [mock.call.get_network_info(fake_down_network.id)])
+        self.assertFalse(self.call_driver.called)
+
+    def test_disable_dhcp_helper_known_network(self):
+        self.cache.get_network_by_id.return_value = fake_network
+        self.dhcp.disable_dhcp_helper(fake_network.id)
+        self.cache.assert_has_calls(
+            [mock.call.get_network_by_id(fake_network.id)])
+        self.call_driver.assert_called_once_with('disable', fake_network)
+
+    def test_disable_dhcp_helper_unknown_network(self):
+        self.cache.get_network_by_id.return_value = None
+        self.dhcp.disable_dhcp_helper('abcdef')
+        self.cache.assert_has_calls(
+            [mock.call.get_network_by_id('abcdef')])
+        self.assertEqual(self.call_driver.call_count, 0)
+
+    def test_network_create_end(self):
+        payload = dict(network=dict(id=fake_network.id))
+
+        with mock.patch.object(self.dhcp, 'enable_dhcp_helper') as enable:
+            self.dhcp.network_create_end(payload)
+            enable.assertCalledOnceWith(fake_network.id)
+
+    def test_network_update_end_admin_state_up(self):
+        payload = dict(network=dict(id=fake_network.id, admin_state_up=True))
+        with mock.patch.object(self.dhcp, 'enable_dhcp_helper') as enable:
+            self.dhcp.network_update_end(payload)
+            enable.assertCalledOnceWith(fake_network.id)
+
+    def test_network_update_end_admin_state_down(self):
+        payload = dict(network=dict(id=fake_network.id, admin_state_up=False))
+        with mock.patch.object(self.dhcp, 'disable_dhcp_helper') as disable:
+            self.dhcp.network_update_end(payload)
+            disable.assertCalledOnceWith(fake_network.id)
+
+    def test_network_delete_start(self):
+        payload = dict(network_id=fake_network.id)
+
+        with mock.patch.object(self.dhcp, 'disable_dhcp_helper') as disable:
+            self.dhcp.network_delete_start(payload)
+            disable.assertCalledOnceWith(fake_network.id)
+
+    def test_subnet_delete_start(self):
+        payload = dict(subnet_id=fake_subnet1.id)
+        self.cache.get_network_by_subnet_id.return_value = fake_network
+
+        self.dhcp.subnet_delete_start(payload)
+
+        self.cache.assert_has_calls(
+            [mock.call.get_network_by_subnet_id(fake_subnet1.id)])
+
+        self.plugin.assert_has_calls(
+            [mock.call.release_port_fixed_ip(fake_network.id,
+                                             mock.ANY,
+                                             fake_subnet1.id)])
+        self.assertEqual(self.call_driver.call_count, 0)
+
+    def test_refresh_dhcp_helper_no_dhcp_enabled_networks(self):
+        network = FakeModel('12345678-1234-5678-1234567890ab',
+                            tenant_id='aaaaaaaa-aaaa-aaaa-aaaaaaaaaaaa',
+                            admin_state_up=True,
+                            subnets=[],
+                            ports=[])
+
+        self.plugin.get_network_info.return_value = network
+        with mock.patch.object(self.dhcp, 'disable_dhcp_helper') as disable:
+            self.dhcp.refresh_dhcp_helper(network.id)
+            disable.called_once_with_args(network.id)
+            self.assertFalse(self.cache.called)
+            self.assertFalse(self.call_driver.called)
+
+    def test_subnet_update_end(self):
+        payload = dict(subnet=dict(network_id=fake_network.id))
+        self.cache.get_network_by_id.return_value = fake_network
+        self.plugin.get_network_info.return_value = fake_network
+
+        self.dhcp.subnet_update_end(payload)
+
+        self.cache.assert_has_calls([mock.call.put(fake_network)])
+        self.call_driver.assert_called_once_with('update_l3', fake_network)
+
+    def test_subnet_update_end_delete_payload(self):
+        payload = dict(subnet_id=fake_subnet1.id)
+        self.cache.get_network_by_subnet_id.return_value = fake_network
+        self.plugin.get_network_info.return_value = fake_network
+
+        self.dhcp.subnet_delete_end(payload)
+
+        self.cache.assert_has_calls([mock.call.put(fake_network)])
+        self.call_driver.assert_called_once_with('update_l3', fake_network)
+
+    def test_port_update_end(self):
+        payload = dict(port=vars(fake_port2))
+        self.cache.get_network_by_id.return_value = fake_network
+        self.dhcp.port_update_end(payload)
+        self.cache.assert_has_calls(
+            [mock.call.get_network_by_id(fake_port2.network_id),
+             mock.call.put_port(mock.ANY)])
+        self.call_driver.assert_called_once_with('reload_allocations',
+                                                 fake_network)
+
+    def test_port_delete_end(self):
+        payload = dict(port_id=fake_port2.id)
+        self.cache.get_network_by_id.return_value = fake_network
+        self.cache.get_port_by_id.return_value = fake_port2
+
+        self.dhcp.port_delete_end(payload)
+
+        self.cache.assert_has_calls(
+            [mock.call.get_port_by_id(fake_port2.id),
+             mock.call.get_network_by_id(fake_network.id),
+             mock.call.remove_port(fake_port2)])
+        self.call_driver.assert_called_once_with('reload_allocations',
+                                                 fake_network)
+
+    def test_port_delete_end_unknown_port(self):
+        payload = dict(port_id='unknown')
+        self.cache.get_port_by_id.return_value = None
+
+        self.dhcp.port_delete_end(payload)
+
+        self.cache.assert_has_calls([mock.call.get_port_by_id('unknown')])
+        self.assertEqual(self.call_driver.call_count, 0)
+
+
+class TestDhcpPluginApiProxy(unittest.TestCase):
+    def setUp(self):
+        self.proxy = dhcp_agent.DhcpPluginApi('foo', {})
+        self.proxy.host = 'foo'
+
+        self.call_p = mock.patch.object(self.proxy, 'call')
+        self.call = self.call_p.start()
+        self.make_msg_p = mock.patch.object(self.proxy, 'make_msg')
+        self.make_msg = self.make_msg_p.start()
+
+    def tearDown(self):
+        self.make_msg_p.stop()
+        self.call_p.stop()
+
+    def test_get_active_networks(self):
+        self.proxy.get_active_networks()
+        self.call.assert_called()
+        self.make_msg.assert_called_once_with('get_active_networks',
+                                              host='foo')
+
+    def test_get_network_info(self):
+        self.call.return_value = dict(a=1)
+        retval = self.proxy.get_network_info('netid')
+        self.assertEqual(retval.a, 1)
+        self.call.assert_called()
+        self.make_msg.assert_called_once_with('get_network_info',
+                                              network_id='netid',
+                                              host='foo')
+
+    def test_get_dhcp_port(self):
+        self.call.return_value = dict(a=1)
+        retval = self.proxy.get_dhcp_port('netid', 'devid')
+        self.assertEqual(retval.a, 1)
+        self.call.assert_called()
+        self.make_msg.assert_called_once_with('get_dhcp_port',
+                                              network_id='netid',
+                                              device_id='devid',
+                                              host='foo')
+
+    def test_release_dhcp_port(self):
+        self.proxy.release_dhcp_port('netid', 'devid')
+        self.call.assert_called()
+        self.make_msg.assert_called_once_with('release_dhcp_port',
+                                              network_id='netid',
+                                              device_id='devid',
+                                              host='foo')
+
+    def test_release_port_fixed_ip(self):
+        self.proxy.release_port_fixed_ip('netid', 'devid', 'subid')
+        self.call.assert_called()
+        self.make_msg.assert_called_once_with('release_port_fixed_ip',
+                                              network_id='netid',
+                                              subnet_id='subid',
+                                              device_id='devid',
+                                              host='foo')
+
+
+class TestNetworkCache(unittest.TestCase):
+    def test_put_network(self):
+        nc = dhcp_agent.NetworkCache()
+        nc.put(fake_network)
+        self.assertEqual(nc.cache,
+                         {fake_network.id: fake_network})
+        self.assertEqual(nc.subnet_lookup,
+                         {fake_subnet1.id: fake_network.id,
+                          fake_subnet2.id: fake_network.id})
+        self.assertEqual(nc.port_lookup,
+                         {fake_port1.id: fake_network.id})
+
+    def test_put_network_existing(self):
+        prev_network_info = mock.Mock()
+        nc = dhcp_agent.NetworkCache()
+        with mock.patch.object(nc, 'remove') as remove:
+            nc.cache[fake_network.id] = prev_network_info
+
+            nc.put(fake_network)
+            remove.assert_called_once_with(prev_network_info)
+        self.assertEqual(nc.cache,
+                         {fake_network.id: fake_network})
+        self.assertEqual(nc.subnet_lookup,
+                         {fake_subnet1.id: fake_network.id,
+                          fake_subnet2.id: fake_network.id})
+        self.assertEqual(nc.port_lookup,
+                         {fake_port1.id: fake_network.id})
+
+    def test_remove_network(self):
+        nc = dhcp_agent.NetworkCache()
+        nc.cache = {fake_network.id: fake_network}
+        nc.subnet_lookup = {fake_subnet1.id: fake_network.id,
+                            fake_subnet2.id: fake_network.id}
+        nc.port_lookup = {fake_port1.id: fake_network.id}
+        nc.remove(fake_network)
+
+        self.assertEqual(len(nc.cache), 0)
+        self.assertEqual(len(nc.subnet_lookup), 0)
+        self.assertEqual(len(nc.port_lookup), 0)
+
+    def test_get_network_by_id(self):
+        nc = dhcp_agent.NetworkCache()
+        nc.put(fake_network)
+
+        self.assertEqual(nc.get_network_by_id(fake_network.id), fake_network)
+
+    def test_get_network_by_subnet_id(self):
+        nc = dhcp_agent.NetworkCache()
+        nc.put(fake_network)
+
+        self.assertEqual(nc.get_network_by_subnet_id(fake_subnet1.id),
+                         fake_network)
+
+    def test_get_network_by_port_id(self):
+        nc = dhcp_agent.NetworkCache()
+        nc.put(fake_network)
+
+        self.assertEqual(nc.get_network_by_port_id(fake_port1.id),
+                         fake_network)
+
+    def test_put_port(self):
+        fake_network = FakeModel('12345678-1234-5678-1234567890ab',
+                                 tenant_id='aaaaaaaa-aaaa-aaaa-aaaaaaaaaaaa',
+                                 subnets=[fake_subnet1],
+                                 ports=[fake_port1])
+        nc = dhcp_agent.NetworkCache()
+        nc.put(fake_network)
+        nc.put_port(fake_port2)
+        self.assertEqual(len(nc.port_lookup), 2)
+        self.assertIn(fake_port2, fake_network.ports)
+
+    def test_put_port_existing(self):
+        fake_network = FakeModel('12345678-1234-5678-1234567890ab',
+                                 tenant_id='aaaaaaaa-aaaa-aaaa-aaaaaaaaaaaa',
+                                 subnets=[fake_subnet1],
+                                 ports=[fake_port1, fake_port2])
+        nc = dhcp_agent.NetworkCache()
+        nc.put(fake_network)
+        nc.put_port(fake_port2)
+
+        self.assertEqual(len(nc.port_lookup), 2)
+        self.assertIn(fake_port2, fake_network.ports)
+
+    def test_remove_port_existing(self):
+        fake_network = FakeModel('12345678-1234-5678-1234567890ab',
+                                 tenant_id='aaaaaaaa-aaaa-aaaa-aaaaaaaaaaaa',
+                                 subnets=[fake_subnet1],
+                                 ports=[fake_port1, fake_port2])
+
+        nc = dhcp_agent.NetworkCache()
+        nc.put(fake_network)
+        nc.remove_port(fake_port2)
+
+        self.assertEqual(len(nc.port_lookup), 1)
+        self.assertNotIn(fake_port2, fake_network.ports)
+
+    def test_get_port_by_id(self):
+        nc = dhcp_agent.NetworkCache()
+        nc.put(fake_network)
+        self.assertEqual(nc.get_port_by_id(fake_port1.id), fake_port1)
 
 
 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.conf.root_helper = 'sudo'
-        self.conf.use_namespaces = True
-
-        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
+        cfg.CONF.register_opts(dhcp_agent.DeviceManager.OPTS)
+        cfg.CONF.register_opts(dhcp_agent.DhcpAgent.OPTS)
+        cfg.CONF.set_override('interface_driver',
+                              'quantum.agent.linux.interface.NullDriver')
+        cfg.CONF.root_helper = 'sudo'
 
         self.device_exists_p = mock.patch(
             'quantum.agent.linux.ip_lib.device_exists')
@@ -399,124 +473,180 @@ class TestDeviceManager(unittest.TestCase):
     def tearDown(self):
         self.dvr_cls_p.stop()
         self.device_exists_p.stop()
-        self.client_cls_p.stop()
 
-    def test_setup(self):
-        port_id = '12345678-1234-aaaa-1234567890ab'
-        network_id = '12345678-1234-5678-1234567890ab'
-        fake_subnets = [FakeModel('12345678-aaaa-aaaa-1234567890ab'),
-                        FakeModel('12345678-bbbb-bbbb-1234567890ab')]
+    def _test_setup_helper(self, device_exists, reuse_existing=False):
+        plugin = mock.Mock()
+        plugin.get_dhcp_port.return_value = fake_port1
+        self.device_exists.return_value = device_exists
+        self.mock_driver.get_device_name.return_value = 'tap12345678-12'
 
-        fake_network = FakeModel(network_id,
-                                 tenant_id='aaaaaaaa-aaaa-aaaa-aaaaaaaaaaaa',
-                                 subnets=fake_subnets)
-
-        fake_port = FakePortModel(port_id, mac_address='aa:bb:cc:dd:ee:ff',
-                                  network_id=network_id,
-                                  allocations=[])
-        fake_port.fixed_ips.append(FakeFixedIPModel('172.9.9.9',
-                                                    '172.9.9.0/24'))
-        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()
-
-        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(cfg.CONF, plugin)
+        dh.setup(fake_network, reuse_existing)
 
-        self.mock_driver.get_device_name.return_value = 'tap12345678-12'
+        plugin.assert_has_calls([
+            mock.call.get_dhcp_port(fake_network.id, mock.ANY)])
 
-        dh = dhcp_agent.DeviceManager(self.conf, mock_db)
-        dh.setup(fake_network)
+        expected = [mock.call.init_l3('tap12345678-12',
+                                      ['172.9.9.9/24'],
+                                      namespace=fake_network.id)]
 
-        self.client_inst.assert_has_calls([
-            mock.call.create_port(mock.ANY)])
+        if not reuse_existing:
+            expected.insert(0,
+                            mock.call.plug(fake_network.id,
+                                           fake_port1.id,
+                                           'tap12345678-12',
+                                           'aa:bb:cc:dd:ee:ff',
+                                           namespace=fake_network.id))
 
-        self.mock_driver.assert_has_calls([
-            mock.call.get_device_name(mock.ANY),
-            mock.call.plug(network_id,
-                           port_id,
-                           'tap12345678-12',
-                           'aa:bb:cc:dd:ee:ff',
-                           namespace=network_id),
-            mock.call.init_l3('tap12345678-12', ['172.9.9.9/24'],
-                              namespace=network_id)]
-        )
+        self.mock_driver.assert_has_calls(expected)
+
+    def test_setup(self):
+        self._test_setup_helper(False)
+
+    def test_setup_device_exists(self):
+        with self.assertRaises(exceptions.PreexistingDeviceFailure):
+            self._test_setup_helper(True)
+
+    def test_setup_device_exists_reuse(self):
+        self._test_setup_helper(True, True)
 
     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')
 
+        fake_port = FakeModel('12345678-1234-aaaa-1234567890ab',
+                              mac_address='aa:bb:cc:dd:ee:ff')
+
+        expected_driver_calls = [mock.call(cfg.CONF),
+                                 mock.call().get_device_name(fake_network),
+                                 mock.call().unplug('tap12345678-12')]
+
+        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)
+            #mock_driver.port = fake_port
+            mock_driver.get_device_name.return_value = 'tap12345678-12'
+            dvr_cls.return_value = mock_driver
+
+            plugin = mock.Mock()
+            plugin.get_dhcp_port.return_value = fake_port
+
+            dh = dhcp_agent.DeviceManager(cfg.CONF, plugin)
+            dh.destroy(fake_network)
+
+            dvr_cls.assert_called_once_with(cfg.CONF)
+            mock_driver.assert_has_calls(
+                [mock.call.get_device_name(mock.ANY),
+                 mock.call.unplug('tap12345678-12',
+                                  namespace=fake_network.id)])
+            plugin.assert_has_calls(
+                [mock.call.get_dhcp_port(fake_network.id, mock.ANY),
+                 mock.call.release_dhcp_port(fake_network.id, mock.ANY)])
+
+    def test_update_l3(self):
+        fake_network = mock.Mock()
+
+        dh = dhcp_agent.DeviceManager(cfg.CONF, None)
+        with mock.patch.object(dh, 'setup') as setup:
+            dh.update_l3(fake_network)
+            setup.called_once_with(fake_network, True)
+
+    def test_get_interface_name(self):
         fake_network = FakeModel('12345678-1234-5678-1234567890ab',
-                                 tenant_id='aaaaaaaa-aaaa-aaaa-aaaaaaaaaaaa',
-                                 subnets=fake_subnets)
+                                 tenant_id='aaaaaaaa-aaaa-aaaa-aaaaaaaaaaaa')
 
         fake_port = FakeModel('12345678-1234-aaaa-1234567890ab',
                               mac_address='aa:bb:cc:dd:ee:ff')
 
-        port_dict = dict(mac_address='aa:bb:cc:dd:ee:ff', allocations=[], id=1)
+        expected_driver_calls = [mock.call(cfg.CONF),
+                                 mock.call().get_device_name(fake_network),
+                                 mock.call().unplug('tap12345678-12')]
+
+        with mock.patch('quantum.agent.linux.interface.NullDriver') as dvr_cls:
+            mock_driver = mock.MagicMock()
+            mock_driver.get_device_name.return_value = 'tap12345678-12'
+            dvr_cls.return_value = mock_driver
+
+            plugin = mock.Mock()
+            plugin.get_dhcp_port.return_value = fake_port
 
-        self.client_inst.create_port.return_value = dict(port=port_dict)
-        self.device_exists.return_value = False
+            dh = dhcp_agent.DeviceManager(cfg.CONF, plugin)
+            dh.get_interface_name(fake_network, fake_port)
 
-        # fake the db
-        filter_by_result = mock.Mock()
-        filter_by_result.one = mock.Mock(return_value=fake_port)
+            dvr_cls.assert_called_once_with(cfg.CONF)
+            mock_driver.assert_has_calls(
+                [mock.call.get_device_name(fake_port)])
+
+            self.assertEqual(len(plugin.mock_calls), 0)
 
-        self.filter_called = False
+    def test_get_interface_name_no_port_provided(self):
+        fake_network = FakeModel('12345678-1234-5678-1234567890ab',
+                                 tenant_id='aaaaaaaa-aaaa-aaaa-aaaaaaaaaaaa')
 
-        def get_filter_results(*args, **kwargs):
-            if self.filter_called:
-                return filter_by_result
-            else:
-                self.filter_called = True
-                raise sqlsoup.SQLAlchemyError()
+        fake_port = FakeModel('12345678-1234-aaaa-1234567890ab',
+                              mac_address='aa:bb:cc:dd:ee:ff')
 
-        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)
+        expected_driver_calls = [mock.call(cfg.CONF),
+                                 mock.call().get_device_name(fake_network),
+                                 mock.call().unplug('tap12345678-12')]
 
         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)
-            mock_driver.port = fake_port
             mock_driver.get_device_name.return_value = 'tap12345678-12'
             dvr_cls.return_value = mock_driver
 
-            dh = dhcp_agent.DeviceManager(self.conf, mock_db)
-            dh.destroy(fake_network)
+            plugin = mock.Mock()
+            plugin.get_dhcp_port.return_value = fake_port
+
+            dh = dhcp_agent.DeviceManager(cfg.CONF, plugin)
+            dh.get_interface_name(fake_network)
 
-            dvr_cls.assert_called_once_with(self.conf)
+            dvr_cls.assert_called_once_with(cfg.CONF)
             mock_driver.assert_has_calls(
-                [mock.call.get_device_name(mock.ANY),
-                 mock.call.unplug('tap12345678-12')])
+                [mock.call.get_device_name(fake_port)])
+
+            plugin.assert_has_calls(
+                [mock.call.get_dhcp_port(fake_network.id, mock.ANY)])
+
+    def test_get_device_id(self):
+        fake_network = FakeModel('12345678-1234-5678-1234567890ab',
+                                 tenant_id='aaaaaaaa-aaaa-aaaa-aaaaaaaaaaaa')
+        expected = ('dhcp1ae5f96c-c527-5079-82ea-371a01645457-12345678-1234-'
+                    '5678-1234567890ab')
+
+        with mock.patch('socket.gethostbyname') as get_host:
+            with mock.patch('uuid.uuid5') as uuid5:
+                uuid5.return_value = '1ae5f96c-c527-5079-82ea-371a01645457'
+                get_host.return_value = 'localhost'
+
+                dh = dhcp_agent.DeviceManager(cfg.CONF, None)
+                uuid5.called_once_with(uuid.NAMESPACE_DNS, 'localhost')
+                self.assertEqual(dh.get_device_id(fake_network), expected)
+
+
+class TestDictModel(unittest.TestCase):
+    def test_basic_dict(self):
+        d = dict(a=1, b=2)
+
+        m = dhcp_agent.DictModel(d)
+        self.assertEqual(m.a, 1)
+        self.assertEqual(m.b, 2)
+
+    def test_dict_has_sub_dict(self):
+        d = dict(a=dict(b=2))
+        m = dhcp_agent.DictModel(d)
+        self.assertEqual(m.a.b, 2)
+
+    def test_dict_contains_list(self):
+        d = dict(a=[1, 2])
+
+        m = dhcp_agent.DictModel(d)
+        self.assertEqual(m.a, [1, 2])
 
+    def test_dict_contains_list_of_dicts(self):
+        d = dict(a=[dict(b=2), dict(c=3)])
 
-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))
+        m = dhcp_agent.DictModel(d)
+        self.assertEqual(m.a[0].b, 2)
+        self.assertEqual(m.a[1].c, 3)
index effdfe8df65fbd458f9477616b41b5551c962725..8bda152cd177def020be77807882c189c4a099f5 100644 (file)
@@ -133,6 +133,9 @@ class TestDhcpBase(unittest.TestCase):
             def disable(self):
                 self.called.append('disable')
 
+            def update_l3(self):
+                pass
+
             def reload_allocations(self):
                 pass
 
@@ -285,6 +288,20 @@ class TestDhcpLocalProcess(TestBase):
                     'cccccccc-cccc-cccc-cccc-cccccccccccc', 'kill', '-9', 5]
         self.execute.assert_called_once_with(exp_args, root_helper='sudo')
 
+    def test_update_l3(self):
+        delegate = mock.Mock()
+        fake_net = FakeDualNetwork()
+        with mock.patch.object(LocalChild, 'active') as active:
+            active.__get__ = mock.Mock(return_value=False)
+            lp = LocalChild(self.conf,
+                            fake_net,
+                            device_delegate=delegate)
+            lp.update_l3()
+
+            delegate.assert_has_calls(
+                [mock.call.update_l3(fake_net)])
+            self.assertEqual(lp.called, ['reload'])
+
     def test_pid(self):
         with mock.patch('__builtin__.open') as mock_open:
             mock_open.return_value.__enter__ = lambda s: s
index 888b46c42b7fa093c187c467befd10062ba4a6e5..61dd2be1fd2f1957b260fe4d5a9d5073649a75a5 100644 (file)
@@ -54,6 +54,7 @@ class FakePort:
     fixed_ips = [FakeAllocation]
     device_id = 'cccccccc-cccc-cccc-cccc-cccccccccccc'
     network = FakeNetwork()
+    network_id = network.id
 
 
 class TestBase(unittest.TestCase):
@@ -252,7 +253,7 @@ class TestBridgeInterfaceDriver(TestBase):
             br.unplug('tap0')
             log.assert_called_once()
 
-        self.ip_dev.assert_has_calls([mock.call('tap0', 'sudo'),
+        self.ip_dev.assert_has_calls([mock.call('tap0', 'sudo', None),
                                       mock.call().link.delete()])