Merge "Wrap Flask into oslo.service"
This commit is contained in:
commit
e8fa9e3272
|
@ -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__':
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.')),
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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``.
|
|
@ -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
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue