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:
ricolin 2018-09-26 09:58:42 +08:00
parent 6990331639
commit 3ad7ab2884
3 changed files with 159 additions and 35 deletions

View File

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

View File

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

View File

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