Override ssl options for heatclient in RemoteStack
Allow the user to set the CA cert for SSL option for contacting the remote Heat in the properties of an OS::Heat::Stack resource. Story: #1702645 Task: #17270 Change-Id: I37528bb2b881a196216a7e6e23af871ab0f313d6
This commit is contained in:
parent
6990331639
commit
3ad7ab2884
@ -11,8 +11,10 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from oslo_log import log as logging
|
||||
from oslo_serialization import jsonutils
|
||||
import six
|
||||
import tempfile
|
||||
|
||||
from heat.common import auth_plugin
|
||||
from heat.common import context
|
||||
@ -26,6 +28,40 @@ from heat.engine import resource
|
||||
from heat.engine import support
|
||||
from heat.engine import template
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TempCACertFile(object):
|
||||
def __init__(self, ca_cert):
|
||||
self._cacert = ca_cert
|
||||
self._cacert_temp_file = None
|
||||
|
||||
def __enter__(self):
|
||||
self.tempfile_path = self._store_temp_ca_cert()
|
||||
return self.tempfile_path
|
||||
|
||||
def __exit__(self, type, value, traceback):
|
||||
if self._cacert_temp_file:
|
||||
self._cacert_temp_file.close()
|
||||
|
||||
def _store_temp_ca_cert(self):
|
||||
if self._cacert:
|
||||
try:
|
||||
self._cacert_temp_file = tempfile.NamedTemporaryFile()
|
||||
self._cacert_temp_file.write(
|
||||
six.text_type(self._cacert).encode('utf-8'))
|
||||
# Add seek func to make sure the writen context will flush to
|
||||
# tempfile with python 2.7. we can use flush() for python 2.7
|
||||
# but not 3.5.
|
||||
self._cacert_temp_file.seek(0)
|
||||
file_path = self._cacert_temp_file.name
|
||||
return file_path
|
||||
except Exception:
|
||||
LOG.exception("Error when create template file for CA cert")
|
||||
if self._cacert_temp_file:
|
||||
self._cacert_temp_file.close()
|
||||
raise
|
||||
|
||||
|
||||
class RemoteStack(resource.Resource):
|
||||
"""A Resource representing a stack.
|
||||
@ -50,9 +86,9 @@ class RemoteStack(resource.Resource):
|
||||
)
|
||||
|
||||
_CONTEXT_KEYS = (
|
||||
REGION_NAME, CREDENTIAL_SECRET_ID
|
||||
REGION_NAME, CREDENTIAL_SECRET_ID, CA_CERT, SSL_INSECURE
|
||||
) = (
|
||||
'region_name', 'credential_secret_id'
|
||||
'region_name', 'credential_secret_id', 'ca_cert', 'insecure'
|
||||
)
|
||||
|
||||
properties_schema = {
|
||||
@ -75,6 +111,22 @@ class RemoteStack(resource.Resource):
|
||||
update_allowed=True,
|
||||
support_status=support.SupportStatus(version='12.0.0'),
|
||||
),
|
||||
CA_CERT: properties.Schema(
|
||||
properties.Schema.STRING,
|
||||
_('CA Cert for SSL.'),
|
||||
required=False,
|
||||
update_allowed=True,
|
||||
support_status=support.SupportStatus(version='12.0.0'),
|
||||
),
|
||||
SSL_INSECURE: properties.Schema(
|
||||
properties.Schema.BOOLEAN,
|
||||
_("If set, then the server's certificate will not be "
|
||||
"verified."),
|
||||
default=False,
|
||||
required=False,
|
||||
update_allowed=True,
|
||||
support_status=support.SupportStatus(version='12.0.0'),
|
||||
),
|
||||
}
|
||||
),
|
||||
TEMPLATE: properties.Schema(
|
||||
@ -112,20 +164,45 @@ class RemoteStack(resource.Resource):
|
||||
super(RemoteStack, self).__init__(name, definition, stack)
|
||||
self._region_name = None
|
||||
self._local_context = None
|
||||
self._ssl_verify = None
|
||||
self._cacert = None
|
||||
|
||||
def _context(self):
|
||||
if self._local_context:
|
||||
return self._local_context
|
||||
@property
|
||||
def cacert(self):
|
||||
ctx_props = self.properties.get(self.CONTEXT)
|
||||
if ctx_props:
|
||||
self._cacert = ctx_props[self.CA_CERT]
|
||||
return self._cacert
|
||||
|
||||
def _get_from_secret(self, key):
|
||||
result = super(RemoteStack, self).client_plugin(
|
||||
'barbican').get_secret_payload_by_ref(
|
||||
secret_ref='secrets/%s' % (key))
|
||||
return result
|
||||
|
||||
def _context(self, cacert_path=None):
|
||||
need_reassign = False
|
||||
# To get ctx_props first, since cacert_path might change each time we
|
||||
# call _context
|
||||
ctx_props = self.properties.get(self.CONTEXT)
|
||||
if ctx_props:
|
||||
self._credential = ctx_props[self.CREDENTIAL_SECRET_ID]
|
||||
self._region_name = ctx_props[self.REGION_NAME] if ctx_props[
|
||||
self.REGION_NAME] else self.context.region_name
|
||||
_insecure = ctx_props[self.SSL_INSECURE]
|
||||
|
||||
_ssl_verify = False if _insecure else (
|
||||
cacert_path or True)
|
||||
need_reassign = self._ssl_verify != _ssl_verify
|
||||
if need_reassign:
|
||||
self._ssl_verify = _ssl_verify
|
||||
else:
|
||||
self._credential = None
|
||||
self._region_name = self.context.region_name
|
||||
|
||||
if self._local_context and not need_reassign:
|
||||
return self._local_context
|
||||
|
||||
if ctx_props and self._credential:
|
||||
return self._prepare_cloud_context()
|
||||
else:
|
||||
@ -134,9 +211,7 @@ class RemoteStack(resource.Resource):
|
||||
def _fetch_barbican_credential(self):
|
||||
"""Fetch credential information and return context dict."""
|
||||
|
||||
auth = super(RemoteStack, self).client_plugin(
|
||||
'barbican').get_secret_payload_by_ref(
|
||||
secret_ref='secrets/%s' % (self._credential))
|
||||
auth = self._get_from_secret(self._credential)
|
||||
return auth
|
||||
|
||||
def _prepare_cloud_context(self):
|
||||
@ -150,6 +225,8 @@ class RemoteStack(resource.Resource):
|
||||
'show_deleted': dict_ctxt['show_deleted']
|
||||
})
|
||||
self._local_context = context.RequestContext.from_dict(dict_ctxt)
|
||||
if self._ssl_verify is not None:
|
||||
self._local_context.keystone_session.verify = self._ssl_verify
|
||||
self._local_context._auth_plugin = (
|
||||
auth_plugin.get_keystone_plugin_loader(
|
||||
auth, self._local_context.keystone_session))
|
||||
@ -163,11 +240,14 @@ class RemoteStack(resource.Resource):
|
||||
dict_ctxt.update({'region_name': self._region_name,
|
||||
'overwrite': False})
|
||||
self._local_context = context.RequestContext.from_dict(dict_ctxt)
|
||||
if self._ssl_verify is not None:
|
||||
self._local_context.keystone_session.verify = self._ssl_verify
|
||||
return self._local_context
|
||||
|
||||
def heat(self):
|
||||
def heat(self, cacert_path):
|
||||
# A convenience method overriding Resource.heat()
|
||||
return self._context().clients.client(self.default_client_name)
|
||||
return self._context(
|
||||
cacert_path).clients.client(self.default_client_name)
|
||||
|
||||
def client_plugin(self):
|
||||
# A convenience method overriding Resource.client_plugin()
|
||||
@ -177,7 +257,8 @@ class RemoteStack(resource.Resource):
|
||||
super(RemoteStack, self).validate()
|
||||
|
||||
try:
|
||||
self.heat()
|
||||
with TempCACertFile(self.cacert) as cacert_path:
|
||||
self.heat(cacert_path)
|
||||
except Exception as ex:
|
||||
if self._credential:
|
||||
location = "remote cloud"
|
||||
@ -197,7 +278,8 @@ class RemoteStack(resource.Resource):
|
||||
'files': self.stack.t.files,
|
||||
'environment': env.user_env_as_dict(),
|
||||
}
|
||||
self.heat().stacks.validate(**args)
|
||||
with TempCACertFile(self.cacert) as cacert_path:
|
||||
self.heat(cacert_path).stacks.validate(**args)
|
||||
except Exception as ex:
|
||||
if self._credential:
|
||||
location = "remote cloud"
|
||||
@ -221,34 +303,44 @@ class RemoteStack(resource.Resource):
|
||||
'files': self.stack.t.files,
|
||||
'environment': env.user_env_as_dict(),
|
||||
}
|
||||
remote_stack_id = self.heat().stacks.create(**args)['stack']['id']
|
||||
with TempCACertFile(self.cacert) as cacert_path:
|
||||
remote_stack_id = self.heat(
|
||||
cacert_path).stacks.create(**args)['stack']['id']
|
||||
self.resource_id_set(remote_stack_id)
|
||||
|
||||
def handle_delete(self):
|
||||
if self.resource_id is not None:
|
||||
with self.client_plugin().ignore_not_found:
|
||||
self.heat().stacks.delete(stack_id=self.resource_id)
|
||||
with TempCACertFile(self.cacert) as cacert_path:
|
||||
self.heat(
|
||||
cacert_path).stacks.delete(stack_id=self.resource_id)
|
||||
|
||||
def handle_resume(self):
|
||||
if self.resource_id is None:
|
||||
raise exception.Error(_('Cannot resume %s, resource not found')
|
||||
% self.name)
|
||||
self.heat().actions.resume(stack_id=self.resource_id)
|
||||
with TempCACertFile(self.cacert) as cacert_path:
|
||||
self.heat(cacert_path).actions.resume(stack_id=self.resource_id)
|
||||
|
||||
def handle_suspend(self):
|
||||
if self.resource_id is None:
|
||||
raise exception.Error(_('Cannot suspend %s, resource not found')
|
||||
% self.name)
|
||||
self.heat().actions.suspend(stack_id=self.resource_id)
|
||||
with TempCACertFile(self.cacert) as cacert_path:
|
||||
self.heat(cacert_path).actions.suspend(stack_id=self.resource_id)
|
||||
|
||||
def handle_snapshot(self):
|
||||
snapshot = self.heat().stacks.snapshot(stack_id=self.resource_id)
|
||||
with TempCACertFile(self.cacert) as cacert_path:
|
||||
snapshot = self.heat(
|
||||
cacert_path).stacks.snapshot(stack_id=self.resource_id)
|
||||
self.data_set('snapshot_id', snapshot['id'])
|
||||
|
||||
def handle_restore(self, defn, restore_data):
|
||||
snapshot_id = restore_data['resource_data']['snapshot_id']
|
||||
snapshot = self.heat().stacks.snapshot_show(self.resource_id,
|
||||
snapshot_id)
|
||||
with TempCACertFile(self.cacert) as cacert_path:
|
||||
snapshot = self.heat(
|
||||
cacert_path).stacks.snapshot_show(self.resource_id,
|
||||
snapshot_id)
|
||||
s_data = snapshot['snapshot']['data']
|
||||
env = environment.Environment(s_data['environment'])
|
||||
files = s_data['files']
|
||||
@ -261,7 +353,8 @@ class RemoteStack(resource.Resource):
|
||||
return defn.freeze(properties=props)
|
||||
|
||||
def handle_check(self):
|
||||
self.heat().actions.check(stack_id=self.resource_id)
|
||||
with TempCACertFile(self.cacert) as cacert_path:
|
||||
self.heat(cacert_path).actions.check(stack_id=self.resource_id)
|
||||
|
||||
def _needs_update(self, after, before, after_props, before_props,
|
||||
prev_resource, check_init_complete=True):
|
||||
@ -293,10 +386,13 @@ class RemoteStack(resource.Resource):
|
||||
'files': self.stack.t.files,
|
||||
'environment': env.user_env_as_dict(),
|
||||
}
|
||||
self.heat().stacks.update(**fields)
|
||||
with TempCACertFile(self.cacert) as cacert_path:
|
||||
self.heat(cacert_path).stacks.update(**fields)
|
||||
|
||||
def _check_action_complete(self, action):
|
||||
stack = self.heat().stacks.get(stack_id=self.resource_id)
|
||||
with TempCACertFile(self.cacert) as cacert_path:
|
||||
stack = self.heat(
|
||||
cacert_path).stacks.get(stack_id=self.resource_id)
|
||||
if stack.action != action:
|
||||
return False
|
||||
|
||||
@ -346,7 +442,9 @@ class RemoteStack(resource.Resource):
|
||||
def _resolve_attribute(self, name):
|
||||
if self.resource_id is None:
|
||||
return
|
||||
stack = self.heat().stacks.get(stack_id=self.resource_id)
|
||||
with TempCACertFile(self.cacert) as cacert_path:
|
||||
stack = self.heat(
|
||||
cacert_path).stacks.get(stack_id=self.resource_id)
|
||||
if name == self.NAME_ATTR:
|
||||
value = getattr(stack, name, None)
|
||||
return value or self.physical_resource_name_or_FnGetRefId()
|
||||
|
@ -25,7 +25,6 @@ from heat.common import exception
|
||||
from heat.common.i18n import _
|
||||
from heat.common import policy
|
||||
from heat.common import template_format
|
||||
from heat.engine.clients.os import barbican as barbican_client
|
||||
from heat.engine.clients.os import heat_plugin
|
||||
from heat.engine import environment
|
||||
from heat.engine import node_data
|
||||
@ -305,23 +304,21 @@ class RemoteStackTest(tests_common.HeatTestCase):
|
||||
self.heat.stacks.create.assert_called_with(**args)
|
||||
self.assertEqual(2, len(self.heat.stacks.get.call_args_list))
|
||||
|
||||
def _create_with_remote_credential(self, credential_secret_id=None):
|
||||
self.auth = (
|
||||
'{"auth_type": "v3applicationcredential", '
|
||||
'"auth": {"auth_url": "http://192.168.1.101/identity/v3", '
|
||||
'"application_credential_id": "9dfa187e5a354484bf9c49a2b674333a", '
|
||||
'"application_credential_secret": "sec"} }')
|
||||
def _create_with_remote_credential(self, credential_secret_id=None,
|
||||
ca_cert=None, insecure=False):
|
||||
|
||||
t = template_format.parse(parent_stack_template)
|
||||
properties = t['resources']['remote_stack']['properties']
|
||||
if credential_secret_id:
|
||||
properties['context']['credential_secret_id'] = (
|
||||
credential_secret_id)
|
||||
if ca_cert:
|
||||
properties['context']['ca_cert'] = (
|
||||
ca_cert)
|
||||
if insecure:
|
||||
properties['context']['insecure'] = insecure
|
||||
t = json.dumps(t)
|
||||
self.patchobject(policy.Enforcer, 'check_is_admin')
|
||||
self.m_gsbr = self.patchobject(
|
||||
barbican_client.BarbicanClientPlugin, 'get_secret_payload_by_ref')
|
||||
self.m_gsbr.return_value = self.auth
|
||||
|
||||
rsrc = self.create_remote_stack(stack_template=t)
|
||||
env = environment.get_child_environment(rsrc.stack.env,
|
||||
@ -340,14 +337,22 @@ class RemoteStackTest(tests_common.HeatTestCase):
|
||||
rsrc.validate()
|
||||
return rsrc
|
||||
|
||||
def test_create_with_credential_secret_id(self):
|
||||
@mock.patch('heat.engine.clients.os.barbican.BarbicanClientPlugin.'
|
||||
'get_secret_payload_by_ref')
|
||||
def test_create_with_credential_secret_id(self, m_gsbr):
|
||||
secret = (
|
||||
'{"auth_type": "v3applicationcredential", '
|
||||
'"auth": {"auth_url": "http://192.168.1.101/identity/v3", '
|
||||
'"application_credential_id": "9dfa187e5a354484bf9c49a2b674333a", '
|
||||
'"application_credential_secret": "sec"} }')
|
||||
m_gsbr.return_value = secret
|
||||
self.m_plugin = mock.Mock()
|
||||
self.m_loader = self.patchobject(
|
||||
ks_loading, 'get_plugin_loader', return_value=self.m_plugin)
|
||||
self._create_with_remote_credential('cred_2')
|
||||
self.assertEqual(
|
||||
[mock.call(secret_ref='secrets/cred_2')]*2,
|
||||
self.m_gsbr.call_args_list)
|
||||
m_gsbr.call_args_list)
|
||||
expected_load_options = [
|
||||
mock.call(
|
||||
application_credential_id='9dfa187e5a354484bf9c49a2b674333a',
|
||||
@ -357,6 +362,19 @@ class RemoteStackTest(tests_common.HeatTestCase):
|
||||
self.assertEqual(expected_load_options,
|
||||
self.m_plugin.load_from_options.call_args_list)
|
||||
|
||||
def test_create_with_ca_cert(self):
|
||||
ca_cert = (
|
||||
"-----BEGIN CERTIFICATE----- A CA CERT -----END CERTIFICATE-----")
|
||||
rsrc = self._create_with_remote_credential(
|
||||
ca_cert=ca_cert)
|
||||
self.assertEqual(ca_cert, rsrc._cacert)
|
||||
self.assertEqual(ca_cert, rsrc.cacert)
|
||||
self.assertTrue('/tmp/' in rsrc._ssl_verify)
|
||||
|
||||
def test_create_with_insecure(self):
|
||||
rsrc = self._create_with_remote_credential(insecure=True)
|
||||
self.assertFalse(rsrc._ssl_verify)
|
||||
|
||||
def test_create_failed(self):
|
||||
returns = [get_stack(stack_status='CREATE_IN_PROGRESS'),
|
||||
get_stack(stack_status='CREATE_FAILED',
|
||||
|
@ -0,0 +1,8 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Add ``ca_cert`` and ``insecure`` properties for
|
||||
``OS::Heat::Stack`` resource type. The ``ca_cert`` is the contents of a CA
|
||||
Certificate file that can be used to verify a remote cloud or region's
|
||||
server certificate. ``insecure`` is boolean option, CA cert will be use if
|
||||
we didn't setup insecure flag.
|
Loading…
Reference in New Issue
Block a user