From 70c803bfc1be9457aa51e14e81baf654f7dcf7e8 Mon Sep 17 00:00:00 2001 From: Hieu LE Date: Tue, 23 Aug 2016 10:57:24 +0700 Subject: [PATCH] Use werkzeug to run Magnum API with SSL wsgiref.simple_server is mono threaded process that can not support SSL context. This patch aim to replace wsgiref.simple_server with werkzeug for running development API server supporting SSL. Change-Id: Ib4360d77030e4cce8abf5ea543d87b7982e0e285 Closes-Bug: #1614596 --- magnum/api/app.py | 12 ++- magnum/cmd/api.py | 34 +++++++-- magnum/tests/unit/cmd/test_api.py | 76 ++++++++++++++++--- ...pport-ssl-magnum-api-e4896928c6562e03.yaml | 6 ++ requirements.txt | 1 + 5 files changed, 113 insertions(+), 16 deletions(-) create mode 100644 releasenotes/notes/bug-1614596-support-ssl-magnum-api-e4896928c6562e03.yaml diff --git a/magnum/api/app.py b/magnum/api/app.py index ddbcaae94e..6f1c8166fc 100644 --- a/magnum/api/app.py +++ b/magnum/api/app.py @@ -36,7 +36,17 @@ API_SERVICE_OPTS = [ cfg.StrOpt('api_paste_config', default="api-paste.ini", help="Configuration file for WSGI definition of API." - ) + ), + cfg.StrOpt('ssl_cert_file', + help="This option allows setting path to the SSL certificate " + "of API server. "), + cfg.StrOpt('ssl_key_file', + help="This option specifies the path to the file where SSL " + "private key of API server is stored when SSL is in " + "effect. "), + cfg.BoolOpt('enabled_ssl', + default=False, + help='Enable SSL Magnum API service') ] CONF = cfg.CONF diff --git a/magnum/cmd/api.py b/magnum/cmd/api.py index f9968c25fd..af81e41648 100644 --- a/magnum/cmd/api.py +++ b/magnum/cmd/api.py @@ -16,22 +16,41 @@ import os import sys -from wsgiref import simple_server from oslo_config import cfg from oslo_log import log as logging from oslo_reports import guru_meditation_report as gmr +from werkzeug import serving from magnum.api import app as api_app from magnum.common import service +from magnum.i18n import _ from magnum.i18n import _LI from magnum.objects import base from magnum import version - +CONF = cfg.CONF LOG = logging.getLogger(__name__) +def _get_ssl_configs(use_ssl): + if use_ssl: + cert_file = CONF.api.ssl_cert_file + key_file = CONF.api.ssl_key_file + + if cert_file and not os.path.exists(cert_file): + raise RuntimeError( + _("Unable to find cert_file : %s") % cert_file) + + if key_file and not os.path.exists(key_file): + raise RuntimeError( + _("Unable to find key_file : %s") % key_file) + + return cert_file, key_file + else: + return None + + def main(): service.prepare_service(sys.argv) @@ -42,15 +61,18 @@ def main(): app = api_app.load_app() + # SSL configuration + use_ssl = CONF.api.enabled_ssl + # Create the WSGI server and start it host, port = cfg.CONF.api.host, cfg.CONF.api.port - srv = simple_server.make_server(host, port, app) LOG.info(_LI('Starting server in PID %s'), os.getpid()) LOG.debug("Configuration:") cfg.CONF.log_opt_values(LOG, logging.DEBUG) - LOG.info(_LI('serving on http://%(host)s:%(port)s'), - dict(host=host, port=port)) + LOG.info(_LI('Serving on %(proto)s://%(host)s:%(port)s'), + dict(proto="https" if use_ssl else "http", host=host, port=port)) - srv.serve_forever() + serving.run_simple(host, port, app, + ssl_context=_get_ssl_configs(use_ssl)) diff --git a/magnum/tests/unit/cmd/test_api.py b/magnum/tests/unit/cmd/test_api.py index a7fa8b0085..e6ce096902 100644 --- a/magnum/tests/unit/cmd/test_api.py +++ b/magnum/tests/unit/cmd/test_api.py @@ -18,21 +18,79 @@ from magnum.cmd import api from magnum.tests import base +# NOTE(hieulq): need to mock MagnumObject, otherwise other test cases +# will be failed because of setting wrong ovo indirection api +@mock.patch('magnum.objects.base.MagnumObject') class TestMagnumAPI(base.TestCase): - # NOTE(hieulq): need to mock MagnumObject, otherwise other test cases - # will be failed because of setting wrong ovo indirection api - @mock.patch('magnum.objects.base.MagnumObject') - @mock.patch('wsgiref.simple_server.make_server') + @mock.patch('werkzeug.serving.run_simple') @mock.patch.object(api, 'api_app') @mock.patch('magnum.common.service.prepare_service') - def test_api(self, mock_prep, mock_app, mock_make, mock_base): + def test_api_http(self, mock_prep, mock_app, mock_run, mock_base): api.main() app = mock_app.load_app.return_value - server = mock_make.return_value mock_prep.assert_called_once_with(mock.ANY) mock_app.load_app.assert_called_once_with() - mock_make.assert_called_once_with(base.CONF.api.host, - base.CONF.api.port, app) - server.serve_forever.assert_called_once_with() + mock_run.assert_called_once_with(base.CONF.api.host, + base.CONF.api.port, + app, ssl_context=None) + + @mock.patch('os.path.exists') + @mock.patch('werkzeug.serving.run_simple') + @mock.patch.object(api, 'api_app') + @mock.patch('magnum.common.service.prepare_service') + def test_api_https_no_cert(self, mock_prep, mock_app, mock_run, + mock_exist, mock_base): + self.config(enabled_ssl=True, + ssl_cert_file='tmp_crt', + group='api') + mock_exist.return_value = False + + self.assertRaises(RuntimeError, api.main) + mock_prep.assert_called_once_with(mock.ANY) + mock_app.load_app.assert_called_once_with() + mock_run.assert_not_called() + mock_exist.assert_called_once_with('tmp_crt') + + @mock.patch('os.path.exists') + @mock.patch('werkzeug.serving.run_simple') + @mock.patch.object(api, 'api_app') + @mock.patch('magnum.common.service.prepare_service') + def test_api_https_no_key(self, mock_prep, mock_app, mock_run, + mock_exist, mock_base): + self.config(enabled_ssl=True, + ssl_cert_file='tmp_crt', + ssl_key_file='tmp_key', + group='api') + mock_exist.side_effect = [True, False] + + self.assertRaises(RuntimeError, api.main) + mock_prep.assert_called_once_with(mock.ANY) + mock_app.load_app.assert_called_once_with() + mock_run.assert_not_called() + mock_exist.assert_has_calls([mock.call('tmp_crt'), + mock.call('tmp_key')]) + + @mock.patch('os.path.exists') + @mock.patch('werkzeug.serving.run_simple') + @mock.patch.object(api, 'api_app') + @mock.patch('magnum.common.service.prepare_service') + def test_api_https(self, mock_prep, mock_app, mock_run, + mock_exist, mock_base): + self.config(enabled_ssl=True, + ssl_cert_file='tmp_crt', + ssl_key_file='tmp_key', + group='api') + mock_exist.side_effect = [True, True] + + api.main() + + app = mock_app.load_app.return_value + mock_prep.assert_called_once_with(mock.ANY) + mock_app.load_app.assert_called_once_with() + mock_exist.assert_has_calls([mock.call('tmp_crt'), + mock.call('tmp_key')]) + mock_run.assert_called_once_with(base.CONF.api.host, + base.CONF.api.port, app, + ssl_context=('tmp_crt', 'tmp_key')) diff --git a/releasenotes/notes/bug-1614596-support-ssl-magnum-api-e4896928c6562e03.yaml b/releasenotes/notes/bug-1614596-support-ssl-magnum-api-e4896928c6562e03.yaml new file mode 100644 index 0000000000..456f2ba57f --- /dev/null +++ b/releasenotes/notes/bug-1614596-support-ssl-magnum-api-e4896928c6562e03.yaml @@ -0,0 +1,6 @@ +--- +upgrade: + - Magnum now support SSL for API service. User can enable SSL for API + via new 3 config options 'enabled_ssl', 'ssl_cert_file' and 'ssl_key_file'. + - Change default API development service from wsgiref simple_server to + werkzeug for better supporting SSL. diff --git a/requirements.txt b/requirements.txt index eb5a8b18e8..48920ce655 100644 --- a/requirements.txt +++ b/requirements.txt @@ -55,3 +55,4 @@ stevedore>=1.16.0 # Apache-2.0 taskflow>=1.26.0 # Apache-2.0 cryptography!=1.3.0,>=1.0 # BSD/Apache-2.0 urllib3>=1.15.1 # MIT +Werkzeug>=0.7 # BSD License