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:
Sharpz7 2024-04-05 01:13:24 +00:00
parent 5810d016e1
commit 7ccc6dc6ec
6 changed files with 307 additions and 1 deletions

View File

@ -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 = {

View File

@ -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,104 @@ 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):
# TODO(adamcarthur) convert this "eventlet disable" into a decorator??
import importlib
# Without this reload, you get
#==============================
# File "/usr/lib/python3.8/ssl.py",
# line 1349, in _real_connect
# self._sslobj = self.context._wrap_socket(
# TypeError: _wrap_socket() argument
# 'sock' must be _socket.socket, not SSLSocket
importlib.reload(socket)
# Without this reload, the test hangs.
importlib.reload(ssl)
# Without this, you get:
# ====================================
# File "/usr/lib/python3.8/ssl.py",
# line 1338, in do_handshake
# self._sslobj.do_handshake()
# ConnectionResetError: [Errno 104]
# Connection reset by peer
importlib.reload(os)
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:
error = e
time.sleep(1)
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."""

101
oslo_service/wsgi_new.py Normal file
View File

@ -0,0 +1,101 @@
# 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):
import importlib
import ssl
# Without this the test hangs
importlib.reload(os)
# Without this, you get:
# =============================
# File "/usr/lib/python3.8/ssl.py",
# line 1338, in do_handshake
# self._sslobj.do_handshake()
# ssl.SSLCertVerificationError:
# [SSL: CERTIFICATE_VERIFY_FAILED]
# certificate verify failed: unable to get local
# issuer certificate (_ssl.c:1131)
importlib.reload(ssl)
# 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
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

View File

@ -11,3 +11,4 @@ PasteDeploy>=1.5.0 # MIT
Routes>=2.3.1 # MIT
Paste>=2.0.2 # MIT
Yappi>=1.0 # MIT
gunicorn

55
temp/eventlet_wsgi.py Normal file
View File

@ -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()

47
temp/gunicorn_wsgi.py Normal file
View File

@ -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()