Merge "Test case for resource_facade"
diff --git a/common/clients.py b/common/clients.py
index c7a7f60..daba708 100644
--- a/common/clients.py
+++ b/common/clients.py
@@ -28,7 +28,7 @@
calling various OpenStack APIs.
"""
- CINDERCLIENT_VERSION = '1'
+ CINDERCLIENT_VERSION = '2'
HEATCLIENT_VERSION = '1'
NOVACLIENT_VERSION = '2'
CEILOMETER_VERSION = '2'
diff --git a/common/test.py b/common/test.py
index 1039625..21c5ba1 100644
--- a/common/test.py
+++ b/common/test.py
@@ -250,9 +250,9 @@
def _verify_status(self, stack, stack_identifier, status, fail_regexp):
if stack.stack_status == status:
- # Handle UPDATE_COMPLETE case: Make sure we don't
- # wait for a stale UPDATE_COMPLETE status.
- if status == 'UPDATE_COMPLETE':
+ # Handle UPDATE_COMPLETE/FAILED case: Make sure we don't
+ # wait for a stale UPDATE_COMPLETE/FAILED status.
+ if status in ('UPDATE_FAILED', 'UPDATE_COMPLETE'):
if self.updated_time.get(
stack_identifier) != stack.updated_time:
self.updated_time[stack_identifier] = stack.updated_time
@@ -263,8 +263,8 @@
wait_for_action = status.split('_')[0]
if (stack.action == wait_for_action and
fail_regexp.search(stack.stack_status)):
- # Handle UPDATE_FAILED case.
- if status == 'UPDATE_FAILED':
+ # Handle UPDATE_COMPLETE/UPDATE_FAILED case.
+ if status in ('UPDATE_FAILED', 'UPDATE_COMPLETE'):
if self.updated_time.get(
stack_identifier) != stack.updated_time:
self.updated_time[stack_identifier] = stack.updated_time
@@ -279,7 +279,7 @@
stack_status_reason=stack.stack_status_reason)
def _wait_for_stack_status(self, stack_identifier, status,
- failure_pattern='^.*_FAILED$',
+ failure_pattern=None,
success_on_not_found=False):
"""
Waits for a Stack to reach a given status.
@@ -288,7 +288,13 @@
CREATE_COMPLETE, not just COMPLETE which is exposed
via the status property of Stack in heatclient
"""
- fail_regexp = re.compile(failure_pattern)
+ if failure_pattern:
+ fail_regexp = re.compile(failure_pattern)
+ elif 'FAILED' in status:
+ # If we're looking for e.g CREATE_FAILED, COMPLETE is unexpected.
+ fail_regexp = re.compile('^.*_COMPLETE$')
+ else:
+ fail_regexp = re.compile('^.*_FAILED$')
build_timeout = self.conf.build_timeout
build_interval = self.conf.build_interval
@@ -323,10 +329,11 @@
stack_identifier, 'DELETE_COMPLETE',
success_on_not_found=True)
- def update_stack(self, stack_identifier, template, environment=None,
+ def update_stack(self, stack_identifier, template=None, environment=None,
files=None, parameters=None, tags=None,
expected_status='UPDATE_COMPLETE',
- disable_rollback=True):
+ disable_rollback=True,
+ existing=False):
env = environment or {}
env_files = files or {}
parameters = parameters or {}
@@ -335,6 +342,8 @@
build_timeout = self.conf.build_timeout
build_interval = self.conf.build_interval
start = timeutils.utcnow()
+ self.updated_time[stack_identifier] = self.client.stacks.get(
+ stack_identifier).updated_time
while timeutils.delta_seconds(start,
timeutils.utcnow()) < build_timeout:
try:
@@ -346,7 +355,8 @@
disable_rollback=disable_rollback,
parameters=parameters,
environment=env,
- tags=tags
+ tags=tags,
+ existing=existing
)
except heat_exceptions.HTTPConflict as ex:
# FIXME(sirushtim): Wait a little for the stack lock to be
diff --git a/common/test_resources/test_resource.py b/common/test_resources/test_resource.py
deleted file mode 100644
index 55255a6..0000000
--- a/common/test_resources/test_resource.py
+++ /dev/null
@@ -1,134 +0,0 @@
-#
-# 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 eventlet
-
-from heat.common.i18n import _
-from heat.engine import attributes
-from heat.engine import properties
-from heat.engine import resource
-from heat.engine import support
-from oslo_log import log as logging
-
-LOG = logging.getLogger(__name__)
-
-
-class TestResource(resource.Resource):
- '''
- A resource which stores the string value that was provided.
-
- This resource is to be used only for testing.
- It has control knobs such as 'update_replace', 'fail', 'wait_secs'
-
- '''
-
- support_status = support.SupportStatus(version='2014.1')
-
- PROPERTIES = (
- VALUE, UPDATE_REPLACE, FAIL, WAIT_SECS
- ) = (
- 'value', 'update_replace', 'fail', 'wait_secs'
- )
-
- ATTRIBUTES = (
- OUTPUT,
- ) = (
- 'output',
- )
-
- properties_schema = {
- VALUE: properties.Schema(
- properties.Schema.STRING,
- _('The input string to be stored.'),
- default='test_string',
- update_allowed=True
- ),
- FAIL: properties.Schema(
- properties.Schema.BOOLEAN,
- _('Value which can be set to fail the resource operation '
- 'to test failure scenarios.'),
- update_allowed=True,
- default=False
- ),
- UPDATE_REPLACE: properties.Schema(
- properties.Schema.BOOLEAN,
- _('Value which can be set to trigger update replace for '
- 'the particular resource'),
- update_allowed=True,
- default=False
- ),
- WAIT_SECS: properties.Schema(
- properties.Schema.NUMBER,
- _('Value which can be set for resource to wait after an action '
- 'is performed.'),
- update_allowed=True,
- default=0,
- ),
- }
-
- attributes_schema = {
- OUTPUT: attributes.Schema(
- _('The string that was stored. This value is '
- 'also available by referencing the resource.'),
- cache_mode=attributes.Schema.CACHE_NONE
- ),
- }
-
- def handle_create(self):
- value = self.properties.get(self.VALUE)
- fail_prop = self.properties.get(self.FAIL)
- sleep_secs = self.properties.get(self.WAIT_SECS)
-
- self.data_set('value', value, redact=False)
- self.resource_id_set(self.physical_resource_name())
-
- # sleep for specified time
- if sleep_secs:
- LOG.debug("Resource %s sleeping for %s seconds",
- self.name, sleep_secs)
- eventlet.sleep(sleep_secs)
-
- # emulate failure
- if fail_prop:
- raise ValueError("Test Resource failed %s" % self.name)
-
- def handle_update(self, json_snippet=None, tmpl_diff=None, prop_diff=None):
- value = prop_diff.get(self.VALUE)
- new_prop = json_snippet._properties
- if value:
- update_replace = new_prop.get(self.UPDATE_REPLACE, False)
- if update_replace:
- raise resource.UpdateReplace(self.name)
- else:
- fail_prop = new_prop.get(self.FAIL, False)
- sleep_secs = new_prop.get(self.WAIT_SECS, 0)
- # emulate failure
- if fail_prop:
- raise Exception("Test Resource failed %s", self.name)
- # update in place
- self.data_set('value', value, redact=False)
-
- if sleep_secs:
- LOG.debug("Update of Resource %s sleeping for %s seconds",
- self.name, sleep_secs)
- eventlet.sleep(sleep_secs)
-
- def _resolve_attribute(self, name):
- if name == self.OUTPUT:
- return self.data().get('value')
-
-
-def resource_mapping():
- return {
- 'OS::Heat::TestResource': TestResource,
- }
diff --git a/functional/test_autoscaling.py b/functional/test_autoscaling.py
index 1b9fe99..9041405 100644
--- a/functional/test_autoscaling.py
+++ b/functional/test_autoscaling.py
@@ -13,7 +13,9 @@
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
@@ -728,8 +730,13 @@
self._wait_for_resource_status(
stack_identifier, 'JobServerGroup', 'SUSPEND_COMPLETE')
- # Send a signal and confirm nothing happened.
- self.client.resources.signal(stack_identifier, 'ScaleUpPolicy')
+ # 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',
diff --git a/functional/test_create_update.py b/functional/test_create_update.py
index 87d7a23..4597d4c 100644
--- a/functional/test_create_update.py
+++ b/functional/test_create_update.py
@@ -26,7 +26,10 @@
'value': 'Test1',
'fail': False,
'update_replace': False,
- 'wait_secs': 0
+ 'wait_secs': 0,
+ 'action_wait_secs': {'create': 1},
+ 'client_name': 'nova',
+ 'entity_name': 'servers',
}
}
}
@@ -42,7 +45,8 @@
'value': 'Test1',
'fail': False,
'update_replace': False,
- 'wait_secs': 0
+ 'wait_secs': 0,
+ 'action_wait_secs': {'update': 1}
}
},
'test2': {
@@ -98,11 +102,15 @@
provider_group_template = '''
heat_template_version: 2013-05-23
+parameters:
+ count:
+ type: number
+ default: 2
resources:
test_group:
type: OS::Heat::ResourceGroup
properties:
- count: 2
+ count: {get_param: count}
resource_def:
type: My::TestResource
'''
@@ -130,6 +138,20 @@
user_data: {get_param: user_data}
'''
+ fail_param_template = '''
+heat_template_version: 2014-10-16
+parameters:
+ do_fail:
+ type: boolean
+ default: False
+resources:
+ aresource:
+ type: OS::Heat::TestResource
+ properties:
+ value: Test
+ fail: {get_param: do_fail}
+'''
+
def setUp(self):
super(UpdateStackTest, self).setUp()
@@ -239,6 +261,29 @@
self.assertEqual(updated_resources,
self.list_resources(stack_identifier))
+ def test_stack_update_from_failed(self):
+ # Prove it's possible to update from an UPDATE_FAILED state
+ template = _change_rsrc_properties(test_template_one_resource,
+ ['test1'],
+ {'value': 'test_update_failed'})
+ stack_identifier = self.stack_create(
+ template=template)
+ initial_resources = {'test1': 'OS::Heat::TestResource'}
+ self.assertEqual(initial_resources,
+ self.list_resources(stack_identifier))
+
+ tmpl_update = _change_rsrc_properties(
+ test_template_one_resource, ['test1'], {'fail': True})
+ # Update with bad template, we should fail
+ self.update_stack(stack_identifier, tmpl_update,
+ expected_status='UPDATE_FAILED')
+ # but then passing a good template should succeed
+ self.update_stack(stack_identifier, test_template_two_resource)
+ updated_resources = {'test1': 'OS::Heat::TestResource',
+ 'test2': 'OS::Heat::TestResource'}
+ self.assertEqual(updated_resources,
+ self.list_resources(stack_identifier))
+
def test_stack_update_provider(self):
template = _change_rsrc_properties(
test_template_one_resource, ['test1'],
@@ -290,7 +335,7 @@
'''Test two-level nested update.'''
# Create a ResourceGroup (which creates a nested stack),
# containing provider resources (which create a nested
- # stack), thus excercising an update which traverses
+ # stack), thus exercising an update which traverses
# two levels of nesting.
template = _change_rsrc_properties(
test_template_one_resource, ['test1'],
@@ -387,3 +432,69 @@
stack_identifier,
template=self.update_userdata_template,
parameters=parms_updated)
+
+ def test_stack_update_provider_group_patch(self):
+ '''Test two-level nested update with PATCH'''
+ template = _change_rsrc_properties(
+ test_template_one_resource, ['test1'],
+ {'value': 'test_provider_group_template'})
+ files = {'provider.template': json.dumps(template)}
+ env = {'resource_registry':
+ {'My::TestResource': 'provider.template'}}
+
+ stack_identifier = self.stack_create(
+ template=self.provider_group_template,
+ files=files,
+ environment=env
+ )
+
+ initial_resources = {'test_group': 'OS::Heat::ResourceGroup'}
+ self.assertEqual(initial_resources,
+ self.list_resources(stack_identifier))
+
+ # Prove the resource is backed by a nested stack, save the ID
+ nested_identifier = self.assert_resource_is_a_stack(stack_identifier,
+ 'test_group')
+
+ # Then check the expected resources are in the nested stack
+ nested_resources = {'0': 'My::TestResource',
+ '1': 'My::TestResource'}
+ self.assertEqual(nested_resources,
+ self.list_resources(nested_identifier))
+
+ # increase the count, pass only the paramter, no env or template
+ params = {'count': 3}
+ self.update_stack(stack_identifier, parameters=params, existing=True)
+
+ # Parent resources should be unchanged and the nested stack
+ # should have been updated in-place without replacement
+ self.assertEqual(initial_resources,
+ self.list_resources(stack_identifier))
+
+ # Resource group stack should also be unchanged (but updated)
+ nested_stack = self.client.stacks.get(nested_identifier)
+ self.assertEqual('UPDATE_COMPLETE', nested_stack.stack_status)
+ # Add a resource, as we should have added one
+ nested_resources['2'] = 'My::TestResource'
+ self.assertEqual(nested_resources,
+ self.list_resources(nested_identifier))
+
+ def test_stack_update_from_failed_patch(self):
+ '''Test PATCH update from a failed state.'''
+
+ # Start with empty template
+ stack_identifier = self.stack_create(
+ template='heat_template_version: 2014-10-16')
+
+ # Update with a good template, but bad parameter
+ self.update_stack(stack_identifier,
+ template=self.fail_param_template,
+ parameters={'do_fail': True},
+ expected_status='UPDATE_FAILED')
+
+ # PATCH update, only providing the parameter
+ self.update_stack(stack_identifier,
+ parameters={'do_fail': False},
+ existing=True)
+ self.assertEqual({u'aresource': u'OS::Heat::TestResource'},
+ self.list_resources(stack_identifier))
diff --git a/functional/test_create_update_neutron_port.py b/functional/test_create_update_neutron_port.py
index 4b2df59..575d21c 100644
--- a/functional/test_create_update_neutron_port.py
+++ b/functional/test_create_update_neutron_port.py
@@ -10,8 +10,6 @@
# License for the specific language governing permissions and limitations
# under the License.
-from testtools import testcase
-
from heat_integrationtests.functional import functional_base
@@ -38,6 +36,12 @@
fixed_ips:
- subnet: {get_resource: subnet}
ip_address: 11.11.11.11
+ test:
+ depends_on: port
+ type: OS::Heat::TestResource
+ properties:
+ value: Test1
+ fail: False
outputs:
port_ip:
value: {get_attr: [port, fixed_ips, 0, ip_address]}
@@ -73,7 +77,6 @@
self.assertNotEqual(_ip, new_ip)
self.assertNotEqual(_id, new_id)
- @testcase.skip('Skipped until bug #1455100 is fixed.')
def test_stack_update_replace_with_ip(self):
# create with default 'mac' parameter
stack_identifier = self.stack_create(template=test_template)
@@ -92,6 +95,62 @@
self.assertEqual(_ip, new_ip)
self.assertNotEqual(_id, new_id)
+ def test_stack_update_replace_with_ip_rollback(self):
+ # create with default 'mac' parameter
+ stack_identifier = self.stack_create(template=test_template)
+
+ _id, _ip = self.get_port_id_and_ip(stack_identifier)
+
+ # Update with another 'mac' parameter
+ parameters = {'mac': '00-00-00-00-AA-AA'}
+
+ # make test resource failing during update
+ fail_template = test_template.replace('fail: False',
+ 'fail: True')
+ fail_template = fail_template.replace('value: Test1',
+ 'value: Rollback')
+
+ # port should be replaced with same ip
+ self.update_stack(stack_identifier, fail_template,
+ parameters=parameters,
+ expected_status='ROLLBACK_COMPLETE',
+ disable_rollback=False)
+
+ new_id, new_ip = self.get_port_id_and_ip(stack_identifier)
+ # port id and ip should be the same after rollback
+ self.assertEqual(_ip, new_ip)
+ self.assertEqual(_id, new_id)
+
+ def test_stack_update_replace_with_ip_after_failed_update(self):
+ # create with default 'mac' parameter
+ stack_identifier = self.stack_create(template=test_template)
+
+ _id, _ip = self.get_port_id_and_ip(stack_identifier)
+
+ # Update with another 'mac' parameter
+ parameters = {'mac': '00-00-00-00-AA-AA'}
+
+ # make test resource failing during update
+ fail_template = test_template.replace('fail: False',
+ 'fail: True')
+ fail_template = fail_template.replace('value: Test1',
+ 'value: Rollback')
+
+ # port should be replaced with same ip
+ self.update_stack(stack_identifier, fail_template,
+ parameters=parameters,
+ expected_status='UPDATE_FAILED')
+
+ # port should be replaced with same ip
+ self.update_stack(stack_identifier, test_template,
+ parameters=parameters)
+
+ new_id, new_ip = self.get_port_id_and_ip(stack_identifier)
+ # ip should be the same, but port id should be different, because it's
+ # restore replace
+ self.assertEqual(_ip, new_ip)
+ self.assertNotEqual(_id, new_id)
+
def test_stack_update_in_place_remove_ip(self):
# create with default 'mac' parameter and defined ip_address
stack_identifier = self.stack_create(template=test_template)
diff --git a/functional/test_software_config.py b/functional/test_software_config.py
index 019e6c9..af7d671 100644
--- a/functional/test_software_config.py
+++ b/functional/test_software_config.py
@@ -83,7 +83,7 @@
2,
5,
deploy_count)
- deploy_count = self.deploy_many_configs(
+ self.deploy_many_configs(
stack_identifier,
server,
config_stacks,
@@ -95,24 +95,15 @@
for config_stack in config_stacks:
self._wait_for_stack_status(config_stack, 'CREATE_COMPLETE')
- def deploy_many_configs(self, stack_identifier, server, config_stacks,
+ def deploy_many_configs(self, stack, server, config_stacks,
stack_count, deploys_per_stack,
deploy_count_start):
for a in range(stack_count):
config_stacks.append(
self.deploy_config(server, deploys_per_stack))
- for config_stack in config_stacks:
- self.wait_for_deploy_physical_id(config_stack)
-
new_count = deploy_count_start + stack_count * deploys_per_stack
- server_metadata = self.client.resources.metadata(
- stack_identifier, 'server')
- self.assertEqual(
- new_count,
- len(server_metadata['deployments']),
- '%s stacks with %s deployments' % (stack_count, deploys_per_stack)
- )
+ self.wait_for_deploy_metadata_set(stack, new_count)
return new_count
def deploy_config(self, server, deploy_count):
@@ -128,19 +119,16 @@
enable_cleanup=self.enable_cleanup,
expected_status=None)
- def wait_for_deploy_physical_id(self, stack):
+ def wait_for_deploy_metadata_set(self, stack, deploy_count):
build_timeout = self.conf.build_timeout
build_interval = self.conf.build_interval
start = timeutils.utcnow()
while timeutils.delta_seconds(start,
timeutils.utcnow()) < build_timeout:
- created = True
- for res in self.client.resources.list(stack, nested_depth='2'):
- if not res.physical_resource_id:
- created = False
- break
- if created:
+ server_metadata = self.client.resources.metadata(
+ stack, 'server')
+ if len(server_metadata['deployments']) == deploy_count:
return
time.sleep(build_interval)
diff --git a/functional/test_stack_events.py b/functional/test_stack_events.py
new file mode 100644
index 0000000..b1b2339
--- /dev/null
+++ b/functional/test_stack_events.py
@@ -0,0 +1,112 @@
+# 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.
+
+from heat_integrationtests.functional import functional_base
+
+
+class StackEventsTest(functional_base.FunctionalTestsBase):
+
+ template = '''
+heat_template_version: 2014-10-16
+parameters:
+resources:
+ test_resource:
+ type: OS::Heat::TestResource
+ properties:
+ value: 'test1'
+ fail: False
+ update_replace: False
+ wait_secs: 0
+outputs:
+ resource_id:
+ description: 'ID of resource'
+ value: { get_resource: test_resource }
+'''
+
+ def setUp(self):
+ super(StackEventsTest, self).setUp()
+
+ def _verify_event_fields(self, event, event_characteristics):
+ self.assertIsNotNone(event_characteristics)
+ self.assertIsNotNone(event.event_time)
+ self.assertIsNotNone(event.links)
+ self.assertIsNotNone(event.logical_resource_id)
+ self.assertIsNotNone(event.resource_status)
+ self.assertIn(event.resource_status, event_characteristics[1])
+ self.assertIsNotNone(event.resource_status_reason)
+ self.assertIsNotNone(event.id)
+
+ def test_event(self):
+ parameters = {}
+
+ test_stack_name = self._stack_rand_name()
+ stack_identifier = self.stack_create(
+ stack_name=test_stack_name,
+ template=self.template,
+ parameters=parameters
+ )
+
+ expected_status = ['CREATE_IN_PROGRESS', 'CREATE_COMPLETE']
+ event_characteristics = {
+ test_stack_name: ('OS::Heat::Stack', expected_status),
+ 'test_resource': ('OS::Heat::TestResource', expected_status)}
+
+ # List stack events
+ # API: GET /v1/{tenant_id}/stacks/{stack_name}/{stack_id}/events
+ stack_events = self.client.events.list(stack_identifier)
+
+ for stack_event in stack_events:
+ # Key on an expected/valid resource name
+ self._verify_event_fields(
+ stack_event,
+ event_characteristics[stack_event.resource_name])
+
+ # Test the event filtering API based on this resource_name
+ # /v1/{tenant_id}/stacks/{stack_name}/{stack_id}/resources/{resource_name}/events
+ resource_events = self.client.events.list(
+ stack_identifier,
+ stack_event.resource_name)
+
+ # Resource events are a subset of the original stack event list
+ self.assertTrue(len(resource_events) < len(stack_events))
+
+ # Get the event details for each resource event
+ for resource_event in resource_events:
+ # A resource_event should be in the original stack event list
+ self.assertIn(resource_event, stack_events)
+ # Given a filtered list, the resource names should be identical
+ self.assertEqual(
+ resource_event.resource_name,
+ stack_event.resource_name)
+ # Verify all fields, keying off the resource_name
+ self._verify_event_fields(
+ resource_event,
+ event_characteristics[resource_event.resource_name])
+
+ # Exercise the event details API
+ # /v1/{tenant_id}/stacks/{stack_name}/{stack_id}/resources/{resource_name}/events/{event_id}
+ event_details = self.client.events.get(
+ stack_identifier,
+ resource_event.resource_name,
+ resource_event.id)
+ self._verify_event_fields(
+ event_details,
+ event_characteristics[event_details.resource_name])
+ # The names should be identical to the non-detailed event
+ self.assertEqual(
+ resource_event.resource_name,
+ event_details.resource_name)
+ # Verify the extra field in the detail results
+ self.assertIsNotNone(event_details.resource_type)
+ self.assertEqual(
+ event_characteristics[event_details.resource_name][0],
+ event_details.resource_type)
diff --git a/functional/test_stack_tags.py b/functional/test_stack_tags.py
index 05600f5..4a97798 100644
--- a/functional/test_stack_tags.py
+++ b/functional/test_stack_tags.py
@@ -19,11 +19,18 @@
heat_template_version: 2014-10-16
description:
foo
+parameters:
+ input:
+ type: string
+ default: test
+resources:
+ not-used:
+ type: OS::Heat::TestResource
+ properties:
+ wait_secs: 1
+ value: {get_param: input}
'''
- def setUp(self):
- super(StackTagTest, self).setUp()
-
def test_stack_tag(self):
# Stack create with stack tags
tags = 'foo,bar'
@@ -41,7 +48,8 @@
self.update_stack(
stack_identifier,
template=self.template,
- tags=updated_tags)
+ tags=updated_tags,
+ parameters={'input': 'next'})
# Ensure property tag is populated and matches updated tags
updated_stack = self.client.stacks.get(stack_identifier)
@@ -50,7 +58,8 @@
# Delete tags
self.update_stack(
stack_identifier,
- template=self.template
+ template=self.template,
+ parameters={'input': 'none'}
)
# Ensure property tag is not populated
diff --git a/functional/test_template_resource.py b/functional/test_template_resource.py
index 6ece7d1..9609664 100644
--- a/functional/test_template_resource.py
+++ b/functional/test_template_resource.py
@@ -12,6 +12,8 @@
import json
+from heatclient import exc as heat_exceptions
+import six
import yaml
from heat_integrationtests.common import test
@@ -749,3 +751,91 @@
self.stack_suspend(stack_identifier=stack_identifier)
self.stack_resume(stack_identifier=stack_identifier)
+
+
+class ValidateFacadeTest(test.HeatIntegrationTest):
+ """Prove that nested stack errors don't suck."""
+ template = '''
+heat_template_version: 2015-10-15
+resources:
+ thisone:
+ type: OS::Thingy
+ properties:
+ one: pre
+ two: post
+outputs:
+ one:
+ value: {get_attr: [thisone, here-it-is]}
+'''
+ templ_facade = '''
+heat_template_version: 2015-04-30
+parameters:
+ one:
+ type: string
+ two:
+ type: string
+outputs:
+ here-it-is:
+ value: noop
+'''
+ env = '''
+resource_registry:
+ OS::Thingy: facade.yaml
+ resources:
+ thisone:
+ OS::Thingy: concrete.yaml
+'''
+
+ def setUp(self):
+ super(ValidateFacadeTest, self).setUp()
+ self.client = self.orchestration_client
+
+ def test_missing_param(self):
+ templ_missing_parameter = '''
+heat_template_version: 2015-04-30
+parameters:
+ one:
+ type: string
+resources:
+ str:
+ type: OS::Heat::RandomString
+outputs:
+ here-it-is:
+ value:
+ not-important
+'''
+ try:
+ self.stack_create(
+ template=self.template,
+ environment=self.env,
+ files={'facade.yaml': self.templ_facade,
+ 'concrete.yaml': templ_missing_parameter},
+ expected_status='CREATE_FAILED')
+ except heat_exceptions.HTTPBadRequest as exc:
+ exp = ('ERROR: Required property two for facade '
+ 'OS::Thingy missing in provider')
+ self.assertEqual(exp, six.text_type(exc))
+
+ def test_missing_output(self):
+ templ_missing_output = '''
+heat_template_version: 2015-04-30
+parameters:
+ one:
+ type: string
+ two:
+ type: string
+resources:
+ str:
+ type: OS::Heat::RandomString
+'''
+ try:
+ self.stack_create(
+ template=self.template,
+ environment=self.env,
+ files={'facade.yaml': self.templ_facade,
+ 'concrete.yaml': templ_missing_output},
+ expected_status='CREATE_FAILED')
+ except heat_exceptions.HTTPBadRequest as exc:
+ exp = ('ERROR: Attribute here-it-is for facade '
+ 'OS::Thingy missing in provider')
+ self.assertEqual(exp, six.text_type(exc))
diff --git a/functional/test_template_validate.py b/functional/test_template_validate.py
new file mode 100644
index 0000000..e62c31b
--- /dev/null
+++ b/functional/test_template_validate.py
@@ -0,0 +1,244 @@
+# 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 six
+
+from heatclient import exc
+
+from heat_integrationtests.functional import functional_base
+
+
+class StackTemplateValidateTest(functional_base.FunctionalTestsBase):
+
+ random_template = '''
+heat_template_version: 2014-10-16
+description: the stack description
+parameters:
+ aparam:
+ type: number
+ default: 10
+ description: the param description
+resources:
+ myres:
+ type: OS::Heat::RandomString
+ properties:
+ length: {get_param: aparam}
+'''
+
+ parent_template = '''
+heat_template_version: 2014-10-16
+description: the parent template
+parameters:
+ pparam:
+ type: number
+ default: 5
+ description: the param description
+resources:
+ nres:
+ type: mynested.yaml
+ properties:
+ aparam: {get_param: pparam}
+'''
+
+ parent_template_noprop = '''
+heat_template_version: 2014-10-16
+description: the parent template
+resources:
+ nres:
+ type: mynested.yaml
+'''
+
+ random_template_groups = '''
+heat_template_version: 2014-10-16
+description: the stack description
+parameters:
+ aparam:
+ type: number
+ default: 10
+ description: the param description
+ bparam:
+ type: string
+ default: foo
+ cparam:
+ type: string
+ default: secret
+ hidden: true
+parameter_groups:
+- label: str_params
+ description: The string params
+ parameters:
+ - bparam
+ - cparam
+resources:
+ myres:
+ type: OS::Heat::RandomString
+ properties:
+ length: {get_param: aparam}
+'''
+
+ def test_template_validate_basic(self):
+ ret = self.client.stacks.validate(template=self.random_template)
+ expected = {'Description': 'the stack description',
+ 'Parameters': {
+ 'aparam': {'Default': 10,
+ 'Description': 'the param description',
+ 'Label': 'aparam',
+ 'NoEcho': 'false',
+ 'Type': 'Number'}}}
+ self.assertEqual(expected, ret)
+
+ def test_template_validate_override_default(self):
+ env = {'parameters': {'aparam': 5}}
+ ret = self.client.stacks.validate(template=self.random_template,
+ environment=env)
+ expected = {'Description': 'the stack description',
+ 'Parameters': {
+ 'aparam': {'Default': 10,
+ 'Value': 5,
+ 'Description': 'the param description',
+ 'Label': 'aparam',
+ 'NoEcho': 'false',
+ 'Type': 'Number'}}}
+ self.assertEqual(expected, ret)
+
+ def test_template_validate_override_none(self):
+ env = {'resource_registry': {
+ 'OS::Heat::RandomString': 'OS::Heat::None'}}
+ ret = self.client.stacks.validate(template=self.random_template,
+ environment=env)
+ expected = {'Description': 'the stack description',
+ 'Parameters': {
+ 'aparam': {'Default': 10,
+ 'Description': 'the param description',
+ 'Label': 'aparam',
+ 'NoEcho': 'false',
+ 'Type': 'Number'}}}
+ self.assertEqual(expected, ret)
+
+ def test_template_validate_basic_required_param(self):
+ tmpl = self.random_template.replace('default: 10', '')
+ ret = self.client.stacks.validate(template=tmpl)
+ expected = {'Description': 'the stack description',
+ 'Parameters': {
+ 'aparam': {'Description': 'the param description',
+ 'Label': 'aparam',
+ 'NoEcho': 'false',
+ 'Type': 'Number'}}}
+ self.assertEqual(expected, ret)
+
+ def test_template_validate_fail_version(self):
+ fail_template = self.random_template.replace('2014-10-16', 'invalid')
+ ex = self.assertRaises(exc.HTTPBadRequest,
+ self.client.stacks.validate,
+ template=fail_template)
+ self.assertIn('The template version is invalid', six.text_type(ex))
+
+ def test_template_validate_parameter_groups(self):
+ ret = self.client.stacks.validate(template=self.random_template_groups)
+ expected = {'Description': 'the stack description',
+ 'ParameterGroups':
+ [{'description': 'The string params',
+ 'label': 'str_params',
+ 'parameters': ['bparam', 'cparam']}],
+ 'Parameters':
+ {'aparam':
+ {'Default': 10,
+ 'Description': 'the param description',
+ 'Label': 'aparam',
+ 'NoEcho': 'false',
+ 'Type': 'Number'},
+ 'bparam':
+ {'Default': 'foo',
+ 'Description': '',
+ 'Label': 'bparam',
+ 'NoEcho': 'false',
+ 'Type': 'String'},
+ 'cparam':
+ {'Default': 'secret',
+ 'Description': '',
+ 'Label': 'cparam',
+ 'NoEcho': 'true',
+ 'Type': 'String'}}}
+ self.assertEqual(expected, ret)
+
+ def test_template_validate_nested_off(self):
+ files = {'mynested.yaml': self.random_template}
+ ret = self.client.stacks.validate(template=self.parent_template,
+ files=files)
+ expected = {'Description': 'the parent template',
+ 'Parameters': {
+ 'pparam': {'Default': 5,
+ 'Description': 'the param description',
+ 'Label': 'pparam',
+ 'NoEcho': 'false',
+ 'Type': 'Number'}}}
+ self.assertEqual(expected, ret)
+
+ def test_template_validate_nested_on(self):
+ files = {'mynested.yaml': self.random_template}
+ ret = self.client.stacks.validate(template=self.parent_template_noprop,
+ files=files,
+ show_nested=True)
+ expected = {'Description': 'the parent template',
+ 'Parameters': {},
+ 'NestedParameters': {
+ 'nres': {'Description': 'the stack description',
+ 'Parameters': {'aparam': {'Default': 10,
+ 'Description':
+ 'the param '
+ 'description',
+ 'Label': 'aparam',
+ 'NoEcho': 'false',
+ 'Type': 'Number'}},
+ 'Type': 'mynested.yaml'}}}
+ self.assertEqual(expected, ret)
+
+ def test_template_validate_nested_on_multiple(self):
+ # parent_template -> nested_template -> random_template
+ nested_template = self.random_template.replace(
+ 'OS::Heat::RandomString', 'mynested2.yaml')
+ files = {'mynested.yaml': nested_template,
+ 'mynested2.yaml': self.random_template}
+ ret = self.client.stacks.validate(template=self.parent_template,
+ files=files,
+ show_nested=True)
+
+ n_param2 = {'myres': {'Description': 'the stack description',
+ 'Parameters': {'aparam': {'Default': 10,
+ 'Description':
+ 'the param '
+ 'description',
+ 'Label': 'aparam',
+ 'NoEcho': 'false',
+ 'Type': 'Number'}},
+ 'Type': 'mynested2.yaml'}}
+ expected = {'Description': 'the parent template',
+ 'Parameters': {
+ 'pparam': {'Default': 5,
+ 'Description': 'the param description',
+ 'Label': 'pparam',
+ 'NoEcho': 'false',
+ 'Type': 'Number'}},
+ 'NestedParameters': {
+ 'nres': {'Description': 'the stack description',
+ 'Parameters': {'aparam': {'Default': 10,
+ 'Description':
+ 'the param '
+ 'description',
+ 'Label': 'aparam',
+ 'Value': 5,
+ 'NoEcho': 'false',
+ 'Type': 'Number'}},
+ 'NestedParameters': n_param2,
+ 'Type': 'mynested.yaml'}}}
+ self.assertEqual(expected, ret)
diff --git a/scenario/templates/app_server_neutron.yaml b/scenario/templates/app_server_neutron.yaml
new file mode 100644
index 0000000..9cbf82a
--- /dev/null
+++ b/scenario/templates/app_server_neutron.yaml
@@ -0,0 +1,65 @@
+heat_template_version: 2015-10-15
+
+description: |
+ App server that is a member of Neutron Pool.
+
+parameters:
+
+ image:
+ type: string
+
+ flavor:
+ type: string
+
+ net:
+ type: string
+
+ sec_group:
+ type: string
+
+ pool_id:
+ type: string
+
+ app_port:
+ type: number
+
+ timeout:
+ type: number
+
+resources:
+
+ config:
+ type: OS::Test::WebAppConfig
+ properties:
+ app_port: { get_param: app_port }
+ wc_curl_cli: { get_attr: [ handle, curl_cli ] }
+
+ server:
+ type: OS::Nova::Server
+ properties:
+ image: { get_param: image }
+ flavor: { get_param: flavor }
+ networks:
+ - network: { get_param: net }
+ security_groups:
+ - { get_param: sec_group }
+ user_data_format: RAW
+ user_data: { get_resource: config }
+
+ handle:
+ type: OS::Heat::WaitConditionHandle
+
+ waiter:
+ type: OS::Heat::WaitCondition
+ depends_on: server
+ properties:
+ timeout: { get_param: timeout }
+ handle: { get_resource: handle }
+
+ pool_member:
+ type: OS::Neutron::PoolMember
+ depends_on: waiter
+ properties:
+ address: { get_attr: [ server, networks, { get_param: net }, 0 ] }
+ pool_id: { get_param: pool_id }
+ protocol_port: { get_param: app_port }
diff --git a/scenario/templates/netcat-webapp.yaml b/scenario/templates/netcat-webapp.yaml
new file mode 100644
index 0000000..fdb0335
--- /dev/null
+++ b/scenario/templates/netcat-webapp.yaml
@@ -0,0 +1,35 @@
+heat_template_version: 2015-10-15
+
+description: |
+ Simplest web-app using netcat reporting only hostname.
+ Specifically tailored for minimal Cirros image.
+
+parameters:
+
+ app_port:
+ type: number
+
+ wc_curl_cli:
+ type: string
+
+resources:
+
+ webapp_nc:
+ type: OS::Heat::SoftwareConfig
+ properties:
+ group: ungrouped
+ config:
+ str_replace:
+ template: |
+ #! /bin/sh -v
+ Body=$(hostname)
+ Response="HTTP/1.1 200 OK\r\nContent-Length: ${#Body}\r\n\r\n$Body"
+ wc_notify --data-binary '{"status": "SUCCESS"}'
+ while true ; do echo -e $Response | nc -llp PORT; done
+ params:
+ PORT: { get_param: app_port }
+ wc_notify: { get_param: wc_curl_cli }
+
+outputs:
+ OS::stack_id:
+ value: { get_resource: webapp_nc }
diff --git a/scenario/templates/test_autoscaling_lb_neutron.yaml b/scenario/templates/test_autoscaling_lb_neutron.yaml
new file mode 100644
index 0000000..d47e787
--- /dev/null
+++ b/scenario/templates/test_autoscaling_lb_neutron.yaml
@@ -0,0 +1,113 @@
+heat_template_version: 2015-04-30
+
+description: |
+ Template which tests Neutron load balancing requests to members of
+ Heat AutoScalingGroup.
+ Instances must be running some webserver on a given app_port
+ producing HTTP response that is different between servers
+ but stable over time for given server.
+
+parameters:
+ flavor:
+ type: string
+ image:
+ type: string
+ net:
+ type: string
+ subnet:
+ type: string
+ public_net:
+ type: string
+ app_port:
+ type: number
+ default: 8080
+ lb_port:
+ type: number
+ default: 80
+ timeout:
+ type: number
+ default: 600
+
+resources:
+
+ sec_group:
+ type: OS::Neutron::SecurityGroup
+ properties:
+ rules:
+ - remote_ip_prefix: 0.0.0.0/0
+ protocol: tcp
+ port_range_min: { get_param: app_port }
+ port_range_max: { get_param: app_port }
+
+ asg:
+ type: OS::Heat::AutoScalingGroup
+ properties:
+ desired_capacity: 1
+ max_size: 2
+ min_size: 1
+ resource:
+ type: OS::Test::NeutronAppServer
+ properties:
+ image: { get_param: image }
+ flavor: { get_param: flavor }
+ net: { get_param: net}
+ sec_group: { get_resource: sec_group }
+ app_port: { get_param: app_port }
+ pool_id: { get_resource: pool }
+ timeout: { get_param: timeout }
+
+ scale_up:
+ type: OS::Heat::ScalingPolicy
+ properties:
+ adjustment_type: change_in_capacity
+ auto_scaling_group_id: { get_resource: asg }
+ scaling_adjustment: 1
+
+ scale_down:
+ type: OS::Heat::ScalingPolicy
+ properties:
+ adjustment_type: change_in_capacity
+ auto_scaling_group_id: { get_resource: asg }
+ scaling_adjustment: -1
+
+ health_monitor:
+ type: OS::Neutron::HealthMonitor
+ properties:
+ delay: 3
+ type: HTTP
+ timeout: 3
+ max_retries: 3
+
+ pool:
+ type: OS::Neutron::Pool
+ properties:
+ lb_method: ROUND_ROBIN
+ protocol: HTTP
+ subnet: { get_param: subnet }
+ monitors:
+ - { get_resource: health_monitor }
+ vip:
+ protocol_port: { get_param: lb_port }
+
+ floating_ip:
+ type: OS::Neutron::FloatingIP
+ properties:
+ floating_network: { get_param: public_net }
+ port_id:
+ { get_attr: [pool, vip, 'port_id'] }
+
+ loadbalancer:
+ type: OS::Neutron::LoadBalancer
+ properties:
+ pool_id: { get_resource: pool }
+ protocol_port: { get_param: app_port }
+
+outputs:
+ lburl:
+ description: URL of the loadbalanced app
+ value:
+ str_replace:
+ template: http://IP_ADDRESS:PORT
+ params:
+ IP_ADDRESS: { get_attr: [ floating_ip, floating_ip_address ] }
+ PORT: { get_param: lb_port }
diff --git a/scenario/templates/test_neutron_autoscaling.yaml b/scenario/templates/test_neutron_autoscaling.yaml
deleted file mode 100644
index a34ec43..0000000
--- a/scenario/templates/test_neutron_autoscaling.yaml
+++ /dev/null
@@ -1,56 +0,0 @@
-heat_template_version: 2014-10-16
-
-description: Auto-scaling Test
-
-parameters:
- image_id:
- type: string
- label: Image ID
- description: Image ID from configurations
- capacity:
- type: string
- label: Capacity
- description: Auto-scaling group desired capacity
- fixed_subnet:
- type: string
- label: fixed subnetwork ID
- description: subnetwork ID used for autoscaling
- instance_type:
- type: string
- label: instance_type
- description: type of instance to launch
-
-resources:
- test_pool:
- type: OS::Neutron::Pool
- properties:
- description: Test Pool
- lb_method: ROUND_ROBIN
- name: test_pool
- protocol: HTTP
- subnet: { get_param: fixed_subnet }
- vip: {
- "description": "Test VIP",
- "protocol_port": 80,
- "name": "test_vip"
- }
- load_balancer:
- type: OS::Neutron::LoadBalancer
- properties:
- protocol_port: 80
- pool_id: { get_resource: test_pool }
- launch_config:
- type: AWS::AutoScaling::LaunchConfiguration
- properties:
- ImageId: { get_param: image_id }
- InstanceType: { get_param: instance_type }
- server_group:
- type: AWS::AutoScaling::AutoScalingGroup
- properties:
- AvailabilityZones : ["nova"]
- LaunchConfigurationName : { get_resource : launch_config }
- VPCZoneIdentifier: [{ get_param: fixed_subnet }]
- MinSize : 1
- MaxSize : 5
- DesiredCapacity: { get_param: capacity }
- LoadBalancerNames : [ { get_resource : load_balancer } ]
diff --git a/scenario/templates/test_neutron_loadbalancer.yaml b/scenario/templates/test_neutron_loadbalancer.yaml
deleted file mode 100644
index dd659d0..0000000
--- a/scenario/templates/test_neutron_loadbalancer.yaml
+++ /dev/null
@@ -1,133 +0,0 @@
-heat_template_version: 2014-10-16
-
-description: |
- Template which tests neutron load balancing resources
-
-parameters:
- key_name:
- type: string
- flavor:
- type: string
- image:
- type: string
- network:
- type: string
- private_subnet_id:
- type: string
- external_network_id:
- type: string
- port:
- type: string
- default: '80'
- timeout:
- type: number
-
-resources:
- sec_group:
- type: OS::Neutron::SecurityGroup
- properties:
- description: Add security group rules for servers
- name: security-group
- rules:
- - remote_ip_prefix: 0.0.0.0/0
- protocol: tcp
- port_range_min: { get_param: port }
- port_range_max: { get_param: port }
- - remote_ip_prefix: 0.0.0.0/0
- protocol: icmp
-
- wait_condition:
- type: OS::Heat::WaitCondition
- properties:
- handle: { get_resource: wait_condition_handle }
- count: 2
- timeout: { get_param: timeout }
-
- wait_condition_handle:
- type: OS::Heat::WaitConditionHandle
-
- config:
- type: OS::Heat::SoftwareConfig
- properties:
- group: ungrouped
- config:
- str_replace:
- template: |
- #!/bin/bash -v
- echo $(hostname) > index.html
- python -m SimpleHTTPServer port &
- wc_notify --data-binary '{"status": "SUCCESS"}'
- params:
- wc_notify: { get_attr: ['wait_condition_handle', 'curl_cli'] }
- port: { get_param: port }
-
- server1:
- type: OS::Nova::Server
- properties:
- name: Server1
- image: { get_param: image }
- flavor: { get_param: flavor }
- key_name: { get_param: key_name }
- networks: [{network: {get_param: network} }]
- security_groups: [{ get_resource: sec_group }]
- user_data_format: SOFTWARE_CONFIG
- user_data: { get_resource: config }
-
- server2:
- type: OS::Nova::Server
- properties:
- name: Server2
- image: { get_param: image }
- flavor: { get_param: flavor }
- key_name: { get_param: key_name }
- networks: [{network: {get_param: network} }]
- security_groups: [{ get_resource: sec_group }]
- user_data_format: SOFTWARE_CONFIG
- user_data: { get_resource: config }
-
- health_monitor:
- type: OS::Neutron::HealthMonitor
- properties:
- delay: 3
- type: HTTP
- timeout: 3
- max_retries: 3
-
- test_pool:
- type: OS::Neutron::Pool
- properties:
- lb_method: ROUND_ROBIN
- protocol: HTTP
- subnet: { get_param: private_subnet_id }
- monitors:
- - { get_resource: health_monitor }
- vip:
- protocol_port: { get_param: port }
-
- floating_ip:
- type: OS::Neutron::FloatingIP
- properties:
- floating_network: { get_param: external_network_id }
- port_id:
- { get_attr: [test_pool, vip, 'port_id'] }
- fixed_ip_address:
- { get_attr: [test_pool, vip, 'address'] }
-
- LBaaS:
- type: OS::Neutron::LoadBalancer
- depends_on: wait_condition
- properties:
- pool_id: { get_resource: test_pool }
- protocol_port: { get_param: port }
- members:
- - { get_resource: server1 }
-
-outputs:
- serv1_ip:
- value: {get_attr: [server1, networks, { get_param: network }, 0]}
- serv2_ip:
- value: {get_attr: [server2, networks, { get_param: network }, 0]}
- vip:
- value: {get_attr: [test_pool, vip, address]}
- fip:
- value: {get_attr: [floating_ip, floating_ip_address]}
diff --git a/scenario/test_autoscaling_lb.py b/scenario/test_autoscaling_lb.py
new file mode 100644
index 0000000..21b27dd
--- /dev/null
+++ b/scenario/test_autoscaling_lb.py
@@ -0,0 +1,114 @@
+#
+# 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 time
+
+import requests
+
+from heat_integrationtests.common import test
+from heat_integrationtests.scenario import scenario_base
+
+
+class AutoscalingLoadBalancerTest(scenario_base.ScenarioTestsBase):
+ """
+ The class is responsible for testing ASG + LB scenario.
+
+ The very common use case tested is an autoscaling group
+ of some web application servers behind a loadbalancer.
+ """
+
+ def setUp(self):
+ super(AutoscalingLoadBalancerTest, self).setUp()
+ self.template_name = 'test_autoscaling_lb_neutron.yaml'
+ self.app_server_template_name = 'app_server_neutron.yaml'
+ self.webapp_template_name = 'netcat-webapp.yaml'
+
+ def check_num_responses(self, url, expected_num, retries=10):
+ resp = set()
+ for count in range(retries):
+ time.sleep(1)
+ r = requests.get(url)
+ # skip unsuccessfull requests
+ if r.status_code == 200:
+ resp.add(r.text)
+ self.assertEqual(expected_num, len(resp))
+
+ def autoscale_complete(self, stack_id, expected_num):
+ res_list = self.client.resources.list(stack_id)
+ all_res_complete = all(res.resource_status in ('UPDATE_COMPLETE',
+ 'CREATE_COMPLETE')
+ for res in res_list)
+ all_res = len(res_list) == expected_num
+ return all_res and all_res_complete
+
+ def test_autoscaling_loadbalancer_neutron(self):
+ """
+ Check work of AutoScaing and Neutron LBaaS resource in Heat.
+
+ The scenario is the following:
+ 1. Launch a stack with a load balancer and autoscaling group
+ of one server, wait until stack create is complete.
+ 2. Check that there is only one distinctive response from
+ loadbalanced IP.
+ 3. Signal the scale_up policy, wait until all resources in
+ autoscaling group are complete.
+ 4. Check that now there are two distinctive responses from
+ loadbalanced IP.
+ """
+
+ parameters = {
+ 'flavor': self.conf.minimal_instance_type,
+ 'image': self.conf.minimal_image_ref,
+ 'net': self.conf.fixed_network_name,
+ 'subnet': self.conf.fixed_subnet_name,
+ 'public_net': self.conf.floating_network_name,
+ 'app_port': 8080,
+ 'lb_port': 80,
+ 'timeout': 600
+ }
+
+ app_server_template = self._load_template(
+ __file__, self.app_server_template_name, self.sub_dir
+ )
+ webapp_template = self._load_template(
+ __file__, self.webapp_template_name, self.sub_dir
+ )
+ files = {'appserver.yaml': app_server_template,
+ 'webapp.yaml': webapp_template}
+ env = {'resource_registry':
+ {'OS::Test::NeutronAppServer': 'appserver.yaml',
+ 'OS::Test::WebAppConfig': 'webapp.yaml'}}
+ # Launch stack
+ sid = self.launch_stack(
+ template_name=self.template_name,
+ parameters=parameters,
+ files=files,
+ environment=env
+ )
+ stack = self.client.stacks.get(sid)
+ lb_url = self._stack_output(stack, 'lburl')
+ # Check number of distinctive responces, must be 1
+ self.check_num_responses(lb_url, 1)
+
+ # Signal the scaling hook
+ self.client.resources.signal(sid, 'scale_up')
+
+ # Wait for AutoScalingGroup update to finish
+ asg = self.client.resources.get(sid, 'asg')
+ test.call_until_true(self.conf.build_timeout,
+ self.conf.build_interval,
+ self.autoscale_complete,
+ asg.physical_resource_id, 2)
+
+ # Check number of distinctive responses, must now be 2
+ self.check_num_responses(lb_url, 2)
diff --git a/scenario/test_neutron_autoscaling.py b/scenario/test_neutron_autoscaling.py
deleted file mode 100644
index e7aae19..0000000
--- a/scenario/test_neutron_autoscaling.py
+++ /dev/null
@@ -1,72 +0,0 @@
-# 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.
-
-from heat_integrationtests.scenario import scenario_base
-
-
-class NeutronAutoscalingTest(scenario_base.ScenarioTestsBase):
- """
- The class is responsible for testing of neutron resources autoscaling.
- """
-
- def setUp(self):
- super(NeutronAutoscalingTest, self).setUp()
- if not self.conf.fixed_subnet_name:
- raise self.skipException("No sub-network configured to test")
- self.template_name = 'test_neutron_autoscaling.yaml'
-
- def test_neutron_autoscaling(self):
- """
- Check autoscaling of load balancer members in Heat.
-
- The alternative scenario is the following:
- 1. Launch a stack with a load balancer.
- 2. Check that the load balancer created
- one load balancer member for stack.
- 3. Update stack definition: increase desired capacity of stack.
- 4. Check that number of members in load balancer was increased.
- """
-
- parameters = {
- "image_id": self.conf.minimal_image_ref,
- "capacity": "1",
- "instance_type": self.conf.minimal_instance_type,
- "fixed_subnet": self.net['subnets'][0],
- }
-
- # Launch stack
- stack_id = self.launch_stack(
- template_name=self.template_name,
- parameters=parameters
- )
-
- # Check number of members
- pool_resource = self.client.resources.get(stack_id, 'test_pool')
- pool_members = self.network_client.list_members(
- pool_id=pool_resource.physical_resource_id)['members']
- self.assertEqual(1, len(pool_members))
-
- # Increase desired capacity and update the stack
- template = self._load_template(
- __file__, self.template_name, self.sub_dir
- )
- parameters["capacity"] = "2"
- self.update_stack(
- stack_id,
- template=template,
- parameters=parameters
- )
-
- # Check number of members
- pool_members = self.network_client.list_members(
- pool_id=pool_resource.physical_resource_id)['members']
- self.assertEqual(2, len(pool_members))
diff --git a/scenario/test_neutron_loadbalancer.py b/scenario/test_neutron_loadbalancer.py
deleted file mode 100644
index d8e0197..0000000
--- a/scenario/test_neutron_loadbalancer.py
+++ /dev/null
@@ -1,108 +0,0 @@
-#
-# 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 time
-
-from six.moves import urllib
-
-from heat_integrationtests.scenario import scenario_base
-
-
-class NeutronLoadBalancerTest(scenario_base.ScenarioTestsBase):
- """
- The class is responsible for testing of neutron resources balancer.
- """
-
- def setUp(self):
- super(NeutronLoadBalancerTest, self).setUp()
- self.public_net = self._get_network(self.conf.floating_network_name)
- self.template_name = 'test_neutron_loadbalancer.yaml'
-
- def collect_responses(self, ip, expected_resp):
- resp = set()
- for count in range(10):
- time.sleep(1)
- resp.add(urllib.request.urlopen('http://%s/' % ip).read())
-
- self.assertEqual(expected_resp, resp)
-
- def test_neutron_loadbalancer(self):
- """
- Check work of Neutron LBaaS resource in Heat.
-
- The alternative scenario is the following:
- 1. Launch a stack with a load balancer, two servers,
- but use only one as a LB member.
- 2. Check connection to the servers and LB.
- 3. Collect info about responces, which were received by LB from
- its members (responces have to be received only from 'server1').
- 4. Update stack definition: include 'server2' into LBaaS.
- 5. Check that number of members in LB was increased and
- responces were received from 'server1' and 'server2'.
- """
-
- parameters = {
- 'key_name': self.keypair_name,
- 'flavor': self.conf.minimal_instance_type,
- 'image': self.conf.image_ref,
- 'network': self.net['name'],
- 'private_subnet_id': self.net['subnets'][0],
- 'external_network_id': self.public_net['id'],
- 'timeout': self.conf.build_timeout
- }
-
- # Launch stack
- sid = self.launch_stack(
- template_name=self.template_name,
- parameters=parameters
- )
-
- stack = self.client.stacks.get(sid)
- floating_ip = self._stack_output(stack, 'fip')
- vip = self._stack_output(stack, 'vip')
- server1_ip = self._stack_output(stack, 'serv1_ip')
- server2_ip = self._stack_output(stack, 'serv2_ip')
- # Check connection and info about received responses
- self.check_connectivity(server1_ip)
- self.collect_responses(server1_ip, {'server1\n'})
-
- self.check_connectivity(server2_ip)
- self.collect_responses(server2_ip, {'server2\n'})
-
- self.check_connectivity(vip)
- self.collect_responses(vip, {'server1\n'})
-
- self.check_connectivity(floating_ip)
- self.collect_responses(floating_ip, {'server1\n'})
-
- # Include 'server2' to LB and update the stack
- template = self._load_template(
- __file__, self.template_name, self.sub_dir
- )
-
- template = template.replace(
- '- { get_resource: server1 }',
- '- { get_resource: server1 }\n - { get_resource: server2 }\n'
- )
-
- self.update_stack(
- sid,
- template=template,
- parameters=parameters
- )
-
- self.check_connectivity(vip)
- self.collect_responses(vip, {'server1\n', 'server2\n'})
-
- self.check_connectivity(floating_ip)
- self.collect_responses(floating_ip, {'server1\n', 'server2\n'})