Add signal_transport property to Heat wait conditions

This change enhances Heat wait condition resources with selectable
signal types. The token based signal is kept for compatibility, but now
the user can opt for any of the other signal types supported by the
SignalResponder class.

Change-Id: Iafc28954b743f0dc46a49d29d42e7123827930b8
Implements: blueprint uniform-resource-signals
This commit is contained in:
Miguel Grinberg 2015-08-28 23:32:11 -07:00
parent 3a827fed7d
commit 939f0fddce
8 changed files with 426 additions and 48 deletions

View File

@ -87,6 +87,11 @@ class HeatWaitCondition(resource.Resource):
def _get_handle_resource(self):
return self.stack.resource_by_refid(self.properties[self.HANDLE])
def _validate_handle_resource(self, handle):
if not isinstance(handle, wc_base.BaseWaitConditionHandle):
raise ValueError(_('%(name)s is not a valid wait condition '
'handle.') % {'name': handle.name})
def _wait(self, handle, started_at, timeout_in):
if timeutils.is_older_than(started_at, timeout_in):
exc = wc_base.WaitConditionTimeout(self, handle)
@ -109,6 +114,7 @@ class HeatWaitCondition(resource.Resource):
def handle_create(self):
handle = self._get_handle_resource()
self._validate_handle_resource(handle)
started_at = timeutils.utcnow()
return handle, started_at, float(self.properties[self.TIMEOUT])

View File

@ -13,10 +13,15 @@
import uuid
from oslo_serialization import jsonutils
from heat.common import exception
from heat.common.i18n import _
from heat.engine import attributes
from heat.engine import constraints
from heat.engine import properties
from heat.engine.resources.aws.cfn import wait_condition_handle as aws_wch
from heat.engine.resources import signal_responder
from heat.engine.resources import wait_condition as wc_base
from heat.engine import support
@ -25,55 +30,115 @@ class HeatWaitConditionHandle(wc_base.BaseWaitConditionHandle):
support_status = support.SupportStatus(version='2014.2')
METADATA_KEYS = (
DATA, REASON, STATUS, UNIQUE_ID
PROPERTIES = (
SIGNAL_TRANSPORT,
) = (
'data', 'reason', 'status', 'id'
'signal_transport',
)
SIGNAL_TRANSPORTS = (
CFN_SIGNAL, TEMP_URL_SIGNAL, HEAT_SIGNAL, NO_SIGNAL,
ZAQAR_SIGNAL, TOKEN_SIGNAL
) = (
'CFN_SIGNAL', 'TEMP_URL_SIGNAL', 'HEAT_SIGNAL', 'NO_SIGNAL',
'ZAQAR_SIGNAL', 'TOKEN_SIGNAL'
)
properties_schema = {
SIGNAL_TRANSPORT: properties.Schema(
properties.Schema.STRING,
_('How the client will signal the wait condition. CFN_SIGNAL '
'will allow an HTTP POST to a CFN keypair signed URL. '
'TEMP_URL_SIGNAL will create a Swift TempURL to be '
'signalled via HTTP PUT. HEAT_SIGNAL will allow calls to the '
'Heat API resource-signal using the provided keystone '
'credentials. ZAQAR_SIGNAL will create a dedicated zaqar queue '
'to be signalled using the provided keystone credentials. '
'TOKEN_SIGNAL will allow and HTTP POST to a Heat API endpoint '
'with the provided keystone token. NO_SIGNAL will result in '
'the resource going to a signalled state without waiting for '
'any signal.'),
default='TOKEN_SIGNAL',
constraints=[
constraints.AllowedValues(SIGNAL_TRANSPORTS),
],
support_status=support.SupportStatus(version='6.0.0'),
),
}
ATTRIBUTES = (
TOKEN,
ENDPOINT,
CURL_CLI,
SIGNAL,
) = (
'token',
'endpoint',
'curl_cli',
'signal',
)
attributes_schema = {
TOKEN: attributes.Schema(
_('Token for stack-user which can be used for signalling handle'),
_('Token for stack-user which can be used for signalling handle '
'when signal_transport is set to TOKEN_SIGNAL. None for all '
'other signal transports.'),
cache_mode=attributes.Schema.CACHE_NONE,
type=attributes.Schema.STRING
),
ENDPOINT: attributes.Schema(
_('Endpoint/url which can be used for signalling handle'),
_('Endpoint/url which can be used for signalling handle when '
'signal_transport is set to TOKEN_SIGNAL. None for all '
'other signal transports.'),
cache_mode=attributes.Schema.CACHE_NONE,
type=attributes.Schema.STRING
),
CURL_CLI: attributes.Schema(
_('Convenience attribute, provides curl CLI command '
'prefix, which can be used for signalling handle completion or '
'failure. You can signal success by adding '
'failure when signal_transport is set to TOKEN_SIGNAL. You '
' can signal success by adding '
'--data-binary \'{"status": "SUCCESS"}\' '
', or signal failure by adding '
'--data-binary \'{"status": "FAILURE"}\''),
'--data-binary \'{"status": "FAILURE"}\'. '
'This attribute is set to None for all other signal '
'transports.'),
cache_mode=attributes.Schema.CACHE_NONE,
type=attributes.Schema.STRING
),
SIGNAL: attributes.Schema(
_('JSON serialized map that includes the endpoint, token and/or '
'other attributes the client must use for signalling this '
'handle. The contents of this map depend on the type of signal '
'selected in the signal_transport property.'),
cache_mode=attributes.Schema.CACHE_NONE,
type=attributes.Schema.STRING
)
}
METADATA_KEYS = (
DATA, REASON, STATUS, UNIQUE_ID
) = (
'data', 'reason', 'status', 'id'
)
def _signal_transport_token(self):
return self.properties.get(
self.SIGNAL_TRANSPORT) == self.TOKEN_SIGNAL
def handle_create(self):
self.password = uuid.uuid4().hex
super(HeatWaitConditionHandle, self).handle_create()
# FIXME(shardy): The assumption here is that token expiry > timeout
# but we probably need a check here to fail fast if that's not true
# Also need to implement an update property, such that the handle
# can be replaced on update which will replace the token
token = self._user_token()
self.data_set('token', token, True)
self.data_set('endpoint', '%s/signal' % self._get_resource_endpoint())
if self._signal_transport_token():
# FIXME(shardy): The assumption here is that token expiry > timeout
# but we probably need a check here to fail fast if that's not true
# Also need to implement an update property, such that the handle
# can be replaced on update which will replace the token
token = self._user_token()
self.data_set('token', token, True)
self.data_set('endpoint',
'%s/signal' % self._get_resource_endpoint())
def _get_resource_endpoint(self):
# Get the endpoint from stack.clients then replace the context
@ -89,19 +154,33 @@ class HeatWaitConditionHandle(wc_base.BaseWaitConditionHandle):
def _resolve_attribute(self, key):
if self.resource_id:
if key == self.TOKEN:
if key == self.SIGNAL:
return jsonutils.dumps(self._get_signal(
signal_type=signal_responder.WAITCONDITION,
multiple_signals=True))
elif key == self.TOKEN:
return self.data().get('token')
elif key == self.ENDPOINT:
return self.data().get('endpoint')
elif key == self.CURL_CLI:
# Construct curl command for template-author convenience
endpoint = self.data().get('endpoint')
token = self.data().get('token')
if endpoint is None or token is None:
return None
return ("curl -i -X POST "
"-H 'X-Auth-Token: %(token)s' "
"-H 'Content-Type: application/json' "
"-H 'Accept: application/json' "
"%(endpoint)s" %
dict(token=self.data().get('token'),
endpoint=self.data().get('endpoint')))
dict(token=token, endpoint=endpoint))
def get_status(self):
# before we check status, we have to update the signal transports
# that require constant polling
self._service_signal()
return super(HeatWaitConditionHandle, self).get_status()
def handle_signal(self, details=None):
"""Validate and update the resource metadata.

View File

@ -16,9 +16,13 @@ import uuid
from keystoneclient.contrib.ec2 import utils as ec2_utils
from oslo_config import cfg
from oslo_log import log as logging
from oslo_serialization import jsonutils
from six.moves.urllib import parse as urlparse
from heat.common import exception
from heat.common.i18n import _
from heat.common.i18n import _LW
from heat.engine.clients.os import swift
from heat.engine.resources import stack_user
LOG = logging.getLogger(__name__)
@ -34,6 +38,18 @@ SIGNAL_VERB = {WAITCONDITION: 'PUT',
class SignalResponder(stack_user.StackUser):
PROPERTIES = (
SIGNAL_TRANSPORT,
) = (
'signal_transport',
)
ATTRIBUTES = (
SIGNAL_ATTR,
) = (
'signal',
)
# Anything which subclasses this may trigger authenticated
# API operations as a consequence of handling a signal
requires_deferred_auth = True
@ -59,6 +75,26 @@ class SignalResponder(stack_user.StackUser):
else:
self.data_set('password', password, True)
def _signal_transport_cfn(self):
return self.properties[
self.SIGNAL_TRANSPORT] == self.CFN_SIGNAL
def _signal_transport_heat(self):
return self.properties[
self.SIGNAL_TRANSPORT] == self.HEAT_SIGNAL
def _signal_transport_none(self):
return self.properties[
self.SIGNAL_TRANSPORT] == self.NO_SIGNAL
def _signal_transport_temp_url(self):
return self.properties[
self.SIGNAL_TRANSPORT] == self.TEMP_URL_SIGNAL
def _signal_transport_zaqar(self):
return self.properties.get(
self.SIGNAL_TRANSPORT) == self.ZAQAR_SIGNAL
def _get_heat_signal_credentials(self):
"""Return OpenStack credentials that can be used to send a signal.
@ -73,7 +109,8 @@ class SignalResponder(stack_user.StackUser):
'username': self.physical_resource_name(),
'user_id': self._get_user_id(),
'password': self.password,
'project_id': self.stack.stack_user_project_id}
'project_id': self.stack.stack_user_project_id,
'domain_id': self.keystone().stack_domain_id}
def _get_ec2_signed_url(self, signal_type=SIGNAL):
"""Create properly formatted and pre-signed URL.
@ -147,10 +184,13 @@ class SignalResponder(stack_user.StackUser):
self.data_delete('ec2_signed_url')
self._delete_keypair()
def _get_heat_signal_url(self):
def _get_heat_signal_url(self, project_id=None):
"""Return a heat-api signal URL for this resource.
This URL is not pre-signed, valid user credentials are required.
If a project_id is provided, it is used in place of the original
project_id. This is useful to generate a signal URL that uses
the heat stack user project instead of the user's.
"""
stored = self.data().get('heat_signal_url')
if stored is not None:
@ -163,6 +203,8 @@ class SignalResponder(stack_user.StackUser):
url = self.client_plugin('heat').get_heat_url()
host_url = urlparse.urlparse(url)
path = self.identifier().url_path()
if project_id is not None:
path = project_id + path[path.find('/'):]
url = urlparse.urlunsplit(
(host_url.scheme, host_url.netloc, 'v1/%s/signal' % path, '', ''))
@ -173,10 +215,12 @@ class SignalResponder(stack_user.StackUser):
def _delete_heat_signal_url(self):
self.data_delete('heat_signal_url')
def _get_swift_signal_url(self):
def _get_swift_signal_url(self, multiple_signals=False):
"""Create properly formatted and pre-signed Swift signal URL.
This uses a Swift pre-signed temp_url.
This uses a Swift pre-signed temp_url. If multiple_signals is
requested, the Swift object referenced by the returned URL will have
versioning enabled.
"""
put_url = self.data().get('swift_signal_url')
if put_url:
@ -191,13 +235,16 @@ class SignalResponder(stack_user.StackUser):
self.client('swift').put_container(container)
put_url = self.client_plugin('swift').get_temp_url(
container, object_name)
if multiple_signals:
put_url = self.client_plugin('swift').get_signal_url(container,
object_name)
else:
put_url = self.client_plugin('swift').get_temp_url(container,
object_name)
self.client('swift').put_object(container, object_name, '')
self.data_set('swift_signal_url', put_url)
self.data_set('swift_signal_object_name', object_name)
self.client('swift').put_object(
container, object_name, '')
return put_url
def _delete_swift_signal_url(self):
@ -205,12 +252,22 @@ class SignalResponder(stack_user.StackUser):
if not object_name:
return
try:
container = self.physical_resource_name()
container_name = self.stack.id
swift = self.client('swift')
swift.delete_object(container, object_name)
headers = swift.head_container(container)
# delete all versions of the object, in case there are some
# signals that are waiting to be handled
container = swift.get_container(container_name)
filtered = [obj for obj in container[1]
if object_name in obj['name']]
for obj in filtered:
# we delete the main object every time, swift takes
# care of restoring the previous version after each delete
swift.delete_object(container_name, object_name)
headers = swift.head_container(container_name)
if int(headers['x-container-object-count']) == 0:
swift.delete_container(container)
swift.delete_container(container_name)
except Exception as ex:
self.client_plugin('swift').ignore_not_found(ex)
self.data_delete('swift_signal_object_name')
@ -250,3 +307,102 @@ class SignalResponder(stack_user.StackUser):
except Exception as ex:
self.client_plugin('zaqar').ignore_not_found(ex)
self.data_delete('zaqar_signal_queue_id')
def _get_signal(self, signal_type=SIGNAL, multiple_signals=False):
"""Return a dictionary with signal details.
Subclasses can invoke this method to retrieve information of the
resource signal for the specified transport.
"""
signal = None
if self._signal_transport_cfn():
signal = {'alarm_url': self._get_ec2_signed_url(
signal_type=signal_type)}
elif self._signal_transport_heat():
signal = self._get_heat_signal_credentials()
signal['alarm_url'] = self._get_heat_signal_url(
project_id=self.stack.stack_user_project_id)
elif self._signal_transport_temp_url():
signal = {'alarm_url': self._get_swift_signal_url(
multiple_signals=multiple_signals)}
elif self._signal_transport_zaqar():
signal = self._get_heat_signal_credentials()
signal['queue_id'] = self._get_zaqar_signal_queue_id()
elif self._signal_transport_none():
signal = {}
return signal
def _service_swift_signal(self):
swift_client = self.client('swift')
try:
container = swift_client.get_container(self.stack.id)
except Exception as exc:
self.client_plugin('swift').ignore_not_found(exc)
LOG.debug("Swift container %s was not found" % self.stack.id)
return
index = container[1]
if not index: # Swift objects were deleted by user
LOG.debug("Swift objects in container %s were not found" %
self.stack.id)
return
# Remove objects that are for other resources, given that
# multiple swift signals in the same stack share a container
object_name = self.physical_resource_name()
filtered = [obj for obj in index if object_name in obj['name']]
# Fetch objects from Swift and filter results
signal_names = []
for obj in filtered:
try:
signal = swift_client.get_object(self.stack.id, obj['name'])
except Exception as exc:
self.client_plugin('swift').ignore_not_found(exc)
continue
body = signal[1]
if body == swift.IN_PROGRESS: # Ignore the initial object
continue
signal_names.append(obj['name'])
if body == "":
self.signal(details={})
continue
try:
self.signal(details=jsonutils.loads(body))
except ValueError:
raise exception.Error(_("Failed to parse JSON data: %s") %
body)
# remove the signals that were consumed
for signal_name in signal_names:
if signal_name != object_name:
swift_client.delete_object(self.stack.id, signal_name)
if object_name in signal_names:
swift_client.delete_object(self.stack.id, object_name)
def _service_zaqar_signal(self):
zaqar = self.client('zaqar')
try:
queue = zaqar.queue(self._get_zaqar_signal_queue_id())
except Exception as ex:
self.client_plugin('zaqar').ignore_not_found(ex)
messages = list(queue.pop())
for message in messages:
self.signal(details=message.body)
def _service_signal(self):
"""Service the signal, when necessary.
This method must be called repeatedly by subclasses to update the
state of the signals that require polling, which are the ones based on
Swift temp URLs and Zaqar queues. The "NO_SIGNAL" case is also handled
here by triggering the signal once per call.
"""
if self._signal_transport_temp_url():
self._service_swift_signal()
elif self._signal_transport_zaqar():
self._service_zaqar_signal()
elif self._signal_transport_none():
self.signal(details={})

View File

@ -94,7 +94,7 @@ class FakeKeystoneClient(object):
def __init__(self, username='test_username', password='password',
user_id='1234', access='4567', secret='8901',
credential_id='abcdxyz', auth_token='abcd1234',
context=None):
context=None, stack_domain_id='4321'):
self.username = username
self.password = password
self.user_id = user_id
@ -105,6 +105,7 @@ class FakeKeystoneClient(object):
self.token = auth_token
self.context = context
self.v3_endpoint = 'http://localhost:5000/v3'
self.stack_domain_id = stack_domain_id
class FakeCred(object):
id = self.credential_id

View File

@ -170,6 +170,14 @@ class ResourceWithRequiredPropsAndEmptyAttrs(GenericResource):
class SignalResource(signal_responder.SignalResponder):
SIGNAL_TRANSPORTS = (
CFN_SIGNAL, TEMP_URL_SIGNAL, HEAT_SIGNAL, NO_SIGNAL,
ZAQAR_SIGNAL
) = (
'CFN_SIGNAL', 'TEMP_URL_SIGNAL', 'HEAT_SIGNAL', 'NO_SIGNAL',
'ZAQAR_SIGNAL'
)
properties_schema = {
'signal_transport': properties.Schema(properties.Schema.STRING,
default='CFN_SIGNAL')}
@ -186,21 +194,10 @@ class SignalResource(signal_responder.SignalResponder):
def _resolve_attribute(self, name):
if self.resource_id is not None:
if self.properties['signal_transport'] == 'CFN_SIGNAL':
d = {'alarm_url': six.text_type(self._get_ec2_signed_url())}
elif self.properties['signal_transport'] == 'HEAT_SIGNAL':
d = self._get_heat_signal_credentials()
d['alarm_url'] = six.text_type(self._get_heat_signal_url())
elif self.properties['signal_transport'] == 'TEMP_URL_SIGNAL':
d = {'alarm_url': six.text_type(self._get_swift_signal_url())}
elif self.properties['signal_transport'] == 'ZAQAR_SIGNAL':
d = self._get_heat_signal_credentials()
d['queue_id'] = six.text_type(
self._get_zaqar_signal_queue_id())
if name == 'AlarmUrl':
return d['alarm_url']
return self._get_signal().get('alarm_url')
elif name == 'signal':
return d
return self._get_signal()
class StackUserResource(stack_user.StackUser):

View File

@ -14,6 +14,7 @@
import datetime
import uuid
import mox
from oslo_serialization import jsonutils as json
from oslo_utils import timeutils
import six
@ -22,6 +23,7 @@ from heat.common import exception
from heat.common import identifier
from heat.common import template_format
from heat.engine.clients.os import heat_plugin
from heat.engine.clients.os import swift as swift_plugin
from heat.engine import environment
from heat.engine.resources.openstack.heat import wait_condition_handle as h_wch
from heat.engine import stack as parser
@ -55,13 +57,49 @@ resources:
type: OS::Heat::WaitConditionHandle
'''
test_template_heat_waithandle = '''
test_template_heat_waithandle_token = '''
heat_template_version: 2013-05-23
resources:
wait_handle:
type: OS::Heat::WaitConditionHandle
'''
test_template_heat_waithandle_heat = '''
heat_template_version: 2013-05-23
resources:
wait_handle:
type: OS::Heat::WaitConditionHandle
properties:
signal_transport: HEAT_SIGNAL
'''
test_template_heat_waithandle_swift = '''
heat_template_version: 2013-05-23
resources:
wait_handle:
type: OS::Heat::WaitConditionHandle
properties:
signal_transport: TEMP_URL_SIGNAL
'''
test_template_heat_waithandle_zaqar = '''
heat_template_version: 2013-05-23
resources:
wait_handle:
type: OS::Heat::WaitConditionHandle
properties:
signal_transport: ZAQAR_SIGNAL
'''
test_template_heat_waithandle_none = '''
heat_template_version: 2013-05-23
resources:
wait_handle:
type: OS::Heat::WaitConditionHandle
properties:
signal_transport: NO_SIGNAL
'''
test_template_update_waithandle = '''
heat_template_version: 2013-05-23
resources:
@ -69,6 +107,18 @@ resources:
type: OS::Heat::UpdateWaitConditionHandle
'''
test_template_bad_waithandle = '''
heat_template_version: 2013-05-23
resources:
wait_condition:
type: OS::Heat::WaitCondition
properties:
handle: {get_resource: wait_handle}
timeout: 5
wait_handle:
type: OS::Heat::RandomString
'''
class HeatWaitConditionTest(common.HeatTestCase):
@ -155,6 +205,18 @@ class HeatWaitConditionTest(common.HeatTestCase):
self.assertEqual('wait_handle', r.name)
self.m.VerifyAll()
def test_bad_wait_handle(self):
self.stack = self.create_stack(
template=test_template_bad_waithandle)
self.m.ReplayAll()
self.stack.create()
rsrc = self.stack['wait_condition']
self.assertEqual((rsrc.CREATE, rsrc.FAILED), rsrc.state)
reason = rsrc.status_reason
self.assertEqual(reason, 'ValueError: resources.wait_condition: '
'wait_handle is not a valid wait condition '
'handle.')
def test_timeout(self):
self.stack = self.create_stack()
@ -269,9 +331,9 @@ class HeatWaitConditionTest(common.HeatTestCase):
json.loads(wc_att))
self.m.VerifyAll()
def _create_heat_handle(self):
self.stack = self.create_stack(
template=test_template_heat_waithandle, stub_status=False)
def _create_heat_handle(self,
template=test_template_heat_waithandle_token):
self.stack = self.create_stack(template=template, stub_status=False)
self.m.ReplayAll()
self.stack.create()
@ -351,6 +413,73 @@ class HeatWaitConditionTest(common.HeatTestCase):
self.assertEqual(expected, handle.FnGetAtt('curl_cli'))
self.m.VerifyAll()
def test_getatt_signal_heat(self):
handle = self._create_heat_handle(
template=test_template_heat_waithandle_heat)
self.assertIsNone(handle.FnGetAtt('token'))
self.assertIsNone(handle.FnGetAtt('endpoint'))
self.assertIsNone(handle.FnGetAtt('curl_cli'))
signal = json.loads(handle.FnGetAtt('signal'))
self.assertIn('alarm_url', signal)
self.assertIn('username', signal)
self.assertIn('password', signal)
self.assertIn('auth_url', signal)
self.assertIn('project_id', signal)
self.assertIn('domain_id', signal)
def test_getatt_signal_swift(self):
self.m.StubOutWithMock(swift_plugin.SwiftClientPlugin, 'get_temp_url')
self.m.StubOutWithMock(swift_plugin.SwiftClientPlugin, 'client')
class mock_swift(object):
@staticmethod
def put_container(container, **kwargs):
pass
@staticmethod
def put_object(container, object, contents, **kwargs):
pass
swift_plugin.SwiftClientPlugin.client().AndReturn(mock_swift)
swift_plugin.SwiftClientPlugin.client().AndReturn(mock_swift)
swift_plugin.SwiftClientPlugin.client().AndReturn(mock_swift)
swift_plugin.SwiftClientPlugin.get_temp_url(mox.IgnoreArg(),
mox.IgnoreArg(),
mox.IgnoreArg())\
.AndReturn('foo')
self.m.ReplayAll()
handle = self._create_heat_handle(
template=test_template_heat_waithandle_swift)
self.assertIsNone(handle.FnGetAtt('token'))
self.assertIsNone(handle.FnGetAtt('endpoint'))
self.assertIsNone(handle.FnGetAtt('curl_cli'))
signal = json.loads(handle.FnGetAtt('signal'))
self.assertIn('alarm_url', signal)
def test_getatt_signal_zaqar(self):
handle = self._create_heat_handle(
template=test_template_heat_waithandle_zaqar)
self.assertIsNone(handle.FnGetAtt('token'))
self.assertIsNone(handle.FnGetAtt('endpoint'))
self.assertIsNone(handle.FnGetAtt('curl_cli'))
signal = json.loads(handle.FnGetAtt('signal'))
self.assertIn('queue_id', signal)
self.assertIn('username', signal)
self.assertIn('password', signal)
self.assertIn('auth_url', signal)
self.assertIn('project_id', signal)
self.assertIn('domain_id', signal)
def test_getatt_signal_none(self):
handle = self._create_heat_handle(
template=test_template_heat_waithandle_none)
self.assertIsNone(handle.FnGetAtt('token'))
self.assertIsNone(handle.FnGetAtt('endpoint'))
self.assertIsNone(handle.FnGetAtt('curl_cli'))
self.assertEqual('{}', handle.FnGetAtt('signal'))
def test_create_update_updatehandle(self):
self.stack = self.create_stack(
template=test_template_update_waithandle, stub_status=False)

View File

@ -400,22 +400,31 @@ class SignalTest(common.HeatTestCase):
'delete_container')
self.m.StubOutWithMock(self.stack.clients.client('swift'),
'head_container')
self.m.StubOutWithMock(self.stack.clients.client('swift'),
'get_container')
self.m.StubOutWithMock(self.stack['signal_handler'],
'physical_resource_name')
self.stack['signal_handler'].physical_resource_name().AndReturn('bar')
self.stack.clients.client('swift').put_container(
mox.IgnoreArg()).AndReturn(None)
self.stack.clients.client_plugin('swift').get_temp_url(
mox.IgnoreArg(), mox.IgnoreArg()).AndReturn(
'http://server.test/v1/AUTH_aprojectid/foo/bar')
self.stack['signal_handler'].physical_resource_name().AndReturn('bar')
self.stack.clients.client('swift').put_object(
mox.IgnoreArg(), mox.IgnoreArg(), mox.IgnoreArg()).AndReturn(None)
self.stack.clients.client('swift').put_container(
mox.IgnoreArg()).AndReturn(None)
self.stack['signal_handler'].physical_resource_name().AndReturn('bar')
self.stack.clients.client_plugin('swift').get_temp_url(
mox.IgnoreArg(), mox.IgnoreArg()).AndReturn(
'http://server.test/v1/AUTH_aprojectid/foo/bar')
self.stack.clients.client('swift').put_object(
mox.IgnoreArg(), mox.IgnoreArg(), mox.IgnoreArg()).AndReturn(None)
self.stack.clients.client('swift').get_container(
mox.IgnoreArg()).AndReturn(({}, [{'name': 'bar'}]))
self.stack.clients.client('swift').delete_object(
mox.IgnoreArg(), mox.IgnoreArg()).AndReturn(None)
self.stack.clients.client('swift').head_container(

View File

@ -1031,6 +1031,7 @@ class SoftwareDeploymentTest(common.HeatTestCase):
return_value=dep_data)
sc = mock.MagicMock()
sc.get_container.return_value = ({}, [{'name': object_name}])
sc.head_container.return_value = {
'x-container-object-count': 0
}
@ -1040,7 +1041,7 @@ class SoftwareDeploymentTest(common.HeatTestCase):
self.deployment.id = 23
self.deployment.uuid = str(uuid.uuid4())
container = self.deployment.physical_resource_name()
container = self.stack.id
self.deployment._delete_swift_signal_url()
sc.delete_object.assert_called_once_with(container, object_name)
self.assertEqual(