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:
parent
677f656dbd
commit
2ed2254879
@ -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
|
||||
|
137
shade/tests/functional/test_stack.py
Normal file
137
shade/tests/functional/test_stack.py
Normal 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))
|
@ -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')
|
||||
|
Loading…
x
Reference in New Issue
Block a user