Merge "Remove nova.wsgi module"

This commit is contained in:
Zuul 2024-03-19 19:42:00 +00:00 committed by Gerrit Code Review
commit 818f0cd4a3
8 changed files with 555 additions and 584 deletions

View File

@ -153,7 +153,7 @@ def get_sort_params(input_params, default_key='created_at',
The input parameters are not modified.
:param input_params: webob.multidict of request parameters (from
nova.wsgi.Request.params)
nova.api.wsgi.Request.params)
:param default_key: default sort key value, added to the list if no
'sort_key' parameters are supplied
:param default_dir: default sort dir value, added to the list if no

View File

@ -880,5 +880,8 @@ class APIRouterV21(base_wsgi.Router):
@classmethod
def factory(cls, global_config, **local_config):
"""Simple paste factory, :class:`nova.wsgi.Router` doesn't have one."""
"""Simple paste factory.
:class:`nova.api.wsgi.Router` doesn't have one.
"""
return cls()

View File

@ -18,13 +18,20 @@
"""Generic Node base class for all workers that run on hosts."""
import os
import os.path
import random
import socket
import ssl
import sys
import eventlet
import eventlet.wsgi
import greenlet
from oslo_concurrency import processutils
from oslo_log import log as logging
import oslo_messaging as messaging
from oslo_service import service
from oslo_utils import excutils
from oslo_utils import importutils
from nova.api import wsgi as api_wsgi
@ -42,15 +49,12 @@ from nova import rpc
from nova import servicegroup
from nova import utils
from nova import version
from nova import wsgi
osprofiler = importutils.try_import("osprofiler")
osprofiler_initializer = importutils.try_import("osprofiler.initializer")
LOG = logging.getLogger(__name__)
CONF = nova.conf.CONF
LOG = logging.getLogger(__name__)
SERVICE_MANAGERS = {
'nova-compute': 'nova.compute.manager.ComputeManager',
@ -323,6 +327,193 @@ class Service(service.Service):
context.CELL_CACHE = {}
class WSGIServer(service.ServiceBase):
"""Server class to manage a WSGI server, serving a WSGI application."""
default_pool_size = CONF.wsgi.default_pool_size
def __init__(self, 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: nova.exception.InvalidInput
"""
# Allow operators to customize http requests max header line size.
eventlet.wsgi.MAX_HEADER_LINE = CONF.wsgi.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("nova.%s.wsgi.server" % self.name)
self._use_ssl = use_ssl
self._max_url_len = max_url_len
self.client_socket_timeout = CONF.wsgi.client_socket_timeout or None
if backlog < 1:
raise exception.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
try:
self._socket = eventlet.listen(bind_addr, family, backlog=backlog)
except EnvironmentError:
LOG.error("Could not bind to %(host)s:%(port)s",
{'host': host, 'port': port})
raise
(self.host, self.port) = self._socket.getsockname()[0:2]
LOG.info("%(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.
dup_socket = self._socket.dup()
dup_socket.setsockopt(socket.SOL_SOCKET,
socket.SO_REUSEADDR, 1)
# sockets can hang around forever without keepalive
dup_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'):
dup_socket.setsockopt(socket.IPPROTO_TCP,
socket.TCP_KEEPIDLE,
CONF.wsgi.tcp_keepidle)
if self._use_ssl:
try:
ca_file = CONF.wsgi.ssl_ca_file
cert_file = CONF.wsgi.ssl_cert_file
key_file = CONF.wsgi.ssl_key_file
if cert_file and not os.path.exists(cert_file):
raise RuntimeError(
_("Unable to find cert_file : %s") % cert_file)
if ca_file and not os.path.exists(ca_file):
raise RuntimeError(
_("Unable to find ca_file : %s") % ca_file)
if key_file and not os.path.exists(key_file):
raise RuntimeError(
_("Unable to find key_file : %s") % key_file)
if self._use_ssl and (not cert_file or not key_file):
raise RuntimeError(
_("When running server in SSL mode, you must "
"specify both a cert_file and key_file "
"option value in your configuration file"))
ssl_kwargs = {
'server_side': True,
'certfile': cert_file,
'keyfile': key_file,
'cert_reqs': ssl.CERT_NONE,
}
if CONF.wsgi.ssl_ca_file:
ssl_kwargs['ca_certs'] = ca_file
ssl_kwargs['cert_reqs'] = ssl.CERT_REQUIRED
dup_socket = eventlet.wrap_ssl(dup_socket,
**ssl_kwargs)
except Exception:
with excutils.save_and_reraise_exception():
LOG.error(
"Failed to start %(name)s on %(host)s:%(port)s with "
"SSL support",
{'name': self.name, 'host': self.host,
'port': self.port})
wsgi_kwargs = {
'func': eventlet.wsgi.server,
'sock': dup_socket,
'site': self.app,
'protocol': self._protocol,
'custom_pool': self._pool,
'log': self._logger,
'log_format': CONF.wsgi.wsgi_log_format,
'debug': False,
'keepalive': 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 = utils.spawn(**wsgi_kwargs)
def reset(self):
"""Reset server greenpool size to default.
:returns: None
"""
self._pool.resize(self.pool_size)
def stop(self):
"""Stop this server.
This is not a very nice action, as currently the method by which a
server is stopped is by killing its eventlet.
:returns: None
"""
LOG.info("Stopping WSGI server.")
if self._server is not None:
# Resize pool to stop new requests from being processed
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:
self._pool.waitall()
self._server.wait()
except greenlet.GreenletExit:
LOG.info("WSGI server has stopped.")
class WSGIService(service.Service):
"""Provides ability to launch API from a 'paste' configuration."""
@ -363,12 +554,14 @@ class WSGIService(service.Service):
'workers': str(self.workers)})
raise exception.InvalidInput(msg)
self.use_ssl = use_ssl
self.server = wsgi.Server(name,
self.app,
host=self.host,
port=self.port,
use_ssl=self.use_ssl,
max_url_len=max_url_len)
self.server = WSGIServer(
name,
self.app,
host=self.host,
port=self.port,
use_ssl=self.use_ssl,
max_url_len=max_url_len,
)
# Pull back actual port used
self.port = self.server.port
self.backdoor_port = None

View File

@ -19,19 +19,17 @@
Test WSGI basics and provide some helper functions for other WSGI tests.
"""
import sys
from unittest import mock
import tempfile
import routes
import webob
from nova.api.openstack import wsgi_app
from nova.api import wsgi
import nova.exception
from nova import test
from nova import utils
class Test(test.NoDBTestCase):
class TestRouter(test.NoDBTestCase):
def test_router(self):
@ -55,14 +53,60 @@ class Test(test.NoDBTestCase):
result = webob.Request.blank('/bad').get_response(Router())
self.assertNotEqual(result.body, "Router result")
@mock.patch('nova.api.openstack.wsgi_app._setup_service', new=mock.Mock())
@mock.patch('paste.deploy.loadapp', new=mock.Mock())
def test_init_application_passes_sys_argv_to_config(self):
with utils.temporary_mutation(sys, argv=mock.sentinel.argv):
with mock.patch('nova.config.parse_args') as mock_parse_args:
wsgi_app.init_application('test-app')
mock_parse_args.assert_called_once_with(
mock.sentinel.argv,
default_config_files=[
'/etc/nova/api-paste.ini', '/etc/nova/nova.conf'])
class TestLoaderNothingExists(test.NoDBTestCase):
"""Loader tests where os.path.exists always returns False."""
def setUp(self):
super(TestLoaderNothingExists, self).setUp()
self.stub_out('os.path.exists', lambda _: False)
def test_relpath_config_not_found(self):
self.flags(api_paste_config='api-paste.ini', group='wsgi')
self.assertRaises(
nova.exception.ConfigNotFound,
wsgi.Loader,
)
def test_asbpath_config_not_found(self):
self.flags(api_paste_config='/etc/nova/api-paste.ini', group='wsgi')
self.assertRaises(
nova.exception.ConfigNotFound,
wsgi.Loader,
)
class TestLoaderNormalFilesystem(test.NoDBTestCase):
"""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.config = tempfile.NamedTemporaryFile(mode="w+t")
self.config.write(self._paste_config.lstrip())
self.config.seek(0)
self.config.flush()
self.loader = wsgi.Loader(self.config.name)
def test_config_found(self):
self.assertEqual(self.config.name, self.loader.config_path)
def test_app_not_found(self):
self.assertRaises(
nova.exception.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.config.close()
super(TestLoaderNormalFilesystem, self).tearDown()

View File

@ -0,0 +1,38 @@
# 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.
import sys
from unittest import mock
from nova.api.openstack import wsgi_app
from nova import test
from nova import utils
class TestInitApplication(test.NoDBTestCase):
@mock.patch('nova.api.openstack.wsgi_app._setup_service', new=mock.Mock())
@mock.patch('paste.deploy.loadapp', new=mock.Mock())
def test_init_application_passes_sys_argv_to_config(self):
with utils.temporary_mutation(sys, argv=mock.sentinel.argv):
with mock.patch('nova.config.parse_args') as mock_parse_args:
wsgi_app.init_application('test-app')
mock_parse_args.assert_called_once_with(
mock.sentinel.argv,
default_config_files=[
'/etc/nova/api-paste.ini', '/etc/nova/nova.conf'])

View File

@ -18,12 +18,18 @@
Unit Tests for remote procedure calls using queue
"""
import os.path
import socket
from unittest import mock
import eventlet
import eventlet.wsgi
from oslo_concurrency import processutils
from oslo_config import cfg
from oslo_service import service as _service
import requests
import testtools
import webob
from nova import exception
from nova import manager
@ -42,6 +48,9 @@ test_service_opts = [
default=0,
help="Port number to bind test service to"),
]
SSL_CERT_DIR = os.path.normpath(
os.path.join(os.path.dirname(os.path.abspath(__file__)), 'ssl_cert')
)
CONF = cfg.CONF
CONF.register_opts(test_service_opts)
@ -314,6 +323,247 @@ class ServiceTestCase(test.NoDBTestCase):
mock_check_old.assert_has_calls([mock.call(), mock.call()])
class TestWSGIServer(test.NoDBTestCase):
"""WSGI server tests."""
def test_no_app(self):
server = service.WSGIServer("test_app", None)
self.assertEqual("test_app", server.name)
def test_custom_max_header_line(self):
self.flags(max_header_line=4096, group='wsgi') # Default is 16384
service.WSGIServer("test_custom_max_header_line", None)
self.assertEqual(CONF.wsgi.max_header_line,
eventlet.wsgi.MAX_HEADER_LINE)
def test_start_random_port(self):
server = service.WSGIServer(
"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 utils.is_ipv6_supported(), "no ipv6 support")
def test_start_random_port_with_ipv6(self):
server = service.WSGIServer(
"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(not utils.is_linux(), 'SO_REUSEADDR behaves differently '
'on OSX and BSD, see bugs '
'1436895 and 1467145')
def test_socket_options_for_simple_server(self):
# test normal socket options has set properly
self.flags(tcp_keepidle=500, group='wsgi')
server = service.WSGIServer(
"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(CONF.wsgi.tcp_keepidle,
sock.getsockopt(socket.IPPROTO_TCP,
socket.TCP_KEEPIDLE))
server.stop()
server.wait()
def test_server_pool_waitall(self):
# test pools waitall method gets called while stopping server
server = service.WSGIServer(
"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):
server = service.WSGIServer(
"test_uri_length_limit", None,
host="127.0.0.1", max_url_len=16384)
server.start()
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 = service.WSGIServer(
"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.addCleanup(server.stop)
self.assertEqual(server._pool.size, CONF.wsgi.default_pool_size)
def test_client_socket_timeout(self):
self.flags(client_socket_timeout=5, group='wsgi')
# mocking eventlet spawn method to check it is called with
# configured 'client_socket_timeout' value.
with mock.patch('nova.utils.spawn') as mock_spawn:
server = service.WSGIServer(
"test_app", None,
host="127.0.0.1", port=0)
server.start()
self.addCleanup(server.stop)
_, kwargs = mock_spawn.call_args
self.assertEqual(CONF.wsgi.client_socket_timeout,
kwargs['socket_timeout'])
def test_keep_alive(self):
self.flags(keep_alive=False, group='wsgi')
# mocking eventlet spawn method to check it is called with
# configured 'keep_alive' value.
with mock.patch('nova.utils.spawn') as mock_spawn:
server = service.WSGIServer(
"test_app", None,
host="127.0.0.1", port=0)
server.start()
self.addCleanup(server.stop)
_, kwargs = mock_spawn.call_args
self.assertEqual(CONF.wsgi.keep_alive,
kwargs['keepalive'])
@testtools.skip("bug/1482633: test hangs on Python 3")
class TestWSGIServerWithSSL(test.NoDBTestCase):
"""WSGI server with SSL tests."""
def setUp(self):
super(TestWSGIServerWithSSL, self).setUp()
self.flags(enabled_ssl_apis=['fake_ssl'])
self.flags(
ssl_cert_file=os.path.join(SSL_CERT_DIR, 'certificate.crt'),
ssl_key_file=os.path.join(SSL_CERT_DIR, 'privatekey.key'),
group='wsgi')
def test_ssl_server(self):
def test_app(env, start_response):
start_response('200 OK', {})
return ['PONG']
fake_ssl_server = service.WSGIServer(
"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)
response = requests.post(
'https://127.0.0.1:%s/' % fake_ssl_server.port,
verify=os.path.join(SSL_CERT_DIR, 'ca.crt'), data='PING')
self.assertEqual(response.text, '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 = service.WSGIServer(
"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 = service.WSGIServer(
"fake", test_app,
host="127.0.0.1", port=0)
fake_server.start()
self.assertNotEqual(0, fake_server.port)
response = requests.post(
'https://127.0.0.1:%s/' % fake_ssl_server.port,
verify=os.path.join(SSL_CERT_DIR, 'ca.crt'), data='PING')
self.assertEqual(response.text, 'PONG')
response = requests.post('http://127.0.0.1:%s/' % fake_server.port,
data='PING')
self.assertEqual(response.text, 'PONG')
fake_ssl_server.stop()
fake_ssl_server.wait()
fake_server.stop()
fake_server.wait()
@testtools.skipIf(not utils.is_linux(), 'SO_REUSEADDR behaves differently '
'on OSX and BSD, see bugs '
'1436895 and 1467145')
def test_socket_options_for_ssl_server(self):
# test normal socket options has set properly
self.flags(tcp_keepidle=500, group='wsgi')
server = service.WSGIServer(
"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.wsgi.tcp_keepidle,
sock.getsockopt(socket.IPPROTO_TCP,
socket.TCP_KEEPIDLE))
server.stop()
server.wait()
@testtools.skipIf(not utils.is_ipv6_supported(), "no ipv6 support")
def test_app_using_ipv6_and_ssl(self):
greetings = 'Hello, World!!!'
@webob.dec.wsgify
def hello_world(req):
return greetings
server = service.WSGIServer(
"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()
class TestWSGIService(test.NoDBTestCase):
def setUp(self):

View File

@ -1,332 +0,0 @@
# 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 `nova.wsgi`."""
import os.path
import socket
import tempfile
from unittest import mock
import eventlet
import eventlet.wsgi
from oslo_config import cfg
import requests
import testtools
import webob
import nova.api.wsgi
import nova.exception
from nova import test
from nova.tests.unit import utils
import nova.wsgi
SSL_CERT_DIR = os.path.normpath(os.path.join(
os.path.dirname(os.path.abspath(__file__)),
'ssl_cert'))
CONF = cfg.CONF
class TestLoaderNothingExists(test.NoDBTestCase):
"""Loader tests where os.path.exists always returns False."""
def setUp(self):
super(TestLoaderNothingExists, self).setUp()
self.stub_out('os.path.exists', lambda _: False)
def test_relpath_config_not_found(self):
self.flags(api_paste_config='api-paste.ini', group='wsgi')
self.assertRaises(
nova.exception.ConfigNotFound,
nova.api.wsgi.Loader,
)
def test_asbpath_config_not_found(self):
self.flags(api_paste_config='/etc/nova/api-paste.ini', group='wsgi')
self.assertRaises(
nova.exception.ConfigNotFound,
nova.api.wsgi.Loader,
)
class TestLoaderNormalFilesystem(test.NoDBTestCase):
"""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.config = tempfile.NamedTemporaryFile(mode="w+t")
self.config.write(self._paste_config.lstrip())
self.config.seek(0)
self.config.flush()
self.loader = nova.api.wsgi.Loader(self.config.name)
def test_config_found(self):
self.assertEqual(self.config.name, self.loader.config_path)
def test_app_not_found(self):
self.assertRaises(
nova.exception.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.config.close()
super(TestLoaderNormalFilesystem, self).tearDown()
class TestWSGIServer(test.NoDBTestCase):
"""WSGI server tests."""
def test_no_app(self):
server = nova.wsgi.Server("test_app", None)
self.assertEqual("test_app", server.name)
def test_custom_max_header_line(self):
self.flags(max_header_line=4096, group='wsgi') # Default is 16384
nova.wsgi.Server("test_custom_max_header_line", None)
self.assertEqual(CONF.wsgi.max_header_line,
eventlet.wsgi.MAX_HEADER_LINE)
def test_start_random_port(self):
server = nova.wsgi.Server("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 utils.is_ipv6_supported(), "no ipv6 support")
def test_start_random_port_with_ipv6(self):
server = nova.wsgi.Server("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(not utils.is_linux(), 'SO_REUSEADDR behaves differently '
'on OSX and BSD, see bugs '
'1436895 and 1467145')
def test_socket_options_for_simple_server(self):
# test normal socket options has set properly
self.flags(tcp_keepidle=500, group='wsgi')
server = nova.wsgi.Server("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(CONF.wsgi.tcp_keepidle,
sock.getsockopt(socket.IPPROTO_TCP,
socket.TCP_KEEPIDLE))
server.stop()
server.wait()
def test_server_pool_waitall(self):
# test pools waitall method gets called while stopping server
server = nova.wsgi.Server("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):
server = nova.wsgi.Server("test_uri_length_limit", None,
host="127.0.0.1", max_url_len=16384)
server.start()
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 = nova.wsgi.Server("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.addCleanup(server.stop)
self.assertEqual(server._pool.size, CONF.wsgi.default_pool_size)
def test_client_socket_timeout(self):
self.flags(client_socket_timeout=5, group='wsgi')
# mocking eventlet spawn method to check it is called with
# configured 'client_socket_timeout' value.
with mock.patch('nova.utils.spawn') as mock_spawn:
server = nova.wsgi.Server("test_app", None,
host="127.0.0.1", port=0)
server.start()
self.addCleanup(server.stop)
_, kwargs = mock_spawn.call_args
self.assertEqual(CONF.wsgi.client_socket_timeout,
kwargs['socket_timeout'])
def test_keep_alive(self):
self.flags(keep_alive=False, group='wsgi')
# mocking eventlet spawn method to check it is called with
# configured 'keep_alive' value.
with mock.patch('nova.utils.spawn') as mock_spawn:
server = nova.wsgi.Server("test_app", None,
host="127.0.0.1", port=0)
server.start()
self.addCleanup(server.stop)
_, kwargs = mock_spawn.call_args
self.assertEqual(CONF.wsgi.keep_alive,
kwargs['keepalive'])
@testtools.skip("bug/1482633: test hangs on Python 3")
class TestWSGIServerWithSSL(test.NoDBTestCase):
"""WSGI server with SSL tests."""
def setUp(self):
super(TestWSGIServerWithSSL, self).setUp()
self.flags(enabled_ssl_apis=['fake_ssl'])
self.flags(
ssl_cert_file=os.path.join(SSL_CERT_DIR, 'certificate.crt'),
ssl_key_file=os.path.join(SSL_CERT_DIR, 'privatekey.key'),
group='wsgi')
def test_ssl_server(self):
def test_app(env, start_response):
start_response('200 OK', {})
return ['PONG']
fake_ssl_server = nova.wsgi.Server("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)
response = requests.post(
'https://127.0.0.1:%s/' % fake_ssl_server.port,
verify=os.path.join(SSL_CERT_DIR, 'ca.crt'), data='PING')
self.assertEqual(response.text, '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 = nova.wsgi.Server("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 = nova.wsgi.Server("fake", test_app,
host="127.0.0.1", port=0)
fake_server.start()
self.assertNotEqual(0, fake_server.port)
response = requests.post(
'https://127.0.0.1:%s/' % fake_ssl_server.port,
verify=os.path.join(SSL_CERT_DIR, 'ca.crt'), data='PING')
self.assertEqual(response.text, 'PONG')
response = requests.post('http://127.0.0.1:%s/' % fake_server.port,
data='PING')
self.assertEqual(response.text, 'PONG')
fake_ssl_server.stop()
fake_ssl_server.wait()
fake_server.stop()
fake_server.wait()
@testtools.skipIf(not utils.is_linux(), 'SO_REUSEADDR behaves differently '
'on OSX and BSD, see bugs '
'1436895 and 1467145')
def test_socket_options_for_ssl_server(self):
# test normal socket options has set properly
self.flags(tcp_keepidle=500, group='wsgi')
server = nova.wsgi.Server("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.wsgi.tcp_keepidle,
sock.getsockopt(socket.IPPROTO_TCP,
socket.TCP_KEEPIDLE))
server.stop()
server.wait()
@testtools.skipIf(not utils.is_ipv6_supported(), "no ipv6 support")
def test_app_using_ipv6_and_ssl(self):
greetings = 'Hello, World!!!'
@webob.dec.wsgify
def hello_world(req):
return greetings
server = nova.wsgi.Server("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()

View File

@ -1,225 +0,0 @@
# 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."""
import os.path
import socket
import ssl
import eventlet
import eventlet.wsgi
import greenlet
from oslo_log import log as logging
from oslo_service import service
from oslo_utils import excutils
import nova.conf
from nova import exception
from nova.i18n import _
from nova import utils
CONF = nova.conf.CONF
LOG = logging.getLogger(__name__)
class Server(service.ServiceBase):
"""Server class to manage a WSGI server, serving a WSGI application."""
default_pool_size = CONF.wsgi.default_pool_size
def __init__(self, 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: nova.exception.InvalidInput
"""
# Allow operators to customize http requests max header line size.
eventlet.wsgi.MAX_HEADER_LINE = CONF.wsgi.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("nova.%s.wsgi.server" % self.name)
self._use_ssl = use_ssl
self._max_url_len = max_url_len
self.client_socket_timeout = CONF.wsgi.client_socket_timeout or None
if backlog < 1:
raise exception.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
try:
self._socket = eventlet.listen(bind_addr, family, backlog=backlog)
except EnvironmentError:
LOG.error("Could not bind to %(host)s:%(port)s",
{'host': host, 'port': port})
raise
(self.host, self.port) = self._socket.getsockname()[0:2]
LOG.info("%(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.
dup_socket = self._socket.dup()
dup_socket.setsockopt(socket.SOL_SOCKET,
socket.SO_REUSEADDR, 1)
# sockets can hang around forever without keepalive
dup_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'):
dup_socket.setsockopt(socket.IPPROTO_TCP,
socket.TCP_KEEPIDLE,
CONF.wsgi.tcp_keepidle)
if self._use_ssl:
try:
ca_file = CONF.wsgi.ssl_ca_file
cert_file = CONF.wsgi.ssl_cert_file
key_file = CONF.wsgi.ssl_key_file
if cert_file and not os.path.exists(cert_file):
raise RuntimeError(
_("Unable to find cert_file : %s") % cert_file)
if ca_file and not os.path.exists(ca_file):
raise RuntimeError(
_("Unable to find ca_file : %s") % ca_file)
if key_file and not os.path.exists(key_file):
raise RuntimeError(
_("Unable to find key_file : %s") % key_file)
if self._use_ssl and (not cert_file or not key_file):
raise RuntimeError(
_("When running server in SSL mode, you must "
"specify both a cert_file and key_file "
"option value in your configuration file"))
ssl_kwargs = {
'server_side': True,
'certfile': cert_file,
'keyfile': key_file,
'cert_reqs': ssl.CERT_NONE,
}
if CONF.wsgi.ssl_ca_file:
ssl_kwargs['ca_certs'] = ca_file
ssl_kwargs['cert_reqs'] = ssl.CERT_REQUIRED
dup_socket = eventlet.wrap_ssl(dup_socket,
**ssl_kwargs)
except Exception:
with excutils.save_and_reraise_exception():
LOG.error(
"Failed to start %(name)s on %(host)s:%(port)s with "
"SSL support",
{'name': self.name, 'host': self.host,
'port': self.port})
wsgi_kwargs = {
'func': eventlet.wsgi.server,
'sock': dup_socket,
'site': self.app,
'protocol': self._protocol,
'custom_pool': self._pool,
'log': self._logger,
'log_format': CONF.wsgi.wsgi_log_format,
'debug': False,
'keepalive': 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 = utils.spawn(**wsgi_kwargs)
def reset(self):
"""Reset server greenpool size to default.
:returns: None
"""
self._pool.resize(self.pool_size)
def stop(self):
"""Stop this server.
This is not a very nice action, as currently the method by which a
server is stopped is by killing its eventlet.
:returns: None
"""
LOG.info("Stopping WSGI server.")
if self._server is not None:
# Resize pool to stop new requests from being processed
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:
self._pool.waitall()
self._server.wait()
except greenlet.GreenletExit:
LOG.info("WSGI server has stopped.")