]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
The patch introduces an API extension for LBaaS service
authorOleg Bondarev <obondarev@mirantis.com>
Tue, 11 Dec 2012 13:40:05 +0000 (17:40 +0400)
committerOleg Bondarev <obondarev@mirantis.com>
Wed, 19 Dec 2012 09:11:47 +0000 (13:11 +0400)
- API extension
- abstract base class for plugin
- some new validators were added to quantum/api/v2/attributes.py

Implements: blueprint lbaas-restapi-tenant
Change-Id: Ic2fd4debc4969389b395ce7352ab208c6854018b

quantum/api/v2/attributes.py
quantum/api/v2/base.py
quantum/extensions/l3.py
quantum/extensions/loadbalancer.py [new file with mode: 0644]
quantum/plugins/common/constants.py
quantum/plugins/services/service_base.py
quantum/tests/unit/test_attributes.py
quantum/tests/unit/test_loadbalancer_plugin.py [new file with mode: 0644]

index 8cda1eee55fb86e73f846f8747242e07e697f661..a30528a56c2ada6f1b7b15739cf82bc8b77f29ff 100644 (file)
@@ -233,6 +233,50 @@ def _validate_uuid(data, valid_values=None):
         return msg
 
 
+def _validate_uuid_or_none(data, valid_values=None):
+    if data is not None:
+        return _validate_uuid(data)
+
+
+def _validate_uuid_list(data, valid_values=None):
+    if not isinstance(data, list):
+        msg = _("'%s' is not a list") % data
+        LOG.debug("validate_uuid_list: %s", msg)
+        return msg
+
+    for item in data:
+        msg = _validate_uuid(item)
+        if msg:
+            LOG.debug("validate_uuid_list: %s", msg)
+            return msg
+
+    if len(set(data)) != len(data):
+        msg = _("Duplicate items in the list: %s") % ', '.join(data)
+        LOG.debug("validate_uuid_list: %s", msg)
+        return msg
+
+
+def _validate_dict(data, valid_values=None):
+    if not isinstance(data, dict):
+        msg = _("'%s' is not a dictionary") % data
+        LOG.debug("validate_dict: %s", msg)
+        return msg
+
+
+def _validate_non_negative(data, valid_values=None):
+    try:
+        data = int(data)
+    except (ValueError, TypeError):
+        msg = _("'%s' is not an integer") % data
+        LOG.debug("validate_non_negative: %s", msg)
+        return msg
+
+    if data < 0:
+        msg = _("'%s' should be non-negative") % data
+        LOG.debug("validate_non_negative: %s", msg)
+        return msg
+
+
 def convert_to_boolean(data):
     if isinstance(data, basestring):
         val = data.lower()
@@ -294,6 +338,15 @@ def convert_none_to_empty_list(value):
     return [] if value is None else value
 
 
+def convert_to_list(data):
+    if data is None:
+        return []
+    elif hasattr(data, '__iter__'):
+        return list(data)
+    else:
+        return [data]
+
+
 HOSTNAME_PATTERN = ("(?=^.{1,254}$)(^(?:(?!\d+\.|-)[a-zA-Z0-9_\-]"
                     "{1,63}(?<!-)\.?)+(?:[a-zA-Z]{2,})$)")
 
@@ -306,18 +359,22 @@ UUID_PATTERN = '-'.join([HEX_ELEM + '{8}', HEX_ELEM + '{4}',
 MAC_PATTERN = "^%s[aceACE02468](:%s{2}){5}$" % (HEX_ELEM, HEX_ELEM)
 
 # Dictionary that maintains a list of validation functions
-validators = {'type:fixed_ips': _validate_fixed_ips,
+validators = {'type:dict': _validate_dict,
+              'type:fixed_ips': _validate_fixed_ips,
               'type:hostroutes': _validate_hostroutes,
               'type:ip_address': _validate_ip_address,
               'type:ip_address_or_none': _validate_ip_address_or_none,
               'type:ip_pools': _validate_ip_pools,
               'type:mac_address': _validate_mac_address,
               'type:nameservers': _validate_nameservers,
+              'type:non_negative': _validate_non_negative,
               'type:range': _validate_range,
               'type:regex': _validate_regex,
               'type:string': _validate_string,
               'type:subnet': _validate_subnet,
               'type:uuid': _validate_uuid,
+              'type:uuid_or_none': _validate_uuid_or_none,
+              'type:uuid_list': _validate_uuid_list,
               'type:values': _validate_values}
 
 # Note: a default of ATTR_NOT_SPECIFIED indicates that an
index 0ddb9c1574ffec45ea837e7643527e2b77a7f8c9..0b65b9a52d29218979a29242bc2b292d155aa564 100644 (file)
@@ -165,8 +165,14 @@ class Controller(object):
 
     def __getattr__(self, name):
         if name in self._member_actions:
-            def _handle_action(request, id, body=None):
-                return getattr(self._plugin, name)(request.context, id, body)
+            def _handle_action(request, id, **kwargs):
+                if 'body' in kwargs:
+                    body = kwargs.pop('body')
+                    return getattr(self._plugin, name)(request.context, id,
+                                                       body, **kwargs)
+                else:
+                    return getattr(self._plugin, name)(request.context, id,
+                                                       **kwargs)
             return _handle_action
         else:
             raise AttributeError
index 7d110f897396b80e05208e23784b29ca18745008..63f03862cb1494844320ac23947a9ece606b1b70 100644 (file)
@@ -86,13 +86,6 @@ class RouterExternalGatewayInUseByFloatingIp(qexception.InUse):
                 "more floating IPs.")
 
 
-def _validate_uuid_or_none(data, valid_values=None):
-    if data is None:
-        return None
-    return attr._validate_uuid(data)
-
-attr.validators['type:uuid_or_none'] = _validate_uuid_or_none
-
 # Attribute Map
 RESOURCE_ATTRIBUTE_MAP = {
     'routers': {
diff --git a/quantum/extensions/loadbalancer.py b/quantum/extensions/loadbalancer.py
new file mode 100644 (file)
index 0000000..58822da
--- /dev/null
@@ -0,0 +1,383 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 OpenStack LLC.
+# 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.
+
+import abc
+
+from quantum.api import extensions
+from quantum.api.v2 import attributes as attr
+from quantum.api.v2 import base
+from quantum import manager
+from quantum.plugins.common import constants
+from quantum.plugins.services.service_base import ServicePluginBase
+
+
+RESOURCE_ATTRIBUTE_MAP = {
+    'vips': {
+        'id': {'allow_post': False, 'allow_put': False,
+               'validate': {'type:uuid': None},
+               'is_visible': True},
+        'tenant_id': {'allow_post': True, 'allow_put': False,
+                      'validate': {'type:string': None},
+                      'required_by_policy': True,
+                      'is_visible': True},
+        'name': {'allow_post': True, 'allow_put': True,
+                 'validate': {'type:string': None},
+                 'is_visible': True},
+        'description': {'allow_post': True, 'allow_put': True,
+                        'validate': {'type:string': None},
+                        'is_visible': True, 'default': ''},
+        'subnet_id': {'allow_post': True, 'allow_put': False,
+                      'validate': {'type:uuid': None},
+                      'is_visible': True},
+        'address': {'allow_post': True, 'allow_put': False,
+                    'default': attr.ATTR_NOT_SPECIFIED,
+                    'validate': {'type:ip_address_or_none': None},
+                    'is_visible': True},
+        'port': {'allow_post': True, 'allow_put': False,
+                 'validate': {'type:range': [0, 65535]},
+                 'convert_to': attr.convert_to_int,
+                 'is_visible': True},
+        'protocol': {'allow_post': True, 'allow_put': False,
+                     'validate': {'type:string': None},
+                     'is_visible': True},
+        'pool_id': {'allow_post': True, 'allow_put': True,
+                    'validate': {'type:uuid': None},
+                    'is_visible': True},
+        'session_persistence': {'allow_post': True, 'allow_put': True,
+                                'default': {},
+                                'validate': {'type:dict': None},
+                                'is_visible': True},
+        'connection_limit': {'allow_post': True, 'allow_put': True,
+                             'default': -1,
+                             'convert_to': attr.convert_to_int,
+                             'is_visible': True},
+        'admin_state_up': {'allow_post': True, 'allow_put': True,
+                           'default': True,
+                           'convert_to': attr.convert_to_boolean,
+                           'is_visible': True},
+        'status': {'allow_post': False, 'allow_put': False,
+                   'is_visible': True}
+    },
+    'pools': {
+        'id': {'allow_post': False, 'allow_put': False,
+               'validate': {'type:uuid': None},
+               'is_visible': True},
+        'tenant_id': {'allow_post': True, 'allow_put': False,
+                      'validate': {'type:string': None},
+                      'required_by_policy': True,
+                      'is_visible': True},
+        'vip_id': {'allow_post': False, 'allow_put': False,
+                   'is_visible': True},
+        'name': {'allow_post': True, 'allow_put': True,
+                 'validate': {'type:string': None},
+                 'is_visible': True},
+        'description': {'allow_post': True, 'allow_put': True,
+                        'validate': {'type:string': None},
+                        'is_visible': True, 'default': ''},
+        'subnet_id': {'allow_post': True, 'allow_put': False,
+                      'validate': {'type:uuid': None},
+                      'is_visible': True},
+        'protocol': {'allow_post': True, 'allow_put': False,
+                     'validate': {'type:string': None},
+                     'is_visible': True},
+        'lb_method': {'allow_post': True, 'allow_put': True,
+                      'validate': {'type:string': None},
+                      'is_visible': True},
+        'members': {'allow_post': False, 'allow_put': False,
+                    'is_visible': True},
+        'health_monitors': {'allow_post': True, 'allow_put': True,
+                            'default': None,
+                            'validate': {'type:uuid_list': None},
+                            'convert_to': attr.convert_to_list,
+                            'is_visible': True},
+        'admin_state_up': {'allow_post': True, 'allow_put': True,
+                           'default': True,
+                           'convert_to': attr.convert_to_boolean,
+                           'is_visible': True},
+        'status': {'allow_post': False, 'allow_put': False,
+                   'is_visible': True}
+    },
+    'members': {
+        'id': {'allow_post': False, 'allow_put': False,
+               'validate': {'type:uuid': None},
+               'is_visible': True},
+        'tenant_id': {'allow_post': True, 'allow_put': False,
+                      'validate': {'type:string': None},
+                      'required_by_policy': True,
+                      'is_visible': True},
+        'pool_id': {'allow_post': True, 'allow_put': True,
+                    'validate': {'type:uuid': None},
+                    'is_visible': True},
+        'address': {'allow_post': True, 'allow_put': False,
+                    'validate': {'type:ip_address': None},
+                    'is_visible': True},
+        'port': {'allow_post': True, 'allow_put': False,
+                 'validate': {'type:range': [0, 65535]},
+                 'convert_to': attr.convert_to_int,
+                 'is_visible': True},
+        'weight': {'allow_post': True, 'allow_put': True,
+                   'default': 1,
+                   'validate': {'type:range': [0, 256]},
+                   'convert_to': attr.convert_to_int,
+                   'is_visible': True},
+        'admin_state_up': {'allow_post': True, 'allow_put': True,
+                           'default': True,
+                           'convert_to': attr.convert_to_boolean,
+                           'is_visible': True},
+        'status': {'allow_post': False, 'allow_put': False,
+                   'is_visible': True}
+    },
+    'health_monitors': {
+        'id': {'allow_post': False, 'allow_put': False,
+               'validate': {'type:uuid': None},
+               'is_visible': True},
+        'tenant_id': {'allow_post': True, 'allow_put': False,
+                      'validate': {'type:string': None},
+                      'required_by_policy': True,
+                      'is_visible': True},
+        'type': {'allow_post': True, 'allow_put': False,
+                 'validate': {'type:values': ['PING', 'TCP', 'HTTP', 'HTTPS']},
+                 'is_visible': True},
+        'delay': {'allow_post': True, 'allow_put': True,
+                  'validate': {'type:non_negative': None},
+                  'convert_to': attr.convert_to_int,
+                  'is_visible': True},
+        'timeout': {'allow_post': True, 'allow_put': True,
+                    'convert_to': attr.convert_to_int,
+                    'is_visible': True},
+        'max_retries': {'allow_post': True, 'allow_put': True,
+                        'validate': {'type:range': [1, 10]},
+                        'convert_to': attr.convert_to_int,
+                        'is_visible': True},
+        'http_method': {'allow_post': True, 'allow_put': True,
+                        'validate': {'type:string': None},
+                        'default': 'GET',
+                        'is_visible': True},
+        'url_path': {'allow_post': True, 'allow_put': True,
+                     'validate': {'type:string': None},
+                     'default': '/',
+                     'is_visible': True},
+        'expected_codes': {'allow_post': True, 'allow_put': True,
+                           'validate': {
+                               'type:regex':
+                               '^(\d{3}(\s*,\s*\d{3})*)$|^(\d{3}-\d{3})$'},
+                           'default': '200',
+                           'is_visible': True},
+        'admin_state_up': {'allow_post': True, 'allow_put': True,
+                           'default': True,
+                           'convert_to': attr.convert_to_boolean,
+                           'is_visible': True},
+        'status': {'allow_post': False, 'allow_put': False,
+                   'is_visible': True}
+    }
+}
+
+SUB_RESOURCE_ATTRIBUTE_MAP = {
+    'health_monitors': {
+        'parent': {'collection_name': 'pools',
+                   'member_name': 'pool'},
+        'parameters': {'id': {'allow_post': True, 'allow_put': False,
+                              'validate': {'type:uuid': None},
+                              'is_visible': True},
+                       'tenant_id': {'allow_post': True, 'allow_put': False,
+                                     'validate': {'type:string': None},
+                                     'required_by_policy': True,
+                                     'is_visible': True},
+                       }
+    }
+}
+
+
+class Loadbalancer(extensions.ExtensionDescriptor):
+
+    @classmethod
+    def get_name(cls):
+        return "LoadBalancing service"
+
+    @classmethod
+    def get_alias(cls):
+        return "lbaas"
+
+    @classmethod
+    def get_description(cls):
+        return "Extension for LoadBalancing service"
+
+    @classmethod
+    def get_namespace(cls):
+        return "http://wiki.openstack.org/Quantum/LBaaS/API_1.0"
+
+    @classmethod
+    def get_updated(cls):
+        return "2012-10-07T10:00:00-00:00"
+
+    @classmethod
+    def get_resources(cls):
+        resources = []
+        plugin = manager.QuantumManager.get_service_plugins()[
+            constants.LOADBALANCER]
+        for collection_name in RESOURCE_ATTRIBUTE_MAP:
+            # Special handling needed for resources with 'y' ending
+            # (e.g. proxies -> proxy)
+            resource_name = collection_name[:-1]
+            params = RESOURCE_ATTRIBUTE_MAP[collection_name]
+
+            member_actions = {}
+            if resource_name == 'pool':
+                member_actions = {'stats': 'GET'}
+
+            controller = base.create_resource(collection_name,
+                                              resource_name,
+                                              plugin, params,
+                                              member_actions=member_actions)
+
+            resource = extensions.ResourceExtension(
+                collection_name,
+                controller,
+                path_prefix=constants.COMMON_PREFIXES[constants.LOADBALANCER],
+                member_actions=member_actions)
+            resources.append(resource)
+
+        for collection_name in SUB_RESOURCE_ATTRIBUTE_MAP:
+            # Special handling needed for sub-resources with 'y' ending
+            # (e.g. proxies -> proxy)
+            resource_name = collection_name[:-1]
+            parent = SUB_RESOURCE_ATTRIBUTE_MAP[collection_name].get('parent')
+            params = SUB_RESOURCE_ATTRIBUTE_MAP[collection_name].get(
+                'parameters')
+
+            controller = base.create_resource(collection_name, resource_name,
+                                              plugin, params,
+                                              allow_bulk=True,
+                                              parent=parent)
+
+            resource = extensions.ResourceExtension(
+                collection_name,
+                controller, parent,
+                path_prefix=constants.COMMON_PREFIXES[constants.LOADBALANCER])
+            resources.append(resource)
+
+        return resources
+
+    @classmethod
+    def get_plugin_interface(cls):
+        return LoadBalancerPluginBase
+
+
+class LoadBalancerPluginBase(ServicePluginBase):
+    __metaclass__ = abc.ABCMeta
+
+    def get_plugin_type(self):
+        return constants.LOADBALANCER
+
+    def get_plugin_description(self):
+        return 'LoadBalancer service plugin'
+
+    @abc.abstractmethod
+    def get_vips(self, context, filters=None, fields=None):
+        pass
+
+    @abc.abstractmethod
+    def get_vip(self, context, id, fields=None):
+        pass
+
+    @abc.abstractmethod
+    def create_vip(self, context, vip):
+        pass
+
+    @abc.abstractmethod
+    def update_vip(self, context, id, vip):
+        pass
+
+    @abc.abstractmethod
+    def delete_vip(self, context, id):
+        pass
+
+    @abc.abstractmethod
+    def get_pools(self, context, filters=None, fields=None):
+        pass
+
+    @abc.abstractmethod
+    def get_pool(self, context, id, fields=None):
+        pass
+
+    @abc.abstractmethod
+    def create_pool(self, context, pool):
+        pass
+
+    @abc.abstractmethod
+    def update_pool(self, context, id, pool):
+        pass
+
+    @abc.abstractmethod
+    def delete_pool(self, context, id):
+        pass
+
+    @abc.abstractmethod
+    def stats(self, context, pool_id):
+        pass
+
+    @abc.abstractmethod
+    def create_pool_health_monitor(self, context, health_monitor, pool_id):
+        pass
+
+    @abc.abstractmethod
+    def get_pool_health_monitor(self, context, id, pool_id, fields=None):
+        pass
+
+    @abc.abstractmethod
+    def delete_pool_health_monitor(self, context, id, pool_id):
+        pass
+
+    @abc.abstractmethod
+    def get_members(self, context, filters=None, fields=None):
+        pass
+
+    @abc.abstractmethod
+    def get_member(self, context, id, fields=None):
+        pass
+
+    @abc.abstractmethod
+    def create_member(self, context, member):
+        pass
+
+    @abc.abstractmethod
+    def update_member(self, context, id, member):
+        pass
+
+    @abc.abstractmethod
+    def delete_member(self, context, id):
+        pass
+
+    @abc.abstractmethod
+    def get_health_monitors(self, context, filters=None, fields=None):
+        pass
+
+    @abc.abstractmethod
+    def get_health_monitor(self, context, id, fields=None):
+        pass
+
+    @abc.abstractmethod
+    def create_health_monitor(self, context, health_monitor):
+        pass
+
+    @abc.abstractmethod
+    def update_health_monitor(self, context, id, health_monitor):
+        pass
+
+    @abc.abstractmethod
+    def delete_health_monitor(self, context, id):
+        pass
index 59da9d82eda5490b3cbc7564f056ed55c499e8f2..ac39d4bc9157980ef0f9fcfb4dd123cacf264bea 100644 (file)
 # service type constants:
 CORE = "CORE"
 DUMMY = "DUMMY"
+LOADBALANCER = "LOADBALANCER"
 
 
 COMMON_PREFIXES = {
     CORE: "",
     DUMMY: "/dummy_svc",
+    LOADBALANCER: "/lb",
 }
index dfa074d4ef2cbe0b857b893166763634ad7a416c..0d0daee97c12b8acf0100820fabfd58209b9b528 100644 (file)
 
 import abc
 
+from quantum.api import extensions
 
-class ServicePluginBase(object):
+
+class ServicePluginBase(extensions.PluginInterface):
     """ defines base interface for any Advanced Service plugin """
     __metaclass__ = abc.ABCMeta
     supported_extension_aliases = []
index 8b470b9009143174ca451e4da257db614b7c6320..a4e2d06b38d47dca4f6f498135fd98e6c7928578 100644 (file)
@@ -371,6 +371,69 @@ class TestAttributes(unittest2.TestCase):
         msg = attributes._validate_uuid('00000000-ffff-ffff-ffff-000000000000')
         self.assertIsNone(msg)
 
+    def test_validate_uuid_list(self):
+        # check not a list
+        uuids = [None,
+                 123,
+                 'e5069610-744b-42a7-8bd8-ceac1a229cd4',
+                 '12345678123456781234567812345678',
+                 {'uuid': 'e5069610-744b-42a7-8bd8-ceac1a229cd4'}]
+        for uuid in uuids:
+            msg = attributes._validate_uuid_list(uuid)
+            error = "'%s' is not a list" % uuid
+            self.assertEquals(msg, error)
+
+        # check invalid uuid in a list
+        invalid_uuid_lists = [[None],
+                              [123],
+                              [123, 'e5069610-744b-42a7-8bd8-ceac1a229cd4'],
+                              ['123', '12345678123456781234567812345678'],
+                              ['t5069610-744b-42a7-8bd8-ceac1a229cd4'],
+                              ['e5069610-744b-42a7-8bd8-ceac1a229cd44'],
+                              ['e50696100-744b-42a7-8bd8-ceac1a229cd4'],
+                              ['e5069610-744bb-42a7-8bd8-ceac1a229cd4']]
+        for uuid_list in invalid_uuid_lists:
+            msg = attributes._validate_uuid_list(uuid_list)
+            error = "'%s' is not a valid UUID" % uuid_list[0]
+            self.assertEquals(msg, error)
+
+        # check duplicate items in a list
+        duplicate_uuids = ['e5069610-744b-42a7-8bd8-ceac1a229cd4',
+                           'f3eeab00-8367-4524-b662-55e64d4cacb5',
+                           'e5069610-744b-42a7-8bd8-ceac1a229cd4']
+        msg = attributes._validate_uuid_list(duplicate_uuids)
+        error = "Duplicate items in the list: %s" % ', '.join(duplicate_uuids)
+        self.assertEquals(msg, error)
+
+        # check valid uuid lists
+        valid_uuid_lists = [['e5069610-744b-42a7-8bd8-ceac1a229cd4'],
+                            ['f3eeab00-8367-4524-b662-55e64d4cacb5'],
+                            ['e5069610-744b-42a7-8bd8-ceac1a229cd4',
+                             'f3eeab00-8367-4524-b662-55e64d4cacb5']]
+        for uuid_list in valid_uuid_lists:
+            msg = attributes._validate_uuid_list(uuid_list)
+            self.assertEquals(msg, None)
+
+    def test_validate_dict(self):
+        for value in (None, True, '1', []):
+            self.assertEquals(attributes._validate_dict(value),
+                              "'%s' is not a dictionary" % value)
+
+        msg = attributes._validate_dict({})
+        self.assertIsNone(msg)
+
+        msg = attributes._validate_dict({'key': 'value'})
+        self.assertIsNone(msg)
+
+    def test_validate_non_negative(self):
+        for value in (-1, '-2'):
+            self.assertEquals(attributes._validate_non_negative(value),
+                              "'%s' should be non-negative" % value)
+
+        for value in (0, 1, '2', True, False):
+            msg = attributes._validate_non_negative(value)
+            self.assertIsNone(msg)
+
 
 class TestConvertToBoolean(unittest2.TestCase):
 
@@ -457,3 +520,22 @@ class TestConvertKvp(unittest2.TestCase):
     def test_convert_kvp_str_to_list_succeeds_for_two_equals(self):
         result = attributes.convert_kvp_str_to_list('a=a=a')
         self.assertEqual(['a', 'a=a'], result)
+
+
+class TestConvertToList(unittest2.TestCase):
+
+    def test_convert_to_empty_list(self):
+        for item in (None, [], (), {}):
+            self.assertEquals(attributes.convert_to_list(item), [])
+
+    def test_convert_to_list_string(self):
+        for item in ('', 'foo'):
+            self.assertEquals(attributes.convert_to_list(item), [item])
+
+    def test_convert_to_list_iterable(self):
+        for item in ([None], [1, 2, 3], (1, 2, 3), set([1, 2, 3]), ['foo']):
+            self.assertEquals(attributes.convert_to_list(item), list(item))
+
+    def test_convert_to_list_non_iterable(self):
+        for item in (True, False, 1, 1.2, object()):
+            self.assertEquals(attributes.convert_to_list(item), [item])
diff --git a/quantum/tests/unit/test_loadbalancer_plugin.py b/quantum/tests/unit/test_loadbalancer_plugin.py
new file mode 100644 (file)
index 0000000..f3c3871
--- /dev/null
@@ -0,0 +1,477 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 OpenStack LLC.
+# 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 spec
+
+import copy
+import mock
+from webob import exc
+import webtest
+import unittest2
+
+from quantum.api import extensions
+from quantum.common import config
+from quantum.extensions import loadbalancer
+from quantum import manager
+from quantum.openstack.common import cfg
+from quantum.openstack.common import uuidutils
+from quantum.plugins.common import constants
+from quantum.tests.unit import test_api_v2
+from quantum.tests.unit import test_extensions
+
+
+_uuid = uuidutils.generate_uuid
+_get_path = test_api_v2._get_path
+
+
+class LoadBalancerTestExtensionManager(object):
+
+    def get_resources(self):
+        return loadbalancer.Loadbalancer.get_resources()
+
+    def get_actions(self):
+        return []
+
+    def get_request_extensions(self):
+        return []
+
+
+class LoadBalancerExtensionTestCase(unittest2.TestCase):
+
+    def setUp(self):
+
+        plugin = 'quantum.extensions.loadbalancer.LoadBalancerPluginBase'
+        # Ensure 'stale' patched copies of the plugin are never returned
+        manager.QuantumManager._instance = None
+
+        # Ensure existing ExtensionManager is not used
+        extensions.PluginAwareExtensionManager._instance = None
+
+        # Create the default configurations
+        args = ['--config-file', test_api_v2.etcdir('quantum.conf.test')]
+        config.parse(args)
+
+        #just stubbing core plugin with LoadBalancer plugin
+        cfg.CONF.set_override('core_plugin', plugin)
+        cfg.CONF.set_override('service_plugins', [plugin])
+
+        self._plugin_patcher = mock.patch(plugin, autospec=True)
+        self.plugin = self._plugin_patcher.start()
+        instance = self.plugin.return_value
+        instance.get_plugin_type.return_value = constants.LOADBALANCER
+
+        ext_mgr = LoadBalancerTestExtensionManager()
+        self.ext_mdw = test_extensions.setup_extensions_middleware(ext_mgr)
+        self.api = webtest.TestApp(self.ext_mdw)
+
+    def tearDown(self):
+        self._plugin_patcher.stop()
+        self.api = None
+        self.plugin = None
+        cfg.CONF.reset()
+
+    def test_vip_create(self):
+        vip_id = _uuid()
+        data = {'vip': {'name': 'vip1',
+                        'description': 'descr_vip1',
+                        'subnet_id': _uuid(),
+                        'address': '127.0.0.1',
+                        'port': 80,
+                        'protocol': 'HTTP',
+                        'pool_id': _uuid(),
+                        'session_persistence': {'type': 'dummy'},
+                        'connection_limit': 100,
+                        'admin_state_up': True,
+                        'tenant_id': _uuid()}}
+        return_value = copy.copy(data['vip'])
+        return_value.update({'status': "ACTIVE", 'id': vip_id})
+
+        instance = self.plugin.return_value
+        instance.create_vip.return_value = return_value
+        res = self.api.post_json(_get_path('lb/vips'), data)
+        instance.create_vip.assert_called_with(mock.ANY,
+                                               vip=data)
+        self.assertEqual(res.status_int, exc.HTTPCreated.code)
+        self.assertTrue('vip' in res.json)
+        self.assertEqual(res.json['vip'], return_value)
+
+    def test_vip_list(self):
+        vip_id = _uuid()
+        return_value = [{'name': 'vip1',
+                         'admin_state_up': True,
+                         'tenant_id': _uuid(),
+                         'id': vip_id}]
+
+        instance = self.plugin.return_value
+        instance.get_vips.return_value = return_value
+
+        res = self.api.get(_get_path('lb/vips'))
+
+        instance.get_vips.assert_called_with(mock.ANY, fields=mock.ANY,
+                                             filters=mock.ANY)
+        self.assertEqual(res.status_int, exc.HTTPOk.code)
+
+    def test_vip_update(self):
+        vip_id = _uuid()
+        update_data = {'vip': {'admin_state_up': False}}
+        return_value = {'name': 'vip1',
+                        'admin_state_up': False,
+                        'tenant_id': _uuid(),
+                        'status': "ACTIVE",
+                        'id': vip_id}
+
+        instance = self.plugin.return_value
+        instance.update_vip.return_value = return_value
+
+        res = self.api.put_json(_get_path('lb/vips', id=vip_id),
+                                update_data)
+
+        instance.update_vip.assert_called_with(mock.ANY, vip_id,
+                                               vip=update_data)
+        self.assertEqual(res.status_int, exc.HTTPOk.code)
+        self.assertTrue('vip' in res.json)
+        self.assertEqual(res.json['vip'], return_value)
+
+    def test_vip_get(self):
+        vip_id = _uuid()
+        return_value = {'name': 'vip1',
+                        'admin_state_up': False,
+                        'tenant_id': _uuid(),
+                        'status': "ACTIVE",
+                        'id': vip_id}
+
+        instance = self.plugin.return_value
+        instance.get_vip.return_value = return_value
+
+        res = self.api.get(_get_path('lb/vips', id=vip_id))
+
+        instance.get_vip.assert_called_with(mock.ANY, vip_id,
+                                            fields=mock.ANY)
+        self.assertEqual(res.status_int, exc.HTTPOk.code)
+        self.assertTrue('vip' in res.json)
+        self.assertEqual(res.json['vip'], return_value)
+
+    def test_vip_delete(self):
+        vip_id = _uuid()
+
+        res = self.api.delete(_get_path('lb/vips', id=vip_id))
+
+        instance = self.plugin.return_value
+        instance.delete_vip.assert_called_with(mock.ANY, vip_id)
+        self.assertEqual(res.status_int, exc.HTTPNoContent.code)
+
+    def test_pool_create(self):
+        pool_id = _uuid()
+        hm_id = _uuid()
+        data = {'pool': {'name': 'pool1',
+                         'description': 'descr_pool1',
+                         'subnet_id': _uuid(),
+                         'protocol': 'HTTP',
+                         'lb_method': 'ROUND_ROBIN',
+                         'health_monitors': [hm_id],
+                         'admin_state_up': True,
+                         'tenant_id': _uuid()}}
+        return_value = copy.copy(data['pool'])
+        return_value.update({'status': "ACTIVE", 'id': pool_id})
+
+        instance = self.plugin.return_value
+        instance.create_pool.return_value = return_value
+        res = self.api.post_json(_get_path('lb/pools'), data)
+        instance.create_pool.assert_called_with(mock.ANY,
+                                                pool=data)
+        self.assertEqual(res.status_int, exc.HTTPCreated.code)
+        self.assertTrue('pool' in res.json)
+        self.assertEqual(res.json['pool'], return_value)
+
+    def test_pool_list(self):
+        pool_id = _uuid()
+        return_value = [{'name': 'pool1',
+                         'admin_state_up': True,
+                         'tenant_id': _uuid(),
+                         'id': pool_id}]
+
+        instance = self.plugin.return_value
+        instance.get_pools.return_value = return_value
+
+        res = self.api.get(_get_path('lb/pools'))
+
+        instance.get_pools.assert_called_with(mock.ANY, fields=mock.ANY,
+                                              filters=mock.ANY)
+        self.assertEqual(res.status_int, exc.HTTPOk.code)
+
+    def test_pool_update(self):
+        pool_id = _uuid()
+        update_data = {'pool': {'admin_state_up': False}}
+        return_value = {'name': 'pool1',
+                        'admin_state_up': False,
+                        'tenant_id': _uuid(),
+                        'status': "ACTIVE",
+                        'id': pool_id}
+
+        instance = self.plugin.return_value
+        instance.update_pool.return_value = return_value
+
+        res = self.api.put_json(_get_path('lb/pools', id=pool_id),
+                                update_data)
+
+        instance.update_pool.assert_called_with(mock.ANY, pool_id,
+                                                pool=update_data)
+        self.assertEqual(res.status_int, exc.HTTPOk.code)
+        self.assertTrue('pool' in res.json)
+        self.assertEqual(res.json['pool'], return_value)
+
+    def test_pool_get(self):
+        pool_id = _uuid()
+        return_value = {'name': 'pool1',
+                        'admin_state_up': False,
+                        'tenant_id': _uuid(),
+                        'status': "ACTIVE",
+                        'id': pool_id}
+
+        instance = self.plugin.return_value
+        instance.get_pool.return_value = return_value
+
+        res = self.api.get(_get_path('lb/pools', id=pool_id))
+
+        instance.get_pool.assert_called_with(mock.ANY, pool_id,
+                                             fields=mock.ANY)
+        self.assertEqual(res.status_int, exc.HTTPOk.code)
+        self.assertTrue('pool' in res.json)
+        self.assertEqual(res.json['pool'], return_value)
+
+    def test_pool_delete(self):
+        pool_id = _uuid()
+
+        res = self.api.delete(_get_path('lb/pools', id=pool_id))
+
+        instance = self.plugin.return_value
+        instance.delete_pool.assert_called_with(mock.ANY, pool_id)
+        self.assertEqual(res.status_int, exc.HTTPNoContent.code)
+
+    def test_pool_stats(self):
+        pool_id = _uuid()
+
+        stats = {'stats': 'dummy'}
+        instance = self.plugin.return_value
+        instance.stats.return_value = stats
+
+        path = _get_path('lb/pools', id=pool_id,
+                         action="stats")
+        res = self.api.get(path)
+
+        instance.stats.assert_called_with(mock.ANY, pool_id)
+        self.assertEqual(res.status_int, exc.HTTPOk.code)
+        self.assertTrue('stats' in res.json)
+        self.assertEqual(res.json['stats'], stats['stats'])
+
+    def test_member_create(self):
+        member_id = _uuid()
+        data = {'member': {'pool_id': _uuid(),
+                           'address': '127.0.0.1',
+                           'port': 80,
+                           'weight': 1,
+                           'admin_state_up': True,
+                           'tenant_id': _uuid()}}
+        return_value = copy.copy(data['member'])
+        return_value.update({'status': "ACTIVE", 'id': member_id})
+
+        instance = self.plugin.return_value
+        instance.create_member.return_value = return_value
+        res = self.api.post_json(_get_path('lb/members'), data)
+        instance.create_member.assert_called_with(mock.ANY,
+                                                  member=data)
+        self.assertEqual(res.status_int, exc.HTTPCreated.code)
+        self.assertTrue('member' in res.json)
+        self.assertEqual(res.json['member'], return_value)
+
+    def test_member_list(self):
+        member_id = _uuid()
+        return_value = [{'name': 'member1',
+                         'admin_state_up': True,
+                         'tenant_id': _uuid(),
+                         'id': member_id}]
+
+        instance = self.plugin.return_value
+        instance.get_members.return_value = return_value
+
+        res = self.api.get(_get_path('lb/members'))
+
+        instance.get_members.assert_called_with(mock.ANY, fields=mock.ANY,
+                                                filters=mock.ANY)
+        self.assertEqual(res.status_int, exc.HTTPOk.code)
+
+    def test_member_update(self):
+        member_id = _uuid()
+        update_data = {'member': {'admin_state_up': False}}
+        return_value = {'admin_state_up': False,
+                        'tenant_id': _uuid(),
+                        'status': "ACTIVE",
+                        'id': member_id}
+
+        instance = self.plugin.return_value
+        instance.update_member.return_value = return_value
+
+        res = self.api.put_json(_get_path('lb/members', id=member_id),
+                                update_data)
+
+        instance.update_member.assert_called_with(mock.ANY, member_id,
+                                                  member=update_data)
+        self.assertEqual(res.status_int, exc.HTTPOk.code)
+        self.assertTrue('member' in res.json)
+        self.assertEqual(res.json['member'], return_value)
+
+    def test_member_get(self):
+        member_id = _uuid()
+        return_value = {'admin_state_up': False,
+                        'tenant_id': _uuid(),
+                        'status': "ACTIVE",
+                        'id': member_id}
+
+        instance = self.plugin.return_value
+        instance.get_member.return_value = return_value
+
+        res = self.api.get(_get_path('lb/members', id=member_id))
+
+        instance.get_member.assert_called_with(mock.ANY, member_id,
+                                               fields=mock.ANY)
+        self.assertEqual(res.status_int, exc.HTTPOk.code)
+        self.assertTrue('member' in res.json)
+        self.assertEqual(res.json['member'], return_value)
+
+    def test_member_delete(self):
+        member_id = _uuid()
+
+        res = self.api.delete(_get_path('lb/members', id=member_id))
+
+        instance = self.plugin.return_value
+        instance.delete_member.assert_called_with(mock.ANY, member_id)
+        self.assertEqual(res.status_int, exc.HTTPNoContent.code)
+
+    def test_health_monitor_create(self):
+        health_monitor_id = _uuid()
+        data = {'health_monitor': {'type': 'HTTP',
+                                   'delay': 2,
+                                   'timeout': 1,
+                                   'max_retries': 3,
+                                   'http_method': 'GET',
+                                   'url_path': '/path',
+                                   'expected_codes': '200-300',
+                                   'admin_state_up': True,
+                                   'tenant_id': _uuid()}}
+        return_value = copy.copy(data['health_monitor'])
+        return_value.update({'status': "ACTIVE", 'id': health_monitor_id})
+
+        instance = self.plugin.return_value
+        instance.create_health_monitor.return_value = return_value
+        res = self.api.post_json(_get_path('lb/health_monitors'), data)
+        instance.create_health_monitor.assert_called_with(mock.ANY,
+                                                          health_monitor=data)
+        self.assertEqual(res.status_int, exc.HTTPCreated.code)
+        self.assertTrue('health_monitor' in res.json)
+        self.assertEqual(res.json['health_monitor'], return_value)
+
+    def test_health_monitor_list(self):
+        health_monitor_id = _uuid()
+        return_value = [{'type': 'HTTP',
+                         'admin_state_up': True,
+                         'tenant_id': _uuid(),
+                         'id': health_monitor_id}]
+
+        instance = self.plugin.return_value
+        instance.get_health_monitors.return_value = return_value
+
+        res = self.api.get(_get_path('lb/health_monitors'))
+
+        instance.get_health_monitors.assert_called_with(
+            mock.ANY, fields=mock.ANY, filters=mock.ANY)
+        self.assertEqual(res.status_int, exc.HTTPOk.code)
+
+    def test_health_monitor_update(self):
+        health_monitor_id = _uuid()
+        update_data = {'health_monitor': {'admin_state_up': False}}
+        return_value = {'type': 'HTTP',
+                        'admin_state_up': False,
+                        'tenant_id': _uuid(),
+                        'status': "ACTIVE",
+                        'id': health_monitor_id}
+
+        instance = self.plugin.return_value
+        instance.update_health_monitor.return_value = return_value
+
+        res = self.api.put_json(_get_path('lb/health_monitors',
+                                          id=health_monitor_id),
+                                update_data)
+
+        instance.update_health_monitor.assert_called_with(
+            mock.ANY, health_monitor_id, health_monitor=update_data)
+        self.assertEqual(res.status_int, exc.HTTPOk.code)
+        self.assertTrue('health_monitor' in res.json)
+        self.assertEqual(res.json['health_monitor'], return_value)
+
+    def test_health_monitor_get(self):
+        health_monitor_id = _uuid()
+        return_value = {'type': 'HTTP',
+                        'admin_state_up': False,
+                        'tenant_id': _uuid(),
+                        'status': "ACTIVE",
+                        'id': health_monitor_id}
+
+        instance = self.plugin.return_value
+        instance.get_health_monitor.return_value = return_value
+
+        res = self.api.get(_get_path('lb/health_monitors',
+                                     id=health_monitor_id))
+
+        instance.get_health_monitor.assert_called_with(
+            mock.ANY, health_monitor_id, fields=mock.ANY)
+        self.assertEqual(res.status_int, exc.HTTPOk.code)
+        self.assertTrue('health_monitor' in res.json)
+        self.assertEqual(res.json['health_monitor'], return_value)
+
+    def test_health_monitor_delete(self):
+        health_monitor_id = _uuid()
+
+        res = self.api.delete(_get_path('lb/health_monitors',
+                                        id=health_monitor_id))
+
+        instance = self.plugin.return_value
+        instance.delete_health_monitor.assert_called_with(mock.ANY,
+                                                          health_monitor_id)
+        self.assertEqual(res.status_int, exc.HTTPNoContent.code)
+
+    def test_create_pool_health_monitor(self):
+        health_monitor_id = _uuid()
+        data = {'health_monitor': {'id': health_monitor_id,
+                                   'tenant_id': _uuid()}}
+
+        return_value = copy.copy(data['health_monitor'])
+        instance = self.plugin.return_value
+        instance.create_pool_health_monitor.return_value = return_value
+        res = self.api.post_json('/lb/pools/id1/health_monitors', data)
+        instance.create_pool_health_monitor.assert_called_with(
+            mock.ANY, pool_id='id1', health_monitor=data)
+        self.assertEqual(res.status_int, exc.HTTPCreated.code)
+        self.assertTrue('health_monitor' in res.json)
+        self.assertEqual(res.json['health_monitor'], return_value)
+
+    def test_delete_pool_health_monitor(self):
+        health_monitor_id = _uuid()
+
+        res = self.api.delete('/lb/pools/id1/health_monitors/%s' %
+                              health_monitor_id)
+
+        instance = self.plugin.return_value
+        instance.delete_pool_health_monitor.assert_called_with(
+            mock.ANY, health_monitor_id, pool_id='id1')
+        self.assertEqual(res.status_int, exc.HTTPNoContent.code)