diff --git a/charmhelpers/contrib/openstack/context.py b/charmhelpers/contrib/openstack/context.py index b25612cbc..0afc510f4 100644 --- a/charmhelpers/contrib/openstack/context.py +++ b/charmhelpers/contrib/openstack/context.py @@ -1059,6 +1059,9 @@ def __call__(self): if config('haproxy-connect-timeout'): ctxt['haproxy_connect_timeout'] = config('haproxy-connect-timeout') + if config('haproxy-enable-proxy-protocol'): + ctxt['haproxy_enable_proxy_protocol'] = True + if config('prefer-ipv6'): ctxt['local_host'] = 'ip6-localhost' ctxt['haproxy_host'] = '::' @@ -1150,7 +1153,8 @@ class ApacheSSLContext(OSContextGenerator): user = group = 'root' def enable_modules(self): - cmd = ['a2enmod', 'ssl', 'proxy', 'proxy_http', 'headers'] + # NOTE(seyeongkim) : remoteip is for proxy protocol + cmd = ['a2enmod', 'ssl', 'proxy', 'proxy_http', 'headers', 'remoteip'] check_call(cmd) def configure_cert(self, cn=None): @@ -1283,6 +1287,8 @@ def __call__(self): ctxt['ext_ports'].append(int(ext_port)) ctxt['ext_ports'] = sorted(list(set(ctxt['ext_ports']))) + if config('haproxy-enable-proxy-protocol'): + ctxt['haproxy_enable_proxy_protocol'] = True return ctxt @@ -1787,11 +1793,19 @@ def __init__(self, name=None, script=None, admin_script=None, self.admin_process_weight = admin_process_weight self.public_process_weight = public_process_weight + def enable_modules(self): + # NOTE(seyeongkim): this is for proxy protocol and non ssl case. + cmd = ['a2enmod', 'remoteip'] + check_call(cmd) + def __call__(self): total_processes = _calculate_workers() enable_wsgi_socket_rotation = config('wsgi-socket-rotation') if enable_wsgi_socket_rotation is None: enable_wsgi_socket_rotation = True + + self.enable_modules() + ctxt = { "service_name": self.service_name, "user": self.user, @@ -1807,6 +1821,8 @@ def __call__(self): "threads": 1, "wsgi_socket_rotation": enable_wsgi_socket_rotation, } + if config('haproxy-enable-proxy-protocol'): + ctxt['haproxy_enable_proxy_protocol'] = True return ctxt diff --git a/charmhelpers/contrib/openstack/templates/haproxy.cfg b/charmhelpers/contrib/openstack/templates/haproxy.cfg index da2522f64..44cedd35f 100644 --- a/charmhelpers/contrib/openstack/templates/haproxy.cfg +++ b/charmhelpers/contrib/openstack/templates/haproxy.cfg @@ -87,11 +87,19 @@ backend {{ service }}_{{ frontend }} {% endif -%} {% endif -%} {% for unit, address in frontends[frontend]['backends'].items() -%} + {% if haproxy_enable_proxy_protocol -%} + {% if https -%} + server {{ unit }} {{ address }}:{{ ports[1] }} check send-proxy check-send-proxy check-ssl verify none + {% else -%} + server {{ unit }} {{ address }}:{{ ports[1] }} check send-proxy check-send-proxy + {% endif -%} + {% else -%} {% if https -%} server {{ unit }} {{ address }}:{{ ports[1] }} check check-ssl verify none {% else -%} server {{ unit }} {{ address }}:{{ ports[1] }} check {% endif -%} + {% endif -%} {% endfor %} {% endfor -%} {% endfor -%} diff --git a/charmhelpers/contrib/openstack/templates/openstack_https_frontend b/charmhelpers/contrib/openstack/templates/openstack_https_frontend index 6ed869a58..7991d56dc 100644 --- a/charmhelpers/contrib/openstack/templates/openstack_https_frontend +++ b/charmhelpers/contrib/openstack/templates/openstack_https_frontend @@ -22,6 +22,10 @@ Listen {{ ext_port }} ProxyPassReverse / http://localhost:{{ int }}/ ProxyPreserveHost on RequestHeader set X-Forwarded-Proto "https" + {% if haproxy_enable_proxy_protocol -%} + RemoteIPProxyProtocol On + RemoteIPProxyProtocolExceptions 127.0.0.1 ::1 + {% endif -%} KeepAliveTimeout 75 MaxKeepAliveRequests 1000 diff --git a/charmhelpers/contrib/openstack/templates/wsgi-openstack-api.conf b/charmhelpers/contrib/openstack/templates/wsgi-openstack-api.conf index de5f603fc..660973e1b 100644 --- a/charmhelpers/contrib/openstack/templates/wsgi-openstack-api.conf +++ b/charmhelpers/contrib/openstack/templates/wsgi-openstack-api.conf @@ -33,6 +33,10 @@ WSGISocketRotation Off ErrorLog /var/log/apache2/{{ service_name }}_error.log CustomLog /var/log/apache2/{{ service_name }}_access.log combined + {% if haproxy_enable_proxy_protocol -%} + RemoteIPProxyProtocol On + RemoteIPProxyProtocolExceptions 127.0.0.1 ::1 + {% endif -%} = 2.4> @@ -61,6 +65,10 @@ WSGISocketRotation Off ErrorLog /var/log/apache2/{{ service_name }}_error.log CustomLog /var/log/apache2/{{ service_name }}_access.log combined + {% if haproxy_enable_proxy_protocol -%} + RemoteIPProxyProtocol On + RemoteIPProxyProtocolExceptions 127.0.0.1 ::1 + {% endif -%} = 2.4> @@ -89,6 +97,10 @@ WSGISocketRotation Off ErrorLog /var/log/apache2/{{ service_name }}_error.log CustomLog /var/log/apache2/{{ service_name }}_access.log combined + {% if haproxy_enable_proxy_protocol -%} + RemoteIPProxyProtocol On + RemoteIPProxyProtocolExceptions 127.0.0.1 ::1 + {% endif -%} = 2.4> diff --git a/tests/contrib/openstack/test_os_contexts.py b/tests/contrib/openstack/test_os_contexts.py index 32df0277d..732cc0ba2 100644 --- a/tests/contrib/openstack/test_os_contexts.py +++ b/tests/contrib/openstack/test_os_contexts.py @@ -361,6 +361,10 @@ def relation_units(self, relation_id): 'haproxy-client-timeout': 50000, } +PROXY_PROTOCOL_CONFIG = { + 'haproxy-enable-proxy-protocol': True, +} + CEPH_RELATION = { 'ceph:0': { 'ceph/0': { @@ -2972,6 +2976,64 @@ def test_haproxy_context_with_prometheus_exporter(self, local_unit, local_addres call('cluster'), call('haproxy-exporter')]) + @patch('charmhelpers.contrib.openstack.context.local_address') + @patch('charmhelpers.contrib.openstack.context.local_unit') + def test_haproxy_context_with_data_proxy_protocol(self, local_unit, local_address): + '''Test haproxy context with proxy protocol enabled''' + cluster_relation = { + 'cluster:0': { + 'peer/1': { + 'private-address': 'cluster-peer1.localnet', + }, + 'peer/2': { + 'private-address': 'cluster-peer2.localnet', + }, + }, + } + local_unit.return_value = 'peer/0' + self.get_relation_ip.side_effect = [None, None, None, + 'cluster-peer0.localnet'] + relation = FakeRelation(cluster_relation) + self.relation_ids.side_effect = relation.relation_ids + self.relation_get.side_effect = relation.get + self.related_units.side_effect = relation.relation_units + self.get_netmask_for_address.return_value = '255.255.0.0' + self.config.return_value = False + self.maxDiff = None + self.config.side_effect = fake_config(PROXY_PROTOCOL_CONFIG) + self.is_ipv6_disabled.return_value = True + haproxy = context.HAProxyContext() + with patch_open() as (_open, _file): + result = haproxy() + ex = { + 'frontends': { + 'cluster-peer0.localnet': { + 'network': 'cluster-peer0.localnet/255.255.0.0', + 'backends': collections.OrderedDict([ + ('peer-0', 'cluster-peer0.localnet'), + ('peer-1', 'cluster-peer1.localnet'), + ('peer-2', 'cluster-peer2.localnet'), + ]), + } + }, + 'default_backend': 'cluster-peer0.localnet', + 'local_host': '127.0.0.1', + 'haproxy_host': '0.0.0.0', + 'ipv6_enabled': False, + 'stat_password': 'testpassword', + 'stat_port': '8888', + 'haproxy_enable_proxy_protocol': True, + } + # Verify the context is generated with proxy protocol enabled. + self.assertEqual(ex, result) + # Verify /etc/default/haproxy is updated. + self.assertEqual(_file.write.call_args_list, + [call('ENABLED=1\n')]) + self.get_relation_ip.assert_has_calls([call('admin', None), + call('internal', None), + call('public', None), + call('cluster')]) + def test_https_context_with_no_https(self): '''Test apache2 https when no https data available''' apache = context.ApacheSSLContext() @@ -3003,6 +3065,8 @@ def _https_context_setup(self): ('10.5.2.100', '10.5.2.1'), ('10.5.3.100', '10.5.3.1'), ] + config = {'haproxy-enable-proxy-protocol': False} + self.config.side_effect = lambda key: config[key] apache.external_ports = '8776' apache.service_namespace = 'cinder' @@ -3075,7 +3139,7 @@ def test_https_context_loads_correct_apache_mods(self): # Test apache2 context also loads required apache modules apache = context.ApacheSSLContext() apache.enable_modules() - ex_cmd = ['a2enmod', 'ssl', 'proxy', 'proxy_http', 'headers'] + ex_cmd = ['a2enmod', 'ssl', 'proxy', 'proxy_http', 'headers', 'remoteip'] self.check_call.assert_called_with(ex_cmd) def test_https_configure_cert(self): @@ -3699,6 +3763,43 @@ def test_wsgi_worker_config_context_user_and_group(self, } self.assertEqual(expect, ctxt()) + def test_wsgi_worker_config_context_apache_mods(self): + # Test apache2 context also loads required apache modules + apache = context.WSGIWorkerConfigContext() + apache.enable_modules() + ex_cmd = ['a2enmod', 'remoteip'] + self.check_call.assert_called_with(ex_cmd) + + @patch.object(context, '_calculate_workers') + def test_wsgi_worker_config_context_proxy_protocol(self, + _calculate_workers): + # Test WSGI worker config context with proxy protocol enabled + self.config.side_effect = fake_config({ + 'worker-multiplier': 2, + 'non-defined-wsgi-socket-rotation': True, + 'haproxy-enable-proxy-protocol': True + }) + _calculate_workers.return_value = 8 + service_name = 'service-name' + script = '/usr/bin/script' + ctxt = context.WSGIWorkerConfigContext(name=service_name, + script=script) + expect = { + "service_name": service_name, + "user": service_name, + "group": service_name, + "script": script, + "admin_script": None, + "public_script": None, + "processes": 8, + "admin_processes": 2, + "public_processes": 6, + "threads": 1, + "wsgi_socket_rotation": True, + "haproxy_enable_proxy_protocol": True + } + self.assertEqual(expect, ctxt()) + def test_zeromq_context_unrelated(self): self.is_relation_made.return_value = False self.assertEquals(context.ZeroMQContext()(), {})