##// END OF EJS Templates
user-sessions: added an API call to cleanup sessions.
marcink -
r1367:11dec75f default
parent child Browse files
Show More
@@ -0,0 +1,44 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2017-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import mock
22 import pytest
23
24 from rhodecode.lib.user_sessions import FileAuthSessions
25 from rhodecode.api.tests.utils import (
26 build_data, api_call, assert_ok, assert_error, crash)
27
28
29 @pytest.mark.usefixtures("testuser_api", "app")
30 class TestCleanupSessions(object):
31 def test_api_cleanup_sessions(self):
32 id_, params = build_data(self.apikey, 'cleanup_sessions')
33 response = api_call(self.app, params)
34
35 expected = {'backend': 'file sessions', 'sessions_removed': 0}
36 assert_ok(id_, expected, given=response.body)
37
38 @mock.patch.object(FileAuthSessions, 'clean_sessions', crash)
39 def test_api_cleanup_error(self):
40 id_, params = build_data(self.apikey, 'cleanup_sessions', )
41 response = api_call(self.app, params)
42
43 expected = 'Error occurred during session cleanup'
44 assert_error(id_, expected, given=response.body)
@@ -1,178 +1,244 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 import logging
23 23
24 24 from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCForbidden
25 25
26 26 from rhodecode.api.utils import (
27 27 Optional, OAttr, has_superadmin_permission, get_user_or_error)
28 28 from rhodecode.lib.utils import repo2db_mapper
29 from rhodecode.lib import system_info
30 from rhodecode.lib import user_sessions
29 31 from rhodecode.model.db import UserIpMap
30 32 from rhodecode.model.scm import ScmModel
31 33
32 34 log = logging.getLogger(__name__)
33 35
34 36
35 37 @jsonrpc_method()
36 38 def get_server_info(request, apiuser):
37 39 """
38 40 Returns the |RCE| server information.
39 41
40 42 This includes the running version of |RCE| and all installed
41 43 packages. This command takes the following options:
42 44
43 45 :param apiuser: This is filled automatically from the |authtoken|.
44 46 :type apiuser: AuthUser
45 47
46 48 Example output:
47 49
48 50 .. code-block:: bash
49 51
50 52 id : <id_given_in_input>
51 53 result : {
52 54 'modules': [<module name>,...]
53 55 'py_version': <python version>,
54 56 'platform': <platform type>,
55 57 'rhodecode_version': <rhodecode version>
56 58 }
57 59 error : null
58 60 """
59 61
60 62 if not has_superadmin_permission(apiuser):
61 63 raise JSONRPCForbidden()
62 64
63 65 server_info = ScmModel().get_server_info(request.environ)
64 66 # rhodecode-index requires those
65 67
66 68 server_info['index_storage'] = server_info['search']['value']['location']
67 69 server_info['storage'] = server_info['storage']['value']['path']
68 70
69 71 return server_info
70 72
71 73
72 74 @jsonrpc_method()
73 75 def get_ip(request, apiuser, userid=Optional(OAttr('apiuser'))):
74 76 """
75 77 Displays the IP Address as seen from the |RCE| server.
76 78
77 79 * This command displays the IP Address, as well as all the defined IP
78 80 addresses for the specified user. If the ``userid`` is not set, the
79 81 data returned is for the user calling the method.
80 82
81 83 This command can only be run using an |authtoken| with admin rights to
82 84 the specified repository.
83 85
84 86 This command takes the following options:
85 87
86 88 :param apiuser: This is filled automatically from |authtoken|.
87 89 :type apiuser: AuthUser
88 90 :param userid: Sets the userid for which associated IP Address data
89 91 is returned.
90 92 :type userid: Optional(str or int)
91 93
92 94 Example output:
93 95
94 96 .. code-block:: bash
95 97
96 98 id : <id_given_in_input>
97 99 result : {
98 100 "server_ip_addr": "<ip_from_clien>",
99 101 "user_ips": [
100 102 {
101 103 "ip_addr": "<ip_with_mask>",
102 104 "ip_range": ["<start_ip>", "<end_ip>"],
103 105 },
104 106 ...
105 107 ]
106 108 }
107 109
108 110 """
109 111 if not has_superadmin_permission(apiuser):
110 112 raise JSONRPCForbidden()
111 113
112 114 userid = Optional.extract(userid, evaluate_locals=locals())
113 115 userid = getattr(userid, 'user_id', userid)
114 116
115 117 user = get_user_or_error(userid)
116 118 ips = UserIpMap.query().filter(UserIpMap.user == user).all()
117 119 return {
118 120 'server_ip_addr': request.rpc_ip_addr,
119 121 'user_ips': ips
120 122 }
121 123
122 124
123 125 @jsonrpc_method()
124 126 def rescan_repos(request, apiuser, remove_obsolete=Optional(False)):
125 127 """
126 128 Triggers a rescan of the specified repositories.
127 129
128 130 * If the ``remove_obsolete`` option is set, it also deletes repositories
129 131 that are found in the database but not on the file system, so called
130 132 "clean zombies".
131 133
132 134 This command can only be run using an |authtoken| with admin rights to
133 135 the specified repository.
134 136
135 137 This command takes the following options:
136 138
137 139 :param apiuser: This is filled automatically from the |authtoken|.
138 140 :type apiuser: AuthUser
139 141 :param remove_obsolete: Deletes repositories from the database that
140 142 are not found on the filesystem.
141 143 :type remove_obsolete: Optional(``True`` | ``False``)
142 144
143 145 Example output:
144 146
145 147 .. code-block:: bash
146 148
147 149 id : <id_given_in_input>
148 150 result : {
149 151 'added': [<added repository name>,...]
150 152 'removed': [<removed repository name>,...]
151 153 }
152 154 error : null
153 155
154 156 Example error output:
155 157
156 158 .. code-block:: bash
157 159
158 160 id : <id_given_in_input>
159 161 result : null
160 162 error : {
161 163 'Error occurred during rescan repositories action'
162 164 }
163 165
164 166 """
165 167 if not has_superadmin_permission(apiuser):
166 168 raise JSONRPCForbidden()
167 169
168 170 try:
169 171 rm_obsolete = Optional.extract(remove_obsolete)
170 172 added, removed = repo2db_mapper(ScmModel().repo_scan(),
171 173 remove_obsolete=rm_obsolete)
172 174 return {'added': added, 'removed': removed}
173 175 except Exception:
174 176 log.exception('Failed to run repo rescann')
175 177 raise JSONRPCError(
176 178 'Error occurred during rescan repositories action'
177 179 )
178 180
181
182 @jsonrpc_method()
183 def cleanup_sessions(request, apiuser, older_then=Optional(60)):
184 """
185 Triggers a session cleanup action.
186
187 If the ``older_then`` option is set, only sessions that hasn't been
188 accessed in the given number of days will be removed.
189
190 This command can only be run using an |authtoken| with admin rights to
191 the specified repository.
192
193 This command takes the following options:
194
195 :param apiuser: This is filled automatically from the |authtoken|.
196 :type apiuser: AuthUser
197 :param older_then: Deletes session that hasn't been accessed
198 in given number of days.
199 :type older_then: Optional(int)
200
201 Example output:
202
203 .. code-block:: bash
204
205 id : <id_given_in_input>
206 result: {
207 "backend": "<type of backend>",
208 "sessions_removed": <number_of_removed_sessions>
209 }
210 error : null
211
212 Example error output:
213
214 .. code-block:: bash
215
216 id : <id_given_in_input>
217 result : null
218 error : {
219 'Error occurred during session cleanup'
220 }
221
222 """
223 if not has_superadmin_permission(apiuser):
224 raise JSONRPCForbidden()
225
226 older_then = Optional.extract(older_then)
227 older_than_seconds = 60 * 60 * 24 * older_then
228
229 config = system_info.rhodecode_config().get_value()['value']['config']
230 session_model = user_sessions.get_session_handler(
231 config.get('beaker.session.type', 'memory'))(config)
232
233 backend = session_model.SESSION_TYPE
234 try:
235 cleaned = session_model.clean_sessions(
236 older_than_seconds=older_than_seconds)
237 return {'sessions_removed': cleaned, 'backend': backend}
238 except user_sessions.CleanupCommand as msg:
239 return {'cleanup_command': msg.message, 'backend': backend}
240 except Exception as e:
241 log.exception('Failed session cleanup')
242 raise JSONRPCError(
243 'Error occurred during session cleanup'
244 )
@@ -1,174 +1,181 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2017-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import os
22 22 import time
23 23 import datetime
24 24 import dateutil
25 25 from rhodecode.model.db import DbSession, Session
26 26
27 27
28 28 class CleanupCommand(Exception):
29 29 pass
30 30
31 31
32 32 class BaseAuthSessions(object):
33 33 SESSION_TYPE = None
34 34 NOT_AVAILABLE = 'NOT AVAILABLE'
35 35
36 36 def __init__(self, config):
37 37 session_conf = {}
38 38 for k, v in config.items():
39 39 if k.startswith('beaker.session'):
40 40 session_conf[k] = v
41 41 self.config = session_conf
42 42
43 43 def get_count(self):
44 44 raise NotImplementedError
45 45
46 46 def get_expired_count(self, older_than_seconds=None):
47 47 raise NotImplementedError
48 48
49 49 def clean_sessions(self, older_than_seconds=None):
50 50 raise NotImplementedError
51 51
52 52 def _seconds_to_date(self, seconds):
53 53 return datetime.datetime.utcnow() - dateutil.relativedelta.relativedelta(
54 54 seconds=seconds)
55 55
56 56
57 57 class DbAuthSessions(BaseAuthSessions):
58 58 SESSION_TYPE = 'ext:database'
59 59
60 60 def get_count(self):
61 61 return DbSession.query().count()
62 62
63 63 def get_expired_count(self, older_than_seconds=None):
64 64 expiry_date = self._seconds_to_date(older_than_seconds)
65 65 return DbSession.query().filter(DbSession.accessed < expiry_date).count()
66 66
67 67 def clean_sessions(self, older_than_seconds=None):
68 68 expiry_date = self._seconds_to_date(older_than_seconds)
69 to_remove = DbSession.query().filter(DbSession.accessed < expiry_date).count()
69 70 DbSession.query().filter(DbSession.accessed < expiry_date).delete()
70 71 Session().commit()
72 return to_remove
71 73
72 74
73 75 class FileAuthSessions(BaseAuthSessions):
74 76 SESSION_TYPE = 'file sessions'
75 77
76 78 def _get_sessions_dir(self):
77 79 data_dir = self.config.get('beaker.session.data_dir')
78 80 return data_dir
79 81
80 82 def _count_on_filesystem(self, path, older_than=0, callback=None):
81 value = dict(percent=0, used=0, total=0, items=0, path=path, text='')
83 value = dict(percent=0, used=0, total=0, items=0, callbacks=0,
84 path=path, text='')
82 85 items_count = 0
83 86 used = 0
87 callbacks = 0
84 88 cur_time = time.time()
85 89 for root, dirs, files in os.walk(path):
86 90 for f in files:
87 91 final_path = os.path.join(root, f)
88 92 try:
89 93 mtime = os.stat(final_path).st_mtime
90 94 if (cur_time - mtime) > older_than:
91 95 items_count += 1
92 96 if callback:
93 97 callback_res = callback(final_path)
98 callbacks += 1
94 99 else:
95 100 used += os.path.getsize(final_path)
96 101 except OSError:
97 102 pass
98 103 value.update({
99 104 'percent': 100,
100 105 'used': used,
101 106 'total': used,
102 'items': items_count
107 'items': items_count,
108 'callbacks': callbacks
103 109 })
104 110 return value
105 111
106 112 def get_count(self):
107 113 try:
108 114 sessions_dir = self._get_sessions_dir()
109 115 items_count = self._count_on_filesystem(sessions_dir)['items']
110 116 except Exception:
111 117 items_count = self.NOT_AVAILABLE
112 118 return items_count
113 119
114 120 def get_expired_count(self, older_than_seconds=0):
115 121 try:
116 122 sessions_dir = self._get_sessions_dir()
117 123 items_count = self._count_on_filesystem(
118 124 sessions_dir, older_than=older_than_seconds)['items']
119 125 except Exception:
120 126 items_count = self.NOT_AVAILABLE
121 127 return items_count
122 128
123 129 def clean_sessions(self, older_than_seconds=0):
124 130 # find . -mtime +60 -exec rm {} \;
125 131
126 132 sessions_dir = self._get_sessions_dir()
127 133
128 134 def remove_item(path):
129 135 os.remove(path)
130 136
131 return self._count_on_filesystem(
137 stats = self._count_on_filesystem(
132 138 sessions_dir, older_than=older_than_seconds,
133 callback=remove_item)['items']
139 callback=remove_item)
140 return stats['callbacks']
134 141
135 142
136 143 class MemcachedAuthSessions(BaseAuthSessions):
137 144 SESSION_TYPE = 'ext:memcached'
138 145
139 146 def get_count(self):
140 147 return self.NOT_AVAILABLE
141 148
142 149 def get_expired_count(self, older_than_seconds=None):
143 150 return self.NOT_AVAILABLE
144 151
145 152 def clean_sessions(self, older_than_seconds=None):
146 153 raise CleanupCommand('Cleanup for this session type not yet available')
147 154
148 155
149 156 class MemoryAuthSessions(BaseAuthSessions):
150 157 SESSION_TYPE = 'memory'
151 158
152 159 def get_count(self):
153 160 return self.NOT_AVAILABLE
154 161
155 162 def get_expired_count(self, older_than_seconds=None):
156 163 return self.NOT_AVAILABLE
157 164
158 165 def clean_sessions(self, older_than_seconds=None):
159 166 raise CleanupCommand('Cleanup for this session type not yet available')
160 167
161 168
162 169 def get_session_handler(session_type):
163 170 types = {
164 171 'file': FileAuthSessions,
165 172 'ext:memcached': MemcachedAuthSessions,
166 173 'ext:database': DbAuthSessions,
167 174 'memory': MemoryAuthSessions
168 175 }
169 176
170 177 try:
171 178 return types[session_type]
172 179 except KeyError:
173 180 raise ValueError(
174 181 'This type {} is not supported'.format(session_type))
General Comments 0
You need to be logged in to leave comments. Login now