diff --git a/charmhelpers/contrib/network/ip.py b/charmhelpers/contrib/network/ip.py index cf9926b95..4caab7e5c 100644 --- a/charmhelpers/contrib/network/ip.py +++ b/charmhelpers/contrib/network/ip.py @@ -16,6 +16,7 @@ import re import subprocess import socket +import ssl from functools import partial @@ -542,6 +543,30 @@ def port_has_listener(address, port): return not (bool(result)) +def port_has_listener_ssl(address, port, key, cert, ca_cert): + """ + Returns True if the address:port is open and being listened to, + else False. + + @param address: an IP address or hostname + @param port: integer port + @param: cert: path to cert + @param: key: path to key + @param: ca_cert: path to ca cert + """ + hostname = address + context = ssl.create_default_context() + context.check_hostname = False + context.load_cert_chain(cert, key) + context.load_verify_locations(ca_cert) + try: + with socket.create_connection((hostname, port)) as sock: + with context.wrap_socket(sock, server_hostname=hostname): + return True + except ConnectionRefusedError: + return False + + def assert_charm_supports_ipv6(): """Check whether we are able to support charms ipv6.""" release = lsb_release()['DISTRIB_CODENAME'].lower() diff --git a/charmhelpers/contrib/openstack/utils.py b/charmhelpers/contrib/openstack/utils.py index da711c65d..25fd5b0a0 100644 --- a/charmhelpers/contrib/openstack/utils.py +++ b/charmhelpers/contrib/openstack/utils.py @@ -80,6 +80,7 @@ get_ipv6_addr, is_ipv6, port_has_listener, + port_has_listener_ssl, ) from charmhelpers.core.host import ( @@ -1207,12 +1208,19 @@ def _ows_check_services_running(services, ports): return ows_check_services_running(services, ports) -def ows_check_services_running(services, ports): +def ows_check_services_running(services, ports, use_ssl_check=False, + ssl_check_info=None): """Check that the services that should be running are actually running and that any ports specified are being listened to. @param services: list of strings OR dictionary specifying services/ports @param ports: list of ports + @param use_ssl_check: Set to True if you want to use ssl to check + service ports rather than netcat. Default is + False. + @param ssl_check_info: If use_ssl_check is True this is a dict of + {key: , cert: } used + by ssl when checking ports using SSL. @returns state, message: strings or None, None """ messages = [] @@ -1228,7 +1236,9 @@ def ows_check_services_running(services, ports): # also verify that the ports that should be open are open # NB, that ServiceManager objects only OPTIONALLY have ports map_not_open, ports_open = ( - _check_listening_on_services_ports(services)) + _check_listening_on_services_ports(services, + use_ssl_check=use_ssl_check, + ssl_check_info=ssl_check_info)) if not all(ports_open): # find which service has missing ports. They are in service # order which makes it a bit easier. @@ -1243,7 +1253,10 @@ def ows_check_services_running(services, ports): if ports is not None: # and we can also check ports which we don't know the service for - ports_open, ports_open_bools = _check_listening_on_ports_list(ports) + ports_open, ports_open_bools = \ + _check_listening_on_ports_list(ports, + use_ssl_check=use_ssl_check, + ssl_check_info=ssl_check_info) if not all(ports_open_bools): messages.append( "Ports which should be open, but are not: {}" @@ -1302,7 +1315,9 @@ def _check_running_services(services): return list(zip(services, services_running)), services_running -def _check_listening_on_services_ports(services, test=False): +def _check_listening_on_services_ports(services, test=False, + use_ssl_check=False, + ssl_check_info=None): """Check that the unit is actually listening (has the port open) on the ports that the service specifies are open. If test is True then the function returns the services with ports that are open rather than @@ -1312,11 +1327,23 @@ def _check_listening_on_services_ports(services, test=False): @param services: OrderedDict(service: [port, ...], ...) @param test: default=False, if False, test for closed, otherwise open. + @param use_ssl_check: Set to True if you want to use ssl to check + service ports rather than netcat. Default is + False. + @param ssl_check_info: If use_ssl_check is True this is a dict of + {key: , cert: } used + by ssl when checking ports using SSL. @returns OrderedDict(service: [port-not-open, ...]...), [boolean] """ test = not (not (test)) # ensure test is True or False all_ports = list(itertools.chain(*services.values())) - ports_states = [port_has_listener('0.0.0.0', p) for p in all_ports] + if use_ssl_check: + def f_port_listener_check(*args): + return port_has_listener_ssl(*args, **ssl_check_info) + else: + f_port_listener_check = port_has_listener + + ports_states = [f_port_listener_check('0.0.0.0', p) for p in all_ports] map_ports = OrderedDict() matched_ports = [p for p, opened in zip(all_ports, ports_states) if opened == test] # essentially opened xor test @@ -1327,16 +1354,29 @@ def _check_listening_on_services_ports(services, test=False): return map_ports, ports_states -def _check_listening_on_ports_list(ports): +def _check_listening_on_ports_list(ports, use_ssl_check=False, + ssl_check_info=None): """Check that the ports list given are being listened to Returns a list of ports being listened to and a list of the booleans. + @param use_ssl_check: Set to True if you want to use ssl to check + service ports rather than netcat. Default is + False. + @param ssl_check_info: If use_ssl_check is True this is a dict of + {key: , cert: } used + by ssl when checking ports using SSL. @param ports: LIST of port numbers. @returns [(port_num, boolean), ...], [boolean] """ - ports_open = [port_has_listener('0.0.0.0', p) for p in ports] + if use_ssl_check: + def f_port_listener_check(*args): + return port_has_listener_ssl(*args, **ssl_check_info) + else: + f_port_listener_check = port_has_listener + + ports_open = [f_port_listener_check('0.0.0.0', p) for p in ports] return zip(ports, ports_open), ports_open diff --git a/tests/contrib/network/test_ip.py b/tests/contrib/network/test_ip.py index 606fc8a39..cf9cbe7ce 100644 --- a/tests/contrib/network/test_ip.py +++ b/tests/contrib/network/test_ip.py @@ -1,5 +1,6 @@ import subprocess import unittest +from contextlib import contextmanager import mock import netifaces @@ -784,6 +785,53 @@ def test_port_has_listener(self, subprocess_call): self.assertEqual(net_ip.port_has_listener('ip-address', 70), True) subprocess_call.assert_called_with(['nc', '-z', 'ip-address', '70']) + @patch('charmhelpers.contrib.network.ip.socket') + @patch('charmhelpers.contrib.network.ip.ssl') + def test_port_has_listener_ssl(self, mock_ssl, mock_socket): + ctxt = mock.MagicMock() + mock_ssl.create_default_context.return_value = ctxt + + @contextmanager + def mock_create_connection(*args, **kwargs): + for x in [1]: + yield x + + @contextmanager + def mock_wrap_socket(*args, **kwargs): + for x in [1]: + yield x + + ctxt.wrap_socket = mock_wrap_socket + mock_socket.create_connection = mock_create_connection + self.assertEqual(net_ip.port_has_listener_ssl('10.0.0.1', 50, + '/etc/ssl/key', + '/etc/ssl/cert', + '/etc/ssl/ca_cert'), + True) + + @patch('charmhelpers.contrib.network.ip.socket') + @patch('charmhelpers.contrib.network.ip.ssl') + def test_port_has_listener_ssl_false(self, mock_ssl, mock_socket): + ctxt = mock.MagicMock() + mock_ssl.create_default_context.return_value = ctxt + + @contextmanager + def mock_create_connection(*args, **kwargs): + raise ConnectionRefusedError + + @contextmanager + def mock_wrap_socket(*args, **kwargs): + for x in [1]: + yield x + + ctxt.wrap_socket = mock_wrap_socket + mock_socket.create_connection = mock_create_connection + self.assertEqual(net_ip.port_has_listener_ssl('10.0.0.1', 50, + '/etc/ssl/key', + '/etc/ssl/cert', + '/etc/ssl/ca_cert'), + False) + @patch.object(net_ip, 'log', lambda *args, **kwargs: None) @patch.object(net_ip, 'config') @patch.object(net_ip, 'network_get_primary_address')