Browse Source

Wrap Flask into oslo.service

This patch is part of inspector HA work, which wraps inspector api into
oslo service.

oslo.service has also provided support to signal processing like SIGHUP or
SIGTERM, so these code were removed in this patch.

Deprecated current SSL cert/key options used by ironic-inspector, code
manually creates ssl context were removed. These options will be fed
from [ssl] section.

Change-Id: Ia5e16fcb9104556d62c90f5507f17b41f73a5208
Story: #2001842
Task: #12609
tags/8.1.0
Kaifeng Wang 1 year ago
parent
commit
edd6810c3d

+ 10
- 2
ironic_inspector/cmd/all.py View File

@@ -14,16 +14,24 @@
14 14
 
15 15
 import sys
16 16
 
17
+from oslo_config import cfg
18
+from oslo_service import service
19
+
20
+from ironic_inspector.common.rpc_service import RPCService
17 21
 from ironic_inspector.common import service_utils
18 22
 from ironic_inspector import wsgi_service
19 23
 
24
+CONF = cfg.CONF
25
+
20 26
 
21 27
 def main(args=sys.argv[1:]):
22 28
     # Parse config file and command line options, then start logging
23 29
     service_utils.prepare_service(args)
24 30
 
25
-    server = wsgi_service.WSGIService()
26
-    server.run()
31
+    launcher = service.ServiceLauncher(CONF, restart_method='mutate')
32
+    launcher.launch_service(wsgi_service.WSGIService())
33
+    launcher.launch_service(RPCService(CONF.host))
34
+    launcher.wait()
27 35
 
28 36
 
29 37
 if __name__ == '__main__':

+ 6
- 0
ironic_inspector/common/service_utils.py View File

@@ -12,6 +12,7 @@
12 12
 
13 13
 from oslo_config import cfg
14 14
 from oslo_log import log
15
+from oslo_service import sslutils
15 16
 
16 17
 from ironic_inspector.conf import opts
17 18
 
@@ -26,5 +27,10 @@ def prepare_service(args=None):
26 27
     opts.parse_args(args)
27 28
     log.setup(CONF, 'ironic_inspector')
28 29
 
30
+    # TODO(kaifeng) Remove deprecated options at T* cycle.
31
+    sslutils.register_opts(CONF)
32
+    CONF.set_default('cert_file', CONF.ssl_cert_path, group='ssl')
33
+    CONF.set_default('key_file', CONF.ssl_key_path, group='ssl')
34
+
29 35
     LOG.debug("Configuration:")
30 36
     CONF.log_opt_values(LOG, log.DEBUG)

+ 7
- 1
ironic_inspector/conf/default.py View File

@@ -52,9 +52,15 @@ _OPTS = [
52 52
                 help=_('SSL Enabled/Disabled')),
53 53
     cfg.StrOpt('ssl_cert_path',
54 54
                default='',
55
+               deprecated_for_removal=True,
56
+               deprecated_reason=_('This option will be superseded by '
57
+                                   '[ssl]cert_file.'),
55 58
                help=_('Path to SSL certificate')),
56 59
     cfg.StrOpt('ssl_key_path',
57 60
                default='',
61
+               deprecated_for_removal=True,
62
+               deprecated_reason=_('This option will be superseded by '
63
+                                   '[ssl]key_file.'),
58 64
                help=_('Path to SSL key')),
59 65
     cfg.IntOpt('max_concurrency',
60 66
                default=1000, min=2,
@@ -78,7 +84,7 @@ _OPTS = [
78 84
                 help=_('Whether the current installation of ironic-inspector '
79 85
                        'can manage PXE booting of nodes. If set to False, '
80 86
                        'the API will reject introspection requests with '
81
-                       'manage_boot missing or set to True.'))
87
+                       'manage_boot missing or set to True.')),
82 88
 ]
83 89
 
84 90
 

+ 34
- 153
ironic_inspector/test/unit/test_wsgi_service.py View File

@@ -11,15 +11,12 @@
11 11
 # See the License for the specific language governing permissions and
12 12
 # limitations under the License.
13 13
 
14
-import ssl
15
-import sys
16
-import unittest
17
-
18 14
 import eventlet  # noqa
19 15
 import fixtures
20 16
 import mock
21 17
 from oslo_config import cfg
22 18
 
19
+from ironic_inspector.common import service_utils
23 20
 from ironic_inspector.test import base as test_base
24 21
 from ironic_inspector import wsgi_service
25 22
 
@@ -32,9 +29,12 @@ class BaseWSGITest(test_base.BaseTest):
32 29
         super(BaseWSGITest, self).setUp()
33 30
         self.app = self.useFixture(fixtures.MockPatchObject(
34 31
             wsgi_service.app, 'app', autospec=True)).mock
32
+        self.server = self.useFixture(fixtures.MockPatchObject(
33
+            wsgi_service.wsgi, 'Server', autospec=True)).mock
35 34
         self.mock_log = self.useFixture(fixtures.MockPatchObject(
36 35
             wsgi_service, 'LOG')).mock
37 36
         self.service = wsgi_service.WSGIService()
37
+        self.service.server = self.server
38 38
 
39 39
 
40 40
 class TestWSGIServiceInitMiddleware(BaseWSGITest):
@@ -66,174 +66,55 @@ class TestWSGIServiceInitMiddleware(BaseWSGITest):
66 66
         self.mock_add_cors_middleware.assert_called_once_with(self.app)
67 67
 
68 68
 
69
-class TestWSGIServiceRun(BaseWSGITest):
69
+class TestWSGIService(BaseWSGITest):
70 70
     def setUp(self):
71
-        super(TestWSGIServiceRun, self).setUp()
71
+        super(TestWSGIService, self).setUp()
72 72
         self.mock__init_middleware = self.useFixture(fixtures.MockPatchObject(
73 73
             self.service, '_init_middleware')).mock
74
-        self.mock__create_ssl_context = self.useFixture(
75
-            fixtures.MockPatchObject(self.service, '_create_ssl_context')).mock
76
-        self.mock_shutdown = self.useFixture(fixtures.MockPatchObject(
77
-            self.service, 'shutdown')).mock
78 74
 
79 75
         # 'positive' settings
80 76
         CONF.set_override('listen_address', '42.42.42.42')
81 77
         CONF.set_override('listen_port', 42)
82 78
 
83
-    def test_run(self):
84
-        self.service.run()
79
+    def test_start(self):
80
+        self.service.start()
85 81
 
86
-        self.mock__create_ssl_context.assert_called_once_with()
87 82
         self.mock__init_middleware.assert_called_once_with()
88
-        self.app.run.assert_called_once_with(
89
-            host=CONF.listen_address, port=CONF.listen_port,
90
-            ssl_context=self.mock__create_ssl_context.return_value)
91
-        self.mock_shutdown.assert_called_once_with()
83
+        self.server.start.assert_called_once_with()
92 84
 
93
-    def test_run_no_ssl_context(self):
94
-        self.mock__create_ssl_context.return_value = None
85
+    def test_stop(self):
86
+        self.service.stop()
87
+        self.server.stop.assert_called_once_with()
95 88
 
96
-        self.service.run()
97
-        self.mock__create_ssl_context.assert_called_once_with()
98
-        self.mock__init_middleware.assert_called_once_with()
99
-        self.app.run.assert_called_once_with(
100
-            host=CONF.listen_address, port=CONF.listen_port)
101
-        self.mock_shutdown.assert_called_once_with()
89
+    def test_wait(self):
90
+        self.service.wait()
91
+        self.server.wait.assert_called_once_with()
102 92
 
103
-    def test_run_app_error(self):
104
-        class MyError(Exception):
105
-            pass
93
+    def test_reset(self):
94
+        self.service.reset()
95
+        self.server.reset.assert_called_once_with()
106 96
 
107
-        error = MyError('Oops!')
108
-        self.app.run.side_effect = error
109
-        self.service.run()
110 97
 
111
-        self.mock__create_ssl_context.assert_called_once_with()
112
-        self.mock__init_middleware.assert_called_once_with()
113
-        self.app.run.assert_called_once_with(
114
-            host=CONF.listen_address, port=CONF.listen_port,
115
-            ssl_context=self.mock__create_ssl_context.return_value)
116
-        self.mock_shutdown.assert_called_once_with(error=str(error))
98
+@mock.patch.object(service_utils.log, 'register_options', autospec=True)
99
+class TestSSLOptions(test_base.BaseTest):
117 100
 
101
+    def test_use_deprecated_options(self, mock_log):
102
+        CONF.set_override('ssl_cert_path', 'fake_cert_file')
103
+        CONF.set_override('ssl_key_path', 'fake_key_file')
118 104
 
119
-class TestWSGIServiceShutdown(BaseWSGITest):
120
-    def setUp(self):
121
-        super(TestWSGIServiceShutdown, self).setUp()
122
-        self.service = wsgi_service.WSGIService()
123
-        self.mock_rpc_service = mock.MagicMock()
124
-        self.service.rpc_service = self.mock_rpc_service
125
-        self.mock_exit = self.useFixture(fixtures.MockPatchObject(
126
-            wsgi_service.sys, 'exit')).mock
105
+        service_utils.prepare_service()
127 106
 
128
-    def test_shutdown(self):
129
-        class MyError(Exception):
130
-            pass
131
-        error = MyError('Oops!')
107
+        self.assertEqual(CONF.ssl.cert_file, 'fake_cert_file')
108
+        self.assertEqual(CONF.ssl.key_file, 'fake_key_file')
132 109
 
133
-        self.service.shutdown(error=error)
134
-        self.mock_rpc_service.stop.assert_called_once_with()
135
-        self.mock_exit.assert_called_once_with(error)
110
+    def test_use_ssl_options(self, mock_log):
111
+        CONF.set_override('ssl_cert_path', 'fake_cert_file')
112
+        CONF.set_override('ssl_key_path', 'fake_key_file')
136 113
 
114
+        service_utils.prepare_service()
137 115
 
138
-class TestCreateSSLContext(test_base.BaseTest):
139
-    def setUp(self):
140
-        super(TestCreateSSLContext, self).setUp()
141
-        self.app = mock.Mock()
142
-        self.service = wsgi_service.WSGIService()
116
+        CONF.set_override('cert_file', 'fake_new_cert', 'ssl')
117
+        CONF.set_override('key_file', 'fake_new_key', 'ssl')
143 118
 
144
-    def test_use_ssl_false(self):
145
-        CONF.set_override('use_ssl', False)
146
-        con = self.service._create_ssl_context()
147
-        self.assertIsNone(con)
148
-
149
-    @mock.patch.object(sys, 'version_info')
150
-    def test_old_python_returns_none(self, mock_version_info):
151
-        mock_version_info.__lt__.return_value = True
152
-        CONF.set_override('use_ssl', True)
153
-        con = self.service._create_ssl_context()
154
-        self.assertIsNone(con)
155
-
156
-    @unittest.skipIf(sys.version_info[:3] < (2, 7, 9),
157
-                     'This feature is unsupported in this version of python '
158
-                     'so the tests will be skipped')
159
-    @mock.patch.object(ssl, 'create_default_context', autospec=True)
160
-    def test_use_ssl_true(self, mock_cdc):
161
-        CONF.set_override('use_ssl', True)
162
-        m_con = mock_cdc()
163
-        con = self.service._create_ssl_context()
164
-        self.assertEqual(m_con, con)
165
-
166
-    @unittest.skipIf(sys.version_info[:3] < (2, 7, 9),
167
-                     'This feature is unsupported in this version of python '
168
-                     'so the tests will be skipped')
169
-    @mock.patch.object(ssl, 'create_default_context', autospec=True)
170
-    def test_only_key_path_provided(self, mock_cdc):
171
-        CONF.set_override('use_ssl', True)
172
-        CONF.set_override('ssl_key_path', '/some/fake/path')
173
-        mock_context = mock_cdc()
174
-        con = self.service._create_ssl_context()
175
-        self.assertEqual(mock_context, con)
176
-        self.assertFalse(mock_context.load_cert_chain.called)
177
-
178
-    @unittest.skipIf(sys.version_info[:3] < (2, 7, 9),
179
-                     'This feature is unsupported in this version of python '
180
-                     'so the tests will be skipped')
181
-    @mock.patch.object(ssl, 'create_default_context', autospec=True)
182
-    def test_only_cert_path_provided(self, mock_cdc):
183
-        CONF.set_override('use_ssl', True)
184
-        CONF.set_override('ssl_cert_path', '/some/fake/path')
185
-        mock_context = mock_cdc()
186
-        con = self.service._create_ssl_context()
187
-        self.assertEqual(mock_context, con)
188
-        self.assertFalse(mock_context.load_cert_chain.called)
189
-
190
-    @unittest.skipIf(sys.version_info[:3] < (2, 7, 9),
191
-                     'This feature is unsupported in this version of python '
192
-                     'so the tests will be skipped')
193
-    @mock.patch.object(ssl, 'create_default_context', autospec=True)
194
-    def test_both_paths_provided(self, mock_cdc):
195
-        key_path = '/some/fake/path/key'
196
-        cert_path = '/some/fake/path/cert'
197
-        CONF.set_override('use_ssl', True)
198
-        CONF.set_override('ssl_key_path', key_path)
199
-        CONF.set_override('ssl_cert_path', cert_path)
200
-        mock_context = mock_cdc()
201
-        con = self.service._create_ssl_context()
202
-        self.assertEqual(mock_context, con)
203
-        mock_context.load_cert_chain.assert_called_once_with(cert_path,
204
-                                                             key_path)
205
-
206
-    @unittest.skipIf(sys.version_info[:3] < (2, 7, 9),
207
-                     'This feature is unsupported in this version of python '
208
-                     'so the tests will be skipped')
209
-    @mock.patch.object(ssl, 'create_default_context', autospec=True)
210
-    def test_load_cert_chain_fails(self, mock_cdc):
211
-        CONF.set_override('use_ssl', True)
212
-        key_path = '/some/fake/path/key'
213
-        cert_path = '/some/fake/path/cert'
214
-        CONF.set_override('use_ssl', True)
215
-        CONF.set_override('ssl_key_path', key_path)
216
-        CONF.set_override('ssl_cert_path', cert_path)
217
-        mock_context = mock_cdc()
218
-        mock_context.load_cert_chain.side_effect = IOError('Boom!')
219
-        con = self.service._create_ssl_context()
220
-        self.assertEqual(mock_context, con)
221
-        mock_context.load_cert_chain.assert_called_once_with(cert_path,
222
-                                                             key_path)
223
-
224
-
225
-class TestWSGIServiceOnSigHup(BaseWSGITest):
226
-    def setUp(self):
227
-        super(TestWSGIServiceOnSigHup, self).setUp()
228
-        self.mock_spawn = self.useFixture(fixtures.MockPatchObject(
229
-            wsgi_service.eventlet, 'spawn')).mock
230
-        self.mock_mutate_conf = self.useFixture(fixtures.MockPatchObject(
231
-            wsgi_service.CONF, 'mutate_config_files')).mock
232
-
233
-    def test_on_sighup(self):
234
-        self.service._handle_sighup()
235
-        self.mock_spawn.assert_called_once_with(self.service._handle_sighup_bg)
236
-
237
-    def test_on_sighup_bg(self):
238
-        self.service._handle_sighup_bg()
239
-        self.mock_mutate_conf.assert_called_once_with()
119
+        self.assertEqual(CONF.ssl.cert_file, 'fake_new_cert')
120
+        self.assertEqual(CONF.ssl.key_file, 'fake_new_key')

+ 24
- 77
ironic_inspector/wsgi_service.py View File

@@ -10,16 +10,11 @@
10 10
 # License for the specific language governing permissions and limitations
11 11
 # under the License.
12 12
 
13
-import signal
14
-import ssl
15
-import sys
16
-
17
-import eventlet
18 13
 from oslo_config import cfg
19 14
 from oslo_log import log
20 15
 from oslo_service import service
16
+from oslo_service import wsgi
21 17
 
22
-from ironic_inspector.common.rpc_service import RPCService
23 18
 from ironic_inspector import main as app
24 19
 from ironic_inspector import utils
25 20
 
@@ -27,21 +22,22 @@ LOG = log.getLogger(__name__)
27 22
 CONF = cfg.CONF
28 23
 
29 24
 
30
-class WSGIService(object):
25
+class WSGIService(service.Service):
31 26
     """Provides ability to launch API from wsgi app."""
32 27
 
33 28
     def __init__(self):
34 29
         self.app = app.app
35
-        signal.signal(signal.SIGHUP, self._handle_sighup)
36
-        signal.signal(signal.SIGTERM, self._handle_sigterm)
37
-        self.rpc_service = RPCService(CONF.host)
30
+        self.server = wsgi.Server(CONF, 'ironic_inspector',
31
+                                  self.app,
32
+                                  host=CONF.listen_address,
33
+                                  port=CONF.listen_port,
34
+                                  use_ssl=CONF.use_ssl)
38 35
 
39 36
     def _init_middleware(self):
40 37
         """Initialize WSGI middleware.
41 38
 
42 39
         :returns: None
43 40
         """
44
-
45 41
         if CONF.auth_strategy != 'noauth':
46 42
             utils.add_auth_middleware(self.app)
47 43
         else:
@@ -49,80 +45,31 @@ class WSGIService(object):
49 45
                         ' configuration')
50 46
         utils.add_cors_middleware(self.app)
51 47
 
52
-    def _create_ssl_context(self):
53
-        if not CONF.use_ssl:
54
-            return
55
-
56
-        MIN_VERSION = (2, 7, 9)
57
-
58
-        if sys.version_info < MIN_VERSION:
59
-            LOG.warning(('Unable to use SSL in this version of Python: '
60
-                         '%(current)s, please ensure your version of Python '
61
-                         'is greater than %(min)s to enable this feature.'),
62
-                        {'current': '.'.join(map(str, sys.version_info[:3])),
63
-                         'min': '.'.join(map(str, MIN_VERSION))})
64
-            return
65
-
66
-        context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH)
67
-        if CONF.ssl_cert_path and CONF.ssl_key_path:
68
-            try:
69
-                context.load_cert_chain(CONF.ssl_cert_path, CONF.ssl_key_path)
70
-            except IOError as exc:
71
-                LOG.warning('Failed to load certificate or key from defined '
72
-                            'locations: %(cert)s and %(key)s, will continue '
73
-                            'to run with the default settings: %(exc)s',
74
-                            {'cert': CONF.ssl_cert_path,
75
-                             'key': CONF.ssl_key_path,
76
-                             'exc': exc})
77
-            except ssl.SSLError as exc:
78
-                LOG.warning('There was a problem with the loaded certificate '
79
-                            'and key, will continue to run with the default '
80
-                            'settings: %s', exc)
81
-        return context
82
-
83
-    def shutdown(self, error=None):
84
-        """Stop serving API.
48
+    def start(self):
49
+        """Start serving this service using loaded configuration.
85 50
 
86 51
         :returns: None
87 52
         """
88
-        LOG.debug('Shutting down')
89
-        self.rpc_service.stop()
90
-        sys.exit(error)
53
+        self._init_middleware()
54
+        self.server.start()
91 55
 
92
-    def run(self):
93
-        """Start serving this service using loaded application.
56
+    def stop(self):
57
+        """Stop serving this API.
94 58
 
95 59
         :returns: None
96 60
         """
97
-        app_kwargs = {'host': CONF.listen_address,
98
-                      'port': CONF.listen_port}
99
-
100
-        context = self._create_ssl_context()
101
-        if context:
102
-            app_kwargs['ssl_context'] = context
103
-
104
-        self._init_middleware()
61
+        self.server.stop()
105 62
 
106
-        LOG.info('Spawning RPC service')
107
-        service.launch(CONF, self.rpc_service,
108
-                       restart_method='mutate')
63
+    def wait(self):
64
+        """Wait for the service to stop serving this API.
109 65
 
110
-        try:
111
-            self.app.run(**app_kwargs)
112
-        except Exception as e:
113
-            self.shutdown(error=str(e))
114
-        else:
115
-            self.shutdown()
116
-
117
-    def _handle_sighup_bg(self, *args):
118
-        """Reload config on SIGHUP."""
119
-        CONF.mutate_config_files()
66
+        :returns: None
67
+        """
68
+        self.server.wait()
120 69
 
121
-    def _handle_sighup(self, *args):
122
-        eventlet.spawn(self._handle_sighup_bg, *args)
70
+    def reset(self):
71
+        """Reset server greenpool size to default.
123 72
 
124
-    def _handle_sigterm(self, *args):
125
-        # This is a workaround to ensure that shutdown() is done when recieving
126
-        # SIGTERM. Raising KeyboardIntrerrupt which won't be caught by any
127
-        # 'except Exception' clauses.
128
-        raise KeyboardInterrupt
73
+        :returns: None
74
+        """
75
+        self.server.reset()

+ 1
- 1
lower-constraints.txt View File

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

+ 7
- 0
releasenotes/notes/deprecate-ssl-opts-40ce8f4618c786ef.yaml View File

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

+ 1
- 0
requirements.txt View File

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

+ 4
- 1
tools/config-generator.conf View File

@@ -4,6 +4,9 @@ namespace = ironic_inspector
4 4
 namespace = keystonemiddleware.auth_token
5 5
 namespace = oslo.db
6 6
 namespace = oslo.log
7
+namespace = oslo.messaging
7 8
 namespace = oslo.middleware.cors
8 9
 namespace = oslo.policy
9
-namespace = oslo.messaging
10
+namespace = oslo.service.service
11
+namespace = oslo.service.sslutils
12
+namespace = oslo.service.wsgi

Loading…
Cancel
Save