diff --git a/rhodecode/api/tests/test_cleanup_sessions.py b/rhodecode/api/tests/test_cleanup_sessions.py new file mode 100644 --- /dev/null +++ b/rhodecode/api/tests/test_cleanup_sessions.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2017-2017 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +import mock +import pytest + +from rhodecode.lib.user_sessions import FileAuthSessions +from rhodecode.api.tests.utils import ( + build_data, api_call, assert_ok, assert_error, crash) + + +@pytest.mark.usefixtures("testuser_api", "app") +class TestCleanupSessions(object): + def test_api_cleanup_sessions(self): + id_, params = build_data(self.apikey, 'cleanup_sessions') + response = api_call(self.app, params) + + expected = {'backend': 'file sessions', 'sessions_removed': 0} + assert_ok(id_, expected, given=response.body) + + @mock.patch.object(FileAuthSessions, 'clean_sessions', crash) + def test_api_cleanup_error(self): + id_, params = build_data(self.apikey, 'cleanup_sessions', ) + response = api_call(self.app, params) + + expected = 'Error occurred during session cleanup' + assert_error(id_, expected, given=response.body) diff --git a/rhodecode/api/views/server_api.py b/rhodecode/api/views/server_api.py --- a/rhodecode/api/views/server_api.py +++ b/rhodecode/api/views/server_api.py @@ -26,6 +26,8 @@ from rhodecode.api import jsonrpc_method from rhodecode.api.utils import ( Optional, OAttr, has_superadmin_permission, get_user_or_error) from rhodecode.lib.utils import repo2db_mapper +from rhodecode.lib import system_info +from rhodecode.lib import user_sessions from rhodecode.model.db import UserIpMap from rhodecode.model.scm import ScmModel @@ -176,3 +178,67 @@ def rescan_repos(request, apiuser, remov 'Error occurred during rescan repositories action' ) + +@jsonrpc_method() +def cleanup_sessions(request, apiuser, older_then=Optional(60)): + """ + Triggers a session cleanup action. + + If the ``older_then`` option is set, only sessions that hasn't been + accessed in the given number of days will be removed. + + This command can only be run using an |authtoken| with admin rights to + the specified repository. + + This command takes the following options: + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + :param older_then: Deletes session that hasn't been accessed + in given number of days. + :type older_then: Optional(int) + + Example output: + + .. code-block:: bash + + id : + result: { + "backend": "", + "sessions_removed": + } + error : null + + Example error output: + + .. code-block:: bash + + id : + result : null + error : { + 'Error occurred during session cleanup' + } + + """ + if not has_superadmin_permission(apiuser): + raise JSONRPCForbidden() + + older_then = Optional.extract(older_then) + older_than_seconds = 60 * 60 * 24 * older_then + + config = system_info.rhodecode_config().get_value()['value']['config'] + session_model = user_sessions.get_session_handler( + config.get('beaker.session.type', 'memory'))(config) + + backend = session_model.SESSION_TYPE + try: + cleaned = session_model.clean_sessions( + older_than_seconds=older_than_seconds) + return {'sessions_removed': cleaned, 'backend': backend} + except user_sessions.CleanupCommand as msg: + return {'cleanup_command': msg.message, 'backend': backend} + except Exception as e: + log.exception('Failed session cleanup') + raise JSONRPCError( + 'Error occurred during session cleanup' + ) diff --git a/rhodecode/lib/user_sessions.py b/rhodecode/lib/user_sessions.py --- a/rhodecode/lib/user_sessions.py +++ b/rhodecode/lib/user_sessions.py @@ -66,8 +66,10 @@ class DbAuthSessions(BaseAuthSessions): def clean_sessions(self, older_than_seconds=None): expiry_date = self._seconds_to_date(older_than_seconds) + to_remove = DbSession.query().filter(DbSession.accessed < expiry_date).count() DbSession.query().filter(DbSession.accessed < expiry_date).delete() Session().commit() + return to_remove class FileAuthSessions(BaseAuthSessions): @@ -78,9 +80,11 @@ class FileAuthSessions(BaseAuthSessions) return data_dir def _count_on_filesystem(self, path, older_than=0, callback=None): - value = dict(percent=0, used=0, total=0, items=0, path=path, text='') + value = dict(percent=0, used=0, total=0, items=0, callbacks=0, + path=path, text='') items_count = 0 used = 0 + callbacks = 0 cur_time = time.time() for root, dirs, files in os.walk(path): for f in files: @@ -91,6 +95,7 @@ class FileAuthSessions(BaseAuthSessions) items_count += 1 if callback: callback_res = callback(final_path) + callbacks += 1 else: used += os.path.getsize(final_path) except OSError: @@ -99,7 +104,8 @@ class FileAuthSessions(BaseAuthSessions) 'percent': 100, 'used': used, 'total': used, - 'items': items_count + 'items': items_count, + 'callbacks': callbacks }) return value @@ -128,9 +134,10 @@ class FileAuthSessions(BaseAuthSessions) def remove_item(path): os.remove(path) - return self._count_on_filesystem( + stats = self._count_on_filesystem( sessions_dir, older_than=older_than_seconds, - callback=remove_item)['items'] + callback=remove_item) + return stats['callbacks'] class MemcachedAuthSessions(BaseAuthSessions):