diff --git a/heat/engine/resources/swiftsignal.py b/heat/engine/resources/swiftsignal.py new file mode 100644 index 000000000..364fe51ca --- /dev/null +++ b/heat/engine/resources/swiftsignal.py @@ -0,0 +1,319 @@ +# +# 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. + +import json +import urlparse + +from swiftclient import client as swiftclient_client + +from heat.common import exception +from heat.engine import attributes +from heat.engine.clients.os import swift +from heat.engine import constraints +from heat.engine import properties +from heat.engine import resource +from heat.engine import scheduler +from heat.openstack.common.gettextutils import _ +from heat.openstack.common import log as logging + +LOG = logging.getLogger(__name__) + + +class SwiftSignalFailure(exception.Error): + def __init__(self, wait_cond): + reasons = wait_cond.get_status_reason(wait_cond.STATUS_FAILURE) + super(SwiftSignalFailure, self).__init__(';'.join(reasons)) + + +class SwiftSignalTimeout(exception.Error): + def __init__(self, wait_cond): + reasons = wait_cond.get_status_reason(wait_cond.STATUS_SUCCESS) + vals = {'len': len(reasons), + 'count': wait_cond.properties[wait_cond.COUNT]} + if reasons: + vals['reasons'] = ';'.join(reasons) + message = (_('%(len)d of %(count)d received - %(reasons)s') % vals) + else: + message = (_('%(len)d of %(count)d received') % vals) + super(SwiftSignalTimeout, self).__init__(message) + + +class SwiftSignalHandle(resource.Resource): + + properties_schema = {} + + ATTRIBUTES = ( + TOKEN, + ENDPOINT, + CURL_CLI, + ) = ( + 'token', + 'endpoint', + 'curl_cli', + ) + + attributes_schema = { + TOKEN: attributes.Schema( + _('Tokens are not needed for Swift TempURLs. This attribute is ' + 'being kept for compatibility with the ' + 'OS::Heat::WaitConditionHandle resource'), + cache_mode=attributes.Schema.CACHE_NONE + ), + ENDPOINT: attributes.Schema( + _('Endpoint/url which can be used for signalling handle'), + cache_mode=attributes.Schema.CACHE_NONE + ), + 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 ' + '--data-binary \'{"status": "SUCCESS"}\' ' + ', or signal failure by adding ' + '--data-binary \'{"status": "FAILURE"}\''), + cache_mode=attributes.Schema.CACHE_NONE + ), + } + + def handle_create(self): + sc = self.client_plugin('swift') + url = sc.get_signal_url(self.stack.id, self.physical_resource_name()) + self.data_set('endpoint', url) + self.resource_id_set(url) + + def update(self, after, before=None, prev_resource=None): + raise resource.UpdateReplace(self.name) + + def _resolve_attribute(self, key): + if self.resource_id: + if key == self.TOKEN: + return '' # HeatWaitConditionHandle compatibility + elif key == self.ENDPOINT: + return self.data().get('endpoint') + elif key == self.CURL_CLI: + return ('curl -i -X PUT \'%s\'' % self.data().get('endpoint')) + + +class SwiftSignal(resource.Resource): + + PROPERTIES = (HANDLE, TIMEOUT, COUNT,) = ('handle', 'timeout', 'count',) + + properties_schema = { + HANDLE: properties.Schema( + properties.Schema.STRING, + required=True, + description=_('URL of TempURL where resource will signal ' + 'completion and optionally upload data.') + ), + TIMEOUT: properties.Schema( + properties.Schema.NUMBER, + description=_('The maximum number of seconds to wait for the ' + 'resource to signal completion. Once the timeout ' + 'is reached, creation of the signal resource will ' + 'fail.'), + required=True, + constraints=[ + constraints.Range(1, 43200), + ] + ), + COUNT: properties.Schema( + properties.Schema.NUMBER, + description=_('The number of success signals that must be ' + 'received before the stack creation process ' + 'continues.'), + default=1, + constraints=[ + constraints.Range(1, 1000), + ] + ) + } + + ATTRIBUTES = (DATA) = 'data' + + attributes_schema = { + DATA: attributes.Schema( + _('JSON data that was uploaded via the SwiftSignalHandle.') + ) + } + + WAIT_STATUSES = ( + STATUS_FAILURE, + STATUS_SUCCESS, + ) = ( + 'FAILURE', + 'SUCCESS', + ) + + METADATA_KEYS = ( + DATA, REASON, STATUS, UNIQUE_ID + ) = ( + 'data', 'reason', 'status', 'id' + ) + + def __init__(self, name, json_snippet, stack): + super(SwiftSignal, self).__init__(name, json_snippet, stack) + self._obj_name = None + self._url = None + + @property + def url(self): + if not self._url: + self._url = urlparse.urlparse(self.properties[self.HANDLE]) + return self._url + + @property + def obj_name(self): + if not self._obj_name: + self._obj_name = self.url.path.split('/')[4] + return self._obj_name + + def _validate_handle_url(self): + parts = self.url.path.split('/') + msg = _('"%(url)s" is not a valid SwiftSignalHandle. The %(part)s ' + 'is invalid') + sc = self.client_plugin('swift') + if not sc.is_valid_temp_url_path(self.url.path): + raise ValueError(msg % {'url': self.url.path, + 'part': 'Swift TempURL path'}) + if not parts[2].endswith(self.context.tenant_id): + raise ValueError(msg % {'url': self.url.path, + 'part': 'tenant'}) + if not parts[3] == self.stack.id: + raise ValueError(msg % {'url': self.url.path, + 'part': 'container name'}) + + def handle_create(self): + self._validate_handle_url() + runner = scheduler.TaskRunner(self._wait) + runner.start(timeout=float(self.properties.get(self.TIMEOUT))) + return runner + + def _wait(self): + while True: + try: + yield + except scheduler.Timeout: + count = self.properties.get(self.COUNT) + raise SwiftSignalTimeout(self) + + count = self.properties.get(self.COUNT) + statuses = self.get_status() + if not statuses: + continue + + for status in statuses: + if status == self.STATUS_FAILURE: + failure = SwiftSignalFailure(self) + LOG.info(_('%(name)s Failed (%(failure)s)') + % {'name': str(self), 'failure': str(failure)}) + raise failure + elif status != self.STATUS_SUCCESS: + raise exception.Error(_("Unknown status: %s") % status) + + if len(statuses) >= count: + LOG.info(_("%s Succeeded") % str(self)) + return + + def get_signals(self): + try: + container = self.swift().get_container(self.stack.id) + except swiftclient_client.ClientException as exc: + if exc.http_status == 404: # Swift container was deleted by user + return None + raise exc + + index = container[1] + if not index: # Swift objects were deleted by user + return None + + # Remove objects in that are for other handle resources, since + # multiple SwiftSignalHandle resources in the same stack share + # a container + filtered = [obj for obj in index if self.obj_name in obj['name']] + + # Fetch objects from Swift and filter results + obj_bodies = [] + for obj in filtered: + try: + signal = self.swift().get_object(self.stack.id, obj['name']) + except swiftclient_client.ClientException as exc: + if exc.http_status == 404: # Swift object disappeared + continue + raise exc + + body = signal[1] + if body == swift.IN_PROGRESS: # Ignore the initial object + continue + if body == "": + obj_bodies.append({}) + continue + try: + obj_bodies.append(json.loads(body)) + except ValueError: + raise exception.Error(_("Failed to parse JSON data: %s") % + body) + + # Set default values on each signal + signals = [] + signal_num = 1 + for signal in obj_bodies: + + # Remove previous signals with the same ID + id = self.UNIQUE_ID + ids = [s.get(id) for s in signals if id in s] + if ids and id in signal and ids.count(signal[id]) > 0: + [signals.remove(s) for s in signals if s.get(id) == signal[id]] + + # Make sure all fields are set, since all are optional + signal.setdefault(self.DATA, None) + unique_id = signal.setdefault(self.UNIQUE_ID, signal_num) + reason = 'Signal %s recieved' % unique_id + signal.setdefault(self.REASON, reason) + signal.setdefault(self.STATUS, self.STATUS_SUCCESS) + + signals.append(signal) + signal_num += 1 + + return signals + + def get_status(self): + return [s[self.STATUS] for s in self.get_signals()] + + def get_status_reason(self, status): + return [s[self.REASON] + for s in self.get_signals() + if s[self.STATUS] == status] + + def get_data(self): + signals = self.get_signals() + if not signals: + return None + data = {} + for signal in signals: + data[signal[self.UNIQUE_ID]] = signal[self.DATA] + return data + + def check_create_complete(self, runner): + return runner.step() + + def _resolve_attribute(self, key): + if key == self.DATA: + return unicode(json.dumps(self.get_data())) + + +def resource_mapping(): + return {'OS::Heat::SwiftSignal': SwiftSignal, + 'OS::Heat::SwiftSignalHandle': SwiftSignalHandle} + + +def available_resource_mapping(): + return resource_mapping() diff --git a/heat/tests/test_swiftsignal.py b/heat/tests/test_swiftsignal.py new file mode 100644 index 000000000..818bae5a9 --- /dev/null +++ b/heat/tests/test_swiftsignal.py @@ -0,0 +1,756 @@ +# +# 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. + +import copy +import json +import time +import uuid + +import mock +import six +from swiftclient import client as swiftclient_client +from testtools.matchers import MatchesRegex + +from heat.common import template_format +from heat.engine.clients.os import swift +from heat.engine import environment +from heat.engine import resource +from heat.engine import rsrc_defn +from heat.engine import scheduler +from heat.engine import stack +from heat.tests.common import HeatTestCase +from heat.tests import utils + + +swiftsignal_template = ''' +heat_template_version: 2013-05-23 + +resources: + test_wait_condition: + type: "OS::Heat::SwiftSignal" + properties: + handle: { get_resource: test_wait_condition_handle } + timeout: 1 + count: 2 + + test_wait_condition_handle: + type: "OS::Heat::SwiftSignalHandle" +''' + +swiftsignalhandle_template = ''' +heat_template_version: 2013-05-23 + +resources: + test_wait_condition_handle: + type: "OS::Heat::SwiftSignalHandle" +''' + +container_header = { + 'content-length': '2', + 'x-container-object-count': '0', + 'accept-ranges': 'bytes', + 'date': 'Fri, 25 Jul 2014 16:02:03 GMT', + 'x-timestamp': '1405019787.66969', + 'x-trans-id': 'tx6651b005324341f685e71-0053d27f7bdfw1', + 'x-container-bytes-used': '0', + 'content-type': 'application/json; charset=utf-8', + 'x-versions-location': 'test' +} + +obj_header = { + 'content-length': '5', + 'accept-ranges': 'bytes', + 'last-modified': 'Fri, 25 Jul 2014 16:05:26 GMT', + 'etag': '5a105e8b9d40e1329780d62ea2265d8a', + 'x-timestamp': '1406304325.40094', + 'x-trans-id': 'tx2f40ff2b4daa4015917fc-0053d28045dfw1', + 'date': 'Fri, 25 Jul 2014 16:05:25 GMT', + 'content-type': 'application/octet-stream' +} + + +def create_stack(template, stack_id=None): + tmpl = template_format.parse(template) + template = stack.Template(tmpl) + ctx = utils.dummy_context(tenant_id='test_tenant') + st = stack.Stack(ctx, 'test_st', template, + environment.Environment(), + disable_rollback=True) + + # Stub out the stack ID so we have a known value + if stack_id is None: + stack_id = str(uuid.uuid4()) + with utils.UUIDStub(stack_id): + st.store() + st.id = stack_id + + return st + + +def cont_index(obj_name, num_version_hist): + objects = [{'bytes': 11, + 'last_modified': '2014-07-03T19:42:03.281640', + 'hash': '9214b4e4460fcdb9f3a369941400e71e', + 'name': "02b" + obj_name + '/1404416326.51383', + 'content_type': 'application/octet-stream'}] * num_version_hist + objects.append({'bytes': 8, + 'last_modified': '2014-07-03T19:42:03.849870', + 'hash': '9ab7c0738852d7dd6a2dc0b261edc300', + 'name': obj_name, + 'content_type': 'application/x-www-form-urlencoded'}) + return (container_header, objects) + + +class SwiftSignalHandleTest(HeatTestCase): + def setUp(self): + super(SwiftSignalHandleTest, self).setUp() + utils.setup_dummy_db() + + @mock.patch.object(swift.SwiftClientPlugin, '_create') + @mock.patch.object(resource.Resource, 'physical_resource_name') + def test_create(self, mock_name, mock_swift): + st = create_stack(swiftsignalhandle_template) + handle = st['test_wait_condition_handle'] + + mock_swift_object = mock.Mock() + mock_swift.return_value = mock_swift_object + mock_swift_object.head_account.return_value = { + 'x-account-meta-temp-url-key': "1234" + } + mock_swift_object.url = "http://fake-host.com:8080/v1/AUTH_1234" + obj_name = "%s-%s-abcdefghijkl" % (st.name, handle.name) + mock_name.return_value = obj_name + mock_swift_object.get_container.return_value = cont_index(obj_name, 2) + mock_swift_object.get_object.return_value = (obj_header, '{"id": "1"}') + + st.create() + handle = st.resources['test_wait_condition_handle'] + obj_name = "%s-%s-abcdefghijkl" % (st.name, handle.name) + regexp = ("http://fake-host.com:8080/v1/AUTH_test_tenant/%s/test_st-" + "test_wait_condition_handle-abcdefghijkl" + "\?temp_url_sig=[0-9a-f]{40}&temp_url_expires=[0-9]{10}" + % st.id) + res_id = st.resources['test_wait_condition_handle'].resource_id + self.assertThat(res_id, MatchesRegex(regexp)) + + # Since the account key is mocked out above + self.assertFalse(mock_swift_object.post_account.called) + + header = {'x-versions-location': st.id} + self.assertEqual({'headers': header}, + mock_swift_object.put_container.call_args[1]) + + @mock.patch.object(swift.SwiftClientPlugin, '_create') + def test_handle_update(self, mock_swift): + st = create_stack(swiftsignalhandle_template) + + mock_swift_object = mock.Mock() + mock_swift.return_value = mock_swift_object + mock_swift_object.head_account.return_value = {} + mock_swift_object.url = "http://fake-host.com:8080/v1/AUTH_1234" + + st.create() + + handle = st['test_wait_condition_handle'] + uprops = copy.copy(handle.properties.data) + uprops['count'] = '5' + update_snippet = rsrc_defn.ResourceDefinition(handle.name, + handle.type(), + uprops) + + updater = scheduler.TaskRunner(handle.update, update_snippet) + self.assertRaises(resource.UpdateReplace, updater) + + +class SwiftSignalTest(HeatTestCase): + def setUp(self): + super(SwiftSignalTest, self).setUp() + utils.setup_dummy_db() + + @mock.patch.object(swift.SwiftClientPlugin, '_create') + @mock.patch.object(resource.Resource, 'physical_resource_name') + def test_create(self, mock_name, mock_swift): + st = create_stack(swiftsignal_template) + handle = st['test_wait_condition_handle'] + + mock_swift_object = mock.Mock() + mock_swift.return_value = mock_swift_object + mock_swift_object.url = "http://fake-host.com:8080/v1/AUTH_1234" + mock_swift_object.head_account.return_value = { + 'x-account-meta-temp-url-key': '123456' + } + obj_name = "%s-%s-abcdefghijkl" % (st.name, handle.name) + mock_name.return_value = obj_name + mock_swift_object.get_container.return_value = cont_index(obj_name, 2) + mock_swift_object.get_object.return_value = (obj_header, '') + + st.create() + self.assertEqual(('CREATE', 'COMPLETE'), st.state) + + @mock.patch.object(swift.SwiftClientPlugin, 'get_signal_url') + def test_validate_handle_url_bad_tempurl(self, mock_handle_url): + mock_handle_url.return_value = ( + "http://fake-host.com:8080/v1/my-container/" + "test_st-test_wait_condition_handle?temp_url_sig=" + "12d8f9f2c923fbeb555041d4ed63d83de6768e95&" + "temp_url_expires=1404762741") + st = create_stack(swiftsignal_template) + + st.create() + self.assertIn('not a valid SwiftSignalHandle. The Swift TempURL path', + six.text_type(st.status_reason)) + + @mock.patch.object(swift.SwiftClientPlugin, 'get_signal_url') + def test_validate_handle_url_bad_container_name(self, mock_handle_url): + mock_handle_url.return_value = ( + "http://fake-host.com:8080/v1/AUTH_test_tenant/my-container/" + "test_st-test_wait_condition_handle?temp_url_sig=" + "12d8f9f2c923fbeb555041d4ed63d83de6768e95&" + "temp_url_expires=1404762741") + st = create_stack(swiftsignal_template) + + st.create() + self.assertIn('not a valid SwiftSignalHandle. The container name', + six.text_type(st.status_reason)) + + @mock.patch.object(swift.SwiftClientPlugin, 'get_signal_url') + def test_validate_handle_url_bad_tenant(self, mock_handle_url): + stack_id = '1234' + mock_handle_url.return_value = ( + "http://fake-host.com:8080/v1/AUTH_foo/%s/" + "test_st-test_wait_condition_handle?temp_url_sig=" + "12d8f9f2c923fbeb555041d4ed63d83de6768e95&" + "temp_url_expires=1404762741" % stack_id) + st = create_stack(swiftsignal_template, stack_id=stack_id) + + st.create() + self.assertIn('not a valid SwiftSignalHandle. The tenant', + six.text_type(st.status_reason)) + + @mock.patch.object(swift.SwiftClientPlugin, '_create') + @mock.patch.object(resource.Resource, 'physical_resource_name') + def test_multiple_signals_same_id_complete(self, mock_name, mock_swift): + st = create_stack(swiftsignal_template) + handle = st['test_wait_condition_handle'] + + mock_swift_object = mock.Mock() + mock_swift.return_value = mock_swift_object + mock_swift_object.url = "http://fake-host.com:8080/v1/AUTH_1234" + mock_swift_object.head_account.return_value = { + 'x-account-meta-temp-url-key': '123456' + } + obj_name = "%s-%s-abcdefghijkl" % (st.name, handle.name) + mock_name.return_value = obj_name + mock_swift_object.get_container.return_value = cont_index(obj_name, 2) + mock_swift_object.get_object.side_effect = ( + (obj_header, json.dumps({'id': 1})), + (obj_header, json.dumps({'id': 1})), + (obj_header, json.dumps({'id': 1})), + + (obj_header, json.dumps({'id': 1})), + (obj_header, json.dumps({'id': 2})), + (obj_header, json.dumps({'id': 3})), + ) + + st.create() + self.assertEqual(('CREATE', 'COMPLETE'), st.state) + + @mock.patch.object(scheduler, 'wallclock') + @mock.patch.object(swift.SwiftClientPlugin, '_create') + @mock.patch.object(resource.Resource, 'physical_resource_name') + def test_multiple_signals_same_id_timeout(self, mock_name, mock_swift, + mock_clock): + st = create_stack(swiftsignal_template) + handle = st['test_wait_condition_handle'] + + mock_swift_object = mock.Mock() + mock_swift.return_value = mock_swift_object + mock_swift_object.url = "http://fake-host.com:8080/v1/AUTH_1234" + mock_swift_object.head_account.return_value = { + 'x-account-meta-temp-url-key': '123456' + } + obj_name = "%s-%s-abcdefghijkl" % (st.name, handle.name) + mock_name.return_value = obj_name + mock_swift_object.get_container.return_value = cont_index(obj_name, 2) + mock_swift_object.get_object.return_value = (obj_header, + json.dumps({'id': 1})) + + time_now = time.time() + time_series = [t + time_now for t in xrange(1, 100)] + scheduler.wallclock.side_effect = time_series + + st.create() + self.assertIn("Resource CREATE failed: SwiftSignalTimeout", + st.status_reason) + wc = st['test_wait_condition'] + self.assertEqual("SwiftSignalTimeout: 1 of 2 received - Signal 1 " + "recieved", wc.status_reason) + + @mock.patch.object(swift.SwiftClientPlugin, '_create') + @mock.patch.object(resource.Resource, 'physical_resource_name') + def test_post_complete_to_handle(self, mock_name, mock_swift): + st = create_stack(swiftsignal_template) + handle = st['test_wait_condition_handle'] + + mock_swift_object = mock.Mock() + mock_swift.return_value = mock_swift_object + mock_swift_object.url = "http://fake-host.com:8080/v1/AUTH_1234" + mock_swift_object.head_account.return_value = { + 'x-account-meta-temp-url-key': '123456' + } + obj_name = "%s-%s-abcdefghijkl" % (st.name, handle.name) + mock_name.return_value = obj_name + mock_swift_object.get_container.return_value = cont_index(obj_name, 2) + mock_swift_object.get_object.side_effect = ( + (obj_header, json.dumps({'id': 1, 'status': "SUCCESS"})), + (obj_header, json.dumps({'id': 1, 'status': "SUCCESS"})), + (obj_header, json.dumps({'id': 2, 'status': "SUCCESS"})), + ) + + st.create() + self.assertEqual(('CREATE', 'COMPLETE'), st.state) + + @mock.patch.object(swift.SwiftClientPlugin, '_create') + @mock.patch.object(resource.Resource, 'physical_resource_name') + def test_post_failed_to_handle(self, mock_name, mock_swift): + st = create_stack(swiftsignal_template) + handle = st['test_wait_condition_handle'] + + mock_swift_object = mock.Mock() + mock_swift.return_value = mock_swift_object + mock_swift_object.url = "http://fake-host.com:8080/v1/AUTH_1234" + mock_swift_object.head_account.return_value = { + 'x-account-meta-temp-url-key': '123456' + } + obj_name = "%s-%s-abcdefghijkl" % (st.name, handle.name) + mock_name.return_value = obj_name + mock_swift_object.get_container.return_value = cont_index(obj_name, 1) + mock_swift_object.get_object.side_effect = ( + # Create + (obj_header, json.dumps({'id': 1, 'status': "FAILURE", + 'reason': "foo"})), + (obj_header, json.dumps({'id': 2, 'status': "FAILURE", + 'reason': "bar"})), + + # SwiftSignalFailure + (obj_header, json.dumps({'id': 1, 'status': "FAILURE", + 'reason': "foo"})), + (obj_header, json.dumps({'id': 2, 'status': "FAILURE", + 'reason': "bar"})), + ) + + st.create() + self.assertEqual(('CREATE', 'FAILED'), st.state) + wc = st['test_wait_condition'] + self.assertEqual("SwiftSignalFailure: foo;bar", wc.status_reason) + + @mock.patch.object(swift.SwiftClientPlugin, '_create') + @mock.patch.object(resource.Resource, 'physical_resource_name') + def test_data(self, mock_name, mock_swift): + st = create_stack(swiftsignal_template) + handle = st['test_wait_condition_handle'] + + mock_swift_object = mock.Mock() + mock_swift.return_value = mock_swift_object + mock_swift_object.url = "http://fake-host.com:8080/v1/AUTH_1234" + mock_swift_object.head_account.return_value = { + 'x-account-meta-temp-url-key': '123456' + } + obj_name = "%s-%s-abcdefghijkl" % (st.name, handle.name) + mock_name.return_value = obj_name + mock_swift_object.get_container.return_value = cont_index(obj_name, 2) + + mock_swift_object.get_object.side_effect = ( + # st create + (obj_header, json.dumps({'id': 1, 'data': "foo"})), + (obj_header, json.dumps({'id': 2, 'data': "bar"})), + (obj_header, json.dumps({'id': 3, 'data': "baz"})), + + # FnGetAtt call + (obj_header, json.dumps({'id': 1, 'data': "foo"})), + (obj_header, json.dumps({'id': 2, 'data': "bar"})), + (obj_header, json.dumps({'id': 3, 'data': "baz"})), + ) + + st.create() + self.assertEqual(('CREATE', 'COMPLETE'), st.state) + wc = st['test_wait_condition'] + self.assertEqual(json.dumps({1: 'foo', 2: 'bar', 3: 'baz'}), + wc.FnGetAtt('data')) + + @mock.patch.object(swift.SwiftClientPlugin, '_create') + @mock.patch.object(resource.Resource, 'physical_resource_name') + def test_data_noid(self, mock_name, mock_swift): + st = create_stack(swiftsignal_template) + handle = st['test_wait_condition_handle'] + + mock_swift_object = mock.Mock() + mock_swift.return_value = mock_swift_object + mock_swift_object.url = "http://fake-host.com:8080/v1/AUTH_1234" + mock_swift_object.head_account.return_value = { + 'x-account-meta-temp-url-key': '123456' + } + obj_name = "%s-%s-abcdefghijkl" % (st.name, handle.name) + mock_name.return_value = obj_name + mock_swift_object.get_container.return_value = cont_index(obj_name, 1) + + mock_swift_object.get_object.side_effect = ( + # st create + (obj_header, json.dumps({'data': "foo", 'reason': "bar", + 'status': "SUCCESS"})), + (obj_header, json.dumps({'data': "dog", 'reason': "cat", + 'status': "SUCCESS"})), + + # FnGetAtt call + (obj_header, json.dumps({'data': "foo", 'reason': "bar", + 'status': "SUCCESS"})), + (obj_header, json.dumps({'data': "dog", 'reason': "cat", + 'status': "SUCCESS"})), + ) + + st.create() + self.assertEqual(('CREATE', 'COMPLETE'), st.state) + wc = st['test_wait_condition'] + self.assertEqual(json.dumps({1: 'foo', 2: 'dog'}), wc.FnGetAtt('data')) + + @mock.patch.object(swift.SwiftClientPlugin, '_create') + @mock.patch.object(resource.Resource, 'physical_resource_name') + def test_data_nodata(self, mock_name, mock_swift): + st = create_stack(swiftsignal_template) + handle = st['test_wait_condition_handle'] + + mock_swift_object = mock.Mock() + mock_swift.return_value = mock_swift_object + mock_swift_object.url = "http://fake-host.com:8080/v1/AUTH_1234" + mock_swift_object.head_account.return_value = { + 'x-account-meta-temp-url-key': '123456' + } + obj_name = "%s-%s-abcdefghijkl" % (st.name, handle.name) + mock_name.return_value = obj_name + mock_swift_object.get_container.return_value = cont_index(obj_name, 1) + + mock_swift_object.get_object.side_effect = ( + # st create + (obj_header, ''), + (obj_header, ''), + + # FnGetAtt call + (obj_header, ''), + (obj_header, ''), + ) + + st.create() + self.assertEqual(('CREATE', 'COMPLETE'), st.state) + wc = st['test_wait_condition'] + self.assertEqual(json.dumps({1: None, 2: None}), wc.FnGetAtt('data')) + + @mock.patch.object(swift.SwiftClientPlugin, '_create') + @mock.patch.object(resource.Resource, 'physical_resource_name') + def test_data_partial_complete(self, mock_name, mock_swift): + st = create_stack(swiftsignal_template) + handle = st['test_wait_condition_handle'] + wc = st['test_wait_condition'] + + mock_swift_object = mock.Mock() + mock_swift.return_value = mock_swift_object + mock_swift_object.url = "http://fake-host.com:8080/v1/AUTH_1234" + mock_swift_object.head_account.return_value = { + 'x-account-meta-temp-url-key': '123456' + } + obj_name = "%s-%s-abcdefghijkl" % (st.name, handle.name) + mock_name.return_value = obj_name + mock_swift_object.get_container.return_value = cont_index(obj_name, 1) + mock_swift_object.get_object.return_value = ( + obj_header, json.dumps({'status': 'SUCCESS'})) + + st.create() + self.assertEqual(['SUCCESS', 'SUCCESS'], wc.get_status()) + expected = [{'status': 'SUCCESS', 'reason': 'Signal 1 recieved', + 'data': None, 'id': 1}, + {'status': 'SUCCESS', 'reason': 'Signal 2 recieved', + 'data': None, 'id': 2}] + self.assertEqual(expected, wc.get_signals()) + + @mock.patch.object(swift.SwiftClientPlugin, '_create') + @mock.patch.object(resource.Resource, 'physical_resource_name') + def test_get_status_none_complete(self, mock_name, mock_swift): + st = create_stack(swiftsignal_template) + handle = st['test_wait_condition_handle'] + wc = st['test_wait_condition'] + + mock_swift_object = mock.Mock() + mock_swift.return_value = mock_swift_object + mock_swift_object.url = "http://fake-host.com:8080/v1/AUTH_1234" + mock_swift_object.head_account.return_value = { + 'x-account-meta-temp-url-key': '123456' + } + obj_name = "%s-%s-abcdefghijkl" % (st.name, handle.name) + mock_name.return_value = obj_name + mock_swift_object.get_container.return_value = cont_index(obj_name, 1) + mock_swift_object.get_object.return_value = (obj_header, '') + + st.create() + self.assertEqual(['SUCCESS', 'SUCCESS'], wc.get_status()) + expected = [{'status': 'SUCCESS', 'reason': 'Signal 1 recieved', + 'data': None, 'id': 1}, + {'status': 'SUCCESS', 'reason': 'Signal 2 recieved', + 'data': None, 'id': 2}] + self.assertEqual(expected, wc.get_signals()) + + @mock.patch.object(swift.SwiftClientPlugin, '_create') + @mock.patch.object(resource.Resource, 'physical_resource_name') + def test_get_status_partial_complete(self, mock_name, mock_swift): + st = create_stack(swiftsignal_template) + handle = st['test_wait_condition_handle'] + wc = st['test_wait_condition'] + + mock_swift_object = mock.Mock() + mock_swift.return_value = mock_swift_object + mock_swift_object.url = "http://fake-host.com:8080/v1/AUTH_1234" + mock_swift_object.head_account.return_value = { + 'x-account-meta-temp-url-key': '123456' + } + obj_name = "%s-%s-abcdefghijkl" % (st.name, handle.name) + mock_name.return_value = obj_name + mock_swift_object.get_container.return_value = cont_index(obj_name, 1) + mock_swift_object.get_object.return_value = ( + obj_header, json.dumps({'id': 1, 'status': "SUCCESS"})) + + st.create() + self.assertEqual(['SUCCESS'], wc.get_status()) + expected = [{'status': 'SUCCESS', 'reason': 'Signal 1 recieved', + 'data': None, 'id': 1}] + self.assertEqual(expected, wc.get_signals()) + + @mock.patch.object(swift.SwiftClientPlugin, '_create') + @mock.patch.object(resource.Resource, 'physical_resource_name') + def test_get_status_failure(self, mock_name, mock_swift): + st = create_stack(swiftsignal_template) + handle = st['test_wait_condition_handle'] + wc = st['test_wait_condition'] + + mock_swift_object = mock.Mock() + mock_swift.return_value = mock_swift_object + mock_swift_object.url = "http://fake-host.com:8080/v1/AUTH_1234" + mock_swift_object.head_account.return_value = { + 'x-account-meta-temp-url-key': '123456' + } + obj_name = "%s-%s-abcdefghijkl" % (st.name, handle.name) + mock_name.return_value = obj_name + mock_swift_object.get_container.return_value = cont_index(obj_name, 1) + mock_swift_object.get_object.return_value = ( + obj_header, json.dumps({'id': 1, 'status': "FAILURE"})) + + st.create() + self.assertEqual(('CREATE', 'FAILED'), st.state) + self.assertEqual(['FAILURE'], wc.get_status()) + expected = [{'status': 'FAILURE', 'reason': 'Signal 1 recieved', + 'data': None, 'id': 1}] + self.assertEqual(expected, wc.get_signals()) + + @mock.patch.object(swift.SwiftClientPlugin, '_create') + @mock.patch.object(resource.Resource, 'physical_resource_name') + def test_getatt_token(self, mock_name, mock_swift): + st = create_stack(swiftsignalhandle_template) + handle = st['test_wait_condition_handle'] + + mock_swift_object = mock.Mock() + mock_swift.return_value = mock_swift_object + mock_swift_object.url = "http://fake-host.com:8080/v1/AUTH_1234" + mock_swift_object.head_account.return_value = { + 'x-account-meta-temp-url-key': '123456' + } + obj_name = "%s-%s-abcdefghijkl" % (st.name, handle.name) + mock_name.return_value = obj_name + mock_swift_object.get_container.return_value = cont_index(obj_name, 1) + + mock_swift_object.get_object.side_effect = ( + # st create + (obj_header, ''), + (obj_header, ''), + ) + + st.create() + self.assertEqual(('CREATE', 'COMPLETE'), st.state) + self.assertEqual(handle.FnGetAtt('token'), '') + + @mock.patch.object(swift.SwiftClientPlugin, '_create') + @mock.patch.object(resource.Resource, 'physical_resource_name') + def test_getatt_endpoint(self, mock_name, mock_swift): + st = create_stack(swiftsignalhandle_template) + handle = st['test_wait_condition_handle'] + + mock_swift_object = mock.Mock() + mock_swift.return_value = mock_swift_object + mock_swift_object.url = "http://fake-host.com:8080/v1/AUTH_1234" + mock_swift_object.head_account.return_value = { + 'x-account-meta-temp-url-key': '123456' + } + obj_name = "%s-%s-abcdefghijkl" % (st.name, handle.name) + mock_name.return_value = obj_name + mock_swift_object.get_container.return_value = cont_index(obj_name, 1) + + mock_swift_object.get_object.side_effect = ( + # st create + (obj_header, ''), + (obj_header, ''), + ) + + st.create() + self.assertEqual(('CREATE', 'COMPLETE'), st.state) + expected = ('http://fake-host.com:8080/v1/AUTH_test_tenant/%s/' + 'test_st-test_wait_condition_handle-abcdefghijkl\?temp_' + 'url_sig=[0-9a-f]{40}&temp_url_expires=[0-9]{10}') % st.id + self.assertThat(handle.FnGetAtt('endpoint'), + MatchesRegex(expected)) + + @mock.patch.object(swift.SwiftClientPlugin, '_create') + @mock.patch.object(resource.Resource, 'physical_resource_name') + def test_getatt_curl_cli(self, mock_name, mock_swift): + st = create_stack(swiftsignalhandle_template) + handle = st['test_wait_condition_handle'] + + mock_swift_object = mock.Mock() + mock_swift.return_value = mock_swift_object + mock_swift_object.url = "http://fake-host.com:8080/v1/AUTH_1234" + mock_swift_object.head_account.return_value = { + 'x-account-meta-temp-url-key': '123456' + } + obj_name = "%s-%s-abcdefghijkl" % (st.name, handle.name) + mock_name.return_value = obj_name + mock_swift_object.get_container.return_value = cont_index(obj_name, 1) + + mock_swift_object.get_object.side_effect = ( + # st create + (obj_header, ''), + (obj_header, ''), + ) + + st.create() + self.assertEqual(('CREATE', 'COMPLETE'), st.state) + expected = ('curl -i -X PUT \'http://fake-host.com:8080/v1/' + 'AUTH_test_tenant/%s/test_st-test_wait_condition_' + 'handle-abcdefghijkl\?temp_url_sig=[0-9a-f]{40}&' + 'temp_url_expires=[0-9]{10}\'') % st.id + self.assertThat(handle.FnGetAtt('curl_cli'), MatchesRegex(expected)) + + @mock.patch.object(swift.SwiftClientPlugin, '_create') + @mock.patch.object(resource.Resource, 'physical_resource_name') + def test_invalid_json_data(self, mock_name, mock_swift): + st = create_stack(swiftsignal_template) + handle = st['test_wait_condition_handle'] + + mock_swift_object = mock.Mock() + mock_swift.return_value = mock_swift_object + mock_swift_object.url = "http://fake-host.com:8080/v1/AUTH_1234" + mock_swift_object.head_account.return_value = { + 'x-account-meta-temp-url-key': '123456' + } + obj_name = "%s-%s-abcdefghijkl" % (st.name, handle.name) + mock_name.return_value = obj_name + mock_swift_object.get_container.return_value = cont_index(obj_name, 1) + + mock_swift_object.get_object.side_effect = ( + # st create + (obj_header, '{"status": "SUCCESS"'), + (obj_header, '{"status": "FAI'), + ) + + st.create() + self.assertEqual(('CREATE', 'FAILED'), st.state) + wc = st['test_wait_condition'] + self.assertEqual('Error: Failed to parse JSON data: {"status": ' + '"SUCCESS"', wc.status_reason) + + @mock.patch.object(swift.SwiftClientPlugin, '_create') + @mock.patch.object(resource.Resource, 'physical_resource_name') + def test_unknown_status(self, mock_name, mock_swift): + st = create_stack(swiftsignal_template) + handle = st['test_wait_condition_handle'] + + mock_swift_object = mock.Mock() + mock_swift.return_value = mock_swift_object + mock_swift_object.url = "http://fake-host.com:8080/v1/AUTH_1234" + mock_swift_object.head_account.return_value = { + 'x-account-meta-temp-url-key': '123456' + } + obj_name = "%s-%s-abcdefghijkl" % (st.name, handle.name) + mock_name.return_value = obj_name + mock_swift_object.get_container.return_value = cont_index(obj_name, 1) + + mock_swift_object.get_object.return_value = ( + obj_header, '{"status": "BOO"}') + + st.create() + self.assertEqual(('CREATE', 'FAILED'), st.state) + wc = st['test_wait_condition'] + self.assertEqual('Error: Unknown status: BOO', wc.status_reason) + + @mock.patch.object(swift.SwiftClientPlugin, '_create') + @mock.patch.object(resource.Resource, 'physical_resource_name') + def test_swift_objects_deleted(self, mock_name, mock_swift): + st = create_stack(swiftsignal_template) + handle = st['test_wait_condition_handle'] + + mock_swift_object = mock.Mock() + mock_swift.return_value = mock_swift_object + mock_swift_object.url = "http://fake-host.com:8080/v1/AUTH_1234" + mock_swift_object.head_account.return_value = { + 'x-account-meta-temp-url-key': '123456' + } + obj_name = "%s-%s-abcdefghijkl" % (st.name, handle.name) + mock_name.return_value = obj_name + mock_swift_object.get_container.side_effect = ( + cont_index(obj_name, 2), # Objects are there during create + (container_header, []), # The user deleted the objects + ) + mock_swift_object.get_object.side_effect = ( + (obj_header, json.dumps({'id': 1})), # Objects there during create + (obj_header, json.dumps({'id': 2})), + (obj_header, json.dumps({'id': 3})), + ) + + st.create() + self.assertEqual(('CREATE', 'COMPLETE'), st.state) + wc = st['test_wait_condition'] + self.assertEqual("null", wc.FnGetAtt('data')) + + @mock.patch.object(swift.SwiftClientPlugin, '_create') + @mock.patch.object(resource.Resource, 'physical_resource_name') + def test_swift_container_deleted(self, mock_name, mock_swift): + st = create_stack(swiftsignal_template) + handle = st['test_wait_condition_handle'] + + mock_swift_object = mock.Mock() + mock_swift.return_value = mock_swift_object + mock_swift_object.url = "http://fake-host.com:8080/v1/AUTH_1234" + mock_swift_object.head_account.return_value = { + 'x-account-meta-temp-url-key': '123456' + } + obj_name = "%s-%s-abcdefghijkl" % (st.name, handle.name) + mock_name.return_value = obj_name + mock_swift_object.get_container.side_effect = [ + cont_index(obj_name, 2), # Objects are there during create + swiftclient_client.ClientException("Container GET failed", + http_status=404) # User deleted + ] + mock_swift_object.get_object.side_effect = ( + (obj_header, json.dumps({'id': 1})), # Objects there during create + (obj_header, json.dumps({'id': 2})), + (obj_header, json.dumps({'id': 3})), + ) + + st.create() + self.assertEqual(('CREATE', 'COMPLETE'), st.state) + wc = st['test_wait_condition'] + self.assertEqual("null", wc.FnGetAtt('data'))