Merge "Use oslo.utils.reflection to extract class name"
diff --git a/common/clients.py b/common/clients.py
index 9718d03..cff3bf2 100644
--- a/common/clients.py
+++ b/common/clients.py
@@ -12,14 +12,49 @@
import os
-import ceilometerclient.client
-import cinderclient.client
-import heatclient.client
-import keystoneclient.exceptions
-import keystoneclient.v2_0.client
-import neutronclient.v2_0.client
-import novaclient.client
-import swiftclient
+from ceilometerclient import client as ceilometer_client
+from cinderclient import client as cinder_client
+from heatclient import client as heat_client
+from keystoneclient.auth.identity.generic import password
+from keystoneclient import exceptions as kc_exceptions
+from keystoneclient import session
+from neutronclient.v2_0 import client as neutron_client
+from novaclient import client as nova_client
+from swiftclient import client as swift_client
+
+
+class KeystoneWrapperClient(object):
+ """Wrapper object for keystone client
+
+ This wraps keystone client, so we can encpasulate certain
+ added properties like auth_token, project_id etc.
+ """
+ def __init__(self, auth_plugin, verify=True):
+ self.auth_plugin = auth_plugin
+ self.session = session.Session(
+ auth=auth_plugin,
+ verify=verify)
+
+ @property
+ def auth_token(self):
+ return self.auth_plugin.get_token(self.session)
+
+ @property
+ def auth_ref(self):
+ return self.auth_plugin.get_access(self.session)
+
+ @property
+ def project_id(self):
+ return self.auth_plugin.get_project_id(self.session)
+
+ def get_endpoint_url(self, service_type, region=None):
+ kwargs = {
+ 'service_type': service_type,
+ 'endpoint_type': 'publicURL'}
+ if region:
+ kwargs.update({'attr': 'region',
+ 'filter_value': region})
+ return self.auth_ref.service_catalog.url_for(**kwargs)
class ClientManager(object):
@@ -36,6 +71,8 @@
def __init__(self, conf):
self.conf = conf
+ self.v2_auth_url = self.conf.auth_url.replace('/v3', '/v2.0')
+ self.auth_version = self.conf.auth_url.split('/v')[1]
self.identity_client = self._get_identity_client()
self.orchestration_client = self._get_orchestration_client()
self.compute_client = self._get_compute_client()
@@ -45,24 +82,19 @@
self.metering_client = self._get_metering_client()
def _get_orchestration_client(self):
- region = self.conf.region
endpoint = os.environ.get('HEAT_URL')
if os.environ.get('OS_NO_CLIENT_AUTH') == 'True':
token = None
else:
- keystone = self._get_identity_client()
- token = keystone.auth_token
+ token = self.identity_client.auth_token
try:
if endpoint is None:
- endpoint = keystone.service_catalog.url_for(
- attr='region',
- filter_value=region,
- service_type='orchestration',
- endpoint_type='publicURL')
- except keystoneclient.exceptions.EndpointNotFound:
+ endpoint = self.identity_client.get_endpoint_url(
+ 'orchestration', self.conf.region)
+ except kc_exceptions.EndpointNotFound:
return None
else:
- return heatclient.client.Client(
+ return heat_client.Client(
self.HEATCLIENT_VERSION,
endpoint,
token=token,
@@ -70,12 +102,22 @@
password=self.conf.password)
def _get_identity_client(self):
- return keystoneclient.v2_0.client.Client(
- username=self.conf.username,
- password=self.conf.password,
- tenant_name=self.conf.tenant_name,
- auth_url=self.conf.auth_url,
- insecure=self.conf.disable_ssl_certificate_validation)
+ domain = self.conf.domain_name
+ kwargs = {
+ 'username': self.conf.username,
+ 'password': self.conf.password,
+ 'tenant_name': self.conf.tenant_name,
+ 'auth_url': self.conf.auth_url
+ }
+ # keystone v2 can't ignore domain details
+ if self.auth_version == '3':
+ kwargs.update({
+ 'project_domain_name': domain,
+ 'user_domain_name': domain})
+ auth = password.Password(**kwargs)
+ return KeystoneWrapperClient(
+ auth,
+ not self.conf.disable_ssl_certificate_validation)
def _get_compute_client(self):
@@ -86,11 +128,12 @@
self.conf.username,
self.conf.password,
self.conf.tenant_name,
- self.conf.auth_url
+ # novaclient can not use v3 url
+ self.v2_auth_url
)
# Create our default Nova client to use in testing
- return novaclient.client.Client(
+ return nova_client.Client(
self.NOVACLIENT_VERSION,
*client_args,
service_type='compute',
@@ -101,28 +144,28 @@
http_log_debug=True)
def _get_network_client(self):
- auth_url = self.conf.auth_url
dscv = self.conf.disable_ssl_certificate_validation
- return neutronclient.v2_0.client.Client(
+ return neutron_client.Client(
username=self.conf.username,
password=self.conf.password,
tenant_name=self.conf.tenant_name,
endpoint_type='publicURL',
- auth_url=auth_url,
+ # neutronclient can not use v3 url
+ auth_url=self.v2_auth_url,
insecure=dscv)
def _get_volume_client(self):
- auth_url = self.conf.auth_url
region = self.conf.region
endpoint_type = 'publicURL'
dscv = self.conf.disable_ssl_certificate_validation
- return cinderclient.client.Client(
+ return cinder_client.Client(
self.CINDERCLIENT_VERSION,
self.conf.username,
self.conf.password,
self.conf.tenant_name,
- auth_url,
+ # cinderclient can not use v3 url
+ self.v2_auth_url,
region_name=region,
endpoint_type=endpoint_type,
insecure=dscv,
@@ -131,7 +174,7 @@
def _get_object_client(self):
dscv = self.conf.disable_ssl_certificate_validation
args = {
- 'auth_version': '2.0',
+ 'auth_version': self.auth_version,
'tenant_name': self.conf.tenant_name,
'user': self.conf.username,
'key': self.conf.password,
@@ -139,20 +182,15 @@
'os_options': {'endpoint_type': 'publicURL'},
'insecure': dscv,
}
- return swiftclient.client.Connection(**args)
+ return swift_client.Connection(**args)
def _get_metering_client(self):
dscv = self.conf.disable_ssl_certificate_validation
-
- keystone = self._get_identity_client()
+ domain = self.conf.domain_name
try:
- endpoint = keystone.service_catalog.url_for(
- attr='region',
- filter_value=self.conf.region,
- service_type='metering',
- endpoint_type='publicURL')
-
- except keystoneclient.exceptions.EndpointNotFound:
+ endpoint = self.identity_client.get_endpoint_url('metering',
+ self.conf.region)
+ except kc_exceptions.EndpointNotFound:
return None
else:
args = {
@@ -165,6 +203,12 @@
'endpoint_type': 'publicURL',
'service_type': 'metering',
}
+ # ceilometerclient can't ignore domain details for
+ # v2 auth_url
+ if self.auth_version == '3':
+ args.update(
+ {'user_domain_name': domain,
+ 'project_domain_name': domain})
- return ceilometerclient.client.Client(self.CEILOMETER_VERSION,
- endpoint, **args)
+ return ceilometer_client.Client(self.CEILOMETER_VERSION,
+ endpoint, **args)
diff --git a/common/config.py b/common/config.py
index 9ad6337..6d35600 100644
--- a/common/config.py
+++ b/common/config.py
@@ -21,10 +21,15 @@
cfg.StrOpt('username',
default=os.environ.get('OS_USERNAME'),
- help="Username to use for API requests."),
+ help="Username to use for non admin API requests."),
cfg.StrOpt('password',
default=os.environ.get('OS_PASSWORD'),
- help="API key to use when authenticating.",
+ help="Non admin API key to use when authenticating.",
+ secret=True),
+ cfg.StrOpt('admin_username',
+ help="Username to use for admin API requests."),
+ cfg.StrOpt('admin_password',
+ help="Admin API key to use when authentication.",
secret=True),
cfg.StrOpt('tenant_name',
default=(os.environ.get('OS_PROJECT_NAME') or
@@ -32,10 +37,14 @@
help="Tenant name to use for API requests."),
cfg.StrOpt('auth_url',
default=os.environ.get('OS_AUTH_URL'),
- help="Full URI of the OpenStack Identity API (Keystone), v2"),
+ help="Full URI of the OpenStack Identity API (Keystone)"),
+ cfg.StrOpt('domain_name',
+ default='default',
+ help="User/project domain name, if keystone v3 auth_url"
+ "is used"),
cfg.StrOpt('region',
default=os.environ.get('OS_REGION_NAME'),
- help="The region name to us"),
+ help="The region name to use"),
cfg.StrOpt('instance_type',
help="Instance type for tests. Needs to be big enough for a "
"full OS plus the test workload"),
@@ -48,10 +57,6 @@
cfg.StrOpt('minimal_image_ref',
help="Name of minimal (e.g cirros) image to use when "
"launching test instances."),
- cfg.StrOpt('auth_version',
- default='v2',
- help="Identity API version to be used for authentication "
- "for API tests."),
cfg.BoolOpt('disable_ssl_certificate_validation',
default=False,
help="Set to True if using self-signed SSL certificates."),
@@ -124,6 +129,7 @@
cfg.StrOpt('heat-config-notify-script',
default=('heat-config-notify'),
help="Path to the script heat-config-notify"),
+
]
diff --git a/common/test.py b/common/test.py
index 1ffe222..4eaa2b7 100644
--- a/common/test.py
+++ b/common/test.py
@@ -257,6 +257,14 @@
(resource_name, status, build_timeout))
raise exceptions.TimeoutException(message)
+ def verify_resource_status(self, stack_identifier, resource_name,
+ status='CREATE_COMPLETE'):
+ try:
+ res = self.client.resources.get(stack_identifier, resource_name)
+ except heat_exceptions.HTTPNotFound:
+ return False
+ return res.resource_status == status
+
def _verify_status(self, stack, stack_identifier, status, fail_regexp):
if stack.stack_status == status:
# Handle UPDATE_COMPLETE/FAILED case: Make sure we don't
@@ -310,7 +318,8 @@
while timeutils.delta_seconds(start,
timeutils.utcnow()) < build_timeout:
try:
- stack = self.client.stacks.get(stack_identifier)
+ stack = self.client.stacks.get(stack_identifier,
+ resolve_outputs=False)
except heat_exceptions.HTTPNotFound:
if success_on_not_found:
return
@@ -367,7 +376,7 @@
stack_name = stack_identifier.split('/')[0]
self.updated_time[stack_identifier] = self.client.stacks.get(
- stack_identifier).updated_time
+ stack_identifier, resolve_outputs=False).updated_time
self._handle_in_progress(
self.client.stacks.update,
@@ -392,7 +401,8 @@
def preview_update_stack(self, stack_identifier, template,
environment=None, files=None, parameters=None,
- tags=None, disable_rollback=True):
+ tags=None, disable_rollback=True,
+ show_nested=False):
env = environment or {}
env_files = files or {}
parameters = parameters or {}
@@ -406,7 +416,8 @@
disable_rollback=disable_rollback,
parameters=parameters,
environment=env,
- tags=tags
+ tags=tags,
+ show_nested=show_nested
)
def assert_resource_is_a_stack(self, stack_identifier, res_name,
@@ -437,7 +448,7 @@
nested_identifier = '/'.join(nested_href.split('/')[-2:])
self.assertEqual(rsrc.physical_resource_id, nested_id)
- nested_stack = self.client.stacks.get(nested_id)
+ nested_stack = self.client.stacks.get(nested_id, resolve_outputs=False)
nested_identifier2 = '%s/%s' % (nested_stack.stack_name,
nested_stack.id)
self.assertEqual(nested_identifier, nested_identifier2)
@@ -451,7 +462,8 @@
rsrc = self.client.resources.get(stack_identifier, group_name)
physical_resource_id = rsrc.physical_resource_id
- nested_stack = self.client.stacks.get(physical_resource_id)
+ nested_stack = self.client.stacks.get(physical_resource_id,
+ resolve_outputs=False)
nested_identifier = '%s/%s' % (nested_stack.stack_name,
nested_stack.id)
parent_id = stack_identifier.split("/")[-1]
@@ -473,7 +485,8 @@
def stack_create(self, stack_name=None, template=None, files=None,
parameters=None, environment=None, tags=None,
expected_status='CREATE_COMPLETE',
- disable_rollback=True, enable_cleanup=True):
+ disable_rollback=True, enable_cleanup=True,
+ environment_files=None):
name = stack_name or self._stack_rand_name()
templ = template or self.template
templ_files = files or {}
@@ -486,12 +499,13 @@
disable_rollback=disable_rollback,
parameters=params,
environment=env,
- tags=tags
+ tags=tags,
+ environment_files=environment_files
)
if expected_status not in ['ROLLBACK_COMPLETE'] and enable_cleanup:
self.addCleanup(self._stack_delete, name)
- stack = self.client.stacks.get(name)
+ stack = self.client.stacks.get(name, resolve_outputs=False)
stack_identifier = '%s/%s' % (name, stack.id)
kwargs = {'stack_identifier': stack_identifier,
'status': expected_status}
@@ -522,8 +536,7 @@
adopt_stack_data=adopt_data,
)
self.addCleanup(self._stack_delete, name)
-
- stack = self.client.stacks.get(name)
+ stack = self.client.stacks.get(name, resolve_outputs=False)
stack_identifier = '%s/%s' % (name, stack.id)
self._wait_for_stack_status(stack_identifier, wait_for_status)
return stack_identifier
diff --git a/functional/test_aws_stack.py b/functional/test_aws_stack.py
index 678ce7e..13c4278 100644
--- a/functional/test_aws_stack.py
+++ b/functional/test_aws_stack.py
@@ -77,7 +77,7 @@
def setUp(self):
super(AwsStackTest, self).setUp()
self.object_container_name = test.rand_name()
- self.project_id = self.identity_client.auth_ref.project_id
+ self.project_id = self.identity_client.project_id
self.swift_key = hashlib.sha224(
str(random.getrandbits(256))).hexdigest()[:32]
key_header = 'x-container-meta-temp-url-key'
@@ -115,7 +115,7 @@
def test_nested_stack_create_with_timeout(self):
url = self.publish_template(self.nested_template)
self.template = self.test_template.replace('the.yaml', url)
- timeout_template = yaml.load(self.template)
+ timeout_template = yaml.safe_load(self.template)
props = timeout_template['Resources']['the_nested']['Properties']
props['TimeoutInMinutes'] = '50'
@@ -142,7 +142,7 @@
}
},
"environment": {"parameters": {}},
- "template": yaml.load(self.template)
+ "template": yaml.safe_load(self.template)
}
stack_identifier = self.stack_adopt(adopt_data=json.dumps(adopt_data))
@@ -163,7 +163,7 @@
}
},
"environment": {"parameters": {}},
- "template": yaml.load(self.template)
+ "template": yaml.safe_load(self.template)
}
stack_identifier = self.stack_adopt(adopt_data=json.dumps(adopt_data),
@@ -180,7 +180,7 @@
stack = self.client.stacks.get(stack_identifier)
self.assertEqual('bar', self._stack_output(stack, 'output_foo'))
- new_template = yaml.load(self.template)
+ new_template = yaml.safe_load(self.template)
props = new_template['Resources']['the_nested']['Properties']
props['TemplateURL'] = self.publish_template(self.update_template,
cleanup=False)
diff --git a/functional/test_conditional_exposure.py b/functional/test_conditional_exposure.py
index 90f7d7a..c1175f1 100644
--- a/functional/test_conditional_exposure.py
+++ b/functional/test_conditional_exposure.py
@@ -42,13 +42,9 @@
"Sahara resources availability.")
def _is_sahara_deployed(self):
- keystone = self.identity_client
try:
- keystone.service_catalog.url_for(
- attr='region',
- filter_value=self.conf.region,
- service_type='data-processing',
- endpoint_type='publicURL')
+ self.identity_client.get_endpoint_url('data-processing',
+ self.conf.region)
except keystoneclient.exceptions.EndpointNotFound:
return False
return True
diff --git a/functional/test_default_parameters.py b/functional/test_default_parameters.py
index 3e00c35..7201969 100644
--- a/functional/test_default_parameters.py
+++ b/functional/test_default_parameters.py
@@ -75,9 +75,9 @@
if not self.temp_def:
# remove the default from the parameter in the nested template.
- ntempl = yaml.load(self.nested_template)
+ ntempl = yaml.safe_load(self.nested_template)
del ntempl['parameters']['length']['default']
- nested_template = yaml.dump(ntempl)
+ nested_template = yaml.safe_dump(ntempl)
else:
nested_template = self.nested_template
diff --git a/functional/test_encryption_vol_type.py b/functional/test_encryption_vol_type.py
index e1a3e76..2679990 100644
--- a/functional/test_encryption_vol_type.py
+++ b/functional/test_encryption_vol_type.py
@@ -42,11 +42,14 @@
class EncryptionVolTypeTest(functional_base.FunctionalTestsBase):
def setUp(self):
super(EncryptionVolTypeTest, self).setUp()
+ if not self.conf.admin_username or not self.conf.admin_password:
+ self.skipTest('No admin creds found, skipping')
self.conf = config.init_conf()
# cinder security policy usage of volume type is limited
# to being used by administrators only.
- # Temporarily set username as admin for this test case.
- self.conf.username = 'admin'
+ # Temporarily switch to admin
+ self.conf.username = self.conf.admin_username
+ self.conf.password = self.conf.admin_password
self.manager = clients.ClientManager(self.conf)
self.client = self.manager.orchestration_client
self.volume_client = self.manager.volume_client
diff --git a/functional/test_env_merge.py b/functional/test_env_merge.py
new file mode 100644
index 0000000..5e222b8
--- /dev/null
+++ b/functional/test_env_merge.py
@@ -0,0 +1,95 @@
+#
+# 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
+
+
+TEMPLATE = '''
+ heat_template_version: 2015-04-30
+ parameters:
+ p0:
+ type: string
+ default: CORRECT
+ p1:
+ type: string
+ default: INCORRECT
+ p2:
+ type: string
+ default: INCORRECT
+ resources:
+ r1:
+ type: test::R1
+ r2:
+ type: test::R2
+ r3a:
+ type: test::R3
+ r3b:
+ type: test::R3
+'''
+
+ENV_1 = '''
+ parameters:
+ p1: CORRECT
+ p2: INCORRECT-E1
+ resource_registry:
+ test::R1: OS::Heat::RandomString
+ test::R2: BROKEN
+ test::R3: OS::Heat::None
+'''
+
+ENV_2 = '''
+ parameters:
+ p2: CORRECT
+ resource_registry:
+ test::R2: OS::Heat::RandomString
+ resources:
+ r3b:
+ test::R3: OS::Heat::RandomString
+'''
+
+
+class EnvironmentMergingTests(functional_base.FunctionalTestsBase):
+
+ def test_server_environment_merging(self):
+
+ # Setup
+ files = {'env1.yaml': ENV_1, 'env2.yaml': ENV_2}
+ environment_files = ['env1.yaml', 'env2.yaml']
+
+ # Test
+ stack_id = self.stack_create(stack_name='env_merge',
+ template=TEMPLATE,
+ files=files,
+ environment_files=environment_files)
+
+ # Verify
+
+ # Since there is no environment show, the registry overriding
+ # is partially verified by there being no error. If it wasn't
+ # working, test::R2 would remain mapped to BROKEN in env1.
+
+ # Sanity check
+ resources = self.list_resources(stack_id)
+ self.assertEqual(4, len(resources))
+
+ # Verify the parameters are correctly set
+ stack = self.client.stacks.get(stack_id)
+ self.assertEqual('CORRECT', stack.parameters['p0'])
+ self.assertEqual('CORRECT', stack.parameters['p1'])
+ self.assertEqual('CORRECT', stack.parameters['p2'])
+
+ # Verify that r3b has been overridden into a RandomString
+ # by checking to see that it has a value
+ r3b = self.client.resources.get(stack_id, 'r3b')
+ r3b_attrs = r3b.attributes
+ self.assertTrue('value' in r3b_attrs)
diff --git a/functional/test_event_sinks.py b/functional/test_event_sinks.py
new file mode 100644
index 0000000..e4a23ff
--- /dev/null
+++ b/functional/test_event_sinks.py
@@ -0,0 +1,66 @@
+# 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 uuid
+
+from zaqarclient.queues.v1 import client as zaqarclient
+
+from heat_integrationtests.functional import functional_base
+
+
+class ZaqarEventSinkTest(functional_base.FunctionalTestsBase):
+ template = '''
+heat_template_version: "2013-05-23"
+resources:
+ test_resource:
+ type: OS::Heat::TestResource
+ properties:
+ value: ok
+'''
+
+ def test_events(self):
+ queue_id = str(uuid.uuid4())
+ environment = {'event_sinks': [{'type': 'zaqar-queue',
+ 'target': queue_id,
+ 'ttl': 120}]}
+ stack_identifier = self.stack_create(
+ template=self.template,
+ environment=environment)
+ stack_name, stack_id = stack_identifier.split('/')
+ conf = {
+ 'auth_opts': {
+ 'backend': 'keystone',
+ 'options': {
+ 'os_username': self.conf.username,
+ 'os_password': self.conf.password,
+ 'os_project_name': self.conf.tenant_name,
+ 'os_auth_url': self.conf.auth_url
+ }
+ }
+ }
+
+ zaqar = zaqarclient.Client(conf=conf, version=1.1)
+ queue = zaqar.queue(queue_id)
+ messages = list(queue.messages())
+ self.assertEqual(4, len(messages))
+ types = [m.body['type'] for m in messages]
+ self.assertEqual(['os.heat.event'] * 4, types)
+ resources = set([m.body['payload']['resource_name'] for m in messages])
+ self.assertEqual(set([stack_name, 'test_resource']), resources)
+ stack_ids = [m.body['payload']['stack_id'] for m in messages]
+ self.assertEqual([stack_id] * 4, stack_ids)
+ statuses = [m.body['payload']['resource_status'] for m in messages]
+ statuses.sort()
+ self.assertEqual(
+ ['COMPLETE', 'COMPLETE', 'IN_PROGRESS', 'IN_PROGRESS'], statuses)
+ actions = [m.body['payload']['resource_action'] for m in messages]
+ self.assertEqual(['CREATE'] * 4, actions)
diff --git a/functional/test_hooks.py b/functional/test_hooks.py
index d9ebd44..bafb0ef 100644
--- a/functional/test_hooks.py
+++ b/functional/test_hooks.py
@@ -191,7 +191,7 @@
res_after.physical_resource_id)
def test_hook_pre_create_nested(self):
- files = {'nested.yaml': yaml.dump(self.template)}
+ files = {'nested.yaml': yaml.safe_dump(self.template)}
env = {'resource_registry':
{'resources':
{'nested':
diff --git a/functional/test_immutable_parameters.py b/functional/test_immutable_parameters.py
new file mode 100644
index 0000000..d223b14
--- /dev/null
+++ b/functional/test_immutable_parameters.py
@@ -0,0 +1,141 @@
+# 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
+from heatclient import exc as heat_exceptions
+
+
+class ImmutableParametersTest(functional_base.FunctionalTestsBase):
+
+ template_param_has_no_immutable_field = '''
+heat_template_version: 2014-10-16
+parameters:
+ param1:
+ type: string
+ default: default_value
+outputs:
+ param1_output:
+ description: 'parameter 1 details'
+ value: { get_param: param1 }
+'''
+
+ template_param_has_immutable_field = '''
+heat_template_version: 2014-10-16
+parameters:
+ param1:
+ type: string
+ default: default_value
+ immutable: false
+outputs:
+ param1_output:
+ description: 'parameter 1 details'
+ value: { get_param: param1 }
+'''
+
+ def test_no_immutable_param_field(self):
+ param1_create_value = 'value1'
+ create_parameters = {"param1": param1_create_value}
+
+ stack_identifier = self.stack_create(
+ template=self.template_param_has_no_immutable_field,
+ parameters=create_parameters
+ )
+ stack = self.client.stacks.get(stack_identifier)
+
+ # Verify the value of the parameter
+ self.assertEqual(param1_create_value,
+ self._stack_output(stack, 'param1_output'))
+
+ param1_update_value = 'value2'
+ update_parameters = {"param1": param1_update_value}
+
+ self.update_stack(
+ stack_identifier,
+ template=self.template_param_has_no_immutable_field,
+ parameters=update_parameters)
+
+ stack = self.client.stacks.get(stack_identifier)
+
+ # Verify the value of the updated parameter
+ self.assertEqual(param1_update_value,
+ self._stack_output(stack, 'param1_output'))
+
+ def test_immutable_param_field_allowed(self):
+ param1_create_value = 'value1'
+ create_parameters = {"param1": param1_create_value}
+
+ stack_identifier = self.stack_create(
+ template=self.template_param_has_immutable_field,
+ parameters=create_parameters
+ )
+ stack = self.client.stacks.get(stack_identifier)
+
+ # Verify the value of the parameter
+ self.assertEqual(param1_create_value,
+ self._stack_output(stack, 'param1_output'))
+
+ param1_update_value = 'value2'
+ update_parameters = {"param1": param1_update_value}
+
+ self.update_stack(
+ stack_identifier,
+ template=self.template_param_has_immutable_field,
+ parameters=update_parameters)
+ stack = self.client.stacks.get(stack_identifier)
+
+ # Verify the value of the updated parameter
+ self.assertEqual(param1_update_value,
+ self._stack_output(stack, 'param1_output'))
+
+ # Ensure stack is not in a failed state
+ self.assertEqual('UPDATE_COMPLETE', stack.stack_status)
+
+ def test_immutable_param_field_error(self):
+ param1_create_value = 'value1'
+ create_parameters = {"param1": param1_create_value}
+
+ # Toggle the immutable field to preclude updating
+ immutable_true = self.template_param_has_immutable_field.replace(
+ 'immutable: false', 'immutable: true')
+
+ stack_identifier = self.stack_create(
+ template=immutable_true,
+ parameters=create_parameters
+ )
+ stack = self.client.stacks.get(stack_identifier)
+
+ param1_update_value = 'value2'
+ update_parameters = {"param1": param1_update_value}
+
+ # Verify the value of the parameter
+ self.assertEqual(param1_create_value,
+ self._stack_output(stack, 'param1_output'))
+
+ # Attempt to update the stack with a new parameter value
+ try:
+ self.update_stack(
+ stack_identifier,
+ template=immutable_true,
+ parameters=update_parameters)
+ except heat_exceptions.HTTPBadRequest as exc:
+ exp = ('The following parameters are immutable and may not be '
+ 'updated: param1')
+ self.assertIn(exp, str(exc))
+
+ stack = self.client.stacks.get(stack_identifier)
+
+ # Ensure stack is not in a failed state
+ self.assertEqual('CREATE_COMPLETE', stack.stack_status)
+
+ # Ensure immutable parameter has not changed
+ self.assertEqual(param1_create_value,
+ self._stack_output(stack, 'param1_output'))
diff --git a/functional/test_nova_server_networks.py b/functional/test_nova_server_networks.py
index 99311d0..ae550b2 100644
--- a/functional/test_nova_server_networks.py
+++ b/functional/test_nova_server_networks.py
@@ -64,3 +64,25 @@
parameters=parms)
networks = self.get_outputs(stack_identifier, 'networks')
self.assertEqual(['11.11.11.11'], networks['my_net'])
+
+ def test_create_update_server_with_subnet(self):
+ parms = {'flavor': self.conf.minimal_instance_type,
+ 'image': self.conf.minimal_image_ref}
+ template = server_with_sub_fixed_ip_template.replace(
+ 'fixed_ip: 11.11.11.11', 'fixed_ip: 11.11.11.22')
+ stack_identifier = self.stack_create(
+ template=template,
+ stack_name='create_server_with_sub_ip',
+ parameters=parms)
+ networks = self.get_outputs(stack_identifier, 'networks')
+ self.assertEqual(['11.11.11.22'], networks['my_net'])
+
+ # update the server only with subnet, we won't pass
+ # both port_id and net_id to attach interface, then update success
+ template_only_subnet = template.replace(
+ 'fixed_ip: 11.11.11.22', '')
+ self.update_stack(stack_identifier,
+ template_only_subnet,
+ parameters=parms)
+ new_networks = self.get_outputs(stack_identifier, 'networks')
+ self.assertNotEqual(['11.11.11.22'], new_networks['my_net'])
diff --git a/functional/test_preview.py b/functional/test_preview.py
index b927a25..5846523 100644
--- a/functional/test_preview.py
+++ b/functional/test_preview.py
@@ -43,7 +43,7 @@
def setUp(self):
super(StackPreviewTest, self).setUp()
self.client = self.orchestration_client
- self.project_id = self.identity_client.auth_ref.project_id
+ self.project_id = self.identity_client.project_id
def _assert_resource(self, res, stack_name):
self.assertEqual(stack_name, res['stack_name'])
diff --git a/functional/test_preview_update.py b/functional/test_preview_update.py
index 0e39bc9..971e9c5 100644
--- a/functional/test_preview_update.py
+++ b/functional/test_preview_update.py
@@ -54,7 +54,14 @@
}
-class UpdatePreviewStackTest(functional_base.FunctionalTestsBase):
+class UpdatePreviewBase(functional_base.FunctionalTestsBase):
+
+ def assert_empty_sections(self, changes, empty_sections):
+ for section in empty_sections:
+ self.assertEqual([], changes[section])
+
+
+class UpdatePreviewStackTest(UpdatePreviewBase):
def test_add_resource(self):
self.stack_identifier = self.stack_create(
@@ -69,9 +76,7 @@
added = changes['added'][0]['resource_name']
self.assertEqual('test2', added)
- empty_sections = ('updated', 'replaced', 'deleted')
- for section in empty_sections:
- self.assertEqual([], changes[section])
+ self.assert_empty_sections(changes, ['updated', 'replaced', 'deleted'])
def test_no_change(self):
self.stack_identifier = self.stack_create(
@@ -83,9 +88,8 @@
unchanged = changes['unchanged'][0]['resource_name']
self.assertEqual('test1', unchanged)
- empty_sections = ('updated', 'replaced', 'deleted', 'added')
- for section in empty_sections:
- self.assertEqual([], changes[section])
+ self.assert_empty_sections(
+ changes, ['updated', 'replaced', 'deleted', 'added'])
def test_update_resource(self):
self.stack_identifier = self.stack_create(
@@ -113,9 +117,8 @@
updated = changes['updated'][0]['resource_name']
self.assertEqual('test1', updated)
- empty_sections = ('added', 'unchanged', 'replaced', 'deleted')
- for section in empty_sections:
- self.assertEqual([], changes[section])
+ self.assert_empty_sections(
+ changes, ['added', 'unchanged', 'replaced', 'deleted'])
def test_replaced_resource(self):
self.stack_identifier = self.stack_create(
@@ -139,9 +142,8 @@
replaced = changes['replaced'][0]['resource_name']
self.assertEqual('test1', replaced)
- empty_sections = ('added', 'unchanged', 'updated', 'deleted')
- for section in empty_sections:
- self.assertEqual([], changes[section])
+ self.assert_empty_sections(
+ changes, ['added', 'unchanged', 'updated', 'deleted'])
def test_delete_resource(self):
self.stack_identifier = self.stack_create(
@@ -156,6 +158,141 @@
deleted = changes['deleted'][0]['resource_name']
self.assertEqual('test2', deleted)
- empty_sections = ('updated', 'replaced', 'added')
- for section in empty_sections:
- self.assertEqual([], changes[section])
+ self.assert_empty_sections(changes, ['updated', 'replaced', 'added'])
+
+
+class UpdatePreviewStackTestNested(UpdatePreviewBase):
+ template_nested_parent = '''
+heat_template_version: 2016-04-08
+resources:
+ nested1:
+ type: nested1.yaml
+'''
+
+ template_nested1 = '''
+heat_template_version: 2016-04-08
+resources:
+ nested2:
+ type: nested2.yaml
+'''
+
+ template_nested2 = '''
+heat_template_version: 2016-04-08
+resources:
+ random:
+ type: OS::Heat::RandomString
+'''
+
+ template_nested2_2 = '''
+heat_template_version: 2016-04-08
+resources:
+ random:
+ type: OS::Heat::RandomString
+ random2:
+ type: OS::Heat::RandomString
+'''
+
+ def _get_by_resource_name(self, changes, name, action):
+ filtered_l = [x for x in changes[action]
+ if x['resource_name'] == name]
+ self.assertEqual(1, len(filtered_l))
+ return filtered_l[0]
+
+ def test_nested_resources_nochange(self):
+ files = {'nested1.yaml': self.template_nested1,
+ 'nested2.yaml': self.template_nested2}
+ self.stack_identifier = self.stack_create(
+ template=self.template_nested_parent, files=files)
+ result = self.preview_update_stack(
+ self.stack_identifier,
+ template=self.template_nested_parent,
+ files=files, show_nested=True)
+ changes = result['resource_changes']
+
+ # The nested random resource should be unchanged, but we always
+ # update nested stacks even when there are no changes
+ self.assertEqual(1, len(changes['unchanged']))
+ self.assertEqual('random', changes['unchanged'][0]['resource_name'])
+ self.assertEqual('nested2', changes['unchanged'][0]['parent_resource'])
+
+ self.assertEqual(2, len(changes['updated']))
+ u_nested1 = self._get_by_resource_name(changes, 'nested1', 'updated')
+ self.assertNotIn('parent_resource', u_nested1)
+ u_nested2 = self._get_by_resource_name(changes, 'nested2', 'updated')
+ self.assertEqual('nested1', u_nested2['parent_resource'])
+
+ self.assert_empty_sections(changes, ['replaced', 'deleted', 'added'])
+
+ def test_nested_resources_add(self):
+ files = {'nested1.yaml': self.template_nested1,
+ 'nested2.yaml': self.template_nested2}
+ self.stack_identifier = self.stack_create(
+ template=self.template_nested_parent, files=files)
+ files['nested2.yaml'] = self.template_nested2_2
+ result = self.preview_update_stack(
+ self.stack_identifier,
+ template=self.template_nested_parent,
+ files=files, show_nested=True)
+ changes = result['resource_changes']
+
+ # The nested random resource should be unchanged, but we always
+ # update nested stacks even when there are no changes
+ self.assertEqual(1, len(changes['unchanged']))
+ self.assertEqual('random', changes['unchanged'][0]['resource_name'])
+ self.assertEqual('nested2', changes['unchanged'][0]['parent_resource'])
+
+ self.assertEqual(1, len(changes['added']))
+ self.assertEqual('random2', changes['added'][0]['resource_name'])
+ self.assertEqual('nested2', changes['added'][0]['parent_resource'])
+
+ self.assert_empty_sections(changes, ['replaced', 'deleted'])
+
+ def test_nested_resources_delete(self):
+ files = {'nested1.yaml': self.template_nested1,
+ 'nested2.yaml': self.template_nested2_2}
+ self.stack_identifier = self.stack_create(
+ template=self.template_nested_parent, files=files)
+ files['nested2.yaml'] = self.template_nested2
+ result = self.preview_update_stack(
+ self.stack_identifier,
+ template=self.template_nested_parent,
+ files=files, show_nested=True)
+ changes = result['resource_changes']
+
+ # The nested random resource should be unchanged, but we always
+ # update nested stacks even when there are no changes
+ self.assertEqual(1, len(changes['unchanged']))
+ self.assertEqual('random', changes['unchanged'][0]['resource_name'])
+ self.assertEqual('nested2', changes['unchanged'][0]['parent_resource'])
+
+ self.assertEqual(1, len(changes['deleted']))
+ self.assertEqual('random2', changes['deleted'][0]['resource_name'])
+ self.assertEqual('nested2', changes['deleted'][0]['parent_resource'])
+
+ self.assert_empty_sections(changes, ['replaced', 'added'])
+
+ def test_nested_resources_replace(self):
+ files = {'nested1.yaml': self.template_nested1,
+ 'nested2.yaml': self.template_nested2}
+ self.stack_identifier = self.stack_create(
+ template=self.template_nested_parent, files=files)
+ parent_none = self.template_nested_parent.replace(
+ 'nested1.yaml', 'OS::Heat::None')
+ result = self.preview_update_stack(
+ self.stack_identifier,
+ template=parent_none,
+ show_nested=True)
+ changes = result['resource_changes']
+
+ # The nested random resource should be unchanged, but we always
+ # update nested stacks even when there are no changes
+ self.assertEqual(1, len(changes['replaced']))
+ self.assertEqual('nested1', changes['replaced'][0]['resource_name'])
+
+ self.assertEqual(2, len(changes['deleted']))
+ d_random = self._get_by_resource_name(changes, 'random', 'deleted')
+ self.assertEqual('nested2', d_random['parent_resource'])
+ d_nested2 = self._get_by_resource_name(changes, 'nested2', 'deleted')
+ self.assertEqual('nested1', d_nested2['parent_resource'])
+
+ self.assert_empty_sections(changes, ['updated', 'unchanged', 'added'])
diff --git a/functional/test_resource_group.py b/functional/test_resource_group.py
index e40376c..1e9edd5 100644
--- a/functional/test_resource_group.py
+++ b/functional/test_resource_group.py
@@ -419,7 +419,7 @@
super(ResourceGroupAdoptTest, self).setUp()
def _yaml_to_json(self, yaml_templ):
- return yaml.load(yaml_templ)
+ return yaml.safe_load(yaml_templ)
def test_adopt(self):
data = {
@@ -455,7 +455,7 @@
}
},
"environment": {"parameters": {}},
- "template": yaml.load(self.main_template)
+ "template": yaml.safe_load(self.main_template)
}
stack_identifier = self.stack_adopt(
adopt_data=json.dumps(data))
@@ -556,7 +556,7 @@
Simple rolling update with no conflict in batch size
and minimum instances in service.
"""
- updt_template = yaml.load(copy.deepcopy(self.template))
+ updt_template = yaml.safe_load(copy.deepcopy(self.template))
grp = updt_template['resources']['random_group']
policy = grp['update_policy']['rolling_update']
policy['min_in_service'] = '1'
@@ -575,7 +575,7 @@
Simple rolling update replace with no conflict in batch size
and minimum instances in service.
"""
- updt_template = yaml.load(copy.deepcopy(self.template))
+ updt_template = yaml.safe_load(copy.deepcopy(self.template))
grp = updt_template['resources']['random_group']
policy = grp['update_policy']['rolling_update']
policy['min_in_service'] = '1'
@@ -594,7 +594,7 @@
Simple rolling update with reduced size.
"""
- updt_template = yaml.load(copy.deepcopy(self.template))
+ updt_template = yaml.safe_load(copy.deepcopy(self.template))
grp = updt_template['resources']['random_group']
policy = grp['update_policy']['rolling_update']
policy['min_in_service'] = '1'
@@ -613,7 +613,7 @@
Simple rolling update with increased size.
"""
- updt_template = yaml.load(copy.deepcopy(self.template))
+ updt_template = yaml.safe_load(copy.deepcopy(self.template))
grp = updt_template['resources']['random_group']
policy = grp['update_policy']['rolling_update']
policy['min_in_service'] = '1'
@@ -632,7 +632,7 @@
Update with capacity adjustment with enough resources.
"""
- updt_template = yaml.load(copy.deepcopy(self.template))
+ updt_template = yaml.safe_load(copy.deepcopy(self.template))
grp = updt_template['resources']['random_group']
policy = grp['update_policy']['rolling_update']
policy['min_in_service'] = '8'
@@ -652,7 +652,7 @@
Rolling update with capacity adjustment due to conflict in
batch size and minimum instances in service.
"""
- updt_template = yaml.load(copy.deepcopy(self.template))
+ updt_template = yaml.safe_load(copy.deepcopy(self.template))
grp = updt_template['resources']['random_group']
policy = grp['update_policy']['rolling_update']
policy['min_in_service'] = '8'
@@ -671,7 +671,7 @@
Rolling Update with a huge batch size(more than
current size).
"""
- updt_template = yaml.load(copy.deepcopy(self.template))
+ updt_template = yaml.safe_load(copy.deepcopy(self.template))
grp = updt_template['resources']['random_group']
policy = grp['update_policy']['rolling_update']
policy['min_in_service'] = '0'
@@ -689,7 +689,7 @@
Rolling Update with a huge number of minimum instances
in service.
"""
- updt_template = yaml.load(copy.deepcopy(self.template))
+ updt_template = yaml.safe_load(copy.deepcopy(self.template))
grp = updt_template['resources']['random_group']
policy = grp['update_policy']['rolling_update']
policy['min_in_service'] = '20'
diff --git a/functional/test_template_resource.py b/functional/test_template_resource.py
index 9249a6e..9a3e833 100644
--- a/functional/test_template_resource.py
+++ b/functional/test_template_resource.py
@@ -602,7 +602,7 @@
super(TemplateResourceAdoptTest, self).setUp()
def _yaml_to_json(self, yaml_templ):
- return yaml.load(yaml_templ)
+ return yaml.safe_load(yaml_templ)
def test_abandon(self):
stack_identifier = self.stack_create(
@@ -635,7 +635,7 @@
}
},
"environment": {"parameters": {}},
- "template": yaml.load(self.main_template)
+ "template": yaml.safe_load(self.main_template)
}
stack_identifier = self.stack_adopt(
@@ -848,3 +848,90 @@
exp = ('ERROR: Attribute here-it-is for facade '
'OS::Thingy missing in provider')
self.assertEqual(exp, six.text_type(exc))
+
+
+class TemplateResourceNewParamTest(functional_base.FunctionalTestsBase):
+
+ main_template = '''
+heat_template_version: 2013-05-23
+resources:
+ my_resource:
+ type: resource.yaml
+ properties:
+ value1: foo
+'''
+ nested_templ = '''
+heat_template_version: 2013-05-23
+parameters:
+ value1:
+ type: string
+resources:
+ test:
+ type: OS::Heat::TestResource
+ properties:
+ value: {get_param: value1}
+'''
+ main_template_update = '''
+heat_template_version: 2013-05-23
+resources:
+ my_resource:
+ type: resource.yaml
+ properties:
+ value1: foo
+ value2: foo
+'''
+ nested_templ_update_fail = '''
+heat_template_version: 2013-05-23
+parameters:
+ value1:
+ type: string
+ value2:
+ type: string
+resources:
+ test:
+ type: OS::Heat::TestResource
+ properties:
+ fail: True
+ value:
+ str_replace:
+ template: VAL1-VAL2
+ params:
+ VAL1: {get_param: value1}
+ VAL2: {get_param: value2}
+'''
+ nested_templ_update = '''
+heat_template_version: 2013-05-23
+parameters:
+ value1:
+ type: string
+ value2:
+ type: string
+resources:
+ test:
+ type: OS::Heat::TestResource
+ properties:
+ value:
+ str_replace:
+ template: VAL1-VAL2
+ params:
+ VAL1: {get_param: value1}
+ VAL2: {get_param: value2}
+'''
+
+ def test_update(self):
+ stack_identifier = self.stack_create(
+ template=self.main_template,
+ files={'resource.yaml': self.nested_templ})
+
+ # Make the update fails with the new parameter inserted.
+ self.update_stack(
+ stack_identifier,
+ self.main_template_update,
+ files={'resource.yaml': self.nested_templ_update_fail},
+ expected_status='UPDATE_FAILED')
+
+ # Fix the update, it should succeed now.
+ self.update_stack(
+ stack_identifier,
+ self.main_template_update,
+ files={'resource.yaml': self.nested_templ_update})
diff --git a/functional/test_update_restricted.py b/functional/test_update_restricted.py
new file mode 100644
index 0000000..ae1907b
--- /dev/null
+++ b/functional/test_update_restricted.py
@@ -0,0 +1,155 @@
+# 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
+
+test_template = {
+ 'heat_template_version': '2013-05-23',
+ 'description': 'Test template to create one instance.',
+ 'resources': {
+ 'bar': {
+ 'type': 'OS::Heat::TestResource',
+ 'properties': {
+ 'value': '1234',
+ 'update_replace': False,
+ }
+ }
+ }
+}
+
+env_both_restrict = {u'resource_registry': {
+ u'resources': {
+ 'bar': {'restricted_actions': ['update', 'replace']}
+ }
+}
+}
+
+env_replace_restrict = {u'resource_registry': {
+ u'resources': {
+ '*ar': {'restricted_actions': 'replace'}
+ }
+}
+}
+
+reason_update_restrict = 'update is restricted for resource.'
+reason_replace_restrict = 'replace is restricted for resource.'
+
+
+class UpdateRestrictedStackTest(functional_base.FunctionalTestsBase):
+
+ def _check_for_restriction_reason(self, events,
+ reason, num_expected=1):
+ matched = [e for e in events
+ if e.resource_status_reason == reason]
+ return len(matched) == num_expected
+
+ def test_update(self):
+ stack_identifier = self.stack_create(template=test_template)
+
+ update_template = test_template.copy()
+ props = update_template['resources']['bar']['properties']
+ props['value'] = '4567'
+
+ # check update fails - with 'both' restricted
+ self.update_stack(stack_identifier, update_template,
+ env_both_restrict,
+ expected_status='UPDATE_FAILED')
+
+ self.assertTrue(self.verify_resource_status(stack_identifier, 'bar',
+ 'CREATE_COMPLETE'))
+ resource_events = self.client.events.list(stack_identifier, 'bar')
+ self.assertTrue(
+ self._check_for_restriction_reason(resource_events,
+ reason_update_restrict))
+
+ # check update succeeds - with only 'replace' restricted
+ self.update_stack(stack_identifier, update_template,
+ env_replace_restrict,
+ expected_status='UPDATE_COMPLETE')
+
+ self.assertTrue(self.verify_resource_status(stack_identifier, 'bar',
+ 'UPDATE_COMPLETE'))
+ resource_events = self.client.events.list(stack_identifier, 'bar')
+ self.assertFalse(
+ self._check_for_restriction_reason(resource_events,
+ reason_update_restrict, 2))
+ self.assertTrue(
+ self._check_for_restriction_reason(resource_events,
+ reason_replace_restrict, 0))
+
+ def test_replace(self):
+ stack_identifier = self.stack_create(template=test_template)
+
+ update_template = test_template.copy()
+ props = update_template['resources']['bar']['properties']
+ props['update_replace'] = True
+
+ # check replace fails - with 'both' restricted
+ self.update_stack(stack_identifier, update_template,
+ env_replace_restrict,
+ expected_status='UPDATE_FAILED')
+
+ self.assertTrue(self.verify_resource_status(stack_identifier, 'bar',
+ 'CREATE_COMPLETE'))
+ resource_events = self.client.events.list(stack_identifier, 'bar')
+ self.assertTrue(
+ self._check_for_restriction_reason(resource_events,
+ reason_replace_restrict))
+
+ # check replace fails - with only 'replace' restricted
+ self.update_stack(stack_identifier, update_template,
+ env_replace_restrict,
+ expected_status='UPDATE_FAILED')
+
+ self.assertTrue(self.verify_resource_status(stack_identifier, 'bar',
+ 'CREATE_COMPLETE'))
+ resource_events = self.client.events.list(stack_identifier, 'bar')
+ self.assertTrue(
+ self._check_for_restriction_reason(resource_events,
+ reason_replace_restrict, 2))
+ self.assertTrue(
+ self._check_for_restriction_reason(resource_events,
+ reason_update_restrict, 0))
+
+ def test_update_type_changed(self):
+ stack_identifier = self.stack_create(template=test_template)
+
+ update_template = test_template.copy()
+ rsrc = update_template['resources']['bar']
+ rsrc['type'] = 'OS::Heat::None'
+
+ # check replace fails - with 'both' restricted
+ self.update_stack(stack_identifier, update_template,
+ env_both_restrict,
+ expected_status='UPDATE_FAILED')
+
+ self.assertTrue(self.verify_resource_status(stack_identifier, 'bar',
+ 'CREATE_COMPLETE'))
+ resource_events = self.client.events.list(stack_identifier, 'bar')
+ self.assertTrue(
+ self._check_for_restriction_reason(resource_events,
+ reason_replace_restrict))
+
+ # check replace fails - with only 'replace' restricted
+ self.update_stack(stack_identifier, update_template,
+ env_replace_restrict,
+ expected_status='UPDATE_FAILED')
+
+ self.assertTrue(self.verify_resource_status(stack_identifier, 'bar',
+ 'CREATE_COMPLETE'))
+ resource_events = self.client.events.list(stack_identifier, 'bar')
+ self.assertTrue(
+ self._check_for_restriction_reason(resource_events,
+ reason_replace_restrict, 2))
+ self.assertTrue(
+ self._check_for_restriction_reason(resource_events,
+ reason_update_restrict, 0))
diff --git a/functional/test_waitcondition.py b/functional/test_waitcondition.py
new file mode 100644
index 0000000..fd5399f
--- /dev/null
+++ b/functional/test_waitcondition.py
@@ -0,0 +1,72 @@
+# 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 json
+
+from keystoneclient.v3 import client as keystoneclient
+from zaqarclient.queues.v1 import client as zaqarclient
+
+from heat_integrationtests.functional import functional_base
+
+
+class ZaqarWaitConditionTest(functional_base.FunctionalTestsBase):
+ template = '''
+heat_template_version: "2013-05-23"
+
+resources:
+ wait_condition:
+ type: OS::Heat::WaitCondition
+ properties:
+ handle: {get_resource: wait_handle}
+ timeout: 120
+ wait_handle:
+ type: OS::Heat::WaitConditionHandle
+ properties:
+ signal_transport: ZAQAR_SIGNAL
+
+outputs:
+ wait_data:
+ value: {'Fn::Select': ['data_id', {get_attr: [wait_condition, data]}]}
+'''
+
+ def test_signal_queues(self):
+ stack_identifier = self.stack_create(
+ template=self.template,
+ expected_status=None)
+ self._wait_for_resource_status(stack_identifier, 'wait_handle',
+ 'CREATE_COMPLETE')
+ resource = self.client.resources.get(stack_identifier, 'wait_handle')
+ signal = json.loads(resource.attributes['signal'])
+ ks = keystoneclient.Client(
+ auth_url=signal['auth_url'],
+ user_id=signal['user_id'],
+ password=signal['password'],
+ project_id=signal['project_id'])
+ endpoint = ks.service_catalog.url_for(
+ service_type='messaging', endpoint_type='publicURL')
+ conf = {
+ 'auth_opts': {
+ 'backend': 'keystone',
+ 'options': {
+ 'os_auth_token': ks.auth_token,
+ 'os_project_id': signal['project_id']
+ }
+ }
+ }
+
+ zaqar = zaqarclient.Client(endpoint, conf=conf, version=1.1)
+
+ queue = zaqar.queue(signal['queue_id'])
+ queue.post({'body': {'data': 'here!', 'id': 'data_id'}, 'ttl': 600})
+ self._wait_for_stack_status(stack_identifier, 'CREATE_COMPLETE')
+ stack = self.client.stacks.get(stack_identifier)
+ self.assertEqual('here!', stack.outputs[0]['output_value'])
diff --git a/scenario/test_autoscaling_lb.py b/scenario/test_autoscaling_lb.py
index bba55e1..e3de091 100644
--- a/scenario/test_autoscaling_lb.py
+++ b/scenario/test_autoscaling_lb.py
@@ -38,8 +38,12 @@
resp = set()
for count in range(retries):
time.sleep(1)
- r = requests.get(url)
- # skip unsuccessfull requests
+ try:
+ r = requests.get(url)
+ except requests.exceptions.ConnectionError:
+ # The LB may not be up yet, let's retry
+ continue
+ # skip unsuccessful requests
if r.status_code == 200:
resp.add(r.text)
self.assertEqual(expected_num, len(resp))