diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index 0a4433191..c494057a7 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -30,6 +30,7 @@ import cinderclient.exceptions as cinder_exceptions import glanceclient import glanceclient.exc import heatclient.client +from heatclient.common import event_utils from heatclient.common import template_utils import keystoneauth1.exceptions import keystoneclient.client @@ -825,7 +826,7 @@ class OpenStackCloud(object): template_file=None, template_url=None, template_object=None, files=None, rollback=True, - wait=False, timeout=180, + wait=False, timeout=3600, environment_files=None, **parameters): envfiles, env = template_utils.process_multiple_environments_and_files( @@ -842,18 +843,15 @@ class OpenStackCloud(object): template=template, files=dict(list(tpl_files.items()) + list(envfiles.items())), environment=env, + timeout_mins=timeout // 60, ) with _utils.shade_exceptions("Error creating stack {name}".format( name=name)): - stack = self.manager.submitTask(_tasks.StackCreate(**params)) - if not wait: - return stack - for count in _utils._iterate_timeout( - timeout, - "Timed out waiting for heat stack to finish"): - stack = self.get_stack(name) - if stack: - return stack + self.manager.submitTask(_tasks.StackCreate(**params)) + if wait: + event_utils.poll_for_events(self.heat_client, stack_name=name, + action='CREATE') + return self.get_stack(name) def delete_stack(self, name_or_id): """Delete a Heat Stack diff --git a/shade/tests/functional/test_stack.py b/shade/tests/functional/test_stack.py new file mode 100644 index 000000000..b4d5a09f4 --- /dev/null +++ b/shade/tests/functional/test_stack.py @@ -0,0 +1,137 @@ +# -*- coding: utf-8 -*- + +# 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. + +""" +test_stack +---------------------------------- + +Functional tests for `shade` stack methods. +""" + +import tempfile + +from shade import openstack_cloud +from shade.tests import base + +simple_template = '''heat_template_version: 2014-10-16 +parameters: + length: + type: number + default: 10 + +resources: + my_rand: + type: OS::Heat::RandomString + properties: + length: {get_param: length} +outputs: + rand: + value: + get_attr: [my_rand, value] +''' + +root_template = '''heat_template_version: 2014-10-16 +parameters: + length: + type: number + default: 10 + count: + type: number + default: 5 + +resources: + my_rands: + type: OS::Heat::ResourceGroup + properties: + count: {get_param: count} + resource_def: + type: My::Simple::Template + properties: + length: {get_param: length} +outputs: + rands: + value: + get_attr: [my_rands, attributes, rand] +''' + +environment = ''' +resource_registry: + My::Simple::Template: %s +''' + + +class TestStack(base.TestCase): + + def setUp(self): + super(TestStack, self).setUp() + self.cloud = openstack_cloud(cloud='devstack') + if not self.cloud.has_service('orchestration'): + self.skipTest('Orchestration service not supported by cloud') + + def _cleanup_stack(self): + self.cloud.delete_stack(self.stack_name) + + def test_stack_simple(self): + test_template = tempfile.NamedTemporaryFile(delete=False) + test_template.write(simple_template) + test_template.close() + self.stack_name = self.getUniqueString('simple_stack') + self.addCleanup(self._cleanup_stack) + stack = self.cloud.create_stack(name=self.stack_name, + template_file=test_template.name, + wait=True) + + # assert expected values in stack + self.assertEqual('CREATE_COMPLETE', stack['stack_status']) + rand = stack['outputs'][0]['output_value'] + self.assertEqual(10, len(rand)) + + # assert get_stack matches returned create_stack + stack = self.cloud.get_stack(self.stack_name) + self.assertEqual('CREATE_COMPLETE', stack['stack_status']) + self.assertEqual(rand, stack['outputs'][0]['output_value']) + + # assert stack is in list_stacks + stacks = self.cloud.list_stacks() + stack_ids = [s['id'] for s in stacks] + self.assertIn(stack['id'], stack_ids) + + def test_stack_nested(self): + + test_template = tempfile.NamedTemporaryFile( + suffix='.yaml', delete=False) + test_template.write(root_template) + test_template.close() + + simple_tmpl = tempfile.NamedTemporaryFile(suffix='.yaml', delete=False) + simple_tmpl.write(simple_template) + simple_tmpl.close() + + env = tempfile.NamedTemporaryFile(suffix='.yaml', delete=False) + env.write(environment % simple_tmpl.name) + env.close() + + self.stack_name = self.getUniqueString('nested_stack') + self.addCleanup(self._cleanup_stack) + stack = self.cloud.create_stack(name=self.stack_name, + template_file=test_template.name, + environment_files=[env.name], + wait=True) + + # assert expected values in stack + self.assertEqual('CREATE_COMPLETE', stack['stack_status']) + rands = stack['outputs'][0]['output_value'] + self.assertEqual(['0', '1', '2', '3', '4'], sorted(rands.keys())) + for rand in rands.values(): + self.assertEqual(10, len(rand)) diff --git a/shade/tests/unit/test_stack.py b/shade/tests/unit/test_stack.py index 157a48ec2..383523bff 100644 --- a/shade/tests/unit/test_stack.py +++ b/shade/tests/unit/test_stack.py @@ -16,6 +16,7 @@ import mock import testtools +from heatclient.common import event_utils from heatclient.common import template_utils import shade @@ -119,16 +120,19 @@ class TestStack(base.TestCase): environment={}, parameters={}, template={}, - files={} + files={}, + timeout_mins=60, ) + @mock.patch.object(event_utils, 'poll_for_events') @mock.patch.object(template_utils, 'get_template_contents') @mock.patch.object(shade.OpenStackCloud, 'get_stack') @mock.patch.object(shade.OpenStackCloud, 'heat_client') - def test_create_stack_wait(self, mock_heat, mock_get, mock_template): + def test_create_stack_wait(self, mock_heat, mock_get, mock_template, + mock_poll): stack = {'id': 'stack_id', 'name': 'stack_name'} mock_template.return_value = ({}, {}) - mock_get.side_effect = iter([None, stack]) + mock_get.return_value = stack ret = self.cloud.create_stack('stack_name', wait=True) self.assertTrue(mock_template.called) mock_heat.stacks.create.assert_called_once_with( @@ -137,9 +141,11 @@ class TestStack(base.TestCase): environment={}, parameters={}, template={}, - files={} + files={}, + timeout_mins=60, ) - self.assertEqual(2, mock_get.call_count) + self.assertEqual(1, mock_get.call_count) + self.assertEqual(1, mock_poll.call_count) self.assertEqual(stack, ret) @mock.patch.object(shade.OpenStackCloud, 'heat_client')