From e9d1298e9266380fc501cdb8c02fe25fe07802d2 Mon Sep 17 00:00:00 2001
From: Thomas Herve
Date: Mon, 12 Aug 2013 18:08:04 +0200
Subject: [PATCH] Implement a load balancer resource using new neutron pool
It creates a new resource linking to a neutron pool, and taking a list
of servers to be linked to it.
Implements: blueprint lbaas-resource
Change-Id: Ie1590e12449a8e086eee7e7960a45103cbf20860
---
heat/engine/resources/neutron/loadbalancer.py | 79 ++++++++++++-
heat/tests/test_neutron_loadbalancer.py | 109 ++++++++++++++++++
2 files changed, 187 insertions(+), 1 deletion(-)
diff --git a/heat/engine/resources/neutron/loadbalancer.py b/heat/engine/resources/neutron/loadbalancer.py
index 51b78b3e7e..7dfbbdf4b9 100644
--- a/heat/engine/resources/neutron/loadbalancer.py
+++ b/heat/engine/resources/neutron/loadbalancer.py
@@ -14,8 +14,11 @@
# under the License.
from heat.common import exception
+from heat.db.sqlalchemy import api as db_api
from heat.engine import clients
+from heat.engine import resource
from heat.engine import scheduler
+from heat.engine.resources import nova_utils
from heat.engine.resources.neutron import neutron
if clients.neutronclient is not None:
@@ -186,7 +189,7 @@ class Pool(neutron.NeutronResource):
client = self.neutron()
monitors = set(prop_diff.pop('monitors', []))
if monitors:
- old_monitors = set(self.t['Properties'].get('monitors', []))
+ old_monitors = set(self.t['Properties']['monitors'])
for monitor in old_monitors - monitors:
client.disassociate_health_monitor(
self.resource_id, {'health_monitor': {'id': monitor}})
@@ -237,6 +240,79 @@ class Pool(neutron.NeutronResource):
self._delete_pool()
+class LoadBalancer(resource.Resource):
+ """
+ A resource to link a neutron pool with servers.
+ """
+
+ properties_schema = {
+ 'pool_id': {
+ 'Type': 'String', 'Required': True,
+ 'Description': _('The ID of the load balancing pool')},
+ 'protocol_port': {
+ 'Type': 'Integer', 'Required': True,
+ 'Description': _('Port number on which the servers are '
+ 'running on the members')},
+ 'members': {
+ 'Type': 'List',
+ 'Description': _('The list of Nova server IDs load balanced')},
+ }
+
+ update_allowed_keys = ('Properties',)
+
+ update_allowed_properties = ('members',)
+
+ def handle_create(self):
+ pool = self.properties['pool_id']
+ client = self.neutron()
+ nova_client = self.nova()
+ protocol_port = self.properties['protocol_port']
+ for member in self.properties['members']:
+ address = nova_utils.server_to_ipaddress(nova_client, member)
+ lb_member = client.create_member({
+ 'member': {
+ 'pool_id': pool,
+ 'address': address,
+ 'protocol_port': protocol_port}})['member']
+ db_api.resource_data_set(self, member, lb_member['id'])
+
+ def handle_update(self, json_snippet, tmpl_diff, prop_diff):
+ if 'members' in prop_diff:
+ members = set(prop_diff['members'])
+ old_members = set(self.t['Properties']['members'])
+ client = self.neutron()
+ for member in old_members - members:
+ member_id = db_api.resource_data_get(self, member)
+ try:
+ client.delete_member(member_id)
+ except NeutronClientException as ex:
+ if ex.status_code != 404:
+ raise ex
+ db_api.resource_data_delete(self, member)
+ pool = self.properties['pool_id']
+ nova_client = self.nova()
+ protocol_port = self.properties['protocol_port']
+ for member in members - old_members:
+ address = nova_utils.server_to_ipaddress(nova_client, member)
+ lb_member = client.create_member({
+ 'member': {
+ 'pool_id': pool,
+ 'address': address,
+ 'protocol_port': protocol_port}})['member']
+ db_api.resource_data_set(self, member, lb_member['id'])
+
+ def handle_delete(self):
+ client = self.neutron()
+ for member in self.properties['members']:
+ member_id = db_api.resource_data_get(self, member)
+ try:
+ client.delete_member(member_id)
+ except NeutronClientException as ex:
+ if ex.status_code != 404:
+ raise ex
+ db_api.resource_data_delete(self, member)
+
+
def resource_mapping():
if clients.neutronclient is None:
return {}
@@ -244,4 +320,5 @@ def resource_mapping():
return {
'OS::Neutron::HealthMonitor': HealthMonitor,
'OS::Neutron::Pool': Pool,
+ 'OS::Neutron::LoadBalancer': LoadBalancer,
}
diff --git a/heat/tests/test_neutron_loadbalancer.py b/heat/tests/test_neutron_loadbalancer.py
index af4409a763..3fd64fba41 100644
--- a/heat/tests/test_neutron_loadbalancer.py
+++ b/heat/tests/test_neutron_loadbalancer.py
@@ -25,6 +25,7 @@ from heat.openstack.common.importutils import try_import
from heat.tests import fakes
from heat.tests import utils
from heat.tests.common import HeatTestCase
+from heat.tests.v1_1 import fakes as nova_fakes
neutronclient = try_import('neutronclient.v2_0.client')
@@ -68,6 +69,24 @@ pool_template = '''
}
'''
+lb_template = '''
+{
+ "AWSTemplateFormatVersion" : "2010-09-09",
+ "Description" : "Template to test load balancer resources",
+ "Parameters" : {},
+ "Resources" : {
+ "lb": {
+ "Type": "OS::Neutron::LoadBalancer",
+ "Properties": {
+ "protocol_port": 8080,
+ "pool_id": "pool123",
+ "members": ["1234"]
+ }
+ }
+ }
+}
+'''
+
@skipIf(neutronclient is None, 'neutronclient unavailable')
class HealthMonitorTest(HeatTestCase):
@@ -517,3 +536,93 @@ class PoolTest(HeatTestCase):
self.assertEqual(None, rsrc.update(update_template))
self.m.VerifyAll()
+
+
+@skipIf(neutronclient is None, 'neutronclient unavailable')
+class LoadBalancerTest(HeatTestCase):
+
+ def setUp(self):
+ super(LoadBalancerTest, self).setUp()
+ self.fc = nova_fakes.FakeClient()
+ self.m.StubOutWithMock(neutronclient.Client, 'create_member')
+ self.m.StubOutWithMock(neutronclient.Client, 'delete_member')
+ self.m.StubOutWithMock(clients.OpenStackClients, 'keystone')
+ self.m.StubOutWithMock(clients.OpenStackClients, 'nova')
+ utils.setup_dummy_db()
+
+ def create_load_balancer(self):
+ clients.OpenStackClients.keystone().AndReturn(
+ fakes.FakeKeystoneClient())
+ clients.OpenStackClients.nova("compute").MultipleTimes().AndReturn(
+ self.fc)
+ neutronclient.Client.create_member({
+ 'member': {
+ 'pool_id': 'pool123', 'protocol_port': 8080,
+ 'address': '1.2.3.4'}}
+ ).AndReturn({'member': {'id': 'member5678'}})
+ snippet = template_format.parse(lb_template)
+ stack = utils.parse_stack(snippet)
+ return loadbalancer.LoadBalancer(
+ 'lb', snippet['Resources']['lb'], stack)
+
+ def test_create(self):
+ rsrc = self.create_load_balancer()
+
+ self.m.ReplayAll()
+ scheduler.TaskRunner(rsrc.create)()
+ self.assertEqual((rsrc.CREATE, rsrc.COMPLETE), rsrc.state)
+ self.m.VerifyAll()
+
+ def test_update(self):
+ rsrc = self.create_load_balancer()
+ neutronclient.Client.delete_member(u'member5678')
+ neutronclient.Client.create_member({
+ 'member': {
+ 'pool_id': 'pool123', 'protocol_port': 8080,
+ 'address': '4.5.6.7'}}
+ ).AndReturn({'member': {'id': 'memberxyz'}})
+
+ self.m.ReplayAll()
+ scheduler.TaskRunner(rsrc.create)()
+
+ update_template = copy.deepcopy(rsrc.t)
+ update_template['Properties']['members'] = ['5678']
+
+ self.assertEqual(None, rsrc.update(update_template))
+ self.m.VerifyAll()
+
+ def test_update_missing_member(self):
+ rsrc = self.create_load_balancer()
+ neutronclient.Client.delete_member(u'member5678').AndRaise(
+ loadbalancer.NeutronClientException(status_code=404))
+
+ self.m.ReplayAll()
+ scheduler.TaskRunner(rsrc.create)()
+
+ update_template = copy.deepcopy(rsrc.t)
+ update_template['Properties']['members'] = []
+
+ self.assertEqual(None, rsrc.update(update_template))
+ self.assertEqual((rsrc.UPDATE, rsrc.COMPLETE), rsrc.state)
+ self.m.VerifyAll()
+
+ def test_delete(self):
+ rsrc = self.create_load_balancer()
+ neutronclient.Client.delete_member(u'member5678')
+
+ self.m.ReplayAll()
+ scheduler.TaskRunner(rsrc.create)()
+ scheduler.TaskRunner(rsrc.delete)()
+ self.assertEqual((rsrc.DELETE, rsrc.COMPLETE), rsrc.state)
+ self.m.VerifyAll()
+
+ def test_delete_missing_member(self):
+ rsrc = self.create_load_balancer()
+ neutronclient.Client.delete_member(u'member5678').AndRaise(
+ loadbalancer.NeutronClientException(status_code=404))
+
+ self.m.ReplayAll()
+ scheduler.TaskRunner(rsrc.create)()
+ scheduler.TaskRunner(rsrc.delete)()
+ self.assertEqual((rsrc.DELETE, rsrc.COMPLETE), rsrc.state)
+ self.m.VerifyAll()
|