]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
Embrane LBaaS Driver
authorIvar Lazzaro <ivar@embrane.com>
Wed, 30 Oct 2013 02:05:20 +0000 (19:05 -0700)
committerIvar Lazzaro <ivar@embrane.com>
Wed, 12 Mar 2014 23:02:11 +0000 (16:02 -0700)
Implements blueprint embrane-lbaas-driver

This commit implements Embrane's driver for LBaaS,
which uses Embrane's heleos(tm) appliances to provide Load Balancing.

Change-Id: Ia76fbc8881d178cfe6df11a2cfe8e77d3f36094f

20 files changed:
etc/neutron.conf
etc/services.conf
neutron/db/migration/alembic_migrations/versions/33dd0a9fa487_embrane_lbaas_driver.py [new file with mode: 0644]
neutron/plugins/embrane/agent/dispatcher.py
neutron/plugins/embrane/common/contexts.py
neutron/plugins/embrane/common/exceptions.py
neutron/services/loadbalancer/drivers/embrane/README [new file with mode: 0644]
neutron/services/loadbalancer/drivers/embrane/__init__.py [new file with mode: 0644]
neutron/services/loadbalancer/drivers/embrane/agent/__init__.py [new file with mode: 0644]
neutron/services/loadbalancer/drivers/embrane/agent/dispatcher.py [new file with mode: 0644]
neutron/services/loadbalancer/drivers/embrane/agent/lb_operations.py [new file with mode: 0644]
neutron/services/loadbalancer/drivers/embrane/config.py [new file with mode: 0644]
neutron/services/loadbalancer/drivers/embrane/constants.py [new file with mode: 0644]
neutron/services/loadbalancer/drivers/embrane/db.py [new file with mode: 0644]
neutron/services/loadbalancer/drivers/embrane/driver.py [new file with mode: 0644]
neutron/services/loadbalancer/drivers/embrane/models.py [new file with mode: 0644]
neutron/services/loadbalancer/drivers/embrane/poller.py [new file with mode: 0644]
neutron/tests/unit/services/loadbalancer/drivers/embrane/__init__.py [new file with mode: 0644]
neutron/tests/unit/services/loadbalancer/drivers/embrane/test_embrane_defaults.py [new file with mode: 0644]
neutron/tests/unit/services/loadbalancer/drivers/embrane/test_plugin_driver.py [new file with mode: 0644]

index 1a3d98bfb7bcd5206c7ffbbefa50b308071482fd..f2e0e89a3b7479508ed5906595be98ae2dca2947 100644 (file)
@@ -339,7 +339,7 @@ notification_driver = neutron.openstack.common.notifier.rpc_notifier
 # quota_health_monitors = -1
 
 # Number of routers allowed per tenant. A negative value means unlimited.
-# quota_router = 10
+# quota_router = 10
 
 # Number of floating IPs allowed per tenant. A negative value means unlimited.
 # quota_floatingip = 50
@@ -427,3 +427,5 @@ service_provider=VPN:openswan:neutron.services.vpn.service_drivers.ipsec.IPsecVP
 # service_provider=LOADBALANCER:NetScaler:neutron.services.loadbalancer.drivers.netscaler.netscaler_driver.NetScalerPluginDriver
 # Uncomment the following line (and comment out the OpenSwan VPN line) to enable Cisco's VPN driver.
 # service_provider=VPN:cisco:neutron.services.vpn.service_drivers.cisco_ipsec.CiscoCsrIPsecVPNDriver:default
+# Uncomment the line below to use Embrane heleos as Load Balancer service provider.
+# service_provider=LOADBALANCER:Embrane:neutron.services.loadbalancer.drivers.embrane.driver.EmbraneLbaas:default
index 23ee9be8b546f0d6e8e55a285cf501531dfd19d5..3a4b538f1b560f2853ab3ce40798e2d3154dc600 100644 (file)
 #netscaler_ncc_uri = https://ncc_server.acme.org/ncc/v1/api
 #netscaler_ncc_username = admin
 #netscaler_ncc_password = secret
+
+[heleoslb]
+#esm_mgmt =
+#admin_username =
+#admin_password =
+#lb_image =
+#inband_id =
+#oob_id =
+#mgmt_id =
+#dummy_utif_id =
+#resource_pool_id =
+#async_requests =
+#lb_flavor = small
+#sync_interval = 60
diff --git a/neutron/db/migration/alembic_migrations/versions/33dd0a9fa487_embrane_lbaas_driver.py b/neutron/db/migration/alembic_migrations/versions/33dd0a9fa487_embrane_lbaas_driver.py
new file mode 100644 (file)
index 0000000..aadf02d
--- /dev/null
@@ -0,0 +1,61 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2014 OpenStack Foundation
+#
+#    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.
+#
+
+"""embrane_lbaas_driver
+
+Revision ID: 33dd0a9fa487
+Revises: 19180cf98af6
+Create Date: 2014-02-25 00:15:35.567111
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '33dd0a9fa487'
+down_revision = '19180cf98af6'
+
+# Change to ['*'] if this migration applies to all plugins
+
+migration_for_plugins = [
+    'neutron.services.loadbalancer.plugin.LoadBalancerPlugin'
+]
+
+from alembic import op
+import sqlalchemy as sa
+
+from neutron.db import migration
+
+
+def upgrade(active_plugins=None, options=None):
+    if not migration.should_run(active_plugins, migration_for_plugins):
+        return
+
+    op.create_table(
+        u'embrane_pool_port',
+        sa.Column(u'pool_id', sa.String(length=36), nullable=False),
+        sa.Column(u'port_id', sa.String(length=36), nullable=False),
+        sa.ForeignKeyConstraint(['pool_id'], [u'pools.id'],
+                                name=u'embrane_pool_port_ibfk_1'),
+        sa.ForeignKeyConstraint(['port_id'], [u'ports.id'],
+                                name=u'embrane_pool_port_ibfk_2'),
+        sa.PrimaryKeyConstraint(u'pool_id'))
+
+
+def downgrade(active_plugins=None, options=None):
+    if not migration.should_run(active_plugins, migration_for_plugins):
+        return
+
+    op.drop_table(u'embrane_pool_port')
index 31bc15511a0dee7130cc0a2135a70f9e7c2bdecc..121abe9ac17f93398545771acedcf51cb6cb4514 100644 (file)
@@ -40,7 +40,7 @@ class Dispatcher(object):
     def dispatch_l3(self, d_context, args=(), kwargs={}):
         item = d_context.item
         event = d_context.event
-        q_context = d_context.q_context
+        n_context = d_context.n_context
         chain = d_context.chain
 
         item_id = item["id"]
@@ -52,7 +52,7 @@ class Dispatcher(object):
                     self.sync_items[item_id] = (queue.Queue(),)
                     first_run = True
                 self.sync_items[item_id][0].put(
-                    ctx.OperationContext(event, q_context, item, chain, f,
+                    ctx.OperationContext(event, n_context, item, chain, f,
                                          args, kwargs))
                 t = None
                 if first_run:
@@ -93,7 +93,7 @@ class Dispatcher(object):
                 try:
                     dva_state = operation_context.function(
                         plugin._esm_api,
-                        operation_context.q_context.tenant_id,
+                        operation_context.n_context.tenant_id,
                         operation_context.item,
                         *operation_context.args,
                         **operation_context.kwargs)
@@ -122,12 +122,12 @@ class Dispatcher(object):
                     if transient_state:
                         if transient_state == p_con.Status.DELETED:
                             current_state = plugin._delete_router(
-                                operation_context.q_context,
+                                operation_context.n_context,
                                 operation_context.item["id"])
                         # Error state cannot be reverted
                         elif transient_state != p_con.Status.ERROR:
                             current_state = plugin._update_neutron_state(
-                                operation_context.q_context,
+                                operation_context.n_context,
                                 operation_context.item,
                                 transient_state)
             except Exception:
index fbbf0aff516defe1a1fb116902d626451e605fa5..f35a024276cabf2c80f0903435bbb998b7ff6f81 100644 (file)
@@ -23,7 +23,7 @@ class DispatcherContext(object):
     def __init__(self, event, item, neutron_context, chain=None):
         self.event = event
         self.item = item
-        self.q_context = neutron_context
+        self.n_context = neutron_context
         self.chain = chain
 
 
index d2e2c1fdd9cd0dad96d938a7c86a4af369715c5c..f7cfa7b24d5203e581beec5588f280d03ad18499 100644 (file)
@@ -22,3 +22,7 @@ from neutron.common import exceptions as neutron_exec
 
 class EmbranePluginException(neutron_exec.NeutronException):
     message = _("An unexpected error occurred:%(err_msg)s")
+
+
+class UnsupportedException(EmbranePluginException):
+    message = _("%(err_msg)s")
diff --git a/neutron/services/loadbalancer/drivers/embrane/README b/neutron/services/loadbalancer/drivers/embrane/README
new file mode 100644 (file)
index 0000000..22326b0
--- /dev/null
@@ -0,0 +1,9 @@
+Embrane LBaaS Driver
+
+This DRIVER interfaces OpenStack Neutron with Embrane's heleos platform,
+Load Balancing appliances for cloud environments.
+
+L2 connectivity is leveraged by one of the supported existing plugins.
+
+For more details on use, configuration and implementation please refer to:
+https://wiki.openstack.org/wiki/Neutron/LBaaS/EmbraneDriver
\ No newline at end of file
diff --git a/neutron/services/loadbalancer/drivers/embrane/__init__.py b/neutron/services/loadbalancer/drivers/embrane/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/neutron/services/loadbalancer/drivers/embrane/agent/__init__.py b/neutron/services/loadbalancer/drivers/embrane/agent/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/neutron/services/loadbalancer/drivers/embrane/agent/dispatcher.py b/neutron/services/loadbalancer/drivers/embrane/agent/dispatcher.py
new file mode 100644 (file)
index 0000000..7ba0005
--- /dev/null
@@ -0,0 +1,108 @@
+# Copyright 2014 Embrane, Inc.
+# 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.
+#
+# @author:  Ivar Lazzaro, Embrane, Inc. ivar@embrane.com
+
+from eventlet import greenthread
+from eventlet import queue
+from heleosapi import exceptions as h_exc
+
+from neutron.openstack.common import log as logging
+from neutron.plugins.embrane.common import contexts as ctx
+from neutron.services.loadbalancer.drivers.embrane.agent import lb_operations
+from neutron.services.loadbalancer.drivers.embrane import constants as econ
+
+LOG = logging.getLogger(__name__)
+
+
+class Dispatcher(object):
+    def __init__(self, driver, async=True):
+        self._async = async
+        self._driver = driver
+        self.sync_items = dict()
+        self.handlers = lb_operations.handlers
+
+    def dispatch_lb(self, d_context, *args, **kwargs):
+        item = d_context.item
+        event = d_context.event
+        n_context = d_context.n_context
+        chain = d_context.chain
+
+        item_id = item["id"]
+        if event in self.handlers:
+            for f in self.handlers[event]:
+                first_run = False
+                if item_id not in self.sync_items:
+                    self.sync_items[item_id] = [queue.Queue()]
+                    first_run = True
+                self.sync_items[item_id][0].put(
+                    ctx.OperationContext(event, n_context, item, chain, f,
+                                         args, kwargs))
+                if first_run:
+                    t = greenthread.spawn(self._consume_lb,
+                                          item_id,
+                                          self.sync_items[item_id][0],
+                                          self._driver,
+                                          self._async)
+                    self.sync_items[item_id].append(t)
+                if not self._async:
+                    t = self.sync_items[item_id][1]
+                    t.wait()
+
+    def _consume_lb(self, sync_item, sync_queue, driver, a_sync):
+        current_state = None
+        while True:
+            try:
+                if current_state == econ.DELETED:
+                    del self.sync_items[sync_item]
+                    return
+                try:
+                    operation_context = sync_queue.get(
+                        block=a_sync,
+                        timeout=econ.QUEUE_TIMEOUT)
+                except queue.Empty:
+                    del self.sync_items[sync_item]
+                    return
+
+                (operation_context.chain and
+                 operation_context.chain.execute_all())
+
+                transient_state = None
+                try:
+                    transient_state = operation_context.function(
+                        driver, operation_context.n_context,
+                        operation_context.item, *operation_context.args,
+                        **operation_context.kwargs)
+                except (h_exc.PendingDva, h_exc.DvaNotFound,
+                        h_exc.BrokenInterface, h_exc.DvaCreationFailed,
+                        h_exc.BrokenDva, h_exc.ConfigurationFailed) as ex:
+                    LOG.warning(econ.error_map[type(ex)], ex.message)
+                except h_exc.DvaDeleteFailed as ex:
+                    LOG.warning(econ.error_map[type(ex)], ex.message)
+                    transient_state = econ.DELETED
+                finally:
+                    # if the returned transient state is None, no operations
+                    # are required on the DVA status
+                    if transient_state == econ.DELETED:
+                        current_state = driver._delete_vip(
+                            operation_context.n_context,
+                            operation_context.item)
+                        # Error state cannot be reverted
+                    else:
+                        driver._update_vip_graph_state(
+                            operation_context.n_context,
+                            operation_context.item)
+            except Exception:
+                LOG.exception(_('Unhandled exception occurred'))
diff --git a/neutron/services/loadbalancer/drivers/embrane/agent/lb_operations.py b/neutron/services/loadbalancer/drivers/embrane/agent/lb_operations.py
new file mode 100644 (file)
index 0000000..15ac419
--- /dev/null
@@ -0,0 +1,179 @@
+# Copyright 2014 Embrane, Inc.
+# 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.
+#
+# @author:  Ivar Lazzaro, Embrane, Inc. ivar@embrane.com
+
+from functools import wraps
+
+from heleosapi import exceptions as h_exc
+
+from neutron.openstack.common import log as logging
+from neutron.services.loadbalancer import constants as lcon
+from neutron.services.loadbalancer.drivers.embrane import constants as econ
+
+LOG = logging.getLogger(__name__)
+handlers = {}
+
+
+def handler(event, handler):
+    def wrap(f):
+        if event not in handler.keys():
+            handler[event] = [f]
+        else:
+            handler[event].append(f)
+
+        @wraps(f)
+        def wrapped_f(*args, **kwargs):
+            return f(*args, **kwargs)
+
+        return wrapped_f
+
+    return wrap
+
+
+@handler(econ.Events.CREATE_VIP, handlers)
+def _provision_load_balancer(driver, context, vip, flavor,
+                             vip_utif_info, vip_ip_allocation_info,
+                             pool_utif_info=None,
+                             pool_ip_allocation_info=None,
+                             pool=None, members=None,
+                             monitors=None):
+    api = driver._heleos_api
+    tenant_id = context.tenant_id
+    admin_state = vip["admin_state_up"]
+    # Architectural configuration
+    api.create_load_balancer(tenant_id=tenant_id,
+                             router_id=vip["id"],
+                             name=vip["name"],
+                             flavor=flavor,
+                             up=False)
+    api.grow_interface(vip_utif_info, False, tenant_id, vip["id"])
+    if pool:
+        api.grow_interface(pool_utif_info, False, tenant_id,
+                           vip["id"])
+
+    # Logical configuration
+    api.allocate_address(vip["id"], True, vip_ip_allocation_info)
+    if pool:
+        api.allocate_address(vip["id"], True, pool_ip_allocation_info)
+    dva = api.configure_load_balancer(vip["id"], admin_state,
+                                      vip, pool,
+                                      monitors, members)
+    return api.extract_dva_state(dva)
+
+
+@handler(econ.Events.UPDATE_VIP, handlers)
+def _update_load_balancer(driver, context, vip,
+                          old_pool_id=None, old_port_id=None,
+                          removed_ip=None, pool_utif_info=None,
+                          pool_ip_allocation_info=None,
+                          new_pool=None, members=None,
+                          monitors=None):
+    api = driver._heleos_api
+    tenant_id = context.tenant_id
+    admin_state = vip["admin_state_up"]
+
+    if old_pool_id:
+        # Architectural Changes
+        api.de_allocate_address(vip['id'], False, old_port_id, removed_ip)
+        api.shrink_interface(tenant_id, vip["id"], False, old_port_id)
+        api.grow_interface(pool_utif_info, False, tenant_id, vip["id"])
+        # Configuration Changes
+        api.allocate_address(vip["id"], True, pool_ip_allocation_info)
+        api.replace_pool(vip["id"], True, vip, old_pool_id,
+                         new_pool, monitors, members)
+
+    api.update_vservice(vip["id"], True, vip)
+    # Dva update
+    dva = api.update_dva(tenant_id, vip["id"], vip["name"],
+                         admin_state, description=vip["description"])
+
+    return api.extract_dva_state(dva)
+
+
+@handler(econ.Events.DELETE_VIP, handlers)
+def _delete_load_balancer(driver, context, vip):
+    try:
+        driver._heleos_api.delete_dva(context.tenant_id, vip['id'])
+    except h_exc.DvaNotFound:
+        LOG.warning(_('The load balancer %s had no physical representation, '
+                      'likely already deleted'), vip['id'])
+    return econ.DELETED
+
+
+@handler(econ.Events.UPDATE_POOL, handlers)
+def _update_server_pool(driver, context, vip, pool,
+                        monitors=None):
+    api = driver._heleos_api
+    cookie = ((vip.get('session_persistence') or {}).get('type') ==
+              lcon.SESSION_PERSISTENCE_HTTP_COOKIE)
+    return api.extract_dva_state(api.update_pool(vip['id'],
+                                                 vip['admin_state_up'],
+                                                 pool, cookie, monitors))
+
+
+@handler(econ.Events.ADD_OR_UPDATE_MEMBER, handlers)
+def _add_or_update_pool_member(driver, context, vip, member, protocol):
+    api = driver._heleos_api
+    return api.extract_dva_state(api.update_backend_server(
+        vip['id'], vip['admin_state_up'], member, protocol))
+
+
+@handler(econ.Events.REMOVE_MEMBER, handlers)
+def _remove_member_from_pool(driver, context, vip, member):
+    api = driver._heleos_api
+    return api.extract_dva_state(api.remove_pool_member(vip['id'],
+                                                        vip['admin_state_up'],
+                                                        member))
+
+
+@handler(econ.Events.DELETE_MEMBER, handlers)
+def _delete_member(driver, context, vip, member):
+    with context.session.begin(subtransactions=True):
+        api = driver._heleos_api
+        dva = api.delete_backend_server(vip['id'], vip['admin_state_up'],
+                                        member)
+        driver._delete_member(context, member)
+        return api.extract_dva_state(dva)
+
+
+@handler(econ.Events.ADD_POOL_HM, handlers)
+def _create_pool_hm(driver, context, vip, hm, pool_id):
+    api = driver._heleos_api
+    return api.extract_dva_state(api.add_pool_monitor(
+        vip['id'], vip['admin_state_up'], hm, pool_id))
+
+
+@handler(econ.Events.UPDATE_POOL_HM, handlers)
+def _update_pool_hm(driver, context, vip, hm, pool_id):
+    api = driver._heleos_api
+    return api.extract_dva_state(api.update_pool_monitor(
+        vip['id'], vip['admin_state_up'], hm, pool_id))
+
+
+@handler(econ.Events.DELETE_POOL_HM, handlers)
+def _delete_pool_hm(driver, context, vip, hm, pool_id):
+    with context.session.begin(subtransactions=True):
+        api = driver._heleos_api
+        dva = api.add_pool_monitor(vip['id'], vip['admin_state_up'],
+                                   hm, pool_id)
+        driver._delete_pool_hm(context, hm, pool_id)
+        return api.extract_dva_state(dva)
+
+
+@handler(econ.Events.POLL_GRAPH, handlers)
+def _poll_graph(driver, context, vip):
+    api = driver._heleos_api
+    return api.extract_dva_state(api.get_dva(vip['id']))
diff --git a/neutron/services/loadbalancer/drivers/embrane/config.py b/neutron/services/loadbalancer/drivers/embrane/config.py
new file mode 100644 (file)
index 0000000..8da2231
--- /dev/null
@@ -0,0 +1,53 @@
+# Copyright 2014 Embrane, Inc.
+# 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.
+#
+# @author:  Ivar Lazzaro, Embrane, Inc. ivar@embrane.com
+
+from oslo.config import cfg
+
+# User may want to use LB service together with the L3 plugin, but using
+# different resources. The service will inherit the configuration from the
+# L3 heleos plugin if present and not overridden.
+heleos_opts = [
+    cfg.StrOpt('esm_mgmt',
+               help=_('ESM management root address')),
+    cfg.StrOpt('admin_username', default=None,
+               help=_('ESM admin username.')),
+    cfg.StrOpt('admin_password', default=None,
+               secret=True,
+               help=_('ESM admin password.')),
+    cfg.StrOpt('lb_image', default=None,
+               help=_('Load Balancer image id (Embrane LB)')),
+    cfg.StrOpt('inband_id', default=None,
+               help=_('In band Security Zone id for LBs')),
+    cfg.StrOpt('oob_id', default=None,
+               help=_('Out of band Security Zone id for LBs')),
+    cfg.StrOpt('mgmt_id', default=None,
+               help=_('Management Security Zone id for LBs')),
+    cfg.StrOpt('dummy_utif_id', default=None,
+               help=_('Dummy user traffic Security Zone id for LBs')),
+    cfg.StrOpt('resource_pool_id', default=None,
+               help=_('Shared resource pool id')),
+    cfg.StrOpt('lb_flavor', default="small",
+               help=_('choose LB image flavor to use, accepted values: small, '
+                      'medium')),
+    cfg.IntOpt('sync_interval', default=60,
+               help=_('resource synchronization interval in seconds')),
+    cfg.BoolOpt('async_requests', default=None,
+                help=_('Define if the requests have '
+                       'run asynchronously or not')),
+]
+
+cfg.CONF.register_opts(heleos_opts, 'heleoslb')
diff --git a/neutron/services/loadbalancer/drivers/embrane/constants.py b/neutron/services/loadbalancer/drivers/embrane/constants.py
new file mode 100644 (file)
index 0000000..61e6095
--- /dev/null
@@ -0,0 +1,74 @@
+# Copyright 2014 Embrane, Inc.
+# 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.
+#
+# @author:  Ivar Lazzaro, Embrane, Inc. ivar@embrane.com
+
+from heleosapi import constants as h_con
+from heleosapi import exceptions as h_exc
+
+from neutron.plugins.common import constants as ccon
+
+DELETED = 'DELETED'  # not visible status
+QUEUE_TIMEOUT = 300
+BACK_SUB_LIMIT = 6
+
+
+class BackendActions:
+    UPDATE = 'update'
+    GROW = 'grow'
+    REMOVE = 'remove'
+    SHRINK = 'shrink'
+
+
+class Events:
+    CREATE_VIP = 'create_vip'
+    UPDATE_VIP = 'update_vip'
+    DELETE_VIP = 'delete_vip'
+    UPDATE_POOL = 'update_pool'
+    UPDATE_MEMBER = 'update_member'
+    ADD_OR_UPDATE_MEMBER = 'add_or_update_member'
+    REMOVE_MEMBER = 'remove_member'
+    DELETE_MEMBER = 'delete_member'
+    POLL_GRAPH = 'poll_graph'
+    ADD_POOL_HM = "create_pool_hm"
+    UPDATE_POOL_HM = "update_pool_hm"
+    DELETE_POOL_HM = "delete_pool_hm"
+
+
+_DVA_PENDING_ERROR_MSG = _('Dva is pending for the following reason: %s')
+_DVA_NOT_FOUNT_ERROR_MSG = _('%s, '
+                             'probably was cancelled through the heleos UI')
+_DVA_BROKEN_ERROR_MSG = _('Dva seems to be broken for reason %s')
+_DVA_CREATION_FAILED_ERROR_MSG = _('Dva creation failed reason %s')
+_DVA_CREATION_PENDING_ERROR_MSG = _('Dva creation is in pending state '
+                                    'for reason %s')
+_CFG_FAILED_ERROR_MSG = _('Dva configuration failed for reason %s')
+_DVA_DEL_FAILED_ERROR_MSG = _('Failed to delete the backend '
+                              'load balancer for reason %s. Please remove '
+                              'it manually through the heleos UI')
+NO_MEMBER_SUBNET_WARN = _('No subnet is associated to member %s (required '
+                          'to identify the proper load balancer port)')
+
+error_map = {h_exc.PendingDva: _DVA_PENDING_ERROR_MSG,
+             h_exc.DvaNotFound: _DVA_NOT_FOUNT_ERROR_MSG,
+             h_exc.BrokenDva: _DVA_BROKEN_ERROR_MSG,
+             h_exc.DvaCreationFailed: _DVA_CREATION_FAILED_ERROR_MSG,
+             h_exc.DvaCreationPending: _DVA_CREATION_PENDING_ERROR_MSG,
+             h_exc.ConfigurationFailed: _CFG_FAILED_ERROR_MSG,
+             h_exc.DvaDeleteFailed: _DVA_DEL_FAILED_ERROR_MSG}
+
+state_map = {h_con.DvaState.POWER_ON: ccon.ACTIVE,
+             None: ccon.ERROR,
+             DELETED: DELETED}
diff --git a/neutron/services/loadbalancer/drivers/embrane/db.py b/neutron/services/loadbalancer/drivers/embrane/db.py
new file mode 100644 (file)
index 0000000..005fbe0
--- /dev/null
@@ -0,0 +1,56 @@
+# Copyright 2014 Embrane, Inc.
+# 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.
+#
+# @author:  Ivar Lazzaro, Embrane, Inc. ivar@embrane.com
+
+import neutron.db.api as db
+from neutron.db import models_v2 as nmodel
+from neutron.services.loadbalancer.drivers.embrane import models
+
+
+def initialize():
+    db.configure_db()
+
+
+def add_pool_port(context, pool_id, port_id):
+    session = context.session
+    with session.begin(subtransactions=True):
+        pool_port = models.PoolPort()
+        pool_port.pool_id = pool_id
+        pool_port.port_id = port_id
+        session.add(pool_port)
+
+
+def get_pool_port(context, pool_id):
+    return (context.session.query(models.PoolPort).filter_by(pool_id=pool_id).
+            first())
+
+
+def delete_pool_backend(context, pool_id):
+    session = context.session
+    backend = (session.query(models.PoolPort).filter_by(
+        pool_id=pool_id))
+    for b in backend:
+        delete_pool_port(context, b)
+
+
+def delete_pool_port(context, backend_port):
+    session = context.session
+    with session.begin(subtransactions=True):
+        port = (session.query(nmodel.Port).filter_by(
+            id=backend_port['port_id'])).first()
+        if port:
+            session.delete(backend_port)
+            session.delete(port)
diff --git a/neutron/services/loadbalancer/drivers/embrane/driver.py b/neutron/services/loadbalancer/drivers/embrane/driver.py
new file mode 100644 (file)
index 0000000..83497b3
--- /dev/null
@@ -0,0 +1,342 @@
+# Copyright 2014 Embrane, Inc.
+# 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.
+#
+# @author:  Ivar Lazzaro, Embrane, Inc. ivar@embrane.com
+
+from heleosapi import backend_operations as h_op
+from heleosapi import constants as h_con
+from heleosapi import info as h_info
+from oslo.config import cfg
+
+from neutron.api.v2 import attributes
+from neutron.common import exceptions as n_exc
+from neutron.db.loadbalancer import loadbalancer_db as ldb
+from neutron.extensions import loadbalancer as lb_ext
+from neutron.openstack.common import log as logging
+from neutron.plugins.common import constants as pcon
+from neutron.plugins.embrane.common import contexts as embrane_ctx
+from neutron.plugins.embrane.common import exceptions as h_exc
+from neutron.plugins.embrane.common import utils
+from neutron.services.loadbalancer import constants as lbcon
+from neutron.services.loadbalancer.drivers import abstract_driver
+from neutron.services.loadbalancer.drivers.embrane.agent import dispatcher
+from neutron.services.loadbalancer.drivers.embrane import config  # noqa
+from neutron.services.loadbalancer.drivers.embrane import constants as econ
+from neutron.services.loadbalancer.drivers.embrane import db as edb
+from neutron.services.loadbalancer.drivers.embrane import poller
+
+LOG = logging.getLogger(__name__)
+conf = cfg.CONF.heleoslb
+confh = {}
+
+try:
+    confh = cfg.CONF.heleos
+except cfg.NoSuchOptError:
+    pass
+
+
+def get_conf(x):
+    try:
+        return conf.get(x) or confh.get(x)
+    except cfg.NoSuchOptError:
+        return
+
+
+class EmbraneLbaas(abstract_driver.LoadBalancerAbstractDriver):
+    def __init__(self, plugin):
+        edb.initialize()
+        config_esm_mgmt = get_conf('esm_mgmt')
+        config_admin_username = get_conf('admin_username')
+        config_admin_password = get_conf('admin_password')
+        config_lb_image_id = get_conf('lb_image')
+        config_security_zones = {h_con.SzType.IB: get_conf('inband_id'),
+                                 h_con.SzType.OOB: get_conf('oob_id'),
+                                 h_con.SzType.MGMT: get_conf('mgmt_id'),
+                                 h_con.SzType.DUMMY: get_conf('dummy_utif_id')}
+        config_resource_pool = get_conf('resource_pool_id')
+        self._heleos_api = h_op.BackendOperations(
+            esm_mgmt=config_esm_mgmt,
+            admin_username=config_admin_username,
+            admin_password=config_admin_password,
+            lb_image_id=config_lb_image_id,
+            security_zones=config_security_zones,
+            resource_pool=config_resource_pool)
+        self._dispatcher = dispatcher.Dispatcher(
+            self, get_conf("async_requests"))
+        self.plugin = plugin
+        poll_interval = conf.get('sync_interval')
+        if poll_interval > 0:
+            self._loop_call = poller.Poller(self)
+            self._loop_call.start_polling(conf.get('sync_interval'))
+        self._flavor = get_conf('lb_flavor')
+
+    def _validate_vip(self, vip):
+        if vip.get('connection_limit') and vip['connection_limit'] != -1:
+            raise h_exc.UnsupportedException(
+                err_msg=_('Connection limit is not supported by Embrane LB'))
+        persistance = vip.get('session_persistence')
+        if (persistance and persistance.get('type') ==
+                lbcon.SESSION_PERSISTENCE_APP_COOKIE):
+            p_type = vip['session_persistence']['type']
+            raise h_exc.UnsupportedException(
+                err_msg=_('Session persistence %s '
+                          'not supported by Embrane LBaaS') % p_type)
+
+    def _delete_vip(self, context, vip):
+        with context.session.begin(subtransactions=True):
+            self.plugin._delete_db_vip(context, vip['id'])
+            return econ.DELETED
+
+    def _delete_member(self, context, member):
+        self.plugin._delete_db_member(context, member['id'])
+
+    def _delete_pool_hm(self, context, health_monitor, pool_id):
+        self.plugin._delete_db_pool_health_monitor(context,
+                                                   health_monitor['id'],
+                                                   pool_id)
+
+    def _update_vip_graph_state(self, context, vip):
+        self._heleos_api.update_vip_status(vip)
+        self.plugin.update_status(context, ldb.Vip, vip['id'],
+                                  vip['status'])
+        if vip['status'] != pcon.ERROR:
+            pool = self.plugin.get_pool(context, vip['pool_id'])
+            pool_members = pool['members']
+            # Manages possible manual changes and monitor actions
+            self._heleos_api.update_pool_status(vip['id'], pool)
+            self._heleos_api.update_members_status(vip['id'], pool['id'],
+                                                   pool_members)
+            self.plugin.update_status(context, ldb.Pool, pool['id'],
+                                      pool['status'])
+            for member in pool_members:
+                self.plugin.update_status(context, ldb.Member,
+                                          member['id'], member['status'])
+
+    def _create_backend_port(self, context, db_pool):
+        try:
+            subnet = self.plugin._core_plugin.get_subnet(context,
+                                                         db_pool["subnet_id"])
+        except n_exc.SubnetNotFound:
+            LOG.warning(_("Subnet assigned to pool %s doesn't exist, "
+                          "backend port can't be created"), db_pool['id'])
+            return
+
+        fixed_ip = {'subnet_id': subnet['id'],
+                    'fixed_ips': attributes.ATTR_NOT_SPECIFIED}
+
+        port_data = {
+            'tenant_id': db_pool['tenant_id'],
+            'name': 'pool-' + db_pool['id'],
+            'network_id': subnet['network_id'],
+            'mac_address': attributes.ATTR_NOT_SPECIFIED,
+            'admin_state_up': False,
+            'device_id': '',
+            'device_owner': '',
+            'fixed_ips': [fixed_ip]
+        }
+
+        port = self.plugin._core_plugin.create_port(context,
+                                                    {'port': port_data})
+        return edb.add_pool_port(context, db_pool['id'], port['id'])
+
+    def _retrieve_utif_info(self, context, neutron_port):
+        network = self.plugin._core_plugin.get_network(
+            context, neutron_port['network_id'])
+        result = h_info.UtifInfo(network.get('provider:segmentation_id'),
+                                 network['name'],
+                                 network['id'],
+                                 False,
+                                 network['tenant_id'],
+                                 neutron_port['id'],
+                                 neutron_port['mac_address'],
+                                 network.get('provider:network_type'))
+        return result
+
+    def create_vip(self, context, vip):
+        self._validate_vip(vip)
+        db_vip = self.plugin.populate_vip_graph(context, vip)
+        vip_port = self.plugin._core_plugin._get_port(context,
+                                                      db_vip['port_id'])
+        vip_utif_info = self._retrieve_utif_info(context, vip_port)
+        vip_ip_allocation_info = utils.retrieve_ip_allocation_info(
+            context, vip_port)
+        vip_ip_allocation_info.is_gw = True
+        db_pool = pool_utif_info = pool_ip_allocation_info = None
+        members = monitors = []
+        if db_vip['pool_id']:
+            db_pool = self.plugin.get_pool(
+                context, db_vip['pool_id'])
+            pool_port = edb.get_pool_port(context, db_pool["id"])
+            if pool_port:
+                db_port = self.plugin._core_plugin._get_port(
+                    context, pool_port["port_id"])
+                pool_utif_info = self._retrieve_utif_info(context, db_port)
+                pool_ip_allocation_info = utils.retrieve_ip_allocation_info(
+                    context, db_port)
+            members = self.plugin.get_members(
+                context, filters={'id': db_pool['members']})
+            monitors = self.plugin.get_members(
+                context, filters={'id': db_pool['health_monitors']})
+        self._dispatcher.dispatch_lb(
+            embrane_ctx.DispatcherContext(econ.Events.CREATE_VIP,
+                                          db_vip, context, None),
+            self._flavor, vip_utif_info, vip_ip_allocation_info,
+            pool_utif_info, pool_ip_allocation_info, db_pool, members,
+            monitors)
+
+    def update_vip(self, context, old_vip, vip):
+        new_pool = old_port_id = removed_ip = None
+        new_pool_utif = new_pool_ip_allocation = None
+        old_pool = {}
+        members = monitors = []
+        if old_vip['pool_id'] != vip['pool_id']:
+            new_pool = self.plugin.get_pool(
+                context, vip['pool_id'])
+            members = self.plugin.get_members(
+                context, filters={'id': new_pool['members']})
+            monitors = self.plugin.get_members(
+                context, filters={'id': new_pool['health_monitors']})
+            new_pool_port = edb.get_pool_port(context, new_pool["id"])
+            if new_pool_port:
+                db_port = self.plugin._core_plugin._get_port(
+                    context, new_pool_port["port_id"])
+                new_pool_utif = self._retrieve_utif_info(context, db_port)
+                new_pool_ip_allocation = utils.retrieve_ip_allocation_info(
+                    context, db_port)
+            old_pool = self.plugin.get_pool(
+                context, old_vip['pool_id'])
+            old_pool_port = edb.get_pool_port(context, old_pool["id"])
+            if old_pool_port:
+                old_port = self.plugin._core_plugin._get_port(
+                    context, old_pool_port['port_id'])
+                # remove that subnet ip
+                removed_ip = old_port['fixed_ips'][0]['ip_address']
+                old_port_id = old_port['id']
+
+        self._dispatcher.dispatch_lb(
+            embrane_ctx.DispatcherContext(econ.Events.UPDATE_VIP, vip,
+                                          context, None),
+            old_pool.get('id'), old_port_id, removed_ip, new_pool_utif,
+            new_pool_ip_allocation, new_pool, members, monitors)
+
+    def delete_vip(self, context, vip):
+        db_vip = self.plugin.populate_vip_graph(context, vip)
+        self._dispatcher.dispatch_lb(
+            embrane_ctx.DispatcherContext(
+                econ.Events.DELETE_VIP, db_vip, context, None))
+
+    def create_pool(self, context, pool):
+        if pool['subnet_id']:
+            self._create_backend_port(context, pool)
+
+    def update_pool(self, context, old_pool, pool):
+        with context.session.begin(subtransactions=True):
+            if old_pool['vip_id']:
+                try:
+                    db_vip = self.plugin._get_resource(
+                        context, ldb.Vip, old_pool['vip_id'])
+                except lb_ext.VipNotFound:
+                    return
+                monitors = self.plugin.get_members(
+                    context, filters={'id': old_pool['health_monitors']})
+                self._dispatcher.dispatch_lb(
+                    embrane_ctx.DispatcherContext(econ.Events.UPDATE_POOL,
+                                                  db_vip, context, None),
+                    pool, monitors)
+
+    def delete_pool(self, context, pool):
+        edb.delete_pool_backend(context, pool['id'])
+        self.plugin._delete_db_pool(context, pool['id'])
+
+    def create_member(self, context, member):
+        db_pool = self.plugin.get_pool(context, member['pool_id'])
+        if db_pool['vip_id']:
+            db_vip = self.plugin._get_resource(context, ldb.Vip,
+                                               db_pool['vip_id'])
+            self._dispatcher.dispatch_lb(
+                embrane_ctx.DispatcherContext(
+                    econ.Events.ADD_OR_UPDATE_MEMBER, db_vip, context, None),
+                member, db_pool['protocol'])
+
+    def update_member(self, context, old_member, member):
+        db_pool = self.plugin.get_pool(context, member['pool_id'])
+        if member['pool_id'] != old_member['pool_id']:
+            old_pool = self.plugin.get_pool(context, old_member['pool_id'])
+            if old_pool['vip_id']:
+                db_vip = self.plugin._get_resource(context, ldb.Vip,
+                                                   old_pool['vip_id'])
+                self._dispatcher.dispatch_lb(
+                    embrane_ctx.DispatcherContext(
+                        econ.Events.REMOVE_MEMBER, db_vip, context, None),
+                    old_member)
+        if db_pool['vip_id']:
+            db_vip = self.plugin._get_resource(
+                context, ldb.Vip, db_pool['vip_id'])
+            self._dispatcher.dispatch_lb(
+                embrane_ctx.DispatcherContext(
+                    econ.Events.ADD_OR_UPDATE_MEMBER, db_vip, context, None),
+                member, db_pool['protocol'])
+
+    def delete_member(self, context, member):
+        db_pool = self.plugin.get_pool(context, member['pool_id'])
+        if db_pool['vip_id']:
+            db_vip = self.plugin._get_resource(context, ldb.Vip,
+                                               db_pool['vip_id'])
+            self._dispatcher.dispatch_lb(
+                embrane_ctx.DispatcherContext(
+                    econ.Events.DELETE_MEMBER, db_vip, context, None),
+                member)
+        else:
+            self._delete_member(context, member)
+
+    def stats(self, context, pool_id):
+        return {'bytes_in': 0,
+                'bytes_out': 0,
+                'active_connections': 0,
+                'total_connections': 0}
+
+    def create_pool_health_monitor(self, context, health_monitor, pool_id):
+        db_pool = self.plugin.get_pool(context, pool_id)
+        # API call only if vip exists
+        if db_pool['vip_id']:
+            db_vip = self.plugin._get_resource(context, ldb.Vip,
+                                               db_pool['vip_id'])
+            self._dispatcher.dispatch_lb(
+                embrane_ctx.DispatcherContext(
+                    econ.Events.ADD_POOL_HM, db_vip, context, None),
+                health_monitor, pool_id)
+
+    def update_pool_health_monitor(self, context, old_health_monitor,
+                                   health_monitor, pool_id):
+        db_pool = self.plugin.get_pool(context, pool_id)
+        if db_pool['vip_id']:
+            db_vip = self.plugin._get_resource(context, ldb.Vip,
+                                               db_pool['vip_id'])
+            self._dispatcher.dispatch_lb(
+                embrane_ctx.DispatcherContext(
+                    econ.Events.UPDATE_POOL_HM, db_vip, context, None),
+                health_monitor, pool_id)
+
+    def delete_pool_health_monitor(self, context, health_monitor, pool_id):
+        db_pool = self.plugin.get_pool(context, pool_id)
+        if db_pool['vip_id']:
+            db_vip = self.plugin._get_resource(context, ldb.Vip,
+                                               db_pool['vip_id'])
+            self._dispatcher.dispatch_lb(
+                embrane_ctx.DispatcherContext(
+                    econ.Events.DELETE_POOL_HM, db_vip, context, None),
+                health_monitor, pool_id)
+        else:
+            self._delete_pool_hm(context, health_monitor, pool_id)
diff --git a/neutron/services/loadbalancer/drivers/embrane/models.py b/neutron/services/loadbalancer/drivers/embrane/models.py
new file mode 100644 (file)
index 0000000..e092c00
--- /dev/null
@@ -0,0 +1,30 @@
+# Copyright 2014 Embrane, Inc.
+# 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.
+#
+# @author:  Ivar Lazzaro, Embrane, Inc. ivar@embrane.com
+
+import sqlalchemy as sql
+
+from neutron.db.models_v2 import model_base
+
+
+class PoolPort(model_base.BASEV2):
+    """Represents the connection between pools and ports."""
+    __tablename__ = 'embrane_pool_port'
+
+    pool_id = sql.Column(sql.String(36), sql.ForeignKey('pools.id'),
+                         primary_key=True)
+    port_id = sql.Column(sql.String(36), sql.ForeignKey('ports.id'),
+                         nullable=False)
diff --git a/neutron/services/loadbalancer/drivers/embrane/poller.py b/neutron/services/loadbalancer/drivers/embrane/poller.py
new file mode 100644 (file)
index 0000000..bf36079
--- /dev/null
@@ -0,0 +1,71 @@
+# Copyright 2014 Embrane, Inc.
+# 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.
+#
+# @author:  Ivar Lazzaro, Embrane, Inc. ivar@embrane.com
+
+from heleosapi import exceptions as h_exc
+
+from neutron import context
+from neutron.db.loadbalancer import loadbalancer_db as ldb
+from neutron.db import servicetype_db as sdb
+from neutron.openstack.common import log as logging
+from neutron.openstack.common import loopingcall
+from neutron.plugins.common import constants as ccon
+from neutron.plugins.embrane.common import contexts as embrane_ctx
+from neutron.services.loadbalancer.drivers.embrane import constants as econ
+
+LOG = logging.getLogger(__name__)
+skip_states = [ccon.PENDING_CREATE,
+               ccon.PENDING_DELETE,
+               ccon.PENDING_UPDATE,
+               ccon.ERROR]
+
+
+class Poller(object):
+    def __init__(self, driver):
+        self.dispatcher = driver._dispatcher
+        service_type_manager = sdb.ServiceTypeManager.get_instance()
+        self.provider = (service_type_manager.get_service_providers(
+            None, filters={
+                'service_type': [ccon.LOADBALANCER],
+                'driver': ['neutron.services.loadbalancer.drivers.'
+                           'embrane.driver.EmbraneLbaas']}))[0]['name']
+
+    def start_polling(self, interval):
+        loop_call = loopingcall.FixedIntervalLoopingCall(self._run)
+        loop_call.start(interval=interval)
+        return loop_call
+
+    def _run(self):
+        ctx = context.get_admin_context()
+        try:
+            self.synchronize_vips(ctx)
+        except h_exc.PollingException as e:
+            LOG.exception(_('Unhandled exception occurred'), e)
+
+    def synchronize_vips(self, ctx):
+        session = ctx.session
+        vips = session.query(ldb.Vip).join(
+            sdb.ProviderResourceAssociation,
+            sdb.ProviderResourceAssociation.resource_id ==
+            ldb.Vip.pool_id).filter(
+                sdb.ProviderResourceAssociation.provider_name == self.provider)
+        # No need to check pending states
+        for vip in vips:
+            if vip['status'] not in skip_states:
+                self.dispatcher.dispatch_lb(
+                    d_context=embrane_ctx.DispatcherContext(
+                        econ.Events.POLL_GRAPH, vip, ctx, None),
+                    args=())
diff --git a/neutron/tests/unit/services/loadbalancer/drivers/embrane/__init__.py b/neutron/tests/unit/services/loadbalancer/drivers/embrane/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/neutron/tests/unit/services/loadbalancer/drivers/embrane/test_embrane_defaults.py b/neutron/tests/unit/services/loadbalancer/drivers/embrane/test_embrane_defaults.py
new file mode 100644 (file)
index 0000000..d3588f8
--- /dev/null
@@ -0,0 +1,35 @@
+# vim:  tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 Embrane, Inc.
+# 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.
+#
+# @author:  Ivar Lazzaro, Embrane, Inc.
+
+import sys
+
+import mock
+from oslo.config import cfg
+
+from neutron.services.loadbalancer.drivers.embrane import config  # noqa
+from neutron.tests import base
+
+sys.modules["heleosapi"] = mock.Mock()
+
+
+class ConfigurationTest(base.BaseTestCase):
+
+    def test_defaults(self):
+        self.assertEqual('small', cfg.CONF.heleoslb.lb_flavor)
+        self.assertEqual(60, cfg.CONF.heleoslb.sync_interval)
diff --git a/neutron/tests/unit/services/loadbalancer/drivers/embrane/test_plugin_driver.py b/neutron/tests/unit/services/loadbalancer/drivers/embrane/test_plugin_driver.py
new file mode 100644 (file)
index 0000000..c98ef5f
--- /dev/null
@@ -0,0 +1,88 @@
+# vim:  tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 Embrane, Inc.
+# 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.
+#
+# @author:  Ivar Lazzaro, Embrane, Inc. ivar@embrane.com
+
+import sys
+
+import mock
+sys.modules["heleosapi"] = mock.Mock()
+from oslo.config import cfg
+
+from neutron import context
+from neutron.openstack.common.db import exception as n_exc
+from neutron.services.loadbalancer.drivers.embrane import config  # noqa
+from neutron.services.loadbalancer.drivers.embrane import constants as h_con
+from neutron.services.loadbalancer.drivers.embrane import db as h_db
+from neutron.tests.unit.db.loadbalancer import test_db_loadbalancer
+
+
+EMBRANE_PROVIDER = ('LOADBALANCER:lbaas:neutron.services.'
+                    'loadbalancer.drivers.embrane.driver.'
+                    'EmbraneLbaas:default')
+
+
+class TestLoadBalancerPluginBase(
+        test_db_loadbalancer.LoadBalancerPluginDbTestCase):
+
+    def setUp(self):
+        cfg.CONF.set_override('admin_password', "admin123", 'heleoslb')
+        cfg.CONF.set_override('sync_interval', 0, 'heleoslb')
+
+        super(TestLoadBalancerPluginBase, self).setUp(
+            lbaas_provider=EMBRANE_PROVIDER)
+        self.driver = self.plugin.drivers['lbaas']
+
+
+class TestLoadBalancerPlugin(test_db_loadbalancer.TestLoadBalancer,
+                             TestLoadBalancerPluginBase):
+
+    def test_create_vip_with_session_persistence_with_app_cookie(self):
+        self.skip("App cookie persistence not supported.")
+
+    def test_pool_port(self):
+        with self.port(no_delete=True) as port:
+            with self.pool() as pool:
+                h_db.add_pool_port(context.get_admin_context(),
+                                   pool['pool']['id'], port['port']['id'])
+                pool_port = h_db.get_pool_port(context.get_admin_context(),
+                                               pool['pool']['id'])
+                self.assertIsNotNone(pool_port)
+            pool_port = h_db.get_pool_port(context.get_admin_context(),
+                                           pool['pool']['id'])
+            self.assertIsNone(pool_port)
+
+    def test_create_pool_port_no_port(self):
+        with self.pool() as pool:
+            self.assertRaises(n_exc.DBError,
+                              h_db.add_pool_port,
+                              context.get_admin_context(),
+                              pool['pool']['id'], None)
+
+    def test_lb_operations_handlers(self):
+        h = self.driver._dispatcher.handlers
+        self.assertIsNotNone(h[h_con.Events.ADD_OR_UPDATE_MEMBER])
+        self.assertIsNotNone(h[h_con.Events.CREATE_VIP])
+        self.assertIsNotNone(h[h_con.Events.DELETE_MEMBER])
+        self.assertIsNotNone(h[h_con.Events.DELETE_VIP])
+        self.assertIsNotNone(h[h_con.Events.POLL_GRAPH])
+        self.assertIsNotNone(h[h_con.Events.REMOVE_MEMBER])
+        self.assertIsNotNone(h[h_con.Events.UPDATE_POOL])
+        self.assertIsNotNone(h[h_con.Events.UPDATE_VIP])
+        self.assertIsNotNone(h[h_con.Events.UPDATE_POOL_HM])
+        self.assertIsNotNone(h[h_con.Events.DELETE_POOL_HM])
+        self.assertIsNotNone(h[h_con.Events.ADD_POOL_HM])