diff --git a/etc/neutron.conf b/etc/neutron.conf index c3cf25a4a7a..a021f4e8143 100644 --- a/etc/neutron.conf +++ b/etc/neutron.conf @@ -611,5 +611,7 @@ service_provider=VPN:openswan:neutron.services.vpn.service_drivers.ipsec.IPsecVP # 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 +# Uncomment the line below to use the A10 Networks LBaaS driver. Requires 'pip install a10-neutron-lbaas'. +#service_provider = LOADBALANCER:A10Networks:neutron.services.loadbalancer.drivers.a10networks.driver_v1.ThunderDriver:default # Uncomment the following line to test the LBaaS v2 API _WITHOUT_ a real backend # service_provider = LOADBALANCER:LoggingNoop:neutron.services.loadbalancer.drivers.logging_noop.driver.LoggingNoopLoadBalancerDriver:default diff --git a/neutron/services/loadbalancer/drivers/a10networks/README.txt b/neutron/services/loadbalancer/drivers/a10networks/README.txt new file mode 100644 index 00000000000..81c2854b50e --- /dev/null +++ b/neutron/services/loadbalancer/drivers/a10networks/README.txt @@ -0,0 +1,48 @@ +A10 Networks LBaaS Driver + +Installation info: + +To use this driver, you must: +- Install the a10-neutron-lbaas module. (E.g.: 'pip install a10-neutron-lbaas') +- Create a driver config file, a sample of which is given below. +- Enable it in neutron.conf +- Restart neutron-server + +Third-party CI info: + +Contact info for any problems is: a10-openstack-ci at a10networks dot com +Or contact Doug Wiegley directly (IRC: dougwig) + +Configuration file: + +Create a configuration file with a list of A10 appliances, similar to the +file below, located at: +/etc/neutron/services/loadbalancer/a10networks/config.py + +Or you can override that directory by setting the environment +variable A10_CONFIG_DIR. + +Example config file: + +devices = { + "ax1": { + "name": "ax1", + "host": "10.10.100.20", + "port": 443, + "protocol": "https", + "username": "admin", + "password": "a10", + "status": True, + "autosnat": False, + "api_version": "2.1", + "v_method": "LSI", + "max_instance": 5000, + "use_float": False, + "method": "hash" + }, + "ax4": { + "host": "10.10.100.23", + "username": "admin", + "password": "a10", + }, +} diff --git a/neutron/services/loadbalancer/drivers/a10networks/__init__.py b/neutron/services/loadbalancer/drivers/a10networks/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/neutron/services/loadbalancer/drivers/a10networks/driver_v1.py b/neutron/services/loadbalancer/drivers/a10networks/driver_v1.py new file mode 100644 index 00000000000..416819388b9 --- /dev/null +++ b/neutron/services/loadbalancer/drivers/a10networks/driver_v1.py @@ -0,0 +1,176 @@ +# Copyright 2014, Doug Wiegley (dougwig), A10 Networks +# +# 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 a10_neutron_lbaas + +from neutron.db import l3_db +from neutron.db.loadbalancer import loadbalancer_db as lb_db +from neutron.openstack.common import log as logging +from neutron.plugins.common import constants +from neutron.services.loadbalancer.drivers import abstract_driver + +VERSION = "1.0.0" +LOG = logging.getLogger(__name__) + + +# Most driver calls below are straight passthroughs to the A10 package +# 'a10_neutron_lbaas'. Any function that has not been fully abstracted +# into the openstack driver/plugin interface is NOT passed through, to +# make it obvious which hidden interfaces/db calls that we rely on. + +class ThunderDriver(abstract_driver.LoadBalancerAbstractDriver): + + def __init__(self, plugin): + LOG.debug("A10Driver: init version=%s", VERSION) + self.plugin = plugin + + # Map the string types to neutron classes/functions, in order to keep + # from reaching into the bowels of Neutron from anywhere but this file. + self.neutron_map = { + 'member': { + 'model': lb_db.Member, + 'delete_func': self.plugin._delete_db_member, + }, + 'pool': { + 'model': lb_db.Pool, + 'delete_func': self.plugin._delete_db_pool, + }, + 'vip': { + 'model': lb_db.Vip, + 'delete_func': self.plugin._delete_db_vip, + }, + } + + LOG.debug("A10Driver: initializing, version=%s, lbaas_manager=%s", + VERSION, a10_neutron_lbaas.VERSION) + + self.a10 = a10_neutron_lbaas.A10OpenstackLBV1(self) + + # The following private helper methods are used by a10_neutron_lbaas, + # and reflect the neutron interfaces required by that package. + + def _hm_binding_count(self, context, hm_id): + return context.session.query(lb_db.PoolMonitorAssociation).filter_by( + monitor_id=hm_id).join(lb_db.Pool).count() + + def _member_count(self, context, member): + return context.session.query(lb_db.Member).filter_by( + tenant_id=member['tenant_id'], + address=member['address']).count() + + def _member_get(self, context, member_id): + return self.plugin.get_member(context, member_id) + + def _member_get_ip(self, context, member, use_float=False): + ip_address = member['address'] + if use_float: + fip_qry = context.session.query(l3_db.FloatingIP) + if (fip_qry.filter_by(fixed_ip_address=ip_address).count() > 0): + float_address = fip_qry.filter_by( + fixed_ip_address=ip_address).first() + ip_address = str(float_address.floating_ip_address) + return ip_address + + def _pool_get_hm(self, context, hm_id): + return self.plugin.get_health_monitor(context, hm_id) + + def _pool_get_tenant_id(self, context, pool_id): + pool_qry = context.session.query(lb_db.Pool).filter_by(id=pool_id) + z = pool_qry.first() + if z: + return z.tenant_id + else: + return '' + + def _pool_get_vip_id(self, context, pool_id): + pool_qry = context.session.query(lb_db.Pool).filter_by(id=pool_id) + z = pool_qry.first() + if z: + return z.vip_id + else: + return '' + + def _pool_total(self, context, tenant_id): + return context.session.query(lb_db.Pool).filter_by( + tenant_id=tenant_id).count() + + def _vip_get(self, context, vip_id): + return self.plugin.get_vip(context, vip_id) + + def _active(self, context, model_type, model_id): + self.plugin.update_status(context, + self.neutron_map[model_type]['model'], + model_id, + constants.ACTIVE) + + def _failed(self, context, model_type, model_id): + self.plugin.update_status(context, + self.neutron_map[model_type]['model'], + model_id, + constants.ERROR) + + def _db_delete(self, context, model_type, model_id): + self.neutron_map[model_type]['delete_func'](context, model_id) + + def _hm_active(self, context, hm_id, pool_id): + self.plugin.update_pool_health_monitor(context, hm_id, pool_id, + constants.ACTIVE) + + def _hm_failed(self, context, hm_id, pool_id): + self.plugin.update_pool_health_monitor(context, hm_id, pool_id, + constants.ERROR) + + def _hm_db_delete(self, context, hm_id, pool_id): + self.plugin._delete_db_pool_health_monitor(context, hm_id, pool_id) + + # Pass-through driver + + def create_vip(self, context, vip): + self.a10.vip.create(context, vip) + + def update_vip(self, context, old_vip, vip): + self.a10.vip.update(context, old_vip, vip) + + def delete_vip(self, context, vip): + self.a10.vip.delete(context, vip) + + def create_pool(self, context, pool): + self.a10.pool.create(context, pool) + + def update_pool(self, context, old_pool, pool): + self.a10.pool.update(context, old_pool, pool) + + def delete_pool(self, context, pool): + self.a10.pool.delete(context, pool) + + def stats(self, context, pool_id): + return self.a10.pool.stats(context, pool_id) + + def create_member(self, context, member): + self.a10.member.create(context, member) + + def update_member(self, context, old_member, member): + self.a10.member.update(context, old_member, member) + + def delete_member(self, context, member): + self.a10.member.delete(context, member) + + def update_pool_health_monitor(self, context, old_hm, hm, pool_id): + self.a10.hm.update(context, old_hm, hm, pool_id) + + def create_pool_health_monitor(self, context, hm, pool_id): + self.a10.hm.create(context, hm, pool_id) + + def delete_pool_health_monitor(self, context, hm, pool_id): + self.a10.hm.delete(context, hm, pool_id) diff --git a/neutron/tests/unit/services/loadbalancer/drivers/a10networks/__init__.py b/neutron/tests/unit/services/loadbalancer/drivers/a10networks/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/neutron/tests/unit/services/loadbalancer/drivers/a10networks/test_driver_v1.py b/neutron/tests/unit/services/loadbalancer/drivers/a10networks/test_driver_v1.py new file mode 100644 index 00000000000..7517c64cc89 --- /dev/null +++ b/neutron/tests/unit/services/loadbalancer/drivers/a10networks/test_driver_v1.py @@ -0,0 +1,179 @@ +# Copyright 2014, Doug Wiegley (dougwig), A10 Networks +# +# 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 sys + +import mock + +from neutron import context +from neutron.db.loadbalancer import loadbalancer_db as lb_db +with mock.patch.dict(sys.modules, {'a10_neutron_lbaas': mock.Mock()}): + from neutron.services.loadbalancer.drivers.a10networks import driver_v1 +from neutron.tests.unit.db.loadbalancer import test_db_loadbalancer + + +def fake_model(id): + return { + 'id': id, + 'tenant_id': "tennant-was-a-great-doctor" + } + + +def fake_member(id): + return { + 'id': id, + 'tenant_id': "vippyvip", + 'address': '1.1.1.1' + } + + +class TestA10ThunderDriver(test_db_loadbalancer.LoadBalancerPluginDbTestCase): + + def setUp(self): + super(TestA10ThunderDriver, self).setUp() + self.context = context.get_admin_context() + self.plugin = mock.Mock() + self.driver = driver_v1.ThunderDriver(self.plugin) + self.driver.a10 = mock.Mock() + self.m = fake_model('p1') + + def test__hm_binding_count(self): + n = self.driver._hm_binding_count(self.context, 'hm01') + self.assertEqual(n, 0) + + def test__member_count(self): + self.m = fake_member('mem1') + n = self.driver._member_count(self.context, self.m) + self.assertEqual(n, 0) + + def test__member_get_ip(self): + self.m = fake_member('mem1') + z = self.driver._member_get_ip(self.context, self.m, False) + self.assertEqual(z, '1.1.1.1') + z = self.driver._member_get_ip(self.context, self.m, True) + self.assertEqual(z, '1.1.1.1') + + def test__pool_get_hm(self): + self.driver._pool_get_hm(self.context, 'hm01') + self.plugin.get_health_monitor.assert_called_once_with( + self.context, 'hm01') + + def test__pool_get_tenant_id(self): + z = self.driver._pool_get_tenant_id(self.context, 'pool1') + self.assertEqual(z, '') + + def test__pool_get_vip_id(self): + z = self.driver._pool_get_vip_id(self.context, 'pool1') + self.assertEqual(z, '') + + def test__pool_total(self): + n = self.driver._pool_total(self.context, + tenant_id='whatareyoudoingdave') + self.assertEqual(n, 0) + + def test__active(self): + self.driver._active(self.context, 'vip', 'vip1') + self.plugin.update_status.assert_called_once_with( + self.context, lb_db.Vip, 'vip1', 'ACTIVE') + + def test__failed(self): + self.driver._failed(self.context, 'vip', 'vip2-1-2') + self.plugin.update_status.assert_called_once_with( + self.context, lb_db.Vip, 'vip2-1-2', 'ERROR') + + def test__db_delete(self): + self.driver._db_delete(self.context, 'pool', 'myid0101') + self.plugin._delete_db_pool.assert_called_once_with( + self.context, 'myid0101') + + def test__hm_active(self): + self.driver._hm_active(self.context, 'hm01', 'pool1') + self.plugin.update_pool_health_monitor.assert_called_once_with( + self.context, 'hm01', 'pool1', 'ACTIVE') + + def test__hm_failed(self): + self.driver._hm_failed(self.context, 'hm01', 'pool1') + self.plugin.update_pool_health_monitor.assert_called_once_with( + self.context, 'hm01', 'pool1', 'ERROR') + + def test__hm_db_delete(self): + self.driver._hm_db_delete(self.context, 'hm01', 'pool2') + self.plugin._delete_db_pool_health_monitor.assert_called_once_with( + self.context, 'hm01', 'pool2') + + def test_create_vip(self): + self.driver.create_vip(self.context, self.m) + self.driver.a10.vip.create.assert_called_once_with( + self.context, self.m) + + def test_update_vip(self): + self.driver.update_vip(self.context, self.m, self.m) + self.driver.a10.vip.update.assert_called_once_with( + self.context, self.m, self.m) + + def test_delete_vip(self): + self.driver.delete_vip(self.context, self.m) + self.driver.a10.vip.delete.assert_called_once_with( + self.context, self.m) + + def test_create_pool(self): + self.driver.create_pool(self.context, self.m) + self.driver.a10.pool.create.assert_called_once_with( + self.context, self.m) + + def test_update_pool(self): + self.driver.update_pool(self.context, self.m, self.m) + self.driver.a10.pool.update.assert_called_once_with( + self.context, self.m, self.m) + + def test_delete_pool(self): + self.driver.delete_pool(self.context, self.m) + self.driver.a10.pool.delete.assert_called_once_with( + self.context, self.m) + + def test_stats(self): + self.driver.stats(self.context, self.m['id']) + self.driver.a10.pool.stats.assert_called_once_with( + self.context, self.m['id']) + + def test_create_member(self): + self.driver.create_member(self.context, self.m) + self.driver.a10.member.create.assert_called_once_with( + self.context, self.m) + + def test_update_member(self): + self.driver.update_member(self.context, self.m, self.m) + self.driver.a10.member.update.assert_called_once_with( + self.context, self.m, self.m) + + def test_delete_member(self): + self.driver.delete_member(self.context, self.m) + self.driver.a10.member.delete.assert_called_once_with( + self.context, self.m) + + def test_update_pool_health_monitor(self): + self.driver.update_pool_health_monitor(self.context, self.m, self.m, + 'pool1') + self.driver.a10.hm.update.assert_called_once_with( + self.context, self.m, self.m, 'pool1') + + def test_create_pool_health_monitor(self): + self.driver.create_pool_health_monitor(self.context, self.m, 'pool1') + self.driver.a10.hm.create.assert_called_once_with( + self.context, self.m, 'pool1') + + def test_delete_pool_health_monitor(self): + self.driver.delete_pool_health_monitor(self.context, self.m, 'pool1') + self.driver.a10.hm.delete.assert_called_once_with( + self.context, self.m, 'pool1')