Merge "Wrap Flask into oslo.service"

This commit is contained in:
Zuul 2018-11-27 03:38:15 +00:00 committed by Gerrit Code Review
commit e8fa9e3272
9 changed files with 96 additions and 237 deletions

View File

@ -14,16 +14,24 @@
import sys
from oslo_config import cfg
from oslo_service import service
from ironic_inspector.common.rpc_service import RPCService
from ironic_inspector.common import service_utils
from ironic_inspector import wsgi_service
CONF = cfg.CONF
def main(args=sys.argv[1:]):
# Parse config file and command line options, then start logging
service_utils.prepare_service(args)
server = wsgi_service.WSGIService()
server.run()
launcher = service.ServiceLauncher(CONF, restart_method='mutate')
launcher.launch_service(wsgi_service.WSGIService())
launcher.launch_service(RPCService(CONF.host))
launcher.wait()
if __name__ == '__main__':

View File

@ -12,6 +12,7 @@
from oslo_config import cfg
from oslo_log import log
from oslo_service import sslutils
from ironic_inspector.conf import opts
@ -26,5 +27,10 @@ def prepare_service(args=None):
opts.parse_args(args)
log.setup(CONF, 'ironic_inspector')
# TODO(kaifeng) Remove deprecated options at T* cycle.
sslutils.register_opts(CONF)
CONF.set_default('cert_file', CONF.ssl_cert_path, group='ssl')
CONF.set_default('key_file', CONF.ssl_key_path, group='ssl')
LOG.debug("Configuration:")
CONF.log_opt_values(LOG, log.DEBUG)

View File

@ -52,9 +52,15 @@ _OPTS = [
help=_('SSL Enabled/Disabled')),
cfg.StrOpt('ssl_cert_path',
default='',
deprecated_for_removal=True,
deprecated_reason=_('This option will be superseded by '
'[ssl]cert_file.'),
help=_('Path to SSL certificate')),
cfg.StrOpt('ssl_key_path',
default='',
deprecated_for_removal=True,
deprecated_reason=_('This option will be superseded by '
'[ssl]key_file.'),
help=_('Path to SSL key')),
cfg.IntOpt('max_concurrency',
default=1000, min=2,
@ -78,7 +84,7 @@ _OPTS = [
help=_('Whether the current installation of ironic-inspector '
'can manage PXE booting of nodes. If set to False, '
'the API will reject introspection requests with '
'manage_boot missing or set to True.'))
'manage_boot missing or set to True.')),
]

View File

@ -11,15 +11,12 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import ssl
import sys
import unittest
import eventlet # noqa
import fixtures
import mock
from oslo_config import cfg
from ironic_inspector.common import service_utils
from ironic_inspector.test import base as test_base
from ironic_inspector import wsgi_service
@ -32,9 +29,12 @@ class BaseWSGITest(test_base.BaseTest):
super(BaseWSGITest, self).setUp()
self.app = self.useFixture(fixtures.MockPatchObject(
wsgi_service.app, 'app', autospec=True)).mock
self.server = self.useFixture(fixtures.MockPatchObject(
wsgi_service.wsgi, 'Server', autospec=True)).mock
self.mock_log = self.useFixture(fixtures.MockPatchObject(
wsgi_service, 'LOG')).mock
self.service = wsgi_service.WSGIService()
self.service.server = self.server
class TestWSGIServiceInitMiddleware(BaseWSGITest):
@ -66,174 +66,55 @@ class TestWSGIServiceInitMiddleware(BaseWSGITest):
self.mock_add_cors_middleware.assert_called_once_with(self.app)
class TestWSGIServiceRun(BaseWSGITest):
class TestWSGIService(BaseWSGITest):
def setUp(self):
super(TestWSGIServiceRun, self).setUp()
super(TestWSGIService, self).setUp()
self.mock__init_middleware = self.useFixture(fixtures.MockPatchObject(
self.service, '_init_middleware')).mock
self.mock__create_ssl_context = self.useFixture(
fixtures.MockPatchObject(self.service, '_create_ssl_context')).mock
self.mock_shutdown = self.useFixture(fixtures.MockPatchObject(
self.service, 'shutdown')).mock
# 'positive' settings
CONF.set_override('listen_address', '42.42.42.42')
CONF.set_override('listen_port', 42)
def test_run(self):
self.service.run()
def test_start(self):
self.service.start()
self.mock__create_ssl_context.assert_called_once_with()
self.mock__init_middleware.assert_called_once_with()
self.app.run.assert_called_once_with(
host=CONF.listen_address, port=CONF.listen_port,
ssl_context=self.mock__create_ssl_context.return_value)
self.mock_shutdown.assert_called_once_with()
self.server.start.assert_called_once_with()
def test_run_no_ssl_context(self):
self.mock__create_ssl_context.return_value = None
def test_stop(self):
self.service.stop()
self.server.stop.assert_called_once_with()
self.service.run()
self.mock__create_ssl_context.assert_called_once_with()
self.mock__init_middleware.assert_called_once_with()
self.app.run.assert_called_once_with(
host=CONF.listen_address, port=CONF.listen_port)
self.mock_shutdown.assert_called_once_with()
def test_wait(self):
self.service.wait()
self.server.wait.assert_called_once_with()
def test_run_app_error(self):
class MyError(Exception):
pass
error = MyError('Oops!')
self.app.run.side_effect = error
self.service.run()
self.mock__create_ssl_context.assert_called_once_with()
self.mock__init_middleware.assert_called_once_with()
self.app.run.assert_called_once_with(
host=CONF.listen_address, port=CONF.listen_port,
ssl_context=self.mock__create_ssl_context.return_value)
self.mock_shutdown.assert_called_once_with(error=str(error))
def test_reset(self):
self.service.reset()
self.server.reset.assert_called_once_with()
class TestWSGIServiceShutdown(BaseWSGITest):
def setUp(self):
super(TestWSGIServiceShutdown, self).setUp()
self.service = wsgi_service.WSGIService()
self.mock_rpc_service = mock.MagicMock()
self.service.rpc_service = self.mock_rpc_service
self.mock_exit = self.useFixture(fixtures.MockPatchObject(
wsgi_service.sys, 'exit')).mock
@mock.patch.object(service_utils.log, 'register_options', autospec=True)
class TestSSLOptions(test_base.BaseTest):
def test_shutdown(self):
class MyError(Exception):
pass
error = MyError('Oops!')
def test_use_deprecated_options(self, mock_log):
CONF.set_override('ssl_cert_path', 'fake_cert_file')
CONF.set_override('ssl_key_path', 'fake_key_file')
self.service.shutdown(error=error)
self.mock_rpc_service.stop.assert_called_once_with()
self.mock_exit.assert_called_once_with(error)
service_utils.prepare_service()
self.assertEqual(CONF.ssl.cert_file, 'fake_cert_file')
self.assertEqual(CONF.ssl.key_file, 'fake_key_file')
class TestCreateSSLContext(test_base.BaseTest):
def setUp(self):
super(TestCreateSSLContext, self).setUp()
self.app = mock.Mock()
self.service = wsgi_service.WSGIService()
def test_use_ssl_options(self, mock_log):
CONF.set_override('ssl_cert_path', 'fake_cert_file')
CONF.set_override('ssl_key_path', 'fake_key_file')
def test_use_ssl_false(self):
CONF.set_override('use_ssl', False)
con = self.service._create_ssl_context()
self.assertIsNone(con)
service_utils.prepare_service()
@mock.patch.object(sys, 'version_info')
def test_old_python_returns_none(self, mock_version_info):
mock_version_info.__lt__.return_value = True
CONF.set_override('use_ssl', True)
con = self.service._create_ssl_context()
self.assertIsNone(con)
CONF.set_override('cert_file', 'fake_new_cert', 'ssl')
CONF.set_override('key_file', 'fake_new_key', 'ssl')
@unittest.skipIf(sys.version_info[:3] < (2, 7, 9),
'This feature is unsupported in this version of python '
'so the tests will be skipped')
@mock.patch.object(ssl, 'create_default_context', autospec=True)
def test_use_ssl_true(self, mock_cdc):
CONF.set_override('use_ssl', True)
m_con = mock_cdc()
con = self.service._create_ssl_context()
self.assertEqual(m_con, con)
@unittest.skipIf(sys.version_info[:3] < (2, 7, 9),
'This feature is unsupported in this version of python '
'so the tests will be skipped')
@mock.patch.object(ssl, 'create_default_context', autospec=True)
def test_only_key_path_provided(self, mock_cdc):
CONF.set_override('use_ssl', True)
CONF.set_override('ssl_key_path', '/some/fake/path')
mock_context = mock_cdc()
con = self.service._create_ssl_context()
self.assertEqual(mock_context, con)
self.assertFalse(mock_context.load_cert_chain.called)
@unittest.skipIf(sys.version_info[:3] < (2, 7, 9),
'This feature is unsupported in this version of python '
'so the tests will be skipped')
@mock.patch.object(ssl, 'create_default_context', autospec=True)
def test_only_cert_path_provided(self, mock_cdc):
CONF.set_override('use_ssl', True)
CONF.set_override('ssl_cert_path', '/some/fake/path')
mock_context = mock_cdc()
con = self.service._create_ssl_context()
self.assertEqual(mock_context, con)
self.assertFalse(mock_context.load_cert_chain.called)
@unittest.skipIf(sys.version_info[:3] < (2, 7, 9),
'This feature is unsupported in this version of python '
'so the tests will be skipped')
@mock.patch.object(ssl, 'create_default_context', autospec=True)
def test_both_paths_provided(self, mock_cdc):
key_path = '/some/fake/path/key'
cert_path = '/some/fake/path/cert'
CONF.set_override('use_ssl', True)
CONF.set_override('ssl_key_path', key_path)
CONF.set_override('ssl_cert_path', cert_path)
mock_context = mock_cdc()
con = self.service._create_ssl_context()
self.assertEqual(mock_context, con)
mock_context.load_cert_chain.assert_called_once_with(cert_path,
key_path)
@unittest.skipIf(sys.version_info[:3] < (2, 7, 9),
'This feature is unsupported in this version of python '
'so the tests will be skipped')
@mock.patch.object(ssl, 'create_default_context', autospec=True)
def test_load_cert_chain_fails(self, mock_cdc):
CONF.set_override('use_ssl', True)
key_path = '/some/fake/path/key'
cert_path = '/some/fake/path/cert'
CONF.set_override('use_ssl', True)
CONF.set_override('ssl_key_path', key_path)
CONF.set_override('ssl_cert_path', cert_path)
mock_context = mock_cdc()
mock_context.load_cert_chain.side_effect = IOError('Boom!')
con = self.service._create_ssl_context()
self.assertEqual(mock_context, con)
mock_context.load_cert_chain.assert_called_once_with(cert_path,
key_path)
class TestWSGIServiceOnSigHup(BaseWSGITest):
def setUp(self):
super(TestWSGIServiceOnSigHup, self).setUp()
self.mock_spawn = self.useFixture(fixtures.MockPatchObject(
wsgi_service.eventlet, 'spawn')).mock
self.mock_mutate_conf = self.useFixture(fixtures.MockPatchObject(
wsgi_service.CONF, 'mutate_config_files')).mock
def test_on_sighup(self):
self.service._handle_sighup()
self.mock_spawn.assert_called_once_with(self.service._handle_sighup_bg)
def test_on_sighup_bg(self):
self.service._handle_sighup_bg()
self.mock_mutate_conf.assert_called_once_with()
self.assertEqual(CONF.ssl.cert_file, 'fake_new_cert')
self.assertEqual(CONF.ssl.key_file, 'fake_new_key')

View File

@ -10,16 +10,11 @@
# License for the specific language governing permissions and limitations
# under the License.
import signal
import ssl
import sys
import eventlet
from oslo_config import cfg
from oslo_log import log
from oslo_service import service
from oslo_service import wsgi
from ironic_inspector.common.rpc_service import RPCService
from ironic_inspector import main as app
from ironic_inspector import utils
@ -27,21 +22,22 @@ LOG = log.getLogger(__name__)
CONF = cfg.CONF
class WSGIService(object):
class WSGIService(service.Service):
"""Provides ability to launch API from wsgi app."""
def __init__(self):
self.app = app.app
signal.signal(signal.SIGHUP, self._handle_sighup)
signal.signal(signal.SIGTERM, self._handle_sigterm)
self.rpc_service = RPCService(CONF.host)
self.server = wsgi.Server(CONF, 'ironic_inspector',
self.app,
host=CONF.listen_address,
port=CONF.listen_port,
use_ssl=CONF.use_ssl)
def _init_middleware(self):
"""Initialize WSGI middleware.
:returns: None
"""
if CONF.auth_strategy != 'noauth':
utils.add_auth_middleware(self.app)
else:
@ -49,80 +45,31 @@ class WSGIService(object):
' configuration')
utils.add_cors_middleware(self.app)
def _create_ssl_context(self):
if not CONF.use_ssl:
return
MIN_VERSION = (2, 7, 9)
if sys.version_info < MIN_VERSION:
LOG.warning(('Unable to use SSL in this version of Python: '
'%(current)s, please ensure your version of Python '
'is greater than %(min)s to enable this feature.'),
{'current': '.'.join(map(str, sys.version_info[:3])),
'min': '.'.join(map(str, MIN_VERSION))})
return
context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH)
if CONF.ssl_cert_path and CONF.ssl_key_path:
try:
context.load_cert_chain(CONF.ssl_cert_path, CONF.ssl_key_path)
except IOError as exc:
LOG.warning('Failed to load certificate or key from defined '
'locations: %(cert)s and %(key)s, will continue '
'to run with the default settings: %(exc)s',
{'cert': CONF.ssl_cert_path,
'key': CONF.ssl_key_path,
'exc': exc})
except ssl.SSLError as exc:
LOG.warning('There was a problem with the loaded certificate '
'and key, will continue to run with the default '
'settings: %s', exc)
return context
def shutdown(self, error=None):
"""Stop serving API.
def start(self):
"""Start serving this service using loaded configuration.
:returns: None
"""
LOG.debug('Shutting down')
self.rpc_service.stop()
sys.exit(error)
def run(self):
"""Start serving this service using loaded application.
:returns: None
"""
app_kwargs = {'host': CONF.listen_address,
'port': CONF.listen_port}
context = self._create_ssl_context()
if context:
app_kwargs['ssl_context'] = context
self._init_middleware()
self.server.start()
LOG.info('Spawning RPC service')
service.launch(CONF, self.rpc_service,
restart_method='mutate')
def stop(self):
"""Stop serving this API.
try:
self.app.run(**app_kwargs)
except Exception as e:
self.shutdown(error=str(e))
else:
self.shutdown()
:returns: None
"""
self.server.stop()
def _handle_sighup_bg(self, *args):
"""Reload config on SIGHUP."""
CONF.mutate_config_files()
def wait(self):
"""Wait for the service to stop serving this API.
def _handle_sighup(self, *args):
eventlet.spawn(self._handle_sighup_bg, *args)
:returns: None
"""
self.server.wait()
def _handle_sigterm(self, *args):
# This is a workaround to ensure that shutdown() is done when recieving
# SIGTERM. Raising KeyboardIntrerrupt which won't be caught by any
# 'except Exception' clauses.
raise KeyboardInterrupt
def reset(self):
"""Reset server greenpool size to default.
:returns: None
"""
self.server.reset()

View File

@ -73,7 +73,7 @@ oslo.middleware==3.31.0
oslo.policy==1.30.0
oslo.rootwrap==5.8.0
oslo.serialization==2.18.0
oslo.service==1.30.0
oslo.service==1.24.0
oslo.utils==3.33.0
oslotest==3.2.0
packaging==17.1

View File

@ -0,0 +1,7 @@
---
deprecations:
- |
Configuration options ``[DEFAULT]ssl_cert_path`` and
``[DEFAULT]ssl_key_path`` are deprecated for ironic-inspector now uses
oslo.service as underlying HTTP service instead of Werkzeug. Please use
``[ssl]cert_file`` and ``[ssl]key_file``.

View File

@ -29,6 +29,7 @@ oslo.middleware>=3.31.0 # Apache-2.0
oslo.policy>=1.30.0 # Apache-2.0
oslo.rootwrap>=5.8.0 # Apache-2.0
oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0
oslo.service!=1.28.1,>=1.24.0 # Apache-2.0
oslo.utils>=3.33.0 # Apache-2.0
retrying!=1.3.0,>=1.2.3 # Apache-2.0
six>=1.10.0 # MIT

View File

@ -4,6 +4,9 @@ namespace = ironic_inspector
namespace = keystonemiddleware.auth_token
namespace = oslo.db
namespace = oslo.log
namespace = oslo.messaging
namespace = oslo.middleware.cors
namespace = oslo.policy
namespace = oslo.messaging
namespace = oslo.service.service
namespace = oslo.service.sslutils
namespace = oslo.service.wsgi