Adding the KmipServer

This change adds the KmipServer, the front-end of the KMIP software
server. The KmipServer is in charge of loading configuration settings,
creating all major server components, and serving and managing client
connections. A KmipServerConfig tool is included to handle configuration
settings. Test cases for all new code are included.
This commit is contained in:
Peter 2016-03-30 14:23:31 -04:00
parent a3da0c6d46
commit 702ba77715
6 changed files with 1549 additions and 0 deletions

View File

@ -203,6 +203,22 @@ class PermissionDenied(KmipError):
)
class ConfigurationError(Exception):
"""
An error generated when a problem occurs with a client or server
configuration.
"""
pass
class NetworkingError(Exception):
"""
An error generated when a problem occurs with client or server networking
activity.
"""
pass
class InvalidKmipEncoding(Exception):
"""
An exception raised when processing invalid KMIP message encodings.

View File

@ -12,3 +12,10 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from kmip.services.server.server import KmipServer
__all__ = [
"KmipServer"
]

View File

@ -0,0 +1,226 @@
# Copyright (c) 2016 The Johns Hopkins University/Applied Physics Laboratory
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import logging
import os
import six
from six.moves import configparser
from kmip.core import exceptions
class KmipServerConfig(object):
"""
A configuration management tool for the KmipServer.
"""
def __init__(self):
"""
Create a KmipServerConfig object.
"""
self._logger = logging.getLogger('kmip.server.config')
self.settings = dict()
self._expected_settings = [
'hostname',
'port',
'certificate_path',
'key_path',
'ca_path',
'auth_suite'
]
def set_setting(self, setting, value):
"""
Set a specific setting value.
This will overwrite the current setting value for the specified
setting.
Args:
setting (string): The name of the setting to set (e.g.,
'certificate_path', 'hostname'). Required.
value (misc): The value of the setting to set. Type varies based
on setting. Required.
Raises:
ConfigurationError: Raised if the setting is not supported or if
the setting value is invalid.
"""
if setting not in self._expected_settings:
raise exceptions.ConfigurationError(
"Setting '{0}' is not supported.".format(setting)
)
if setting == 'hostname':
self._set_hostname(value)
elif setting == 'port':
self._set_port(value)
elif setting == 'certificate_path':
self._set_certificate_path(value)
elif setting == 'key_path':
self._set_key_path(value)
elif setting == 'ca_path':
self._set_ca_path(value)
else:
self._set_auth_suite(value)
def load_settings(self, path):
"""
Load configuration settings from the file pointed to by path.
This will overwrite all current setting values.
Args:
path (string): The path to the configuration file containing
the settings to load. Required.
Raises:
ConfigurationError: Raised if the path does not point to an
existing file or if a setting value is invalid.
"""
if not os.path.exists(path):
raise exceptions.ConfigurationError(
"The server configuration file ('{0}') could not be "
"located.".format(path)
)
self._logger.info(
"Loading server configuration settings from: {0}".format(path)
)
parser = configparser.SafeConfigParser()
parser.read(path)
self._parse_settings(parser)
def _parse_settings(self, parser):
if not parser.has_section('server'):
raise exceptions.ConfigurationError(
"The server configuration file does not have a 'server' "
"section."
)
settings = [x[0] for x in parser.items('server')]
for setting in settings:
if setting not in self._expected_settings:
raise exceptions.ConfigurationError(
"Setting '{0}' is not a supported setting. Please "
"remove it from the configuration file.".format(setting)
)
for setting in self._expected_settings:
if setting not in settings:
raise exceptions.ConfigurationError(
"Setting '{0}' is missing from the configuration "
"file.".format(setting)
)
if parser.has_option('server', 'hostname'):
self._set_hostname(parser.get('server', 'hostname'))
if parser.has_option('server', 'port'):
self._set_port(parser.getint('server', 'port'))
if parser.has_option('server', 'certificate_path'):
self._set_certificate_path(parser.get(
'server',
'certificate_path')
)
if parser.has_option('server', 'key_path'):
self._set_key_path(parser.get('server', 'key_path'))
if parser.has_option('server', 'ca_path'):
self._set_ca_path(parser.get('server', 'ca_path'))
if parser.has_option('server', 'auth_suite'):
self._set_auth_suite(parser.get('server', 'auth_suite'))
def _set_hostname(self, value):
if isinstance(value, six.string_types):
self.settings['hostname'] = value
else:
raise exceptions.ConfigurationError(
"The hostname value must be a string."
)
def _set_port(self, value):
if isinstance(value, six.integer_types):
if 0 < value < 65535:
self.settings['port'] = value
else:
raise exceptions.ConfigurationError(
"The port value must be an integer in the range 0 - 65535."
)
else:
raise exceptions.ConfigurationError(
"The port value must be an integer in the range 0 - 65535."
)
def _set_certificate_path(self, value):
if value is None:
self.settings['certificate_path'] = None
elif isinstance(value, six.string_types):
if os.path.exists(value):
self.settings['certificate_path'] = value
else:
raise exceptions.ConfigurationError(
"The certificate path value, if specified, must be a "
"valid string path to a certificate file."
)
else:
raise exceptions.ConfigurationError(
"The certificate path value, if specified, must be a valid "
"string path to a certificate file."
)
def _set_key_path(self, value):
if value is None:
self.settings['key_path'] = None
elif isinstance(value, six.string_types):
if os.path.exists(value):
self.settings['key_path'] = value
else:
raise exceptions.ConfigurationError(
"The key path value, if specified, must be a valid string "
"path to a certificate key file."
)
else:
raise exceptions.ConfigurationError(
"The key path value, if specified, must be a valid string "
"path to a certificate key file."
)
def _set_ca_path(self, value):
if value is None:
self.settings['ca_path'] = None
elif isinstance(value, six.string_types):
if os.path.exists(value):
self.settings['ca_path'] = value
else:
raise exceptions.ConfigurationError(
"The certificate authority (CA) path value, if "
"specified, must be a valid string path to a CA "
"certificate file."
)
else:
raise exceptions.ConfigurationError(
"The certificate authority (CA) path value, if specified, "
"must be a valid string path to a CA certificate file."
)
def _set_auth_suite(self, value):
auth_suites = ['Basic', 'TLS1.2']
if value not in auth_suites:
raise exceptions.ConfigurationError(
"The authentication suite must be one of the "
"following: Basic, TLS1.2"
)
else:
self.settings['auth_suite'] = value

View File

@ -0,0 +1,343 @@
# Copyright (c) 2015 The Johns Hopkins University/Applied Physics Laboratory
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import errno
import logging
import logging.handlers as handlers
import os
import signal
import socket
import ssl
import threading
from kmip.core import exceptions
from kmip.services import auth
from kmip.services.server import config
from kmip.services.server import engine
from kmip.services.server import session
class KmipServer(object):
"""
The public front-end for the entire KmipServer service.
The KmipServer manages the server configuration and oversees the creation
of KmipSessions for all successful client connections. It creates the
KmipEngine used to process all KMIP requests and is in charge of safely
shutting down all server components upon receiving a termination signal.
"""
def __init__(
self,
hostname=None,
port=None,
certificate_path=None,
key_path=None,
ca_path=None,
auth_suite=None,
config_path='/etc/pykmip/server.conf',
log_path='/var/log/pykmip/server.log'):
"""
Create a KmipServer.
Settings are loaded initially from the configuration file located at
config_path, if specified. All other configuration options listed
below, if specified, will override the settings loaded from the
configuration file.
A rotating file logger will be set up with the base log file located
at log_path. The server itself will handle rotating the log files as
the logs grow. The server process must have permission to read/write
to the specified log directory.
The main KmipEngine request processor is created here, along with all
information required to manage KMIP client connections and sessions.
Args:
hostname (string): The host address the server will be bound to
(e.g., '127.0.0.1'). Optional, defaults to None.
port (int): The port number the server will be bound to
(e.g., 5696). Optional, defaults to None.
certificate_path (string): The path to the server certificate file
(e.g., '/etc/pykmip/certs/server.crt'). Optional, defaults to
None.
key_path (string): The path to the server certificate key file
(e.g., '/etc/pykmip/certs/server.key'). Optional, defaults to
None.
ca_path (string): The path to the certificate authority (CA)
certificate file (e.g., '/etc/pykmip/certs/ca.crt'). Optional,
defaults to None.
auth_suite (string): A string value indicating the type of
authentication suite to use for establishing TLS connections.
Accepted values are: 'Basic', 'TLS1.2'. Optional, defaults to
None.
config_path (string): The path to the server configuration file
(e.g., '/etc/pykmip/server.conf'). Optional, defaults to
'/etc/pykmip/server.conf'.
log_path (string): The path to the base server log file
(e.g., '/var/log/pykmip/server.log'). Optional, defaults to
'/var/log/pykmip/server.log'.
"""
self._logger = logging.getLogger('kmip.server')
self._setup_logging(log_path)
self.config = config.KmipServerConfig()
self._setup_configuration(
config_path,
hostname,
port,
certificate_path,
key_path,
ca_path,
auth_suite
)
if self.config.settings.get('auth_suite') == 'TLS1.2':
self.auth_suite = auth.TLS12AuthenticationSuite()
else:
self.auth_suite = auth.BasicAuthenticationSuite()
self._engine = engine.KmipEngine()
self._session_id = 1
self._is_serving = False
def _setup_logging(self, path):
# Create the logging directory/file if it doesn't exist.
if not os.path.exists(path):
if not os.path.isdir(os.path.dirname(path)):
os.makedirs(os.path.dirname(path))
open(path, 'w').close()
handler = handlers.RotatingFileHandler(
path,
mode='a',
maxBytes=1000000,
backupCount=5
)
handler.setFormatter(
logging.Formatter(
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
)
self._logger.addHandler(handler)
self._logger.setLevel(logging.INFO)
def _setup_configuration(
self,
path=None,
hostname=None,
port=None,
certificate_path=None,
key_path=None,
ca_path=None,
auth_suite=None):
if path:
self.config.load_settings(path)
if hostname:
self.config.set_setting('hostname', hostname)
if port:
self.config.set_setting('port', port)
if certificate_path:
self.config.set_setting('certificate_path', certificate_path)
if key_path:
self.config.set_setting('key_path', key_path)
if ca_path:
self.config.set_setting('ca_path', ca_path)
if auth_suite:
self.config.set_setting('auth_suite', auth_suite)
def start(self):
"""
Prepare the server to start serving connections.
Configure the server socket handler and establish a TLS wrapping
socket from which all client connections descend. Bind this TLS
socket to the specified network address for the server.
Raises:
NetworkingError: Raised if the TLS socket cannot be bound to the
network address.
"""
self._logger.info("Starting server socket handler.")
# Create a TCP stream socket and configure it for immediate reuse.
self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self._socket = ssl.wrap_socket(
self._socket,
keyfile=self.config.settings.get('key_path'),
certfile=self.config.settings.get('certificate_path'),
server_side=True,
cert_reqs=ssl.CERT_REQUIRED,
ssl_version=self.auth_suite.protocol,
ca_certs=self.config.settings.get('ca_path'),
do_handshake_on_connect=True,
suppress_ragged_eofs=True,
ciphers=self.auth_suite.ciphers
)
try:
self._socket.bind(
(
self.config.settings.get('hostname'),
int(self.config.settings.get('port'))
)
)
except Exception as e:
self._logger.exception(e)
raise exceptions.NetworkingError(
"Server failed to bind socket handler to {0}:{1}".format(
self.config.settings.get('hostname'),
self.config.settings.get('port')
)
)
else:
self._logger.info(
"Server successfully bound socket handler to {0}:{1}".format(
self.config.settings.get('hostname'),
self.config.settings.get('port')
)
)
self._is_serving = True
def stop(self):
"""
Stop the server.
Halt server client connections and clean up any existing connection
threads.
Raises:
NetworkingError: Raised if a failure occurs while sutting down
or closing the TLS server socket.
"""
self._logger.info("Cleaning up remaining connection threads.")
for thread in threading.enumerate():
if thread is not threading.current_thread():
try:
thread.join(10.0)
except Exception as e:
self._logger.info(
"Error occurred while attempting to cleanup thread: "
"{0}".format(thread.name)
)
self._logger.exception(e)
else:
if thread.is_alive():
self._logger.warning(
"Cleanup failed for thread: {0}. Thread is "
"still alive".format(thread.name)
)
else:
self._logger.info(
"Cleanup succeeded for thread: {0}".format(
thread.name
)
)
self._logger.info("Shutting down server socket handler.")
try:
self._socket.shutdown(socket.SHUT_RDWR)
self._socket.close()
except Exception as e:
self._logger.exception(e)
raise exceptions.NetworkingError(
"Server failed to shutdown socket handler."
)
def serve(self):
"""
Serve client connections.
Begin listening for client connections, spinning off new KmipSessions
as connections are handled. Set up signal handling to shutdown
connection service as needed.
"""
self._socket.listen(5)
def _signal_handler(signal_number, stack_frame):
self._is_serving = False
signal.signal(signal.SIGINT, _signal_handler)
signal.signal(signal.SIGTERM, _signal_handler)
self._logger.info("Starting connection service...")
while self._is_serving:
try:
connection, address = self._socket.accept()
except socket.error as e:
if e.errno == errno.EINTR:
self._logger.warning("Interrupting connection service.")
else:
self._logger.warning(
"Error detected while establishing new connection."
)
self._logger.exception(e)
break
except Exception as e:
self._logger.warning(
"Error detected while establishing new connection."
)
self._logger.exception(e)
else:
self._setup_connection_handler(connection, address)
self._logger.info("Stopping connection service.")
def _setup_connection_handler(self, connection, address):
self._logger.info(
"Receiving incoming connection from: {0}:{1}".format(
address[0],
address[1]
)
)
session_name = "{0:08}".format(self._session_id)
self._session_id += 1
self._logger.info(
"Dedicating session {0} to {1}:{2}".format(
session_name,
address[0],
address[1]
)
)
try:
s = session.KmipSession(
self._engine,
connection,
name=session_name
)
s.daemon = True
s.start()
except Exception as e:
self._logger.warning(
"Failure occurred while starting session: {0}".format(
session_name
)
)
self._logger.exception(e)
def __enter__(self):
self.start()
return self
def __exit__(self, exc_type, exc_value, traceback):
self.stop()

View File

@ -0,0 +1,480 @@
# Copyright (c) 2016 The Johns Hopkins University/Applied Physics Laboratory
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import mock
from six.moves import configparser
import testtools
from kmip.core import exceptions
from kmip.services.server import config
class TestKmipServerConfig(testtools.TestCase):
"""
A test suite for the KmipServerConfig tool.
"""
def setUp(self):
super(TestKmipServerConfig, self).setUp()
def tearDown(self):
super(TestKmipServerConfig, self).tearDown()
def test_init(self):
"""
Test that a KmipServerConfig object can be created without error.
"""
config.KmipServerConfig()
def test_set_setting(self):
"""
Test that the right errors are raised and methods are called when
setting individual settings.
"""
c = config.KmipServerConfig()
c._set_auth_suite = mock.MagicMock()
c._set_ca_path = mock.MagicMock()
c._set_certificate_path = mock.MagicMock()
c._set_hostname = mock.MagicMock()
c._set_key_path = mock.MagicMock()
c._set_port = mock.MagicMock()
# Test the right error is generated when setting an unsupported
# setting.
args = ('invalid', None)
regex = "Setting 'invalid' is not supported."
self.assertRaisesRegexp(
exceptions.ConfigurationError,
regex,
c.set_setting,
*args
)
# Test the right methods are called when setting supported settings.
c.set_setting('hostname', '127.0.0.1')
c._set_hostname.assert_called_once_with('127.0.0.1')
c.set_setting('port', 5696)
c._set_port.assert_called_once_with(5696)
c.set_setting('certificate_path', '/etc/pykmip/certs/server.crt')
c._set_certificate_path.assert_called_once_with(
'/etc/pykmip/certs/server.crt'
)
c.set_setting('key_path', '/etc/pykmip/certs/server.key')
c._set_key_path.assert_called_once_with(
'/etc/pykmip/certs/server.key'
)
c.set_setting('ca_path', '/etc/pykmip/certs/ca.crt')
c._set_ca_path.assert_called_once_with('/etc/pykmip/certs/ca.crt')
c.set_setting('auth_suite', 'Basic')
c._set_auth_suite.assert_called_once_with('Basic')
def test_load_settings(self):
"""
Test that the right calls are made and the right errors generated when
loading configuration settings from a configuration file specified by
a path string.
"""
c = config.KmipServerConfig()
c._logger = mock.MagicMock()
c._parse_settings = mock.MagicMock()
# Test that the right calls are made when correctly processing the
# configuration file.
with mock.patch('os.path.exists') as os_mock:
os_mock.return_value = True
with mock.patch(
'six.moves.configparser.SafeConfigParser.read'
) as parser_mock:
c.load_settings("/test/path/server.conf")
c._logger.info.assert_any_call(
"Loading server configuration settings from: "
"/test/path/server.conf"
)
parser_mock.assert_called_with("/test/path/server.conf")
self.assertTrue(c._parse_settings.called)
# Test that a ConfigurationError is generated when the path is invalid.
c._logger.reset_mock()
with mock.patch('os.path.exists') as os_mock:
os_mock.return_value = False
args = ('/test/path/server.conf', )
self.assertRaises(
exceptions.ConfigurationError,
c.load_settings,
*args
)
def test_parse_settings(self):
"""
Test that the right methods are called and the right errors generated
when parsing the configuration settings.
"""
c = config.KmipServerConfig()
c._logger = mock.MagicMock()
c._set_auth_suite = mock.MagicMock()
c._set_ca_path = mock.MagicMock()
c._set_certificate_path = mock.MagicMock()
c._set_hostname = mock.MagicMock()
c._set_key_path = mock.MagicMock()
c._set_port = mock.MagicMock()
# Test that the right calls are made when correctly parsing settings.
parser = configparser.SafeConfigParser()
parser.add_section('server')
parser.set('server', 'hostname', '127.0.0.1')
parser.set('server', 'port', '5696')
parser.set('server', 'certificate_path', '/test/path/server.crt')
parser.set('server', 'key_path', '/test/path/server.key')
parser.set('server', 'ca_path', '/test/path/ca.crt')
parser.set('server', 'auth_suite', 'Basic')
c._parse_settings(parser)
c._set_hostname.assert_called_once_with('127.0.0.1')
c._set_port.assert_called_once_with(5696)
c._set_certificate_path.assert_called_once_with(
'/test/path/server.crt'
)
c._set_key_path.assert_called_once_with('/test/path/server.key')
c._set_ca_path.assert_called_once_with('/test/path/ca.crt')
c._set_auth_suite.assert_called_once_with('Basic')
# Test that a ConfigurationError is generated when the expected
# section is missing.
parser = configparser.SafeConfigParser()
args = (parser, )
regex = (
"The server configuration file does not have a 'server' section."
)
self.assertRaisesRegexp(
exceptions.ConfigurationError,
regex,
c._parse_settings,
*args
)
# Test that a ConfigurationError is generated when an unexpected
# setting is provided.
parser = configparser.SafeConfigParser()
parser.add_section('server')
parser.set('server', 'invalid', 'invalid')
args = (parser, )
regex = (
"Setting 'invalid' is not a supported setting. Please remove it "
"from the configuration file."
)
self.assertRaisesRegexp(
exceptions.ConfigurationError,
regex,
c._parse_settings,
*args
)
# Test that a ConfigurationError is generated when an expected
# setting is missing.
parser = configparser.SafeConfigParser()
parser.add_section('server')
args = (parser, )
regex = (
"Setting 'hostname' is missing from the configuration file."
)
self.assertRaisesRegexp(
exceptions.ConfigurationError,
regex,
c._parse_settings,
*args
)
# Test that a ConfigurationError is generated when an expected
# setting is missing.
parser = configparser.SafeConfigParser()
parser.add_section('server')
args = (parser, )
regex = (
"Setting 'hostname' is missing from the configuration file."
)
self.assertRaisesRegexp(
exceptions.ConfigurationError,
regex,
c._parse_settings,
*args
)
def test_set_hostname(self):
"""
Test that the hostname configuration property can be set correctly.
"""
c = config.KmipServerConfig()
c._logger = mock.MagicMock()
# Test that the setting is set correctly with a valid value.
c._set_hostname('127.0.0.1')
self.assertIn('hostname', c.settings.keys())
self.assertEqual('127.0.0.1', c.settings.get('hostname'))
# Test that a ConfigurationError is generated when setting the wrong
# value.
args = (0, )
regex = "The hostname value must be a string."
self.assertRaisesRegexp(
exceptions.ConfigurationError,
regex,
c._set_hostname,
*args
)
self.assertNotEqual(0, c.settings.get('hostname'))
def test_set_port(self):
"""
Test that the port configuration property can be set correctly.
"""
c = config.KmipServerConfig()
c._logger = mock.MagicMock()
# Test that the setting is set correctly with a valid value.
c._set_port(5696)
self.assertIn('port', c.settings.keys())
self.assertEqual(5696, c.settings.get('port'))
# Test that a ConfigurationError is generated when setting the wrong
# value.
args = ('invalid', )
regex = "The port value must be an integer in the range 0 - 65535."
self.assertRaisesRegexp(
exceptions.ConfigurationError,
regex,
c._set_port,
*args
)
self.assertNotEqual('invalid', c.settings.get('port'))
args = (65536, )
regex = "The port value must be an integer in the range 0 - 65535."
self.assertRaisesRegexp(
exceptions.ConfigurationError,
regex,
c._set_port,
*args
)
self.assertNotEqual(65536, c.settings.get('port'))
def test_set_certificate_path(self):
"""
Test that the certificate_path configuration property can be set
correctly.
"""
c = config.KmipServerConfig()
c._logger = mock.MagicMock()
self.assertNotIn('certificate_path', c.settings.keys())
# Test that the setting is set correctly with a valid value.
with mock.patch('os.path.exists') as os_mock:
os_mock.return_value = True
c._set_certificate_path('/test/path/server.crt')
self.assertIn('certificate_path', c.settings.keys())
self.assertEqual(
'/test/path/server.crt',
c.settings.get('certificate_path')
)
c._set_certificate_path(None)
self.assertIn('certificate_path', c.settings.keys())
self.assertEqual(None, c.settings.get('certificate_path'))
# Test that a ConfigurationError is generated when setting the wrong
# value.
c = config.KmipServerConfig()
c._logger = mock.MagicMock()
args = (0, )
regex = (
"The certificate path value, if specified, must be a valid "
"string path to a certificate file."
)
self.assertRaisesRegexp(
exceptions.ConfigurationError,
regex,
c._set_certificate_path,
*args
)
self.assertNotEqual(0, c.settings.get('certificate_path'))
args = ('/test/path/server.crt', )
regex = (
"The certificate path value, if specified, must be a valid "
"string path to a certificate file."
)
with mock.patch('os.path.exists') as os_mock:
os_mock.return_value = False
self.assertRaisesRegexp(
exceptions.ConfigurationError,
regex,
c._set_certificate_path,
*args
)
self.assertNotEqual(
'/test/path/server.crt',
c.settings.get('certificate_path')
)
def test_set_key_path(self):
"""
Test that the key_path configuration property can be set correctly.
"""
c = config.KmipServerConfig()
c._logger = mock.MagicMock()
self.assertNotIn('key_path', c.settings.keys())
# Test that the setting is set correctly with a valid value.
with mock.patch('os.path.exists') as os_mock:
os_mock.return_value = True
c._set_key_path('/test/path/server.key')
self.assertIn('key_path', c.settings.keys())
self.assertEqual(
'/test/path/server.key',
c.settings.get('key_path')
)
c._set_key_path(None)
self.assertIn('key_path', c.settings.keys())
self.assertEqual(None, c.settings.get('key_path'))
# Test that a ConfigurationError is generated when setting the wrong
# value.
c = config.KmipServerConfig()
c._logger = mock.MagicMock()
args = (0, )
regex = (
"The key path value, if specified, must be a valid string path "
"to a certificate key file."
)
self.assertRaisesRegexp(
exceptions.ConfigurationError,
regex,
c._set_key_path,
*args
)
self.assertNotEqual(0, c.settings.get('key_path'))
args = ('/test/path/server.key', )
regex = (
"The key path value, if specified, must be a valid string path "
"to a certificate key file."
)
with mock.patch('os.path.exists') as os_mock:
os_mock.return_value = False
self.assertRaisesRegexp(
exceptions.ConfigurationError,
regex,
c._set_key_path,
*args
)
self.assertNotEqual(
'/test/path/server.key',
c.settings.get('key_path')
)
def test_set_ca_path(self):
"""
Test that the ca_path configuration property can be set correctly.
"""
c = config.KmipServerConfig()
c._logger = mock.MagicMock()
self.assertNotIn('ca_path', c.settings.keys())
# Test that the setting is set correctly with a valid value.
with mock.patch('os.path.exists') as os_mock:
os_mock.return_value = True
c._set_ca_path('/test/path/ca.crt')
self.assertIn('ca_path', c.settings.keys())
self.assertEqual(
'/test/path/ca.crt',
c.settings.get('ca_path')
)
c._set_ca_path(None)
self.assertIn('ca_path', c.settings.keys())
self.assertEqual(None, c.settings.get('ca_path'))
# Test that a ConfigurationError is generated when setting the wrong
# value.
c._logger.reset_mock()
args = (0, )
self.assertRaises(
exceptions.ConfigurationError,
c._set_ca_path,
*args
)
self.assertNotEqual(0, c.settings.get('ca_path'))
args = ('/test/path/ca.crt', )
with mock.patch('os.path.exists') as os_mock:
os_mock.return_value = False
self.assertRaises(
exceptions.ConfigurationError,
c._set_ca_path,
*args
)
self.assertNotEqual(0, c.settings.get('ca_path'))
def test_set_auth_suite(self):
"""
Test that the auth_suite configuration property can be set correctly.
"""
c = config.KmipServerConfig()
c._logger = mock.MagicMock()
# Test that the setting is set correctly with a valid value.
c._set_auth_suite('Basic')
self.assertIn('auth_suite', c.settings.keys())
self.assertEqual('Basic', c.settings.get('auth_suite'))
c._set_auth_suite('TLS1.2')
self.assertIn('auth_suite', c.settings.keys())
self.assertEqual('TLS1.2', c.settings.get('auth_suite'))
# Test that a ConfigurationError is generated when setting the wrong
# value.
args = ('invalid', )
regex = (
"The authentication suite must be one of the following: "
"Basic, TLS1.2"
)
self.assertRaisesRegexp(
exceptions.ConfigurationError,
regex,
c._set_auth_suite,
*args
)
self.assertNotEqual('invalid', c.settings.get('auth_suite'))

View File

@ -0,0 +1,477 @@
# Copyright (c) 2016 The Johns Hopkins University/Applied Physics Laboratory
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import errno
import logging
import mock
import signal
import socket
import testtools
from kmip.core import exceptions
from kmip.services import auth
from kmip.services.server import server
class TestKmipServer(testtools.TestCase):
"""
A test suite for the KmipServer.
"""
def setUp(self):
super(TestKmipServer, self).setUp()
def tearDown(self):
super(TestKmipServer, self).tearDown()
@mock.patch('kmip.services.server.server.KmipServer._setup_logging')
@mock.patch('kmip.services.server.server.KmipServer._setup_configuration')
def test_init(self, config_mock, logging_mock):
"""
Test that a KmipServer can be instantiated without error.
"""
s = server.KmipServer()
self.assertTrue(config_mock.called)
self.assertTrue(logging_mock.called)
self.assertIsInstance(s.auth_suite, auth.BasicAuthenticationSuite)
self.assertIsNotNone(s._engine)
self.assertEqual(1, s._session_id)
self.assertFalse(s._is_serving)
@mock.patch('logging.getLogger', side_effect=mock.MagicMock())
@mock.patch('logging.handlers.RotatingFileHandler')
@mock.patch('kmip.services.server.server.KmipServer._setup_configuration')
@mock.patch('os.path.exists')
@mock.patch('os.path.isdir')
@mock.patch('os.makedirs')
def test_setup_logging(
self,
makedirs_mock,
isdir_mock,
path_mock,
config_mock,
handler_mock,
logging_mock):
"""
Verify that the server logger is setup correctly.
"""
path_mock.return_value = False
isdir_mock.return_value = False
open_mock = mock.mock_open()
# Dynamically mock out the built-in open function. Approach changes
# across Python versions.
try:
import io # NOQA
module = 'kmip.services.server.server'
except:
module = '__builtin__'
with mock.patch('{0}.open'.format(module), open_mock):
s = server.KmipServer(log_path='/test/path/server.log')
path_mock.assert_called_once_with('/test/path/server.log')
isdir_mock.assert_called_once_with('/test/path')
makedirs_mock.assert_called_once_with('/test/path')
open_mock.assert_called_once_with('/test/path/server.log', 'w')
self.assertTrue(s._logger.addHandler.called)
s._logger.setLevel.assert_called_once_with(logging.INFO)
@mock.patch('kmip.services.auth.TLS12AuthenticationSuite')
@mock.patch('kmip.services.server.server.KmipServer._setup_logging')
def test_setup_configuration(self, logging_mock, auth_mock):
"""
Test that the server setup configuration works without error.
"""
s = server.KmipServer(config_path=None)
s.config = mock.MagicMock()
# Test the right calls are made when reinvoking config setup
s._setup_configuration(
'/etc/pykmip/server.conf',
'127.0.0.1',
5696,
'/etc/pykmip/certs/server.crt',
'/etc/pykmip/certs/server.key',
'/etc/pykmip/certs/ca.crt',
'Basic'
)
s.config.load_settings.assert_called_with('/etc/pykmip/server.conf')
s.config.set_setting.assert_any_call('hostname', '127.0.0.1')
s.config.set_setting.assert_any_call('port', 5696)
s.config.set_setting.assert_any_call(
'certificate_path',
'/etc/pykmip/certs/server.crt'
)
s.config.set_setting.assert_any_call(
'key_path',
'/etc/pykmip/certs/server.key'
)
s.config.set_setting.assert_any_call(
'ca_path',
'/etc/pykmip/certs/ca.crt'
)
s.config.set_setting.assert_any_call('auth_suite', 'Basic')
# Test that an attempt is made to instantiate the TLS 1.2 auth suite
s = server.KmipServer(auth_suite='TLS1.2', config_path=None)
self.assertEqual('TLS1.2', s.config.settings.get('auth_suite'))
self.assertIsNotNone(s.auth_suite)
@mock.patch('kmip.services.server.server.KmipServer._setup_logging')
def test_start(self, logging_mock):
"""
Test that starting the KmipServer either runs as expected or generates
the expected error.
"""
a_mock = mock.MagicMock()
b_mock = mock.MagicMock()
s = server.KmipServer(
hostname='127.0.0.1',
port=5696,
config_path=None
)
s._logger = mock.MagicMock()
self.assertFalse(s._is_serving)
# Test that in ideal cases no errors are generated and the right
# log messages are.
with mock.patch('socket.socket') as socket_mock:
with mock.patch('ssl.wrap_socket') as ssl_mock:
socket_mock.return_value = a_mock
ssl_mock.return_value = b_mock
s.start()
s._logger.info.assert_any_call(
"Starting server socket handler."
)
socket_mock.assert_called_once_with(
socket.AF_INET,
socket.SOCK_STREAM
)
a_mock.setsockopt.assert_called_once_with(
socket.SOL_SOCKET,
socket.SO_REUSEADDR,
1
)
self.assertTrue(ssl_mock.called)
b_mock.bind.assert_called_once_with(('127.0.0.1', 5696))
s._logger.info.assert_called_with(
"Server successfully bound socket handler to "
"127.0.0.1:5696"
)
self.assertTrue(s._is_serving)
a_mock.reset_mock()
b_mock.reset_mock()
# Test that a NetworkingError is generated if the socket bind fails.
with mock.patch('socket.socket') as socket_mock:
with mock.patch('ssl.wrap_socket') as ssl_mock:
socket_mock.return_value = a_mock
ssl_mock.return_value = b_mock
test_exception = Exception()
b_mock.bind.side_effect = test_exception
regex = (
"Server failed to bind socket handler to 127.0.0.1:5696"
)
self.assertRaisesRegexp(
exceptions.NetworkingError,
regex,
s.start
)
s._logger.info.assert_any_call(
"Starting server socket handler."
)
s._logger.exception.assert_called_once_with(test_exception)
@mock.patch('kmip.services.server.server.KmipServer._setup_logging')
def test_stop(self, logging_mock):
"""
Test that the right calls and log messages are triggered while
cleaning up the server and any remaining sessions.
"""
s = server.KmipServer(
hostname='127.0.0.1',
port=5696,
config_path=None
)
s._logger = mock.MagicMock()
s._socket = mock.MagicMock()
# Test the expected behavior for a normal server stop sequence
thread_mock = mock.MagicMock()
thread_mock.join = mock.MagicMock()
thread_mock.is_alive = mock.MagicMock(return_value=False)
thread_mock.name = 'TestThread'
with mock.patch('threading.enumerate') as threading_mock:
threading_mock.return_value = [thread_mock]
s.stop()
s._logger.info.assert_any_call(
"Cleaning up remaining connection threads."
)
self.assertTrue(threading_mock.called)
thread_mock.join.assert_called_once_with(10.0)
s._logger.info.assert_any_call(
"Cleanup succeeded for thread: TestThread"
)
s._logger.info.assert_any_call(
"Shutting down server socket handler."
)
s._socket.shutdown.assert_called_once_with(socket.SHUT_RDWR)
s._socket.close.assert_called_once_with()
# Test the expected behavior when stopping multiple server session
# threads goes wrong
thread_mock.reset_mock()
test_exception = Exception()
thread_mock.join = mock.MagicMock(side_effect=test_exception)
s._logger.reset_mock()
s._socket.reset_mock()
with mock.patch('threading.enumerate') as threading_mock:
threading_mock.return_value = [thread_mock]
s.stop()
s._logger.info.assert_any_call(
"Cleaning up remaining connection threads."
)
self.assertTrue(threading_mock.called)
thread_mock.join.assert_called_once_with(10.0)
s._logger.info.assert_any_call(
"Error occurred while attempting to cleanup thread: TestThread"
)
s._logger.exception.assert_called_once_with(test_exception)
s._logger.info.assert_any_call(
"Shutting down server socket handler."
)
s._socket.shutdown.assert_called_once_with(socket.SHUT_RDWR)
s._socket.close.assert_called_once_with()
thread_mock.reset_mock()
test_exception = Exception()
thread_mock.join = mock.MagicMock()
thread_mock.is_alive = mock.MagicMock(return_value=True)
s._logger.reset_mock()
s._socket.reset_mock()
with mock.patch('threading.enumerate') as threading_mock:
threading_mock.return_value = [thread_mock]
s.stop()
s._logger.info.assert_any_call(
"Cleaning up remaining connection threads."
)
self.assertTrue(threading_mock.called)
thread_mock.join.assert_called_once_with(10.0)
s._logger.warning.assert_any_call(
"Cleanup failed for thread: TestThread. Thread is still alive"
)
s._logger.info.assert_any_call(
"Shutting down server socket handler."
)
s._socket.shutdown.assert_called_once_with(socket.SHUT_RDWR)
s._socket.close.assert_called_once_with()
# Test that the right errors and log messages are generated when
# stopping the server goes wrong
s._logger.reset_mock()
s._socket.reset_mock()
test_exception = Exception()
s._socket.close = mock.MagicMock(side_effect=test_exception)
regex = "Server failed to shutdown socket handler."
self.assertRaisesRegexp(
exceptions.NetworkingError,
regex,
s.stop
)
s._logger.info.assert_any_call(
"Cleaning up remaining connection threads."
)
s._logger.info.assert_any_call(
"Shutting down server socket handler."
)
s._socket.shutdown.assert_called_once_with(socket.SHUT_RDWR)
s._socket.close.assert_called_once_with()
s._logger.exception(test_exception)
@mock.patch('kmip.services.server.server.KmipServer._setup_logging')
def test_serve(self, logging_mock):
"""
Test that the right calls and log messages are triggered while
serving connections.
"""
s = server.KmipServer(
hostname='127.0.0.1',
port=5696,
config_path=None
)
s._is_serving = True
s._logger = mock.MagicMock()
s._socket = mock.MagicMock()
s._setup_connection_handler = mock.MagicMock()
expected_error = socket.error()
expected_error.errno = errno.EINTR
# Test the expected behavior for a normal server/interrupt sequence
s._socket.accept = mock.MagicMock(
side_effect=[('connection', 'address'), expected_error]
)
s.serve()
s._socket.listen.assert_called_once_with(5)
s._socket.accept.assert_any_call()
s._setup_connection_handler.assert_called_once_with(
'connection',
'address'
)
s._logger.warning.assert_called_with(
"Interrupting connection service."
)
s._logger.info.assert_called_with("Stopping connection service.")
# Test the behavior for an unexpected socket error.
unexpected_error = socket.error()
s._is_serving = True
s._logger.reset_mock()
s._socket.accept = mock.MagicMock(
side_effect=[unexpected_error]
)
s.serve()
s._socket.accept.assert_any_call()
s._logger.warning.assert_any_call(
"Error detected while establishing new connection."
)
s._logger.exception.assert_called_with(unexpected_error)
s._logger.info.assert_called_with("Stopping connection service.")
# Test the behavior for an unexpected error.
unexpected_error = Exception()
s._is_serving = True
s._logger.reset_mock()
s._socket.accept = mock.MagicMock(
side_effect=[unexpected_error, expected_error]
)
s.serve()
s._socket.accept.assert_any_call()
s._logger.warning.assert_any_call(
"Error detected while establishing new connection."
)
s._logger.exception.assert_called_with(unexpected_error)
s._logger.info.assert_called_with("Stopping connection service.")
# Test the signal handler for each expected signal
s._is_serving = True
handler = signal.getsignal(signal.SIGINT)
handler(None, None)
self.assertFalse(s._is_serving)
s._is_serving = True
handler = signal.getsignal(signal.SIGTERM)
handler(None, None)
self.assertFalse(s._is_serving)
@mock.patch('kmip.services.server.server.KmipServer._setup_logging')
def test_setup_connection_handler(self, logging_mock):
"""
Test that a KmipSession can be successfully created and spun off from
the KmipServer.
"""
s = server.KmipServer(
hostname='127.0.0.1',
port=5696,
config_path=None
)
s._logger = mock.MagicMock()
# Test that the right calls and log messages are made when
# starting a new session.
with mock.patch(
'kmip.services.server.session.KmipSession.start'
) as session_mock:
address = ('127.0.0.1', 5696)
s._setup_connection_handler(None, address)
s._logger.info.assert_any_call(
"Receiving incoming connection from: 127.0.0.1:5696"
)
s._logger.info.assert_any_call(
"Dedicating session 00000001 to 127.0.0.1:5696"
)
session_mock.assert_called_once_with()
self.assertEqual(2, s._session_id)
# Test that the right error messages are logged when the session
# fails to start.
test_exception = Exception()
with mock.patch(
'kmip.services.server.session.KmipSession.start',
side_effect=test_exception
) as session_mock:
address = ('127.0.0.1', 5696)
s._setup_connection_handler(None, address)
s._logger.info.assert_any_call(
"Receiving incoming connection from: 127.0.0.1:5696"
)
s._logger.info.assert_any_call(
"Dedicating session 00000001 to 127.0.0.1:5696"
)
session_mock.assert_called_once_with()
s._logger.warning.assert_called_once_with(
"Failure occurred while starting session: 00000002"
)
s._logger.exception.assert_called_once_with(test_exception)
self.assertEqual(3, s._session_id)
@mock.patch('kmip.services.server.server.KmipServer._setup_logging')
def test_as_context_manager(self, logging_mock):
"""
Test that the right methods are called when the KmipServer is used
as a context manager.
"""
s = server.KmipServer(
hostname='127.0.0.1',
port=5696,
config_path=None
)
s._logger = mock.MagicMock()
s.start = mock.MagicMock()
s.stop = mock.MagicMock()
with s:
pass
self.assertTrue(s.start.called)
self.assertTrue(s.stop.called)