From d54c102cee7a61dd3eccd62d60af218aa97a85fc Mon Sep 17 00:00:00 2001 From: Ivan Devat Date: Thu, 9 Jan 2020 15:53:37 +0100 Subject: [PATCH 6/7] squash bz1783106 fix-sinatra-wrapper-performance-issue create prototype of tornado - thin communication put socket path to settings don't mix logs from threads in ruby daemon run ruby daemon via systemd units support trailing slash by gui urls e.g. /manage/ decode body from ruby response for log configure ruby wrapper by socket path remove env values not used for ruby calls any more deal with ruby daemon communication issues fix tests cleanup ruby server code deal with errors from ruby daemon in python daemon remove unused cmdline wrapper add ruby daemon infrastructure to spec etc. stop logging to stderr from ruby daemon fix spec file * add missing cp for new rubygems * make sure to start the new ruby daemon on package upgrade * tests: give the new daemon enough time to start --- .gitlab-ci.yml | 7 +- Makefile | 6 + pcs.spec.in | 30 ++++- pcs/daemon/app/sinatra_ui.py | 2 +- pcs/daemon/env.py | 36 ------ pcs/daemon/ruby_pcsd.py | 136 +++++++++++----------- pcs/daemon/run.py | 8 +- pcs/settings_default.py | 1 + pcs_test/tier0/daemon/app/fixtures_app.py | 3 +- pcs_test/tier0/daemon/test_env.py | 66 +---------- pcs_test/tier0/daemon/test_ruby_pcsd.py | 13 +-- pcsd/Gemfile | 1 + pcsd/Gemfile.lock | 7 ++ pcsd/Makefile | 3 + pcsd/bootstrap.rb | 20 +++- pcsd/cfgsync.rb | 6 +- pcsd/pcs.rb | 9 +- pcsd/pcsd-cli.rb | 3 +- pcsd/pcsd-ruby.service | 20 ++++ pcsd/pcsd.conf | 4 + pcsd/pcsd.rb | 31 ++--- pcsd/pcsd.service | 2 + pcsd/pcsd.service-runner | 24 ++++ pcsd/remote.rb | 6 +- pcsd/rserver.rb | 98 ++++++++++++++++ pcsd/settings.rb | 1 + pcsd/settings.rb.debian | 1 + pcsd/sinatra_cmdline_wrapper.rb | 63 ---------- 28 files changed, 330 insertions(+), 277 deletions(-) create mode 100644 pcsd/pcsd-ruby.service create mode 100644 pcsd/pcsd.service-runner create mode 100644 pcsd/rserver.rb delete mode 100644 pcsd/sinatra_cmdline_wrapper.rb diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 23ab56a9..92b32033 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -116,8 +116,11 @@ python_smoke_tests: procps-ng rpms/pcs-ci-*.rpm " - - /usr/sbin/pcsd & # start pcsd - - sleep 10 # wait for pcsd to start up properly + - export GEM_HOME=/usr/lib/pcsd/vendor/bundle/ruby + - /usr/lib/pcsd/pcsd & # start pcsd (ruby - thin) + - sleep 10 # wait for pcsd (ruby - thin) to start up properly + - /usr/sbin/pcsd & # start pcsd (python - tornado) + - sleep 10 # wait for pcsd (python - tornado) to start up properly - pcs_test/smoke.sh artifacts: paths: diff --git a/Makefile b/Makefile index f2b0d9b9..b9f64acd 100644 --- a/Makefile +++ b/Makefile @@ -267,7 +267,12 @@ ifeq ($(IS_DEBIAN)$(IS_SYSTEMCTL),truefalse) else install -d ${DEST_SYSTEMD_SYSTEM} install -m 644 ${SYSTEMD_SERVICE_FILE} ${DEST_SYSTEMD_SYSTEM}/pcsd.service + install -m 644 pcsd/pcsd-ruby.service ${DEST_SYSTEMD_SYSTEM}/pcsd-ruby.service endif + # ${DEST_LIB}/pcsd/pcsd holds the selinux context + install -m 755 pcsd/pcsd.service-runner ${DEST_LIB}/pcsd/pcsd + rm ${DEST_LIB}/pcsd/pcsd.service-runner + install -m 700 -d ${DESTDIR}/var/lib/pcsd install -m 644 -D pcsd/pcsd.logrotate ${DESTDIR}/etc/logrotate.d/pcsd install -m644 -D pcsd/pcsd.8 ${DEST_MAN}/pcsd.8 @@ -293,6 +298,7 @@ ifeq ($(IS_DEBIAN)$(IS_SYSTEMCTL),truefalse) rm -f ${DEST_INIT}/pcsd else rm -f ${DEST_SYSTEMD_SYSTEM}/pcsd.service + rm -f ${DEST_SYSTEMD_SYSTEM}/pcsd-ruby.service rm -f ${DEST_SYSTEMD_SYSTEM}/pcs_snmp_agent.service endif rm -f ${DESTDIR}/etc/pam.d/pcsd diff --git a/pcs.spec.in b/pcs.spec.in index 5195dc51..32fbf614 100644 --- a/pcs.spec.in +++ b/pcs.spec.in @@ -28,7 +28,9 @@ Summary: Pacemaker Configuration System %global pyagentx_version 0.4.pcs.2 %global tornado_version 6.0.3 %global version_rubygem_backports 3.11.4 +%global version_rubygem_daemons 1.3.1 %global version_rubygem_ethon 0.11.0 +%global version_rubygem_eventmachine 1.2.7 %global version_rubygem_ffi 1.9.25 %global version_rubygem_json 2.1.0 %global version_rubygem_mustermann 1.0.3 @@ -37,6 +39,7 @@ Summary: Pacemaker Configuration System %global version_rubygem_rack_protection 2.0.4 %global version_rubygem_rack_test 1.0.0 %global version_rubygem_sinatra 2.0.4 +%global version_rubygem_thin 1.7.2 %global version_rubygem_tilt 2.0.9 # We do not use _libdir macro because upstream is not prepared for it. @@ -83,6 +86,9 @@ Source89: https://rubygems.org/downloads/rack-protection-%{version_rubygem_rack_ Source90: https://rubygems.org/downloads/rack-test-%{version_rubygem_rack_test}.gem Source91: https://rubygems.org/downloads/sinatra-%{version_rubygem_sinatra}.gem Source92: https://rubygems.org/downloads/tilt-%{version_rubygem_tilt}.gem +Source93: https://rubygems.org/downloads/eventmachine-%{version_rubygem_eventmachine}.gem +Source94: https://rubygems.org/downloads/daemons-%{version_rubygem_daemons}.gem +Source95: https://rubygems.org/downloads/thin-%{version_rubygem_thin}.gem Source100: https://github.com/idevat/pcs-web-ui/archive/%{ui_commit}/%{ui_src_name}.tar.gz Source101: https://github.com/idevat/pcs-web-ui/releases/download/%{ui_commit}/pcs-web-ui-node-modules-%{ui_commit}.tar.xz @@ -164,7 +170,9 @@ Recommends: overpass-fonts Provides: bundled(tornado) = %{tornado_version} Provides: bundled(backports) = %{version_rubygem_backports} +Provides: bundled(daemons) = %{version_rubygem_daemons} Provides: bundled(ethon) = %{version_rubygem_ethon} +Provides: bundled(eventmachine) = %{version_rubygem_eventmachine} Provides: bundled(ffi) = %{version_rubygem_ffi} Provides: bundled(json) = %{version_rubygem_json} Provides: bundled(mustermann) = %{version_rubygem_mustermann} @@ -173,6 +181,7 @@ Provides: bundled(rack) = %{version_rubygem_rack} Provides: bundled(rack) = %{version_rubygem_rack_protection} Provides: bundled(rack) = %{version_rubygem_rack_test} Provides: bundled(sinatra) = %{version_rubygem_sinatra} +Provides: bundled(thin) = %{version_rubygem_thin} Provides: bundled(tilt) = %{version_rubygem_tilt} %description @@ -228,6 +237,9 @@ cp -f %SOURCE89 pcsd/vendor/cache cp -f %SOURCE90 pcsd/vendor/cache cp -f %SOURCE91 pcsd/vendor/cache cp -f %SOURCE92 pcsd/vendor/cache +cp -f %SOURCE93 pcsd/vendor/cache +cp -f %SOURCE94 pcsd/vendor/cache +cp -f %SOURCE95 pcsd/vendor/cache # 3) dir for python bundles @@ -262,15 +274,18 @@ gem install \ --force --verbose -l --no-user-install %{gem_install_params} \ -i %{rubygem_bundle_dir} \ %{rubygem_cache_dir}/backports-%{version_rubygem_backports}.gem \ + %{rubygem_cache_dir}/daemons-%{version_rubygem_daemons}.gem \ %{rubygem_cache_dir}/ethon-%{version_rubygem_ethon}.gem \ + %{rubygem_cache_dir}/eventmachine-%{version_rubygem_eventmachine}.gem \ %{rubygem_cache_dir}/ffi-%{version_rubygem_ffi}.gem \ %{rubygem_cache_dir}/json-%{version_rubygem_json}.gem \ %{rubygem_cache_dir}/mustermann-%{version_rubygem_mustermann}.gem \ %{rubygem_cache_dir}/open4-%{version_rubygem_open4}.gem \ - %{rubygem_cache_dir}/rack-%{version_rubygem_rack}.gem \ %{rubygem_cache_dir}/rack-protection-%{version_rubygem_rack_protection}.gem \ %{rubygem_cache_dir}/rack-test-%{version_rubygem_rack_test}.gem \ + %{rubygem_cache_dir}/rack-%{version_rubygem_rack}.gem \ %{rubygem_cache_dir}/sinatra-%{version_rubygem_sinatra}.gem \ + %{rubygem_cache_dir}/thin-%{version_rubygem_thin}.gem \ %{rubygem_cache_dir}/tilt-%{version_rubygem_tilt}.gem \ -- '--with-ldflags=-Wl,-z,relro -Wl,-z,ibt -Wl,-z,now -Wl,--gc-sections' \ '--with-cflags=-O2 -ffunction-sections' @@ -324,20 +339,31 @@ rm -r -v ${pcsd_dir}/test # remove javascript testing files rm -r -v ${pcsd_dir}/public/js/dev +%posttrans +# Make sure the new version of the daemon is runnning. +# Also, make sure to start pcsd-ruby if it hasn't been started or even +# installed before. This is done by restarting pcsd.service. +%{_bindir}/systemctl daemon-reload +%{_bindir}/systemctl try-restart pcsd.service + + %post %systemd_post pcsd.service +%systemd_post pcsd-ruby.service %post -n %{pcs_snmp_pkg_name} %systemd_post pcs_snmp_agent.service %preun %systemd_preun pcsd.service +%systemd_preun pcsd-ruby.service %preun -n %{pcs_snmp_pkg_name} %systemd_preun pcs_snmp_agent.service %postun %systemd_postun_with_restart pcsd.service +%systemd_postun_with_restart pcsd-ruby.service %postun -n %{pcs_snmp_pkg_name} %systemd_postun_with_restart pcs_snmp_agent.service @@ -357,6 +383,7 @@ rm -r -v ${pcsd_dir}/public/js/dev %{pcs_libdir}/pcsd/.bundle/config %{pcs_libdir}/pcs/bundled/packages/tornado* %{_unitdir}/pcsd.service +%{_unitdir}/pcsd-ruby.service %{_datadir}/bash-completion/completions/pcs %{_sharedstatedir}/pcsd %{_sysconfdir}/pam.d/pcsd @@ -374,6 +401,7 @@ rm -r -v ${pcsd_dir}/public/js/dev %{_mandir}/man8/pcsd.* %exclude %{pcs_libdir}/pcsd/*.debian %exclude %{pcs_libdir}/pcsd/pcsd.service +%exclude %{pcs_libdir}/pcsd/pcsd-ruby.service %exclude %{pcs_libdir}/pcsd/pcsd.conf %exclude %{pcs_libdir}/pcsd/pcsd.8 %exclude %{pcs_libdir}/pcsd/public/js/dev/* diff --git a/pcs/daemon/app/sinatra_ui.py b/pcs/daemon/app/sinatra_ui.py index 1348134d..5315a48f 100644 --- a/pcs/daemon/app/sinatra_ui.py +++ b/pcs/daemon/app/sinatra_ui.py @@ -153,7 +153,7 @@ def get_routes( # The protection by session was moved from ruby code to python code # (tornado). ( - r"/($|manage$|permissions$|managec/.+/main)", + r"/($|manage/?$|permissions/?$|managec/.+/main)", SinatraGuiProtected, {**sessions, **ruby_wrapper} ), diff --git a/pcs/daemon/env.py b/pcs/daemon/env.py index 54a9819f..26cdcf9b 100644 --- a/pcs/daemon/env.py +++ b/pcs/daemon/env.py @@ -15,7 +15,6 @@ from pcs.lib.validate import is_port_number # Relative location instead of system location is used for development purposes. PCSD_LOCAL_DIR = realpath(dirname(abspath(__file__)) + "/../../pcsd") -PCSD_CMDLINE_ENTRY_RB_SCRIPT = "sinatra_cmdline_wrapper.rb" PCSD_STATIC_FILES_DIR_NAME = "public" PCSD_PORT = "PCSD_PORT" @@ -26,12 +25,8 @@ NOTIFY_SOCKET = "NOTIFY_SOCKET" PCSD_DEBUG = "PCSD_DEBUG" PCSD_DISABLE_GUI = "PCSD_DISABLE_GUI" PCSD_SESSION_LIFETIME = "PCSD_SESSION_LIFETIME" -GEM_HOME = "GEM_HOME" PCSD_DEV = "PCSD_DEV" -PCSD_CMDLINE_ENTRY = "PCSD_CMDLINE_ENTRY" PCSD_STATIC_FILES_DIR = "PCSD_STATIC_FILES_DIR" -HTTPS_PROXY = "HTTPS_PROXY" -NO_PROXY = "NO_PROXY" Env = namedtuple("Env", [ PCSD_PORT, @@ -42,11 +37,7 @@ Env = namedtuple("Env", [ PCSD_DEBUG, PCSD_DISABLE_GUI, PCSD_SESSION_LIFETIME, - GEM_HOME, - PCSD_CMDLINE_ENTRY, PCSD_STATIC_FILES_DIR, - HTTPS_PROXY, - NO_PROXY, PCSD_DEV, "has_errors", ]) @@ -62,11 +53,7 @@ def prepare_env(environ, logger=None): loader.pcsd_debug(), loader.pcsd_disable_gui(), loader.session_lifetime(), - loader.gem_home(), - loader.pcsd_cmdline_entry(), loader.pcsd_static_files_dir(), - loader.https_proxy(), - loader.no_proxy(), loader.pcsd_dev(), loader.has_errors(), ) @@ -173,20 +160,6 @@ class EnvLoader: def pcsd_debug(self): return self.__has_true_in_environ(PCSD_DEBUG) - def gem_home(self): - if settings.pcsd_gem_path is None: - return None - return self.__in_pcsd_path( - settings.pcsd_gem_path, - "Ruby gem location" - ) - - def pcsd_cmdline_entry(self): - return self.__in_pcsd_path( - PCSD_CMDLINE_ENTRY_RB_SCRIPT, - "Ruby handlers entrypoint" - ) - def pcsd_static_files_dir(self): return self.__in_pcsd_path( PCSD_STATIC_FILES_DIR_NAME, @@ -194,15 +167,6 @@ class EnvLoader: existence_required=not self.pcsd_disable_gui() ) - def https_proxy(self): - for key in ["https_proxy", HTTPS_PROXY, "all_proxy", "ALL_PROXY"]: - if key in self.environ: - return self.environ[key] - return None - - def no_proxy(self): - return self.environ.get("no_proxy", self.environ.get(NO_PROXY, None)) - @lru_cache() def pcsd_dev(self): return self.__has_true_in_environ(PCSD_DEV) diff --git a/pcs/daemon/ruby_pcsd.py b/pcs/daemon/ruby_pcsd.py index 5bdaffeb..e612f8da 100644 --- a/pcs/daemon/ruby_pcsd.py +++ b/pcs/daemon/ruby_pcsd.py @@ -1,14 +1,16 @@ import json import logging -import os.path -from base64 import b64decode +from base64 import b64decode, b64encode, binascii from collections import namedtuple from time import time as now -from tornado.gen import multi, convert_yielded +import pycurl +from tornado.gen import convert_yielded from tornado.web import HTTPError from tornado.httputil import split_host_and_port, HTTPServerRequest -from tornado.process import Subprocess +from tornado.httpclient import AsyncHTTPClient +from tornado.curl_httpclient import CurlError + from pcs.daemon import log @@ -33,7 +35,7 @@ class SinatraResult(namedtuple("SinatraResult", "headers, status, body")): return cls( response["headers"], response["status"], - b64decode(response["body"]) + response["body"] ) def log_group_id_generator(): @@ -58,24 +60,12 @@ def process_response_logs(rb_log_list): group_id=group_id ) -def log_communication(request_json, stdout, stderr): - log.pcsd.debug("Request for ruby pcsd wrapper: '%s'", request_json) - log.pcsd.debug("Response stdout from ruby pcsd wrapper: '%s'", stdout) - log.pcsd.debug("Response stderr from ruby pcsd wrapper: '%s'", stderr) - class Wrapper: - # pylint: disable=too-many-instance-attributes - def __init__( - self, pcsd_cmdline_entry, gem_home=None, debug=False, - ruby_executable="ruby", https_proxy=None, no_proxy=None - ): - self.__gem_home = gem_home - self.__pcsd_cmdline_entry = pcsd_cmdline_entry - self.__pcsd_dir = os.path.dirname(pcsd_cmdline_entry) - self.__ruby_executable = ruby_executable + def __init__(self, pcsd_ruby_socket, debug=False): self.__debug = debug - self.__https_proxy = https_proxy - self.__no_proxy = no_proxy + AsyncHTTPClient.configure('tornado.curl_httpclient.CurlAsyncHTTPClient') + self.__client = AsyncHTTPClient() + self.__pcsd_ruby_socket = pcsd_ruby_socket @staticmethod def get_sinatra_request(request: HTTPServerRequest): @@ -102,55 +92,76 @@ class Wrapper: "rack.input": request.body.decode("utf8"), }} + def prepare_curl_callback(self, curl): + curl.setopt(pycurl.UNIX_SOCKET_PATH, self.__pcsd_ruby_socket) + curl.setopt(pycurl.TIMEOUT, 70) + async def send_to_ruby(self, request_json): - env = { - "PCSD_DEBUG": "true" if self.__debug else "false" - } - if self.__gem_home is not None: - env["GEM_HOME"] = self.__gem_home - - if self.__no_proxy is not None: - env["NO_PROXY"] = self.__no_proxy - if self.__https_proxy is not None: - env["HTTPS_PROXY"] = self.__https_proxy - - pcsd_ruby = Subprocess( - [ - self.__ruby_executable, "-I", - self.__pcsd_dir, - self.__pcsd_cmdline_entry - ], - stdin=Subprocess.STREAM, - stdout=Subprocess.STREAM, - stderr=Subprocess.STREAM, - env=env - ) - await pcsd_ruby.stdin.write(str.encode(request_json)) - pcsd_ruby.stdin.close() - return await multi([ - pcsd_ruby.stdout.read_until_close(), - pcsd_ruby.stderr.read_until_close(), - pcsd_ruby.wait_for_exit(raise_error=False), - ]) + # We do not need location for communication with ruby itself since we + # communicate via unix socket. But it is required by AsyncHTTPClient so + # "localhost" is used. + tornado_request = b64encode(request_json.encode()).decode() + return (await self.__client.fetch( + "localhost", + method="POST", + body=f"TORNADO_REQUEST={tornado_request}", + prepare_curl_callback=self.prepare_curl_callback, + )).body async def run_ruby(self, request_type, request=None): + """ + request_type: SINATRA_GUI|SINATRA_REMOTE|SYNC_CONFIGS + request: result of get_sinatra_request|None + i.e. it has structure returned by get_sinatra_request if the request + is not None - so we can get SERVER_NAME and SERVER_PORT + """ request = request or {} request.update({"type": request_type}) request_json = json.dumps(request) - stdout, stderr, dummy_status = await self.send_to_ruby(request_json) + + if self.__debug: + log.pcsd.debug("Ruby daemon request: '%s'", request_json) try: - response = json.loads(stdout) - except json.JSONDecodeError as e: - self.__log_bad_response( - f"Cannot decode json from ruby pcsd wrapper: '{e}'", - request_json, stdout, stderr + ruby_response = await self.send_to_ruby(request_json) + except CurlError as e: + log.pcsd.error( + "Cannot connect to ruby daemon (message: '%s'). Is it running?", + e ) raise HTTPError(500) - else: - if self.__debug: - log_communication(request_json, stdout, stderr) - process_response_logs(response["logs"]) + + try: + response = json.loads(ruby_response) + if "error" in response: + log.pcsd.error( + "Ruby daemon response contains an error: '%s'", + json.dumps(response) + ) + raise HTTPError(500) + + logs = response.pop("logs", []) + if "body" in response: + body = b64decode(response.pop("body")) + if self.__debug: + log.pcsd.debug( + "Ruby daemon response (without logs and body): '%s'", + json.dumps(response) + ) + log.pcsd.debug("Ruby daemon response body: '%s'", body) + response["body"] = body + + elif self.__debug: + log.pcsd.debug( + "Ruby daemon response (without logs): '%s'", + json.dumps(response) + ) + process_response_logs(logs) return response + except (json.JSONDecodeError, binascii.Error) as e: + if self.__debug: + log.pcsd.debug("Ruby daemon response: '%s'", ruby_response) + log.pcsd.error("Cannot decode json from ruby pcsd wrapper: '%s'", e) + raise HTTPError(500) async def request_gui( self, request: HTTPServerRequest, user, groups, is_authenticated @@ -186,8 +197,3 @@ class Wrapper: except HTTPError: log.pcsd.error("Config synchronization failed") return int(now()) + DEFAULT_SYNC_CONFIG_DELAY - - def __log_bad_response(self, error_message, request_json, stdout, stderr): - log.pcsd.error(error_message) - if self.__debug: - log_communication(request_json, stdout, stderr) diff --git a/pcs/daemon/run.py b/pcs/daemon/run.py index bafd9f3c..874ee2f1 100644 --- a/pcs/daemon/run.py +++ b/pcs/daemon/run.py @@ -65,6 +65,8 @@ def configure_app( # old web ui by default [(r"/", RedirectHandler, dict(url="/manage"))] + + [(r"/ui", RedirectHandler, dict(url="/ui/"))] + + ui.get_routes( url_prefix="/ui/", app_dir=os.path.join(public_dir, "ui"), @@ -101,12 +103,8 @@ def main(): sync_config_lock = Lock() ruby_pcsd_wrapper = ruby_pcsd.Wrapper( - pcsd_cmdline_entry=env.PCSD_CMDLINE_ENTRY, - gem_home=env.GEM_HOME, + settings.pcsd_ruby_socket, debug=env.PCSD_DEBUG, - ruby_executable=settings.ruby_executable, - https_proxy=env.HTTPS_PROXY, - no_proxy=env.NO_PROXY, ) make_app = configure_app( session.Storage(env.PCSD_SESSION_LIFETIME), diff --git a/pcs/settings_default.py b/pcs/settings_default.py index 6d8f33ac..f761ce43 100644 --- a/pcs/settings_default.py +++ b/pcs/settings_default.py @@ -43,6 +43,7 @@ cibadmin = os.path.join(pacemaker_binaries, "cibadmin") crm_mon_schema = '/usr/share/pacemaker/crm_mon.rng' agent_metadata_schema = "/usr/share/resource-agents/ra-api-1.dtd" pcsd_var_location = "/var/lib/pcsd/" +pcsd_ruby_socket = "/run/pcsd-ruby.socket" pcsd_cert_location = os.path.join(pcsd_var_location, "pcsd.crt") pcsd_key_location = os.path.join(pcsd_var_location, "pcsd.key") pcsd_known_hosts_location = os.path.join(pcsd_var_location, "known-hosts") diff --git a/pcs_test/tier0/daemon/app/fixtures_app.py b/pcs_test/tier0/daemon/app/fixtures_app.py index 2e4feba4..8d5b8f4c 100644 --- a/pcs_test/tier0/daemon/app/fixtures_app.py +++ b/pcs_test/tier0/daemon/app/fixtures_app.py @@ -1,4 +1,3 @@ -from base64 import b64encode from pprint import pformat from urllib.parse import urlencode @@ -30,7 +29,7 @@ class RubyPcsdWrapper(ruby_pcsd.Wrapper): return { "headers": self.headers, "status": self.status_code, - "body": b64encode(self.body), + "body": self.body, } class AppTest(AsyncHTTPTestCase): diff --git a/pcs_test/tier0/daemon/test_env.py b/pcs_test/tier0/daemon/test_env.py index 9e78eafd..e2f7f5b1 100644 --- a/pcs_test/tier0/daemon/test_env.py +++ b/pcs_test/tier0/daemon/test_env.py @@ -41,11 +41,7 @@ class Prepare(TestCase, create_setup_patch_mixin(env)): env.PCSD_DEBUG: False, env.PCSD_DISABLE_GUI: False, env.PCSD_SESSION_LIFETIME: settings.gui_session_lifetime_seconds, - env.GEM_HOME: pcsd_dir(settings.pcsd_gem_path), - env.PCSD_CMDLINE_ENTRY: pcsd_dir(env.PCSD_CMDLINE_ENTRY_RB_SCRIPT), env.PCSD_STATIC_FILES_DIR: pcsd_dir(env.PCSD_STATIC_FILES_DIR_NAME), - env.HTTPS_PROXY: None, - env.NO_PROXY: None, env.PCSD_DEV: False, "has_errors": False, } @@ -77,8 +73,6 @@ class Prepare(TestCase, create_setup_patch_mixin(env)): env.PCSD_DISABLE_GUI: "true", env.PCSD_SESSION_LIFETIME: str(session_lifetime), env.PCSD_DEV: "true", - env.HTTPS_PROXY: "proxy1", - env.NO_PROXY: "host", env.PCSD_DEV: "true", } self.assert_environ_produces_modified_pcsd_env( @@ -92,15 +86,9 @@ class Prepare(TestCase, create_setup_patch_mixin(env)): env.PCSD_DEBUG: True, env.PCSD_DISABLE_GUI: True, env.PCSD_SESSION_LIFETIME: session_lifetime, - env.GEM_HOME: pcsd_dir(settings.pcsd_gem_path), - env.PCSD_CMDLINE_ENTRY: pcsd_dir( - env.PCSD_CMDLINE_ENTRY_RB_SCRIPT - ), env.PCSD_STATIC_FILES_DIR: pcsd_dir( env.PCSD_STATIC_FILES_DIR_NAME ), - env.HTTPS_PROXY: environ[env.HTTPS_PROXY], - env.NO_PROXY: environ[env.NO_PROXY], env.PCSD_DEV: True, }, ) @@ -167,13 +155,6 @@ class Prepare(TestCase, create_setup_patch_mixin(env)): self.assert_environ_produces_modified_pcsd_env( specific_env_values={"has_errors": True}, errors=[ - f"Ruby gem location '{pcsd_dir(settings.pcsd_gem_path)}'" - " does not exist" - , - "Ruby handlers entrypoint" - f" '{pcsd_dir(env.PCSD_CMDLINE_ENTRY_RB_SCRIPT)}'" - " does not exist" - , "Directory with web UI assets" f" '{pcsd_dir(env.PCSD_STATIC_FILES_DIR_NAME)}'" " does not exist" @@ -181,54 +162,13 @@ class Prepare(TestCase, create_setup_patch_mixin(env)): ] ) - def test_errors_on_missing_paths_disabled_gui(self): + def test_no_errors_on_missing_paths_disabled_gui(self): self.path_exists.return_value = False - pcsd_dir = partial(join_path, settings.pcsd_exec_location) self.assert_environ_produces_modified_pcsd_env( environ={env.PCSD_DISABLE_GUI: "true"}, specific_env_values={ env.PCSD_DISABLE_GUI: True, - "has_errors": True, + "has_errors": False, }, - errors=[ - f"Ruby gem location '{pcsd_dir(settings.pcsd_gem_path)}'" - " does not exist" - , - "Ruby handlers entrypoint" - f" '{pcsd_dir(env.PCSD_CMDLINE_ENTRY_RB_SCRIPT)}'" - " does not exist" - , - ] + errors=[] ) - - def test_lower_case_no_proxy_has_precedence(self): - def it_selects(proxy_value): - self.assert_environ_produces_modified_pcsd_env( - environ=environ, - specific_env_values={env.NO_PROXY: proxy_value} - ) - - environ = {"NO_PROXY": "no_proxy_1"} - it_selects("no_proxy_1") - - environ["no_proxy"] = "no_proxy_2" - it_selects("no_proxy_2") - - def test_http_proxy_is_setup_by_precedence(self): - def it_selects(proxy_value): - self.assert_environ_produces_modified_pcsd_env( - environ=environ, - specific_env_values={env.HTTPS_PROXY: proxy_value} - ) - - environ = {"ALL_PROXY": "all_proxy_1"} - it_selects("all_proxy_1") - - environ["all_proxy"] = "all_proxy_2" - it_selects("all_proxy_2") - - environ["HTTPS_PROXY"] = "https_proxy_1" - it_selects("https_proxy_1") - - environ["https_proxy"] = "https_proxy_2" - it_selects("https_proxy_2") diff --git a/pcs_test/tier0/daemon/test_ruby_pcsd.py b/pcs_test/tier0/daemon/test_ruby_pcsd.py index d7fd71a0..28f14c87 100644 --- a/pcs_test/tier0/daemon/test_ruby_pcsd.py +++ b/pcs_test/tier0/daemon/test_ruby_pcsd.py @@ -16,10 +16,7 @@ from pcs.daemon import ruby_pcsd logging.getLogger("pcs.daemon").setLevel(logging.CRITICAL) def create_wrapper(): - return ruby_pcsd.Wrapper( - rc("/path/to/gem_home"), - rc("/path/to/pcsd/cmdline/entry"), - ) + return ruby_pcsd.Wrapper(rc("/path/to/ruby_socket")) def create_http_request(): return HTTPServerRequest( @@ -63,9 +60,7 @@ patch_ruby_pcsd = create_patcher(ruby_pcsd) class RunRuby(AsyncTestCase): def setUp(self): - self.stdout = "" - self.stderr = "" - self.exit_status = 0 + self.ruby_response = "" self.request = self.create_request() self.wrapper = create_wrapper() patcher = mock.patch.object( @@ -79,14 +74,14 @@ class RunRuby(AsyncTestCase): async def send_to_ruby(self, request_json): self.assertEqual(json.loads(request_json), self.request) - return self.stdout, self.stderr, self.exit_status + return self.ruby_response @staticmethod def create_request(_type=ruby_pcsd.SYNC_CONFIGS): return {"type": _type} def set_run_result(self, run_result): - self.stdout = json.dumps({**run_result, "logs": []}) + self.ruby_response = json.dumps({**run_result, "logs": []}) def assert_sinatra_result(self, result, headers, status, body): self.assertEqual(result.headers, headers) diff --git a/pcsd/Gemfile b/pcsd/Gemfile index 27898f71..716991a6 100644 --- a/pcsd/Gemfile +++ b/pcsd/Gemfile @@ -10,3 +10,4 @@ gem 'json' gem 'open4' gem 'ffi' gem 'ethon' +gem 'thin' diff --git a/pcsd/Gemfile.lock b/pcsd/Gemfile.lock index 6f833888..c8b02a94 100644 --- a/pcsd/Gemfile.lock +++ b/pcsd/Gemfile.lock @@ -2,8 +2,10 @@ GEM remote: https://rubygems.org/ specs: backports (3.11.4) + daemons (1.3.1) ethon (0.11.0) ffi (>= 1.3.0) + eventmachine (1.2.7) ffi (1.9.25) json (2.1.0) mustermann (1.0.3) @@ -18,6 +20,10 @@ GEM rack (~> 2.0) rack-protection (= 2.0.4) tilt (~> 2.0) + thin (1.7.2) + daemons (~> 1.0, >= 1.0.9) + eventmachine (~> 1.0, >= 1.0.4) + rack (>= 1, < 3) tilt (2.0.9) PLATFORMS @@ -33,6 +39,7 @@ DEPENDENCIES rack-protection rack-test sinatra + thin tilt BUNDLED WITH diff --git a/pcsd/Makefile b/pcsd/Makefile index 5fe3f3f3..5dde50e3 100644 --- a/pcsd/Makefile +++ b/pcsd/Makefile @@ -26,6 +26,9 @@ build_gems_without_bundler: vendor/cache/rack-test-1.1.0.gem \ vendor/cache/sinatra-2.0.4.gem \ vendor/cache/tilt-2.0.9.gem \ + vendor/cache/eventmachine-1.2.7.gem \ + vendor/cache/daemons-1.3.1.gem \ + vendor/cache/thin-1.7.2.gem \ -- '--with-ldflags="-Wl,-z,now -Wl,-z,relro"' get_gems: diff --git a/pcsd/bootstrap.rb b/pcsd/bootstrap.rb index ec6b535c..fc9d9b8c 100644 --- a/pcsd/bootstrap.rb +++ b/pcsd/bootstrap.rb @@ -51,8 +51,23 @@ if not defined? $cur_node_name $cur_node_name = `/bin/hostname`.chomp end -def configure_logger(log_device) - logger = Logger.new(log_device) +def configure_logger() + logger = Logger.new(StringIO.new()) + logger.formatter = proc {|severity, datetime, progname, msg| + if Thread.current.key?(:pcsd_logger_container) + Thread.current[:pcsd_logger_container] << { + :level => severity, + :timestamp_usec => (datetime.to_f * 1000000).to_i, + :message => msg, + } + else + STDERR.puts("#{datetime} #{progname} #{severity} #{msg}") + end + } + return logger +end + +def early_log(logger) if ENV['PCSD_DEBUG'] and ENV['PCSD_DEBUG'].downcase == "true" then logger.level = Logger::DEBUG logger.info "PCSD Debugging enabled" @@ -65,7 +80,6 @@ def configure_logger(log_device) else logger.debug "Detected systemd is not in use" end - return logger end def get_capabilities(logger) diff --git a/pcsd/cfgsync.rb b/pcsd/cfgsync.rb index 16bcfbdc..1cab512e 100644 --- a/pcsd/cfgsync.rb +++ b/pcsd/cfgsync.rb @@ -468,7 +468,8 @@ module Cfgsync node_response = {} threads = [] @nodes.each { |node| - threads << Thread.new { + threads << Thread.new(Thread.current[:pcsd_logger_container]) { |logger| + Thread.current[:pcsd_logger_container] = logger code, out = send_request_with_token( @auth_user, node, 'set_configs', true, data, true, nil, 30, @additional_known_hosts @@ -616,7 +617,8 @@ module Cfgsync node_configs = {} connected_to = {} nodes.each { |node| - threads << Thread.new { + threads << Thread.new(Thread.current[:pcsd_logger_container]) { |logger| + Thread.current[:pcsd_logger_container] = logger code, out = send_request_with_token( @auth_user, node, 'get_configs', false, data, true, nil, nil, @additional_known_hosts diff --git a/pcsd/pcs.rb b/pcsd/pcs.rb index 7b991ac0..9a0efb46 100644 --- a/pcsd/pcs.rb +++ b/pcsd/pcs.rb @@ -923,7 +923,8 @@ def is_auth_against_nodes(auth_user, node_names, timeout=10) offline_nodes = [] node_names.uniq.each { |node_name| - threads << Thread.new { + threads << Thread.new(Thread.current[:pcsd_logger_container]) { |logger| + Thread.current[:pcsd_logger_container] = logger code, response = send_request_with_token( auth_user, node_name, 'check_auth', false, {}, true, nil, timeout ) @@ -963,7 +964,8 @@ def pcs_auth(auth_user, nodes) auth_responses = {} threads = [] nodes.each { |node_name, node_data| - threads << Thread.new { + threads << Thread.new(Thread.current[:pcsd_logger_container]) { |logger| + Thread.current[:pcsd_logger_container] = logger begin addr = node_data.fetch('dest_list').fetch(0).fetch('addr') port = node_data.fetch('dest_list').fetch(0).fetch('port') @@ -1199,7 +1201,8 @@ def cluster_status_from_nodes(auth_user, cluster_nodes, cluster_name) threads = [] cluster_nodes.uniq.each { |node| - threads << Thread.new { + threads << Thread.new(Thread.current[:pcsd_logger_container]) { |logger| + Thread.current[:pcsd_logger_container] = logger code, response = send_request_with_token( auth_user, node, diff --git a/pcsd/pcsd-cli.rb b/pcsd/pcsd-cli.rb index 942bae84..4daa93ba 100755 --- a/pcsd/pcsd-cli.rb +++ b/pcsd/pcsd-cli.rb @@ -29,7 +29,8 @@ end auth_user = {} PCS = get_pcs_path() $logger_device = StringIO.new -$logger = configure_logger($logger_device) +$logger = Logger.new($logger_device) +early_log($logger) capabilities, capabilities_pcsd = get_capabilities($logger) CAPABILITIES = capabilities.freeze diff --git a/pcsd/pcsd-ruby.service b/pcsd/pcsd-ruby.service new file mode 100644 index 00000000..deefdf4f --- /dev/null +++ b/pcsd/pcsd-ruby.service @@ -0,0 +1,20 @@ +[Unit] +Description=PCS GUI and remote configuration interface (Ruby) +Documentation=man:pcsd(8) +Documentation=man:pcs(8) +Requires=network-online.target +After=network-online.target +# Stop the service automatically if nothing that depends on it is running +StopWhenUnneeded=true +# When stopping or restarting pcsd, stop or restart pcsd-ruby as well +PartOf=pcsd.service + +[Service] +EnvironmentFile=/etc/sysconfig/pcsd +Environment=GEM_HOME=/usr/lib/pcsd/vendor/bundle/ruby +# This file holds the selinux context +ExecStart=/usr/lib/pcsd/pcsd +Type=notify + +[Install] +WantedBy=multi-user.target diff --git a/pcsd/pcsd.conf b/pcsd/pcsd.conf index 4761c73f..a968f459 100644 --- a/pcsd/pcsd.conf +++ b/pcsd/pcsd.conf @@ -38,3 +38,7 @@ PCSD_SESSION_LIFETIME=3600 #HTTPS_PROXY= # Do not use proxy for specified hostnames #NO_PROXY= + + +# Do not change +RACK_ENV=production diff --git a/pcsd/pcsd.rb b/pcsd/pcsd.rb index eff5c9a8..4cb98799 100644 --- a/pcsd/pcsd.rb +++ b/pcsd/pcsd.rb @@ -22,6 +22,7 @@ require 'permissions.rb' use Rack::CommonLogger set :app_file, __FILE__ +set :logging, false def __msg_cluster_name_already_used(cluster_name) return "The cluster name '#{cluster_name}' has already been added. You may not add two clusters with the same name." @@ -44,17 +45,17 @@ end def getAuthUser() return { - :username => $tornado_username, - :usergroups => $tornado_groups, + :username => Thread.current[:tornado_username], + :usergroups => Thread.current[:tornado_groups], } end before do # nobody is logged in yet @auth_user = nil - @tornado_session_username = $tornado_username - @tornado_session_groups = $tornado_groups - @tornado_is_authenticated = $tornado_is_authenticated + @tornado_session_username = Thread.current[:tornado_username] + @tornado_session_groups = Thread.current[:tornado_groups] + @tornado_is_authenticated = Thread.current[:tornado_is_authenticated] if(request.path.start_with?('/remote/') and request.path != "/remote/auth") or request.path == '/run_pcs' # Sets @auth_user to a hash containing info about logged in user or halts @@ -71,18 +72,8 @@ end configure do PCS = get_pcs_path() PCS_INTERNAL = get_pcs_internal_path() - $logger = configure_logger(StringIO.new()) - $logger.formatter = proc {|severity, datetime, progname, msg| - # rushing a raw logging info into the global - $tornado_logs << { - :level => severity, - :timestamp_usec => (datetime.to_f * 1000000).to_i, - :message => msg, - } - # don't need any log to the stream - "" - } - + $logger = configure_logger() + early_log($logger) capabilities, capabilities_pcsd = get_capabilities($logger) CAPABILITIES = capabilities.freeze CAPABILITIES_PCSD = capabilities_pcsd.freeze @@ -599,7 +590,8 @@ get '/manage/get_nodes_sw_versions' do nodes = params[:node_list] end nodes.each {|node| - threads << Thread.new { + threads << Thread.new(Thread.current[:pcsd_logger_container]) { |logger| + Thread.current[:pcsd_logger_container] = logger code, response = send_request_with_token( auth_user, node, 'get_sw_versions' ) @@ -625,7 +617,8 @@ post '/manage/auth_gui_against_nodes' do data = JSON.parse(params.fetch('data_json')) data.fetch('nodes').each { |node_name, node_data| - threads << Thread.new { + threads << Thread.new(Thread.current[:pcsd_logger_container]) { |logger| + Thread.current[:pcsd_logger_container] = logger dest_list = node_data.fetch('dest_list') addr = dest_list.fetch(0).fetch('addr') port = dest_list.fetch(0).fetch('port') diff --git a/pcsd/pcsd.service b/pcsd/pcsd.service index 88d237af..0cab20ef 100644 --- a/pcsd/pcsd.service +++ b/pcsd/pcsd.service @@ -4,6 +4,8 @@ Documentation=man:pcsd(8) Documentation=man:pcs(8) Requires=network-online.target After=network-online.target +Requires=pcsd-ruby.service +After=pcsd-ruby.service [Service] EnvironmentFile=/etc/sysconfig/pcsd diff --git a/pcsd/pcsd.service-runner b/pcsd/pcsd.service-runner new file mode 100644 index 00000000..40c401fa --- /dev/null +++ b/pcsd/pcsd.service-runner @@ -0,0 +1,24 @@ +#!/usr/bin/ruby +# This file is a runner for ruby part of pcsd callable from a systemd unit. +# It also serves as a holder of a selinux context. + +begin + # add pcsd to the load path (ruby -I) + libdir = File.dirname(__FILE__) + $LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir) + + # change current directory (ruby -C) + Dir.chdir('/var/lib/pcsd') + + # import and run ruby daemon + require 'rserver.rb' +rescue SignalException => e + if [Signal.list['INT'], Signal.list['TERM']].include?(e.signo) + # gracefully exit on SIGINT and SIGTERM + # pcsd sets up signal handlers later, this catches exceptions which occur + # by recieving signals before the handlers have been set up. + exit + else + raise + end +end diff --git a/pcsd/remote.rb b/pcsd/remote.rb index 28b91382..760d3374 100644 --- a/pcsd/remote.rb +++ b/pcsd/remote.rb @@ -938,7 +938,8 @@ def status_all(params, request, auth_user, nodes=[], dont_update_config=false) threads = [] forbidden_nodes = {} nodes.each {|node| - threads << Thread.new { + threads << Thread.new(Thread.current[:pcsd_logger_container]) { |logger| + Thread.current[:pcsd_logger_container] = logger code, response = send_request_with_token(auth_user, node, 'status') if 403 == code forbidden_nodes[node] = true @@ -994,7 +995,8 @@ def clusters_overview(params, request, auth_user) threads = [] config = PCSConfig.new(Cfgsync::PcsdSettings.from_file().text()) config.clusters.each { |cluster| - threads << Thread.new { + threads << Thread.new(Thread.current[:pcsd_logger_container]) { |logger| + Thread.current[:pcsd_logger_container] = logger cluster_map[cluster.name] = { 'cluster_name' => cluster.name, 'error_list' => [ diff --git a/pcsd/rserver.rb b/pcsd/rserver.rb new file mode 100644 index 00000000..6002a73c --- /dev/null +++ b/pcsd/rserver.rb @@ -0,0 +1,98 @@ +require "base64" +require "date" +require "json" +require 'rack' +require 'sinatra' +require 'thin' + +require 'settings.rb' + +def pack_response(response) + return [200, {}, [response.to_json.to_str]] +end + +def unpack_request(transport_env) + return JSON.parse(Base64.strict_decode64( + transport_env["rack.request.form_hash"]["TORNADO_REQUEST"] + )) +end + +class TornadoCommunicationMiddleware + def initialize(app) + @app = app + end + + def call(transport_env) + Thread.current[:pcsd_logger_container] = [] + begin + request = unpack_request(transport_env) + + if ["sinatra_gui", "sinatra_remote"].include?(request["type"]) + if request["type"] == "sinatra_gui" + session = request["session"] + Thread.current[:tornado_username] = session["username"] + Thread.current[:tornado_groups] = session["groups"] + Thread.current[:tornado_is_authenticated] = session["is_authenticated"] + end + + # Keys rack.input and rack.errors are required. We make sure they are + # there. + request_env = request["env"] + request_env["rack.input"] = StringIO.new(request_env["rack.input"]) + request_env["rack.errors"] = StringIO.new() + + status, headers, body = @app.call(request_env) + + rack_errors = request_env['rack.errors'].string() + if not rack_errors.empty?() + $logger.error(rack_errors) + end + + return pack_response({ + :status => status, + :headers => headers, + :body => Base64.encode64(body.join("")), + :logs => Thread.current[:pcsd_logger_container], + }) + end + + if request["type"] == "sync_configs" + return pack_response({ + :next => Time.now.to_i + run_cfgsync(), + :logs => Thread.current[:pcsd_logger_container], + }) + end + + raise "Unexpected value for key 'type': '#{request['type']}'" + rescue => e + return pack_response({:error => "Processing request error: '#{e}'"}) + end + end +end + + +use TornadoCommunicationMiddleware + +require 'pcsd' + +::Rack::Handler.get('thin').run(Sinatra::Application, { + :Host => PCSD_RUBY_SOCKET, +}) do |server| + puts server.class + server.threaded = true + # notify systemd we are running + if ISSYSTEMCTL + if ENV['NOTIFY_SOCKET'] + socket_name = ENV['NOTIFY_SOCKET'].dup + if socket_name.start_with?('@') + # abstract namespace socket + socket_name[0] = "\0" + end + $logger.info("Notifying systemd we are running (socket #{socket_name})") + sd_socket = Socket.new(Socket::AF_UNIX, Socket::SOCK_DGRAM) + sd_socket.connect(Socket.pack_sockaddr_un(socket_name)) + sd_socket.send('READY=1', 0) + sd_socket.close() + end + end +end diff --git a/pcsd/settings.rb b/pcsd/settings.rb index e8dc0c96..4caa5b4c 100644 --- a/pcsd/settings.rb +++ b/pcsd/settings.rb @@ -3,6 +3,7 @@ PCS_INTERNAL_EXEC = '/usr/lib/pcs/pcs_internal' PCSD_EXEC_LOCATION = '/usr/lib/pcsd/' PCSD_VAR_LOCATION = '/var/lib/pcsd/' PCSD_DEFAULT_PORT = 2224 +PCSD_RUBY_SOCKET = '/run/pcsd-ruby.socket' CRT_FILE = PCSD_VAR_LOCATION + 'pcsd.crt' KEY_FILE = PCSD_VAR_LOCATION + 'pcsd.key' diff --git a/pcsd/settings.rb.debian b/pcsd/settings.rb.debian index daaae37b..c547bc51 100644 --- a/pcsd/settings.rb.debian +++ b/pcsd/settings.rb.debian @@ -3,6 +3,7 @@ PCS_INTERNAL_EXEC = '/usr/lib/pcs/pcs_internal' PCSD_EXEC_LOCATION = '/usr/share/pcsd/' PCSD_VAR_LOCATION = '/var/lib/pcsd/' PCSD_DEFAULT_PORT = 2224 +PCSD_RUBY_SOCKET = '/run/pcsd-ruby.socket' CRT_FILE = PCSD_VAR_LOCATION + 'pcsd.crt' KEY_FILE = PCSD_VAR_LOCATION + 'pcsd.key' diff --git a/pcsd/sinatra_cmdline_wrapper.rb b/pcsd/sinatra_cmdline_wrapper.rb deleted file mode 100644 index f7b22008..00000000 --- a/pcsd/sinatra_cmdline_wrapper.rb +++ /dev/null @@ -1,63 +0,0 @@ -require "base64" -require "date" -require "json" - -request_json = ARGF.read() - -begin - request = JSON.parse(request_json) -rescue => e - puts e - exit -end - -if !request.include?("type") - result = {:error => "Type not specified"} - print result.to_json - exit -end - -$tornado_logs = [] - -require 'pcsd' - -if ["sinatra_gui", "sinatra_remote"].include?(request["type"]) - if request["type"] == "sinatra_gui" - $tornado_username = request["session"]["username"] - $tornado_groups = request["session"]["groups"] - $tornado_is_authenticated = request["session"]["is_authenticated"] - end - - set :logging, true - set :run, false - # Do not turn exceptions into fancy 100kB HTML pages and print them on stdout. - # Instead, rack.errors is logged and therefore returned in result[:log]. - set :show_exceptions, false - app = [Sinatra::Application][0] - - env = request["env"] - env["rack.input"] = StringIO.new(env["rack.input"]) - env["rack.errors"] = StringIO.new() - - status, headers, body = app.call(env) - rack_errors = env['rack.errors'].string() - if not rack_errors.empty?() - $logger.error(rack_errors) - end - - result = { - :status => status, - :headers => headers, - :body => Base64.encode64(body.join("")), - } - -elsif request["type"] == "sync_configs" - result = { - :next => Time.now.to_i + run_cfgsync() - } -else - result = {:error => "Unknown type: '#{request["type"]}'"} -end - -result[:logs] = $tornado_logs -print result.to_json -- 2.21.1