From 2e6384a067929a185fed50fdcb7768f4aff4832b Mon Sep 17 00:00:00 2001 From: Peter Hamilton Date: Mon, 16 Apr 2018 11:03:32 -0400 Subject: [PATCH] Add server support for customizing the backend storage file This change updates the server, adding in support for customizing the backend storage file used to store all server data. The server currently uses a simple SQLite database for storage. Prior versions of the server kept this database file in /tmp, to emphasize the testing focus of the server. This change loosens that restriction, now allowing users to customize where the database file lives. A new configuration option, 'database_path', has been added that will override the default /tmp location for the database file. This value can also be passed in if invoking the server via script using the '-d' flag. --- kmip/services/server/config.py | 20 ++++++- kmip/services/server/engine.py | 11 +++- kmip/services/server/server.py | 31 ++++++++-- .../tests/unit/services/server/test_config.py | 59 +++++++++++++++++++ .../tests/unit/services/server/test_server.py | 7 ++- 5 files changed, 119 insertions(+), 9 deletions(-) diff --git a/kmip/services/server/config.py b/kmip/services/server/config.py index 383378f..ea29a1d 100644 --- a/kmip/services/server/config.py +++ b/kmip/services/server/config.py @@ -51,7 +51,8 @@ class KmipServerConfig(object): 'policy_path', 'enable_tls_client_auth', 'tls_cipher_suites', - 'logging_level' + 'logging_level', + 'database_path' ] def set_setting(self, setting, value): @@ -93,8 +94,10 @@ class KmipServerConfig(object): self._set_enable_tls_client_auth(value) elif setting == 'tls_cipher_suites': self._set_tls_cipher_suites(value) - else: + elif setting == 'logging_level': self._set_logging_level(value) + else: + self._set_database_path(value) def load_settings(self, path): """ @@ -179,6 +182,8 @@ class KmipServerConfig(object): self._set_logging_level( parser.get('server', 'logging_level') ) + if parser.has_option('server', 'database_path'): + self._set_database_path(parser.get('server', 'database_path')) def _set_hostname(self, value): if isinstance(value, six.string_types): @@ -334,3 +339,14 @@ class KmipServerConfig(object): "The logging level must be a string representing a valid " "logging level." ) + + def _set_database_path(self, value): + if not value: + self.settings['database_path'] = None + elif isinstance(value, six.string_types): + self.settings['database_path'] = value + else: + raise exceptions.ConfigurationError( + "The database path, if specified, must be a valid path to a " + "SQLite database file." + ) diff --git a/kmip/services/server/engine.py b/kmip/services/server/engine.py index fcda637..8c1e306 100644 --- a/kmip/services/server/engine.py +++ b/kmip/services/server/engine.py @@ -74,7 +74,7 @@ class KmipEngine(object): * Cryptographic usage mask enforcement per object type """ - def __init__(self, policies=None): + def __init__(self, policies=None, database_path=None): """ Create a KmipEngine. @@ -82,13 +82,20 @@ class KmipEngine(object): policy_path (string): The path to the filesystem directory containing PyKMIP server operation policy JSON files. Optional, defaults to None. + database_path (string): The path to the SQLite database file + used to store all server data. Optional, defaults to None. + If none, database path defaults to '/tmp/pykmip.database'. """ self._logger = logging.getLogger('kmip.server.engine') self._cryptography_engine = engine.CryptographyEngine() + self.database_path = 'sqlite:///{}'.format(database_path) + if not database_path: + self.database_path = 'sqlite:////tmp/pykmip.database' + self._data_store = sqlalchemy.create_engine( - 'sqlite:////tmp/pykmip.database', + self.database_path, echo=False, connect_args={'check_same_thread': False} ) diff --git a/kmip/services/server/server.py b/kmip/services/server/server.py index c99572a..6c0622e 100644 --- a/kmip/services/server/server.py +++ b/kmip/services/server/server.py @@ -45,6 +45,7 @@ class KmipServer(object): shutting down all server components upon receiving a termination signal. """ + # TODO (peter-hamilton) Move to using **kwargs for all server parameters. def __init__( self, hostname=None, @@ -59,7 +60,8 @@ class KmipServer(object): enable_tls_client_auth=None, tls_cipher_suites=None, logging_level=None, - live_policies=False + live_policies=False, + database_path=None ): """ Create a KmipServer. @@ -123,6 +125,8 @@ class KmipServer(object): policy directory should be actively monitored to autoload any policy changes while the server is running. Optional, defaults to False. + database_path (string): The path to the server's SQLite database + file. Optional, defaults to None. """ self._logger = logging.getLogger('kmip.server') self._setup_logging(log_path) @@ -139,7 +143,8 @@ class KmipServer(object): policy_path, enable_tls_client_auth, tls_cipher_suites, - logging_level + logging_level, + database_path ) self.live_policies = live_policies self.policies = {} @@ -188,7 +193,8 @@ class KmipServer(object): policy_path=None, enable_tls_client_auth=None, tls_cipher_suites=None, - logging_level=None + logging_level=None, + database_path=None ): if path: self.config.load_settings(path) @@ -219,6 +225,8 @@ class KmipServer(object): ) if logging_level: self.config.set_setting('logging_level', logging_level) + if database_path: + self.config.set_setting('database_path', database_path) def start(self): """ @@ -252,7 +260,8 @@ class KmipServer(object): self.policy_monitor.start() self._engine = engine.KmipEngine( - policies=self.policies + policies=self.policies, + database_path=self.config.settings.get('database_path') ) self._logger.info("Starting server socket handler.") @@ -604,6 +613,18 @@ def build_argument_parser(): "DEBUG, INFO). Optional, defaults to None." ) ) + parser.add_option( + "-d", + "--database_path", + action="store", + type="str", + default=None, + dest="database_path", + help=( + "A string representing a path to the server's SQLite database " + "file. Optional, defaults to None." + ), + ) return parser @@ -636,6 +657,8 @@ def main(args=None): kwargs['enable_tls_client_auth'] = False if opts.logging_level: kwargs['logging_level'] = opts.logging_level + if opts.database_path: + kwargs['database_path'] = opts.database_path kwargs['live_policies'] = True diff --git a/kmip/tests/unit/services/server/test_config.py b/kmip/tests/unit/services/server/test_config.py index fe9ea5d..0a5d804 100644 --- a/kmip/tests/unit/services/server/test_config.py +++ b/kmip/tests/unit/services/server/test_config.py @@ -68,6 +68,7 @@ class TestKmipServerConfig(testtools.TestCase): c._set_enable_tls_client_auth = mock.MagicMock() c._set_tls_cipher_suites = mock.MagicMock() c._set_logging_level = mock.MagicMock() + c._set_database_path = mock.MagicMock() # Test the right error is generated when setting an unsupported # setting. @@ -115,6 +116,9 @@ class TestKmipServerConfig(testtools.TestCase): c.set_setting('logging_level', 'WARNING') c._set_logging_level.assert_called_once_with('WARNING') + c.set_setting('database_path', '/var/pykmip/pykmip.db') + c._set_database_path.assert_called_once_with('/var/pykmip/pykmip.db') + def test_load_settings(self): """ Test that the right calls are made and the right errors generated when @@ -232,6 +236,7 @@ class TestKmipServerConfig(testtools.TestCase): c._set_enable_tls_client_auth = mock.MagicMock() c._set_tls_cipher_suites = mock.MagicMock() c._set_logging_level = mock.MagicMock() + c._set_database_path = mock.MagicMock() # Test that the right calls are made when correctly parsing settings. parser = configparser.SafeConfigParser() @@ -250,6 +255,7 @@ class TestKmipServerConfig(testtools.TestCase): "\n TLS_RSA_WITH_AES_256_CBC_SHA256" ) parser.set('server', 'logging_level', 'ERROR') + parser.set('server', 'database_path', '/var/pykmip/pykmip.db') c._parse_settings(parser) @@ -267,6 +273,7 @@ class TestKmipServerConfig(testtools.TestCase): "\n TLS_RSA_WITH_AES_256_CBC_SHA256" ) c._set_logging_level.assert_called_once_with('ERROR') + c._set_database_path.assert_called_once_with('/var/pykmip/pykmip.db') # Test that a ConfigurationError is generated when the expected # section is missing. @@ -819,3 +826,55 @@ class TestKmipServerConfig(testtools.TestCase): c._set_logging_level, *args ) + + def test_set_database_path(self): + """ + Test that the database_path configuration property can be set + correctly. + """ + c = config.KmipServerConfig() + c._logger = mock.MagicMock() + + self.assertNotIn('database_path', c.settings.keys()) + + with mock.patch('os.path.exists') as os_mock: + os_mock.return_value = True + c._set_database_path('/test/path/database.db') + + self.assertIn('database_path', c.settings.keys()) + self.assertEqual( + '/test/path/database.db', + c.settings.get('database_path') + ) + + def test_set_database_path_default(self): + """ + Test that the database_path configuration property can be set correctly + without specifying a value. + """ + c = config.KmipServerConfig() + c._logger = mock.MagicMock() + + self.assertNotIn('database_path', c.settings.keys()) + + c._set_database_path(None) + self.assertIn('database_path', c.settings.keys()) + self.assertEqual(None, c.settings.get('database_path')) + + def test_set_database_path_invalid_value(self): + """ + Test that the right error is raised when an invalid value is used to + set the database_path configuration property. + """ + c = config.KmipServerConfig() + c._logger = mock.MagicMock() + + self.assertNotIn('database_path', c.settings.keys()) + + args = (1, ) + self.assertRaises( + exceptions.ConfigurationError, + c._set_database_path, + *args + ) + self.assertNotEqual(1, c.settings.get('database_path')) diff --git a/kmip/tests/unit/services/server/test_server.py b/kmip/tests/unit/services/server/test_server.py index e576d06..d450e09 100644 --- a/kmip/tests/unit/services/server/test_server.py +++ b/kmip/tests/unit/services/server/test_server.py @@ -121,7 +121,8 @@ class TestKmipServer(testtools.TestCase): '/etc/pykmip/policies', False, 'TLS_RSA_WITH_AES_128_CBC_SHA,TLS_RSA_WITH_AES_256_CBC_SHA', - 'DEBUG' + 'DEBUG', + '/var/pykmip/pykmip.db' ) s.config.load_settings.assert_called_with('/etc/pykmip/server.conf') @@ -156,6 +157,10 @@ class TestKmipServer(testtools.TestCase): ] ) s.config.set_setting.assert_any_call('logging_level', 'DEBUG') + s.config.set_setting.assert_any_call( + 'database_path', + '/var/pykmip/pykmip.db' + ) # Test that an attempt is made to instantiate the TLS 1.2 auth suite s = server.KmipServer(