heat engine : Convert WaitConditionHandle to pre-signed URLs
Change WaitConditionHandle so it provides a pre-signed URL which allows authenticated wait condition notification via the CFN API blueprint metsrv-remove Change-Id: I5c1c3a17ade35c810e49b1f27d80bcfea9e89485 Signed-off-by: Steven Hardy <shardy@redhat.com>
This commit is contained in:
parent
d4bb435ed2
commit
17dd71b017
|
@ -25,9 +25,9 @@ heat_metadata_server_url = http://127.0.0.1:8000
|
||||||
# of waitcondition events (ie via cfn-signal)
|
# of waitcondition events (ie via cfn-signal)
|
||||||
# e.g the IP of the bridge device connecting the
|
# e.g the IP of the bridge device connecting the
|
||||||
# instances with the host and the bind_port of
|
# instances with the host and the bind_port of
|
||||||
# the heat-metadata API
|
# the CFN API
|
||||||
# NOTE : change this from 127.0.0.1 !!
|
# NOTE : change this from 127.0.0.1 !!
|
||||||
heat_waitcondition_server_url = http://127.0.0.1:8002
|
heat_waitcondition_server_url = http://127.0.0.1:8000/v1/waitcondition
|
||||||
|
|
||||||
# URL for instances to connect for publishing metric
|
# URL for instances to connect for publishing metric
|
||||||
# data (ie via cfn-push-stats)
|
# data (ie via cfn-push-stats)
|
||||||
|
|
|
@ -0,0 +1,105 @@
|
||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2012 OpenStack LLC
|
||||||
|
# Copyright 2010 United States Government as represented by the
|
||||||
|
# Administrator of the National Aeronautics and Space Administration.
|
||||||
|
# Copyright 2011 - 2012 Justin Santa Barbara
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# 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 base64
|
||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
|
import urllib
|
||||||
|
|
||||||
|
# FIXME : This should be imported from keystoneclient, so this can be removed
|
||||||
|
# when we no longer require an internal fallback implementation
|
||||||
|
# see : https://review.openstack.org/#/c/16964/
|
||||||
|
# https://blueprints.launchpad.net/keystone/+spec/ec2signer-to-keystoneclient
|
||||||
|
|
||||||
|
|
||||||
|
class Ec2Signer(object):
|
||||||
|
"""
|
||||||
|
Utility class which adds allows a request to be signed with an AWS style
|
||||||
|
signature, which can then be used for authentication via the keystone ec2
|
||||||
|
authentication extension
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, secret_key):
|
||||||
|
secret_key = secret_key.encode()
|
||||||
|
self.hmac = hmac.new(secret_key, digestmod=hashlib.sha1)
|
||||||
|
if hashlib.sha256:
|
||||||
|
self.hmac_256 = hmac.new(secret_key, digestmod=hashlib.sha256)
|
||||||
|
|
||||||
|
def generate(self, credentials):
|
||||||
|
"""Generate auth string according to what SignatureVersion is given."""
|
||||||
|
if credentials['params']['SignatureVersion'] == '0':
|
||||||
|
return self._calc_signature_0(credentials['params'])
|
||||||
|
if credentials['params']['SignatureVersion'] == '1':
|
||||||
|
return self._calc_signature_1(credentials['params'])
|
||||||
|
if credentials['params']['SignatureVersion'] == '2':
|
||||||
|
return self._calc_signature_2(credentials['params'],
|
||||||
|
credentials['verb'],
|
||||||
|
credentials['host'],
|
||||||
|
credentials['path'])
|
||||||
|
raise Exception('Unknown Signature Version: %s' %
|
||||||
|
credentials['params']['SignatureVersion'])
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_utf8_value(value):
|
||||||
|
"""Get the UTF8-encoded version of a value."""
|
||||||
|
if not isinstance(value, str) and not isinstance(value, unicode):
|
||||||
|
value = str(value)
|
||||||
|
if isinstance(value, unicode):
|
||||||
|
return value.encode('utf-8')
|
||||||
|
else:
|
||||||
|
return value
|
||||||
|
|
||||||
|
def _calc_signature_0(self, params):
|
||||||
|
"""Generate AWS signature version 0 string."""
|
||||||
|
s = params['Action'] + params['Timestamp']
|
||||||
|
self.hmac.update(s)
|
||||||
|
return base64.b64encode(self.hmac.digest())
|
||||||
|
|
||||||
|
def _calc_signature_1(self, params):
|
||||||
|
"""Generate AWS signature version 1 string."""
|
||||||
|
keys = params.keys()
|
||||||
|
keys.sort(cmp=lambda x, y: cmp(x.lower(), y.lower()))
|
||||||
|
for key in keys:
|
||||||
|
self.hmac.update(key)
|
||||||
|
val = self._get_utf8_value(params[key])
|
||||||
|
self.hmac.update(val)
|
||||||
|
return base64.b64encode(self.hmac.digest())
|
||||||
|
|
||||||
|
def _calc_signature_2(self, params, verb, server_string, path):
|
||||||
|
"""Generate AWS signature version 2 string."""
|
||||||
|
string_to_sign = '%s\n%s\n%s\n' % (verb, server_string, path)
|
||||||
|
if self.hmac_256:
|
||||||
|
current_hmac = self.hmac_256
|
||||||
|
params['SignatureMethod'] = 'HmacSHA256'
|
||||||
|
else:
|
||||||
|
current_hmac = self.hmac
|
||||||
|
params['SignatureMethod'] = 'HmacSHA1'
|
||||||
|
keys = params.keys()
|
||||||
|
keys.sort()
|
||||||
|
pairs = []
|
||||||
|
for key in keys:
|
||||||
|
val = self._get_utf8_value(params[key])
|
||||||
|
val = urllib.quote(val, safe='-_~')
|
||||||
|
pairs.append(urllib.quote(key, safe='') + '=' + val)
|
||||||
|
qs = '&'.join(pairs)
|
||||||
|
string_to_sign += qs
|
||||||
|
current_hmac.update(string_to_sign)
|
||||||
|
b64 = base64.b64encode(current_hmac.digest())
|
||||||
|
return b64
|
|
@ -14,6 +14,9 @@
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import eventlet
|
import eventlet
|
||||||
|
import time
|
||||||
|
import urllib
|
||||||
|
import urlparse
|
||||||
|
|
||||||
from heat.common import exception
|
from heat.common import exception
|
||||||
from heat.engine import resource
|
from heat.engine import resource
|
||||||
|
@ -22,6 +25,16 @@ from heat.openstack.common import log as logging
|
||||||
|
|
||||||
from heat.openstack.common import cfg
|
from heat.openstack.common import cfg
|
||||||
|
|
||||||
|
# FIXME : we should remove the common.ec2signer fallback implementation
|
||||||
|
# when the versions of keystoneclient we support all have the Ec2Signer
|
||||||
|
# utility class
|
||||||
|
# Ref https://review.openstack.org/#/c/16964/
|
||||||
|
# https://blueprints.launchpad.net/keystone/+spec/ec2signer-to-keystoneclient
|
||||||
|
try:
|
||||||
|
from keystoneclient.contrib.ec2.utils import Ec2Signer
|
||||||
|
except ImportError:
|
||||||
|
from heat.common.ec2signer import Ec2Signer
|
||||||
|
|
||||||
logger = logging.getLogger('heat.engine.wait_condition')
|
logger = logging.getLogger('heat.engine.wait_condition')
|
||||||
|
|
||||||
|
|
||||||
|
@ -38,15 +51,63 @@ class WaitConditionHandle(resource.Resource):
|
||||||
def __init__(self, name, json_snippet, stack):
|
def __init__(self, name, json_snippet, stack):
|
||||||
super(WaitConditionHandle, self).__init__(name, json_snippet, stack)
|
super(WaitConditionHandle, self).__init__(name, json_snippet, stack)
|
||||||
|
|
||||||
|
def _sign_url(self, credentials, path):
|
||||||
|
"""
|
||||||
|
Create properly formatted and pre-signed URL using supplied credentials
|
||||||
|
See http://docs.amazonwebservices.com/AWSECommerceService/latest/DG/
|
||||||
|
rest-signature.html
|
||||||
|
Also see boto/auth.py::QuerySignatureV2AuthHandler
|
||||||
|
"""
|
||||||
|
host_url = urlparse.urlparse(cfg.CONF.heat_waitcondition_server_url)
|
||||||
|
request = {'host': host_url.netloc.lower(),
|
||||||
|
'verb': 'PUT',
|
||||||
|
'path': host_url.path + path,
|
||||||
|
'params': {'SignatureMethod': 'HmacSHA256',
|
||||||
|
'SignatureVersion': '2',
|
||||||
|
'AWSAccessKeyId': credentials.access,
|
||||||
|
'Timestamp': time.strftime("%Y-%m-%dT%H:%M:%SZ",
|
||||||
|
time.gmtime())}}
|
||||||
|
# Sign the request
|
||||||
|
signer = Ec2Signer(credentials.secret)
|
||||||
|
request['params']['Signature'] = signer.generate(request)
|
||||||
|
|
||||||
|
qs = urllib.urlencode(request['params'])
|
||||||
|
url = "%s%s?%s" % (cfg.CONF.heat_waitcondition_server_url.lower(),
|
||||||
|
path, qs)
|
||||||
|
return url
|
||||||
|
|
||||||
def handle_create(self):
|
def handle_create(self):
|
||||||
self.resource_id = '%s/stacks/%s/resources/%s' % \
|
# Create a keystone user so we can create a signed URL via FnGetRefId
|
||||||
(cfg.CONF.heat_waitcondition_server_url,
|
user_id = self.keystone().create_stack_user(
|
||||||
self.stack.id,
|
self.physical_resource_name())
|
||||||
self.name)
|
kp = self.keystone().get_ec2_keypair(user_id)
|
||||||
|
if not kp:
|
||||||
|
raise exception.Error("Error creating ec2 keypair for user %s" %
|
||||||
|
user_id)
|
||||||
|
else:
|
||||||
|
self.resource_id_set(user_id)
|
||||||
|
|
||||||
|
def handle_delete(self):
|
||||||
|
if self.resource_id is None:
|
||||||
|
return
|
||||||
|
self.keystone().delete_stack_user(self.resource_id)
|
||||||
|
|
||||||
def handle_update(self):
|
def handle_update(self):
|
||||||
return self.UPDATE_REPLACE
|
return self.UPDATE_REPLACE
|
||||||
|
|
||||||
|
def FnGetRefId(self):
|
||||||
|
'''
|
||||||
|
Override the default resource FnGetRefId so we return the signed URL
|
||||||
|
'''
|
||||||
|
if self.resource_id:
|
||||||
|
urlpath = '/%s/resources/%s' % (self.stack.id, self.name)
|
||||||
|
ec2_creds = self.keystone().get_ec2_keypair(self.resource_id)
|
||||||
|
signed_url = self._sign_url(ec2_creds, urlpath)
|
||||||
|
return unicode(signed_url)
|
||||||
|
else:
|
||||||
|
return unicode(self.name)
|
||||||
|
|
||||||
|
|
||||||
WAIT_STATUSES = (
|
WAIT_STATUSES = (
|
||||||
WAITING,
|
WAITING,
|
||||||
TIMEDOUT,
|
TIMEDOUT,
|
||||||
|
@ -87,7 +148,7 @@ class WaitCondition(resource.Resource):
|
||||||
def _get_handle_resource_id(self):
|
def _get_handle_resource_id(self):
|
||||||
if self.resource_id is None:
|
if self.resource_id is None:
|
||||||
handle_url = self.properties['Handle']
|
handle_url = self.properties['Handle']
|
||||||
self.resource_id = handle_url.split('/')[-1]
|
self.resource_id = handle_url.split('/')[-1].split('?')[0]
|
||||||
return self.resource_id
|
return self.resource_id
|
||||||
|
|
||||||
def _get_status_reason(self, handle):
|
def _get_status_reason(self, handle):
|
||||||
|
|
|
@ -17,17 +17,22 @@ import json
|
||||||
import logging
|
import logging
|
||||||
import mox
|
import mox
|
||||||
import sys
|
import sys
|
||||||
|
import uuid
|
||||||
|
import time
|
||||||
|
|
||||||
import eventlet
|
import eventlet
|
||||||
import nose
|
import nose
|
||||||
import unittest
|
import unittest
|
||||||
from nose.plugins.attrib import attr
|
from nose.plugins.attrib import attr
|
||||||
|
from heat.tests import fakes
|
||||||
|
|
||||||
import heat.db as db_api
|
import heat.db as db_api
|
||||||
from heat.engine import format
|
from heat.engine import format
|
||||||
from heat.engine import parser
|
from heat.engine import parser
|
||||||
from heat.engine.resources import wait_condition as wc
|
from heat.engine.resources import wait_condition as wc
|
||||||
from heat.common import context
|
from heat.common import context
|
||||||
|
from heat.common import config
|
||||||
|
from heat.openstack.common import cfg
|
||||||
|
|
||||||
logger = logging.getLogger('test_waitcondition')
|
logger = logging.getLogger('test_waitcondition')
|
||||||
|
|
||||||
|
@ -52,9 +57,9 @@ test_template_waitcondition = '''
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
|
||||||
@attr(tag=['unit', 'resource'])
|
@attr(tag=['unit', 'resource', 'WaitCondition'])
|
||||||
@attr(speed='slow')
|
@attr(speed='slow')
|
||||||
class stacksTest(unittest.TestCase):
|
class WaitConditionTest(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.m = mox.Mox()
|
self.m = mox.Mox()
|
||||||
self.m.StubOutWithMock(wc.WaitCondition,
|
self.m.StubOutWithMock(wc.WaitCondition,
|
||||||
|
@ -63,6 +68,12 @@ class stacksTest(unittest.TestCase):
|
||||||
'_create_timeout')
|
'_create_timeout')
|
||||||
self.m.StubOutWithMock(eventlet, 'sleep')
|
self.m.StubOutWithMock(eventlet, 'sleep')
|
||||||
|
|
||||||
|
config.register_engine_opts()
|
||||||
|
cfg.CONF.set_default('heat_waitcondition_server_url',
|
||||||
|
'http://127.0.0.1:8000/v1/waitcondition')
|
||||||
|
|
||||||
|
self.fc = fakes.FakeKeystoneClient()
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
self.m.UnsetStubs()
|
self.m.UnsetStubs()
|
||||||
|
|
||||||
|
@ -90,6 +101,9 @@ class stacksTest(unittest.TestCase):
|
||||||
wc.WaitCondition._get_status_reason(
|
wc.WaitCondition._get_status_reason(
|
||||||
mox.IgnoreArg()).AndReturn(('SUCCESS', 'woot toot'))
|
mox.IgnoreArg()).AndReturn(('SUCCESS', 'woot toot'))
|
||||||
|
|
||||||
|
self.m.StubOutWithMock(wc.WaitConditionHandle, 'keystone')
|
||||||
|
wc.WaitConditionHandle.keystone().MultipleTimes().AndReturn(self.fc)
|
||||||
|
|
||||||
self.m.ReplayAll()
|
self.m.ReplayAll()
|
||||||
|
|
||||||
stack.create()
|
stack.create()
|
||||||
|
@ -118,6 +132,9 @@ class stacksTest(unittest.TestCase):
|
||||||
mox.IgnoreArg()).AndReturn(('WAITING', ''))
|
mox.IgnoreArg()).AndReturn(('WAITING', ''))
|
||||||
eventlet.sleep(1).AndRaise(tmo)
|
eventlet.sleep(1).AndRaise(tmo)
|
||||||
|
|
||||||
|
self.m.StubOutWithMock(wc.WaitConditionHandle, 'keystone')
|
||||||
|
wc.WaitConditionHandle.keystone().MultipleTimes().AndReturn(self.fc)
|
||||||
|
|
||||||
self.m.ReplayAll()
|
self.m.ReplayAll()
|
||||||
|
|
||||||
stack.create()
|
stack.create()
|
||||||
|
@ -133,7 +150,78 @@ class stacksTest(unittest.TestCase):
|
||||||
|
|
||||||
self.m.VerifyAll()
|
self.m.VerifyAll()
|
||||||
|
|
||||||
# allows testing of the test directly
|
|
||||||
if __name__ == '__main__':
|
@attr(tag=['unit', 'resource', 'WaitConditionHandle'])
|
||||||
sys.argv.append(__file__)
|
@attr(speed='fast')
|
||||||
nose.main()
|
class WaitConditionHandleTest(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.m = mox.Mox()
|
||||||
|
config.register_engine_opts()
|
||||||
|
cfg.CONF.set_default('heat_waitcondition_server_url',
|
||||||
|
'http://127.0.0.1:8000/v1/waitcondition')
|
||||||
|
|
||||||
|
self.fc = fakes.FakeKeystoneClient()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.m.UnsetStubs()
|
||||||
|
|
||||||
|
def create_stack(self, stack_name='test_stack2', params={}):
|
||||||
|
temp = format.parse_to_template(test_template_waitcondition)
|
||||||
|
template = parser.Template(temp)
|
||||||
|
parameters = parser.Parameters(stack_name, template, params)
|
||||||
|
stack = parser.Stack(context.get_admin_context(), stack_name,
|
||||||
|
template, parameters)
|
||||||
|
# Stub out the UUID for this test, so we can get an expected signature
|
||||||
|
self.m.StubOutWithMock(uuid, 'uuid4')
|
||||||
|
uuid.uuid4().AndReturn('STACKABCD1234')
|
||||||
|
self.m.ReplayAll()
|
||||||
|
stack.store()
|
||||||
|
return stack
|
||||||
|
|
||||||
|
def test_handle(self):
|
||||||
|
stack = self.create_stack()
|
||||||
|
|
||||||
|
# Stub waitcondition status so all goes CREATE_COMPLETE
|
||||||
|
self.m.StubOutWithMock(wc.WaitCondition, '_get_status_reason')
|
||||||
|
wc.WaitCondition._get_status_reason(
|
||||||
|
mox.IgnoreArg()).AndReturn(('SUCCESS', 'woot toot'))
|
||||||
|
self.m.StubOutWithMock(wc.WaitCondition, '_create_timeout')
|
||||||
|
wc.WaitCondition._create_timeout().AndReturn(eventlet.Timeout(5))
|
||||||
|
|
||||||
|
# Stub keystone() with fake client
|
||||||
|
self.m.StubOutWithMock(wc.WaitConditionHandle, 'keystone')
|
||||||
|
wc.WaitConditionHandle.keystone().MultipleTimes().AndReturn(self.fc)
|
||||||
|
|
||||||
|
# Stub time to a fixed value so we can get an expected signature
|
||||||
|
t = time.gmtime(1354196977)
|
||||||
|
self.m.StubOutWithMock(time, 'gmtime')
|
||||||
|
time.gmtime().MultipleTimes().AndReturn(t)
|
||||||
|
|
||||||
|
self.m.ReplayAll()
|
||||||
|
stack.create()
|
||||||
|
|
||||||
|
resource = stack.resources['WaitHandle']
|
||||||
|
self.assertEqual(resource.state, 'CREATE_COMPLETE')
|
||||||
|
|
||||||
|
expected_url = "".join(
|
||||||
|
["http://127.0.0.1:8000/v1/waitcondition/STACKABCD1234",
|
||||||
|
"/resources/WaitHandle",
|
||||||
|
"?Timestamp=2012-11-29T13%3A49%3A37Z",
|
||||||
|
"&SignatureMethod=HmacSHA256",
|
||||||
|
"&AWSAccessKeyId=4567",
|
||||||
|
"&SignatureVersion=2",
|
||||||
|
"&Signature=",
|
||||||
|
"%2BY5r9xvxTzTrRkz%2Br5T1wGeFwoU1wTh2c5u8a2sCurQ%3D"])
|
||||||
|
self.assertEqual(expected_url, resource.FnGetRefId())
|
||||||
|
|
||||||
|
self.assertEqual(resource.UPDATE_REPLACE,
|
||||||
|
resource.handle_update())
|
||||||
|
|
||||||
|
stack.delete()
|
||||||
|
|
||||||
|
self.m.VerifyAll()
|
||||||
|
|
||||||
|
# allows testing of the test directly
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv.append(__file__)
|
||||||
|
nose.main()
|
||||||
|
|
Loading…
Reference in New Issue