Removing oslo_wsgi eventlet dependency
This Commit includes a range of changes to oslo_service wsgi, with suggested directions on removing the eventlet dependency. The idea is that if people like the directions of this code, I will implement the changes in seperate, cleaned and tested commits. The Short term goal is to test these changes in ironic-python-agent. All Tests pass, and the "temp" examples run on python3.8 and python3.12 Changes include: - Removing reliance on ssl.wrap which is depreciated in python 3.12 - A new gunicorn wsgi server that is non-blocking Change-Id: Ic87860be22edbde5d72ccf6ae751ae2aafd0fed2
This commit is contained in:
parent
5810d016e1
commit
4ef1294935
|
@ -24,7 +24,8 @@ config_section = 'ssl'
|
|||
|
||||
_SSL_PROTOCOLS = {
|
||||
"tlsv1": ssl.PROTOCOL_TLSv1,
|
||||
"sslv23": ssl.PROTOCOL_SSLv23
|
||||
"sslv23": ssl.PROTOCOL_SSLv23,
|
||||
"tls_latest": ssl.PROTOCOL_TLS,
|
||||
}
|
||||
|
||||
_OPTIONAL_PROTOCOLS = {
|
||||
|
|
|
@ -14,17 +14,17 @@ import os
|
|||
|
||||
import eventlet
|
||||
|
||||
if os.name == 'nt':
|
||||
# eventlet monkey patching the os and thread modules causes
|
||||
# subprocess.Popen to fail on Windows when using pipes due
|
||||
# to missing non-blocking IO support.
|
||||
#
|
||||
# bug report on eventlet:
|
||||
# https://bitbucket.org/eventlet/eventlet/issue/132/
|
||||
# eventletmonkey_patch-breaks
|
||||
eventlet.monkey_patch(os=False, thread=False)
|
||||
else:
|
||||
eventlet.monkey_patch()
|
||||
# if os.name == 'nt':
|
||||
# # eventlet monkey patching the os and thread modules causes
|
||||
# # subprocess.Popen to fail on Windows when using pipes due
|
||||
# # to missing non-blocking IO support.
|
||||
# #
|
||||
# # bug report on eventlet:
|
||||
# # https://bitbucket.org/eventlet/eventlet/issue/132/
|
||||
# # eventletmonkey_patch-breaks
|
||||
# eventlet.monkey_patch(os=False, thread=False)
|
||||
# else:
|
||||
# eventlet.monkey_patch()
|
||||
|
||||
# Monkey patch the original current_thread to use the up-to-date _active
|
||||
# global variable. See https://bugs.launchpad.net/bugs/1863021 and
|
||||
|
|
|
@ -19,7 +19,9 @@
|
|||
import os
|
||||
import platform
|
||||
import socket
|
||||
import ssl
|
||||
import tempfile
|
||||
import time
|
||||
import testtools
|
||||
from unittest import mock
|
||||
|
||||
|
@ -32,6 +34,7 @@ from oslo_config import cfg
|
|||
from oslo_service import sslutils
|
||||
from oslo_service.tests import base
|
||||
from oslo_service import wsgi
|
||||
from oslo_service import wsgi_new
|
||||
from oslo_utils import netutils
|
||||
|
||||
|
||||
|
@ -282,6 +285,79 @@ def requesting(host, port, ca_certs=None, method="POST",
|
|||
data = sock.recv(1024).decode()
|
||||
return data
|
||||
|
||||
def requesting_non_eventlet(host, port, method="POST",
|
||||
content_type="application/x-www-form-urlencoded",
|
||||
address_family=socket.AF_INET):
|
||||
|
||||
frame = bytes("{verb} / HTTP/1.1\r\n\r\n".format(verb=method), "utf-8")
|
||||
data = None
|
||||
MAX_ATTEMPTS = 5
|
||||
error = None
|
||||
|
||||
for _ in range(MAX_ATTEMPTS):
|
||||
try:
|
||||
# Create a regular socket
|
||||
with socket.socket(address_family, socket.SOCK_STREAM) as sock:
|
||||
# Wrap the socket using SSL for encrypted communication
|
||||
context = ssl.create_default_context()
|
||||
with context.wrap_socket(sock, server_hostname=host) as secured_socket:
|
||||
print(f"Connecting to {host}:{port}")
|
||||
secured_socket.connect((host, port))
|
||||
secured_socket.send(frame)
|
||||
data = secured_socket.recv(1024).decode()
|
||||
error = None
|
||||
break
|
||||
except (ConnectionRefusedError, ConnectionResetError) as e:
|
||||
print(f"Connection error: {e}")
|
||||
error = e
|
||||
continue
|
||||
|
||||
if error:
|
||||
raise error
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class TestWSGIServerWithSSLEventLetOff(WsgiTestCase):
|
||||
"""WSGI server with SSL tests."""
|
||||
|
||||
def setUp(self):
|
||||
super(TestWSGIServerWithSSLEventLetOff, self).setUp()
|
||||
cert_file_name = os.path.join(SSL_CERT_DIR, 'certificate.crt')
|
||||
key_file_name = os.path.join(SSL_CERT_DIR, 'privatekey.key')
|
||||
|
||||
self.host = "127.0.0.1"
|
||||
|
||||
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_new.Server(self.conf, "fake_ssl", test_app,
|
||||
host=self.host, port=0, use_ssl=True)
|
||||
fake_ssl_server.start()
|
||||
self.assertNotEqual(0, fake_ssl_server.port)
|
||||
|
||||
# Need to catch execeptions and force the server to stop
|
||||
# since the gunicorn process stays "alive"
|
||||
try:
|
||||
response = requesting_non_eventlet(
|
||||
method='GET',
|
||||
host=self.host,
|
||||
port=fake_ssl_server.port,
|
||||
)
|
||||
self.assertEqual('PONG', response[-4:])
|
||||
except Exception as e:
|
||||
fake_ssl_server.stop()
|
||||
# fake_ssl_server.wait()
|
||||
raise e
|
||||
|
||||
fake_ssl_server.stop()
|
||||
# fake_ssl_server.wait()
|
||||
|
||||
class TestWSGIServerWithSSL(WsgiTestCase):
|
||||
"""WSGI server with SSL tests."""
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
# Copyright (c) 2012 OpenStack Foundation.
|
||||
# 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.
|
||||
|
||||
import multiprocessing
|
||||
import socket
|
||||
import os
|
||||
import time
|
||||
|
||||
from contextlib import closing
|
||||
|
||||
from gunicorn.app.base import BaseApplication
|
||||
|
||||
from oslo_service import _options
|
||||
|
||||
|
||||
oslo_to_gunicorn = {
|
||||
"cert_file": "certfile",
|
||||
"key_file": "keyfile",
|
||||
"ca_file": "ca_certs",
|
||||
"version": "ssl_version",
|
||||
}
|
||||
|
||||
def find_free_port():
|
||||
with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
|
||||
s.bind(('', 0))
|
||||
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
return s.getsockname()[1]
|
||||
|
||||
class Server(BaseApplication):
|
||||
def __init__(self, conf, name, app, host=None, port=0, use_ssl=True):
|
||||
self.name = name
|
||||
self.host = host
|
||||
|
||||
if port != 0:
|
||||
self.port = port
|
||||
else:
|
||||
self.port = find_free_port()
|
||||
|
||||
self.conf = conf
|
||||
self.conf.register_opts(_options.wsgi_opts)
|
||||
|
||||
self.application = app
|
||||
super(Server, self).__init__()
|
||||
|
||||
self.cfg.set("bind", f"{self.host}:{self.port}")
|
||||
|
||||
# TODO(adamcarthur) Find a way to get the groups correctly
|
||||
# TODO(adamcarthur) Detect when doesn't exist
|
||||
# TODO(adamcarthur) Do not use ssh_version as its deprecated. Use ssl_context
|
||||
if use_ssl:
|
||||
for oslo_key, gunicorn_key in oslo_to_gunicorn.items():
|
||||
self.cfg.set(gunicorn_key, getattr(conf.ssl, oslo_key))
|
||||
|
||||
def load_config(self):
|
||||
pass
|
||||
|
||||
def load(self):
|
||||
return self.application
|
||||
|
||||
def start(self):
|
||||
# Start self.run in a new process
|
||||
self.pid = multiprocessing.Process(target=self.run)
|
||||
self.pid.start()
|
||||
|
||||
def stop(self):
|
||||
"""Stop the server process."""
|
||||
self.pid.terminate() # Send terminate request
|
||||
self.pid.terminate()
|
||||
|
||||
def wait(self, timeout=None):
|
||||
"""Wait for the server process to stop, with an optional timeout."""
|
||||
self.pid.join() # Wait for the process to terminate
|
|
@ -11,3 +11,4 @@ PasteDeploy>=1.5.0 # MIT
|
|||
Routes>=2.3.1 # MIT
|
||||
Paste>=2.0.2 # MIT
|
||||
Yappi>=1.0 # MIT
|
||||
gunicorn
|
|
@ -0,0 +1,55 @@
|
|||
# Copyright (c) 2012 OpenStack Foundation.
|
||||
# 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.
|
||||
|
||||
import eventlet
|
||||
|
||||
eventlet.monkey_patch()
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_service import sslutils, wsgi
|
||||
|
||||
# Register SSL options
|
||||
CONF = cfg.CONF
|
||||
CONF(default_config_files=["env/oslo.conf"])
|
||||
sslutils.register_opts(CONF)
|
||||
|
||||
|
||||
def app_factory(global_config, **local_conf):
|
||||
def application(environ, start_response):
|
||||
status = "200 OK"
|
||||
headers = [("Content-type", "text/plain")]
|
||||
start_response(status, headers)
|
||||
response_body = b"Hello, World! - From Eventlet Oslo"
|
||||
return [response_body]
|
||||
|
||||
return application
|
||||
|
||||
|
||||
def main():
|
||||
app = app_factory(None)
|
||||
|
||||
server = wsgi.Server(
|
||||
CONF, "your_app_name", app, host="0.0.0.0", port=6262, use_ssl=True
|
||||
)
|
||||
server.start()
|
||||
|
||||
# Keep the server running using eventlet sleep
|
||||
while True:
|
||||
eventlet.sleep(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -0,0 +1,47 @@
|
|||
# Copyright (c) 2012 OpenStack Foundation.
|
||||
# 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.
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_service import sslutils, wsgi_new
|
||||
|
||||
# Register SSL options
|
||||
CONF = cfg.CONF
|
||||
CONF(default_config_files=["env/oslo.conf"])
|
||||
sslutils.register_opts(CONF)
|
||||
|
||||
|
||||
def app_factory(global_config, **local_conf):
|
||||
def application(environ, start_response):
|
||||
status = "200 OK"
|
||||
headers = [("Content-type", "text/plain")]
|
||||
start_response(status, headers)
|
||||
response_body = b"Hello, World! - From Gunicorn OSLO"
|
||||
return [response_body]
|
||||
|
||||
return application
|
||||
|
||||
|
||||
def main():
|
||||
app = app_factory(None)
|
||||
|
||||
server = wsgi_new.Server(
|
||||
CONF, "your_app_name", app, host="0.0.0.0", port=6262, use_ssl=True
|
||||
)
|
||||
server.start()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
Loading…
Reference in New Issue