Merge "Consume sslutils and wsgi modules from oslo.service"

This commit is contained in:
Jenkins 2015-10-08 22:20:42 +00:00 committed by Gerrit Code Review
commit e739ba36ee
9 changed files with 53 additions and 315 deletions

View File

@ -50,6 +50,13 @@ Neutron API is not very stable, and there are cases when a desired change in
neutron tree is expected to trigger breakage for one or more external
repositories under the neutron tent. Below you can find a list of known
incompatible changes that could or are known to trigger those breakages.
The changes are listed in reverse chronological order (newer at the top).
* change: Consume sslutils and wsgi modules from oslo.service.
- commit: Ibfdf07e665fcfcd093a0e31274e1a6116706aec2
- solution: switch using oslo_service.wsgi.Router; stop using neutron.wsgi.Router.
- severity: Low (some out-of-tree plugins might be affected).
* change: oslo.service adopted.

View File

@ -344,15 +344,21 @@
# use_ssl = False
# Certificate file to use when starting API server securely
# This option is deprecated for removal in the N release, please
# use cert_file option from [ssl] section instead.
# ssl_cert_file = /path/to/certfile
# Private key file to use when starting API server securely
# This option is deprecated for removal in the N release, please
# use key_file option from [ssl] section instead.
# ssl_key_file = /path/to/keyfile
# CA certificate file to use when starting API server securely to
# verify connecting clients. This is an optional parameter only required if
# API clients need to authenticate to the API server using SSL certificates
# signed by a trusted CA
# This option is deprecated for removal in the N release, please
# use ca_file option from [ssl] section instead.
# ssl_ca_file = /path/to/cafile
# ======== end of WSGI parameters related to the API server ==========
@ -1038,3 +1044,21 @@ lock_path = $state_path/lock
[qos]
# Drivers list to use to send the update notification
# notification_drivers = message_queue
[ssl]
#
# From oslo.service.sslutils
#
# CA certificate file to use to verify connecting clients. (string
# value)
#ca_file = <None>
# Certificate file to use when starting the server securely. (string
# value)
#cert_file = <None>
# Private key file to use when starting the server securely. (string
# value)
#key_file = <None>

View File

@ -15,6 +15,7 @@
import httplib2
from oslo_config import cfg
from oslo_log import log as logging
from oslo_service import wsgi as base_wsgi
from oslo_utils import encodeutils
import six
import six.moves.urllib.parse as urlparse
@ -45,7 +46,7 @@ class NetworkMetadataProxyHandler(object):
if network_id is None and router_id is None:
raise exceptions.NetworkIdOrRouterIdRequiredError()
@webob.dec.wsgify(RequestClass=webob.Request)
@webob.dec.wsgify(RequestClass=base_wsgi.Request)
def __call__(self, req):
LOG.debug("Request: %s", req)
try:

View File

@ -15,6 +15,7 @@
from oslo_config import cfg
from oslo_log import log as logging
from oslo_service import wsgi as base_wsgi
import routes as routes_mapper
import six
import six.moves.urllib.parse as urlparse
@ -66,7 +67,7 @@ class Index(wsgi.Application):
return webob.Response(body=body, content_type=content_type)
class APIRouter(wsgi.Router):
class APIRouter(base_wsgi.Router):
@classmethod
def factory(cls, global_config, **local_config):

View File

@ -17,7 +17,6 @@
Routines for configuring Neutron
"""
import os
import sys
from keystoneclient import auth
@ -26,7 +25,8 @@ from oslo_config import cfg
from oslo_db import options as db_options
from oslo_log import log as logging
import oslo_messaging
from paste import deploy
from oslo_service import _options
from oslo_service import wsgi
from neutron.api.v2 import attributes
from neutron.common import utils
@ -42,8 +42,6 @@ core_opts = [
help=_("The host IP to bind to")),
cfg.IntOpt('bind_port', default=9696,
help=_("The port to bind to")),
cfg.StrOpt('api_paste_config', default="api-paste.ini",
help=_("The API paste config file to use")),
cfg.StrOpt('api_extensions_path', default="",
help=_("The path for API extensions")),
cfg.StrOpt('auth_strategy', default='keystone',
@ -153,6 +151,9 @@ core_cli_opts = [
# Register the configuration options
cfg.CONF.register_opts(core_opts)
cfg.CONF.register_cli_opts(core_cli_opts)
# TODO(eezhova): Replace it with wsgi.register_opts(CONF) when oslo.service
# 0.10.0 releases.
cfg.CONF.register_opts(_options.wsgi_opts)
# Ensure that the control exchange is set correctly
oslo_messaging.set_transport_defaults(control_exchange='neutron')
@ -232,24 +233,7 @@ def load_paste_app(app_name):
"""Builds and returns a WSGI app from a paste config file.
:param app_name: Name of the application to load
:raises ConfigFilesNotFoundError when config file cannot be located
:raises RuntimeError when application cannot be loaded from config file
"""
config_path = cfg.CONF.find_file(cfg.CONF.api_paste_config)
if not config_path:
raise cfg.ConfigFilesNotFoundError(
config_files=[cfg.CONF.api_paste_config])
config_path = os.path.abspath(config_path)
LOG.info(_LI("Config paste file: %s"), config_path)
try:
app = deploy.loadapp("config:%s" % config_path, name=app_name)
except (LookupError, ImportError):
msg = (_("Unable to load %(app_name)s from "
"configuration file %(config_path)s.") %
{'app_name': app_name,
'config_path': config_path})
LOG.exception(msg)
raise RuntimeError(msg)
loader = wsgi.Loader(cfg.CONF)
app = loader.load_app(app_name)
return app

View File

@ -19,6 +19,7 @@ import mock
from oslo_config import cfg
from oslo_log import log as logging
from oslo_serialization import jsonutils
from oslo_service import wsgi as base_wsgi
import routes
import six
import webob
@ -48,7 +49,7 @@ _get_path = test_base._get_path
extensions_path = ':'.join(neutron.tests.unit.extensions.__path__)
class ExtensionsTestApp(wsgi.Router):
class ExtensionsTestApp(base_wsgi.Router):
def __init__(self, options={}):
mapper = routes.Mapper()

View File

@ -1,31 +0,0 @@
# Copyright (c) 2012 OpenStack Foundation.
#
# 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 mock
from oslo_config import cfg
from neutron.common import config
from neutron.tests import base
class ConfigurationTest(base.BaseTestCase):
def test_load_paste_app_not_found(self):
self.config(api_paste_config='no_such_file.conf')
with mock.patch.object(cfg.CONF, 'find_file', return_value=None) as ff:
e = self.assertRaises(cfg.ConfigFilesNotFoundError,
config.load_paste_app, 'app')
ff.assert_called_once_with('no_such_file.conf')
self.assertEqual(['no_such_file.conf'], e.config_files)

View File

@ -713,139 +713,3 @@ class FaultTest(base.BaseTestCase):
"/", method='POST', headers={'Content-Type': "unknow"})
response = my_fault(request)
self.assertEqual(415, response.status_int)
class TestWSGIServerWithSSL(base.BaseTestCase):
"""WSGI server tests."""
def setUp(self):
super(TestWSGIServerWithSSL, self).setUp()
if six.PY3:
self.skip("bug/1482633")
@mock.patch("exceptions.RuntimeError")
@mock.patch("os.path.exists")
def test__check_ssl_settings(self, exists_mock, runtime_error_mock):
exists_mock.return_value = True
CONF.set_default('use_ssl', True)
CONF.set_default("ssl_cert_file", 'certificate.crt')
CONF.set_default("ssl_key_file", 'privatekey.key')
CONF.set_default("ssl_ca_file", 'cacert.pem')
wsgi.Server("test_app")
self.assertFalse(runtime_error_mock.called)
@mock.patch("os.path.exists")
def test__check_ssl_settings_no_ssl_cert_file_fails(self, exists_mock):
exists_mock.side_effect = [False]
CONF.set_default('use_ssl', True)
CONF.set_default("ssl_cert_file", "/no/such/file")
self.assertRaises(RuntimeError, wsgi.Server, "test_app")
@mock.patch("os.path.exists")
def test__check_ssl_settings_no_ssl_key_file_fails(self, exists_mock):
exists_mock.side_effect = [True, False]
CONF.set_default('use_ssl', True)
CONF.set_default("ssl_cert_file", 'certificate.crt')
CONF.set_default("ssl_key_file", "/no/such/file")
self.assertRaises(RuntimeError, wsgi.Server, "test_app")
@mock.patch("os.path.exists")
def test__check_ssl_settings_no_ssl_ca_file_fails(self, exists_mock):
exists_mock.side_effect = [True, True, False]
CONF.set_default('use_ssl', True)
CONF.set_default("ssl_cert_file", 'certificate.crt')
CONF.set_default("ssl_key_file", 'privatekey.key')
CONF.set_default("ssl_ca_file", "/no/such/file")
self.assertRaises(RuntimeError, wsgi.Server, "test_app")
@mock.patch("ssl.wrap_socket")
@mock.patch("os.path.exists")
def _test_wrap_ssl(self, exists_mock, wrap_socket_mock, **kwargs):
exists_mock.return_value = True
sock = mock.Mock()
CONF.set_default("ssl_cert_file", 'certificate.crt')
CONF.set_default("ssl_key_file", 'privatekey.key')
ssl_kwargs = {'server_side': True,
'certfile': CONF.ssl_cert_file,
'keyfile': CONF.ssl_key_file,
'cert_reqs': ssl.CERT_NONE,
}
if kwargs:
ssl_kwargs.update(**kwargs)
server = wsgi.Server("test_app")
server.wrap_ssl(sock)
wrap_socket_mock.assert_called_once_with(sock, **ssl_kwargs)
def test_wrap_ssl(self):
self._test_wrap_ssl()
def test_wrap_ssl_ca_file(self):
CONF.set_default("ssl_ca_file", 'cacert.pem')
ssl_kwargs = {'ca_certs': CONF.ssl_ca_file,
'cert_reqs': ssl.CERT_REQUIRED
}
self._test_wrap_ssl(**ssl_kwargs)
def test_app_using_ssl(self):
CONF.set_default('use_ssl', True)
CONF.set_default("ssl_cert_file",
os.path.join(TEST_VAR_DIR, 'certificate.crt'))
CONF.set_default("ssl_key_file",
os.path.join(TEST_VAR_DIR, 'privatekey.key'))
greetings = 'Hello, World!!!'
@webob.dec.wsgify
def hello_world(req):
return greetings
server = wsgi.Server("test_app")
server.start(hello_world, 0, host="127.0.0.1")
response = open_no_proxy('https://127.0.0.1:%d/' % server.port)
self.assertEqual(greetings, response.read())
server.stop()
def test_app_using_ssl_combined_cert_and_key(self):
CONF.set_default('use_ssl', True)
CONF.set_default("ssl_cert_file",
os.path.join(TEST_VAR_DIR, 'certandkey.pem'))
greetings = 'Hello, World!!!'
@webob.dec.wsgify
def hello_world(req):
return greetings
server = wsgi.Server("test_app")
server.start(hello_world, 0, host="127.0.0.1")
response = open_no_proxy('https://127.0.0.1:%d/' % server.port)
self.assertEqual(greetings, response.read())
server.stop()
def test_app_using_ipv6_and_ssl(self):
CONF.set_default('use_ssl', True)
CONF.set_default("ssl_cert_file",
os.path.join(TEST_VAR_DIR, 'certificate.crt'))
CONF.set_default("ssl_key_file",
os.path.join(TEST_VAR_DIR, 'privatekey.key'))
greetings = 'Hello, World!!!'
@webob.dec.wsgify
def hello_world(req):
return greetings
server = wsgi.Server("test_app")
server.start(hello_world, 0, host="::1")
response = open_no_proxy('https://[::1]:%d/' % server.port)
self.assertEqual(greetings, response.read())
server.stop()

View File

@ -19,9 +19,7 @@ Utility methods for working with WSGI servers
from __future__ import print_function
import errno
import os
import socket
import ssl
import sys
import time
@ -30,10 +28,12 @@ from oslo_config import cfg
import oslo_i18n
from oslo_log import log as logging
from oslo_serialization import jsonutils
from oslo_service import _options
from oslo_service import service as common_service
from oslo_service import sslutils
from oslo_service import systemd
from oslo_service import wsgi
from oslo_utils import excutils
import routes.middleware
import six
import webob.dec
import webob.exc
@ -50,44 +50,19 @@ socket_opts = [
default=4096,
help=_("Number of backlog requests to configure "
"the socket with")),
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('retry_until_window',
default=30,
help=_("Number of seconds to keep retrying to listen")),
cfg.IntOpt('max_header_line',
default=16384,
help=_("Max header line to accommodate large tokens")),
cfg.BoolOpt('use_ssl',
default=False,
help=_('Enable SSL on the API server')),
cfg.StrOpt('ssl_ca_file',
help=_("CA certificate file to use to verify "
"connecting clients")),
cfg.StrOpt('ssl_cert_file',
help=_("Certificate file to use when starting "
"the server securely")),
cfg.StrOpt('ssl_key_file',
help=_("Private key file to use when starting "
"the server securely")),
cfg.BoolOpt('wsgi_keep_alive',
default=True,
help=_("Determines if connections are allowed to be held "
"open by clients after a request is fulfilled. A value "
"of False will ensure that the socket connection will "
"be explicitly closed once a response has been sent to "
"the client.")),
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.")),
]
CONF = cfg.CONF
CONF.register_opts(socket_opts)
# TODO(eezhova): Replace it with wsgi.register_opts(CONF) when oslo.service
# 0.10.0 releases.
CONF.register_opts(_options.wsgi_opts)
LOG = logging.getLogger(__name__)
@ -118,7 +93,7 @@ class WorkerService(worker.NeutronWorker):
# Duplicate a socket object to keep a file descriptor usable.
dup_sock = self._service._socket.dup()
if CONF.use_ssl:
dup_sock = self._service.wrap_ssl(dup_sock)
dup_sock = sslutils.wrap(CONF, dup_sock)
self._server = self._service.pool.spawn(self._service._run,
self._application,
dup_sock)
@ -152,7 +127,7 @@ class Server(object):
# wsgi server to wait forever.
self.client_socket_timeout = CONF.client_socket_timeout or None
if CONF.use_ssl:
self._check_ssl_settings()
sslutils.is_enabled(CONF)
def _get_socket(self, host, port, backlog):
bind_addr = (host, port)
@ -201,37 +176,6 @@ class Server(object):
return sock
@staticmethod
def _check_ssl_settings():
if not os.path.exists(CONF.ssl_cert_file):
raise RuntimeError(_("Unable to find ssl_cert_file "
": %s") % CONF.ssl_cert_file)
# ssl_key_file is optional because the key may be embedded in the
# certificate file
if CONF.ssl_key_file and not os.path.exists(CONF.ssl_key_file):
raise RuntimeError(_("Unable to find "
"ssl_key_file : %s") % CONF.ssl_key_file)
# ssl_ca_file is optional
if CONF.ssl_ca_file and not os.path.exists(CONF.ssl_ca_file):
raise RuntimeError(_("Unable to find ssl_ca_file "
": %s") % CONF.ssl_ca_file)
@staticmethod
def wrap_ssl(sock):
ssl_kwargs = {'server_side': True,
'certfile': CONF.ssl_cert_file,
'keyfile': CONF.ssl_key_file,
'cert_reqs': ssl.CERT_NONE,
}
if CONF.ssl_ca_file:
ssl_kwargs['ca_certs'] = CONF.ssl_ca_file
ssl_kwargs['cert_reqs'] = ssl.CERT_REQUIRED
return ssl.wrap_socket(sock, **ssl_kwargs)
def start(self, application, port, host='0.0.0.0', workers=0):
"""Run a WSGI server with the given application."""
self._host = host
@ -353,7 +297,7 @@ class Middleware(object):
return self.process_response(response)
class Request(webob.Request):
class Request(wsgi.Request):
def best_match_content_type(self):
"""Determine the most acceptable content-type.
@ -710,63 +654,6 @@ class Debug(Middleware):
print()
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 a wsgi.Controller, who will route
the request to the action 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("network", "networks", controller=nc)
# 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
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 a Request.
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:
language = req.best_match_language()
msg = _('The resource could not be found.')
msg = oslo_i18n.translate(msg, language)
return webob.exc.HTTPNotFound(explanation=msg)
app = match['controller']
return app
class Resource(Application):
"""WSGI app that handles (de)serialization and controller dispatch.