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
heat
releasenotes/notes
@ -11,8 +11,10 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
from oslo_log import log as logging
|
||||||
from oslo_serialization import jsonutils
|
from oslo_serialization import jsonutils
|
||||||
import six
|
import six
|
||||||
|
import tempfile
|
||||||
|
|
||||||
from heat.common import auth_plugin
|
from heat.common import auth_plugin
|
||||||
from heat.common import context
|
from heat.common import context
|
||||||
@ -26,6 +28,40 @@ from heat.engine import resource
|
|||||||
from heat.engine import support
|
from heat.engine import support
|
||||||
from heat.engine import template
|
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):
|
class RemoteStack(resource.Resource):
|
||||||
"""A Resource representing a stack.
|
"""A Resource representing a stack.
|
||||||
@ -50,9 +86,9 @@ class RemoteStack(resource.Resource):
|
|||||||
)
|
)
|
||||||
|
|
||||||
_CONTEXT_KEYS = (
|
_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 = {
|
properties_schema = {
|
||||||
@ -75,6 +111,22 @@ class RemoteStack(resource.Resource):
|
|||||||
update_allowed=True,
|
update_allowed=True,
|
||||||
support_status=support.SupportStatus(version='12.0.0'),
|
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(
|
TEMPLATE: properties.Schema(
|
||||||
@ -112,20 +164,45 @@ class RemoteStack(resource.Resource):
|
|||||||
super(RemoteStack, self).__init__(name, definition, stack)
|
super(RemoteStack, self).__init__(name, definition, stack)
|
||||||
self._region_name = None
|
self._region_name = None
|
||||||
self._local_context = None
|
self._local_context = None
|
||||||
|
self._ssl_verify = None
|
||||||
|
self._cacert = None
|
||||||
|
|
||||||
def _context(self):
|
@property
|
||||||
if self._local_context:
|
def cacert(self):
|
||||||
return self._local_context
|
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)
|
ctx_props = self.properties.get(self.CONTEXT)
|
||||||
if ctx_props:
|
if ctx_props:
|
||||||
self._credential = ctx_props[self.CREDENTIAL_SECRET_ID]
|
self._credential = ctx_props[self.CREDENTIAL_SECRET_ID]
|
||||||
self._region_name = ctx_props[self.REGION_NAME] if ctx_props[
|
self._region_name = ctx_props[self.REGION_NAME] if ctx_props[
|
||||||
self.REGION_NAME] else self.context.region_name
|
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:
|
else:
|
||||||
self._credential = None
|
self._credential = None
|
||||||
self._region_name = self.context.region_name
|
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:
|
if ctx_props and self._credential:
|
||||||
return self._prepare_cloud_context()
|
return self._prepare_cloud_context()
|
||||||
else:
|
else:
|
||||||
@ -134,9 +211,7 @@ class RemoteStack(resource.Resource):
|
|||||||
def _fetch_barbican_credential(self):
|
def _fetch_barbican_credential(self):
|
||||||
"""Fetch credential information and return context dict."""
|
"""Fetch credential information and return context dict."""
|
||||||
|
|
||||||
auth = super(RemoteStack, self).client_plugin(
|
auth = self._get_from_secret(self._credential)
|
||||||
'barbican').get_secret_payload_by_ref(
|
|
||||||
secret_ref='secrets/%s' % (self._credential))
|
|
||||||
return auth
|
return auth
|
||||||
|
|
||||||
def _prepare_cloud_context(self):
|
def _prepare_cloud_context(self):
|
||||||
@ -150,6 +225,8 @@ class RemoteStack(resource.Resource):
|
|||||||
'show_deleted': dict_ctxt['show_deleted']
|
'show_deleted': dict_ctxt['show_deleted']
|
||||||
})
|
})
|
||||||
self._local_context = context.RequestContext.from_dict(dict_ctxt)
|
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 = (
|
self._local_context._auth_plugin = (
|
||||||
auth_plugin.get_keystone_plugin_loader(
|
auth_plugin.get_keystone_plugin_loader(
|
||||||
auth, self._local_context.keystone_session))
|
auth, self._local_context.keystone_session))
|
||||||
@ -163,11 +240,14 @@ class RemoteStack(resource.Resource):
|
|||||||
dict_ctxt.update({'region_name': self._region_name,
|
dict_ctxt.update({'region_name': self._region_name,
|
||||||
'overwrite': False})
|
'overwrite': False})
|
||||||
self._local_context = context.RequestContext.from_dict(dict_ctxt)
|
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
|
return self._local_context
|
||||||
|
|
||||||
def heat(self):
|
def heat(self, cacert_path):
|
||||||
# A convenience method overriding Resource.heat()
|
# 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):
|
def client_plugin(self):
|
||||||
# A convenience method overriding Resource.client_plugin()
|
# A convenience method overriding Resource.client_plugin()
|
||||||
@ -177,7 +257,8 @@ class RemoteStack(resource.Resource):
|
|||||||
super(RemoteStack, self).validate()
|
super(RemoteStack, self).validate()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.heat()
|
with TempCACertFile(self.cacert) as cacert_path:
|
||||||
|
self.heat(cacert_path)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
if self._credential:
|
if self._credential:
|
||||||
location = "remote cloud"
|
location = "remote cloud"
|
||||||
@ -197,7 +278,8 @@ class RemoteStack(resource.Resource):
|
|||||||
'files': self.stack.t.files,
|
'files': self.stack.t.files,
|
||||||
'environment': env.user_env_as_dict(),
|
'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:
|
except Exception as ex:
|
||||||
if self._credential:
|
if self._credential:
|
||||||
location = "remote cloud"
|
location = "remote cloud"
|
||||||
@ -221,34 +303,44 @@ class RemoteStack(resource.Resource):
|
|||||||
'files': self.stack.t.files,
|
'files': self.stack.t.files,
|
||||||
'environment': env.user_env_as_dict(),
|
'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)
|
self.resource_id_set(remote_stack_id)
|
||||||
|
|
||||||
def handle_delete(self):
|
def handle_delete(self):
|
||||||
if self.resource_id is not None:
|
if self.resource_id is not None:
|
||||||
with self.client_plugin().ignore_not_found:
|
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):
|
def handle_resume(self):
|
||||||
if self.resource_id is None:
|
if self.resource_id is None:
|
||||||
raise exception.Error(_('Cannot resume %s, resource not found')
|
raise exception.Error(_('Cannot resume %s, resource not found')
|
||||||
% self.name)
|
% 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):
|
def handle_suspend(self):
|
||||||
if self.resource_id is None:
|
if self.resource_id is None:
|
||||||
raise exception.Error(_('Cannot suspend %s, resource not found')
|
raise exception.Error(_('Cannot suspend %s, resource not found')
|
||||||
% self.name)
|
% 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):
|
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'])
|
self.data_set('snapshot_id', snapshot['id'])
|
||||||
|
|
||||||
def handle_restore(self, defn, restore_data):
|
def handle_restore(self, defn, restore_data):
|
||||||
snapshot_id = restore_data['resource_data']['snapshot_id']
|
snapshot_id = restore_data['resource_data']['snapshot_id']
|
||||||
snapshot = self.heat().stacks.snapshot_show(self.resource_id,
|
with TempCACertFile(self.cacert) as cacert_path:
|
||||||
snapshot_id)
|
snapshot = self.heat(
|
||||||
|
cacert_path).stacks.snapshot_show(self.resource_id,
|
||||||
|
snapshot_id)
|
||||||
s_data = snapshot['snapshot']['data']
|
s_data = snapshot['snapshot']['data']
|
||||||
env = environment.Environment(s_data['environment'])
|
env = environment.Environment(s_data['environment'])
|
||||||
files = s_data['files']
|
files = s_data['files']
|
||||||
@ -261,7 +353,8 @@ class RemoteStack(resource.Resource):
|
|||||||
return defn.freeze(properties=props)
|
return defn.freeze(properties=props)
|
||||||
|
|
||||||
def handle_check(self):
|
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,
|
def _needs_update(self, after, before, after_props, before_props,
|
||||||
prev_resource, check_init_complete=True):
|
prev_resource, check_init_complete=True):
|
||||||
@ -293,10 +386,13 @@ class RemoteStack(resource.Resource):
|
|||||||
'files': self.stack.t.files,
|
'files': self.stack.t.files,
|
||||||
'environment': env.user_env_as_dict(),
|
'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):
|
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:
|
if stack.action != action:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -346,7 +442,9 @@ class RemoteStack(resource.Resource):
|
|||||||
def _resolve_attribute(self, name):
|
def _resolve_attribute(self, name):
|
||||||
if self.resource_id is None:
|
if self.resource_id is None:
|
||||||
return
|
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:
|
if name == self.NAME_ATTR:
|
||||||
value = getattr(stack, name, None)
|
value = getattr(stack, name, None)
|
||||||
return value or self.physical_resource_name_or_FnGetRefId()
|
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.i18n import _
|
||||||
from heat.common import policy
|
from heat.common import policy
|
||||||
from heat.common import template_format
|
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.clients.os import heat_plugin
|
||||||
from heat.engine import environment
|
from heat.engine import environment
|
||||||
from heat.engine import node_data
|
from heat.engine import node_data
|
||||||
@ -305,23 +304,21 @@ class RemoteStackTest(tests_common.HeatTestCase):
|
|||||||
self.heat.stacks.create.assert_called_with(**args)
|
self.heat.stacks.create.assert_called_with(**args)
|
||||||
self.assertEqual(2, len(self.heat.stacks.get.call_args_list))
|
self.assertEqual(2, len(self.heat.stacks.get.call_args_list))
|
||||||
|
|
||||||
def _create_with_remote_credential(self, credential_secret_id=None):
|
def _create_with_remote_credential(self, credential_secret_id=None,
|
||||||
self.auth = (
|
ca_cert=None, insecure=False):
|
||||||
'{"auth_type": "v3applicationcredential", '
|
|
||||||
'"auth": {"auth_url": "http://192.168.1.101/identity/v3", '
|
|
||||||
'"application_credential_id": "9dfa187e5a354484bf9c49a2b674333a", '
|
|
||||||
'"application_credential_secret": "sec"} }')
|
|
||||||
|
|
||||||
t = template_format.parse(parent_stack_template)
|
t = template_format.parse(parent_stack_template)
|
||||||
properties = t['resources']['remote_stack']['properties']
|
properties = t['resources']['remote_stack']['properties']
|
||||||
if credential_secret_id:
|
if credential_secret_id:
|
||||||
properties['context']['credential_secret_id'] = (
|
properties['context']['credential_secret_id'] = (
|
||||||
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)
|
t = json.dumps(t)
|
||||||
self.patchobject(policy.Enforcer, 'check_is_admin')
|
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)
|
rsrc = self.create_remote_stack(stack_template=t)
|
||||||
env = environment.get_child_environment(rsrc.stack.env,
|
env = environment.get_child_environment(rsrc.stack.env,
|
||||||
@ -340,14 +337,22 @@ class RemoteStackTest(tests_common.HeatTestCase):
|
|||||||
rsrc.validate()
|
rsrc.validate()
|
||||||
return rsrc
|
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_plugin = mock.Mock()
|
||||||
self.m_loader = self.patchobject(
|
self.m_loader = self.patchobject(
|
||||||
ks_loading, 'get_plugin_loader', return_value=self.m_plugin)
|
ks_loading, 'get_plugin_loader', return_value=self.m_plugin)
|
||||||
self._create_with_remote_credential('cred_2')
|
self._create_with_remote_credential('cred_2')
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
[mock.call(secret_ref='secrets/cred_2')]*2,
|
[mock.call(secret_ref='secrets/cred_2')]*2,
|
||||||
self.m_gsbr.call_args_list)
|
m_gsbr.call_args_list)
|
||||||
expected_load_options = [
|
expected_load_options = [
|
||||||
mock.call(
|
mock.call(
|
||||||
application_credential_id='9dfa187e5a354484bf9c49a2b674333a',
|
application_credential_id='9dfa187e5a354484bf9c49a2b674333a',
|
||||||
@ -357,6 +362,19 @@ class RemoteStackTest(tests_common.HeatTestCase):
|
|||||||
self.assertEqual(expected_load_options,
|
self.assertEqual(expected_load_options,
|
||||||
self.m_plugin.load_from_options.call_args_list)
|
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):
|
def test_create_failed(self):
|
||||||
returns = [get_stack(stack_status='CREATE_IN_PROGRESS'),
|
returns = [get_stack(stack_status='CREATE_IN_PROGRESS'),
|
||||||
get_stack(stack_status='CREATE_FAILED',
|
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