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:
Steven Hardy 2012-11-26 11:26:00 +00:00
parent d4bb435ed2
commit 17dd71b017
4 changed files with 267 additions and 13 deletions

View File

@ -25,9 +25,9 @@ heat_metadata_server_url = http://127.0.0.1:8000
# of waitcondition events (ie via cfn-signal)
# e.g the IP of the bridge device connecting the
# instances with the host and the bind_port of
# the heat-metadata API
# the CFN API
# 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
# data (ie via cfn-push-stats)

105
heat/common/ec2signer.py Normal file
View File

@ -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

View File

@ -14,6 +14,9 @@
# under the License.
import eventlet
import time
import urllib
import urlparse
from heat.common import exception
from heat.engine import resource
@ -22,6 +25,16 @@ from heat.openstack.common import log as logging
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')
@ -38,15 +51,63 @@ class WaitConditionHandle(resource.Resource):
def __init__(self, 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):
self.resource_id = '%s/stacks/%s/resources/%s' % \
(cfg.CONF.heat_waitcondition_server_url,
self.stack.id,
self.name)
# Create a keystone user so we can create a signed URL via FnGetRefId
user_id = self.keystone().create_stack_user(
self.physical_resource_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):
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 = (
WAITING,
TIMEDOUT,
@ -87,7 +148,7 @@ class WaitCondition(resource.Resource):
def _get_handle_resource_id(self):
if self.resource_id is None:
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
def _get_status_reason(self, handle):

View File

@ -17,17 +17,22 @@ import json
import logging
import mox
import sys
import uuid
import time
import eventlet
import nose
import unittest
from nose.plugins.attrib import attr
from heat.tests import fakes
import heat.db as db_api
from heat.engine import format
from heat.engine import parser
from heat.engine.resources import wait_condition as wc
from heat.common import context
from heat.common import config
from heat.openstack.common import cfg
logger = logging.getLogger('test_waitcondition')
@ -52,9 +57,9 @@ test_template_waitcondition = '''
'''
@attr(tag=['unit', 'resource'])
@attr(tag=['unit', 'resource', 'WaitCondition'])
@attr(speed='slow')
class stacksTest(unittest.TestCase):
class WaitConditionTest(unittest.TestCase):
def setUp(self):
self.m = mox.Mox()
self.m.StubOutWithMock(wc.WaitCondition,
@ -63,6 +68,12 @@ class stacksTest(unittest.TestCase):
'_create_timeout')
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):
self.m.UnsetStubs()
@ -90,6 +101,9 @@ class stacksTest(unittest.TestCase):
wc.WaitCondition._get_status_reason(
mox.IgnoreArg()).AndReturn(('SUCCESS', 'woot toot'))
self.m.StubOutWithMock(wc.WaitConditionHandle, 'keystone')
wc.WaitConditionHandle.keystone().MultipleTimes().AndReturn(self.fc)
self.m.ReplayAll()
stack.create()
@ -118,6 +132,9 @@ class stacksTest(unittest.TestCase):
mox.IgnoreArg()).AndReturn(('WAITING', ''))
eventlet.sleep(1).AndRaise(tmo)
self.m.StubOutWithMock(wc.WaitConditionHandle, 'keystone')
wc.WaitConditionHandle.keystone().MultipleTimes().AndReturn(self.fc)
self.m.ReplayAll()
stack.create()
@ -133,7 +150,78 @@ class stacksTest(unittest.TestCase):
self.m.VerifyAll()
# allows testing of the test directly
if __name__ == '__main__':
@attr(tag=['unit', 'resource', 'WaitConditionHandle'])
@attr(speed='fast')
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()