multi-vendor-support-for-lbaas

BluePrint: multi-vendor-support-for-lbaas-step0

This is the first stage in a 3 stages BP

Change-Id: Ic5b2c46c5a74338c3fa14cc991f4420cabd7798e
This commit is contained in:
Avishay Balderman 2013-05-05 02:22:17 -07:00
parent d83caf9c1e
commit dd3a40d92b
8 changed files with 543 additions and 61 deletions

View File

@ -317,3 +317,9 @@ admin_tenant_name = %SERVICE_TENANT_NAME%
admin_user = %SERVICE_USER% admin_user = %SERVICE_USER%
admin_password = %SERVICE_PASSWORD% admin_password = %SERVICE_PASSWORD%
signing_dir = /var/lib/quantum/keystone-signing signing_dir = /var/lib/quantum/keystone-signing
[LBAAS]
# ==================================================================================================
# driver_fqn is the fully qualified name of the lbaas driver that will be loaded by the lbass plugin
# ==================================================================================================
#driver_fqn = quantum.plugins.services.agent_loadbalancer.drivers.noop.noop_driver.NoopLbaaSDriver

View File

@ -449,15 +449,24 @@ class LoadBalancerPluginDb(LoadBalancerPluginBase,
return self._fields(res, fields) return self._fields(res, fields)
def _create_pool_stats(self, context, pool_id): def _update_pool_stats(self, context, pool_id, data=None):
"""Update a pool with new stats structure."""
with context.session.begin(subtransactions=True):
pool_db = self._get_resource(context, Pool, pool_id)
self.assert_modification_allowed(pool_db)
pool_db.stats = self._create_pool_stats(context, pool_id, data)
def _create_pool_stats(self, context, pool_id, data=None):
# This is internal method to add pool statistics. It won't # This is internal method to add pool statistics. It won't
# be exposed to API # be exposed to API
if not data:
data = {}
stats_db = PoolStatistics( stats_db = PoolStatistics(
pool_id=pool_id, pool_id=pool_id,
bytes_in=0, bytes_in=data.get("bytes_in", 0),
bytes_out=0, bytes_out=data.get("bytes_out", 0),
active_connections=0, active_connections=data.get("active_connections", 0),
total_connections=0 total_connections=data.get("total_connections", 0)
) )
return stats_db return stats_db

View File

@ -0,0 +1,131 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Copyright 2013 Radware LTD.
#
# 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: Avishay Balderman, Radware
import abc
class LoadBalancerAbstractDriver(object):
"""Abstract lbaas driver that expose ~same API as lbaas plugin.
The configuration elements (Vip,Member,etc) are the dicts that
are returned to the tenant.
Get operations are not part of the API - it will be handled
by the lbaas plugin.
"""
__metaclass__ = abc.ABCMeta
@abc.abstractmethod
def create_vip(self, context, vip):
"""A real driver would invoke a call to his backend
and set the Vip status to ACTIVE/ERROR according
to the backend call result
self.plugin.update_status(context, Vip, vip["id"],
constants.ACTIVE)
"""
pass
@abc.abstractmethod
def update_vip(self, context, old_vip, vip):
"""Driver may call the code below in order to update the status.
self.plugin.update_status(context, Vip, id, constants.ACTIVE)
"""
pass
@abc.abstractmethod
def delete_vip(self, context, vip):
"""A real driver would invoke a call to his backend
and try to delete the Vip.
if the deletion was successfull, delete the record from the database.
if the deletion has failed, set the Vip status to ERROR.
"""
pass
@abc.abstractmethod
def create_pool(self, context, pool):
"""Driver may call the code below in order to update the status.
self.plugin.update_status(context, Pool, pool["id"],
constants.ACTIVE)
"""
pass
@abc.abstractmethod
def update_pool(self, context, old_pool, pool):
"""Driver may call the code below in order to update the status.
self.plugin.update_status(context,
Pool,
pool["id"], constants.ACTIVE)
"""
pass
@abc.abstractmethod
def delete_pool(self, context, pool):
"""Driver can call the code below in order to delete the pool.
self.plugin._delete_db_pool(context, pool["id"])
or set the status to ERROR if deletion failed
"""
pass
@abc.abstractmethod
def stats(self, context, pool_id):
pass
@abc.abstractmethod
def create_member(self, context, member):
"""Driver may call the code below in order to update the status.
self.plugin.update_status(context, Member, member["id"],
constants.ACTIVE)
"""
pass
@abc.abstractmethod
def update_member(self, context, old_member, member):
"""Driver may call the code below in order to update the status.
self.plugin.update_status(context, Member,
member["id"], constants.ACTIVE)
"""
pass
@abc.abstractmethod
def delete_member(self, context, member):
pass
@abc.abstractmethod
def create_health_monitor(self, context, health_monitor):
"""Driver may call the code below in order to update the status.
self.plugin.update_status(context, HealthMonitor,
health_monitor["id"],
constants.ACTIVE)
"""
pass
@abc.abstractmethod
def update_health_monitor(self, context,
old_health_monitor,
health_monitor,
pool_association):
pass
@abc.abstractmethod
def create_pool_health_monitor(self, context,
health_monitor,
pool_id):
pass
@abc.abstractmethod
def delete_pool_health_monitor(self, context, health_monitor, pool_id):
pass

View File

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

View File

@ -0,0 +1,112 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Copyright 2013 Radware LTD.
#
# 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: Avishay Balderman, Radware
from quantum.openstack.common import log as logging
from quantum.plugins.services.agent_loadbalancer.drivers import (
abstract_driver
)
LOG = logging.getLogger(__name__)
def log(method):
def wrapper(*args, **kwargs):
data = {"method_name": method.__name__, "args": args, "kwargs": kwargs}
LOG.debug(_('NoopLbaaSDriver method %(method_name)s'
'called with arguments %(args)s %(kwargs)s ')
% data)
return method(*args, **kwargs)
return wrapper
class NoopLbaaSDriver(abstract_driver.LoadBalancerAbstractDriver):
"""A dummy lbass driver that:
1) Logs methods input
2) Uses the plugin API in order to update
the config elements status in DB
"""
def __init__(self, plugin):
self.plugin = plugin
@log
def create_vip(self, context, vip):
pass
@log
def update_vip(self, context, old_vip, vip):
pass
@log
def delete_vip(self, context, vip):
self.plugin._delete_db_vip(context, vip["id"])
@log
def create_pool(self, context, pool):
pass
@log
def update_pool(self, context, old_pool, pool):
pass
@log
def delete_pool(self, context, pool):
pass
@log
def stats(self, context, pool_id):
return {"bytes_in": 0,
"bytes_out": 0,
"active_connections": 0,
"total_connections": 0}
@log
def create_member(self, context, member):
pass
@log
def update_member(self, context, old_member, member):
pass
@log
def delete_member(self, context, member):
self.plugin._delete_db_member(context, member["id"])
@log
def create_health_monitor(self, context, health_monitor):
pass
@log
def update_health_monitor(self, context, old_health_monitor,
health_monitor,
pool_association):
pass
@log
def create_pool_health_monitor(self, context,
health_monitor, pool_id):
pass
@log
def delete_pool_health_monitor(self, context, health_monitor, pool_id):
self.plugin._delete_db_pool_health_monitor(
context, health_monitor["id"],
pool_id
)

View File

@ -0,0 +1,221 @@
#
# Copyright 2013 Radware LTD.
#
# 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: Avishay Balderman, Radware
from oslo.config import cfg
from quantum.db import api as qdbapi
from quantum.db.loadbalancer import loadbalancer_db
from quantum.openstack.common import importutils
from quantum.openstack.common import log as logging
from quantum.plugins.common import constants
LOG = logging.getLogger(__name__)
DEFAULT_DRIVER = ("quantum.plugins.services.agent_loadbalancer"
".drivers.noop"
".noop_driver.NoopLbaaSDriver")
lbaas_plugin_opts = [
cfg.StrOpt('driver_fqn',
default=DEFAULT_DRIVER,
help=_('LBaaS driver Fully Qualified Name'))
]
cfg.CONF.register_opts(lbaas_plugin_opts, "LBAAS")
class LoadBalancerPlugin(loadbalancer_db.LoadBalancerPluginDb):
"""Implementation of the Quantum Loadbalancer Service Plugin.
This class manages the workflow of LBaaS request/response.
Most DB related works are implemented in class
loadbalancer_db.LoadBalancerPluginDb.
"""
supported_extension_aliases = ["lbaas"]
def __init__(self):
"""Initialization for the loadbalancer service plugin."""
qdbapi.register_models()
self.driver = importutils.import_object(
cfg.CONF.LBAAS.driver_fqn, self)
def get_plugin_type(self):
return constants.LOADBALANCER
def get_plugin_description(self):
return "Quantum LoadBalancer Service Plugin"
def create_vip(self, context, vip):
v = super(LoadBalancerPlugin, self).create_vip(context, vip)
self.driver.create_vip(context, v)
return v
def update_vip(self, context, id, vip):
if 'status' not in vip['vip']:
vip['vip']['status'] = constants.PENDING_UPDATE
old_vip = self.get_vip(context, id)
v = super(LoadBalancerPlugin, self).update_vip(context, id, vip)
self.driver.update_vip(context, old_vip, v)
return v
def _delete_db_vip(self, context, id):
super(LoadBalancerPlugin, self).delete_vip(context, id)
def delete_vip(self, context, id):
self.update_status(context, loadbalancer_db.Vip,
id, constants.PENDING_DELETE)
v = self.get_vip(context, id)
self.driver.delete_vip(context, v)
def create_pool(self, context, pool):
p = super(LoadBalancerPlugin, self).create_pool(context, pool)
self.driver.create_pool(context, p)
return p
def update_pool(self, context, id, pool):
if 'status' not in pool['pool']:
pool['pool']['status'] = constants.PENDING_UPDATE
old_pool = self.get_pool(context, id)
p = super(LoadBalancerPlugin, self).update_pool(context, id, pool)
self.driver.update_pool(context, old_pool, p)
return p
def _delete_db_pool(self, context, id):
super(LoadBalancerPlugin, self).delete_pool(context, id)
def delete_pool(self, context, id):
self.update_status(context, loadbalancer_db.Pool,
id, constants.PENDING_DELETE)
p = self.get_pool(context, id)
self.driver.delete_pool(context, p)
def create_member(self, context, member):
m = super(LoadBalancerPlugin, self).create_member(context, member)
self.driver.create_member(context, m)
return m
def update_member(self, context, id, member):
if 'status' not in member['member']:
member['member']['status'] = constants.PENDING_UPDATE
old_member = self.get_member(context, id)
m = super(LoadBalancerPlugin, self).update_member(context, id, member)
self.driver.update_member(context, old_member, m)
return m
def _delete_db_member(self, context, id):
super(LoadBalancerPlugin, self).delete_member(context, id)
def delete_member(self, context, id):
self.update_status(context, loadbalancer_db.Member,
id, constants.PENDING_DELETE)
m = self.get_member(context, id)
self.driver.delete_member(context, m)
def create_health_monitor(self, context, health_monitor):
hm = super(LoadBalancerPlugin, self).create_health_monitor(
context,
health_monitor
)
self.driver.create_health_monitor(context, hm)
return hm
def update_health_monitor(self, context, id, health_monitor):
if 'status' not in health_monitor['health_monitor']:
health_monitor['health_monitor']['status'] = (
constants.PENDING_UPDATE
)
old_hm = self.get_health_monitor(context, id)
hm = super(LoadBalancerPlugin, self).update_health_monitor(
context,
id,
health_monitor
)
with context.session.begin(subtransactions=True):
qry = context.session.query(
loadbalancer_db.PoolMonitorAssociation
)
qry = qry.filter_by(monitor_id=hm['id'])
assocs = qry.all()
for assoc in assocs:
self.driver.update_health_monitor(context, old_hm, hm, assoc)
return hm
def _delete_db_pool_health_monitor(self, context, hm_id, pool_id):
super(LoadBalancerPlugin, self).delete_pool_health_monitor(context,
hm_id,
pool_id)
def delete_health_monitor(self, context, id):
with context.session.begin(subtransactions=True):
qry = context.session.query(
loadbalancer_db.PoolMonitorAssociation
)
qry = qry.filter_by(monitor_id=id)
assocs = qry.all()
hm = self.get_health_monitor(context, id)
for assoc in assocs:
self.driver.delete_pool_health_monitor(context,
hm,
assoc['pool_id'])
def create_pool_health_monitor(self, context, health_monitor, pool_id):
retval = super(LoadBalancerPlugin, self).create_pool_health_monitor(
context,
health_monitor,
pool_id
)
# open issue: PoolMonitorAssociation has no status field
# so we cant set the status to pending and let the driver
# set the real status of the association
self.driver.create_pool_health_monitor(
context, health_monitor, pool_id)
return retval
def delete_pool_health_monitor(self, context, id, pool_id):
hm = self.get_health_monitor(context, id)
self.driver.delete_pool_health_monitor(
context, hm, pool_id)
def stats(self, context, pool_id):
stats_data = self.driver.stats(context, pool_id)
# if we get something from the driver -
# update the db and return the value from db
# else - return what we have in db
if stats_data:
super(LoadBalancerPlugin, self)._update_pool_stats(
context,
pool_id,
stats_data
)
return super(LoadBalancerPlugin, self).stats(context,
pool_id)
def populate_vip_graph(self, context, vip):
"""Populate the vip with: pool, members, healthmonitors."""
pool = self.get_pool(context, vip['pool_id'])
vip['pool'] = pool
vip['members'] = [
self.get_member(context, member_id)
for member_id in pool['members']]
vip['health_monitors'] = [
self.get_health_monitor(context, hm_id)
for hm_id in pool['health_monitors']]
return vip

View File

@ -38,7 +38,8 @@ LOG = logging.getLogger(__name__)
DB_CORE_PLUGIN_KLASS = 'quantum.db.db_base_plugin_v2.QuantumDbPluginV2' DB_CORE_PLUGIN_KLASS = 'quantum.db.db_base_plugin_v2.QuantumDbPluginV2'
DB_LB_PLUGIN_KLASS = ( DB_LB_PLUGIN_KLASS = (
"quantum.plugins.services.agent_loadbalancer.plugin.LoadBalancerPlugin" "quantum.plugins.services.agent_loadbalancer."
"lbaas_plugin.LoadBalancerPlugin"
) )
ROOTDIR = os.path.dirname(__file__) + '../../../..' ROOTDIR = os.path.dirname(__file__) + '../../../..'
ETCDIR = os.path.join(ROOTDIR, 'etc') ETCDIR = os.path.join(ROOTDIR, 'etc')
@ -923,6 +924,31 @@ class TestLoadBalancer(LoadBalancerPluginDbTestCase):
(m1, m2, m3), (m1, m2, m3),
('delay', 'asc'), 2, 2) ('delay', 'asc'), 2, 2)
def test_update_pool_stats_with_no_stats(self):
keys = ["bytes_in", "bytes_out",
"active_connections",
"total_connections"]
with self.pool() as pool:
pool_id = pool['pool']['id']
ctx = context.get_admin_context()
self.plugin._update_pool_stats(ctx, pool_id)
pool_obj = ctx.session.query(ldb.Pool).filter_by(id=pool_id).one()
for key in keys:
self.assertEqual(pool_obj.stats.__dict__[key], 0)
def test_update_pool_stats(self):
stats_data = {"bytes_in": 1,
"bytes_out": 2,
"active_connections": 3,
"total_connections": 4}
with self.pool() as pool:
pool_id = pool['pool']['id']
ctx = context.get_admin_context()
self.plugin._update_pool_stats(ctx, pool_id, stats_data)
pool_obj = ctx.session.query(ldb.Pool).filter_by(id=pool_id).one()
for k, v in stats_data.items():
self.assertEqual(pool_obj.stats.__dict__[k], v)
def test_get_pool_stats(self): def test_get_pool_stats(self):
keys = [("bytes_in", 0), keys = [("bytes_in", 0),
("bytes_out", 0), ("bytes_out", 0),

View File

@ -22,6 +22,7 @@ import mock
from quantum import context from quantum import context
from quantum.db.loadbalancer import loadbalancer_db as ldb from quantum.db.loadbalancer import loadbalancer_db as ldb
from quantum import manager from quantum import manager
from quantum.openstack.common import importutils
from quantum.openstack.common import uuidutils from quantum.openstack.common import uuidutils
from quantum.plugins.common import constants from quantum.plugins.common import constants
from quantum.plugins.services.agent_loadbalancer import plugin from quantum.plugins.services.agent_loadbalancer import plugin
@ -40,7 +41,21 @@ class TestLoadBalancerPluginBase(
# we need access to loaded plugins to modify models # we need access to loaded plugins to modify models
loaded_plugins = manager.QuantumManager().get_service_plugins() loaded_plugins = manager.QuantumManager().get_service_plugins()
self.plugin_instance = loaded_plugins[constants.LOADBALANCER] # TODO(avishayb) - below is a little hack that helps the
# test to pass :-)
# the problem is the code below assumes the existance of 'callbacks'
# on the plugin. So the bypass is to load the plugin that has
# the callbacks as a member.The hack will be removed once we will
# have one lbaas plugin. (we currently have 2 - (Grizzly and Havana))
hack = True
if hack:
HACK_KLASS = (
"quantum.plugins.services.agent_loadbalancer."
"plugin.LoadBalancerPlugin"
)
self.plugin_instance = importutils.import_object(HACK_KLASS)
else:
self.plugin_instance = loaded_plugins[constants.LOADBALANCER]
self.callbacks = self.plugin_instance.callbacks self.callbacks = self.plugin_instance.callbacks
@ -257,57 +272,3 @@ class TestLoadBalancerAgentApi(base.BaseTestCase):
def test_modify_pool(self): def test_modify_pool(self):
self._call_test_helper('modify_pool') self._call_test_helper('modify_pool')
class TestLoadBalancerPluginNotificationWrapper(TestLoadBalancerPluginBase):
def setUp(self):
self.log = mock.patch.object(plugin, 'LOG')
api_cls = mock.patch.object(plugin, 'LoadBalancerAgentApi').start()
super(TestLoadBalancerPluginNotificationWrapper, self).setUp()
self.mock_api = api_cls.return_value
self.addCleanup(mock.patch.stopall)
def test_create_vip(self):
with self.subnet() as subnet:
with self.pool(subnet=subnet) as pool:
with self.vip(pool=pool, subnet=subnet) as vip:
self.mock_api.reload_pool.assert_called_once_with(
mock.ANY,
vip['vip']['pool_id']
)
def test_update_vip(self):
with self.subnet() as subnet:
with self.pool(subnet=subnet) as pool:
with self.vip(pool=pool, subnet=subnet) as vip:
self.mock_api.reset_mock()
ctx = context.get_admin_context()
vip['vip'].pop('status')
new_vip = self.plugin_instance.update_vip(
ctx,
vip['vip']['id'],
vip
)
self.mock_api.reload_pool.assert_called_once_with(
mock.ANY,
vip['vip']['pool_id']
)
self.assertEqual(
new_vip['status'],
constants.PENDING_UPDATE
)
def test_delete_vip(self):
with self.subnet() as subnet:
with self.pool(subnet=subnet) as pool:
with self.vip(pool=pool, subnet=subnet, no_delete=True) as vip:
self.mock_api.reset_mock()
ctx = context.get_admin_context()
self.plugin_instance.delete_vip(ctx, vip['vip']['id'])
self.mock_api.destroy_pool.assert_called_once_with(
mock.ANY,
vip['vip']['pool_id']
)