diff --git a/doc/source/usage.rst b/doc/source/usage.rst index 0253410a..02f4e5a1 100644 --- a/doc/source/usage.rst +++ b/doc/source/usage.rst @@ -58,6 +58,9 @@ files. * :func:`oslo.service.sslutils ` The options from the sslutils module for the [ssl] section. +* :func:`oslo.service.wsgi ` + The options from the wsgi module for the [DEFAULT] section. + **ATTENTION:** The library doesn't provide an oslo.service entry point. .. code-block:: bash diff --git a/oslo_service/_options.py b/oslo_service/_options.py index 3db55026..e835f887 100644 --- a/oslo_service/_options.py +++ b/oslo_service/_options.py @@ -41,6 +41,42 @@ 'options when starting a service (at DEBUG level).'), ] +wsgi_opts = [ + cfg.StrOpt('api_paste_config', + default="api-paste.ini", + help='File name for the paste.deploy config for api service'), + cfg.StrOpt('wsgi_log_format', + default='%(client_ip)s "%(request_line)s" status: ' + '%(status_code)s len: %(body_length)s time:' + ' %(wall_seconds).7f', + help='A python format string that is used as the template to ' + 'generate log lines. The following values can be' + 'formatted into it: client_ip, date_time, request_line, ' + 'status_code, body_length, wall_seconds.'), + cfg.IntOpt('tcp_keepidle', + default=600, + help="Sets the value of TCP_KEEPIDLE in seconds for each " + "server socket. Not supported on OS X."), + cfg.IntOpt('wsgi_default_pool_size', + default=1000, + help="Size of the pool of greenthreads used by wsgi"), + cfg.IntOpt('max_header_line', + default=16384, + help="Maximum line size of message headers to be accepted. " + "max_header_line may need to be increased when using " + "large tokens (typically those generated by the " + "Keystone v3 API with big service catalogs)."), + cfg.BoolOpt('wsgi_keep_alive', + default=True, + help="If False, closes the client socket connection " + "explicitly."), + cfg.IntOpt('client_socket_timeout', default=900, + help="Timeout for client connections' socket operations. " + "If an incoming connection is idle for this number of " + "seconds it will be closed. A value of '0' means " + "wait forever."), + ] + ssl_opts = [ cfg.StrOpt('ca_file', help="CA certificate file to use to verify " diff --git a/oslo_service/tests/ssl_cert/ca.crt b/oslo_service/tests/ssl_cert/ca.crt new file mode 100644 index 00000000..3b7c65a3 --- /dev/null +++ b/oslo_service/tests/ssl_cert/ca.crt @@ -0,0 +1,40 @@ +-----BEGIN CERTIFICATE----- +MIIHADCCBOigAwIBAgIJAOjPGLL9VDhjMA0GCSqGSIb3DQEBDQUAMIGwMQswCQYD +VQQGEwJVUzEOMAwGA1UECBMFVGV4YXMxDzANBgNVBAcTBkF1c3RpbjEdMBsGA1UE +ChMUT3BlblN0YWNrIEZvdW5kYXRpb24xHTAbBgNVBAsTFE9wZW5TdGFjayBEZXZl +bG9wZXJzMRAwDgYDVQQDEwdUZXN0IENBMTAwLgYJKoZIhvcNAQkBFiFvcGVuc3Rh +Y2stZGV2QGxpc3RzLm9wZW5zdGFjay5vcmcwHhcNMTUwMTA4MDIyOTEzWhcNMjUw +MTA4MDIyOTEzWjCBsDELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMQ8wDQYD +VQQHEwZBdXN0aW4xHTAbBgNVBAoTFE9wZW5TdGFjayBGb3VuZGF0aW9uMR0wGwYD +VQQLExRPcGVuU3RhY2sgRGV2ZWxvcGVyczEQMA4GA1UEAxMHVGVzdCBDQTEwMC4G +CSqGSIb3DQEJARYhb3BlbnN0YWNrLWRldkBsaXN0cy5vcGVuc3RhY2sub3JnMIIC +IjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAwILIMebpHYK1E1zhyi6713GG +TQ9DFeLOE1T25+XTJqAkO7efQzZfB8QwCXy/8bmbhmKgQQ7APuuDci8SKCkYeWCx +qJRGmg0tZVlj5gCfrV2u+olwS+XyaOGCFkYScs6D34BaE2rGD2GDryoSPc2feAt6 +X4+ZkDPZnvaHQP6j9Ofq/4WmsECEas0IO5X8SDF8afA47U9ZXFkcgQK6HCHDcokL +aaZxEyZFSaPex6ZAESNthkGOxEThRPxAkJhqYCeMl3Hff98XEUcFNzuAOmcnQJJg +RemwJO2hS5KS3Y3p9/nBRlh3tSAG1nbY5kXSpyaq296D9x/esnXlt+9JUmn1rKyv +maFBC/SbzyyQoO3MT5r8rKte0bulLw1bZOZNlhxSv2KCg5RD6vlNrnpsZszw4nj2 +8fBroeFp0JMeT8jcqGs3qdm8sXLcBgiTalLYtiCNV9wZjOduQotuFN6mDwZvfa6h +zZjcBNfqeLyTEnFb5k6pIla0wydWx/jvBAzoxOkEcVjak747A+p/rriD5hVUBH0B +uNaWcEgKe9jcHnLvU8hUxFtgPxUHOOR+eMa+FS3ApKf9sJ/zVUq0uxyA9hUnsvnq +v/CywLSvaNKBiKQTL0QLEXnw6EQb7g/XuwC5mmt+l30wGh9M1U/QMaU/+YzT4sVL +TXIHJ7ExRTbEecbNbjsCAwEAAaOCARkwggEVMB0GA1UdDgQWBBQTWz2WEB0sJg9c +xfM5JeJMIAJq0jCB5QYDVR0jBIHdMIHagBQTWz2WEB0sJg9cxfM5JeJMIAJq0qGB +tqSBszCBsDELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMQ8wDQYDVQQHEwZB +dXN0aW4xHTAbBgNVBAoTFE9wZW5TdGFjayBGb3VuZGF0aW9uMR0wGwYDVQQLExRP +cGVuU3RhY2sgRGV2ZWxvcGVyczEQMA4GA1UEAxMHVGVzdCBDQTEwMC4GCSqGSIb3 +DQEJARYhb3BlbnN0YWNrLWRldkBsaXN0cy5vcGVuc3RhY2sub3JnggkA6M8Ysv1U +OGMwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQ0FAAOCAgEAIfAD6uVorT5WomG1 +2DWRm3kuwa+EDimgVF6VRvxCzyHx7e/6KJQj149KpMQ6e0ZPjqQw+pZ+jJSgq6TP +MEjCHgIDwdKhi9LmQWIlo8xdzgfZW2VQkVLvwkqAnWWhCy9oGc/Ypk8pjiZfCx+/ +DSJBbFnopI9f8epAKMq7N3jJyEMoTctzmI0KckrZnJ1Gq4MZpoxGmkJiGhWoUk8p +r8apXZ6B1DzO1XxpGw2BIcrUC3bQS/vPrg5/XbyaAu2BSgu6iF7ULqkBsEd0yK/L +i2gO9eTacaX3zJBQOlMJFsIAgIiVw6Rq6BuhU9zxDoopY4feta/NDOpk1OjY3MV7 +4rcLTU6XYaItMDRe+dmjBOK+xspsaCU4kHEkA7mHL5YZhEEWLHj6QY8tAiIQMVQZ +RuTpQIbNkjLW8Ls+CbwL2LkUFB19rKu9tFpzEJ1IIeFmt5HZsL5ri6W2qkSPIbIe +Qq15kl/a45jgBbgn2VNA5ecjW20hhXyaS9AKWXK+AeFBaFIFDUrB2UP4YSDbJWUJ +0LKe+QuumXdl+iRdkgb1Tll7qme8gXAeyzVGHK2AsaBg+gkEeSyVLRKIixceyy+3 +6yqlKJhk2qeV3ceOfVm9ZdvRlzWyVctaTcGIpDFqf4y8YyVhL1e2KGKcmYtbLq+m +rtku4CM3HldxcM4wqSB1VcaTX8o= +-----END CERTIFICATE----- diff --git a/oslo_service/tests/ssl_cert/ca.key b/oslo_service/tests/ssl_cert/ca.key new file mode 100644 index 00000000..db17076b --- /dev/null +++ b/oslo_service/tests/ssl_cert/ca.key @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +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 +-----END RSA PRIVATE KEY----- diff --git a/oslo_service/tests/ssl_cert/certificate.crt b/oslo_service/tests/ssl_cert/certificate.crt new file mode 100644 index 00000000..45851c3a --- /dev/null +++ b/oslo_service/tests/ssl_cert/certificate.crt @@ -0,0 +1,41 @@ +-----BEGIN CERTIFICATE----- +MIIHHjCCBQagAwIBAgIBATANBgkqhkiG9w0BAQ0FADCBsDELMAkGA1UEBhMCVVMx +DjAMBgNVBAgTBVRleGFzMQ8wDQYDVQQHEwZBdXN0aW4xHTAbBgNVBAoTFE9wZW5T +dGFjayBGb3VuZGF0aW9uMR0wGwYDVQQLExRPcGVuU3RhY2sgRGV2ZWxvcGVyczEQ +MA4GA1UEAxMHVGVzdCBDQTEwMC4GCSqGSIb3DQEJARYhb3BlbnN0YWNrLWRldkBs +aXN0cy5vcGVuc3RhY2sub3JnMB4XDTE1MDEwODAyNTQzNVoXDTI1MDEwODAyNTQz +NVoweDELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFzMQ8wDQYDVQQHEwZBdXN0 +aW4xHTAbBgNVBAoTFE9wZW5TdGFjayBGb3VuZGF0aW9uMR0wGwYDVQQLExRPcGVu +U3RhY2sgRGV2ZWxvcGVyczEKMAgGA1UEAxQBKjCCAiIwDQYJKoZIhvcNAQEBBQAD +ggIPADCCAgoCggIBANBJtvyhMKBn397hE7x9Ce/Ny+4ENQfr9VrHuvGNCR3W/uUb +QafdNdZCYNAGPrq2T3CEYK0IJxZjr2HuTcSK9StBMFauTeIPqVUVkO3Tjq1Rkv+L +np/e6DhHkjCU6Eq/jIw3ic0QoxLygTybGxXgJgVoBzGsJufzOQ14tfkzGeGyE3L5 +z5DpCNQqWLWF7soMx3kM5hBm+LWeoiBPjmsEXQY+UYiDlSLW/6I855X/wwDW5+Ot +P6/1lWUfcyAyIqj3t0pmxZeY7xQnabWjhXT2dTK+dlwRjb77w665AgeF1R5lpTvU +yT1aQwgH1kd9GeQbkBDwWSVLH9boPPgdMLtX2ipUgQAAEhIOUWXOYZVHVNXhV6Cr +jAgvfdF39c9hmuXovPP24ikW4L+d5RPE7Vq9KJ4Uzijw9Ghu4lQQCRZ8SCNZIYJn +Tz53+6fs93WwnnEPto9tFRKeNWt3jx/wjluDFhhBTZO4snNIq9xnCYSEQAIsRBVW +Ahv7LqWLigUy7a9HMIyi3tQEZN9NCDy4BNuJDu33XWLLVMwNrIiR5mdCUFoRKt/E ++YPj7bNlzZMTSGLoBFPM71Lnfym9HazHDE1KxvT4gzYMubK4Y07meybiL4QNvU08 +ITgFU6DAGob+y/GHqw+bmez5y0F/6FlyV+SiSrbVEEtzp9Ewyrxb85OJFK0tAgMB +AAGjggF4MIIBdDBLBgNVHREERDBCgglsb2NhbGhvc3SCDWlwNi1sb2NhbGhvc3SC +CTEyNy4wLjAuMYIDOjoxhwR/AAABhxAAAAAAAAAAAAAAAAAAAAABMB0GA1UdDgQW +BBSjWxD0qedj9eeGUWyGphy5PU67dDCB5QYDVR0jBIHdMIHagBQTWz2WEB0sJg9c +xfM5JeJMIAJq0qGBtqSBszCBsDELMAkGA1UEBhMCVVMxDjAMBgNVBAgTBVRleGFz +MQ8wDQYDVQQHEwZBdXN0aW4xHTAbBgNVBAoTFE9wZW5TdGFjayBGb3VuZGF0aW9u +MR0wGwYDVQQLExRPcGVuU3RhY2sgRGV2ZWxvcGVyczEQMA4GA1UEAxMHVGVzdCBD +QTEwMC4GCSqGSIb3DQEJARYhb3BlbnN0YWNrLWRldkBsaXN0cy5vcGVuc3RhY2su +b3JnggkA6M8Ysv1UOGMwCQYDVR0TBAIwADATBgNVHSUEDDAKBggrBgEFBQcDATAN +BgkqhkiG9w0BAQ0FAAOCAgEAIGx/acXQEiGYFBJUduE6/Y6LBuHEVMcj0yfbLzja +Eb35xKWHuX7tgQPwXy6UGlYM8oKIptIp/9eEuYXte6u5ncvD7e/JldCUVd0fW8hm +fBOhfqVstcTmlfZ6WqTJD6Bp/FjUH+8qf8E+lsjNy7i0EsmcQOeQm4mkocHG1AA4 +MEeuDg33lV6XCjW450BoZ/FTfwZSuTlGgFlEzUUrAe/ETdajF9G9aJ+0OvXzE1tU +pvbvkU8eg4pLXxrzboOhyQMEmCikdkMYjo/0ZQrXrrJ1W8mCinkJdz6CToc7nUkU +F8tdAY0rKMEM8SYHngMJU2943lpGbQhE5B4oms8I+SMTyCVz2Vu5I43Px68Y0GUN +Bn5qu0w2Vj8eradoPF8pEAIVICIvlbiRepPbNZ7FieSsY2TEfLtxBd2DLE1YWeE5 +p/RDBxqcDrGQuSg6gFSoLEhYgQcGnYgD75EIE8f/LrHFOAeSYEOhibFbK5G8p/2h +EHcKZ9lvTgqwHn0FiTqZ3LWxVFsZiTsiyXErpJ2Nu2WTzo0k1xJMUpJqHuUZraei +N5fA5YuDp2ShXRoZyVieRvp0TCmm6sHL8Pn0K8weJchYrvV1yvPKeuISN/fVCQev +88yih5Rh5R2szwoY3uVImpd99bMm0e1bXrQug43ZUz9rC4ABN6+lZvuorDWRVI7U +I1M= +-----END CERTIFICATE----- diff --git a/oslo_service/tests/ssl_cert/privatekey.key b/oslo_service/tests/ssl_cert/privatekey.key new file mode 100644 index 00000000..b94525a3 --- /dev/null +++ b/oslo_service/tests/ssl_cert/privatekey.key @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +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 +-----END RSA PRIVATE KEY----- diff --git a/oslo_service/tests/test_wsgi.py b/oslo_service/tests/test_wsgi.py new file mode 100644 index 00000000..4e96b3cb --- /dev/null +++ b/oslo_service/tests/test_wsgi.py @@ -0,0 +1,369 @@ +# Copyright 2011 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# 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. + +"""Unit tests for `wsgi`.""" + +import os.path +import platform +import socket +import tempfile +import testtools + +import eventlet +import eventlet.wsgi +import mock +import requests +import webob + +from oslo_config import cfg +from oslo_config import fixture as config +from oslo_service import _options +from oslo_service import sslutils +from oslo_service import wsgi +from oslo_utils import netutils +from oslotest import base as test_base +from oslotest import moxstubout + + +SSL_CERT_DIR = os.path.normpath(os.path.join( + os.path.dirname(os.path.abspath(__file__)), + 'ssl_cert')) +CONF = cfg.CONF + + +class WsgiTestCase(test_base.BaseTestCase): + """Base class for WSGI tests.""" + + def setUp(self): + super(WsgiTestCase, self).setUp() + self.conf_fixture = self.useFixture(config.Config()) + self.conf_fixture.register_opts(_options.wsgi_opts) + self.conf = self.conf_fixture.conf + self.config = self.conf_fixture.config + self.conf(args=[], default_config_files=[]) + + +class TestLoaderNothingExists(WsgiTestCase): + """Loader tests where os.path.exists always returns False.""" + + def setUp(self): + super(TestLoaderNothingExists, self).setUp() + mox_fixture = self.useFixture(moxstubout.MoxStubout()) + self.stubs = mox_fixture.stubs + self.stubs.Set(os.path, 'exists', lambda _: False) + + def test_relpath_config_not_found(self): + self.config(api_paste_config='api-paste.ini') + self.assertRaises( + wsgi.ConfigNotFound, + wsgi.Loader, + self.conf + ) + + def test_asbpath_config_not_found(self): + self.config(api_paste_config='/etc/openstack-srv/api-paste.ini') + self.assertRaises( + wsgi.ConfigNotFound, + wsgi.Loader, + self.conf + ) + + +class TestLoaderNormalFilesystem(WsgiTestCase): + """Loader tests with normal filesystem (unmodified os.path module).""" + + _paste_config = """ +[app:test_app] +use = egg:Paste#static +document_root = /tmp + """ + + def setUp(self): + super(TestLoaderNormalFilesystem, self).setUp() + self.paste_config = tempfile.NamedTemporaryFile(mode="w+t") + self.paste_config.write(self._paste_config.lstrip()) + self.paste_config.seek(0) + self.paste_config.flush() + + self.config(api_paste_config=self.paste_config.name) + self.loader = wsgi.Loader(CONF) + + def test_config_found(self): + self.assertEqual(self.paste_config.name, self.loader.config_path) + + def test_app_not_found(self): + self.assertRaises( + wsgi.PasteAppNotFound, + self.loader.load_app, + "nonexistent app", + ) + + def test_app_found(self): + url_parser = self.loader.load_app("test_app") + self.assertEqual("/tmp", url_parser.directory) + + def tearDown(self): + self.paste_config.close() + super(TestLoaderNormalFilesystem, self).tearDown() + + +class TestWSGIServer(WsgiTestCase): + """WSGI server tests.""" + + def setUp(self): + super(TestWSGIServer, self).setUp() + + def test_no_app(self): + server = wsgi.Server(self.conf, "test_app", None) + self.assertEqual("test_app", server.name) + + def test_custom_max_header_line(self): + self.config(max_header_line=4096) # Default value is 16384 + wsgi.Server(self.conf, "test_custom_max_header_line", None) + self.assertEqual(self.conf.max_header_line, + eventlet.wsgi.MAX_HEADER_LINE) + + def test_start_random_port(self): + server = wsgi.Server(self.conf, "test_random_port", None, + host="127.0.0.1", port=0) + server.start() + self.assertNotEqual(0, server.port) + server.stop() + server.wait() + + @testtools.skipIf(not netutils.is_ipv6_enabled(), "no ipv6 support") + def test_start_random_port_with_ipv6(self): + server = wsgi.Server(self.conf, "test_random_port", None, + host="::1", port=0) + server.start() + self.assertEqual("::1", server.host) + self.assertNotEqual(0, server.port) + server.stop() + server.wait() + + @testtools.skipIf(platform.mac_ver()[0] != '', + 'SO_REUSEADDR behaves differently ' + 'on OSX, see bug 1436895') + def test_socket_options_for_simple_server(self): + # test normal socket options has set properly + self.config(tcp_keepidle=500) + server = wsgi.Server(self.conf, "test_socket_options", None, + host="127.0.0.1", port=0) + server.start() + sock = server._socket + self.assertEqual(1, sock.getsockopt(socket.SOL_SOCKET, + socket.SO_REUSEADDR)) + self.assertEqual(1, sock.getsockopt(socket.SOL_SOCKET, + socket.SO_KEEPALIVE)) + if hasattr(socket, 'TCP_KEEPIDLE'): + self.assertEqual(self.conf.tcp_keepidle, + sock.getsockopt(socket.IPPROTO_TCP, + socket.TCP_KEEPIDLE)) + self.assertFalse(server._server.dead) + server.stop() + server.wait() + self.assertTrue(server._server.dead) + + def test_server_pool_waitall(self): + # test pools waitall method gets called while stopping server + server = wsgi.Server(self.conf, "test_server", None, host="127.0.0.1") + server.start() + with mock.patch.object(server._pool, + 'waitall') as mock_waitall: + server.stop() + server.wait() + mock_waitall.assert_called_once_with() + + def test_uri_length_limit(self): + eventlet.monkey_patch(os=False, thread=False) + server = wsgi.Server(self.conf, "test_uri_length_limit", None, + host="127.0.0.1", max_url_len=16384, port=33337) + server.start() + self.assertFalse(server._server.dead) + + uri = "http://127.0.0.1:%d/%s" % (server.port, 10000 * 'x') + resp = requests.get(uri, proxies={"http": ""}) + eventlet.sleep(0) + self.assertNotEqual(resp.status_code, + requests.codes.REQUEST_URI_TOO_LARGE) + + uri = "http://127.0.0.1:%d/%s" % (server.port, 20000 * 'x') + resp = requests.get(uri, proxies={"http": ""}) + eventlet.sleep(0) + self.assertEqual(resp.status_code, + requests.codes.REQUEST_URI_TOO_LARGE) + server.stop() + server.wait() + + def test_reset_pool_size_to_default(self): + server = wsgi.Server(self.conf, "test_resize", None, + host="127.0.0.1", max_url_len=16384) + server.start() + + # Stopping the server, which in turn sets pool size to 0 + server.stop() + self.assertEqual(server._pool.size, 0) + + # Resetting pool size to default + server.reset() + server.start() + self.assertEqual(server._pool.size, CONF.wsgi_default_pool_size) + + def test_client_socket_timeout(self): + self.config(client_socket_timeout=5) + + # mocking eventlet spawn method to check it is called with + # configured 'client_socket_timeout' value. + with mock.patch.object(eventlet, + 'spawn') as mock_spawn: + server = wsgi.Server(self.conf, "test_app", None, + host="127.0.0.1", port=0) + server.start() + _, kwargs = mock_spawn.call_args + self.assertEqual(self.conf.client_socket_timeout, + kwargs['socket_timeout']) + server.stop() + + def test_wsgi_keep_alive(self): + self.config(wsgi_keep_alive=False) + + # mocking eventlet spawn method to check it is called with + # configured 'wsgi_keep_alive' value. + with mock.patch.object(eventlet, + 'spawn') as mock_spawn: + server = wsgi.Server(self.conf, "test_app", None, + host="127.0.0.1", port=0) + server.start() + _, kwargs = mock_spawn.call_args + self.assertEqual(self.conf.wsgi_keep_alive, + kwargs['keepalive']) + server.stop() + + +class TestWSGIServerWithSSL(WsgiTestCase): + """WSGI server with SSL tests.""" + + def setUp(self): + super(TestWSGIServerWithSSL, self).setUp() + self.conf_fixture.register_opts(_options.ssl_opts, + sslutils.config_section) + cert_file_name = os.path.join(SSL_CERT_DIR, 'certificate.crt') + key_file_name = os.path.join(SSL_CERT_DIR, 'privatekey.key') + eventlet.monkey_patch(os=False, thread=False) + + self.config(cert_file=cert_file_name, + key_file=key_file_name, + group=sslutils.config_section) + + def test_ssl_server(self): + def test_app(env, start_response): + start_response('200 OK', {}) + return ['PONG'] + + fake_ssl_server = wsgi.Server(self.conf, "fake_ssl", test_app, + host="127.0.0.1", port=0, use_ssl=True) + fake_ssl_server.start() + self.assertNotEqual(0, fake_ssl_server.port) + + cli = eventlet.connect(("localhost", fake_ssl_server.port)) + ca_certs_name = os.path.join(SSL_CERT_DIR, 'ca.crt') + cli = eventlet.wrap_ssl(cli, ca_certs=ca_certs_name) + + cli.write('POST / HTTP/1.1\r\nHost: localhost\r\n' + 'Connection: close\r\nContent-length:4\r\n\r\nPING') + response = cli.read(8192) + self.assertEqual(response[-4:], "PONG") + + fake_ssl_server.stop() + fake_ssl_server.wait() + + def test_two_servers(self): + def test_app(env, start_response): + start_response('200 OK', {}) + return ['PONG'] + + fake_ssl_server = wsgi.Server(self.conf, "fake_ssl", test_app, + host="127.0.0.1", port=0, use_ssl=True) + fake_ssl_server.start() + self.assertNotEqual(0, fake_ssl_server.port) + + fake_server = wsgi.Server(self.conf, "fake", test_app, + host="127.0.0.1", port=0) + fake_server.start() + self.assertNotEqual(0, fake_server.port) + + cli = eventlet.connect(("localhost", fake_ssl_server.port)) + cli = eventlet.wrap_ssl(cli, + ca_certs=os.path.join(SSL_CERT_DIR, 'ca.crt')) + + cli.write('POST / HTTP/1.1\r\nHost: localhost\r\n' + 'Connection: close\r\nContent-length:4\r\n\r\nPING') + response = cli.read(8192) + self.assertEqual(response[-4:], "PONG") + + cli = eventlet.connect(("localhost", fake_server.port)) + + cli.sendall('POST / HTTP/1.1\r\nHost: localhost\r\n' + 'Connection: close\r\nContent-length:4\r\n\r\nPING') + response = cli.recv(8192) + self.assertEqual(response[-4:], "PONG") + + fake_ssl_server.stop() + fake_ssl_server.wait() + + @testtools.skipIf(platform.mac_ver()[0] != '', + 'SO_REUSEADDR behaves differently ' + 'on OSX, see bug 1436895') + def test_socket_options_for_ssl_server(self): + # test normal socket options has set properly + self.config(tcp_keepidle=500) + server = wsgi.Server(self.conf, "test_socket_options", None, + host="127.0.0.1", port=0, use_ssl=True) + server.start() + sock = server._socket + self.assertEqual(1, sock.getsockopt(socket.SOL_SOCKET, + socket.SO_REUSEADDR)) + self.assertEqual(1, sock.getsockopt(socket.SOL_SOCKET, + socket.SO_KEEPALIVE)) + if hasattr(socket, 'TCP_KEEPIDLE'): + self.assertEqual(CONF.tcp_keepidle, + sock.getsockopt(socket.IPPROTO_TCP, + socket.TCP_KEEPIDLE)) + server.stop() + server.wait() + + @testtools.skipIf(not netutils.is_ipv6_enabled(), "no ipv6 support") + def test_app_using_ipv6_and_ssl(self): + greetings = 'Hello, World!!!' + + @webob.dec.wsgify + def hello_world(req): + return greetings + + server = wsgi.Server(self.conf, "fake_ssl", + hello_world, + host="::1", + port=0, + use_ssl=True) + + server.start() + + response = requests.get('https://[::1]:%d/' % server.port, + verify=os.path.join(SSL_CERT_DIR, 'ca.crt')) + self.assertEqual(greetings, response.text) + + server.stop() + server.wait() diff --git a/oslo_service/wsgi.py b/oslo_service/wsgi.py new file mode 100644 index 00000000..6feae863 --- /dev/null +++ b/oslo_service/wsgi.py @@ -0,0 +1,318 @@ +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# Copyright 2010 OpenStack Foundation +# 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. + +"""Utility methods for working with WSGI servers.""" + +from __future__ import print_function + +import copy +import os +import socket + +import eventlet +import eventlet.wsgi +import greenlet +from paste import deploy +import routes.middleware +import webob.dec +import webob.exc + +from oslo_log import log as logging +from oslo_service import _options +from oslo_service import sslutils +from oslo_service._i18n import _, _LE, _LI + + +LOG = logging.getLogger(__name__) + + +def list_opts(): + """Entry point for oslo-config-generator.""" + return [(None, copy.deepcopy(_options.wsgi_opts))] + + +class InvalidInput(Exception): + message = _("Invalid input received: " + "Unexpected argument for periodic task creation: %(arg)s.") + + +class Server(object): + """Server class to manage a WSGI server, serving a WSGI application.""" + + def __init__(self, conf, name, app, host='0.0.0.0', port=0, pool_size=None, + protocol=eventlet.wsgi.HttpProtocol, backlog=128, + use_ssl=False, max_url_len=None): + """Initialize, but do not start, a WSGI server. + + :param name: Pretty name for logging. + :param app: The WSGI application to serve. + :param host: IP address to serve the application. + :param port: Port number to server the application. + :param pool_size: Maximum number of eventlets to spawn concurrently. + :param backlog: Maximum number of queued connections. + :param max_url_len: Maximum length of permitted URLs. + :returns: None + :raises: InvalidInput + """ + + self.conf = conf + self.conf.register_opts(_options.wsgi_opts) + + self.default_pool_size = self.conf.wsgi_default_pool_size + + # Allow operators to customize http requests max header line size. + eventlet.wsgi.MAX_HEADER_LINE = conf.max_header_line + self.name = name + self.app = app + self._server = None + self._protocol = protocol + self.pool_size = pool_size or self.default_pool_size + self._pool = eventlet.GreenPool(self.pool_size) + self._logger = logging.getLogger("eventlet.wsgi.server") + self._use_ssl = use_ssl + self._max_url_len = max_url_len + self.client_socket_timeout = conf.client_socket_timeout or None + self.default_pool_size = conf.wsgi_default_pool_size + + if backlog < 1: + raise InvalidInput(reason='The backlog must be more than 0') + + bind_addr = (host, port) + # TODO(dims): eventlet's green dns/socket module does not actually + # support IPv6 in getaddrinfo(). We need to get around this in the + # future or monitor upstream for a fix + try: + info = socket.getaddrinfo(bind_addr[0], + bind_addr[1], + socket.AF_UNSPEC, + socket.SOCK_STREAM)[0] + family = info[0] + bind_addr = info[-1] + except Exception: + family = socket.AF_INET + + if self._use_ssl: + sslutils.is_enabled(conf) + + try: + self._socket = eventlet.listen(bind_addr, family, backlog=backlog) + except EnvironmentError: + LOG.error(_LE("Could not bind to %(host)s:%(port)s"), + {'host': host, 'port': port}) + raise + + (self.host, self.port) = self._socket.getsockname()[0:2] + LOG.info(_LI("%(name)s listening on %(host)s:%(port)s"), + {'name': self.name, 'host': self.host, 'port': self.port}) + + def start(self): + """Start serving a WSGI application. + + :returns: None + """ + # The server socket object will be closed after server exits, + # but the underlying file descriptor will remain open, and will + # give bad file descriptor error. So duplicating the socket object, + # to keep file descriptor usable. + + self.dup_socket = self._socket.dup() + + self.dup_socket = self._set_socket_opts(self.dup_socket) + + if self._use_ssl: + self.dup_socket = sslutils.wrap(self.conf, self.dup_socket) + + wsgi_kwargs = { + 'func': eventlet.wsgi.server, + 'sock': self.dup_socket, + 'site': self.app, + 'protocol': self._protocol, + 'custom_pool': self._pool, + 'log': self._logger, + 'log_format': self.conf.wsgi_log_format, + 'debug': False, + 'keepalive': self.conf.wsgi_keep_alive, + 'socket_timeout': self.client_socket_timeout + } + + if self._max_url_len: + wsgi_kwargs['url_length_limit'] = self._max_url_len + + self._server = eventlet.spawn(**wsgi_kwargs) + + def _set_socket_opts(self, _socket): + _socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + # sockets can hang around forever without keepalive + _socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) + + # This option isn't available in the OS X version of eventlet + if hasattr(socket, 'TCP_KEEPIDLE'): + _socket.setsockopt(socket.IPPROTO_TCP, + socket.TCP_KEEPIDLE, + self.conf.tcp_keepidle) + + return _socket + + def reset(self): + """Reset server greenpool size to default. + + :returns: None + + """ + self._pool.resize(self.pool_size) + + def stop(self): + """Stops eventlet server. Doesn't allow accept new connecting. + + :returns: None + + """ + LOG.info(_LI("Stopping WSGI server.")) + + if self._server is not None: + # let eventlet close socket + self._pool.resize(0) + self._server.kill() + + def wait(self): + """Block, until the server has stopped. + + Waits on the server's eventlet to finish, then returns. + + :returns: None + + """ + try: + if self._server is not None: + num = self._pool.running() + LOG.debug("Waiting WSGI server to finish %d requests.", num) + self._pool.waitall() + except greenlet.GreenletExit: + LOG.info(_LI("WSGI server has stopped.")) + + +class Request(webob.Request): + pass + + +class Router(object): + """WSGI middleware that maps incoming requests to WSGI apps.""" + + def __init__(self, mapper): + """Create a router for the given routes.Mapper. + + Each route in `mapper` must specify a 'controller', which is a + WSGI app to call. You'll probably want to specify an 'action' as + well and have your controller be an object that can route + the request to the action-specific method. + + Examples: + mapper = routes.Mapper() + sc = ServerController() + + # Explicit mapping of one route to a controller+action + mapper.connect(None, '/svrlist', controller=sc, action='list') + + # Actions are all implicitly defined + mapper.resource('server', 'servers', controller=sc) + + # Pointing to an arbitrary WSGI app. You can specify the + # {path_info:.*} parameter so the target app can be handed just that + # section of the URL. + mapper.connect(None, '/v1.0/{path_info:.*}', controller=BlogApp()) + + """ + self.map = mapper + self._router = routes.middleware.RoutesMiddleware(self._dispatch, + self.map) + + @webob.dec.wsgify(RequestClass=Request) + def __call__(self, req): + """Route the incoming request to a controller based on self.map. + + If no match, return a 404. + + """ + return self._router + + @staticmethod + @webob.dec.wsgify(RequestClass=Request) + def _dispatch(req): + """Dispatch the request to the appropriate controller. + + Called by self._router after matching the incoming request to a route + and putting the information into req.environ. Either returns 404 + or the routed WSGI app's response. + + """ + match = req.environ['wsgiorg.routing_args'][1] + if not match: + return webob.exc.HTTPNotFound() + app = match['controller'] + return app + + +class ConfigNotFound(Exception): + def __init__(self, path): + msg = _('Could not find config at %(path)s') % {'path': path} + super(ConfigNotFound, self).__init__(msg) + + +class PasteAppNotFound(Exception): + def __init__(self, name, path): + msg = (_("Could not load paste app '%(name)s' from %(path)s") % + {'name': name, 'path': path}) + super(PasteAppNotFound, self).__init__(msg) + + +class Loader(object): + """Used to load WSGI applications from paste configurations.""" + + def __init__(self, conf): + """Initialize the loader, and attempt to find the config. + + :param conf + :returns: None + + """ + conf.register_opts(_options.wsgi_opts) + self.config_path = None + + config_path = conf.api_paste_config + if not os.path.isabs(config_path): + self.config_path = conf.find_file(config_path) + elif os.path.exists(config_path): + self.config_path = config_path + + if not self.config_path: + raise ConfigNotFound(path=config_path) + + def load_app(self, name): + """Return the paste URLMap wrapped WSGI application. + + :param name: Name of the application to load. + :returns: Paste URLMap object wrapping the requested application. + :raises: `PasteAppNotFound` + + """ + try: + LOG.debug("Loading app %(name)s from %(path)s", + {'name': name, 'path': self.config_path}) + return deploy.loadapp("config:%s" % self.config_path, name=name) + except LookupError: + LOG.exception(_LE("Couldn't lookup app: %s"), name) + raise PasteAppNotFound(name=name, path=self.config_path) diff --git a/requirements.txt b/requirements.txt index b8699df7..6206e8e9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,11 +3,17 @@ # process, which may cause wedges in the gate later. Babel>=1.3 +WebOb>=1.2.3 eventlet>=0.17.4 greenlet>=0.3.2 monotonic>=0.3 # Apache-2.0 oslo.utils>=2.0.0 # Apache-2.0 oslo.concurrency>=2.3.0 # Apache-2.0 oslo.config>=2.1.0 # Apache-2.0 +oslo.log>=1.8.0 # Apache-2.0 six>=1.9.0 oslo.i18n>=1.5.0 # Apache-2.0 +PasteDeploy>=1.5.0 +Routes!=2.0,!=2.1,>=1.12.3;python_version=='2.7' +Routes!=2.0,>=1.12.3;python_version!='2.7' +Paste diff --git a/setup.cfg b/setup.cfg index 3b04cd86..0dc1e564 100644 --- a/setup.cfg +++ b/setup.cfg @@ -32,6 +32,7 @@ oslo.config.opts = oslo.service.periodic_task = oslo_service.periodic_task:list_opts oslo.service.service = oslo_service.service:list_opts oslo.service.sslutils = oslo_service.sslutils:list_opts + oslo.service.wsgi = oslo_service.wsgi:list_opts [build_sphinx] source-dir = doc/source diff --git a/tox.ini b/tox.ini index d6987bd2..7d177594 100644 --- a/tox.ini +++ b/tox.ini @@ -19,6 +19,16 @@ commands = python setup.py testr --slowest --testr-args='{posargs}' [testenv:pep8] commands = flake8 +[testenv:py34] +commands = + python -m testtools.run \ + oslo_service.tests.test_eventlet_backdoor \ + oslo_service.tests.test_loopingcall \ + oslo_service.tests.test_periodic \ + oslo_service.tests.test_service \ + oslo_service.tests.test_systemd \ + oslo_service.tests.test_threadgroup + [testenv:venv] commands = {posargs}