From ca8effbbae50392672d4474622ab3af770358828 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Thu, 2 Jul 2015 18:11:50 -0700 Subject: [PATCH] Move signals to SignalResponder class This change relocates the code that creates and deletes heat, swift and zaqar signals out of SoftwareDeployment and into SignalResponder, as a first step in making these signals available to all resources. As part of this change the structure of the Swift URLs was changed to match that of the SwiftSignal resource. Change-Id: Id5701b3696b0ab41433a3f158fabce1f36aabe16 Implements: blueprint uniform-resource-signals --- .../openstack/heat/software_deployment.py | 101 +-------- heat/engine/resources/signal_responder.py | 149 ++++++++++++- heat/engine/resources/stack_user.py | 20 +- heat/tests/fakes.py | 1 + heat/tests/generic_resource.py | 25 ++- heat/tests/test_signal.py | 202 +++++++++++++++--- heat/tests/test_software_deployment.py | 59 ++--- 7 files changed, 394 insertions(+), 163 deletions(-) diff --git a/heat/engine/resources/openstack/heat/software_deployment.py b/heat/engine/resources/openstack/heat/software_deployment.py index 0b87aea57a..b782e87580 100644 --- a/heat/engine/resources/openstack/heat/software_deployment.py +++ b/heat/engine/resources/openstack/heat/software_deployment.py @@ -13,7 +13,6 @@ import copy import six -import uuid from oslo_config import cfg from oslo_log import log as logging @@ -315,63 +314,6 @@ class SoftwareDeployment(signal_responder.SignalResponder): def _build_derived_options(self, action, source): return source.get(sc.SoftwareConfig.OPTIONS) - def _get_temp_url(self): - put_url = self.data().get('signal_temp_url') - if put_url: - return put_url - - container = self.physical_resource_name() - object_name = str(uuid.uuid4()) - - self.client('swift').put_container(container) - - put_url = self.client_plugin('swift').get_temp_url( - container, object_name) - self.data_set('signal_temp_url', put_url) - self.data_set('signal_object_name', object_name) - - self.client('swift').put_object( - container, object_name, '') - return put_url - - def _get_queue_id(self): - queue_id = self.data().get('signal_queue_id') - if queue_id: - return queue_id - - queue_id = self.physical_resource_name() - zaqar = self.client('zaqar') - zaqar.queue(queue_id).ensure_exists() - self.data_set('signal_queue_id', queue_id) - return queue_id - - def _delete_temp_url(self): - object_name = self.data().get('signal_object_name') - if not object_name: - return - try: - container = self.physical_resource_name() - swift = self.client('swift') - swift.delete_object(container, object_name) - headers = swift.head_container(container) - if int(headers['x-container-object-count']) == 0: - swift.delete_container(container) - except Exception as ex: - self.client_plugin('swift').ignore_not_found(ex) - self.data_delete('signal_object_name') - self.data_delete('signal_temp_url') - - def _delete_queue(self): - queue_id = self.data().get('signal_queue_id') - if not queue_id: - return - zaqar = self.client('zaqar') - try: - zaqar.queue(queue_id).delete() - except Exception as ex: - self.client_plugin('zaqar').ignore_not_found(ex) - self.data_delete('signal_queue_id') - def _build_derived_inputs(self, action, source): scl = sc.SoftwareConfig inputs = copy.deepcopy(source.get(scl.INPUTS)) or [] @@ -440,7 +382,7 @@ class SoftwareDeployment(signal_responder.SignalResponder): scl.DESCRIPTION: _('ID of signal to use for signaling ' 'output values'), scl.TYPE: 'String', - 'value': self._get_temp_url() + 'value': self._get_swift_signal_url() }) inputs.append({ scl.NAME: self.DEPLOY_SIGNAL_VERB, @@ -450,31 +392,32 @@ class SoftwareDeployment(signal_responder.SignalResponder): 'value': 'PUT' }) elif self._signal_transport_heat() or self._signal_transport_zaqar(): + creds = self._get_heat_signal_credentials() inputs.extend([{ scl.NAME: self.DEPLOY_AUTH_URL, scl.DESCRIPTION: _('URL for API authentication'), scl.TYPE: 'String', - 'value': self.keystone().v3_endpoint + 'value': creds['auth_url'] }, { scl.NAME: self.DEPLOY_USERNAME, scl.DESCRIPTION: _('Username for API authentication'), scl.TYPE: 'String', - 'value': self.physical_resource_name(), + 'value': creds['username'] }, { scl.NAME: self.DEPLOY_USER_ID, scl.DESCRIPTION: _('User ID for API authentication'), scl.TYPE: 'String', - 'value': self._get_user_id(), + 'value': creds['user_id'] }, { scl.NAME: self.DEPLOY_PASSWORD, scl.DESCRIPTION: _('Password for API authentication'), scl.TYPE: 'String', - 'value': self.password + 'value': creds['password'] }, { scl.NAME: self.DEPLOY_PROJECT_ID, scl.DESCRIPTION: _('ID of project for API authentication'), scl.TYPE: 'String', - 'value': self.stack.stack_user_project_id + 'value': creds['project_id'] }]) if self._signal_transport_zaqar(): inputs.append({ @@ -482,31 +425,14 @@ class SoftwareDeployment(signal_responder.SignalResponder): scl.DESCRIPTION: _('ID of queue to use for signaling ' 'output values'), scl.TYPE: 'String', - 'value': self._get_queue_id() + 'value': self._get_zaqar_signal_queue_id() }) return inputs def handle_create(self): - if self._signal_transport_cfn(): - self._create_user() - self._create_keypair() - if self._signal_transport_heat() or self._signal_transport_zaqar(): - self.password = uuid.uuid4().hex - self._create_user() return self._handle_action(self.CREATE) - @property - def password(self): - return self.data().get('password') - - @password.setter - def password(self, password): - if password is None: - self.data_delete('password') - else: - self.data_set('password', password, True) - def check_create_complete(self, sd): if not sd: return True @@ -536,15 +462,8 @@ class SoftwareDeployment(signal_responder.SignalResponder): return True def _delete_resource(self): - if self._signal_transport_cfn(): - self._delete_ec2_signed_url() - self._delete_user() - elif self._signal_transport_heat(): - self._delete_user() - elif self._signal_transport_temp_url(): - self._delete_temp_url() - elif self._signal_transport_zaqar(): - self._delete_queue() + self._delete_signals() + self._delete_user() derived_config_id = None if self.resource_id is not None: diff --git a/heat/engine/resources/signal_responder.py b/heat/engine/resources/signal_responder.py index 5693bf5968..2579247fd0 100644 --- a/heat/engine/resources/signal_responder.py +++ b/heat/engine/resources/signal_responder.py @@ -11,6 +11,8 @@ # License for the specific language governing permissions and limitations # under the License. +import uuid + from keystoneclient.contrib.ec2 import utils as ec2_utils from oslo_config import cfg from oslo_log import log as logging @@ -36,17 +38,42 @@ class SignalResponder(stack_user.StackUser): # API operations as a consequence of handling a signal requires_deferred_auth = True - def handle_create(self): - super(SignalResponder, self).handle_create() - self._create_keypair() - def handle_delete(self): + self._delete_signals() super(SignalResponder, self).handle_delete() + + def _delete_signals(self): self._delete_ec2_signed_url() self._delete_heat_signal_url() + self._delete_swift_signal_url() + self._delete_zaqar_signal_queue() - def _delete_ec2_signed_url(self): - self.data_delete('ec2_signed_url') + @property + def password(self): + return self.data().get('password') + + @password.setter + def password(self, password): + if password is None: + self.data_delete('password') + else: + self.data_set('password', password, True) + + def _get_heat_signal_credentials(self): + """Return OpenStack credentials that can be used to send a signal. + + These credentials are for the user associated with this resource in + the heat stack user domain. + """ + if self._get_user_id() is None: + if self.password is None: + self.password = uuid.uuid4().hex + self._create_user() + return {'auth_url': self.keystone().v3_endpoint, + 'username': self.physical_resource_name(), + 'user_id': self._get_user_id(), + 'password': self.password, + 'project_id': self.stack.stack_user_project_id} def _get_ec2_signed_url(self, signal_type=SIGNAL): """Create properly formatted and pre-signed URL. @@ -65,9 +92,19 @@ class SignalResponder(stack_user.StackUser): secret_key = self.data().get('secret_key') if not access_key or not secret_key: - LOG.warn(_LW('Cannot generate signed url, ' - 'no stored access/secret key')) - return + if self.id is None or self.action == self.DELETE: + # it is either too early or too late to do this + return + if self._get_user_id() is None: + self._create_user() + self._create_keypair() + access_key = self.data().get('access_key') + secret_key = self.data().get('secret_key') + + if not access_key or not secret_key: + LOG.warn(_LW('Cannot generate signed url, ' + 'unable to create keypair')) + return config_url = cfg.CONF.heat_waitcondition_server_url if config_url: @@ -106,14 +143,23 @@ class SignalResponder(stack_user.StackUser): self.data_set('ec2_signed_url', url) return url - def _delete_heat_signal_url(self): - self.data_delete('heat_signal_url') + def _delete_ec2_signed_url(self): + self.data_delete('ec2_signed_url') + self._delete_keypair() def _get_heat_signal_url(self): + """Return a heat-api signal URL for this resource. + + This URL is not pre-signed, valid user credentials are required. + """ stored = self.data().get('heat_signal_url') if stored is not None: return stored + if self.id is None or self.action == self.DELETE: + # it is either too early or too late to do this + return + url = self.client_plugin('heat').get_heat_url() host_url = urlparse.urlparse(url) path = self.identifier().url_path() @@ -123,3 +169,84 @@ class SignalResponder(stack_user.StackUser): self.data_set('heat_signal_url', url) return url + + def _delete_heat_signal_url(self): + self.data_delete('heat_signal_url') + + def _get_swift_signal_url(self): + """Create properly formatted and pre-signed Swift signal URL. + + This uses a Swift pre-signed temp_url. + """ + put_url = self.data().get('swift_signal_url') + if put_url: + return put_url + + if self.id is None or self.action == self.DELETE: + # it is either too early or too late to do this + return + + container = self.stack.id + object_name = self.physical_resource_name() + + self.client('swift').put_container(container) + + put_url = self.client_plugin('swift').get_temp_url( + 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): + object_name = self.data().get('swift_signal_object_name') + if not object_name: + return + try: + container = self.physical_resource_name() + swift = self.client('swift') + swift.delete_object(container, object_name) + headers = swift.head_container(container) + if int(headers['x-container-object-count']) == 0: + swift.delete_container(container) + except Exception as ex: + self.client_plugin('swift').ignore_not_found(ex) + self.data_delete('swift_signal_object_name') + self.data_delete('swift_signal_url') + + def _get_zaqar_signal_queue_id(self): + """Return a zaqar queue_id for signaling this resource. + + This uses the created user for the credentials. + """ + queue_id = self.data().get('zaqar_signal_queue_id') + if queue_id: + return queue_id + + if self.id is None or self.action == self.DELETE: + # it is either too early or too late to do this + return + + if self._get_user_id() is None: + if self.password is None: + self.password = uuid.uuid4().hex + self._create_user() + + queue_id = self.physical_resource_name() + zaqar = self.client('zaqar') + zaqar.queue(queue_id).ensure_exists() + self.data_set('zaqar_signal_queue_id', queue_id) + return queue_id + + def _delete_zaqar_signal_queue(self): + queue_id = self.data().get('zaqar_signal_queue_id') + if not queue_id: + return + zaqar = self.client('zaqar') + try: + zaqar.queue(queue_id).delete() + except Exception as ex: + self.client_plugin('zaqar').ignore_not_found(ex) + self.data_delete('zaqar_signal_queue_id') diff --git a/heat/engine/resources/stack_user.py b/heat/engine/resources/stack_user.py index b4c034f7a2..d6aa26e6c1 100644 --- a/heat/engine/resources/stack_user.py +++ b/heat/engine/resources/stack_user.py @@ -25,7 +25,7 @@ LOG = logging.getLogger(__name__) class StackUser(resource.Resource): # Subclasses create a user, and optionally keypair associated with a - # resource in a stack. Users are created in the heat stack user domain + # resource in a stack. Users are created in the heat stack user domain # (in a project specific to the stack) def __init__(self, name, json_snippet, stack): super(StackUser, self).__init__(name, json_snippet, stack) @@ -34,6 +34,10 @@ class StackUser(resource.Resource): self._create_user() def _create_user(self): + if self.data().get('user_id'): + # a user has been created already + return + # Check for stack user project, create if not yet set if not self.stack.stack_user_project_id: project_id = self.keystone().create_stack_domain_project( @@ -85,6 +89,10 @@ class StackUser(resource.Resource): user_id = self._get_user_id() if user_id is None: return + + # the user is going away, so we want the keypair gone as well + self._delete_keypair() + try: self.keystone().delete_stack_domain_user( user_id=user_id, project_id=self.stack.stack_user_project_id) @@ -100,8 +108,7 @@ class StackUser(resource.Resource): self.keystone().delete_stack_user(user_id) except kc_exception.NotFound: pass - for data_key in ('credential_id', 'access_key', 'secret_key'): - self.data_delete(data_key) + self.data_delete('user_id') def handle_suspend(self): user_id = self._get_user_id() @@ -125,6 +132,9 @@ class StackUser(resource.Resource): # Subclasses may optionally call this in handle_create to create # an ec2 keypair associated with the user, the resulting keys are # stored in resource_data + if self.data().get('credential_id'): + return # a keypair was created already + user_id = self._get_user_id() kp = self.keystone().create_stack_domain_user_keypair( user_id=user_id, project_id=self.stack.stack_user_project_id) @@ -146,10 +156,12 @@ class StackUser(resource.Resource): def _delete_keypair(self): # Subclasses may optionally call this to delete a keypair created # via _create_keypair - user_id = self._get_user_id() credential_id = self.data().get('credential_id') if not credential_id: return + user_id = self._get_user_id() + if user_id is None: + return try: self.keystone().delete_stack_domain_user_keypair( diff --git a/heat/tests/fakes.py b/heat/tests/fakes.py index 7d86ffd1e4..02d5d9eda7 100644 --- a/heat/tests/fakes.py +++ b/heat/tests/fakes.py @@ -109,6 +109,7 @@ class FakeKeystoneClient(object): self.credential_id = credential_id self.token = auth_token self.context = context + self.v3_endpoint = 'http://localhost:5000/v3' class FakeCred(object): id = self.credential_id diff --git a/heat/tests/generic_resource.py b/heat/tests/generic_resource.py index 5466b109c2..5ec9437d25 100644 --- a/heat/tests/generic_resource.py +++ b/heat/tests/generic_resource.py @@ -132,8 +132,11 @@ class ResourceWithRequiredProps(GenericResource): class SignalResource(signal_responder.SignalResponder): - properties_schema = {} - attributes_schema = {'AlarmUrl': attributes.Schema('Get a signed webhook')} + properties_schema = { + 'signal_transport': properties.Schema(properties.Schema.STRING, + default='CFN_SIGNAL')} + attributes_schema = {'AlarmUrl': attributes.Schema('Get a signed webhook'), + 'signal': attributes.Schema('Get a signal')} def handle_create(self): super(SignalResource, self).handle_create() @@ -144,8 +147,22 @@ class SignalResource(signal_responder.SignalResponder): {'type': self.type(), 'details': details}) def _resolve_attribute(self, name): - if name == 'AlarmUrl' and self.resource_id is not None: - return six.text_type(self._get_ec2_signed_url()) + 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'] + elif name == 'signal': + return d class StackUserResource(stack_user.StackUser): diff --git a/heat/tests/test_signal.py b/heat/tests/test_signal.py index fed3f6ff98..4082899cd8 100644 --- a/heat/tests/test_signal.py +++ b/heat/tests/test_signal.py @@ -12,14 +12,15 @@ # under the License. import datetime -import six +import uuid from keystoneclient import exceptions as kc_exceptions +import mox +import six from heat.common import exception from heat.common import template_format from heat.engine import resource -from heat.engine.resources import stack_user from heat.engine import scheduler from heat.engine import stack as parser from heat.engine import template @@ -30,13 +31,53 @@ from heat.tests import generic_resource from heat.tests import utils -test_template_signal = ''' +test_cfn_template_signal = ''' { "AWSTemplateFormatVersion" : "2010-09-09", "Description" : "Just a test.", "Parameters" : {}, "Resources" : { - "signal_handler" : {"Type" : "SignalResourceType"}, + "signal_handler" : {"Type" : "SignalResourceType", + "Properties": {"signal_transport": "CFN_SIGNAL"}}, + "resource_X" : {"Type" : "GenericResourceType"} + } +} +''' + +test_heat_template_signal = ''' +{ + "AWSTemplateFormatVersion" : "2010-09-09", + "Description" : "Just a test.", + "Parameters" : {}, + "Resources" : { + "signal_handler" : {"Type" : "SignalResourceType", + "Properties": {"signal_transport": "HEAT_SIGNAL"}}, + "resource_X" : {"Type" : "GenericResourceType"} + } +} +''' + +test_swift_template_signal = ''' +{ + "AWSTemplateFormatVersion" : "2010-09-09", + "Description" : "Just a test.", + "Parameters" : {}, + "Resources" : { + "signal_handler" : {"Type" : "SignalResourceType", + "Properties": {"signal_transport": "TEMP_URL_SIGNAL"}}, + "resource_X" : {"Type" : "GenericResourceType"} + } +} +''' + +test_zaqar_template_signal = ''' +{ + "AWSTemplateFormatVersion" : "2010-09-09", + "Description" : "Just a test.", + "Parameters" : {}, + "Resources" : { + "signal_handler" : {"Type" : "SignalResourceType", + "Properties": {"signal_transport": "ZAQAR_SIGNAL"}}, "resource_X" : {"Type" : "GenericResourceType"} } } @@ -52,11 +93,12 @@ class SignalTest(common.HeatTestCase): def tearDown(self): super(SignalTest, self).tearDown() - def create_stack(self, stack_name='test_stack', stub=True): - templ = template.Template(template_format.parse(test_template_signal)) + def create_stack(self, templ=test_cfn_template_signal, + stack_name='test_stack', stub=True): + tpl = template.Template(template_format.parse(templ)) ctx = utils.dummy_context() ctx.tenant_id = 'test_tenant' - stack = parser.Stack(ctx, stack_name, templ, + stack = parser.Stack(ctx, stack_name, tpl, disable_rollback=True) # Stub out the stack ID so we have a known value @@ -67,23 +109,6 @@ class SignalTest(common.HeatTestCase): return stack - def test_handle_create_fail_keypair_raise(self): - self.stack = self.create_stack(stack_name='create_fail_keypair') - - self.m.StubOutWithMock(stack_user.StackUser, '_create_keypair') - stack_user.StackUser._create_keypair().AndRaise(Exception('Failed')) - self.m.ReplayAll() - - self.stack.create() - - rsrc = self.stack['signal_handler'] - rs_data = resource_data_object.ResourceData.get_all(rsrc) - self.assertEqual((rsrc.CREATE, rsrc.FAILED), rsrc.state) - self.assertIn('Failed', rsrc.status_reason) - self.assertEqual('1234', rs_data.get('user_id')) - self.assertIsNone(rsrc.resource_id) - self.m.VerifyAll() - def test_resource_data(self): self.stub_keystoneclient( access='anaccesskey', @@ -97,6 +122,7 @@ class SignalTest(common.HeatTestCase): rsrc = self.stack['signal_handler'] self.assertEqual((rsrc.CREATE, rsrc.COMPLETE), rsrc.state) + rsrc._create_keypair() # Ensure the resource data has been stored correctly rs_data = resource_data_object.ResourceData.get_all(rsrc) @@ -174,8 +200,131 @@ class SignalTest(common.HeatTestCase): rsrc = self.stack['signal_handler'] self.assertEqual((rsrc.CREATE, rsrc.COMPLETE), rsrc.state) - first_url = rsrc.FnGetAtt('AlarmUrl') - second_url = rsrc.FnGetAtt('AlarmUrl') + first_url = rsrc.FnGetAtt('signal') + second_url = rsrc.FnGetAtt('signal') + self.assertEqual(first_url, second_url) + self.m.VerifyAll() + + def test_FnGetAtt_Heat_Signal(self): + self.stack = self.create_stack(test_heat_template_signal) + self.m.StubOutWithMock(self.stack.clients.client_plugin('heat'), + 'get_heat_url') + + self.stack.clients.client_plugin('heat').get_heat_url().AndReturn( + 'http://server.test:8004/v1') + + self.m.ReplayAll() + self.stack.create() + + rsrc = self.stack['signal_handler'] + self.assertEqual((rsrc.CREATE, rsrc.COMPLETE), rsrc.state) + + signal = rsrc.FnGetAtt('signal') + self.assertEqual('http://localhost:5000/v3', signal['auth_url']) + self.assertEqual('aprojectid', signal['project_id']) + self.assertEqual('1234', signal['user_id']) + self.assertIn('username', signal) + self.assertIn('password', signal) + self.m.VerifyAll() + + def test_FnGetAtt_Heat_Signal_is_cached(self): + self.stack = self.create_stack(test_heat_template_signal) + + self.m.ReplayAll() + self.stack.create() + + rsrc = self.stack['signal_handler'] + self.assertEqual((rsrc.CREATE, rsrc.COMPLETE), rsrc.state) + + first_url = rsrc.FnGetAtt('signal') + second_url = rsrc.FnGetAtt('signal') + self.assertEqual(first_url, second_url) + self.m.VerifyAll() + + def test_FnGetAtt_Zaqar_Signal(self): + self.stack = self.create_stack(test_zaqar_template_signal) + + self.m.ReplayAll() + self.stack.create() + + rsrc = self.stack['signal_handler'] + self.assertEqual((rsrc.CREATE, rsrc.COMPLETE), rsrc.state) + + signal = rsrc.FnGetAtt('signal') + self.assertEqual('http://localhost:5000/v3', signal['auth_url']) + self.assertEqual('aprojectid', signal['project_id']) + self.assertEqual('1234', signal['user_id']) + self.assertIn('username', signal) + self.assertIn('password', signal) + self.assertIn('queue_id', signal) + self.m.VerifyAll() + + def test_FnGetAtt_Zaqar_Signal_is_cached(self): + self.stack = self.create_stack(test_zaqar_template_signal) + + self.m.ReplayAll() + self.stack.create() + + rsrc = self.stack['signal_handler'] + self.assertEqual((rsrc.CREATE, rsrc.COMPLETE), rsrc.state) + + first_url = rsrc.FnGetAtt('signal') + second_url = rsrc.FnGetAtt('signal') + self.assertEqual(first_url, second_url) + self.m.VerifyAll() + + def test_FnGetAtt_Swift_Signal(self): + self.stack = self.create_stack(test_swift_template_signal) + self.m.StubOutWithMock(self.stack.clients.client('swift'), + 'put_container') + self.m.StubOutWithMock(self.stack.clients.client('swift'), + 'put_object') + self.m.StubOutWithMock(self.stack.clients.client_plugin('swift'), + 'get_temp_url') + + 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://192.0.2.1/v1/AUTH_aprojectid/foo/bar') + self.stack.clients.client('swift').put_object( + mox.IgnoreArg(), mox.IgnoreArg(), mox.IgnoreArg()).AndReturn(None) + + self.m.ReplayAll() + self.stack.create() + + rsrc = self.stack['signal_handler'] + self.assertEqual((rsrc.CREATE, rsrc.COMPLETE), rsrc.state) + + self.assertEqual('http://192.0.2.1/v1/AUTH_aprojectid/foo/bar', + rsrc.FnGetAtt('AlarmUrl')) + self.m.VerifyAll() + + def test_FnGetAtt_Swift_Signal_is_cached(self): + self.stack = self.create_stack(test_swift_template_signal) + self.m.StubOutWithMock(self.stack.clients.client('swift'), + 'put_container') + self.m.StubOutWithMock(self.stack.clients.client('swift'), + 'put_object') + self.m.StubOutWithMock(self.stack.clients.client_plugin('swift'), + 'get_temp_url') + + 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://192.0.2.1/v1/AUTH_aprojectid/foo/' + uuid.uuid4().hex) + self.stack.clients.client('swift').put_object( + mox.IgnoreArg(), mox.IgnoreArg(), mox.IgnoreArg()).AndReturn(None) + + self.m.ReplayAll() + self.stack.create() + + rsrc = self.stack['signal_handler'] + self.assertEqual((rsrc.CREATE, rsrc.COMPLETE), rsrc.state) + + first_url = rsrc.FnGetAtt('signal') + second_url = rsrc.FnGetAtt('signal') self.assertEqual(first_url, second_url) self.m.VerifyAll() @@ -190,6 +339,7 @@ class SignalTest(common.HeatTestCase): self.stack.create() rsrc = self.stack['signal_handler'] + rsrc.resource_id_set('signal') self.assertEqual((rsrc.CREATE, rsrc.COMPLETE), rsrc.state) self.assertIn('http://server.test:8000/v1/signal', diff --git a/heat/tests/test_software_deployment.py b/heat/tests/test_software_deployment.py index 85167e2c80..413b137ec9 100644 --- a/heat/tests/test_software_deployment.py +++ b/heat/tests/test_software_deployment.py @@ -979,22 +979,22 @@ class SoftwareDeploymentTest(common.HeatTestCase): self.deployment.id = 23 self.deployment.uuid = str(uuid.uuid4()) self.deployment.action = self.deployment.CREATE - container = self.deployment.physical_resource_name() + object_name = self.deployment.physical_resource_name() - temp_url = self.deployment._get_temp_url() + temp_url = self.deployment._get_swift_signal_url() temp_url_pattern = re.compile( '^http://192.0.2.1/v1/AUTH_test_tenant_id/' - '(software_deployment_test_stack-deployment_mysql-.*)/(.*)' + '(.*)/(software_deployment_test_stack-deployment_mysql-.*)' '\\?temp_url_sig=.*&temp_url_expires=\\d*$') self.assertRegex(temp_url, temp_url_pattern) m = temp_url_pattern.search(temp_url) - object_name = m.group(2) - self.assertEqual(container, m.group(1)) - self.assertEqual(dep_data['signal_object_name'], object_name) + container = m.group(1) + self.assertEqual(object_name, m.group(2)) + self.assertEqual(dep_data['swift_signal_object_name'], object_name) - self.assertEqual(dep_data['signal_temp_url'], temp_url) + self.assertEqual(dep_data['swift_signal_url'], temp_url) - self.assertEqual(temp_url, self.deployment._get_temp_url()) + self.assertEqual(temp_url, self.deployment._get_swift_signal_url()) sc.put_container.assert_called_once_with(container) sc.put_object.assert_called_once_with(container, object_name, '') @@ -1002,7 +1002,7 @@ class SoftwareDeploymentTest(common.HeatTestCase): def test_delete_temp_url(self): object_name = str(uuid.uuid4()) dep_data = { - 'signal_object_name': object_name + 'swift_signal_object_name': object_name } self._create_stack(self.template_temp_url_signal) @@ -1021,31 +1021,34 @@ class SoftwareDeploymentTest(common.HeatTestCase): self.deployment.id = 23 self.deployment.uuid = str(uuid.uuid4()) container = self.deployment.physical_resource_name() - self.deployment._delete_temp_url() + self.deployment._delete_swift_signal_url() sc.delete_object.assert_called_once_with(container, object_name) self.assertEqual( - [mock.call('signal_object_name'), mock.call('signal_temp_url')], + [mock.call('swift_signal_object_name'), + mock.call('swift_signal_url')], self.deployment.data_delete.mock_calls) swift_exc = swift.SwiftClientPlugin.exceptions_module sc.delete_object.side_effect = swift_exc.ClientException( 'Not found', http_status=404) - self.deployment._delete_temp_url() + self.deployment._delete_swift_signal_url() self.assertEqual( - [mock.call('signal_object_name'), mock.call('signal_temp_url'), - mock.call('signal_object_name'), mock.call('signal_temp_url')], + [mock.call('swift_signal_object_name'), + mock.call('swift_signal_url'), + mock.call('swift_signal_object_name'), + mock.call('swift_signal_url')], self.deployment.data_delete.mock_calls) - del(dep_data['signal_object_name']) + del(dep_data['swift_signal_object_name']) self.deployment.physical_resource_name = mock.Mock() - self.deployment._delete_temp_url() + self.deployment._delete_swift_signal_url() self.assertFalse(self.deployment.physical_resource_name.called) def test_handle_action_temp_url(self): self._create_stack(self.template_temp_url_signal) dep_data = { - 'signal_temp_url': ( + 'swift_signal_url': ( 'http://192.0.2.1/v1/AUTH_a/b/c' '?temp_url_sig=ctemp_url_expires=1234') } @@ -1079,17 +1082,18 @@ class SoftwareDeploymentTest(common.HeatTestCase): self.deployment.uuid = str(uuid.uuid4()) self.deployment.action = self.deployment.CREATE - queue_id = self.deployment._get_queue_id() + queue_id = self.deployment._get_zaqar_signal_queue_id() self.assertEqual(2, len(zc.queue.mock_calls)) self.assertEqual(queue_id, zc.queue.mock_calls[0][1][0]) - self.assertEqual(queue_id, dep_data['signal_queue_id']) + self.assertEqual(queue_id, dep_data['zaqar_signal_queue_id']) - self.assertEqual(queue_id, self.deployment._get_queue_id()) + self.assertEqual(queue_id, + self.deployment._get_zaqar_signal_queue_id()) def test_delete_zaqar_queue(self): queue_id = str(uuid.uuid4()) dep_data = { - 'signal_queue_id': queue_id + 'zaqar_signal_queue_id': queue_id } self._create_stack(self.template_zaqar_signal) @@ -1103,23 +1107,24 @@ class SoftwareDeploymentTest(common.HeatTestCase): self.deployment.id = 23 self.deployment.uuid = str(uuid.uuid4()) - self.deployment._delete_queue() + self.deployment._delete_zaqar_signal_queue() zc.queue.assert_called_once_with(queue_id) zc.queue.delete.assert_called_once() self.assertEqual( - [mock.call('signal_queue_id')], + [mock.call('zaqar_signal_queue_id')], self.deployment.data_delete.mock_calls) zaqar_exc = zaqar.ZaqarClientPlugin.exceptions_module zc.queue.delete.side_effect = zaqar_exc.ResourceNotFound() - self.deployment._delete_queue() + self.deployment._delete_zaqar_signal_queue() self.assertEqual( - [mock.call('signal_queue_id'), mock.call('signal_queue_id')], + [mock.call('zaqar_signal_queue_id'), + mock.call('zaqar_signal_queue_id')], self.deployment.data_delete.mock_calls) - dep_data.pop('signal_queue_id') + dep_data.pop('zaqar_signal_queue_id') self.deployment.physical_resource_name = mock.Mock() - self.deployment._delete_queue() + self.deployment._delete_zaqar_signal_queue() self.assertEqual(2, len(self.deployment.data_delete.mock_calls))