heat/heat_integrationtests/functional/test_autoscaling.py

753 lines
31 KiB
Python

# 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 copy
import json
from heatclient import exc
from oslo_log import log as logging
import six
from testtools import matchers
from heat_integrationtests.common import test
from heat_integrationtests.functional import functional_base
LOG = logging.getLogger(__name__)
class AutoscalingGroupTest(functional_base.FunctionalTestsBase):
template = '''
{
"AWSTemplateFormatVersion" : "2010-09-09",
"Description" : "Template to create multiple instances.",
"Parameters" : {"size": {"Type": "String", "Default": "1"},
"AZ": {"Type": "String", "Default": "nova"},
"image": {"Type": "String"},
"flavor": {"Type": "String"}},
"Resources": {
"JobServerGroup": {
"Type" : "AWS::AutoScaling::AutoScalingGroup",
"Properties" : {
"AvailabilityZones" : [{"Ref": "AZ"}],
"LaunchConfigurationName" : { "Ref" : "JobServerConfig" },
"MinSize" : {"Ref": "size"},
"MaxSize" : "20"
}
},
"JobServerConfig" : {
"Type" : "AWS::AutoScaling::LaunchConfiguration",
"Metadata": {"foo": "bar"},
"Properties": {
"ImageId" : {"Ref": "image"},
"InstanceType" : {"Ref": "flavor"},
"SecurityGroups" : [ "sg-1" ],
"UserData" : "jsconfig data"
}
}
},
"Outputs": {
"InstanceList": {"Value": {
"Fn::GetAtt": ["JobServerGroup", "InstanceList"]}},
"JobServerConfigRef": {"Value": {
"Ref": "JobServerConfig"}}
}
}
'''
instance_template = '''
heat_template_version: 2013-05-23
parameters:
ImageId: {type: string}
InstanceType: {type: string}
SecurityGroups: {type: comma_delimited_list}
UserData: {type: string}
Tags: {type: comma_delimited_list, default: "x,y"}
resources:
random1:
type: OS::Heat::RandomString
properties:
salt: {get_param: ImageId}
outputs:
PublicIp: {value: {get_attr: [random1, value]}}
AvailabilityZone: {value: 'not-used11'}
PrivateDnsName: {value: 'not-used12'}
PublicDnsName: {value: 'not-used13'}
PrivateIp: {value: 'not-used14'}
'''
# This is designed to fail.
bad_instance_template = '''
heat_template_version: 2013-05-23
parameters:
ImageId: {type: string}
InstanceType: {type: string}
SecurityGroups: {type: comma_delimited_list}
UserData: {type: string}
Tags: {type: comma_delimited_list, default: "x,y"}
resources:
random1:
type: OS::Heat::RandomString
depends_on: waiter
ready_poster:
type: AWS::CloudFormation::WaitConditionHandle
waiter:
type: AWS::CloudFormation::WaitCondition
properties:
Handle: {get_resource: ready_poster}
Timeout: 1
outputs:
PublicIp:
value: {get_attr: [random1, value]}
'''
def setUp(self):
super(AutoscalingGroupTest, self).setUp()
if not self.conf.image_ref:
raise self.skipException("No image configured to test")
if not self.conf.minimal_image_ref:
raise self.skipException("No minimal image configured to test")
if not self.conf.instance_type:
raise self.skipException("No flavor configured to test")
def assert_instance_count(self, stack, expected_count):
inst_list = self._stack_output(stack, 'InstanceList')
self.assertEqual(expected_count, len(inst_list.split(',')))
def _assert_instance_state(self, nested_identifier,
num_complete, num_failed):
for res in self.client.resources.list(nested_identifier):
if 'COMPLETE' in res.resource_status:
num_complete = num_complete - 1
elif 'FAILED' in res.resource_status:
num_failed = num_failed - 1
self.assertEqual(0, num_failed)
self.assertEqual(0, num_complete)
class AutoscalingGroupBasicTest(AutoscalingGroupTest):
def test_basic_create_works(self):
"""Make sure the working case is good.
Note this combines test_override_aws_ec2_instance into this test as
well, which is:
If AWS::EC2::Instance is overridden, AutoScalingGroup will
automatically use that overridden resource type.
"""
files = {'provider.yaml': self.instance_template}
env = {'resource_registry': {'AWS::EC2::Instance': 'provider.yaml'},
'parameters': {'size': 4,
'image': self.conf.image_ref,
'flavor': self.conf.instance_type}}
stack_identifier = self.stack_create(template=self.template,
files=files, environment=env)
initial_resources = {
'JobServerConfig': 'AWS::AutoScaling::LaunchConfiguration',
'JobServerGroup': 'AWS::AutoScaling::AutoScalingGroup'}
self.assertEqual(initial_resources,
self.list_resources(stack_identifier))
stack = self.client.stacks.get(stack_identifier)
self.assert_instance_count(stack, 4)
def test_size_updates_work(self):
files = {'provider.yaml': self.instance_template}
env = {'resource_registry': {'AWS::EC2::Instance': 'provider.yaml'},
'parameters': {'size': 2,
'image': self.conf.image_ref,
'flavor': self.conf.instance_type}}
stack_identifier = self.stack_create(template=self.template,
files=files,
environment=env)
stack = self.client.stacks.get(stack_identifier)
self.assert_instance_count(stack, 2)
# Increase min size to 5
env2 = {'resource_registry': {'AWS::EC2::Instance': 'provider.yaml'},
'parameters': {'size': 5,
'image': self.conf.image_ref,
'flavor': self.conf.instance_type}}
self.update_stack(stack_identifier, self.template,
environment=env2, files=files)
stack = self.client.stacks.get(stack_identifier)
self.assert_instance_count(stack, 5)
def test_update_group_replace(self):
"""Make sure that during a group update the non updatable
properties cause a replacement.
"""
files = {'provider.yaml': self.instance_template}
env = {'resource_registry':
{'AWS::EC2::Instance': 'provider.yaml'},
'parameters': {'size': '1',
'image': self.conf.image_ref,
'flavor': self.conf.instance_type}}
stack_identifier = self.stack_create(template=self.template,
files=files,
environment=env)
rsrc = self.client.resources.get(stack_identifier, 'JobServerGroup')
orig_asg_id = rsrc.physical_resource_id
env2 = {'resource_registry':
{'AWS::EC2::Instance': 'provider.yaml'},
'parameters': {'size': '1',
'AZ': 'wibble',
'image': self.conf.image_ref,
'flavor': self.conf.instance_type}}
self.update_stack(stack_identifier, self.template,
environment=env2, files=files)
# replacement will cause the resource physical_resource_id to change.
rsrc = self.client.resources.get(stack_identifier, 'JobServerGroup')
self.assertNotEqual(orig_asg_id, rsrc.physical_resource_id)
def test_create_instance_error_causes_group_error(self):
"""If a resource in an instance group fails to be created, the instance
group itself will fail and the broken inner resource will remain.
"""
stack_name = self._stack_rand_name()
files = {'provider.yaml': self.bad_instance_template}
env = {'resource_registry': {'AWS::EC2::Instance': 'provider.yaml'},
'parameters': {'size': 2,
'image': self.conf.image_ref,
'flavor': self.conf.instance_type}}
self.client.stacks.create(
stack_name=stack_name,
template=self.template,
files=files,
disable_rollback=True,
parameters={},
environment=env
)
self.addCleanup(self._stack_delete, stack_name)
stack = self.client.stacks.get(stack_name)
stack_identifier = '%s/%s' % (stack_name, stack.id)
self._wait_for_stack_status(stack_identifier, 'CREATE_FAILED')
initial_resources = {
'JobServerConfig': 'AWS::AutoScaling::LaunchConfiguration',
'JobServerGroup': 'AWS::AutoScaling::AutoScalingGroup'}
self.assertEqual(initial_resources,
self.list_resources(stack_identifier))
nested_ident = self.assert_resource_is_a_stack(stack_identifier,
'JobServerGroup')
self._assert_instance_state(nested_ident, 0, 2)
def test_update_instance_error_causes_group_error(self):
"""If a resource in an instance group fails to be created during an
update, the instance group itself will fail and the broken inner
resource will remain.
"""
files = {'provider.yaml': self.instance_template}
env = {'resource_registry': {'AWS::EC2::Instance': 'provider.yaml'},
'parameters': {'size': 2,
'image': self.conf.image_ref,
'flavor': self.conf.instance_type}}
stack_identifier = self.stack_create(template=self.template,
files=files,
environment=env)
initial_resources = {
'JobServerConfig': 'AWS::AutoScaling::LaunchConfiguration',
'JobServerGroup': 'AWS::AutoScaling::AutoScalingGroup'}
self.assertEqual(initial_resources,
self.list_resources(stack_identifier))
stack = self.client.stacks.get(stack_identifier)
self.assert_instance_count(stack, 2)
nested_ident = self.assert_resource_is_a_stack(stack_identifier,
'JobServerGroup')
self._assert_instance_state(nested_ident, 2, 0)
initial_list = [res.resource_name
for res in self.client.resources.list(nested_ident)]
env['parameters']['size'] = 3
files2 = {'provider.yaml': self.bad_instance_template}
self.client.stacks.update(
stack_id=stack_identifier,
template=self.template,
files=files2,
disable_rollback=True,
parameters={},
environment=env
)
self._wait_for_stack_status(stack_identifier, 'UPDATE_FAILED')
# assert that there are 3 bad instances
nested_ident = self.assert_resource_is_a_stack(stack_identifier,
'JobServerGroup')
# 2 resources should be in update failed, and one create failed.
for res in self.client.resources.list(nested_ident):
if res.resource_name in initial_list:
self._wait_for_resource_status(nested_ident,
res.resource_name,
'UPDATE_FAILED')
else:
self._wait_for_resource_status(nested_ident,
res.resource_name,
'CREATE_FAILED')
def test_group_suspend_resume(self):
files = {'provider.yaml': self.instance_template}
env = {'resource_registry': {'AWS::EC2::Instance': 'provider.yaml'},
'parameters': {'size': 4,
'image': self.conf.image_ref,
'flavor': self.conf.instance_type}}
stack_identifier = self.stack_create(template=self.template,
files=files, environment=env)
nested_ident = self.assert_resource_is_a_stack(stack_identifier,
'JobServerGroup')
self.stack_suspend(stack_identifier)
self._wait_for_all_resource_status(nested_ident, 'SUSPEND_COMPLETE')
self.stack_resume(stack_identifier)
self._wait_for_all_resource_status(nested_ident, 'RESUME_COMPLETE')
class AutoscalingGroupUpdatePolicyTest(AutoscalingGroupTest):
def ig_tmpl_with_updt_policy(self):
templ = json.loads(copy.deepcopy(self.template))
up = {"AutoScalingRollingUpdate": {
"MinInstancesInService": "1",
"MaxBatchSize": "2",
"PauseTime": "PT1S"}}
templ['Resources']['JobServerGroup']['UpdatePolicy'] = up
return templ
def update_instance_group(self, updt_template,
num_updates_expected_on_updt,
num_creates_expected_on_updt,
num_deletes_expected_on_updt,
update_replace):
# setup stack from the initial template
files = {'provider.yaml': self.instance_template}
size = 10
env = {'resource_registry': {'AWS::EC2::Instance': 'provider.yaml'},
'parameters': {'size': size,
'image': self.conf.image_ref,
'flavor': self.conf.instance_type}}
stack_name = self._stack_rand_name()
stack_identifier = self.stack_create(
stack_name=stack_name,
template=self.ig_tmpl_with_updt_policy(),
files=files,
environment=env)
stack = self.client.stacks.get(stack_identifier)
nested_ident = self.assert_resource_is_a_stack(stack_identifier,
'JobServerGroup')
# test that physical resource name of launch configuration is used
conf_name = self._stack_output(stack, 'JobServerConfigRef')
conf_name_pattern = '%s-JobServerConfig-[a-zA-Z0-9]+$' % stack_name
self.assertThat(conf_name,
matchers.MatchesRegex(conf_name_pattern))
# test the number of instances created
self.assert_instance_count(stack, size)
# saves info from initial list of instances for comparison later
init_instances = self.client.resources.list(nested_ident)
init_names = [inst.resource_name for inst in init_instances]
# test stack update
self.update_stack(stack_identifier, updt_template,
environment=env, files=files)
updt_stack = self.client.stacks.get(stack_identifier)
# test that the launch configuration is replaced
updt_conf_name = self._stack_output(updt_stack, 'JobServerConfigRef')
self.assertThat(updt_conf_name,
matchers.MatchesRegex(conf_name_pattern))
self.assertNotEqual(conf_name, updt_conf_name)
# test that the group size are the same
updt_instances = self.client.resources.list(nested_ident)
updt_names = [inst.resource_name for inst in updt_instances]
self.assertEqual(len(init_names), len(updt_names))
for res in updt_instances:
self.assertEqual('UPDATE_COMPLETE', res.resource_status)
# test that the appropriate number of instance names are the same
matched_names = set(updt_names) & set(init_names)
self.assertEqual(num_updates_expected_on_updt, len(matched_names))
# test that the appropriate number of new instances are created
self.assertEqual(num_creates_expected_on_updt,
len(set(updt_names) - set(init_names)))
# test that the appropriate number of instances are deleted
self.assertEqual(num_deletes_expected_on_updt,
len(set(init_names) - set(updt_names)))
# test that the older instances are the ones being deleted
if num_deletes_expected_on_updt > 0:
deletes_expected = init_names[:num_deletes_expected_on_updt]
self.assertNotIn(deletes_expected, updt_names)
def test_instance_group_update_replace(self):
"""
Test simple update replace with no conflict in batch size and
minimum instances in service.
"""
updt_template = self.ig_tmpl_with_updt_policy()
grp = updt_template['Resources']['JobServerGroup']
policy = grp['UpdatePolicy']['AutoScalingRollingUpdate']
policy['MinInstancesInService'] = '1'
policy['MaxBatchSize'] = '3'
config = updt_template['Resources']['JobServerConfig']
config['Properties']['ImageId'] = self.conf.minimal_image_ref
self.update_instance_group(updt_template,
num_updates_expected_on_updt=10,
num_creates_expected_on_updt=0,
num_deletes_expected_on_updt=0,
update_replace=True)
def test_instance_group_update_replace_with_adjusted_capacity(self):
"""
Test update replace with capacity adjustment due to conflict in
batch size and minimum instances in service.
"""
updt_template = self.ig_tmpl_with_updt_policy()
grp = updt_template['Resources']['JobServerGroup']
policy = grp['UpdatePolicy']['AutoScalingRollingUpdate']
policy['MinInstancesInService'] = '8'
policy['MaxBatchSize'] = '4'
config = updt_template['Resources']['JobServerConfig']
config['Properties']['ImageId'] = self.conf.minimal_image_ref
self.update_instance_group(updt_template,
num_updates_expected_on_updt=8,
num_creates_expected_on_updt=2,
num_deletes_expected_on_updt=2,
update_replace=True)
def test_instance_group_update_replace_huge_batch_size(self):
"""
Test update replace with a huge batch size.
"""
updt_template = self.ig_tmpl_with_updt_policy()
group = updt_template['Resources']['JobServerGroup']
policy = group['UpdatePolicy']['AutoScalingRollingUpdate']
policy['MinInstancesInService'] = '0'
policy['MaxBatchSize'] = '20'
config = updt_template['Resources']['JobServerConfig']
config['Properties']['ImageId'] = self.conf.minimal_image_ref
self.update_instance_group(updt_template,
num_updates_expected_on_updt=10,
num_creates_expected_on_updt=0,
num_deletes_expected_on_updt=0,
update_replace=True)
def test_instance_group_update_replace_huge_min_in_service(self):
"""
Test update replace with a huge number of minimum instances in service.
"""
updt_template = self.ig_tmpl_with_updt_policy()
group = updt_template['Resources']['JobServerGroup']
policy = group['UpdatePolicy']['AutoScalingRollingUpdate']
policy['MinInstancesInService'] = '20'
policy['MaxBatchSize'] = '1'
policy['PauseTime'] = 'PT0S'
config = updt_template['Resources']['JobServerConfig']
config['Properties']['ImageId'] = self.conf.minimal_image_ref
self.update_instance_group(updt_template,
num_updates_expected_on_updt=9,
num_creates_expected_on_updt=1,
num_deletes_expected_on_updt=1,
update_replace=True)
def test_instance_group_update_no_replace(self):
"""
Test simple update only and no replace (i.e. updated instance flavor
in Launch Configuration) with no conflict in batch size and
minimum instances in service.
"""
updt_template = self.ig_tmpl_with_updt_policy()
group = updt_template['Resources']['JobServerGroup']
policy = group['UpdatePolicy']['AutoScalingRollingUpdate']
policy['MinInstancesInService'] = '1'
policy['MaxBatchSize'] = '3'
policy['PauseTime'] = 'PT0S'
config = updt_template['Resources']['JobServerConfig']
config['Properties']['InstanceType'] = 'm1.tiny'
self.update_instance_group(updt_template,
num_updates_expected_on_updt=10,
num_creates_expected_on_updt=0,
num_deletes_expected_on_updt=0,
update_replace=False)
def test_instance_group_update_no_replace_with_adjusted_capacity(self):
"""
Test update only and no replace (i.e. updated instance flavor in
Launch Configuration) with capacity adjustment due to conflict in
batch size and minimum instances in service.
"""
updt_template = self.ig_tmpl_with_updt_policy()
group = updt_template['Resources']['JobServerGroup']
policy = group['UpdatePolicy']['AutoScalingRollingUpdate']
policy['MinInstancesInService'] = '8'
policy['MaxBatchSize'] = '4'
policy['PauseTime'] = 'PT0S'
config = updt_template['Resources']['JobServerConfig']
config['Properties']['InstanceType'] = 'm1.tiny'
self.update_instance_group(updt_template,
num_updates_expected_on_updt=8,
num_creates_expected_on_updt=2,
num_deletes_expected_on_updt=2,
update_replace=False)
class AutoScalingSignalTest(AutoscalingGroupTest):
template = '''
{
"AWSTemplateFormatVersion" : "2010-09-09",
"Description" : "Template to create multiple instances.",
"Parameters" : {"size": {"Type": "String", "Default": "1"},
"AZ": {"Type": "String", "Default": "nova"},
"image": {"Type": "String"},
"flavor": {"Type": "String"}},
"Resources": {
"custom_lb": {
"Type": "AWS::EC2::Instance",
"Properties": {
"ImageId": {"Ref": "image"},
"InstanceType": {"Ref": "flavor"},
"UserData": "foo",
"SecurityGroups": [ "sg-1" ],
"Tags": []
},
"Metadata": {
"IPs": {"Fn::GetAtt": ["JobServerGroup", "InstanceList"]}
}
},
"JobServerGroup": {
"Type" : "AWS::AutoScaling::AutoScalingGroup",
"Properties" : {
"AvailabilityZones" : [{"Ref": "AZ"}],
"LaunchConfigurationName" : { "Ref" : "JobServerConfig" },
"DesiredCapacity" : {"Ref": "size"},
"MinSize" : "0",
"MaxSize" : "20"
}
},
"JobServerConfig" : {
"Type" : "AWS::AutoScaling::LaunchConfiguration",
"Metadata": {"foo": "bar"},
"Properties": {
"ImageId" : {"Ref": "image"},
"InstanceType" : {"Ref": "flavor"},
"SecurityGroups" : [ "sg-1" ],
"UserData" : "jsconfig data"
}
},
"ScaleUpPolicy" : {
"Type" : "AWS::AutoScaling::ScalingPolicy",
"Properties" : {
"AdjustmentType" : "ChangeInCapacity",
"AutoScalingGroupName" : { "Ref" : "JobServerGroup" },
"Cooldown" : "0",
"ScalingAdjustment": "1"
}
},
"ScaleDownPolicy" : {
"Type" : "AWS::AutoScaling::ScalingPolicy",
"Properties" : {
"AdjustmentType" : "ChangeInCapacity",
"AutoScalingGroupName" : { "Ref" : "JobServerGroup" },
"Cooldown" : "0",
"ScalingAdjustment" : "-2"
}
}
},
"Outputs": {
"InstanceList": {"Value": {
"Fn::GetAtt": ["JobServerGroup", "InstanceList"]}}
}
}
'''
lb_template = '''
heat_template_version: 2013-05-23
parameters:
ImageId: {type: string}
InstanceType: {type: string}
SecurityGroups: {type: comma_delimited_list}
UserData: {type: string}
Tags: {type: comma_delimited_list, default: "x,y"}
resources:
outputs:
PublicIp: {value: "not-used"}
AvailabilityZone: {value: 'not-used1'}
PrivateDnsName: {value: 'not-used2'}
PublicDnsName: {value: 'not-used3'}
PrivateIp: {value: 'not-used4'}
'''
def setUp(self):
super(AutoScalingSignalTest, self).setUp()
self.build_timeout = self.conf.build_timeout
self.build_interval = self.conf.build_interval
self.files = {'provider.yaml': self.instance_template,
'lb.yaml': self.lb_template}
self.env = {'resource_registry':
{'resources':
{'custom_lb': {'AWS::EC2::Instance': 'lb.yaml'}},
'AWS::EC2::Instance': 'provider.yaml'},
'parameters': {'size': 2,
'image': self.conf.image_ref,
'flavor': self.conf.instance_type}}
def check_instance_count(self, stack_identifier, expected):
md = self.client.resources.metadata(stack_identifier, 'custom_lb')
actual_md = len(md['IPs'].split(','))
if actual_md != expected:
LOG.warn('check_instance_count exp:%d, meta:%s' % (expected,
md['IPs']))
return False
stack = self.client.stacks.get(stack_identifier)
inst_list = self._stack_output(stack, 'InstanceList')
actual = len(inst_list.split(','))
if actual != expected:
LOG.warn('check_instance_count exp:%d, act:%s' % (expected,
inst_list))
return actual == expected
def test_scaling_meta_update(self):
"""Use heatclient to signal the up and down policy.
Then confirm that the metadata in the custom_lb is updated each
time.
"""
stack_identifier = self.stack_create(template=self.template,
files=self.files,
environment=self.env)
self.assertTrue(test.call_until_true(
self.build_timeout, self.build_interval,
self.check_instance_count, stack_identifier, 2))
nested_ident = self.assert_resource_is_a_stack(stack_identifier,
'JobServerGroup')
# Scale up one, Trigger alarm
self.client.resources.signal(stack_identifier, 'ScaleUpPolicy')
self._wait_for_stack_status(nested_ident, 'UPDATE_COMPLETE')
self.assertTrue(test.call_until_true(
self.build_timeout, self.build_interval,
self.check_instance_count, stack_identifier, 3))
# Scale down two, Trigger alarm
self.client.resources.signal(stack_identifier, 'ScaleDownPolicy')
self._wait_for_stack_status(nested_ident, 'UPDATE_COMPLETE')
self.assertTrue(test.call_until_true(
self.build_timeout, self.build_interval,
self.check_instance_count, stack_identifier, 1))
def test_signal_with_policy_update(self):
"""Prove that an updated policy is used in the next signal."""
stack_identifier = self.stack_create(template=self.template,
files=self.files,
environment=self.env)
self.assertTrue(test.call_until_true(
self.build_timeout, self.build_interval,
self.check_instance_count, stack_identifier, 2))
nested_ident = self.assert_resource_is_a_stack(stack_identifier,
'JobServerGroup')
# Scale up one, Trigger alarm
self.client.resources.signal(stack_identifier, 'ScaleUpPolicy')
self._wait_for_stack_status(nested_ident, 'UPDATE_COMPLETE')
self.assertTrue(test.call_until_true(
self.build_timeout, self.build_interval,
self.check_instance_count, stack_identifier, 3))
# increase the adjustment to "+2" and remove the DesiredCapacity
# so we don't go from 3 to 2.
new_template = self.template.replace(
'"ScalingAdjustment": "1"',
'"ScalingAdjustment": "2"').replace(
'"DesiredCapacity" : {"Ref": "size"},', '')
self.update_stack(stack_identifier, template=new_template,
environment=self.env, files=self.files)
# Scale up two, Trigger alarm
self.client.resources.signal(stack_identifier, 'ScaleUpPolicy')
self._wait_for_stack_status(nested_ident, 'UPDATE_COMPLETE')
self.assertTrue(test.call_until_true(
self.build_timeout, self.build_interval,
self.check_instance_count, stack_identifier, 5))
def test_signal_during_suspend(self):
"""Prove that a signal will fail when the stack is in suspend."""
stack_identifier = self.stack_create(template=self.template,
files=self.files,
environment=self.env)
self.assertTrue(test.call_until_true(
self.build_timeout, self.build_interval,
self.check_instance_count, stack_identifier, 2))
nested_ident = self.assert_resource_is_a_stack(stack_identifier,
'JobServerGroup')
# suspend the top level stack.
self.client.actions.suspend(stack_id=stack_identifier)
self._wait_for_resource_status(
stack_identifier, 'JobServerGroup', 'SUSPEND_COMPLETE')
# Send a signal and a exception will raise
ex = self.assertRaises(exc.BadRequest,
self.client.resources.signal,
stack_identifier, 'ScaleUpPolicy')
error_msg = 'Signal resource during SUSPEND is not supported'
self.assertIn(error_msg, six.text_type(ex))
ev = self.wait_for_event_with_reason(
stack_identifier,
reason='Cannot signal resource during SUSPEND',
rsrc_name='ScaleUpPolicy')
self.assertEqual('SUSPEND_COMPLETE', ev[0].resource_status)
# still SUSPEND_COMPLETE (not gone to UPDATE_COMPLETE)
self._wait_for_stack_status(nested_ident, 'SUSPEND_COMPLETE')
self._wait_for_stack_status(stack_identifier, 'SUSPEND_COMPLETE')
# still 2 instances.
self.assertTrue(test.call_until_true(
self.build_timeout, self.build_interval,
self.check_instance_count, stack_identifier, 2))