Skip to content

Commit

Permalink
Reload server configuration file
Browse files Browse the repository at this point in the history
  • Loading branch information
csordasmarton committed Nov 6, 2018
1 parent 9d83d4c commit ce2f282
Show file tree
Hide file tree
Showing 7 changed files with 153 additions and 22 deletions.
7 changes: 7 additions & 0 deletions bin/CodeChecker
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,13 @@ def main(subcommand=None):
signal.signal(signal.SIGTERM, signal_term_handler)
signal.signal(signal.SIGINT, signal_term_handler)

def signal_reload_handler(sig, frame):
global proc_pid
if proc_pid:
os.kill(proc_pid, signal.SIGHUP)

signal.signal(signal.SIGHUP, signal_reload_handler)

try:
run_codechecker(checker_env, subcommand)
finally:
Expand Down
10 changes: 10 additions & 0 deletions docs/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,15 @@ is handled.

After this many login attempts made towards the server, it will perform an
automatic cleanup of old, expired sessions.
This option can be changed and reloaded without server restart by using the
`--reload` option of CodeChecker server command.
* `session_lifetime`

(in seconds) The lifetime of the session sets that after this many seconds
since last session access the session is permanently invalidated.

This option can be changed and reloaded without server restart by using the
`--reload` option of CodeChecker server command.
* `refresh_time`

(in seconds) Refresh time of the local session objects. We use local session
Expand All @@ -64,6 +68,8 @@ is handled.
seconds since last session access the local session is permanently
invalidated.

This option can be changed and reloaded without server restart by using the
`--reload` option of CodeChecker server command.
If the server is shut down, every session is **immediately** invalidated. The
running sessions are only stored in the server's memory, they are not written
to storage.
Expand All @@ -77,6 +83,10 @@ authenticates them. Authentications are attempted in the order they are
described here: *dicitonary* takes precedence, *pam* is a secondary and *ldap*
is a tertiary backend, if enabled.

Only `refresh_time`, `session_lifetime` and `logins_until_cleanup` options can
be changed and reloaded without server restart by using the `--reload`
option of `CodeChecker server` command.

### <a name="dictionary-authentication"></a> <i>Dictionary</i> authentication

The `authentication.method_dictionary` contains a plaintext `username:password`
Expand Down
10 changes: 8 additions & 2 deletions docs/server_config.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,16 @@ stored on the server for a product.
If this field is not present in the config file or the value of this field is a
negative value, run storage becomes unlimited.

This option can be changed and reloaded without server restart by using the
`--reload` option of CodeChecker server command.

## Storage
The `store` section of the config file controls storage specific options for the
server and command line.

All sub-values of this option can be changed and reloaded without server restart
by using the `--reload` option of CodeChecker server command.

### Directory of analysis statistics
The `analysis_statistics_dir` option specifies a directory where analysis
statistics should be stored. If this option is specified in the config file the
Expand All @@ -52,5 +58,5 @@ size of uploadable compilation database file in *bytes*.
*Default value*: 104857600 bytes = 100 MB

## Authentication
For authentication configuration options see the
[Authentication](authentication.md) documentation.
For authentication configuration options and which options can be reloaded see
the [Authentication](authentication.md) documentation.
7 changes: 7 additions & 0 deletions docs/user_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -1343,6 +1343,8 @@ SSL will be automatically enabled.
~~~~~~~~~~~~~~~~~~~~~
running server management:
-l, --list List the servers that has been started by you.
-r, --reload Sends the CodeChecker server process a SIGHUP signal,
causing it to reread it's configuration files.
-s, --stop Stops the server associated with the given view-port
and workspace.
--stop-all Stops all of your running CodeChecker server
Expand All @@ -1366,6 +1368,11 @@ CodeChecker server -w ~/my_codechecker_workspace -p 8002 --stop

`--stop-all` will stop every running server that is printed by `--list`.

`CodeChecker server --reload` command allows you to changing configuration-file
options that do not require a complete restart to take effect. For more
information which option can be reloaded see
[server config](docs/server_config.md).

### <a name="manage-server-database-upgrade"></a> Manage server database upgrades

Use these arguments to manage the database versions handled by the server.
Expand Down
39 changes: 39 additions & 0 deletions libcodechecker/libhandlers/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import argparse
import errno
import os
import signal
import socket
import sys

Expand Down Expand Up @@ -239,6 +240,15 @@ def add_arguments_to_parser(parser):
help="List the servers that has been started "
"by you.")

instance_mgmnt.add_argument('-r', '--reload',
dest="reload",
action='store_true',
default=argparse.SUPPRESS,
required=False,
help="Sends the CodeChecker server process a "
"SIGHUP signal, causing it to reread "
"it's configuration files.")

# TODO: '-s' was removed from 'quickcheck', it shouldn't be here either?
instance_mgmnt.add_argument('-s', '--stop',
dest="stop",
Expand Down Expand Up @@ -640,6 +650,31 @@ def __instance_management(args):
raise


def __reload_config(args):
"""
Sends the CodeChecker server process a SIGHUP signal, causing it to
reread it's configuration files.
"""
for i in instance_manager.get_instances():
if i['hostname'] != socket.gethostname():
continue

# A RELOAD only reloads the server associated with the given workspace
# and view-port.
if 'reload' in args and \
not (i['port'] == args.view_port and
os.path.abspath(i['workspace']) ==
os.path.abspath(args.config_directory)):
continue

try:
os.kill(i['pid'], signal.SIGHUP)
except Exception:
LOG.error("Couldn't reload configuration file for process PID #%s",
str(i['pid']))
raise


def server_init_start(args):
"""
Start or manage a CodeChecker report server.
Expand All @@ -649,6 +684,10 @@ def server_init_start(args):
__instance_management(args)
sys.exit(0)

if 'reload' in args:
__reload_config(args)
sys.exit(0)

# Actual server starting from this point.
if not host_check.check_zlib():
raise Exception("zlib is not available on the system!")
Expand Down
8 changes: 8 additions & 0 deletions libcodechecker/server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -981,6 +981,14 @@ def signal_handler(*args, **kwargs):
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)

def reload_signal_handler(*args, **kwargs):
"""
Reloads server configuration file.
"""
manager.reload_config()

signal.signal(signal.SIGHUP, reload_signal_handler)

try:
instance_manager.register(os.getpid(),
os.path.abspath(
Expand Down
94 changes: 74 additions & 20 deletions libcodechecker/server/session_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

from datetime import datetime
import hashlib
import json

from libcodechecker.logger import get_logger
from libcodechecker.util import check_file_owner_rw, load_json_or_empty, \
Expand Down Expand Up @@ -49,8 +50,8 @@ def __init__(self, token, username, groups,
self.user = username
self.groups = groups

self.__session_lifetime = session_lifetime
self.__refresh_time = refresh_time if refresh_time else None
self.session_lifetime = session_lifetime
self.refresh_time = refresh_time if refresh_time else None
self.__root = is_root
self.__database = database
self.__can_expire = can_expire
Expand All @@ -67,11 +68,11 @@ def is_refresh_time_expire(self):
"""
Returns if the refresh time of the session is expired.
"""
if not self.__refresh_time:
if not self.refresh_time:
return True

return (datetime.now() - self.last_access).total_seconds() > \
self.__refresh_time
self.refresh_time

@property
def is_alive(self):
Expand All @@ -83,7 +84,7 @@ def is_alive(self):
return True

return (datetime.now() - self.last_access).total_seconds() <= \
self.__session_lifetime
self.session_lifetime

def revalidate(self):
"""
Expand Down Expand Up @@ -144,27 +145,15 @@ def __init__(self, configuration_file, session_salt,
self.__logins_since_prune = 0
self.__sessions = []
self.__session_salt = hashlib.sha1(session_salt).hexdigest()
self.__configuration_file = configuration_file

LOG.debug(configuration_file)
scfg_dict = load_json_or_empty(configuration_file, {},
'server configuration')
if scfg_dict != {}:
check_file_owner_rw(configuration_file)
else:
# If the configuration dict is empty, it means a JSON couldn't
# have been parsed from it.
raise ValueError("Server configuration file was invalid, or "
"empty.")
scfg_dict = self.__get_config_dict()

# FIXME: Refactor this. This is irrelevant to authentication config,
# so it should NOT be handled by session_manager. A separate config
# handler for the server's stuff should be created, that can properly
# instantiate SessionManager with the found configuration.
self.__max_run_count = scfg_dict['max_run_count'] \
if 'max_run_count' in scfg_dict else None

self.__report_dir_store = None

self.__max_run_count = scfg_dict.get('max_run_count', None)
self.__store_config = scfg_dict.get('store', {})
self.__auth_config = scfg_dict['authentication']

Expand Down Expand Up @@ -222,6 +211,71 @@ def __init__(self, configuration_file, session_salt,
"Falling back to no authentication.")
self.__auth_config['enabled'] = False

def __get_config_dict(self):
"""
Get server config information from the configuration file. Raise
ValueError if the configuration file is invalid.
"""
LOG.debug(self.__configuration_file)
cfg_dict = load_json_or_empty(self.__configuration_file, {},
'server configuration')
if cfg_dict != {}:
check_file_owner_rw(self.__configuration_file)
else:
# If the configuration dict is empty, it means a JSON couldn't
# have been parsed from it.
raise ValueError("Server configuration file was invalid, or "
"empty.")
return cfg_dict

def reload_config(self):
LOG.info("Reload server configuration file...")
try:
cfg_dict = self.__get_config_dict()

prev_max_run_count = self.__max_run_count
new_max_run_count = cfg_dict.get('max_run_count', None)
if prev_max_run_count != new_max_run_count:
self.__max_run_count = new_max_run_count
LOG.debug("Changed '%s' value from %s to %s", 'max_run_count',
prev_max_run_count, new_max_run_count)

prev_store_config = json.dumps(self.__store_config, sort_keys=True,
indent=2)
new_store_config_val = cfg_dict.get('store', {})
new_store_config = json.dumps(new_store_config_val, sort_keys=True,
indent=2)
if prev_store_config != new_store_config:
self.__store_config = new_store_config_val
LOG.debug("Updating 'store' config from %s to %s",
prev_store_config, new_store_config)

update_sessions = False
auth_fields_to_update = ['session_lifetime', 'refresh_time',
'logins_until_cleanup']
for field in auth_fields_to_update:
if field in self.__auth_config:
prev_value = self.__auth_config[field]
new_value = cfg_dict['authentication'].get(field, 0)
if prev_value != new_value:
self.__auth_config[field] = new_value
LOG.debug("Changed '%s' value from %s to %s",
field, prev_value, new_value)
update_sessions = True

if update_sessions:
# Update configuration options of the already existing
# sessions.
for session in self.__sessions:
session.session_lifetime = \
self.__auth_config['session_lifetime']
session.refresh_time = self.__auth_config['refresh_time']

LOG.info("Done.")
except ValueError as ex:
LOG.error("Couldn't reload server configuration file")
LOG.error(str(ex))

@property
def is_enabled(self):
return self.__auth_config.get('enabled')
Expand Down

0 comments on commit ce2f282

Please sign in to comment.