From de3bb495167415f3680f956b0fcd3732df36e0ee Mon Sep 17 00:00:00 2001 From: jk464 Date: Wed, 15 May 2024 13:07:25 +0100 Subject: [PATCH 1/2] Extend SSL/TLS support to st2stream and st2api --- CHANGELOG.rst | 2 ++ st2api/st2api/app.py | 1 + st2api/st2api/cmd/api.py | 24 +++++++++++++++++++++++- st2api/st2api/config.py | 17 +++++++++++++---- st2stream/st2stream/app.py | 1 + st2stream/st2stream/cmd/api.py | 22 +++++++++++++++++++++- st2stream/st2stream/config.py | 11 +++++++++++ 7 files changed, 72 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3cf9d948b0..51c53e83b9 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -35,6 +35,8 @@ Added Contributed by @amanda11 * Ensure `.pth` files in the st2 virtualenv get loaded by pack virtualenvs. #6183 Contributed by @cognifloyd +* Add support for SSL/TLS to `st2api` and `st2stream` components. #6204 + Contributed by @jk464 3.8.1 - December 13, 2023 ------------------------- diff --git a/st2api/st2api/app.py b/st2api/st2api/app.py index ffb65ff313..035e7c0fe2 100644 --- a/st2api/st2api/app.py +++ b/st2api/st2api/app.py @@ -47,6 +47,7 @@ def setup_app(config=None): "name": "api", "listen_host": cfg.CONF.api.host, "listen_port": cfg.CONF.api.port, + "listen_ssl": cfg.CONF.api.use_ssl, "type": "active", } diff --git a/st2api/st2api/cmd/api.py b/st2api/st2api/cmd/api.py index b80f6f4bed..8bdc358ff3 100644 --- a/st2api/st2api/cmd/api.py +++ b/st2api/st2api/cmd/api.py @@ -54,6 +54,7 @@ def _setup(): "name": "api", "listen_host": cfg.CONF.api.host, "listen_port": cfg.CONF.api.port, + "listen_ssl": cfg.CONF.api.use_ssl, "type": "active", } @@ -76,13 +77,34 @@ def _setup(): def _run_server(): host = cfg.CONF.api.host port = cfg.CONF.api.port + use_ssl = cfg.CONF.api.use_ssl - LOG.info("(PID=%s) ST2 API is serving on http://%s:%s.", os.getpid(), host, port) + cert_file_path = os.path.realpath(cfg.CONF.api.cert) + key_file_path = os.path.realpath(cfg.CONF.api.key) + + if use_ssl and not os.path.isfile(cert_file_path): + raise ValueError('Certificate file "%s" doesn\'t exist' % (cert_file_path)) + + if use_ssl and not os.path.isfile(key_file_path): + raise ValueError('Private key file "%s" doesn\'t exist' % (key_file_path)) + + LOG.info( + "(PID=%s) ST2 API is serving on %s://%s:%s.", + os.getpid(), + "https" if use_ssl else "http", + host, + port, + ) max_pool_size = eventlet.wsgi.DEFAULT_MAX_SIMULTANEOUS_REQUESTS worker_pool = eventlet.GreenPool(max_pool_size) sock = eventlet.listen((host, port)) + if use_ssl: + sock = eventlet.wrap_ssl( + sock, certfile=cert_file_path, keyfile=key_file_path, server_side=True + ) + wsgi.server( sock, app.setup_app(), custom_pool=worker_pool, log=LOG, log_output=False ) diff --git a/st2api/st2api/config.py b/st2api/st2api/config.py index 5009763cdf..bc71925c88 100644 --- a/st2api/st2api/config.py +++ b/st2api/st2api/config.py @@ -76,7 +76,7 @@ def _register_app_opts(ignore_errors=False): pecan_opts, group="api_pecan", ignore_errors=ignore_errors ) - logging_opts = [ + api_opts = [ cfg.BoolOpt("debug", default=False), cfg.StrOpt( "logging", @@ -89,8 +89,17 @@ def _register_app_opts(ignore_errors=False): help="Maximum limit (page size) argument which can be " "specified by the user in a query string.", ), + cfg.BoolOpt("use_ssl", default=False, help="Specify to enable SSL / TLS mode"), + cfg.StrOpt( + "cert", + default="/etc/apache2/ssl/mycert.crt", + help='Path to the SSL certificate file. Only used when "use_ssl" is specified.', + ), + cfg.StrOpt( + "key", + default="/etc/apache2/ssl/mycert.key", + help='Path to the SSL private key file. Only used when "use_ssl" is specified.', + ), ] - common_config.do_register_opts( - logging_opts, group="api", ignore_errors=ignore_errors - ) + common_config.do_register_opts(api_opts, group="api", ignore_errors=ignore_errors) diff --git a/st2stream/st2stream/app.py b/st2stream/st2stream/app.py index 0235d1ad56..c7f41fc5fa 100644 --- a/st2stream/st2stream/app.py +++ b/st2stream/st2stream/app.py @@ -52,6 +52,7 @@ def setup_app(config={}): "name": "stream", "listen_host": cfg.CONF.stream.host, "listen_port": cfg.CONF.stream.port, + "listen_ssl": cfg.CONF.stream.use_ssl, "type": "active", } # This should be called in gunicorn case because we only want diff --git a/st2stream/st2stream/cmd/api.py b/st2stream/st2stream/cmd/api.py index 3de0c89f03..5a1101a8e7 100644 --- a/st2stream/st2stream/cmd/api.py +++ b/st2stream/st2stream/cmd/api.py @@ -60,6 +60,7 @@ def _setup(): "name": "stream", "listen_host": cfg.CONF.stream.host, "listen_port": cfg.CONF.stream.port, + "listen_ssl": cfg.CONF.stream.use_ssl, "type": "active", } common_setup( @@ -78,15 +79,34 @@ def _setup(): def _run_server(): host = cfg.CONF.stream.host port = cfg.CONF.stream.port + use_ssl = cfg.CONF.stream.use_ssl + + cert_file_path = os.path.realpath(cfg.CONF.stream.cert) + key_file_path = os.path.realpath(cfg.CONF.stream.key) + + if use_ssl and not os.path.isfile(cert_file_path): + raise ValueError('Certificate file "%s" doesn\'t exist' % (cert_file_path)) + + if use_ssl and not os.path.isfile(key_file_path): + raise ValueError('Private key file "%s" doesn\'t exist' % (key_file_path)) LOG.info( - "(PID=%s) ST2 Stream API is serving on http://%s:%s.", os.getpid(), host, port + "(PID=%s) ST2 Stream API is serving on %s://%s:%s.", + os.getpid(), + "https" if use_ssl else "http", + host, + port, ) max_pool_size = eventlet.wsgi.DEFAULT_MAX_SIMULTANEOUS_REQUESTS worker_pool = eventlet.GreenPool(max_pool_size) sock = eventlet.listen((host, port)) + if use_ssl: + sock = eventlet.wrap_ssl( + sock, certfile=cert_file_path, keyfile=key_file_path, server_side=True + ) + def queue_shutdown(signal_number, stack_frame): deregister_service(STREAM) eventlet.spawn_n( diff --git a/st2stream/st2stream/config.py b/st2stream/st2stream/config.py index eef5c55830..6d76706445 100644 --- a/st2stream/st2stream/config.py +++ b/st2stream/st2stream/config.py @@ -66,6 +66,17 @@ def _register_app_opts(ignore_errors=False): default="/etc/st2/logging.stream.conf", help="location of the logging.conf file", ), + cfg.BoolOpt("use_ssl", default=False, help="Specify to enable SSL / TLS mode"), + cfg.StrOpt( + "cert", + default="/etc/apache2/ssl/mycert.crt", + help='Path to the SSL certificate file. Only used when "use_ssl" is specified.', + ), + cfg.StrOpt( + "key", + default="/etc/apache2/ssl/mycert.key", + help='Path to the SSL private key file. Only used when "use_ssl" is specified.', + ), ] common_config.do_register_opts( From 53d2e928211b9aff1cb602e619d17eb49d621324 Mon Sep 17 00:00:00 2001 From: jk464 Date: Wed, 22 May 2024 15:21:27 +0100 Subject: [PATCH 2/2] Move common API code into shared library --- CHANGELOG.rst | 2 + st2api/st2api/app.py | 90 ++++------------ st2api/st2api/cmd/api.py | 107 ++++--------------- st2api/st2api/config.py | 37 +------ st2auth/st2auth/app.py | 86 ++++----------- st2auth/st2auth/cmd/api.py | 98 ++++------------- st2auth/st2auth/config.py | 43 ++------ st2common/st2common/openapi/BUILD | 1 + st2common/st2common/openapi/__init__.py | 0 st2common/st2common/openapi/api.py | 136 ++++++++++++++++++++++++ st2common/st2common/openapi/app.py | 124 +++++++++++++++++++++ st2common/st2common/openapi/config.py | 63 +++++++++++ st2stream/st2stream/app.py | 74 +++---------- st2stream/st2stream/cmd/api.py | 116 +++----------------- st2stream/st2stream/config.py | 39 ++----- 15 files changed, 454 insertions(+), 562 deletions(-) create mode 100644 st2common/st2common/openapi/BUILD create mode 100644 st2common/st2common/openapi/__init__.py create mode 100644 st2common/st2common/openapi/api.py create mode 100644 st2common/st2common/openapi/app.py create mode 100644 st2common/st2common/openapi/config.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 51c53e83b9..a52b51b4d2 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -37,6 +37,8 @@ Added Contributed by @cognifloyd * Add support for SSL/TLS to `st2api` and `st2stream` components. #6204 Contributed by @jk464 +* Move API code for `st2api`, `st2auth` and `st2stream` components to common library `st2common.openapi`. #6204 + Contributed by @jk464 3.8.1 - December 13, 2023 ------------------------- diff --git a/st2api/st2api/app.py b/st2api/st2api/app.py index 035e7c0fe2..be8212b5d3 100644 --- a/st2api/st2api/app.py +++ b/st2api/st2api/app.py @@ -16,83 +16,39 @@ from oslo_config import cfg from st2api import config as st2api_config -from st2common import log as logging -from st2common.middleware.streaming import StreamingMiddleware -from st2common.middleware.error_handling import ErrorHandlingMiddleware -from st2common.middleware.cors import CorsMiddleware -from st2common.middleware.request_id import RequestIDMiddleware -from st2common.middleware.logging import LoggingMiddleware -from st2common.middleware.instrumentation import RequestInstrumentationMiddleware -from st2common.middleware.instrumentation import ResponseInstrumentationMiddleware -from st2common.router import Router -from st2common.constants.system import VERSION_STRING -from st2common.service_setup import setup as common_setup -from st2common.util import spec_loader +from st2common.openapi import app from st2api.validation import validate_auth_cookie_is_correctly_configured from st2api.validation import validate_rbac_is_correctly_configured -LOG = logging.getLogger(__name__) +def setup_app(config={}): + common_setup = { + "register_mq_exchanges": True, + "register_internal_trigger_types": True, + "run_migrations": True, + } -def setup_app(config=None): - config = config or {} - - LOG.info("Creating st2api: %s as OpenAPI app.", VERSION_STRING) - - is_gunicorn = config.get("is_gunicorn", False) - if is_gunicorn: - # NOTE: We only want to perform this logic in the WSGI worker - st2api_config.register_opts(ignore_errors=True) - capabilities = { - "name": "api", - "listen_host": cfg.CONF.api.host, - "listen_port": cfg.CONF.api.port, - "listen_ssl": cfg.CONF.api.use_ssl, - "type": "active", - } - - # This should be called in gunicorn case because we only want - # workers to connect to db, rabbbitmq etc. In standalone HTTP - # server case, this setup would have already occurred. - common_setup( - service="api", - config=st2api_config, - setup_db=True, - register_mq_exchanges=True, - register_signal_handlers=True, - register_internal_trigger_types=True, - run_migrations=True, - service_registry=True, - capabilities=capabilities, - config_args=config.get("config_args", None), - ) - - # Additional pre-run time checks - validate_auth_cookie_is_correctly_configured() - validate_rbac_is_correctly_configured() - - router = Router( - debug=cfg.CONF.api.debug, auth=cfg.CONF.auth.enable, is_gunicorn=is_gunicorn - ) + pre_run_checks = [ + validate_auth_cookie_is_correctly_configured, + validate_rbac_is_correctly_configured, + ] - spec = spec_loader.load_spec("st2common", "openapi.yaml.j2") transforms = { "^/api/v1/$": ["/v1"], "^/api/v1/": ["/", "/v1/"], "^/api/v1/executions": ["/actionexecutions", "/v1/actionexecutions"], "^/api/exp/": ["/exp/"], } - router.add_spec(spec, transforms=transforms) - - app = router.as_wsgi - # Order is important. Check middleware for detailed explanation. - app = StreamingMiddleware(app, path_whitelist=["/v1/executions/*/output*"]) - app = ErrorHandlingMiddleware(app) - app = CorsMiddleware(app) - app = LoggingMiddleware(app, router) - app = ResponseInstrumentationMiddleware(app, router, service_name="api") - app = RequestIDMiddleware(app) - app = RequestInstrumentationMiddleware(app, router, service_name="api") - - return app + path_whitelist = ["/v1/executions/*/output*"] + + return app.setup_app( + service_name="api", + app_config=st2api_config, + oslo_cfg=cfg.CONF.api, + pre_run_checks=pre_run_checks, + transforms=transforms, + common_setup_kwargs=common_setup, + path_whitelist=path_whitelist, + config=config, + ) diff --git a/st2api/st2api/cmd/api.py b/st2api/st2api/cmd/api.py index 8bdc358ff3..eef0ae133c 100644 --- a/st2api/st2api/cmd/api.py +++ b/st2api/st2api/cmd/api.py @@ -13,9 +13,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import os -import sys - # NOTE: It's important that we perform monkey patch as early as possible before any other modules # are important, otherwise SSL support for MongoDB won't work. # See https://github.com/StackStorm/st2/issues/4832 and https://github.com/gevent/gevent/issues/1016 @@ -24,15 +21,11 @@ monkey_patch() -import eventlet from oslo_config import cfg -from eventlet import wsgi from st2common import log as logging -from st2common.service_setup import setup as common_setup -from st2common.service_setup import teardown as common_teardown -from st2common.service_setup import deregister_service from st2api import config +from st2common.openapi import api config.register_opts(ignore_errors=True) @@ -43,87 +36,27 @@ __all__ = ["main"] LOG = logging.getLogger(__name__) -API = "api" - -# How much time to give to the request in progress to finish in seconds before killing them -WSGI_SERVER_REQUEST_SHUTDOWN_TIME = 2 -def _setup(): - capabilities = { - "name": "api", - "listen_host": cfg.CONF.api.host, - "listen_port": cfg.CONF.api.port, - "listen_ssl": cfg.CONF.api.use_ssl, - "type": "active", +def main(): + common_setup = { + "register_mq_exchanges": True, + "register_internal_trigger_types": True, } - common_setup( - service=API, - config=config, - setup_db=True, - register_mq_exchanges=True, - register_signal_handlers=True, - register_internal_trigger_types=True, - service_registry=True, - capabilities=capabilities, - ) - - # Additional pre-run time checks - validate_auth_cookie_is_correctly_configured() - validate_rbac_is_correctly_configured() - - -def _run_server(): - host = cfg.CONF.api.host - port = cfg.CONF.api.port - use_ssl = cfg.CONF.api.use_ssl - - cert_file_path = os.path.realpath(cfg.CONF.api.cert) - key_file_path = os.path.realpath(cfg.CONF.api.key) - - if use_ssl and not os.path.isfile(cert_file_path): - raise ValueError('Certificate file "%s" doesn\'t exist' % (cert_file_path)) - - if use_ssl and not os.path.isfile(key_file_path): - raise ValueError('Private key file "%s" doesn\'t exist' % (key_file_path)) - - LOG.info( - "(PID=%s) ST2 API is serving on %s://%s:%s.", - os.getpid(), - "https" if use_ssl else "http", - host, - port, - ) - - max_pool_size = eventlet.wsgi.DEFAULT_MAX_SIMULTANEOUS_REQUESTS - worker_pool = eventlet.GreenPool(max_pool_size) - sock = eventlet.listen((host, port)) - - if use_ssl: - sock = eventlet.wrap_ssl( - sock, certfile=cert_file_path, keyfile=key_file_path, server_side=True - ) - - wsgi.server( - sock, app.setup_app(), custom_pool=worker_pool, log=LOG, log_output=False + pre_run_checks = [ + validate_auth_cookie_is_correctly_configured, + validate_rbac_is_correctly_configured, + ] + + api.run( + service_name="api", + app_config=config, + cfg=cfg.CONF.api, + app=app, + log=LOG, + use_custom_pool=False, + log_output=False, + common_setup_kwargs=common_setup, + pre_run_checks=pre_run_checks, ) - return 0 - - -def _teardown(): - common_teardown() - - -def main(): - try: - _setup() - return _run_server() - except SystemExit as exit_code: - deregister_service(API) - sys.exit(exit_code) - except Exception: - LOG.exception("(PID=%s) ST2 API quit due to exception.", os.getpid()) - return 1 - finally: - _teardown() diff --git a/st2api/st2api/config.py b/st2api/st2api/config.py index bc71925c88..4bc168d929 100644 --- a/st2api/st2api/config.py +++ b/st2api/st2api/config.py @@ -24,32 +24,22 @@ from oslo_config import cfg import st2common.config as common_config -from st2common.constants.system import VERSION_STRING -from st2common.constants.system import DEFAULT_CONFIG_FILE_PATH +from st2common.openapi import config CONF = cfg.CONF BASE_DIR = os.path.dirname(os.path.abspath(__file__)) def parse_args(args=None): - cfg.CONF( - args=args, - version=VERSION_STRING, - default_config_files=[DEFAULT_CONFIG_FILE_PATH], - ) + config.parse_args(args=args) def register_opts(ignore_errors=False): - _register_common_opts(ignore_errors=ignore_errors) - _register_app_opts(ignore_errors=ignore_errors) - - -def _register_common_opts(ignore_errors=False): - common_config.register_opts(ignore_errors=ignore_errors) + config.register_opts(_register_app_opts, ignore_errors=ignore_errors) def get_logging_config_path(): - return cfg.CONF.api.logging + return config.get_logging_config_path(cfg.CONF.api) def _register_app_opts(ignore_errors=False): @@ -76,30 +66,13 @@ def _register_app_opts(ignore_errors=False): pecan_opts, group="api_pecan", ignore_errors=ignore_errors ) - api_opts = [ - cfg.BoolOpt("debug", default=False), - cfg.StrOpt( - "logging", - default="/etc/st2/logging.api.conf", - help="location of the logging.conf file", - ), + api_opts = config.get_base_opts("api") + [ cfg.IntOpt( "max_page_size", default=100, help="Maximum limit (page size) argument which can be " "specified by the user in a query string.", ), - cfg.BoolOpt("use_ssl", default=False, help="Specify to enable SSL / TLS mode"), - cfg.StrOpt( - "cert", - default="/etc/apache2/ssl/mycert.crt", - help='Path to the SSL certificate file. Only used when "use_ssl" is specified.', - ), - cfg.StrOpt( - "key", - default="/etc/apache2/ssl/mycert.key", - help='Path to the SSL private key file. Only used when "use_ssl" is specified.', - ), ] common_config.do_register_opts(api_opts, group="api", ignore_errors=ignore_errors) diff --git a/st2auth/st2auth/app.py b/st2auth/st2auth/app.py index 2a90779bf4..1b284e967b 100644 --- a/st2auth/st2auth/app.py +++ b/st2auth/st2auth/app.py @@ -15,78 +15,28 @@ from oslo_config import cfg -from st2common import log as logging -from st2common.middleware.error_handling import ErrorHandlingMiddleware -from st2common.middleware.cors import CorsMiddleware -from st2common.middleware.request_id import RequestIDMiddleware -from st2common.middleware.logging import LoggingMiddleware -from st2common.middleware.instrumentation import RequestInstrumentationMiddleware -from st2common.middleware.instrumentation import ResponseInstrumentationMiddleware -from st2common.router import Router -from st2common.constants.system import VERSION_STRING -from st2common.service_setup import setup as common_setup -from st2common.util import spec_loader -from st2common.util.monkey_patch import use_select_poll_workaround +from st2common.openapi import app from st2auth import config as st2auth_config from st2auth.validation import validate_auth_backend_is_correctly_configured -LOG = logging.getLogger(__name__) +def setup_app(config={}): + common_setup = { + "register_mq_exchanges": False, + "register_internal_trigger_types": False, + "run_migrations": False, + } -def setup_app(config=None): - config = config or {} - - LOG.info("Creating st2auth: %s as OpenAPI app.", VERSION_STRING) - - is_gunicorn = config.get("is_gunicorn", False) - if is_gunicorn: - # NOTE: We only want to perform this logic in the WSGI worker - st2auth_config.register_opts(ignore_errors=True) - capabilities = { - "name": "auth", - "listen_host": cfg.CONF.auth.host, - "listen_port": cfg.CONF.auth.port, - "listen_ssl": cfg.CONF.auth.use_ssl, - "type": "active", - } - - # This should be called in gunicorn case because we only want - # workers to connect to db, rabbbitmq etc. In standalone HTTP - # server case, this setup would have already occurred. - common_setup( - service="auth", - config=st2auth_config, - setup_db=True, - register_mq_exchanges=False, - register_signal_handlers=True, - register_internal_trigger_types=False, - run_migrations=False, - service_registry=True, - capabilities=capabilities, - config_args=config.get("config_args", None), - ) - - # pysaml2 uses subprocess communicate which calls communicate_with_poll - if cfg.CONF.auth.sso and cfg.CONF.auth.sso_backend == "saml2": - use_select_poll_workaround(nose_only=False) - - # Additional pre-run time checks - validate_auth_backend_is_correctly_configured() - - router = Router(debug=cfg.CONF.auth.debug, is_gunicorn=is_gunicorn) - - spec = spec_loader.load_spec("st2common", "openapi.yaml.j2") transforms = {"^/auth/v1/": ["/", "/v1/"]} - router.add_spec(spec, transforms=transforms) - - app = router.as_wsgi - - # Order is important. Check middleware for detailed explanation. - app = ErrorHandlingMiddleware(app) - app = CorsMiddleware(app) - app = LoggingMiddleware(app, router) - app = ResponseInstrumentationMiddleware(app, router, service_name="auth") - app = RequestIDMiddleware(app) - app = RequestInstrumentationMiddleware(app, router, service_name="auth") - return app + pre_run_checks = [validate_auth_backend_is_correctly_configured] + + return app.setup_app( + service_name="auth", + app_config=st2auth_config, + oslo_cfg=cfg.CONF.auth, + pre_run_checks=pre_run_checks, + transforms=transforms, + common_setup_kwargs=common_setup, + config=config, + ) diff --git a/st2auth/st2auth/cmd/api.py b/st2auth/st2auth/cmd/api.py index e817765d97..3290ffeee8 100644 --- a/st2auth/st2auth/cmd/api.py +++ b/st2auth/st2auth/cmd/api.py @@ -17,18 +17,11 @@ monkey_patch() -import eventlet -import os -import sys - from oslo_config import cfg -from eventlet import wsgi from st2common import log as logging -from st2common.service_setup import setup as common_setup -from st2common.service_setup import teardown as common_teardown -from st2common.service_setup import deregister_service from st2auth import config +from st2common.openapi import api config.register_opts(ignore_errors=True) @@ -40,80 +33,25 @@ LOG = logging.getLogger(__name__) -AUTH = "auth" -def _setup(): - capabilities = { - "name": "auth", - "listen_host": cfg.CONF.auth.host, - "listen_port": cfg.CONF.auth.port, - "listen_ssl": cfg.CONF.auth.use_ssl, - "type": "active", +def main(): + common_setup = { + "register_mq_exchanges": False, + "register_internal_trigger_types": False, + "run_migrations": False, } - common_setup( - service=AUTH, - config=config, - setup_db=True, - register_mq_exchanges=False, - register_signal_handlers=True, - register_internal_trigger_types=False, - run_migrations=False, - service_registry=True, - capabilities=capabilities, - ) - - # Additional pre-run time checks - validate_auth_backend_is_correctly_configured() - - -def _run_server(): - host = cfg.CONF.auth.host - port = cfg.CONF.auth.port - use_ssl = cfg.CONF.auth.use_ssl - - cert_file_path = os.path.realpath(cfg.CONF.auth.cert) - key_file_path = os.path.realpath(cfg.CONF.auth.key) - - if use_ssl and not os.path.isfile(cert_file_path): - raise ValueError('Certificate file "%s" doesn\'t exist' % (cert_file_path)) - - if use_ssl and not os.path.isfile(key_file_path): - raise ValueError('Private key file "%s" doesn\'t exist' % (key_file_path)) - socket = eventlet.listen((host, port)) - - if use_ssl: - socket = eventlet.wrap_ssl( - socket, certfile=cert_file_path, keyfile=key_file_path, server_side=True - ) - - LOG.info('ST2 Auth API running in "%s" auth mode', cfg.CONF.auth.mode) - LOG.info( - "(PID=%s) ST2 Auth API is serving on %s://%s:%s.", - os.getpid(), - "https" if use_ssl else "http", - host, - port, + pre_run_checks = [validate_auth_backend_is_correctly_configured] + + api.run( + service_name="auth", + app_config=config, + cfg=cfg.CONF.auth, + app=app, + log=LOG, + use_custom_pool=True, + log_output=True, + common_setup_kwargs=common_setup, + pre_run_checks=pre_run_checks, ) - - wsgi.server(socket, app.setup_app(), log=LOG, log_output=False) - return 0 - - -def _teardown(): - common_teardown() - - -def main(): - try: - _setup() - return _run_server() - except SystemExit as exit_code: - deregister_service(AUTH) - sys.exit(exit_code) - except Exception: - LOG.exception("(PID=%s) ST2 Auth API quit due to exception.", os.getpid()) - return 1 - finally: - _teardown() diff --git a/st2auth/st2auth/config.py b/st2auth/st2auth/config.py index aa85ae4584..bc00cf1a3e 100644 --- a/st2auth/st2auth/config.py +++ b/st2auth/st2auth/config.py @@ -17,41 +17,31 @@ from oslo_config import cfg -from st2common import config as st2cfg -from st2common.constants.system import VERSION_STRING -from st2common.constants.system import DEFAULT_CONFIG_FILE_PATH +import st2common.config as common_config from st2common.constants.auth import DEFAULT_MODE from st2common.constants.auth import DEFAULT_BACKEND from st2common.constants.auth import DEFAULT_SSO_BACKEND from st2common.constants.auth import VALID_MODES +from st2common.openapi import config from st2auth import backends as auth_backends def parse_args(args=None): - cfg.CONF( - args=args, - version=VERSION_STRING, - default_config_files=[DEFAULT_CONFIG_FILE_PATH], - ) + config.parse_args(args=args) def register_opts(ignore_errors=False): - _register_common_opts(ignore_errors=ignore_errors) - _register_app_opts(ignore_errors=ignore_errors) + config.register_opts(_register_app_opts, ignore_errors=ignore_errors) def get_logging_config_path(): - return cfg.CONF.auth.logging - - -def _register_common_opts(ignore_errors=False): - st2cfg.register_opts(ignore_errors=ignore_errors) + return config.get_logging_config_path(cfg.CONF.auth) def _register_app_opts(ignore_errors=False): available_backends = auth_backends.get_available_backends() - auth_opts = [ + auth_opts = config.get_base_opts("auth") + [ cfg.StrOpt( "host", default="127.0.0.1", @@ -60,23 +50,6 @@ def _register_app_opts(ignore_errors=False): cfg.IntOpt( "port", default=9100, help="Port on which the service should listen on." ), - cfg.BoolOpt("use_ssl", default=False, help="Specify to enable SSL / TLS mode"), - cfg.StrOpt( - "cert", - default="/etc/apache2/ssl/mycert.crt", - help='Path to the SSL certificate file. Only used when "use_ssl" is specified.', - ), - cfg.StrOpt( - "key", - default="/etc/apache2/ssl/mycert.key", - help='Path to the SSL private key file. Only used when "use_ssl" is specified.', - ), - cfg.StrOpt( - "logging", - default="/etc/st2/logging.auth.conf", - help="Path to the logging config.", - ), - cfg.BoolOpt("debug", default=False, help="Specify to enable debug mode."), cfg.StrOpt( "mode", default=DEFAULT_MODE, @@ -110,4 +83,6 @@ def _register_app_opts(ignore_errors=False): ), ] - st2cfg.do_register_cli_opts(auth_opts, group="auth", ignore_errors=ignore_errors) + common_config.do_register_cli_opts( + auth_opts, group="auth", ignore_errors=ignore_errors + ) diff --git a/st2common/st2common/openapi/BUILD b/st2common/st2common/openapi/BUILD new file mode 100644 index 0000000000..db46e8d6c9 --- /dev/null +++ b/st2common/st2common/openapi/BUILD @@ -0,0 +1 @@ +python_sources() diff --git a/st2common/st2common/openapi/__init__.py b/st2common/st2common/openapi/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/st2common/st2common/openapi/api.py b/st2common/st2common/openapi/api.py new file mode 100644 index 0000000000..e7180f5d0f --- /dev/null +++ b/st2common/st2common/openapi/api.py @@ -0,0 +1,136 @@ +# Copyright 2020 The StackStorm Authors. +# Copyright 2019 Extreme Networks, Inc. +# +# 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 eventlet +import os +import sys + +from eventlet import wsgi + +from st2common.openapi import app as common_app +from st2common.service_setup import deregister_service +from st2common.service_setup import teardown as common_teardown +from st2common.stream.listener import get_listener_if_set +from st2common.util.wsgi import shutdown_server_kill_pending_requests +from st2stream.signal_handlers import register_stream_signal_handlers + + +def run_server( + service_name, cfg, app, log=None, log_output=True, use_custom_pool=False +): + host = cfg.host + port = cfg.port + use_ssl = cfg.use_ssl + + cert_file_path = os.path.realpath(cfg.cert) + key_file_path = os.path.realpath(cfg.key) + + if use_ssl and not os.path.isfile(cert_file_path): + raise ValueError('Certificate file "%s" doesn\'t exist' % (cert_file_path)) + + if use_ssl and not os.path.isfile(key_file_path): + raise ValueError('Private key file "%s" doesn\'t exist' % (key_file_path)) + + worker_pool = None + if use_custom_pool: + max_pool_size = eventlet.wsgi.DEFAULT_MAX_SIMULTANEOUS_REQUESTS + worker_pool = eventlet.GreenPool(max_pool_size) + + socket = eventlet.listen((host, port)) + + if use_ssl: + socket = eventlet.wrap_ssl( + socket, certfile=cert_file_path, keyfile=key_file_path, server_side=True + ) + + if service_name == "auth": + log.info('ST2 Auth API running in "%s" auth mode', cfg.mode) + + if service_name == "stream": + + def queue_shutdown(signal_number, stack_frame): + deregister_service(service_name) + eventlet.spawn_n( + shutdown_server_kill_pending_requests, + sock=socket, + worker_pool=worker_pool, + wait_time=2, + ) + + # We register a custom SIGINT handler which allows us to kill long running active requests. + # Note: Eventually we will support draining (waiting for short-running requests), but we + # will still want to kill long running stream requests. + register_stream_signal_handlers(handler_func=queue_shutdown) + + log.info( + "(PID=%s) ST2 %s API is serving on %s://%s:%s.", + os.getpid(), + service_name, + "https" if use_ssl else "http", + host, + port, + ) + + wsgi.server( + socket, app.setup_app(), custom_pool=worker_pool, log=log, log_output=log_output + ) + + return 0 + + +def run( + service_name, + app_config, + cfg, + app, + log, + use_custom_pool=False, + log_output=True, + common_setup_kwargs={}, + pre_run_checks=[], +): + try: + common_app.setup( + service_name=service_name, + app_config=app_config, + oslo_cfg=cfg, + common_setup_kwargs=common_setup_kwargs, + ) + common_app.run_pre_run_checks(pre_run_checks=pre_run_checks) + return run_server( + service_name=service_name, + cfg=cfg, + app=app, + log=log, + use_custom_pool=use_custom_pool, + log_output=log_output, + ) + except SystemExit as exit_code: + deregister_service(service_name) + + if service_name == "stream": + listener = get_listener_if_set(name=service_name) + + if listener: + listener.shutdown() + else: + sys.exit(exit_code) + except Exception: + log.exception( + "(PID=%s) ST2 %s API quit due to exception.", os.getpid(), service_name + ) + return 1 + finally: + common_teardown() diff --git a/st2common/st2common/openapi/app.py b/st2common/st2common/openapi/app.py new file mode 100644 index 0000000000..ce7a3b9aff --- /dev/null +++ b/st2common/st2common/openapi/app.py @@ -0,0 +1,124 @@ +# Copyright 2020 The StackStorm Authors. +# Copyright 2019 Extreme Networks, Inc. +# +# 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 st2common import log as logging +from st2common.middleware.streaming import StreamingMiddleware +from st2common.middleware.error_handling import ErrorHandlingMiddleware +from st2common.middleware.cors import CorsMiddleware +from st2common.middleware.request_id import RequestIDMiddleware +from st2common.middleware.logging import LoggingMiddleware +from st2common.middleware.instrumentation import RequestInstrumentationMiddleware +from st2common.middleware.instrumentation import ResponseInstrumentationMiddleware +from st2common.router import Router +from st2common.constants.system import VERSION_STRING +from st2common.service_setup import setup as common_setup +from st2common.util import spec_loader +from st2common.util.monkey_patch import use_select_poll_workaround + +LOG = logging.getLogger(__name__) + + +def setup( + service_name, + app_config, + oslo_cfg, + common_setup_kwargs={}, +): + capabilities = { + "name": service_name, + "listen_host": oslo_cfg.host, + "listen_port": oslo_cfg.port, + "listen_ssl": oslo_cfg.use_ssl, + "type": "active", + } + + # This should be called in gunicorn case because we only want + # workers to connect to db, rabbbitmq etc. In standalone HTTP + # server case, this setup would have already occurred. + common_setup( + service=service_name, + config=app_config, + setup_db=True, + register_signal_handlers=True, + service_registry=True, + capabilities=capabilities, + **common_setup_kwargs, + ) + + +def run_pre_run_checks(pre_run_checks=[]): + # Additional pre-run time checks + for pre_run_check in pre_run_checks: + pre_run_check() + + +def setup_app( + service_name, + app_config, + oslo_cfg, + pre_run_checks=[], + transforms={}, + common_setup_kwargs={}, + path_whitelist=[], + config={}, +): + LOG.info("Creating st2%s: %s as OpenAPI app.", service_name, VERSION_STRING) + + is_gunicorn = config.get("is_gunicorn", False) + if is_gunicorn: + # NOTE: We only want to perform this logic in the WSGI worker + app_config.register_opts(ignore_errors=True) + + common_setup_kwargs["config_args"] = config.get("config_args", None) + setup( + service_name=service_name, + app_config=app_config, + oslo_cfg=oslo_cfg, + common_setup_kwargs=common_setup_kwargs, + ) + + if service_name == "auth": + # pysaml2 uses subprocess communicate which calls communicate_with_poll + if oslo_cfg.sso and oslo_cfg.sso_backend == "saml2": + use_select_poll_workaround(nose_only=False) + + run_pre_run_checks(pre_run_checks=pre_run_checks) + + auth = True + + if service_name != "auth": + auth = cfg.CONF.auth.enable + + router = Router(debug=oslo_cfg.debug, is_gunicorn=is_gunicorn, auth=auth) + + spec = spec_loader.load_spec("st2common", "openapi.yaml.j2") + router.add_spec(spec, transforms=transforms) + + app = router.as_wsgi + + # Order is important. Check middleware for detailed explanation. + if service_name != "auth": + app = StreamingMiddleware(app, path_whitelist=path_whitelist) + + app = ErrorHandlingMiddleware(app) + app = CorsMiddleware(app) + app = LoggingMiddleware(app, router) + app = ResponseInstrumentationMiddleware(app, router, service_name=service_name) + app = RequestIDMiddleware(app) + app = RequestInstrumentationMiddleware(app, router, service_name=service_name) + + return app diff --git a/st2common/st2common/openapi/config.py b/st2common/st2common/openapi/config.py new file mode 100644 index 0000000000..739aad5eb0 --- /dev/null +++ b/st2common/st2common/openapi/config.py @@ -0,0 +1,63 @@ +# Copyright 2020 The StackStorm Authors. +# Copyright 2019 Extreme Networks, Inc. +# +# 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 + +import st2common.config as common_config +from st2common.constants.system import VERSION_STRING +from st2common.constants.system import DEFAULT_CONFIG_FILE_PATH + + +def parse_args(args=None): + cfg.CONF( + args=args, + version=VERSION_STRING, + default_config_files=[DEFAULT_CONFIG_FILE_PATH], + ) + + +def register_opts(_register_app_opts, ignore_errors=False): + _register_common_opts(ignore_errors=ignore_errors) + _register_app_opts(ignore_errors=ignore_errors) + + +def get_logging_config_path(conf_path): + return conf_path.logging + + +def _register_common_opts(ignore_errors=False): + common_config.register_opts(ignore_errors=ignore_errors) + + +def get_base_opts(service): + return [ + cfg.BoolOpt("use_ssl", default=False, help="Specify to enable SSL / TLS mode"), + cfg.StrOpt( + "cert", + default="/etc/apache2/ssl/mycert.crt", + help='Path to the SSL certificate file. Only used when "use_ssl" is specified.', + ), + cfg.StrOpt( + "key", + default="/etc/apache2/ssl/mycert.key", + help='Path to the SSL private key file. Only used when "use_ssl" is specified.', + ), + cfg.StrOpt( + "logging", + default=f"/etc/st2/logging.{service}.conf", + help="Path to the logging config.", + ), + cfg.BoolOpt("debug", default=False, help="Specify to enable debug mode."), + ] diff --git a/st2stream/st2stream/app.py b/st2stream/st2stream/app.py index c7f41fc5fa..1a411e8d3d 100644 --- a/st2stream/st2stream/app.py +++ b/st2stream/st2stream/app.py @@ -24,70 +24,24 @@ from oslo_config import cfg +from st2common.openapi import app from st2stream import config as st2stream_config -from st2common import log as logging -from st2common.middleware.streaming import StreamingMiddleware -from st2common.middleware.error_handling import ErrorHandlingMiddleware -from st2common.middleware.cors import CorsMiddleware -from st2common.middleware.request_id import RequestIDMiddleware -from st2common.middleware.logging import LoggingMiddleware -from st2common.middleware.instrumentation import RequestInstrumentationMiddleware -from st2common.middleware.instrumentation import ResponseInstrumentationMiddleware -from st2common.router import Router -from st2common.constants.system import VERSION_STRING -from st2common.service_setup import setup as common_setup -from st2common.util import spec_loader - -LOG = logging.getLogger(__name__) def setup_app(config={}): - LOG.info("Creating st2stream: %s as OpenAPI app.", VERSION_STRING) - - is_gunicorn = config.get("is_gunicorn", False) - if is_gunicorn: - - st2stream_config.register_opts(ignore_errors=True) - capabilities = { - "name": "stream", - "listen_host": cfg.CONF.stream.host, - "listen_port": cfg.CONF.stream.port, - "listen_ssl": cfg.CONF.stream.use_ssl, - "type": "active", - } - # This should be called in gunicorn case because we only want - # workers to connect to db, rabbbitmq etc. In standalone HTTP - # server case, this setup would have already occurred. - common_setup( - service="stream", - config=st2stream_config, - setup_db=True, - register_mq_exchanges=True, - register_signal_handlers=True, - register_internal_trigger_types=False, - run_migrations=False, - service_registry=True, - capabilities=capabilities, - config_args=config.get("config_args", None), - ) - - router = Router( - debug=cfg.CONF.stream.debug, auth=cfg.CONF.auth.enable, is_gunicorn=is_gunicorn - ) + common_setup = { + "register_mq_exchanges": True, + "register_internal_trigger_types": False, + "run_migrations": False, + } - spec = spec_loader.load_spec("st2common", "openapi.yaml.j2") transforms = {"^/stream/v1/": ["/", "/v1/"]} - router.add_spec(spec, transforms=transforms) - - app = router.as_wsgi - # Order is important. Check middleware for detailed explanation. - app = StreamingMiddleware(app) - app = ErrorHandlingMiddleware(app) - app = CorsMiddleware(app) - app = LoggingMiddleware(app, router) - app = ResponseInstrumentationMiddleware(app, router, service_name="stream") - app = RequestIDMiddleware(app) - app = RequestInstrumentationMiddleware(app, router, service_name="stream") - - return app + return app.setup_app( + service_name="stream", + app_config=st2stream_config, + oslo_cfg=cfg.CONF.stream, + transforms=transforms, + common_setup_kwargs=common_setup, + config=config, + ) diff --git a/st2stream/st2stream/cmd/api.py b/st2stream/st2stream/cmd/api.py index 5a1101a8e7..6528a5cd68 100644 --- a/st2stream/st2stream/cmd/api.py +++ b/st2stream/st2stream/cmd/api.py @@ -17,21 +17,14 @@ monkey_patch() -import os import sys import eventlet from oslo_config import cfg -from eventlet import wsgi from st2common import log as logging -from st2common.service_setup import setup as common_setup -from st2common.service_setup import teardown as common_teardown -from st2common.service_setup import deregister_service -from st2common.stream.listener import get_listener_if_set -from st2common.util.wsgi import shutdown_server_kill_pending_requests -from st2stream.signal_handlers import register_stream_signal_handlers from st2stream import config +from st2common.openapi import api config.register_opts(ignore_errors=True) @@ -49,101 +42,22 @@ ) LOG = logging.getLogger(__name__) -STREAM = "stream" -# How much time to give to the request in progress to finish in seconds before killing them -WSGI_SERVER_REQUEST_SHUTDOWN_TIME = 2 - -def _setup(): - capabilities = { - "name": "stream", - "listen_host": cfg.CONF.stream.host, - "listen_port": cfg.CONF.stream.port, - "listen_ssl": cfg.CONF.stream.use_ssl, - "type": "active", +def main(): + common_setup = { + "register_mq_exchanges": True, + "register_internal_trigger_types": False, + "run_migrations": False, } - common_setup( - service=STREAM, - config=config, - setup_db=True, - register_mq_exchanges=True, - register_signal_handlers=True, - register_internal_trigger_types=False, - run_migrations=False, - service_registry=True, - capabilities=capabilities, - ) - - -def _run_server(): - host = cfg.CONF.stream.host - port = cfg.CONF.stream.port - use_ssl = cfg.CONF.stream.use_ssl - cert_file_path = os.path.realpath(cfg.CONF.stream.cert) - key_file_path = os.path.realpath(cfg.CONF.stream.key) - - if use_ssl and not os.path.isfile(cert_file_path): - raise ValueError('Certificate file "%s" doesn\'t exist' % (cert_file_path)) - - if use_ssl and not os.path.isfile(key_file_path): - raise ValueError('Private key file "%s" doesn\'t exist' % (key_file_path)) - - LOG.info( - "(PID=%s) ST2 Stream API is serving on %s://%s:%s.", - os.getpid(), - "https" if use_ssl else "http", - host, - port, + api.run( + service_name="stream", + app_config=config, + cfg=cfg.CONF.stream, + app=app, + log=LOG, + use_custom_pool=True, + log_output=True, + common_setup_kwargs=common_setup, ) - - max_pool_size = eventlet.wsgi.DEFAULT_MAX_SIMULTANEOUS_REQUESTS - worker_pool = eventlet.GreenPool(max_pool_size) - sock = eventlet.listen((host, port)) - - if use_ssl: - sock = eventlet.wrap_ssl( - sock, certfile=cert_file_path, keyfile=key_file_path, server_side=True - ) - - def queue_shutdown(signal_number, stack_frame): - deregister_service(STREAM) - eventlet.spawn_n( - shutdown_server_kill_pending_requests, - sock=sock, - worker_pool=worker_pool, - wait_time=WSGI_SERVER_REQUEST_SHUTDOWN_TIME, - ) - - # We register a custom SIGINT handler which allows us to kill long running active requests. - # Note: Eventually we will support draining (waiting for short-running requests), but we - # will still want to kill long running stream requests. - register_stream_signal_handlers(handler_func=queue_shutdown) - - wsgi.server(sock, app.setup_app(), custom_pool=worker_pool) - return 0 - - -def _teardown(): - common_teardown() - - -def main(): - try: - _setup() - return _run_server() - except SystemExit as exit_code: - deregister_service(STREAM) - sys.exit(exit_code) - except KeyboardInterrupt: - deregister_service(STREAM) - listener = get_listener_if_set(name="stream") - - if listener: - listener.shutdown() - except Exception: - LOG.exception("(PID=%s) ST2 Stream API quit due to exception.", os.getpid()) - return 1 - finally: - _teardown() diff --git a/st2stream/st2stream/config.py b/st2stream/st2stream/config.py index 6d76706445..f280104587 100644 --- a/st2stream/st2stream/config.py +++ b/st2stream/st2stream/config.py @@ -24,61 +24,34 @@ from oslo_config import cfg import st2common.config as common_config -from st2common.constants.system import VERSION_STRING -from st2common.constants.system import DEFAULT_CONFIG_FILE_PATH +from st2common.openapi import config CONF = cfg.CONF BASE_DIR = os.path.dirname(os.path.abspath(__file__)) def parse_args(args=None): - cfg.CONF( - args=args, - version=VERSION_STRING, - default_config_files=[DEFAULT_CONFIG_FILE_PATH], - ) + config.parse_args(args=args) def register_opts(ignore_errors=False): - _register_common_opts(ignore_errors=ignore_errors) - _register_app_opts(ignore_errors=ignore_errors) - - -def _register_common_opts(ignore_errors=False): - common_config.register_opts(ignore_errors=ignore_errors) + config.register_opts(_register_app_opts, ignore_errors=ignore_errors) def get_logging_config_path(): - return cfg.CONF.stream.logging + return config.get_logging_config_path(cfg.CONF.stream) def _register_app_opts(ignore_errors=False): # Note "allow_origin", "mask_secrets", "heartbeat" options are registered as part of st2common # config since they are also used outside st2stream - api_opts = [ + stream_opts = config.get_base_opts("stream") + [ cfg.StrOpt( "host", default="127.0.0.1", help="StackStorm stream API server host" ), cfg.IntOpt("port", default=9102, help="StackStorm API stream, server port"), - cfg.BoolOpt("debug", default=False, help="Specify to enable debug mode."), - cfg.StrOpt( - "logging", - default="/etc/st2/logging.stream.conf", - help="location of the logging.conf file", - ), - cfg.BoolOpt("use_ssl", default=False, help="Specify to enable SSL / TLS mode"), - cfg.StrOpt( - "cert", - default="/etc/apache2/ssl/mycert.crt", - help='Path to the SSL certificate file. Only used when "use_ssl" is specified.', - ), - cfg.StrOpt( - "key", - default="/etc/apache2/ssl/mycert.key", - help='Path to the SSL private key file. Only used when "use_ssl" is specified.', - ), ] common_config.do_register_opts( - api_opts, group="stream", ignore_errors=ignore_errors + stream_opts, group="stream", ignore_errors=ignore_errors )