mirror of https://github.com/OpenKMIP/PyKMIP.git
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:
parent
a3da0c6d46
commit
702ba77715
|
@ -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.
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
|
|
|
@ -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
|
|
@ -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()
|
|
@ -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'))
|
|
@ -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)
|
Loading…
Reference in New Issue