diff --git a/heat/engine/resources/openstack/heat/remote_stack.py b/heat/engine/resources/openstack/heat/remote_stack.py index 66c6b33396..3b8dafdb90 100644 --- a/heat/engine/resources/openstack/heat/remote_stack.py +++ b/heat/engine/resources/openstack/heat/remote_stack.py @@ -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() diff --git a/heat/tests/openstack/heat/test_remote_stack.py b/heat/tests/openstack/heat/test_remote_stack.py index e777f92a3d..69a10a2283 100644 --- a/heat/tests/openstack/heat/test_remote_stack.py +++ b/heat/tests/openstack/heat/test_remote_stack.py @@ -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', diff --git a/releasenotes/notes/support_remote_stack_override_ssl_options-69c82b351920af57.yaml b/releasenotes/notes/support_remote_stack_override_ssl_options-69c82b351920af57.yaml new file mode 100644 index 0000000000..6259e9c5a6 --- /dev/null +++ b/releasenotes/notes/support_remote_stack_override_ssl_options-69c82b351920af57.yaml @@ -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.