# 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)
--- /dev/null
+# 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
# under the License.
import eventlet
+import time
+import urllib
+import urlparse
from heat.common import exception
from heat.engine import resource
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')
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,
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):
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')
'''
-@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,
'_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()
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()
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()
self.m.VerifyAll()
- # allows testing of the test directly
- if __name__ == '__main__':
- sys.argv.append(__file__)
- nose.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()