heat/heat/engine/resources/signal_responder.py

253 lines
8.7 KiB
Python

#
# 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 uuid
from keystoneclient.contrib.ec2 import utils as ec2_utils
from oslo_config import cfg
from oslo_log import log as logging
from six.moves.urllib import parse as urlparse
from heat.common.i18n import _LW
from heat.engine.resources import stack_user
LOG = logging.getLogger(__name__)
SIGNAL_TYPES = (
WAITCONDITION, SIGNAL
) = (
'/waitcondition', '/signal'
)
SIGNAL_VERB = {WAITCONDITION: 'PUT',
SIGNAL: 'POST'}
class SignalResponder(stack_user.StackUser):
# Anything which subclasses this may trigger authenticated
# API operations as a consequence of handling a signal
requires_deferred_auth = True
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()
@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.
This uses the created user for the credentials.
See boto/auth.py::QuerySignatureV2AuthHandler
:param signal_type: either WAITCONDITION or SIGNAL.
"""
stored = self.data().get('ec2_signed_url')
if stored is not None:
return stored
access_key = self.data().get('access_key')
secret_key = self.data().get('secret_key')
if not access_key or not secret_key:
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:
signal_url = config_url.replace('/waitcondition', signal_type)
else:
heat_client_plugin = self.stack.clients.client_plugin('heat')
endpoint = heat_client_plugin.get_heat_cfn_url()
signal_url = ''.join([endpoint, signal_type])
host_url = urlparse.urlparse(signal_url)
path = self.identifier().arn_url_path()
# Note the WSGI spec apparently means that the webob request we end up
# processing in the CFN API (ec2token.py) has an unquoted path, so we
# need to calculate the signature with the path component unquoted, but
# ensure the actual URL contains the quoted version...
unquoted_path = urlparse.unquote(host_url.path + path)
request = {'host': host_url.netloc.lower(),
'verb': SIGNAL_VERB[signal_type],
'path': unquoted_path,
'params': {'SignatureMethod': 'HmacSHA256',
'SignatureVersion': '2',
'AWSAccessKeyId': access_key,
'Timestamp':
self.created_time.strftime("%Y-%m-%dT%H:%M:%SZ")
}}
# Sign the request
signer = ec2_utils.Ec2Signer(secret_key)
request['params']['Signature'] = signer.generate(request)
qs = urlparse.urlencode(request['params'])
url = "%s%s?%s" % (signal_url.lower(),
path, qs)
self.data_set('ec2_signed_url', url)
return 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()
url = urlparse.urlunsplit(
(host_url.scheme, host_url.netloc, 'v1/%s/signal' % path, '', ''))
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')