]> review.fuel-infra Code Review - openstack-build/heat-build.git/commitdiff
Implement neutron pool resource
authorThomas Herve <th@rackspace.com>
Wed, 7 Aug 2013 11:48:04 +0000 (13:48 +0200)
committerThomas Herve <th@rackspace.com>
Thu, 8 Aug 2013 15:17:44 +0000 (17:17 +0200)
The branch adds a new resource creating pool instances in
Neutron LBAAS.

Change-Id: Ie55dd0cc5d2a67296ab175dc3bea11ffb63ba928
Implements: blueprint lbaas-resource

heat/engine/resources/neutron/loadbalancer.py
heat/tests/test_neutron_loadbalancer.py

index a6dc9ffb42ad759aace8f79dd267e89bd5031fb6..51b78b3e7ec21125f8bdb6fc48ed70dd99acfcf5 100644 (file)
@@ -13,6 +13,7 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+from heat.common import exception
 from heat.engine import clients
 from heat.engine import scheduler
 from heat.engine.resources.neutron import neutron
@@ -44,7 +45,7 @@ class HealthMonitor(neutron.NeutronResource):
                                  'expected_codes', 'url_path')
 
     attributes_schema = {
-        'admin_state_up': 'the administrative state of this port',
+        'admin_state_up': 'the administrative state of this health monitor',
         'delay': 'the minimum time in seconds between regular connections '
                  'of the member',
         'expected_codes': 'the list of HTTP status codes expected in '
@@ -75,8 +76,9 @@ class HealthMonitor(neutron.NeutronResource):
             self.resource_id)['health_monitor']
 
     def handle_update(self, json_snippet, tmpl_diff, prop_diff):
-        self.neutron().update_health_monitor(
-            self.resource_id, {'health_monitor': prop_diff})
+        if prop_diff:
+            self.neutron().update_health_monitor(
+                self.resource_id, {'health_monitor': prop_diff})
 
     def handle_delete(self):
         try:
@@ -88,10 +90,158 @@ class HealthMonitor(neutron.NeutronResource):
             return scheduler.TaskRunner(self._confirm_delete)()
 
 
+class Pool(neutron.NeutronResource):
+    """
+    A resource for managing load balancer pools in Neutron.
+    """
+
+    vip_schema = {
+        'name': {'Type': 'String'},
+        'description': {'Type': 'String'},
+        'address': {'Type': 'String'},
+        'connection_limit': {'Type': 'Integer'},
+        'protocol_port': {'Type': 'Integer', 'Required': True},
+        'admin_state_up': {'Default': True, 'Type': 'Boolean'},
+    }
+
+    properties_schema = {
+        'protocol': {'Type': 'String', 'Required': True,
+                     'AllowedValues': ['TCP', 'HTTP', 'HTTPS']},
+        'subnet_id': {'Type': 'String', 'Required': True},
+        'lb_method': {'Type': 'String', 'Required': True,
+                      'AllowedValues': ['ROUND_ROBIN', 'LEAST_CONNECTIONS',
+                                        'SOURCE_IP']},
+        'name': {'Type': 'String'},
+        'description': {'Type': 'String'},
+        'admin_state_up': {'Default': True, 'Type': 'Boolean'},
+        'vip': {'Type': 'Map', 'Schema': vip_schema, 'Required': True},
+        'monitors': {'Type': 'List'},
+    }
+
+    update_allowed_keys = ('Properties',)
+    update_allowed_properties = ('description', 'admin_state_up', 'lb_method',
+                                 'monitors')
+
+    attributes_schema = {
+        'admin_state_up': 'the administrative state of this pool',
+        'id': 'unique identifier for this pool',
+        'name': 'friendly name of the pool',
+        'protocol': 'protocol to balance',
+        'subnet_id': 'the subnet on which the members of the pool '
+                     'will be located',
+        'lb_method': 'the algorithm used to distribute load between the '
+                     'members of the pool',
+        'description': 'description of the pool',
+        'tenant_id': 'tenant owning the pool',
+        'vip': 'ip of the pool',
+    }
+
+    def handle_create(self):
+        properties = self.prepare_properties(
+            self.properties,
+            self.physical_resource_name())
+        vip_properties = properties.pop('vip')
+        monitors = properties.pop('monitors', [])
+        client = self.neutron()
+        pool = client.create_pool({'pool': properties})['pool']
+        self.resource_id_set(pool['id'])
+
+        for monitor in monitors:
+            client.associate_health_monitor(
+                pool['id'], {'health_monitor': {'id': monitor}})
+
+        vip_arguments = self.prepare_properties(
+            vip_properties,
+            '%s.vip' % (self.name,))
+        vip_arguments['protocol'] = self.properties['protocol']
+        vip_arguments['subnet_id'] = self.properties['subnet_id']
+        vip_arguments['pool_id'] = pool['id']
+        vip = client.create_vip({'vip': vip_arguments})['vip']
+
+        self.metadata = {'vip': vip['id']}
+
+    def _show_resource(self):
+        return self.neutron().show_pool(self.resource_id)['pool']
+
+    def check_create_complete(self, data):
+        attributes = self._show_resource()
+        if attributes['status'] == 'PENDING_CREATE':
+            return False
+        elif attributes['status'] == 'ACTIVE':
+            vip_attributes = self.neutron().show_vip(
+                self.metadata['vip'])['vip']
+            if vip_attributes['status'] == 'PENDING_CREATE':
+                return False
+            elif vip_attributes['status'] == 'ACTIVE':
+                return True
+            raise exception.Error(
+                'neutron reported unexpected vip resource[%s] status[%s]' %
+                (vip_attributes['name'], vip_attributes['status']))
+        raise exception.Error(
+            'neutron report unexpected pool resource[%s] status[%s]' %
+            (attributes['name'], attributes['status']))
+
+    def handle_update(self, json_snippet, tmpl_diff, prop_diff):
+        if prop_diff:
+            client = self.neutron()
+            monitors = set(prop_diff.pop('monitors', []))
+            if monitors:
+                old_monitors = set(self.t['Properties'].get('monitors', []))
+                for monitor in old_monitors - monitors:
+                    client.disassociate_health_monitor(
+                        self.resource_id, {'health_monitor': {'id': monitor}})
+                for monitor in monitors - old_monitors:
+                    client.associate_health_monitor(
+                        self.resource_id, {'health_monitor': {'id': monitor}})
+
+            if prop_diff:
+                client.update_pool(self.resource_id, {'pool': prop_diff})
+
+    def _resolve_attribute(self, name):
+        if name == 'vip':
+            return self.neutron().show_vip(self.metadata['vip'])['vip']
+        return super(Pool, self)._resolve_attribute(name)
+
+    def _confirm_vip_delete(self):
+        client = self.neutron()
+        while True:
+            try:
+                yield
+                client.show_vip(self.metadata['vip'])
+            except NeutronClientException as ex:
+                if ex.status_code != 404:
+                    raise ex
+                break
+        self._delete_pool()
+
+    def _delete_pool(self):
+        try:
+            self.neutron().delete_pool(self.resource_id)
+        except NeutronClientException as ex:
+            if ex.status_code != 404:
+                raise ex
+        else:
+            return scheduler.TaskRunner(self._confirm_delete)()
+
+    def handle_delete(self):
+        if self.metadata:
+            try:
+                self.neutron().delete_vip(self.metadata['vip'])
+            except NeutronClientException as ex:
+                if ex.status_code != 404:
+                    raise ex
+                self._delete_pool()
+            else:
+                return scheduler.TaskRunner(self._confirm_vip_delete)()
+        else:
+            self._delete_pool()
+
+
 def resource_mapping():
     if clients.neutronclient is None:
         return {}
 
     return {
         'OS::Neutron::HealthMonitor': HealthMonitor,
+        'OS::Neutron::Pool': Pool,
     }
index a4cbbe8b4c59b11abd31715137bd8e1978b595a2..af4409a763bab2f73b76955c82d24878b92b6b25 100644 (file)
@@ -47,6 +47,27 @@ health_monitor_template = '''
 }
 '''
 
+pool_template = '''
+{
+  "AWSTemplateFormatVersion" : "2010-09-09",
+  "Description" : "Template to test load balancer resources",
+  "Parameters" : {},
+  "Resources" : {
+    "pool": {
+      "Type": "OS::Neutron::Pool",
+      "Properties": {
+        "protocol": "HTTP",
+        "subnet_id": "sub123",
+        "lb_method": "ROUND_ROBIN",
+        "vip": {
+            "protocol_port": 80
+        }
+      }
+    }
+  }
+}
+'''
+
 
 @skipIf(neutronclient is None, 'neutronclient unavailable')
 class HealthMonitorTest(HeatTestCase):
@@ -95,13 +116,16 @@ class HealthMonitorTest(HeatTestCase):
         stack = utils.parse_stack(snippet)
         rsrc = loadbalancer.HealthMonitor(
             'monitor', snippet['Resources']['monitor'], stack)
-        self.assertRaises(exception.ResourceFailure,
-                          scheduler.TaskRunner(rsrc.create))
+        error = self.assertRaises(exception.ResourceFailure,
+                                  scheduler.TaskRunner(rsrc.create))
+        self.assertEqual(
+            'NeutronClientException: An unknown exception occurred.',
+            str(error))
         self.assertEqual((rsrc.CREATE, rsrc.FAILED), rsrc.state)
         self.m.VerifyAll()
 
     def test_delete(self):
-        neutronclient.Client.delete_health_monitor('5678').AndReturn(None)
+        neutronclient.Client.delete_health_monitor('5678')
         neutronclient.Client.show_health_monitor('5678').AndRaise(
             loadbalancer.NeutronClientException(status_code=404))
 
@@ -130,8 +154,11 @@ class HealthMonitorTest(HeatTestCase):
         rsrc = self.create_health_monitor()
         self.m.ReplayAll()
         scheduler.TaskRunner(rsrc.create)()
-        self.assertRaises(exception.ResourceFailure,
-                          scheduler.TaskRunner(rsrc.delete))
+        error = self.assertRaises(exception.ResourceFailure,
+                                  scheduler.TaskRunner(rsrc.delete))
+        self.assertEqual(
+            'NeutronClientException: An unknown exception occurred.',
+            str(error))
         self.assertEqual((rsrc.DELETE, rsrc.FAILED), rsrc.state)
         self.m.VerifyAll()
 
@@ -150,14 +177,17 @@ class HealthMonitorTest(HeatTestCase):
         rsrc = self.create_health_monitor()
         self.m.ReplayAll()
         scheduler.TaskRunner(rsrc.create)()
-        self.assertRaises(exception.InvalidTemplateAttribute,
-                          rsrc.FnGetAtt, 'subnet_id')
+        error = self.assertRaises(exception.InvalidTemplateAttribute,
+                                  rsrc.FnGetAtt, 'subnet_id')
+        self.assertEqual(
+            'The Referenced Attribute (monitor subnet_id) is incorrect.',
+            str(error))
         self.m.VerifyAll()
 
     def test_update(self):
         rsrc = self.create_health_monitor()
         neutronclient.Client.update_health_monitor(
-            '5678', {'health_monitor': {'delay': 10}}).AndReturn(None)
+            '5678', {'health_monitor': {'delay': 10}})
         self.m.ReplayAll()
         scheduler.TaskRunner(rsrc.create)()
 
@@ -166,3 +196,324 @@ class HealthMonitorTest(HeatTestCase):
         self.assertEqual(None, rsrc.update(update_template))
 
         self.m.VerifyAll()
+
+
+@skipIf(neutronclient is None, 'neutronclient unavailable')
+class PoolTest(HeatTestCase):
+
+    def setUp(self):
+        super(PoolTest, self).setUp()
+        self.m.StubOutWithMock(neutronclient.Client, 'create_pool')
+        self.m.StubOutWithMock(neutronclient.Client, 'delete_pool')
+        self.m.StubOutWithMock(neutronclient.Client, 'show_pool')
+        self.m.StubOutWithMock(neutronclient.Client, 'update_pool')
+        self.m.StubOutWithMock(neutronclient.Client,
+                               'associate_health_monitor')
+        self.m.StubOutWithMock(neutronclient.Client,
+                               'disassociate_health_monitor')
+        self.m.StubOutWithMock(neutronclient.Client, 'create_vip')
+        self.m.StubOutWithMock(neutronclient.Client, 'delete_vip')
+        self.m.StubOutWithMock(neutronclient.Client, 'show_vip')
+        self.m.StubOutWithMock(clients.OpenStackClients, 'keystone')
+        utils.setup_dummy_db()
+
+    def create_pool(self):
+        clients.OpenStackClients.keystone().AndReturn(
+            fakes.FakeKeystoneClient())
+        neutronclient.Client.create_pool({
+            'pool': {
+                'subnet_id': 'sub123', 'protocol': u'HTTP',
+                'name': utils.PhysName('test_stack', 'pool'),
+                'lb_method': 'ROUND_ROBIN', 'admin_state_up': True}}
+        ).AndReturn({'pool': {'id': '5678'}})
+        neutronclient.Client.create_vip({
+            'vip': {
+                'protocol': u'HTTP', 'name': 'pool.vip',
+                'admin_state_up': True, 'subnet_id': u'sub123',
+                'pool_id': '5678', 'protocol_port': 80}}
+        ).AndReturn({'vip': {'id': 'xyz'}})
+        neutronclient.Client.show_pool('5678').AndReturn(
+            {'pool': {'status': 'ACTIVE'}})
+        neutronclient.Client.show_vip('xyz').AndReturn(
+            {'vip': {'status': 'ACTIVE'}})
+
+        snippet = template_format.parse(pool_template)
+        stack = utils.parse_stack(snippet)
+        return loadbalancer.Pool(
+            'pool', snippet['Resources']['pool'], stack)
+
+    def test_create(self):
+        rsrc = self.create_pool()
+        self.m.ReplayAll()
+        scheduler.TaskRunner(rsrc.create)()
+        self.assertEqual((rsrc.CREATE, rsrc.COMPLETE), rsrc.state)
+        self.m.VerifyAll()
+
+    def test_create_pending(self):
+        clients.OpenStackClients.keystone().AndReturn(
+            fakes.FakeKeystoneClient())
+        neutronclient.Client.create_pool({
+            'pool': {
+                'subnet_id': 'sub123', 'protocol': u'HTTP',
+                'name': utils.PhysName('test_stack', 'pool'),
+                'lb_method': 'ROUND_ROBIN', 'admin_state_up': True}}
+        ).AndReturn({'pool': {'id': '5678'}})
+        neutronclient.Client.create_vip({
+            'vip': {
+                'protocol': u'HTTP', 'name': 'pool.vip',
+                'admin_state_up': True, 'subnet_id': u'sub123',
+                'pool_id': '5678', 'protocol_port': 80}}
+        ).AndReturn({'vip': {'id': 'xyz'}})
+        neutronclient.Client.show_pool('5678').AndReturn(
+            {'pool': {'status': 'PENDING_CREATE'}})
+        neutronclient.Client.show_pool('5678').MultipleTimes().AndReturn(
+            {'pool': {'status': 'ACTIVE'}})
+        neutronclient.Client.show_vip('xyz').AndReturn(
+            {'vip': {'status': 'PENDING_CREATE'}})
+        neutronclient.Client.show_vip('xyz').AndReturn(
+            {'vip': {'status': 'ACTIVE'}})
+
+        snippet = template_format.parse(pool_template)
+        stack = utils.parse_stack(snippet)
+        rsrc = loadbalancer.Pool(
+            'pool', snippet['Resources']['pool'], stack)
+        self.m.ReplayAll()
+        scheduler.TaskRunner(rsrc.create)()
+        self.assertEqual((rsrc.CREATE, rsrc.COMPLETE), rsrc.state)
+        self.m.VerifyAll()
+
+    def test_create_failed_unexpected_status(self):
+        clients.OpenStackClients.keystone().AndReturn(
+            fakes.FakeKeystoneClient())
+        neutronclient.Client.create_pool({
+            'pool': {
+                'subnet_id': 'sub123', 'protocol': u'HTTP',
+                'name': utils.PhysName('test_stack', 'pool'),
+                'lb_method': 'ROUND_ROBIN', 'admin_state_up': True}}
+        ).AndReturn({'pool': {'id': '5678'}})
+        neutronclient.Client.create_vip({
+            'vip': {
+                'protocol': u'HTTP', 'name': 'pool.vip',
+                'admin_state_up': True, 'subnet_id': u'sub123',
+                'pool_id': '5678', 'protocol_port': 80}}
+        ).AndReturn({'vip': {'id': 'xyz'}})
+        neutronclient.Client.show_pool('5678').AndReturn(
+            {'pool': {'status': 'ERROR', 'name': '5678'}})
+
+        snippet = template_format.parse(pool_template)
+        stack = utils.parse_stack(snippet)
+        rsrc = loadbalancer.Pool(
+            'pool', snippet['Resources']['pool'], stack)
+        self.m.ReplayAll()
+        error = self.assertRaises(exception.ResourceFailure,
+                                  scheduler.TaskRunner(rsrc.create))
+        self.assertEqual(
+            'Error: neutron report unexpected pool '
+            'resource[5678] status[ERROR]',
+            str(error))
+        self.assertEqual((rsrc.CREATE, rsrc.FAILED), rsrc.state)
+        self.m.VerifyAll()
+
+    def test_create_failed_unexpected_vip_status(self):
+        clients.OpenStackClients.keystone().AndReturn(
+            fakes.FakeKeystoneClient())
+        neutronclient.Client.create_pool({
+            'pool': {
+                'subnet_id': 'sub123', 'protocol': u'HTTP',
+                'name': utils.PhysName('test_stack', 'pool'),
+                'lb_method': 'ROUND_ROBIN', 'admin_state_up': True}}
+        ).AndReturn({'pool': {'id': '5678'}})
+        neutronclient.Client.create_vip({
+            'vip': {
+                'protocol': u'HTTP', 'name': 'pool.vip',
+                'admin_state_up': True, 'subnet_id': u'sub123',
+                'pool_id': '5678', 'protocol_port': 80}}
+        ).AndReturn({'vip': {'id': 'xyz'}})
+        neutronclient.Client.show_pool('5678').MultipleTimes().AndReturn(
+            {'pool': {'status': 'ACTIVE'}})
+        neutronclient.Client.show_vip('xyz').AndReturn(
+            {'vip': {'status': 'ERROR', 'name': 'xyz'}})
+
+        snippet = template_format.parse(pool_template)
+        stack = utils.parse_stack(snippet)
+        rsrc = loadbalancer.Pool(
+            'pool', snippet['Resources']['pool'], stack)
+        self.m.ReplayAll()
+        error = self.assertRaises(exception.ResourceFailure,
+                                  scheduler.TaskRunner(rsrc.create))
+        self.assertEqual(
+            'Error: neutron reported unexpected vip '
+            'resource[xyz] status[ERROR]',
+            str(error))
+        self.assertEqual((rsrc.CREATE, rsrc.FAILED), rsrc.state)
+        self.m.VerifyAll()
+
+    def test_create_failed(self):
+        clients.OpenStackClients.keystone().AndReturn(
+            fakes.FakeKeystoneClient())
+        neutronclient.Client.create_pool({
+            'pool': {
+                'subnet_id': 'sub123', 'protocol': u'HTTP',
+                'name': utils.PhysName('test_stack', 'pool'),
+                'lb_method': 'ROUND_ROBIN', 'admin_state_up': True}}
+        ).AndRaise(loadbalancer.NeutronClientException())
+        self.m.ReplayAll()
+
+        snippet = template_format.parse(pool_template)
+        stack = utils.parse_stack(snippet)
+        rsrc = loadbalancer.Pool(
+            'pool', snippet['Resources']['pool'], stack)
+        error = self.assertRaises(exception.ResourceFailure,
+                                  scheduler.TaskRunner(rsrc.create))
+        self.assertEqual(
+            'NeutronClientException: An unknown exception occurred.',
+            str(error))
+        self.assertEqual((rsrc.CREATE, rsrc.FAILED), rsrc.state)
+        self.m.VerifyAll()
+
+    def test_delete(self):
+        rsrc = self.create_pool()
+        neutronclient.Client.delete_vip('xyz')
+        neutronclient.Client.show_vip('xyz').AndRaise(
+            loadbalancer.NeutronClientException(status_code=404))
+        neutronclient.Client.delete_pool('5678')
+        neutronclient.Client.show_pool('5678').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()
+
+    def test_delete_already_gone(self):
+        neutronclient.Client.delete_vip('xyz').AndRaise(
+            loadbalancer.NeutronClientException(status_code=404))
+        neutronclient.Client.delete_pool('5678').AndRaise(
+            loadbalancer.NeutronClientException(status_code=404))
+
+        rsrc = self.create_pool()
+        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_vip_failed(self):
+        neutronclient.Client.delete_vip('xyz').AndRaise(
+            loadbalancer.NeutronClientException(status_code=400))
+
+        rsrc = self.create_pool()
+        self.m.ReplayAll()
+        scheduler.TaskRunner(rsrc.create)()
+        error = self.assertRaises(exception.ResourceFailure,
+                                  scheduler.TaskRunner(rsrc.delete))
+        self.assertEqual(
+            'NeutronClientException: An unknown exception occurred.',
+            str(error))
+        self.assertEqual((rsrc.DELETE, rsrc.FAILED), rsrc.state)
+        self.m.VerifyAll()
+
+    def test_delete_failed(self):
+        neutronclient.Client.delete_vip('xyz').AndRaise(
+            loadbalancer.NeutronClientException(status_code=404))
+        neutronclient.Client.delete_pool('5678').AndRaise(
+            loadbalancer.NeutronClientException(status_code=400))
+
+        rsrc = self.create_pool()
+        self.m.ReplayAll()
+        scheduler.TaskRunner(rsrc.create)()
+        error = self.assertRaises(exception.ResourceFailure,
+                                  scheduler.TaskRunner(rsrc.delete))
+        self.assertEqual(
+            'NeutronClientException: An unknown exception occurred.',
+            str(error))
+        self.assertEqual((rsrc.DELETE, rsrc.FAILED), rsrc.state)
+        self.m.VerifyAll()
+
+    def test_attribute(self):
+        rsrc = self.create_pool()
+        neutronclient.Client.show_pool('5678').MultipleTimes(
+        ).AndReturn(
+            {'pool': {'admin_state_up': True, 'lb_method': 'ROUND_ROBIN'}})
+        self.m.ReplayAll()
+        scheduler.TaskRunner(rsrc.create)()
+        self.assertEqual(True, rsrc.FnGetAtt('admin_state_up'))
+        self.assertEqual('ROUND_ROBIN', rsrc.FnGetAtt('lb_method'))
+        self.m.VerifyAll()
+
+    def test_vip_attribute(self):
+        rsrc = self.create_pool()
+        neutronclient.Client.show_vip('xyz').AndReturn(
+            {'vip': {'address': '10.0.0.3', 'name': 'xyz'}})
+        self.m.ReplayAll()
+        scheduler.TaskRunner(rsrc.create)()
+        self.assertEqual({'address': '10.0.0.3', 'name': 'xyz'},
+                         rsrc.FnGetAtt('vip'))
+        self.m.VerifyAll()
+
+    def test_attribute_failed(self):
+        rsrc = self.create_pool()
+        self.m.ReplayAll()
+        scheduler.TaskRunner(rsrc.create)()
+        error = self.assertRaises(exception.InvalidTemplateAttribute,
+                                  rsrc.FnGetAtt, 'net_id')
+        self.assertEqual(
+            'The Referenced Attribute (pool net_id) is incorrect.',
+            str(error))
+        self.m.VerifyAll()
+
+    def test_update(self):
+        rsrc = self.create_pool()
+        neutronclient.Client.update_pool(
+            '5678', {'pool': {'admin_state_up': False}})
+        self.m.ReplayAll()
+        scheduler.TaskRunner(rsrc.create)()
+
+        update_template = copy.deepcopy(rsrc.t)
+        update_template['Properties']['admin_state_up'] = False
+        self.assertEqual(None, rsrc.update(update_template))
+
+        self.m.VerifyAll()
+
+    def test_update_monitors(self):
+        clients.OpenStackClients.keystone().AndReturn(
+            fakes.FakeKeystoneClient())
+        neutronclient.Client.create_pool({
+            'pool': {
+                'subnet_id': 'sub123', 'protocol': u'HTTP',
+                'name': utils.PhysName('test_stack', 'pool'),
+                'lb_method': 'ROUND_ROBIN', 'admin_state_up': True}}
+        ).AndReturn({'pool': {'id': '5678'}})
+        neutronclient.Client.associate_health_monitor(
+            '5678', {'health_monitor': {'id': 'mon123'}})
+        neutronclient.Client.associate_health_monitor(
+            '5678', {'health_monitor': {'id': 'mon456'}})
+        neutronclient.Client.create_vip({
+            'vip': {
+                'protocol': u'HTTP', 'name': 'pool.vip',
+                'admin_state_up': True, 'subnet_id': u'sub123',
+                'pool_id': '5678', 'protocol_port': 80}}
+        ).AndReturn({'vip': {'id': 'xyz'}})
+        neutronclient.Client.show_pool('5678').AndReturn(
+            {'pool': {'status': 'ACTIVE'}})
+        neutronclient.Client.show_vip('xyz').AndReturn(
+            {'vip': {'status': 'ACTIVE'}})
+        neutronclient.Client.disassociate_health_monitor(
+            '5678', {'health_monitor': {'id': 'mon456'}})
+        neutronclient.Client.associate_health_monitor(
+            '5678', {'health_monitor': {'id': 'mon789'}})
+
+        snippet = template_format.parse(pool_template)
+        stack = utils.parse_stack(snippet)
+        snippet['Resources']['pool']['Properties']['monitors'] = [
+            'mon123', 'mon456']
+        rsrc = loadbalancer.Pool(
+            'pool', snippet['Resources']['pool'], stack)
+        self.m.ReplayAll()
+        scheduler.TaskRunner(rsrc.create)()
+
+        update_template = copy.deepcopy(rsrc.t)
+        update_template['Properties']['monitors'] = ['mon123', 'mon789']
+        self.assertEqual(None, rsrc.update(update_template))
+
+        self.m.VerifyAll()