]> review.fuel-infra Code Review - openstack-build/heat-build.git/commitdiff
Add UpdatePolicy attribute to Instance/AutoScalingGroup
authorWinson Chan <winson.c.chan@intel.com>
Fri, 16 Aug 2013 18:35:04 +0000 (11:35 -0700)
committerWinson Chan <winson.c.chan@intel.com>
Wed, 21 Aug 2013 23:06:32 +0000 (16:06 -0700)
This is the second part of a series to implement support
for AutoScaling UpdatePolicy.

Defined new update_policy attribute for InstanceGroup
and AutoScalingGroup, and modified init of InstanceGroup and
AutoScalingGroup to parse UpdatePolicy from the template.
Currently, only InstanceGroup and AutoScalingGroup manages
update using UpdatePolicy and so this is not implemented in the
Resource class. This can be revisited when UpdatePolicy is
applicable to other resource types. The resource validation
method is also overridden here to validate the UpdatePolicy.
Included tests to validate various uses cases of templates
with, with bad, without, and removal of UpdatePolicy. This
patch does not address handling of instances update with
UpdatePolicy yet. The next patch will address that.

Change-Id: I1faf1a8a3ba1faf063b6687b34b57b2a96c6bfa8
blueprint: as-update-policy

heat/engine/resources/autoscaling.py
heat/tests/test_autoscaling_update_policy.py [new file with mode: 0644]
heat/tests/test_instance_group_update_policy.py

index 79ad56ae3579a8fdecb4ff884a8b0258b2cb2c61..04987b8892cee2df7d03f20b7930c86b89013512 100644 (file)
@@ -14,6 +14,7 @@
 #    under the License.
 
 import copy
+
 from heat.common import exception
 from heat.engine import resource
 from heat.engine import signal_responder
@@ -21,6 +22,7 @@ from heat.engine import signal_responder
 from heat.openstack.common import log as logging
 from heat.openstack.common import timeutils
 from heat.engine.properties import Properties
+from heat.engine import properties
 from heat.engine import stack_resource
 
 logger = logging.getLogger(__name__)
@@ -73,12 +75,43 @@ class InstanceGroup(stack_resource.StackResource):
                  'Schema': {'Type': 'Map',
                             'Schema': tags_schema}}
     }
-    update_allowed_keys = ('Properties',)
+    update_allowed_keys = ('Properties', 'UpdatePolicy',)
     update_allowed_properties = ('Size', 'LaunchConfigurationName',)
     attributes_schema = {
         "InstanceList": ("A comma-delimited list of server ip addresses. "
                          "(Heat extension)")
     }
+    rolling_update_schema = {
+        'MinInstancesInService': properties.Schema(properties.NUMBER,
+                                                   default=0),
+        'MaxBatchSize': properties.Schema(properties.NUMBER,
+                                          default=1),
+        'PauseTime': properties.Schema(properties.STRING,
+                                       default='PT0S')
+    }
+    update_policy_schema = {
+        'RollingUpdate': properties.Schema(properties.MAP,
+                                           schema=rolling_update_schema)
+    }
+
+    def __init__(self, name, json_snippet, stack):
+        """
+        UpdatePolicy is currently only specific to InstanceGroup and
+        AutoScalingGroup. Therefore, init is overridden to parse for the
+        UpdatePolicy.
+        """
+        super(InstanceGroup, self).__init__(name, json_snippet, stack)
+        self.update_policy = Properties(self.update_policy_schema,
+                                        self.t.get('UpdatePolicy', {}),
+                                        parent_name=self.name)
+
+    def validate(self):
+        """
+        Add validation for update_policy
+        """
+        super(InstanceGroup, self).validate()
+        if self.update_policy:
+            self.update_policy.validate()
 
     def get_instance_names(self):
         """Get a list of resource names of the instances in this InstanceGroup.
@@ -120,6 +153,14 @@ class InstanceGroup(stack_resource.StackResource):
         If Properties has changed, update self.properties, so we
         get the new values during any subsequent adjustment.
         """
+        if tmpl_diff:
+            # parse update policy
+            if 'UpdatePolicy' in tmpl_diff:
+                self.update_policy = Properties(
+                    self.update_policy_schema,
+                    json_snippet.get('UpdatePolicy', {}),
+                    parent_name=self.name)
+
         if prop_diff:
             self.properties = Properties(self.properties_schema,
                                          json_snippet.get('Properties', {}),
@@ -238,10 +279,22 @@ class AutoScalingGroup(InstanceGroup, CooldownMixin):
         'Tags': {'Type': 'List', 'Schema': {'Type': 'Map',
                                             'Schema': tags_schema}}
     }
+    rolling_update_schema = {
+        'MinInstancesInService': properties.Schema(properties.NUMBER,
+                                                   default=0),
+        'MaxBatchSize': properties.Schema(properties.NUMBER,
+                                          default=1),
+        'PauseTime': properties.Schema(properties.STRING,
+                                       default='PT0S')
+    }
+    update_policy_schema = {
+        'AutoScalingRollingUpdate': properties.Schema(
+            properties.MAP, schema=rolling_update_schema)
+    }
 
     # template keys and properties supported for handle_update,
     # note trailing comma is required for a single item to get a tuple
-    update_allowed_keys = ('Properties',)
+    update_allowed_keys = ('Properties', 'UpdatePolicy',)
     update_allowed_properties = ('LaunchConfigurationName',
                                  'MaxSize', 'MinSize',
                                  'Cooldown', 'DesiredCapacity',)
@@ -267,6 +320,14 @@ class AutoScalingGroup(InstanceGroup, CooldownMixin):
         If Properties has changed, update self.properties, so we get the new
         values during any subsequent adjustment.
         """
+        if tmpl_diff:
+            # parse update policy
+            if 'UpdatePolicy' in tmpl_diff:
+                self.update_policy = Properties(
+                    self.update_policy_schema,
+                    json_snippet.get('UpdatePolicy', {}),
+                    parent_name=self.name)
+
         if prop_diff:
             self.properties = Properties(self.properties_schema,
                                          json_snippet.get('Properties', {}),
diff --git a/heat/tests/test_autoscaling_update_policy.py b/heat/tests/test_autoscaling_update_policy.py
new file mode 100644 (file)
index 0000000..4fceef1
--- /dev/null
@@ -0,0 +1,382 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+#    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 re
+
+from heat.common import exception
+from heat.common import template_format
+from heat.engine.resources import instance
+from heat.engine import parser
+from heat.tests.common import HeatTestCase
+from heat.tests.utils import setup_dummy_db
+from heat.tests import utils
+
+
+asg_tmpl_without_updt_policy = '''
+{
+  "AWSTemplateFormatVersion" : "2010-09-09",
+  "Description" : "Template to create autoscaling group.",
+  "Parameters" : {},
+  "Resources" : {
+    "WebServerGroup" : {
+      "Type" : "AWS::AutoScaling::AutoScalingGroup",
+      "Properties" : {
+        "AvailabilityZones" : ["nova"],
+        "LaunchConfigurationName" : { "Ref" : "LaunchConfig" },
+        "MinSize" : "1",
+        "MaxSize" : "10"
+      }
+    },
+    "LaunchConfig" : {
+      "Type" : "AWS::AutoScaling::LaunchConfiguration",
+      "Properties": {
+        "ImageId"           : "foo",
+        "InstanceType"      : "m1.medium",
+        "KeyName"           : "test",
+        "SecurityGroups"    : [ "sg-1" ],
+        "UserData"          : "jsconfig data"
+      }
+    }
+  }
+}
+'''
+
+asg_tmpl_with_bad_updt_policy = '''
+{
+  "AWSTemplateFormatVersion" : "2010-09-09",
+  "Description" : "Template to create autoscaling group.",
+  "Parameters" : {},
+  "Resources" : {
+    "WebServerGroup" : {
+      "UpdatePolicy": {
+        "foo": {
+        }
+      },
+      "Type" : "AWS::AutoScaling::AutoScalingGroup",
+      "Properties" : {
+        "AvailabilityZones" : ["nova"],
+        "LaunchConfigurationName" : { "Ref" : "LaunchConfig" },
+        "MinSize" : "1",
+        "MaxSize" : "10"
+      }
+    },
+    "LaunchConfig" : {
+      "Type" : "AWS::AutoScaling::LaunchConfiguration",
+      "Properties": {
+        "ImageId"           : "foo",
+        "InstanceType"      : "m1.medium",
+        "KeyName"           : "test",
+        "SecurityGroups"    : [ "sg-1" ],
+        "UserData"          : "jsconfig data"
+      }
+    }
+  }
+}
+'''
+
+asg_tmpl_with_default_updt_policy = '''
+{
+  "AWSTemplateFormatVersion" : "2010-09-09",
+  "Description" : "Template to create autoscaling group.",
+  "Parameters" : {},
+  "Resources" : {
+    "WebServerGroup" : {
+      "UpdatePolicy" : {
+        "AutoScalingRollingUpdate" : {
+        }
+      },
+      "Type" : "AWS::AutoScaling::AutoScalingGroup",
+      "Properties" : {
+        "AvailabilityZones" : ["nova"],
+        "LaunchConfigurationName" : { "Ref" : "LaunchConfig" },
+        "MinSize" : "1",
+        "MaxSize" : "10"
+      }
+    },
+    "LaunchConfig" : {
+      "Type" : "AWS::AutoScaling::LaunchConfiguration",
+      "Properties": {
+        "ImageId"           : "foo",
+        "InstanceType"      : "m1.medium",
+        "KeyName"           : "test",
+        "SecurityGroups"    : [ "sg-1" ],
+        "UserData"          : "jsconfig data"
+      }
+    }
+  }
+}
+'''
+
+asg_tmpl_with_updt_policy_1 = '''
+{
+  "AWSTemplateFormatVersion" : "2010-09-09",
+  "Description" : "Template to create autoscaling group.",
+  "Parameters" : {},
+  "Resources" : {
+    "WebServerGroup" : {
+      "UpdatePolicy" : {
+        "AutoScalingRollingUpdate" : {
+          "MinInstancesInService" : "1",
+          "MaxBatchSize" : "3",
+          "PauseTime" : "PT30S"
+        }
+      },
+      "Type" : "AWS::AutoScaling::AutoScalingGroup",
+      "Properties" : {
+        "AvailabilityZones" : ["nova"],
+        "LaunchConfigurationName" : { "Ref" : "LaunchConfig" },
+        "MinSize" : "1",
+        "MaxSize" : "10"
+      }
+    },
+    "LaunchConfig" : {
+      "Type" : "AWS::AutoScaling::LaunchConfiguration",
+      "Properties": {
+        "ImageId"           : "foo",
+        "InstanceType"      : "m1.medium",
+        "KeyName"           : "test",
+        "SecurityGroups"    : [ "sg-1" ],
+        "UserData"          : "jsconfig data"
+      }
+    }
+  }
+}
+'''
+
+asg_tmpl_with_updt_policy_2 = '''
+{
+  "AWSTemplateFormatVersion" : "2010-09-09",
+  "Description" : "Template to create autoscaling group.",
+  "Parameters" : {},
+  "Resources" : {
+    "WebServerGroup" : {
+      "UpdatePolicy" : {
+        "AutoScalingRollingUpdate" : {
+          "MinInstancesInService" : "1",
+          "MaxBatchSize" : "5",
+          "PauseTime" : "PT30S"
+        }
+      },
+      "Type" : "AWS::AutoScaling::AutoScalingGroup",
+      "Properties" : {
+        "AvailabilityZones" : ["nova"],
+        "LaunchConfigurationName" : { "Ref" : "LaunchConfig" },
+        "MinSize" : "1",
+        "MaxSize" : "10"
+      }
+    },
+    "LaunchConfig" : {
+      "Type" : "AWS::AutoScaling::LaunchConfiguration",
+      "Properties": {
+        "ImageId"           : "foo",
+        "InstanceType"      : "m1.large",
+        "KeyName"           : "test",
+        "SecurityGroups"    : [ "sg-1" ],
+        "UserData"          : "jsconfig data"
+      }
+    }
+  }
+}
+'''
+
+
+class InstanceGroupTest(HeatTestCase):
+    def setUp(self):
+        super(InstanceGroupTest, self).setUp()
+        setup_dummy_db()
+
+    def _stub_create(self, num, instance_class=instance.Instance):
+        """
+        Expect creation of C{num} number of Instances.
+
+        :param instance_class: The resource class to expect to be created
+                               instead of instance.Instance.
+        """
+
+        self.m.StubOutWithMock(parser.Stack, 'validate')
+        parser.Stack.validate()
+
+        self.m.StubOutWithMock(instance_class, 'handle_create')
+        self.m.StubOutWithMock(instance_class, 'check_create_complete')
+        cookie = object()
+        for x in range(num):
+            instance_class.handle_create().AndReturn(cookie)
+        instance_class.check_create_complete(cookie).AndReturn(False)
+        instance_class.check_create_complete(
+            cookie).MultipleTimes().AndReturn(True)
+
+    def get_launch_conf_name(self, stack, ig_name):
+        return stack.resources[ig_name].properties['LaunchConfigurationName']
+
+    def test_parse_without_update_policy(self):
+        tmpl = template_format.parse(asg_tmpl_without_updt_policy)
+        stack = utils.parse_stack(tmpl)
+        grp = stack.resources['WebServerGroup']
+        self.assertFalse(grp.update_policy['AutoScalingRollingUpdate'])
+
+    def test_parse_with_update_policy(self):
+        tmpl = template_format.parse(asg_tmpl_with_updt_policy_1)
+        stack = utils.parse_stack(tmpl)
+        grp = stack.resources['WebServerGroup']
+        self.assertTrue(grp.update_policy)
+        self.assertTrue(len(grp.update_policy) == 1)
+        self.assertTrue('AutoScalingRollingUpdate' in grp.update_policy)
+        policy = grp.update_policy['AutoScalingRollingUpdate']
+        self.assertTrue(policy and len(policy) > 0)
+        self.assertEqual(int(policy['MinInstancesInService']), 1)
+        self.assertEqual(int(policy['MaxBatchSize']), 3)
+        self.assertEqual(policy['PauseTime'], 'PT30S')
+
+    def test_parse_with_default_update_policy(self):
+        tmpl = template_format.parse(asg_tmpl_with_default_updt_policy)
+        stack = utils.parse_stack(tmpl)
+        grp = stack.resources['WebServerGroup']
+        self.assertTrue(grp.update_policy)
+        self.assertTrue(len(grp.update_policy) == 1)
+        self.assertTrue('AutoScalingRollingUpdate' in grp.update_policy)
+        policy = grp.update_policy['AutoScalingRollingUpdate']
+        self.assertTrue(policy and len(policy) > 0)
+        self.assertEqual(int(policy['MinInstancesInService']), 0)
+        self.assertEqual(int(policy['MaxBatchSize']), 1)
+        self.assertEqual(policy['PauseTime'], 'PT0S')
+
+    def test_parse_with_bad_update_policy(self):
+        tmpl = template_format.parse(asg_tmpl_with_bad_updt_policy)
+        stack = utils.parse_stack(tmpl)
+        self.assertRaises(exception.StackValidationFailed, stack.validate)
+
+    def validate_update_policy_diff(self, current, updated):
+
+        # load current stack
+        current_tmpl = template_format.parse(current)
+        current_stack = utils.parse_stack(current_tmpl)
+
+        # get the json snippet for the current InstanceGroup resource
+        current_grp = current_stack.resources['WebServerGroup']
+        current_snippets = dict((r.name, r.parsed_template())
+                                for r in current_stack)
+        current_grp_json = current_snippets[current_grp.name]
+
+        # load the updated stack
+        updated_tmpl = template_format.parse(updated)
+        updated_stack = utils.parse_stack(updated_tmpl)
+
+        # get the updated json snippet for the InstanceGroup resource in the
+        # context of the current stack
+        updated_grp = updated_stack.resources['WebServerGroup']
+        updated_grp_json = current_stack.resolve_runtime_data(updated_grp.t)
+
+        # identify the template difference
+        tmpl_diff = updated_grp.update_template_diff(
+            updated_grp_json, current_grp_json)
+        updated_policy = (updated_grp.t['UpdatePolicy']
+                          if 'UpdatePolicy' in updated_grp.t else None)
+        expected = {u'UpdatePolicy': updated_policy}
+        self.assertEqual(tmpl_diff, expected)
+
+    def test_update_policy_added(self):
+        self.validate_update_policy_diff(asg_tmpl_without_updt_policy,
+                                         asg_tmpl_with_updt_policy_1)
+
+    def test_update_policy_updated(self):
+        self.validate_update_policy_diff(asg_tmpl_with_updt_policy_1,
+                                         asg_tmpl_with_updt_policy_2)
+
+    def test_update_policy_removed(self):
+        self.validate_update_policy_diff(asg_tmpl_with_updt_policy_1,
+                                         asg_tmpl_without_updt_policy)
+
+    def test_autoscaling_group_update(self):
+
+        # setup stack from the initial template
+        tmpl = template_format.parse(asg_tmpl_with_updt_policy_1)
+        stack = utils.parse_stack(tmpl)
+        nested = stack.resources['WebServerGroup'].nested()
+
+        # test stack create
+        # test the number of instance creation
+        # test that physical resource name of launch configuration is used
+        size = int(stack.resources['WebServerGroup'].properties['MinSize'])
+        self._stub_create(size)
+        self.m.ReplayAll()
+        stack.create()
+        self.m.VerifyAll()
+        self.assertEqual(stack.state, ('CREATE', 'COMPLETE'))
+        conf = stack.resources['LaunchConfig']
+        conf_name_pattern = '%s-LaunchConfig-[a-zA-Z0-9]+$' % stack.name
+        regex_pattern = re.compile(conf_name_pattern)
+        self.assertTrue(regex_pattern.match(conf.FnGetRefId()))
+        nested = stack.resources['WebServerGroup'].nested()
+        self.assertTrue(len(nested.resources), size)
+
+        # test stack update
+        # test that update policy is updated
+        # test that launch configuration is replaced
+        current_grp = stack.resources['WebServerGroup']
+        self.assertTrue('AutoScalingRollingUpdate'
+                        in current_grp.update_policy)
+        current_policy = current_grp.update_policy['AutoScalingRollingUpdate']
+        self.assertTrue(current_policy and len(current_policy) > 0)
+        self.assertEqual(int(current_policy['MaxBatchSize']), 3)
+        conf_name = self.get_launch_conf_name(stack, 'WebServerGroup')
+        updated_tmpl = template_format.parse(asg_tmpl_with_updt_policy_2)
+        updated_stack = utils.parse_stack(updated_tmpl)
+        stack.update(updated_stack)
+        self.assertEqual(stack.state, ('UPDATE', 'COMPLETE'))
+        updated_grp = stack.resources['WebServerGroup']
+        self.assertTrue('AutoScalingRollingUpdate'
+                        in updated_grp.update_policy)
+        updated_policy = updated_grp.update_policy['AutoScalingRollingUpdate']
+        self.assertTrue(updated_policy and len(updated_policy) > 0)
+        self.assertEqual(int(updated_policy['MaxBatchSize']), 5)
+        updated_conf_name = self.get_launch_conf_name(stack, 'WebServerGroup')
+        self.assertNotEqual(conf_name, updated_conf_name)
+
+    def test_autoscaling_group_update_policy_removed(self):
+
+        # setup stack from the initial template
+        tmpl = template_format.parse(asg_tmpl_with_updt_policy_1)
+        stack = utils.parse_stack(tmpl)
+        nested = stack.resources['WebServerGroup'].nested()
+
+        # test stack create
+        # test the number of instance creation
+        # test that physical resource name of launch configuration is used
+        size = int(stack.resources['WebServerGroup'].properties['MinSize'])
+        self._stub_create(size)
+        self.m.ReplayAll()
+        stack.create()
+        self.m.VerifyAll()
+        self.assertEqual(stack.state, ('CREATE', 'COMPLETE'))
+        conf = stack.resources['LaunchConfig']
+        conf_name_pattern = '%s-LaunchConfig-[a-zA-Z0-9]+$' % stack.name
+        regex_pattern = re.compile(conf_name_pattern)
+        self.assertTrue(regex_pattern.match(conf.FnGetRefId()))
+        nested = stack.resources['WebServerGroup'].nested()
+        self.assertTrue(len(nested.resources), size)
+
+        # test stack update
+        # test that update policy is removed
+        current_grp = stack.resources['WebServerGroup']
+        self.assertTrue('AutoScalingRollingUpdate'
+                        in current_grp.update_policy)
+        current_policy = current_grp.update_policy['AutoScalingRollingUpdate']
+        self.assertTrue(current_policy and len(current_policy) > 0)
+        self.assertEqual(int(current_policy['MaxBatchSize']), 3)
+        updated_tmpl = template_format.parse(asg_tmpl_without_updt_policy)
+        updated_stack = utils.parse_stack(updated_tmpl)
+        stack.update(updated_stack)
+        self.assertEqual(stack.state, ('UPDATE', 'COMPLETE'))
+        updated_grp = stack.resources['WebServerGroup']
+        self.assertFalse(updated_grp.update_policy['AutoScalingRollingUpdate'])
index 2ac000b15f74669ae3303ac89541392871dabcbe..bdffeeb4787a2de9bd8671562bdba238b15213b0 100644 (file)
 
 import re
 
+from heat.common import exception
 from heat.common import template_format
 from heat.engine.resources import instance
 from heat.engine import parser
 from heat.tests.common import HeatTestCase
 from heat.tests.utils import setup_dummy_db
-from heat.tests.utils import parse_stack
+from heat.tests import utils
 
 
-ig_template_before = '''
+ig_tmpl_without_updt_policy = '''
 {
   "AWSTemplateFormatVersion" : "2010-09-09",
   "Description" : "Template to create multiple instances.",
@@ -50,13 +51,118 @@ ig_template_before = '''
 }
 '''
 
-ig_template_after = '''
+ig_tmpl_with_bad_updt_policy = '''
 {
   "AWSTemplateFormatVersion" : "2010-09-09",
   "Description" : "Template to create multiple instances.",
   "Parameters" : {},
   "Resources" : {
     "JobServerGroup" : {
+      "UpdatePolicy" : {
+        "RollingUpdate": "foo"
+      },
+      "Type" : "OS::Heat::InstanceGroup",
+      "Properties" : {
+        "LaunchConfigurationName" : { "Ref" : "JobServerConfig" },
+        "Size" : "8",
+        "AvailabilityZones" : ["nova"]
+      }
+    },
+    "JobServerConfig" : {
+      "Type" : "AWS::AutoScaling::LaunchConfiguration",
+      "Properties": {
+        "ImageId"           : "foo",
+        "InstanceType"      : "m1.medium",
+        "KeyName"           : "test",
+        "SecurityGroups"    : [ "sg-1" ],
+        "UserData"          : "jsconfig data"
+      }
+    }
+  }
+}
+'''
+
+ig_tmpl_with_default_updt_policy = '''
+{
+  "AWSTemplateFormatVersion" : "2010-09-09",
+  "Description" : "Template to create multiple instances.",
+  "Parameters" : {},
+  "Resources" : {
+    "JobServerGroup" : {
+      "UpdatePolicy" : {
+        "RollingUpdate" : {
+        }
+      },
+      "Type" : "OS::Heat::InstanceGroup",
+      "Properties" : {
+        "LaunchConfigurationName" : { "Ref" : "JobServerConfig" },
+        "Size" : "8",
+        "AvailabilityZones" : ["nova"]
+      }
+    },
+    "JobServerConfig" : {
+      "Type" : "AWS::AutoScaling::LaunchConfiguration",
+      "Properties": {
+        "ImageId"           : "foo",
+        "InstanceType"      : "m1.medium",
+        "KeyName"           : "test",
+        "SecurityGroups"    : [ "sg-1" ],
+        "UserData"          : "jsconfig data"
+      }
+    }
+  }
+}
+'''
+
+ig_tmpl_with_updt_policy_1 = '''
+{
+  "AWSTemplateFormatVersion" : "2010-09-09",
+  "Description" : "Template to create multiple instances.",
+  "Parameters" : {},
+  "Resources" : {
+    "JobServerGroup" : {
+      "UpdatePolicy" : {
+        "RollingUpdate" : {
+          "MinInstancesInService" : "1",
+          "MaxBatchSize" : "3",
+          "PauseTime" : "PT30S"
+        }
+      },
+      "Type" : "OS::Heat::InstanceGroup",
+      "Properties" : {
+        "LaunchConfigurationName" : { "Ref" : "JobServerConfig" },
+        "Size" : "8",
+        "AvailabilityZones" : ["nova"]
+      }
+    },
+    "JobServerConfig" : {
+      "Type" : "AWS::AutoScaling::LaunchConfiguration",
+      "Properties": {
+        "ImageId"           : "foo",
+        "InstanceType"      : "m1.medium",
+        "KeyName"           : "test",
+        "SecurityGroups"    : [ "sg-1" ],
+        "UserData"          : "jsconfig data"
+      }
+    }
+  }
+}
+'''
+
+ig_tmpl_with_updt_policy_2 = '''
+{
+  "AWSTemplateFormatVersion" : "2010-09-09",
+  "Description" : "Template to create multiple instances.",
+  "Parameters" : {},
+  "Resources" : {
+    "JobServerGroup" : {
+      "UpdatePolicy" : {
+        "RollingUpdate" : {
+          "MinInstancesInService" : "1",
+          "MaxBatchSize" : "5",
+          "PauseTime" : "PT30S"
+        }
+      },
       "Type" : "OS::Heat::InstanceGroup",
       "Properties" : {
         "LaunchConfigurationName" : { "Ref" : "JobServerConfig" },
@@ -107,11 +213,90 @@ class InstanceGroupTest(HeatTestCase):
     def get_launch_conf_name(self, stack, ig_name):
         return stack.resources[ig_name].properties['LaunchConfigurationName']
 
-    def test_instance_group(self):
+    def test_parse_without_update_policy(self):
+        tmpl = template_format.parse(ig_tmpl_without_updt_policy)
+        stack = utils.parse_stack(tmpl)
+        grp = stack.resources['JobServerGroup']
+        self.assertFalse(grp.update_policy['RollingUpdate'])
+
+    def test_parse_with_update_policy(self):
+        tmpl = template_format.parse(ig_tmpl_with_updt_policy_1)
+        stack = utils.parse_stack(tmpl)
+        grp = stack.resources['JobServerGroup']
+        self.assertTrue(grp.update_policy)
+        self.assertTrue(len(grp.update_policy) == 1)
+        self.assertTrue('RollingUpdate' in grp.update_policy)
+        policy = grp.update_policy['RollingUpdate']
+        self.assertTrue(policy and len(policy) > 0)
+        self.assertEqual(int(policy['MinInstancesInService']), 1)
+        self.assertEqual(int(policy['MaxBatchSize']), 3)
+        self.assertEqual(policy['PauseTime'], 'PT30S')
+
+    def test_parse_with_default_update_policy(self):
+        tmpl = template_format.parse(ig_tmpl_with_default_updt_policy)
+        stack = utils.parse_stack(tmpl)
+        grp = stack.resources['JobServerGroup']
+        self.assertTrue(grp.update_policy)
+        self.assertTrue(len(grp.update_policy) == 1)
+        self.assertTrue('RollingUpdate' in grp.update_policy)
+        policy = grp.update_policy['RollingUpdate']
+        self.assertTrue(policy and len(policy) > 0)
+        self.assertEqual(int(policy['MinInstancesInService']), 0)
+        self.assertEqual(int(policy['MaxBatchSize']), 1)
+        self.assertEqual(policy['PauseTime'], 'PT0S')
+
+    def test_parse_with_bad_update_policy(self):
+        tmpl = template_format.parse(ig_tmpl_with_bad_updt_policy)
+        stack = utils.parse_stack(tmpl)
+        self.assertRaises(exception.StackValidationFailed, stack.validate)
+
+    def validate_update_policy_diff(self, current, updated):
+
+        # load current stack
+        current_tmpl = template_format.parse(current)
+        current_stack = utils.parse_stack(current_tmpl)
+
+        # get the json snippet for the current InstanceGroup resource
+        current_grp = current_stack.resources['JobServerGroup']
+        current_snippets = dict((r.name, r.parsed_template())
+                                for r in current_stack)
+        current_grp_json = current_snippets[current_grp.name]
+
+        # load the updated stack
+        updated_tmpl = template_format.parse(updated)
+        updated_stack = utils.parse_stack(updated_tmpl)
+
+        # get the updated json snippet for the InstanceGroup resource in the
+        # context of the current stack
+        updated_grp = updated_stack.resources['JobServerGroup']
+        updated_grp_json = current_stack.resolve_runtime_data(updated_grp.t)
+
+        # identify the template difference
+        tmpl_diff = updated_grp.update_template_diff(
+            updated_grp_json, current_grp_json)
+        updated_policy = (updated_grp.t['UpdatePolicy']
+                          if 'UpdatePolicy' in updated_grp.t else None)
+        expected = {u'UpdatePolicy': updated_policy}
+        self.assertEqual(tmpl_diff, expected)
+
+    def test_update_policy_added(self):
+        self.validate_update_policy_diff(ig_tmpl_without_updt_policy,
+                                         ig_tmpl_with_updt_policy_1)
+
+    def test_update_policy_updated(self):
+        self.validate_update_policy_diff(ig_tmpl_with_updt_policy_1,
+                                         ig_tmpl_with_updt_policy_2)
+
+    def test_update_policy_removed(self):
+        self.validate_update_policy_diff(ig_tmpl_with_updt_policy_1,
+                                         ig_tmpl_without_updt_policy)
+
+    def test_instance_group_update(self):
 
         # setup stack from the initial template
-        tmpl = template_format.parse(ig_template_before)
-        stack = parse_stack(tmpl)
+        tmpl = template_format.parse(ig_tmpl_with_updt_policy_1)
+        stack = utils.parse_stack(tmpl)
+        nested = stack.resources['JobServerGroup'].nested()
 
         # test stack create
         # test the number of instance creation
@@ -121,17 +306,68 @@ class InstanceGroupTest(HeatTestCase):
         self.m.ReplayAll()
         stack.create()
         self.m.VerifyAll()
-        self.assertEqual(stack.status, stack.COMPLETE)
+        self.assertEqual(stack.state, ('CREATE', 'COMPLETE'))
         conf = stack.resources['JobServerConfig']
         conf_name_pattern = '%s-JobServerConfig-[a-zA-Z0-9]+$' % stack.name
         regex_pattern = re.compile(conf_name_pattern)
         self.assertTrue(regex_pattern.match(conf.FnGetRefId()))
+        nested = stack.resources['JobServerGroup'].nested()
+        self.assertTrue(len(nested.resources), size)
 
         # test stack update
+        # test that update policy is updated
         # test that launch configuration is replaced
+        current_grp = stack.resources['JobServerGroup']
+        self.assertTrue('RollingUpdate' in current_grp.update_policy)
+        current_policy = current_grp.update_policy['RollingUpdate']
+        self.assertTrue(current_policy and len(current_policy) > 0)
+        self.assertEqual(int(current_policy['MaxBatchSize']), 3)
         conf_name = self.get_launch_conf_name(stack, 'JobServerGroup')
-        updated_tmpl = template_format.parse(ig_template_after)
-        updated_stack = parse_stack(updated_tmpl)
+        updated_tmpl = template_format.parse(ig_tmpl_with_updt_policy_2)
+        updated_stack = utils.parse_stack(updated_tmpl)
         stack.update(updated_stack)
+        self.assertEqual(stack.state, ('UPDATE', 'COMPLETE'))
+        updated_grp = stack.resources['JobServerGroup']
+        self.assertTrue('RollingUpdate' in updated_grp.update_policy)
+        updated_policy = updated_grp.update_policy['RollingUpdate']
+        self.assertTrue(updated_policy and len(updated_policy) > 0)
+        self.assertEqual(int(updated_policy['MaxBatchSize']), 5)
         updated_conf_name = self.get_launch_conf_name(stack, 'JobServerGroup')
         self.assertNotEqual(conf_name, updated_conf_name)
+
+    def test_instance_group_update_policy_removed(self):
+
+        # setup stack from the initial template
+        tmpl = template_format.parse(ig_tmpl_with_updt_policy_1)
+        stack = utils.parse_stack(tmpl)
+        nested = stack.resources['JobServerGroup'].nested()
+
+        # test stack create
+        # test the number of instance creation
+        # test that physical resource name of launch configuration is used
+        size = int(stack.resources['JobServerGroup'].properties['Size'])
+        self._stub_create(size)
+        self.m.ReplayAll()
+        stack.create()
+        self.m.VerifyAll()
+        self.assertEqual(stack.state, ('CREATE', 'COMPLETE'))
+        conf = stack.resources['JobServerConfig']
+        conf_name_pattern = '%s-JobServerConfig-[a-zA-Z0-9]+$' % stack.name
+        regex_pattern = re.compile(conf_name_pattern)
+        self.assertTrue(regex_pattern.match(conf.FnGetRefId()))
+        nested = stack.resources['JobServerGroup'].nested()
+        self.assertTrue(len(nested.resources), size)
+
+        # test stack update
+        # test that update policy is removed
+        current_grp = stack.resources['JobServerGroup']
+        self.assertTrue('RollingUpdate' in current_grp.update_policy)
+        current_policy = current_grp.update_policy['RollingUpdate']
+        self.assertTrue(current_policy and len(current_policy) > 0)
+        self.assertEqual(int(current_policy['MaxBatchSize']), 3)
+        updated_tmpl = template_format.parse(ig_tmpl_without_updt_policy)
+        updated_stack = utils.parse_stack(updated_tmpl)
+        stack.update(updated_stack)
+        self.assertEqual(stack.state, ('UPDATE', 'COMPLETE'))
+        updated_grp = stack.resources['JobServerGroup']
+        self.assertFalse(updated_grp.update_policy['RollingUpdate'])