]> review.fuel-infra Code Review - openstack-build/heat-build.git/commitdiff
Implement a load balancer resource using new neutron pool
authorThomas Herve <th@rackspace.com>
Mon, 12 Aug 2013 16:08:04 +0000 (18:08 +0200)
committerThomas Herve <th@rackspace.com>
Fri, 23 Aug 2013 08:41:04 +0000 (10:41 +0200)
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
heat/tests/test_neutron_loadbalancer.py

index 51b78b3e7ec21125f8bdb6fc48ed70dd99acfcf5..7dfbbdf4b939b096c19f0986fee666ed241c2343 100644 (file)
 #    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,
     }
index af4409a763bab2f73b76955c82d24878b92b6b25..3fd64fba4178997232f4b8eb8d3f6d76b7682a94 100644 (file)
@@ -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()