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:
parent
3a827fed7d
commit
939f0fddce
|
@ -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])
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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={})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Reference in New Issue