Merge "Preparing for service splitting"
This commit is contained in:
commit
6277e92b67
2
ironic_inspector/cmd/__init__.py
Normal file
2
ironic_inspector/cmd/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
import eventlet # noqa
|
||||
eventlet.monkey_patch()
|
29
ironic_inspector/cmd/all.py
Normal file
29
ironic_inspector/cmd/all.py
Normal file
@ -0,0 +1,29 @@
|
||||
# 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.
|
||||
|
||||
"""The Ironic Inspector service."""
|
||||
|
||||
import sys
|
||||
|
||||
from ironic_inspector.common import service_utils
|
||||
from ironic_inspector import wsgi_service
|
||||
|
||||
|
||||
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()
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
35
ironic_inspector/common/service_utils.py
Normal file
35
ironic_inspector/common/service_utils.py
Normal file
@ -0,0 +1,35 @@
|
||||
# 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.
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log
|
||||
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
def prepare_service(args):
|
||||
log.register_options(CONF)
|
||||
log.set_defaults(default_log_levels=['sqlalchemy=WARNING',
|
||||
'iso8601=WARNING',
|
||||
'requests=WARNING',
|
||||
'urllib3.connectionpool=WARNING',
|
||||
'keystonemiddleware=WARNING',
|
||||
'swiftclient=WARNING',
|
||||
'keystoneauth=WARNING',
|
||||
'ironicclient=WARNING'])
|
||||
CONF(args, project='ironic-inspector')
|
||||
log.setup(CONF, 'ironic_inspector')
|
||||
|
||||
LOG.debug("Configuration:")
|
||||
CONF.log_opt_values(LOG, log.DEBUG)
|
@ -11,19 +11,12 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import eventlet # noqa
|
||||
eventlet.monkey_patch()
|
||||
|
||||
import functools
|
||||
import os
|
||||
import re
|
||||
import ssl
|
||||
import sys
|
||||
|
||||
import flask
|
||||
from futurist import periodics
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log
|
||||
from oslo_utils import uuidutils
|
||||
import werkzeug
|
||||
|
||||
@ -32,11 +25,8 @@ from ironic_inspector.common.i18n import _
|
||||
from ironic_inspector.common import ironic as ir_utils
|
||||
from ironic_inspector.common import swift
|
||||
from ironic_inspector import conf # noqa
|
||||
from ironic_inspector import db
|
||||
from ironic_inspector import firewall
|
||||
from ironic_inspector import introspect
|
||||
from ironic_inspector import node_cache
|
||||
from ironic_inspector.plugins import base as plugins_base
|
||||
from ironic_inspector import process
|
||||
from ironic_inspector import rules
|
||||
from ironic_inspector import utils
|
||||
@ -350,161 +340,3 @@ def api_rule(uuid):
|
||||
@app.errorhandler(404)
|
||||
def handle_404(error):
|
||||
return error_response(error, code=404)
|
||||
|
||||
|
||||
def periodic_update(): # pragma: no cover
|
||||
try:
|
||||
firewall.update_filters()
|
||||
except Exception:
|
||||
LOG.exception('Periodic update of firewall rules failed')
|
||||
|
||||
|
||||
def periodic_clean_up(): # pragma: no cover
|
||||
try:
|
||||
if node_cache.clean_up():
|
||||
firewall.update_filters()
|
||||
sync_with_ironic()
|
||||
except Exception:
|
||||
LOG.exception('Periodic clean up of node cache failed')
|
||||
|
||||
|
||||
def sync_with_ironic():
|
||||
ironic = ir_utils.get_client()
|
||||
# TODO(yuikotakada): pagination
|
||||
ironic_nodes = ironic.node.list(limit=0)
|
||||
ironic_node_uuids = {node.uuid for node in ironic_nodes}
|
||||
node_cache.delete_nodes_not_in_list(ironic_node_uuids)
|
||||
|
||||
|
||||
def create_ssl_context():
|
||||
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
|
||||
|
||||
|
||||
class Service(object):
|
||||
_periodics_worker = None
|
||||
|
||||
def setup_logging(self, args):
|
||||
log.register_options(CONF)
|
||||
CONF(args, project='ironic-inspector')
|
||||
|
||||
log.set_defaults(default_log_levels=[
|
||||
'sqlalchemy=WARNING',
|
||||
'iso8601=WARNING',
|
||||
'requests=WARNING',
|
||||
'urllib3.connectionpool=WARNING',
|
||||
'keystonemiddleware=WARNING',
|
||||
'swiftclient=WARNING',
|
||||
'keystoneauth=WARNING',
|
||||
'ironicclient=WARNING'
|
||||
])
|
||||
log.setup(CONF, 'ironic_inspector')
|
||||
|
||||
LOG.debug("Configuration:")
|
||||
CONF.log_opt_values(LOG, log.DEBUG)
|
||||
|
||||
def init(self):
|
||||
if CONF.auth_strategy != 'noauth':
|
||||
utils.add_auth_middleware(app)
|
||||
else:
|
||||
LOG.warning('Starting unauthenticated, please check'
|
||||
' configuration')
|
||||
|
||||
if CONF.processing.store_data == 'none':
|
||||
LOG.warning('Introspection data will not be stored. Change '
|
||||
'"[processing] store_data" option if this is not '
|
||||
'the desired behavior')
|
||||
elif CONF.processing.store_data == 'swift':
|
||||
LOG.info('Introspection data will be stored in Swift in the '
|
||||
'container %s', CONF.swift.container)
|
||||
|
||||
utils.add_cors_middleware(app)
|
||||
|
||||
db.init()
|
||||
|
||||
try:
|
||||
hooks = plugins_base.validate_processing_hooks()
|
||||
except Exception as exc:
|
||||
LOG.critical(str(exc))
|
||||
sys.exit(1)
|
||||
|
||||
LOG.info('Enabled processing hooks: %s', [h.name for h in hooks])
|
||||
|
||||
if CONF.firewall.manage_firewall:
|
||||
firewall.init()
|
||||
|
||||
periodic_update_ = periodics.periodic(
|
||||
spacing=CONF.firewall.firewall_update_period,
|
||||
enabled=CONF.firewall.manage_firewall
|
||||
)(periodic_update)
|
||||
periodic_clean_up_ = periodics.periodic(
|
||||
spacing=CONF.clean_up_period
|
||||
)(periodic_clean_up)
|
||||
|
||||
self._periodics_worker = periodics.PeriodicWorker(
|
||||
callables=[(periodic_update_, None, None),
|
||||
(periodic_clean_up_, None, None)],
|
||||
executor_factory=periodics.ExistingExecutor(utils.executor()))
|
||||
utils.executor().submit(self._periodics_worker.start)
|
||||
|
||||
def shutdown(self):
|
||||
LOG.debug('Shutting down')
|
||||
|
||||
firewall.clean_up()
|
||||
|
||||
if self._periodics_worker is not None:
|
||||
self._periodics_worker.stop()
|
||||
self._periodics_worker.wait()
|
||||
self._periodics_worker = None
|
||||
|
||||
if utils.executor().alive:
|
||||
utils.executor().shutdown(wait=True)
|
||||
|
||||
LOG.info('Shut down successfully')
|
||||
|
||||
def run(self, args, application):
|
||||
self.setup_logging(args)
|
||||
|
||||
app_kwargs = {'host': CONF.listen_address,
|
||||
'port': CONF.listen_port}
|
||||
|
||||
context = create_ssl_context()
|
||||
if context:
|
||||
app_kwargs['ssl_context'] = context
|
||||
|
||||
self.init()
|
||||
try:
|
||||
application.run(**app_kwargs)
|
||||
finally:
|
||||
self.shutdown()
|
||||
|
||||
|
||||
def main(args=sys.argv[1:]): # pragma: no cover
|
||||
service = Service()
|
||||
service.run(args, app)
|
||||
|
@ -34,6 +34,7 @@ import requests
|
||||
import six
|
||||
from six.moves import urllib
|
||||
|
||||
from ironic_inspector.cmd import all as inspector_cmd
|
||||
from ironic_inspector.common import ironic as ir_utils
|
||||
from ironic_inspector.common import swift
|
||||
from ironic_inspector import db
|
||||
@ -779,7 +780,7 @@ def mocked_server():
|
||||
cfg.CONF.reset()
|
||||
cfg.CONF.unregister_opt(dbsync.command_opt)
|
||||
|
||||
eventlet.greenthread.spawn_n(main.main,
|
||||
eventlet.greenthread.spawn_n(inspector_cmd.main,
|
||||
args=['--config-file', conf_file])
|
||||
eventlet.greenthread.sleep(1)
|
||||
# Wait for service to start up to 30 seconds
|
||||
|
@ -13,8 +13,6 @@
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import ssl
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
@ -22,8 +20,6 @@ from oslo_utils import uuidutils
|
||||
|
||||
from ironic_inspector.common import ironic as ir_utils
|
||||
from ironic_inspector import conf
|
||||
from ironic_inspector import db
|
||||
from ironic_inspector import firewall
|
||||
from ironic_inspector import introspect
|
||||
from ironic_inspector import introspection_state as istate
|
||||
from ironic_inspector import main
|
||||
@ -648,155 +644,3 @@ class TestPlugins(unittest.TestCase):
|
||||
def test_manager_is_cached(self):
|
||||
self.assertIs(plugins_base.processing_hooks_manager(),
|
||||
plugins_base.processing_hooks_manager())
|
||||
|
||||
|
||||
@mock.patch.object(firewall, 'init')
|
||||
@mock.patch.object(utils, 'add_auth_middleware')
|
||||
@mock.patch.object(ir_utils, 'get_client')
|
||||
@mock.patch.object(db, 'init')
|
||||
class TestInit(test_base.BaseTest):
|
||||
def setUp(self):
|
||||
super(TestInit, self).setUp()
|
||||
# Tests default to a synchronous executor which can't be used here
|
||||
utils._EXECUTOR = None
|
||||
self.service = main.Service()
|
||||
|
||||
@mock.patch.object(firewall, 'clean_up', lambda: None)
|
||||
def tearDown(self):
|
||||
self.service.shutdown()
|
||||
super(TestInit, self).tearDown()
|
||||
|
||||
def test_ok(self, mock_node_cache, mock_get_client, mock_auth,
|
||||
mock_firewall):
|
||||
CONF.set_override('auth_strategy', 'keystone')
|
||||
self.service.init()
|
||||
mock_auth.assert_called_once_with(main.app)
|
||||
mock_node_cache.assert_called_once_with()
|
||||
mock_firewall.assert_called_once_with()
|
||||
|
||||
def test_init_without_authenticate(self, mock_node_cache, mock_get_client,
|
||||
mock_auth, mock_firewall):
|
||||
CONF.set_override('auth_strategy', 'noauth')
|
||||
self.service.init()
|
||||
self.assertFalse(mock_auth.called)
|
||||
|
||||
@mock.patch.object(main.LOG, 'warning')
|
||||
def test_init_with_no_data_storage(self, mock_log, mock_node_cache,
|
||||
mock_get_client, mock_auth,
|
||||
mock_firewall):
|
||||
msg = ('Introspection data will not be stored. Change '
|
||||
'"[processing] store_data" option if this is not the '
|
||||
'desired behavior')
|
||||
self.service.init()
|
||||
mock_log.assert_called_once_with(msg)
|
||||
|
||||
@mock.patch.object(main.LOG, 'info')
|
||||
def test_init_with_swift_storage(self, mock_log, mock_node_cache,
|
||||
mock_get_client, mock_auth,
|
||||
mock_firewall):
|
||||
CONF.set_override('store_data', 'swift', 'processing')
|
||||
msg = mock.call('Introspection data will be stored in Swift in the '
|
||||
'container %s', CONF.swift.container)
|
||||
self.service.init()
|
||||
self.assertIn(msg, mock_log.call_args_list)
|
||||
|
||||
def test_init_without_manage_firewall(self, mock_node_cache,
|
||||
mock_get_client, mock_auth,
|
||||
mock_firewall):
|
||||
CONF.set_override('manage_firewall', False, 'firewall')
|
||||
self.service.init()
|
||||
self.assertFalse(mock_firewall.called)
|
||||
|
||||
@mock.patch.object(main.LOG, 'critical')
|
||||
def test_init_failed_processing_hook(self, mock_log, mock_node_cache,
|
||||
mock_get_client, mock_auth,
|
||||
mock_firewall):
|
||||
CONF.set_override('processing_hooks', 'foo!', 'processing')
|
||||
plugins_base._HOOKS_MGR = None
|
||||
|
||||
self.assertRaises(SystemExit, self.service.init)
|
||||
mock_log.assert_called_once_with(
|
||||
'The following hook(s) are missing or failed to load: foo!')
|
||||
|
||||
|
||||
class TestCreateSSLContext(test_base.BaseTest):
|
||||
|
||||
def test_use_ssl_false(self):
|
||||
CONF.set_override('use_ssl', False)
|
||||
con = main.create_ssl_context()
|
||||
self.assertIsNone(con)
|
||||
|
||||
@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 = main.create_ssl_context()
|
||||
self.assertIsNone(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_use_ssl_true(self, mock_cdc):
|
||||
CONF.set_override('use_ssl', True)
|
||||
m_con = mock_cdc()
|
||||
con = main.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 = main.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 = main.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 = main.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 = main.create_ssl_context()
|
||||
self.assertEqual(mock_context, con)
|
||||
mock_context.load_cert_chain.assert_called_once_with(cert_path,
|
||||
key_path)
|
||||
|
207
ironic_inspector/test/unit/test_wsgi_service.py
Normal file
207
ironic_inspector/test/unit/test_wsgi_service.py
Normal file
@ -0,0 +1,207 @@
|
||||
# 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 ssl
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
import eventlet # noqa
|
||||
import fixtures
|
||||
import mock
|
||||
from oslo_config import cfg
|
||||
|
||||
from ironic_inspector import db
|
||||
from ironic_inspector import firewall
|
||||
from ironic_inspector import main
|
||||
from ironic_inspector.plugins import base as plugins_base
|
||||
from ironic_inspector.test import base as test_base
|
||||
from ironic_inspector import utils
|
||||
from ironic_inspector import wsgi_service
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
@mock.patch.object(firewall, 'clean_up', lambda: None)
|
||||
@mock.patch.object(db, 'init', lambda: None)
|
||||
@mock.patch.object(wsgi_service.WSGIService, '_init_host', lambda x: None)
|
||||
@mock.patch.object(utils, 'add_auth_middleware')
|
||||
class TestWSGIService(test_base.BaseTest):
|
||||
def setUp(self):
|
||||
super(TestWSGIService, self).setUp()
|
||||
self.app = self.useFixture(fixtures.MockPatchObject(
|
||||
main, 'app', autospec=True)).mock
|
||||
self.service = wsgi_service.WSGIService()
|
||||
|
||||
def test_init_middleware(self, mock_auth):
|
||||
CONF.set_override('auth_strategy', 'keystone')
|
||||
self.service._init_middleware()
|
||||
|
||||
mock_auth.assert_called_once_with(self.app)
|
||||
|
||||
@mock.patch.object(wsgi_service.WSGIService, '_init_middleware')
|
||||
def test_run_ok(self, mock_init_middlw, mock_auth):
|
||||
self.service.run()
|
||||
|
||||
mock_init_middlw.assert_called_once_with()
|
||||
self.app.run.assert_called_once_with(host='0.0.0.0', port=5050)
|
||||
|
||||
@mock.patch.object(wsgi_service.LOG, 'info')
|
||||
def test_init_with_swift_storage(self, mock_log, mock_auth):
|
||||
|
||||
CONF.set_override('store_data', 'swift', 'processing')
|
||||
msg = mock.call('Introspection data will be stored in Swift in the '
|
||||
'container %s', CONF.swift.container)
|
||||
self.service.run()
|
||||
self.assertIn(msg, mock_log.call_args_list)
|
||||
|
||||
def test_init_without_authenticate(self, mock_auth):
|
||||
|
||||
CONF.set_override('auth_strategy', 'noauth')
|
||||
self.service.run()
|
||||
self.assertFalse(mock_auth.called)
|
||||
|
||||
@mock.patch.object(wsgi_service.LOG, 'warning')
|
||||
def test_init_with_no_data_storage(self, mock_log, mock_auth):
|
||||
msg = ('Introspection data will not be stored. Change '
|
||||
'"[processing] store_data" option if this is not the '
|
||||
'desired behavior')
|
||||
self.service.run()
|
||||
mock_log.assert_called_once_with(msg)
|
||||
|
||||
|
||||
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_false(self):
|
||||
CONF.set_override('use_ssl', False)
|
||||
con = self.service._create_ssl_context()
|
||||
self.assertIsNone(con)
|
||||
|
||||
@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)
|
||||
|
||||
@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)
|
||||
|
||||
|
||||
@mock.patch.object(firewall, 'init')
|
||||
@mock.patch.object(db, 'init')
|
||||
class TestInit(test_base.BaseTest):
|
||||
def setUp(self):
|
||||
super(TestInit, self).setUp()
|
||||
# Tests default to a synchronous executor which can't be used here
|
||||
utils._EXECUTOR = None
|
||||
# Monkey patch for periodic tasks
|
||||
eventlet.monkey_patch()
|
||||
self.wsgi = wsgi_service.WSGIService()
|
||||
|
||||
@mock.patch.object(firewall, 'clean_up', lambda: None)
|
||||
def tearDown(self):
|
||||
self.wsgi.shutdown()
|
||||
super(TestInit, self).tearDown()
|
||||
|
||||
def test_ok(self, mock_db, mock_firewall):
|
||||
self.wsgi._init_host()
|
||||
|
||||
mock_db.assert_called_once_with()
|
||||
mock_firewall.assert_called_once_with()
|
||||
|
||||
def test_init_without_manage_firewall(self, mock_db, mock_firewall):
|
||||
|
||||
CONF.set_override('manage_firewall', False, 'firewall')
|
||||
self.wsgi._init_host()
|
||||
self.assertFalse(mock_firewall.called)
|
||||
|
||||
@mock.patch.object(wsgi_service.LOG, 'critical')
|
||||
def test_init_failed_processing_hook(self, mock_log,
|
||||
mock_db, mock_firewall):
|
||||
|
||||
CONF.set_override('processing_hooks', 'foo!', 'processing')
|
||||
plugins_base._HOOKS_MGR = None
|
||||
|
||||
self.assertRaises(SystemExit, self.wsgi._init_host)
|
||||
mock_log.assert_called_once_with(
|
||||
'The following hook(s) are missing or failed to load: foo!')
|
196
ironic_inspector/wsgi_service.py
Normal file
196
ironic_inspector/wsgi_service.py
Normal file
@ -0,0 +1,196 @@
|
||||
# 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 ssl
|
||||
import sys
|
||||
|
||||
from futurist import periodics
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log
|
||||
|
||||
from ironic_inspector.common import ironic as ir_utils
|
||||
from ironic_inspector import db
|
||||
from ironic_inspector import firewall
|
||||
from ironic_inspector import main as app
|
||||
from ironic_inspector import node_cache
|
||||
from ironic_inspector.plugins import base as plugins_base
|
||||
from ironic_inspector import utils
|
||||
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
class WSGIService(object):
|
||||
"""Provides ability to launch API from wsgi app."""
|
||||
|
||||
def __init__(self):
|
||||
self.app = app.app
|
||||
self._periodics_worker = None
|
||||
|
||||
def _init_middleware(self):
|
||||
"""Initialize WSGI middleware.
|
||||
|
||||
:returns: None
|
||||
"""
|
||||
|
||||
if CONF.auth_strategy != 'noauth':
|
||||
utils.add_auth_middleware(self.app)
|
||||
else:
|
||||
LOG.warning('Starting unauthenticated, please check'
|
||||
' configuration')
|
||||
|
||||
# TODO(aarefiev): move to WorkerService once we split service
|
||||
if CONF.processing.store_data == 'none':
|
||||
LOG.warning('Introspection data will not be stored. Change '
|
||||
'"[processing] store_data" option if this is not '
|
||||
'the desired behavior')
|
||||
elif CONF.processing.store_data == 'swift':
|
||||
LOG.info('Introspection data will be stored in Swift in the '
|
||||
'container %s', CONF.swift.container)
|
||||
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
|
||||
|
||||
# TODO(aarefiev): move init code to WorkerService
|
||||
def _init_host(self):
|
||||
"""Initialize Worker host
|
||||
|
||||
Init db connection, load and validate processing
|
||||
hooks, runs periodic tasks.
|
||||
|
||||
:returns None
|
||||
"""
|
||||
db.init()
|
||||
|
||||
try:
|
||||
hooks = plugins_base.validate_processing_hooks()
|
||||
except Exception as exc:
|
||||
LOG.critical(str(exc))
|
||||
sys.exit(1)
|
||||
|
||||
LOG.info('Enabled processing hooks: %s', [h.name for h in hooks])
|
||||
|
||||
if CONF.firewall.manage_firewall:
|
||||
firewall.init()
|
||||
|
||||
periodic_update_ = periodics.periodic(
|
||||
spacing=CONF.firewall.firewall_update_period,
|
||||
enabled=CONF.firewall.manage_firewall
|
||||
)(periodic_update)
|
||||
periodic_clean_up_ = periodics.periodic(
|
||||
spacing=CONF.clean_up_period
|
||||
)(periodic_clean_up)
|
||||
|
||||
self._periodics_worker = periodics.PeriodicWorker(
|
||||
callables=[(periodic_update_, None, None),
|
||||
(periodic_clean_up_, None, None)],
|
||||
executor_factory=periodics.ExistingExecutor(utils.executor()))
|
||||
utils.executor().submit(self._periodics_worker.start)
|
||||
|
||||
def shutdown(self):
|
||||
"""Stop serving API, clean up.
|
||||
|
||||
:returns: None
|
||||
"""
|
||||
# TODO(aarefiev): move shutdown code to WorkerService
|
||||
LOG.debug('Shutting down')
|
||||
|
||||
firewall.clean_up()
|
||||
|
||||
if self._periodics_worker is not None:
|
||||
try:
|
||||
self._periodics_worker.stop()
|
||||
self._periodics_worker.wait()
|
||||
except Exception as e:
|
||||
LOG.exception('Service error occurred when stopping '
|
||||
'periodic workers. Error: %s', e)
|
||||
self._periodics_worker = None
|
||||
|
||||
if utils.executor().alive:
|
||||
utils.executor().shutdown(wait=True)
|
||||
|
||||
LOG.info('Shut down successfully')
|
||||
|
||||
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._init_host()
|
||||
|
||||
try:
|
||||
self.app.run(**app_kwargs)
|
||||
finally:
|
||||
self.shutdown()
|
||||
|
||||
|
||||
def periodic_update(): # pragma: no cover
|
||||
try:
|
||||
firewall.update_filters()
|
||||
except Exception:
|
||||
LOG.exception('Periodic update of firewall rules failed')
|
||||
|
||||
|
||||
def periodic_clean_up(): # pragma: no cover
|
||||
try:
|
||||
if node_cache.clean_up():
|
||||
firewall.update_filters()
|
||||
sync_with_ironic()
|
||||
except Exception:
|
||||
LOG.exception('Periodic clean up of node cache failed')
|
||||
|
||||
|
||||
def sync_with_ironic():
|
||||
ironic = ir_utils.get_client()
|
||||
# TODO(yuikotakada): pagination
|
||||
ironic_nodes = ironic.node.list(limit=0)
|
||||
ironic_node_uuids = {node.uuid for node in ironic_nodes}
|
||||
node_cache.delete_nodes_not_in_list(ironic_node_uuids)
|
@ -22,7 +22,7 @@ packages =
|
||||
|
||||
[entry_points]
|
||||
console_scripts =
|
||||
ironic-inspector = ironic_inspector.main:main
|
||||
ironic-inspector = ironic_inspector.cmd.all:main
|
||||
ironic-inspector-dbsync = ironic_inspector.dbsync:main
|
||||
ironic-inspector-rootwrap = oslo_rootwrap.cmd:main
|
||||
ironic_inspector.hooks.processing =
|
||||
|
Loading…
Reference in New Issue
Block a user