mirror of
https://github.com/OpenKMIP/PyKMIP.git
synced 2025-07-21 13:04:22 +02:00
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):
|
class InvalidKmipEncoding(Exception):
|
||||||
"""
|
"""
|
||||||
An exception raised when processing invalid KMIP message encodings.
|
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
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
from kmip.services.server.server import KmipServer
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"KmipServer"
|
||||||
|
]
|
||||||
|
226
kmip/services/server/config.py
Normal file
226
kmip/services/server/config.py
Normal 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
|
343
kmip/services/server/server.py
Normal file
343
kmip/services/server/server.py
Normal 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()
|
480
kmip/tests/unit/services/server/test_config.py
Normal file
480
kmip/tests/unit/services/server/test_config.py
Normal 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'))
|
477
kmip/tests/unit/services/server/test_server.py
Normal file
477
kmip/tests/unit/services/server/test_server.py
Normal 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)
|
Loading…
x
Reference in New Issue
Block a user