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 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 repositories under the neutron tent. Below you can find a list of known
incompatible changes that could or are known to trigger those breakages. 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. * change: oslo.service adopted.

View File

@ -344,15 +344,21 @@
# use_ssl = False # use_ssl = False
# Certificate file to use when starting API server securely # 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 # ssl_cert_file = /path/to/certfile
# Private key file to use when starting API server securely # 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 # ssl_key_file = /path/to/keyfile
# CA certificate file to use when starting API server securely to # CA certificate file to use when starting API server securely to
# verify connecting clients. This is an optional parameter only required if # verify connecting clients. This is an optional parameter only required if
# API clients need to authenticate to the API server using SSL certificates # API clients need to authenticate to the API server using SSL certificates
# signed by a trusted CA # 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 # ssl_ca_file = /path/to/cafile
# ======== end of WSGI parameters related to the API server ========== # ======== end of WSGI parameters related to the API server ==========
@ -1038,3 +1044,21 @@ lock_path = $state_path/lock
[qos] [qos]
# Drivers list to use to send the update notification # Drivers list to use to send the update notification
# notification_drivers = message_queue # 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 import httplib2
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log as logging from oslo_log import log as logging
from oslo_service import wsgi as base_wsgi
from oslo_utils import encodeutils from oslo_utils import encodeutils
import six import six
import six.moves.urllib.parse as urlparse import six.moves.urllib.parse as urlparse
@ -45,7 +46,7 @@ class NetworkMetadataProxyHandler(object):
if network_id is None and router_id is None: if network_id is None and router_id is None:
raise exceptions.NetworkIdOrRouterIdRequiredError() raise exceptions.NetworkIdOrRouterIdRequiredError()
@webob.dec.wsgify(RequestClass=webob.Request) @webob.dec.wsgify(RequestClass=base_wsgi.Request)
def __call__(self, req): def __call__(self, req):
LOG.debug("Request: %s", req) LOG.debug("Request: %s", req)
try: try:

View File

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

View File

@ -17,7 +17,6 @@
Routines for configuring Neutron Routines for configuring Neutron
""" """
import os
import sys import sys
from keystoneclient import auth from keystoneclient import auth
@ -26,7 +25,8 @@ from oslo_config import cfg
from oslo_db import options as db_options from oslo_db import options as db_options
from oslo_log import log as logging from oslo_log import log as logging
import oslo_messaging 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.api.v2 import attributes
from neutron.common import utils from neutron.common import utils
@ -42,8 +42,6 @@ core_opts = [
help=_("The host IP to bind to")), help=_("The host IP to bind to")),
cfg.IntOpt('bind_port', default=9696, cfg.IntOpt('bind_port', default=9696,
help=_("The port to bind to")), 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="", cfg.StrOpt('api_extensions_path', default="",
help=_("The path for API extensions")), help=_("The path for API extensions")),
cfg.StrOpt('auth_strategy', default='keystone', cfg.StrOpt('auth_strategy', default='keystone',
@ -153,6 +151,9 @@ core_cli_opts = [
# Register the configuration options # Register the configuration options
cfg.CONF.register_opts(core_opts) cfg.CONF.register_opts(core_opts)
cfg.CONF.register_cli_opts(core_cli_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 # Ensure that the control exchange is set correctly
oslo_messaging.set_transport_defaults(control_exchange='neutron') 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. """Builds and returns a WSGI app from a paste config file.
:param app_name: Name of the application to load :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
""" """
loader = wsgi.Loader(cfg.CONF)
config_path = cfg.CONF.find_file(cfg.CONF.api_paste_config) app = loader.load_app(app_name)
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)
return app return app

View File

@ -19,6 +19,7 @@ import mock
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log as logging from oslo_log import log as logging
from oslo_serialization import jsonutils from oslo_serialization import jsonutils
from oslo_service import wsgi as base_wsgi
import routes import routes
import six import six
import webob import webob
@ -48,7 +49,7 @@ _get_path = test_base._get_path
extensions_path = ':'.join(neutron.tests.unit.extensions.__path__) extensions_path = ':'.join(neutron.tests.unit.extensions.__path__)
class ExtensionsTestApp(wsgi.Router): class ExtensionsTestApp(base_wsgi.Router):
def __init__(self, options={}): def __init__(self, options={}):
mapper = routes.Mapper() 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"}) "/", method='POST', headers={'Content-Type': "unknow"})
response = my_fault(request) response = my_fault(request)
self.assertEqual(415, response.status_int) 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 from __future__ import print_function
import errno import errno
import os
import socket import socket
import ssl
import sys import sys
import time import time
@ -30,10 +28,12 @@ from oslo_config import cfg
import oslo_i18n import oslo_i18n
from oslo_log import log as logging from oslo_log import log as logging
from oslo_serialization import jsonutils from oslo_serialization import jsonutils
from oslo_service import _options
from oslo_service import service as common_service from oslo_service import service as common_service
from oslo_service import sslutils
from oslo_service import systemd from oslo_service import systemd
from oslo_service import wsgi
from oslo_utils import excutils from oslo_utils import excutils
import routes.middleware
import six import six
import webob.dec import webob.dec
import webob.exc import webob.exc
@ -50,44 +50,19 @@ socket_opts = [
default=4096, default=4096,
help=_("Number of backlog requests to configure " help=_("Number of backlog requests to configure "
"the socket with")), "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', cfg.IntOpt('retry_until_window',
default=30, default=30,
help=_("Number of seconds to keep retrying to listen")), 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', cfg.BoolOpt('use_ssl',
default=False, default=False,
help=_('Enable SSL on the API server')), 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 = cfg.CONF
CONF.register_opts(socket_opts) 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__) LOG = logging.getLogger(__name__)
@ -118,7 +93,7 @@ class WorkerService(worker.NeutronWorker):
# Duplicate a socket object to keep a file descriptor usable. # Duplicate a socket object to keep a file descriptor usable.
dup_sock = self._service._socket.dup() dup_sock = self._service._socket.dup()
if CONF.use_ssl: 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._server = self._service.pool.spawn(self._service._run,
self._application, self._application,
dup_sock) dup_sock)
@ -152,7 +127,7 @@ class Server(object):
# wsgi server to wait forever. # wsgi server to wait forever.
self.client_socket_timeout = CONF.client_socket_timeout or None self.client_socket_timeout = CONF.client_socket_timeout or None
if CONF.use_ssl: if CONF.use_ssl:
self._check_ssl_settings() sslutils.is_enabled(CONF)
def _get_socket(self, host, port, backlog): def _get_socket(self, host, port, backlog):
bind_addr = (host, port) bind_addr = (host, port)
@ -201,37 +176,6 @@ class Server(object):
return sock 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): def start(self, application, port, host='0.0.0.0', workers=0):
"""Run a WSGI server with the given application.""" """Run a WSGI server with the given application."""
self._host = host self._host = host
@ -353,7 +297,7 @@ class Middleware(object):
return self.process_response(response) return self.process_response(response)
class Request(webob.Request): class Request(wsgi.Request):
def best_match_content_type(self): def best_match_content_type(self):
"""Determine the most acceptable content-type. """Determine the most acceptable content-type.
@ -710,63 +654,6 @@ class Debug(Middleware):
print() 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): class Resource(Application):
"""WSGI app that handles (de)serialization and controller dispatch. """WSGI app that handles (de)serialization and controller dispatch.