Use event_utils.poll_for_events for stack polling

Calling get_stack to poll for stack state transitions causes
unnecessary high load on heat servers so should be avoided if possible
(this is true whether get_stack lists all stacks or a fetches a single
full stack).

Heatclient has a utility function which instead polls for stack events
(with a fallback to fetching the stack when events are not
forthcoming).  This function is used extensively by heat client and
the openstackclient stack commands - it would be appropriate to use it
here too.

The timeout is passed to the stack create call, meaning that the stack
will go to CREATE_FAILED if the timeout is exceeded. The default
timeout_mins is usually 60 minutes, so the client-side timeout would
never be reached anyway.

Also, the current polling approach was not filtering for
CREATE_COMPLETE so it wasn't actually waiting for anything.

This change adds functional tests which cover get_stack, create_stack
and list_stacks. test_stack_nested exercises the stack_create
environment_files file composition.

Change-Id: Ia14d47f0f51e1f8825b6de6d8dc5a12335913f55
This commit is contained in:
Steve Baker 2016-03-14 10:38:28 +13:00
parent 677f656dbd
commit 2ed2254879
3 changed files with 156 additions and 15 deletions

View File

@ -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

View File

@ -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))

View File

@ -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')