Commit 96a0b656 authored by Stavros Sachtouris's avatar Stavros Sachtouris

Add ignore-ssl and ca-certs options to CLI

Refs #54

Both options are set either in the config file or as runtime
arguments.

ignore-ssl option allows connections without checking SSL certificates.

The ca-certs option provides a CA file for SSL authentication.

Normally, if no cert files are provided or they are invalid, kamaki
attempts to connect anyway, and an SSL Error ensues.

If ignore-ssl is set to "on", kamaki connects to servers regardless of
the existence or validity of a cert file and all SSL errors are ignored.

To help package maintainers to provide the correct CA file for their platform,
the default path is set in "kamaki/defaults.py" as CACERTS_DEFAULT_PATH
parent e70e95c2
......@@ -32,11 +32,47 @@ authentication URL is retrieved from the Synnefo Web UI and should be set as
the cloud URL for kamaki. Users of Synnefo clouds >=0.14 are advised against
using any service-specific URLs.
SSL Authentication
------------------
HTTPS connections are authenticated with SSL since version 0.13, as long as a
file of CA Certificates is provided. The file can be set with the
`ca_certs configuration option <#available-options>`_ or with the *- -ca-certs*
runtime argument. Packages for various operating systems are built with a
default value for the 'ca_certs' configuration option, which is specific for
each platform.
If the CA Certificates are not provided or fail to authenticate a particular
cloud, ``kamaki`` will exit with an SSL error and instructions.
Users have the option to ignore SSL authentication errors with the
`ignore_ssl configuration option <#available-options>`_ or the *- -ignore-ssl*
runtime argument and connect to the cloud insecurely.
To check the SSL settings on an installation:
.. code-block:: console
$ kamaki config get ca_certs
$ kamaki config get ignore_ssl
To set a CA certificates path:
.. code-block:: console
$ kamaki config set ca_certs CA_FILE
To connect to clouds even when SSL authentication fails:
.. code-block:: console
$ kamaki config set ignore_ssl on
Migrating configuration file to latest version
----------------------------------------------
Each new version of kamaki might demand some changes to the configuration file.
Kamaki features a mechanism of automatic migration of the configration file to
Kamaki features a mechanism of automatic migration of the configuration file to
the latest version, which involves heuristics for guessing and translating the
file.
......@@ -424,6 +460,12 @@ history and log files, log detail options and pithos-specific options.
* global.default_cloud <cloud name>
pick a cloud configuration as default. It must refer to an existing cloud.
* global.ca_certs <CA Certificates>
set the path of the file with the CA Certificates for SSL authentication
* global.ignore_ssl <on|off>
ignore / don't ignore SSL errors
* global.colors <on|off>
enable / disable colors in command line based uis. Requires ansicolors,
otherwise it is ignored
......
......@@ -45,7 +45,8 @@ from kamaki.cli.utils import (
from kamaki.cli.errors import CLIError, CLICmdSpecError
from kamaki.cli import logger
from kamaki.clients.astakos import CachedAstakosClient
from kamaki.clients import ClientError
from kamaki.clients import ClientError, KamakiSSLError
from kamaki.clients.utils import https
_debug = False
......@@ -212,7 +213,7 @@ def _check_config_version(cnf):
'For automatic conversion, rerun and say Y'])
def _init_session(arguments, is_non_API=False):
def _init_session(arguments, is_non_api=False):
"""
:returns: cloud name
"""
......@@ -224,9 +225,21 @@ def _init_session(arguments, is_non_API=False):
_setup_logging(_debug, _verbose)
if _help or is_non_API:
if _help or is_non_api:
return None
# Patch https for SSL Authentication
ca_file = arguments['ca_file'].value or _cnf.get('global', 'ca_certs')
ignore_ssl = arguments['ignore_ssl'].value or (
_cnf.get('global', 'ignore_ssl').lower() == 'on')
if ca_file:
https.patch_with_certs(ca_file)
else:
warn = red('WARNING: CA certifications path not set (insecure) ')
kloger.warning(warn)
https.patch_to_raise_ssl_errors(not ignore_ssl)
_check_config_version(_cnf.value)
_colors = _cnf.value.get('global', 'colors')
......@@ -252,7 +265,7 @@ def _init_session(arguments, is_non_API=False):
' kamaki config set default_cloud <cloud name>',
'To pick a cloud for the current session, use --cloud:',
' kamaki --cloud=<cloud name> ...'])
if not cloud in _cnf.value.keys('cloud'):
if cloud not in _cnf.value.keys('cloud'):
raise CLIError(
'No cloud%s is configured' % ((' "%s"' % cloud) if cloud else ''),
importance=3, details=[
......@@ -482,11 +495,11 @@ def set_command_params(parameters):
# CLI Choice:
def is_non_API(parser):
nonAPIs = ('history', 'config')
def is_non_api(parser):
non_apis = ('history', 'config')
for term in parser.unparsed:
if not term.startswith('-'):
if term in nonAPIs:
if term in non_apis:
return True
return False
return False
......@@ -503,11 +516,11 @@ def main(func):
except UnicodeDecodeError as ude:
raise CLIError(
'Invalid encoding in command', importance=3, details=[
'The invalid term is #%s (with "%s" being 0)' % (
i, exe),
'Its encoding is invalid with current locale settings '
'(%s)' % pref_enc,
'( %s )' % ude])
'The invalid term is #%s (with "%s" being 0)' % (
i, exe),
'Encoding is invalid with current locale settings '
'(%s)' % pref_enc,
'( %s )' % ude])
for i, a in enumerate(internal_argv):
argv[i] = a
......@@ -524,7 +537,12 @@ def main(func):
'Print current version', ('-V', '--version')),
options=RuntimeConfigArgument(
_config_arg,
'Override a config value', ('-o', '--options')))
'Override a config value', ('-o', '--options')),
ignore_ssl=FlagArgument(
'Allow connections to SSL sites without certs',
('-k', '--ignore-ssl', '--insecure')),
ca_file=ValueArgument(
'CA certificates for SSL authentication', '--ca-certs'),)
)
if parser.arguments['version'].value:
exit(0)
......@@ -546,6 +564,31 @@ def main(func):
if _debug:
raise err
exit(1)
except KamakiSSLError as err:
ca_arg = parser.arguments.get('ca_file')
ca = ca_arg.value if ca_arg and ca_arg.value else _cnf.get(
'global', 'ca_certs')
stderr.write(red('SSL Authentication failed\n'))
if ca:
stderr.write('Path used for CA certifications file: %s\n' % ca)
stderr.write('Please make sure the path is correct\n')
if not (ca_arg and ca_arg.value):
stderr.write('| To set the correct path:\n')
stderr.write('| kamaki config set ca_certs CA_FILE\n')
else:
stderr.write('| To use a CA certifications file:\n')
stderr.write('| kamaki config set ca_certs CA_FILE\n')
stderr.write('| OR run with --ca-certs=FILE_LOCATION\n')
stderr.write('| To ignore SSL errors and move on (%s):\n' % (
red('insecure')))
stderr.write('| kamaki config set ignore_ssl on\n')
stderr.write('| OR run with --ignore-ssl\n')
stderr.flush()
if _debug:
raise
stderr.write('| %s: %s\n' % (type(err), err))
stderr.flush()
exit(1)
except KeyboardInterrupt:
print('Canceled by user')
exit(1)
......@@ -576,7 +619,7 @@ def run_shell(exe, parser):
@main
def run_one_cmd(exe, parser):
cloud = _init_session(parser.arguments, is_non_API(parser))
cloud = _init_session(parser.arguments, is_non_api(parser))
if parser.unparsed:
global _history
cnf = parser.arguments['config']
......
......@@ -62,6 +62,14 @@ CLOUD_PREFIX = 'cloud'
# Name of a shell variable to bypass the CONFIG_PATH value
CONFIG_ENV = 'KAMAKI_CONFIG'
# Get default CA Certifications file path - created while packaging
try:
from kamaki import defaults
CACERTS_DEFAULT_PATH = getattr(defaults, 'CACERTS_DEFAULT_PATH', '')
except ImportError as ie:
log.debug('ImportError while loading default certs: %s' % ie)
CACERTS_DEFAULT_PATH = ''
version = ''
for c in '%s' % __version__:
if c not in '0.123456789':
......@@ -99,26 +107,28 @@ DEFAULTS = {
'image_cli': 'image',
'imagecompute_cli': 'image',
'config_cli': 'config',
'history_cli': 'history'
'history_cli': 'history',
'ignore_ssl': 'off',
'ca_certs': CACERTS_DEFAULT_PATH,
# Optional command specs:
# 'service_cli': 'astakos'
# 'endpoint_cli': 'astakos'
# 'commission_cli': 'astakos'
},
CLOUD_PREFIX: {
#'default': {
# 'url': '',
# 'token': ''
# 'pithos_container': 'THIS IS DANGEROUS'
# 'pithos_type': 'object-store',
# 'pithos_version': 'v1',
# 'cyclades_type': 'compute',
# 'cyclades_version': 'v2.0',
# 'plankton_type': 'image',
# 'plankton_version': '',
# 'astakos_type': 'identity',
# 'astakos_version': 'v2.0'
#}
# 'default': {
# 'url': '',
# 'token': ''
# 'pithos_container': 'THIS IS DANGEROUS'
# 'pithos_type': 'object-store',
# 'pithos_version': 'v1',
# 'cyclades_type': 'compute',
# 'cyclades_version': 'v2.0',
# 'plankton_type': 'image',
# 'plankton_version': '',
# 'astakos_type': 'identity',
# 'astakos_version': 'v2.0'
# }
}
}
......@@ -192,22 +202,23 @@ class Config(RawConfigParser):
except KeyError:
cval = ''
if gval and cval and (
gval.lower().strip('/') != cval.lower().strip('/')):
raise CLISyntaxError(
'Conflicting values for default %s' % (term),
importance=2, details=[
' global.%s: %s' % (term, gval),
' %s.%s.%s: %s' % (
CLOUD_PREFIX,
default_cloud,
term,
cval),
'Please remove one of them manually:',
' /config delete global.%s' % term,
' or'
' /config delete %s.%s.%s' % (
CLOUD_PREFIX, default_cloud, term),
'and try again'])
gval.lower().strip('/') != cval.lower().strip('/')
):
raise CLISyntaxError(
'Conflicting values for default %s' % (term),
importance=2, details=[
' global.%s: %s' % (term, gval),
' %s.%s.%s: %s' % (
CLOUD_PREFIX,
default_cloud,
term,
cval),
'Please remove one of them manually:',
' /config delete global.%s' % term,
' or'
' /config delete %s.%s.%s' % (
CLOUD_PREFIX, default_cloud, term),
'and try again'])
elif gval:
err.write(u'... rescue %s.%s => %s.%s.%s\n' % (
s, term, CLOUD_PREFIX, default_cloud, term))
......
......@@ -41,7 +41,6 @@ from kamaki.cli.errors import CLIUnknownCommand, CLIError
def run(cloud, parser):
group = get_command_group(list(parser.unparsed), parser.arguments)
if not group:
#parser.parser.print_help()
parser.print_help()
_groups_help(parser.arguments)
exit(0)
......@@ -100,8 +99,8 @@ def run(cloud, parser):
astakos, help_message = init_cached_authenticator(_cnf, cloud, kloger) if (
cloud) else (None, [])
if not astakos:
from kamaki.cli import is_non_API
if not is_non_API(parser):
from kamaki.cli import is_non_api
if not is_non_api(parser):
raise CLIError(
'Failed to initialize an identity client',
importance=3, details=help_message)
......
......@@ -174,6 +174,8 @@ class Shell(Cmd):
tmp_args.pop('debug', None)
tmp_args.pop('verbose', None)
tmp_args.pop('config', None)
tmp_args.pop('ignore_ssl', None)
tmp_args.pop('ca_file', None)
help_parser = ArgumentParseManager(
cmd_name, tmp_args, required,
syntax=syntax, description=descr, check_required=False)
......
......@@ -39,6 +39,7 @@ from httplib import ResponseNotReady, HTTPException
from time import sleep
from random import random
from logging import getLogger
import ssl
from kamaki.clients.utils import https
......@@ -93,6 +94,10 @@ class ClientError(Exception):
self.details = details if details else []
class KamakiSSLError(ClientError):
"""SSL Connection Error"""
class Logged(object):
LOG_TOKEN = False
......@@ -177,20 +182,23 @@ class RequestManager(Logged):
"""
self._encode_headers()
self.dump_log()
conn.request(
method=self.method.upper(),
url=self.path.encode('utf-8'),
headers=self.headers,
body=self.data)
sendlog.info('')
keep_trying = TIMEOUT
while keep_trying > 0:
try:
return conn.getresponse()
except ResponseNotReady:
wait = 0.03 * random()
sleep(wait)
keep_trying -= wait
try:
conn.request(
method=self.method.upper(),
url=self.path.encode('utf-8'),
headers=self.headers,
body=self.data)
sendlog.info('')
keep_trying = TIMEOUT
while keep_trying > 0:
try:
return conn.getresponse()
except ResponseNotReady:
wait = 0.03 * random()
sleep(wait)
keep_trying -= wait
except ssl.SSLError as ssle:
raise KamakiSSLError('SSL Connection error (%s)' % ssle)
plog = ('\t[%s]' % self) if self.LOG_PID else ''
logmsg = 'Kamaki Timeout %s %s%s' % (self.method, self.path, plog)
recvlog.debug(logmsg)
......
......@@ -33,10 +33,13 @@
from logging import getLogger
import inspect
import ssl
from astakosclient import AstakosClientException, parse_endpoints
import astakosclient
from kamaki.clients import Client, ClientError, RequestManager, recvlog
from kamaki.clients import (
Client, ClientError, KamakiSSLError, RequestManager, recvlog)
from kamaki.clients.utils import https
......@@ -53,6 +56,8 @@ def _astakos_error(foo):
try:
return foo(self, *args, **kwargs)
except AstakosClientException as sace:
if isinstance(getattr(sace, 'errobject', None), ssl.SSLError):
raise KamakiSSLError('SSL Connection error (%s)' % sace)
raise AstakosClientError(
getattr(sace, 'message', '%s' % sace),
details=sace.details, status=sace.status)
......
# Copyright 2014 GRNET S.A. All rights reserved.
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
#
# 1. Redistributions of source code must retain the above
# copyright notice, this list of conditions and the following
# disclaimer.
#
# 2. Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials
# provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# The views and conclusions contained in the software and
# documentation are those of the authors and should not be
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.
# ABOUT THIS FILE
# Set default values for various settings for use in the code. For example, set
# OS-specific settings when packaging kamaki for a platform.
# The default path for the CA certifications file of the host system
CACERTS_DEFAULT_PATH = ''
# To overwrite any of the above, append new assignments bellow
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment