]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
Add routed-service-insertion
authorKaiwei Fan <fank@vmware.com>
Mon, 4 Feb 2013 03:22:21 +0000 (19:22 -0800)
committerKaiwei Fan <fank@vmware.com>
Mon, 18 Feb 2013 15:52:49 +0000 (07:52 -0800)
Extend L3 router to add "service_type_id" attribute, and extend LB vip, pool,
and health_monitor to add "router_id" attribute.

Implements: API and DB model for blueprint routed-service-insertion
Change-Id: I12f053e0b0e6cf0813ea88dd568aae49b09b8215

quantum/db/routedserviceinsertion_db.py [new file with mode: 0644]
quantum/db/routerservicetype_db.py [new file with mode: 0644]
quantum/extensions/l3.py
quantum/extensions/loadbalancer.py
quantum/extensions/routedserviceinsertion.py [new file with mode: 0644]
quantum/extensions/routerservicetype.py [new file with mode: 0644]
quantum/tests/unit/test_routerserviceinsertion.py [new file with mode: 0644]

diff --git a/quantum/db/routedserviceinsertion_db.py b/quantum/db/routedserviceinsertion_db.py
new file mode 100644 (file)
index 0000000..1d7dc51
--- /dev/null
@@ -0,0 +1,88 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2013 VMware, Inc.  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.
+#
+# @author: Kaiwei Fan, VMware, Inc
+
+import sqlalchemy as sa
+from sqlalchemy import event
+from sqlalchemy.orm import exc
+
+from quantum.common import exceptions as qexception
+from quantum.db import model_base
+from quantum.extensions import routedserviceinsertion as rsi
+
+
+class ServiceRouterBinding(model_base.BASEV2):
+    resource_id = sa.Column(sa.String(36),
+                            primary_key=True)
+    resource_type = sa.Column(sa.String(36),
+                              primary_key=True)
+    router_id = sa.Column(sa.String(36),
+                          sa.ForeignKey('routers.id'),
+                          nullable=False)
+
+
+class AttributeException(qexception.QuantumException):
+    message = _("Resource type '%(resource_type)s' is longer "
+                "than %(maxlen)d characters")
+
+
+@event.listens_for(ServiceRouterBinding.resource_type, 'set', retval=True)
+def validate_resource_type(target, value, oldvalue, initiator):
+    """Make sure the resource type fit the resource_type column."""
+    maxlen = ServiceRouterBinding.resource_type.property.columns[0].type.length
+    if len(value) > maxlen:
+        raise AttributeException(resource_type=value, maxlen=maxlen)
+    return value
+
+
+class RoutedServiceInsertionDbMixin(object):
+    """Mixin class to add router service insertion."""
+
+    def _process_create_resource_router_id(self, context, resource, model):
+        with context.session.begin(subtransactions=True):
+            db = ServiceRouterBinding(
+                resource_id=resource['id'],
+                resource_type=model.__tablename__,
+                router_id=resource[rsi.ROUTER_ID])
+            context.session.add(db)
+        return self._make_resource_router_id_dict(db, model)
+
+    def _extend_resource_router_id_dict(self, context, resource, model):
+        binding = self._get_resource_router_id_binding(
+            context, resource['resource_id'], model)
+        resource[rsi.ROUTER_ID] = binding['router_id']
+
+    def _get_resource_router_id_binding(self, context, resource_id, model):
+        query = self._model_query(context, ServiceRouterBinding)
+        query = query.filter(
+            ServiceRouterBinding.resource_id == resource_id,
+            ServiceRouterBinding.resource_type == model.__tablename__)
+        return query.first()
+
+    def _make_resource_router_id_dict(self, resource_router_binding, model,
+                                      fields=None):
+        resource = {'resource_id': resource_router_binding['resource_id'],
+                    'resource_type': model.__tablename__,
+                    rsi.ROUTER_ID: resource_router_binding[rsi.ROUTER_ID]}
+        return self._fields(resource, fields)
+
+    def _delete_resource_router_id_binding(self, context, resource_id, model):
+        with context.session.begin(subtransactions=True):
+            binding = self._get_resource_router_id_binding(
+                context, resource_id, model)
+            if binding:
+                context.session.delete(binding)
diff --git a/quantum/db/routerservicetype_db.py b/quantum/db/routerservicetype_db.py
new file mode 100644 (file)
index 0000000..d764769
--- /dev/null
@@ -0,0 +1,61 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2013 VMware, Inc.  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.
+#
+# @author: Kaiwei Fan, VMware, Inc
+
+from sqlalchemy.orm import exc
+import sqlalchemy as sa
+
+from quantum.db import model_base
+from quantum.extensions import routerservicetype as rst
+
+
+class RouterServiceTypeBinding(model_base.BASEV2):
+    router_id = sa.Column(sa.String(36),
+                          sa.ForeignKey('routers.id', ondelete="CASCADE"),
+                          primary_key=True)
+    service_type_id = sa.Column(sa.String(36),
+                                sa.ForeignKey('servicetypes.id'),
+                                nullable=False)
+
+
+class RouterServiceTypeDbMixin(object):
+    """Mixin class to add router service type."""
+
+    def _process_create_router_service_type_id(self, context, router):
+        with context.session.begin(subtransactions=True):
+            db = RouterServiceTypeBinding(
+                router_id=router['id'],
+                service_type_id=router[rst.SERVICE_TYPE_ID])
+            context.session.add(db)
+        return self._make_router_service_type_id_dict(db)
+
+    def _extend_router_service_type_id_dict(self, context, router):
+        rsbind = self._get_router_service_type_id_binding(
+            context, router['id'])
+        if rsbind:
+            router[rst.SERVICE_TYPE_ID] = rsbind['service_type_id']
+
+    def _get_router_service_type_id_binding(self, context, router_id):
+        query = self._model_query(context, RouterServiceTypeBinding)
+        query = query.filter(
+            RouterServiceTypeBinding.router_id == router_id)
+        return query.first()
+
+    def _make_router_service_type_id_dict(self, router_service_type):
+        res = {'router_id': router_service_type['router_id'],
+               'service_type_id': router_service_type[rst.SERVICE_TYPE_ID]}
+        return self._fields(res, None)
index 2b251f6017f6e7104db4eb4f9b2768e62540f6b0..587b5ccd391afdfb6d13c3ddd7d84e8a4cd1630b 100644 (file)
@@ -214,9 +214,14 @@ class L3(extensions.ExtensionDescriptor):
 
         return exts
 
+    def update_attributes_map(self, attributes):
+        super(L3, self).update_attributes_map(
+            attributes, extension_attrs_map=RESOURCE_ATTRIBUTE_MAP)
+
     def get_extended_resources(self, version):
         if version == "2.0":
-            return EXTENDED_ATTRIBUTES_2_0
+            return dict(EXTENDED_ATTRIBUTES_2_0.items() +
+                        RESOURCE_ATTRIBUTE_MAP.items())
         else:
             return {}
 
index 6c5a2a17511934b8fa55b4785f10f500f92e9d8a..d33307e091ce119143715c720f499d383feee3c5 100644 (file)
@@ -318,6 +318,16 @@ class Loadbalancer(extensions.ExtensionDescriptor):
     def get_plugin_interface(cls):
         return LoadBalancerPluginBase
 
+    def update_attributes_map(self, attributes):
+        super(Loadbalancer, self).update_attributes_map(
+            attributes, extension_attrs_map=RESOURCE_ATTRIBUTE_MAP)
+
+    def get_extended_resources(self, version):
+        if version == "2.0":
+            return RESOURCE_ATTRIBUTE_MAP
+        else:
+            return {}
+
 
 class LoadBalancerPluginBase(ServicePluginBase):
     __metaclass__ = abc.ABCMeta
diff --git a/quantum/extensions/routedserviceinsertion.py b/quantum/extensions/routedserviceinsertion.py
new file mode 100644 (file)
index 0000000..b396253
--- /dev/null
@@ -0,0 +1,67 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2013 VMware, Inc.  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.
+#
+# @author: Kaiwei Fan, VMware, Inc
+
+
+ROUTER_ID = 'router_id'
+EXTENDED_ATTRIBUTES_2_0 = {
+    'vips': {
+        ROUTER_ID: {'allow_post': True, 'allow_put': False,
+                    'validate': {'type:uuid_or_none': None},
+                    'default': None, 'is_visible': True},
+    },
+    'pools': {
+        ROUTER_ID: {'allow_post': True, 'allow_put': False,
+                    'validate': {'type:uuid_or_none': None},
+                    'default': None, 'is_visible': True},
+    },
+    'health_monitors': {
+        ROUTER_ID: {'allow_post': True, 'allow_put': False,
+                    'validate': {'type:uuid_or_none': None},
+                    'default': None, 'is_visible': True},
+    },
+}
+
+
+class Routedserviceinsertion(object):
+    """Extension class supporting routed service type."""
+
+    @classmethod
+    def get_name(cls):
+        return "Routed Service Insertion"
+
+    @classmethod
+    def get_alias(cls):
+        return "routed-service-insertion"
+
+    @classmethod
+    def get_description(cls):
+        return "Provides routed service type"
+
+    @classmethod
+    def get_namespace(cls):
+        return ""
+
+    @classmethod
+    def get_updated(cls):
+        return "2013-01-29T00:00:00-00:00"
+
+    def get_extended_resources(self, version):
+        if version == "2.0":
+            return EXTENDED_ATTRIBUTES_2_0
+        else:
+            return {}
diff --git a/quantum/extensions/routerservicetype.py b/quantum/extensions/routerservicetype.py
new file mode 100644 (file)
index 0000000..17f0323
--- /dev/null
@@ -0,0 +1,57 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2013 VMware, Inc.  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.
+#
+# @author: Kaiwei Fan, VMware, Inc
+
+
+SERVICE_TYPE_ID = 'service_type_id'
+EXTENDED_ATTRIBUTES_2_0 = {
+    'routers': {
+        SERVICE_TYPE_ID: {'allow_post': True, 'allow_put': False,
+                          'validate': {'type:uuid_or_none': None},
+                          'default': None, 'is_visible': True},
+    }
+}
+
+
+class Routerservicetype(object):
+    """Extension class supporting router service type."""
+
+    @classmethod
+    def get_name(cls):
+        return "Router Service Type"
+
+    @classmethod
+    def get_alias(cls):
+        return "router-service-type"
+
+    @classmethod
+    def get_description(cls):
+        return "Provides router service type"
+
+    @classmethod
+    def get_namespace(cls):
+        return ""
+
+    @classmethod
+    def get_updated(cls):
+        return "2013-01-29T00:00:00-00:00"
+
+    def get_extended_resources(self, version):
+        if version == "2.0":
+            return EXTENDED_ATTRIBUTES_2_0
+        else:
+            return {}
diff --git a/quantum/tests/unit/test_routerserviceinsertion.py b/quantum/tests/unit/test_routerserviceinsertion.py
new file mode 100644 (file)
index 0000000..633629f
--- /dev/null
@@ -0,0 +1,435 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 VMware, Inc. 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 unittest2 as unittest
+import webob.exc as webexc
+
+import quantum
+from quantum.api import extensions
+from quantum.api.v2 import router
+from quantum.common import config
+from quantum.db.loadbalancer import loadbalancer_db as lb_db
+from quantum.db import db_base_plugin_v2
+from quantum.db import l3_db
+from quantum.db import routedserviceinsertion_db as rsi_db
+from quantum.db import routerservicetype_db as rst_db
+from quantum.db import servicetype_db as st_db
+from quantum.extensions import routedserviceinsertion as rsi
+from quantum.extensions import routerservicetype as rst
+from quantum.openstack.common import cfg
+from quantum.plugins.common import constants
+from quantum.tests.unit import test_api_v2
+from quantum.tests.unit import testlib_api
+from quantum import wsgi
+
+_uuid = test_api_v2._uuid
+_get_path = test_api_v2._get_path
+extensions_path = ':'.join(quantum.extensions.__path__)
+
+
+class RouterServiceInsertionTestPlugin(
+    rst_db.RouterServiceTypeDbMixin,
+    rsi_db.RoutedServiceInsertionDbMixin,
+    st_db.ServiceTypeManager,
+    lb_db.LoadBalancerPluginDb,
+    l3_db.L3_NAT_db_mixin,
+    db_base_plugin_v2.QuantumDbPluginV2):
+
+    supported_extension_aliases = [
+        "router", "router-service-type", "routed-service-insertion",
+        "service-type", "lbaas"
+    ]
+
+    def create_router(self, context, router):
+        with context.session.begin(subtransactions=True):
+            r = super(RouterServiceInsertionTestPlugin, self).create_router(
+                context, router)
+            service_type_id = router['router'].get(rst.SERVICE_TYPE_ID)
+            if service_type_id is not None:
+                r[rst.SERVICE_TYPE_ID] = service_type_id
+                self._process_create_router_service_type_id(
+                    context, r)
+        return r
+
+    def get_router(self, context, id, fields=None):
+        with context.session.begin(subtransactions=True):
+            r = super(RouterServiceInsertionTestPlugin, self).get_router(
+                context, id, fields)
+            rsbind = self._get_router_service_type_id_binding(context, id)
+            if rsbind:
+                r[rst.SERVICE_TYPE_ID] = rsbind['service_type_id']
+        return r
+
+    def delete_router(self, context, id):
+        with context.session.begin(subtransactions=True):
+            super(RouterServiceInsertionTestPlugin, self).delete_router(
+                context, id)
+        rsbind = self._get_router_service_type_id_binding(context, id)
+        if rsbind:
+            raise Exception('Router service-type binding is not deleted')
+
+    def create_resource(self, res, context, resource, model):
+        with context.session.begin(subtransactions=True):
+            method_name = "create_{0}".format(res)
+            method = getattr(super(RouterServiceInsertionTestPlugin, self),
+                             method_name)
+            o = method(context, resource)
+            router_id = resource[res].get(rsi.ROUTER_ID)
+            if router_id is not None:
+                o[rsi.ROUTER_ID] = router_id
+                self._process_create_resource_router_id(
+                    context, o, model)
+        return o
+
+    def get_resource(self, res, context, id, fields, model):
+        method_name = "get_{0}".format(res)
+        method = getattr(super(RouterServiceInsertionTestPlugin, self),
+                         method_name)
+        o = method(context, id, fields)
+        if fields is None or rsi.ROUTER_ID in fields:
+            rsbind = self._get_resource_router_id_binding(
+                context, id, model)
+            if rsbind:
+                o[rsi.ROUTER_ID] = rsbind['router_id']
+        return o
+
+    def delete_resource(self, res, context, id, model):
+        method_name = "delete_{0}".format(res)
+        with context.session.begin(subtransactions=True):
+            method = getattr(super(RouterServiceInsertionTestPlugin, self),
+                             method_name)
+            method(context, id)
+            self._delete_resource_router_id_binding(context, id, model)
+        if self._get_resource_router_id_binding(context, id, model):
+            raise Exception("{0}-router binding is not deleted".format(res))
+
+    def create_pool(self, context, pool):
+        return self.create_resource('pool', context, pool, lb_db.Pool)
+
+    def get_pool(self, context, id, fields=None):
+        return self.get_resource('pool', context, id, fields, lb_db.Pool)
+
+    def delete_pool(self, context, id):
+        return self.delete_resource('pool', context, id, lb_db.Pool)
+
+    def create_health_monitor(self, context, health_monitor):
+        return self.create_resource('health_monitor', context, health_monitor,
+                                    lb_db.HealthMonitor)
+
+    def get_health_monitor(self, context, id, fields=None):
+        return self.get_resource('health_monitor', context, id, fields,
+                                 lb_db.HealthMonitor)
+
+    def delete_health_monitor(self, context, id):
+        return self.delete_resource('health_monitor', context, id,
+                                    lb_db.HealthMonitor)
+
+    def create_vip(self, context, vip):
+        return self.create_resource('vip', context, vip, lb_db.Vip)
+
+    def get_vip(self, context, id, fields=None):
+        return self.get_resource(
+            'vip', context, id, fields, lb_db.Vip)
+
+    def delete_vip(self, context, id):
+        return self.delete_resource('vip', context, id, lb_db.Vip)
+
+    def stats(self, context, pool_id):
+        pass
+
+
+class RouterServiceInsertionTestCase(unittest.TestCase):
+    def setUp(self):
+        plugin = (
+            "quantum.tests.unit.test_routerserviceinsertion."
+            "RouterServiceInsertionTestPlugin"
+        )
+
+        # point config file to: quantum/tests/etc/quantum.conf.test
+        args = ['--config-file', test_api_v2.etcdir('quantum.conf.test')]
+        config.parse(args=args)
+
+        #just stubbing core plugin with LoadBalancer plugin
+        cfg.CONF.set_override('core_plugin', plugin)
+        cfg.CONF.set_override('service_plugins', [plugin])
+        cfg.CONF.set_override('quota_router', -1, group='QUOTAS')
+
+        # Ensure 'stale' patched copies of the plugin are never returned
+        quantum.manager.QuantumManager._instance = None
+        # Ensure existing ExtensionManager is not used
+
+        ext_mgr = extensions.PluginAwareExtensionManager(
+            extensions_path,
+            {constants.LOADBALANCER: RouterServiceInsertionTestPlugin()}
+        )
+        extensions.PluginAwareExtensionManager._instance = ext_mgr
+        router.APIRouter()
+
+        app = config.load_paste_app('extensions_test_app')
+        self._api = extensions.ExtensionMiddleware(app, ext_mgr=ext_mgr)
+
+        self._tenant_id = "8c70909f-b081-452d-872b-df48e6c355d1"
+
+        res = self._do_request('GET', _get_path('service-types'))
+        self._service_type_id = res['service_types'][0]['id']
+
+    def tearDown(self):
+        self._api = None
+        cfg.CONF.reset()
+
+    def _do_request(self, method, path, data=None, params=None, action=None):
+        content_type = 'application/json'
+        body = None
+        if data is not None:  # empty dict is valid
+            body = wsgi.Serializer().serialize(data, content_type)
+
+        req = testlib_api.create_request(
+            path, body, content_type,
+            method, query_string=params)
+        res = req.get_response(self._api)
+        if res.status_code >= 400:
+            raise webexc.HTTPClientError(detail=res.body, code=res.status_code)
+        if res.status_code != webexc.HTTPNoContent.code:
+            return res.json
+
+    def _router_create(self, service_type_id=None):
+        data = {
+            "router": {
+                "tenant_id": self._tenant_id,
+                "name": "test",
+                "admin_state_up": True,
+                "service_type_id": service_type_id,
+            }
+        }
+
+        res = self._do_request('POST', _get_path('routers'), data)
+        return res['router']
+
+    def test_router_create_no_service_type_id(self):
+        router = self._router_create()
+        self.assertEqual(router.get('service_type_id'), None)
+
+    def test_router_create_with_service_type_id(self):
+        router = self._router_create(self._service_type_id)
+        self.assertEqual(router['service_type_id'], self._service_type_id)
+
+    def test_router_get(self):
+        router = self._router_create(self._service_type_id)
+        res = self._do_request('GET',
+                               _get_path('routers/{0}'.format(router['id'])))
+        self.assertEqual(res['router']['service_type_id'],
+                         self._service_type_id)
+
+    def _test_router_update(self, update_service_type_id):
+        router = self._router_create(self._service_type_id)
+        router_id = router['id']
+        new_name = _uuid()
+        data = {
+            "router": {
+                "name": new_name,
+                "admin_state_up": router['admin_state_up'],
+            }
+        }
+        if update_service_type_id:
+            data["router"]["service_type_id"] = _uuid()
+            with self.assertRaises(webexc.HTTPClientError) as ctx_manager:
+                res = self._do_request(
+                    'PUT', _get_path('routers/{0}'.format(router_id)), data)
+            self.assertEqual(ctx_manager.exception.code, 400)
+        else:
+            res = self._do_request(
+                'PUT', _get_path('routers/{0}'.format(router_id)), data)
+            res = self._do_request(
+                'GET', _get_path('routers/{0}'.format(router['id'])))
+            self.assertEqual(res['router']['name'], new_name)
+
+    def test_router_update_with_service_type_id(self):
+        self._test_router_update(True)
+
+    def test_router_update_without_service_type_id(self):
+        self._test_router_update(False)
+
+    def test_router_delete(self):
+        router = self._router_create(self._service_type_id)
+        self._do_request(
+            'DELETE', _get_path('routers/{0}'.format(router['id'])))
+
+    def _test_lb_setup(self):
+        self._subnet_id = _uuid()
+        router = self._router_create(self._service_type_id)
+        self._router_id = router['id']
+
+    def _test_pool_setup(self):
+        self._test_lb_setup()
+
+    def _test_health_monitor_setup(self):
+        self._test_lb_setup()
+
+    def _test_vip_setup(self):
+        self._test_pool_setup()
+        pool = self._pool_create(self._router_id)
+        self._pool_id = pool['id']
+
+    def _create_resource(self, res, data):
+        resp = self._do_request('POST', _get_path('lb/{0}s'.format(res)), data)
+        return resp[res]
+
+    def _pool_create(self, router_id=None):
+        data = {
+            "pool": {
+                "tenant_id": self._tenant_id,
+                "name": "test",
+                "protocol": "HTTP",
+                "subnet_id": self._subnet_id,
+                "lb_method": "ROUND_ROBIN",
+                "router_id": router_id
+            }
+        }
+
+        return self._create_resource('pool', data)
+
+    def _pool_update_attrs(self, pool):
+        uattr = {}
+        fields = [
+            'name', 'description', 'lb_method',
+            'health_monitors', 'admin_state_up'
+        ]
+        for field in fields:
+            uattr[field] = pool[field]
+        return uattr
+
+    def _health_monitor_create(self, router_id=None):
+        data = {
+            "health_monitor": {
+                "tenant_id": self._tenant_id,
+                "type": "HTTP",
+                "delay": 1,
+                "timeout": 1,
+                "max_retries": 1,
+                "router_id": router_id
+            }
+        }
+
+        return self._create_resource('health_monitor', data)
+
+    def _health_monitor_update_attrs(self, hm):
+        uattr = {}
+        fields = ['delay', 'timeout', 'max_retries']
+        for field in fields:
+            uattr[field] = hm[field]
+        return uattr
+
+    def _vip_create(self, router_id=None):
+        data = {
+            "vip": {
+                "tenant_id": self._tenant_id,
+                "name": "test",
+                "protocol": "HTTP",
+                "port": 80,
+                "subnet_id": self._subnet_id,
+                "pool_id": self._pool_id,
+                "address": "192.168.1.101",
+                "connection_limit": 100,
+                "admin_state_up": True,
+                "router_id": router_id
+            }
+        }
+
+        return self._create_resource('vip', data)
+
+    def _vip_update_attrs(self, vip):
+        uattr = {}
+        fields = [
+            'name', 'description', 'pool_id', 'connection_limit',
+            'admin_state_up'
+        ]
+        for field in fields:
+            uattr[field] = vip[field]
+        return uattr
+
+    def _test_resource_create(self, res):
+        getattr(self, "_test_{0}_setup".format(res))()
+        obj = getattr(self, "_{0}_create".format(res))()
+        obj = getattr(self, "_{0}_create".format(res))(self._router_id)
+        self.assertEqual(obj['router_id'], self._router_id)
+
+    def _test_resource_update(self, res, update_router_id,
+                              update_attr, update_value):
+        getattr(self, "_test_{0}_setup".format(res))()
+        obj = getattr(self, "_{0}_create".format(res))(self._router_id)
+        uattrs = getattr(self, "_{0}_update_attrs".format(res))(obj)
+        uattrs[update_attr] = update_value
+        data = {res: uattrs}
+        if update_router_id:
+            uattrs['router_id'] = self._router_id
+            with self.assertRaises(webexc.HTTPClientError) as ctx_manager:
+                newobj = self._do_request(
+                    'PUT',
+                    _get_path('lb/{0}s/{1}'.format(res, obj['id'])), data)
+            self.assertEqual(ctx_manager.exception.code, 400)
+        else:
+            newobj = self._do_request(
+                'PUT',
+                _get_path('lb/{0}s/{1}'.format(res, obj['id'])), data)
+            updated = self._do_request(
+                'GET',
+                _get_path('lb/{0}s/{1}'.format(res, obj['id'])))
+            self.assertEqual(updated[res][update_attr], update_value)
+
+    def _test_resource_delete(self, res):
+        getattr(self, "_test_{0}_setup".format(res))()
+        obj = getattr(self, "_{0}_create".format(res))()
+        self._do_request(
+            'DELETE', _get_path('lb/{0}s/{1}'.format(res, obj['id'])))
+        obj = getattr(self, "_{0}_create".format(res))(self._router_id)
+        self._do_request(
+            'DELETE', _get_path('lb/{0}s/{1}'.format(res, obj['id'])))
+
+    def test_pool_create(self):
+        self._test_resource_create('pool')
+
+    def test_pool_update_with_router_id(self):
+        self._test_resource_update('pool', True, 'name', _uuid())
+
+    def test_pool_update_without_router_id(self):
+        self._test_resource_update('pool', False, 'name', _uuid())
+
+    def test_pool_delete(self):
+        self._test_resource_delete('pool')
+
+    def test_health_monitor_create(self):
+        self._test_resource_create('health_monitor')
+
+    def test_health_monitor_update_with_router_id(self):
+        self._test_resource_update('health_monitor', True, 'timeout', 2)
+
+    def test_health_monitor_update_without_router_id(self):
+        self._test_resource_update('health_monitor', False, 'timeout', 2)
+
+    def test_health_monitor_delete(self):
+        self._test_resource_delete('health_monitor')
+
+    def test_vip_create(self):
+        self._test_resource_create('vip')
+
+    def test_vip_update_with_router_id(self):
+        self._test_resource_update('vip', True, 'name', _uuid())
+
+    def test_vip_update_without_router_id(self):
+        self._test_resource_update('vip', False, 'name', _uuid())
+
+    def test_vip_delete(self):
+        self._test_resource_delete('vip')