From b4644c47aee28214fe8f3dfb8f01b3698d35dcad Mon Sep 17 00:00:00 2001 From: Hadi Esiely Date: Wed, 25 Nov 2015 12:43:40 -0500 Subject: [PATCH] Server Failover Feature This feature enables the PyKMIP library to switch between KMIP service provider hosts in the event one of them is unavailable. To list more than than one host, include all necessary host IP addresses separated by commas in the "host" field in the pykmip.conf file. Signed-off-by: Hadi Esiely --- kmip/logconfig.ini | 2 +- kmip/pykmip.conf | 6 +- kmip/services/kmip_client.py | 56 +++++++++++++----- kmip/tests/unit/services/test_kmip_client.py | 60 ++++++++++++++++++++ 4 files changed, 107 insertions(+), 17 deletions(-) diff --git a/kmip/logconfig.ini b/kmip/logconfig.ini index 32cf501..190d62c 100644 --- a/kmip/logconfig.ini +++ b/kmip/logconfig.ini @@ -18,4 +18,4 @@ formatter=simpleFormatter args=(sys.stdout,) [formatter_simpleFormatter] -format=%(asctime)s - %(name)s - %(levelname)s - %(message)s \ No newline at end of file +format=%(asctime)s - %(name)s - %(levelname)s - %(message)s diff --git a/kmip/pykmip.conf b/kmip/pykmip.conf index 783f864..2dae375 100644 --- a/kmip/pykmip.conf +++ b/kmip/pykmip.conf @@ -5,7 +5,7 @@ keyfile=None certfile=None cert_reqs=CERT_REQUIRED ssl_version=PROTOCOL_SSLv23 -ca_certs=../demos/certs/server.crt +ca_certs=./demos/certs/server.crt do_handshake_on_connect=True suppress_ragged_eofs=True username=None @@ -15,8 +15,8 @@ timeout=30 [server] host=127.0.0.1 port=5696 -keyfile=../demos/certs/server.key -certfile=../demos/certs/server.crt +keyfile=./demos/certs/server.key +certfile=./demos/certs/server.crt cert_reqs=CERT_NONE ssl_version=PROTOCOL_SSLv23 ca_certs=None diff --git a/kmip/services/kmip_client.py b/kmip/services/kmip_client.py index 0cfbf9d..94f9e1e 100644 --- a/kmip/services/kmip_client.py +++ b/kmip/services/kmip_client.py @@ -76,7 +76,8 @@ CONFIG_FILE = os.path.normpath(os.path.join(FILE_PATH, '../kmipconfig.ini')) class KMIPProxy(KMIP): - def __init__(self, host=None, port=None, keyfile=None, certfile=None, + def __init__(self, host=None, port=None, keyfile=None, + certfile=None, cert_reqs=None, ssl_version=None, ca_certs=None, do_handshake_on_connect=None, suppress_ragged_eofs=None, @@ -193,7 +194,6 @@ class KMIPProxy(KMIP): self.is_authentication_suite_supported(authentication_suite)) def open(self): - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.logger.debug("KMIPProxy keyfile: {0}".format(self.keyfile)) self.logger.debug("KMIPProxy certfile: {0}".format(self.certfile)) @@ -209,6 +209,23 @@ class KMIPProxy(KMIP): self.logger.debug("KMIPProxy suppress_ragged_eofs: {0}".format( self.suppress_ragged_eofs)) + for host in self.host_list: + self.host = host + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self._create_socket(sock) + self.protocol = KMIPProtocol(self.socket) + try: + self.socket.connect((self.host, self.port)) + except Exception as e: + self.logger.error("An error occurred while connecting to " + "appliance " + self.host) + self.socket.close() + self.socket = None + else: + return + raise e + + def _create_socket(self, sock): self.socket = ssl.wrap_socket( sock, keyfile=self.keyfile, @@ -218,16 +235,8 @@ class KMIPProxy(KMIP): ca_certs=self.ca_certs, do_handshake_on_connect=self.do_handshake_on_connect, suppress_ragged_eofs=self.suppress_ragged_eofs) - self.protocol = KMIPProtocol(self.socket) - self.socket.settimeout(self.timeout) - try: - self.socket.connect((self.host, self.port)) - except socket.timeout as e: - self.logger.error("timeout occurred while connecting to appliance") - raise e - def __del__(self): # Close the socket properly, helpful in case close() is not called. self.close() @@ -881,9 +890,14 @@ class KMIPProxy(KMIP): username, password, timeout): conf = ConfigHelper() - self.host = conf.get_valid_value( + # TODO: set this to a host list + self.host_list_str = conf.get_valid_value( host, self.config, 'host', conf.DEFAULT_HOST) + self.host_list = self._build_host_list(self.host_list_str) + + self.host = self.host_list[0] + self.port = int(conf.get_valid_value( port, self.config, 'port', conf.DEFAULT_PORT)) @@ -922,11 +936,27 @@ class KMIPProxy(KMIP): self.password = conf.get_valid_value( password, self.config, 'password', conf.DEFAULT_PASSWORD) - self.timeout = conf.get_valid_value( - timeout, self.config, 'timeout', conf.DEFAULT_TIMEOUT) + self.timeout = int(conf.get_valid_value( + timeout, self.config, 'timeout', conf.DEFAULT_TIMEOUT)) if self.timeout < 0: self.logger.warning( "Negative timeout value specified, " "resetting to safe default of {0} seconds".format( conf.DEFAULT_TIMEOUT)) self.timeout = conf.DEFAULT_TIMEOUT + + def _build_host_list(self, host_list_str): + ''' + This internal function takes the host string from the config file + and turns it into a list + :return: LIST host list + ''' + + host_list = [] + if isinstance(host_list_str, str): + host_list = host_list_str.replace(' ', '').split(',') + else: + raise TypeError("Unrecognized variable type provided for host " + "list string. 'String' type expected but '" + + str(type(host_list_str)) + "' received") + return host_list diff --git a/kmip/tests/unit/services/test_kmip_client.py b/kmip/tests/unit/services/test_kmip_client.py index 082e10d..3f4cf48 100644 --- a/kmip/tests/unit/services/test_kmip_client.py +++ b/kmip/tests/unit/services/test_kmip_client.py @@ -61,6 +61,10 @@ from kmip.services.results import RekeyKeyPairResult import kmip.core.utils as utils +import mock + +import socket + class TestKMIPClient(TestCase): @@ -501,6 +505,62 @@ class TestKMIPClient(TestCase): self.assertEqual(uid, result.uid) self.assertEqual(names, result.names) + def test_host_list_import_string(self): + """ + This test verifies that the client can process a string with + multiple IP addresses specified in it. It also tests that + unnecessary spaces are ignored. + """ + + host_list_string = '127.0.0.1,127.0.0.3, 127.0.0.5' + host_list_expected = ['127.0.0.1', '127.0.0.3', '127.0.0.5'] + + self.client._set_variables(host=host_list_string, + port=None, keyfile=None, certfile=None, + cert_reqs=None, ssl_version=None, + ca_certs=None, + do_handshake_on_connect=False, + suppress_ragged_eofs=None, username=None, + password=None, timeout=None) + self.assertEqual(host_list_expected, self.client.host_list) + + def test_host_is_invalid_input(self): + """ + This test verifies that invalid values are not processed when + setting the client object parameters + """ + host = 1337 + expected_error = TypeError + + kwargs = {'host': host, 'port': None, 'keyfile': None, + 'certfile': None, 'cert_reqs': None, 'ssl_version': None, + 'ca_certs': None, 'do_handshake_on_connect': False, + 'suppress_ragged_eofs': None, 'username': None, + 'password': None, 'timeout': None} + + self.assertRaises(expected_error, self.client._set_variables, + **kwargs) + + @mock.patch('socket.socket.connect') + @mock.patch('ssl.SSLSocket.gettimeout') + def test_timeout_all_hosts(self, mock_ssl_timeout, mock_connect_return): + """ + This test verifies that the client will throw an exception if no + hosts are available for connection. + """ + + mock_ssl_timeout.return_value = 1 + mock_connect_return.return_value = socket.timeout + try: + self.client.open() + except Exception as e: + # TODO: once the exception is properly defined in the + # kmip_client.py file this test needs to change to reflect that. + self.assertIsInstance(e, Exception) + self.client.close() + else: + self.client.close() + class TestClientProfileInformation(TestCase): """