Skip to content

Commit 8a123bc

Browse files
committed
Merge branch 'release/2.11.0'
2 parents 38b3d13 + 805c373 commit 8a123bc

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+528
-424
lines changed

CHANGES.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Changelog
22

3+
## Version 2.11.0
4+
5+
* Added search option to user view
6+
* Added support for discarding sessions. Admins can now mark sessions for deletion after a certain period of time (30 days by default). This is useful for temporary sessions that are not interesting for keeping forever.
7+
* Sessions can have a TTL, automatically marking their discard date on keepalives and session ends
8+
* Support reporting of remote and local SCM branches
9+
* Removed the "mark archived" and "mark investigated" features. Investigation will be added in a future release as a part of a stricter workflow, and archiving can now be replaced with discarding sessions
10+
311
## Version 2.10.0
412

513
* Added pagination navigation to session and test error views

_lib/celery.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@ def celery():
1313

1414
@celery.command()
1515
@click.argument('name')
16+
@click.option('--defer', default=False, is_flag=True)
1617
@requires_env('app')
17-
def task(name):
18+
def task(name, defer):
1819

1920
from flask_app import tasks
2021
from flask_app.app import create_app
@@ -24,5 +25,9 @@ def task(name):
2425
if task is None:
2526
click.echo('Could not find task named {}'.format(task))
2627
raise click.Abort()
27-
with create_app().app_context():
28-
task()
28+
29+
with create_app().app_context(), logbook.StderrHandler(level='DEBUG'):
30+
if defer:
31+
task.delay()
32+
else:
33+
task()

_lib/frontend_tmux.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ windows:
77
- python manage.py testserver --no-tmux --port 8800 --no-livereload
88
- window_name: celery worker
99
panes:
10-
- .env/bin/celery -A flask_app.tasks worker --loglevel=info -B
10+
- .env/bin/celery -A flask_app.tasks worker --loglevel=info -B --max-tasks-per-child=500
1111
- .env/bin/celery -A flask_app.tasks shell
1212
- window_name: redis
1313
panes:

_lib/slash_running.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def _get_initial_session_metadata(self):
3434
return returned
3535

3636
def _get_extra_session_start_kwargs(self):
37-
returned = {}
37+
returned = super()._get_extra_session_start_kwargs()
3838
if use_subjects:
3939
returned['subjects'] = [
4040
{

deps/develop.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ pytest
22
pytest-cov
33
pytest-selenium
44
Flask-Loopback
5-
backslash>=2.28.0
5+
backslash>=2.31.1
66
tmuxp
77
livereload
88
ipython

docker/docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ services:
2626

2727
worker:
2828
image: getslash/backslash
29-
command: dockerize -timeout 3600s -wait tcp://rabbitmq:5672 .env/bin/celery -A flask_app.tasks worker -B --loglevel=info
29+
command: dockerize -timeout 3600s -wait tcp://rabbitmq:5672 .env/bin/celery -A flask_app.tasks worker -B --loglevel=info --max-tasks-per-child=500
3030
logging:
3131
driver: syslog
3232
environment:

flask_app/activity.py

Lines changed: 1 addition & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -4,85 +4,9 @@
44

55
from flask import g
66

7-
from sqlalchemy.sql import select, union_all, literal_column, text
8-
from sqlalchemy import and_
9-
107
from flask_security import current_user
118

129

13-
14-
ACTION_ARCHIVED, ACTION_UNARCHIVED, ACTION_INVESTIGATED, ACTION_UNINVESTIGATED, MAX_ACTIVITY, ACTION_COMMENTED = range(6)
15-
16-
_ACTION_STRINGS = {
17-
ACTION_COMMENTED: 'commented',
18-
ACTION_ARCHIVED: 'archived',
19-
ACTION_UNARCHIVED: 'unarchived',
20-
ACTION_INVESTIGATED: 'investigated',
21-
ACTION_UNINVESTIGATED: 'uninvestigated',
22-
}
23-
24-
def register_user_activity(action, **kw):
25-
from . import models
26-
assert action < MAX_ACTIVITY
27-
models.db.session.add(
28-
models.Activity(
29-
action=action,
30-
user_id=current_user.id,
31-
**kw))
32-
33-
def get_action_string(action):
34-
return _ACTION_STRINGS.get(action)
35-
36-
37-
def get_activity_query(user_id=None, session_id=None, test_id=None):
38-
# pylint: disable=no-member
39-
from .models import Activity, Comment, User
40-
41-
_filter = functools.partial(_apply_filters, user_id=user_id, session_id=session_id, test_id=test_id)
42-
43-
comments = select([
44-
literal_column("('comment:' || comment.id)").label('id'),
45-
literal_column(str(ACTION_COMMENTED)).label('action'),
46-
Comment.user_id.label('user_id'),
47-
Comment.session_id.label('session_id'),
48-
Comment.test_id.label('test_id'),
49-
Comment.timestamp.label('timestamp'),
50-
Comment.comment.label('text'),
51-
(User.first_name + ' ' + User.last_name).label('user_name'),
52-
User.email.label('user_email'),
53-
]).select_from(Comment.__table__.join(User, User.id == Comment.user_id))
54-
55-
comments = _filter(Comment, comments)
56-
57-
activity = select([
58-
literal_column("('activity:' || activity.id)").label('id'),
59-
Activity.action.label('action'),
60-
Activity.user_id.label('user_id'),
61-
Activity.session_id.label('session_id'),
62-
Activity.test_id.label('test_id'),
63-
Activity.timestamp.label('timestamp'),
64-
literal_column("NULL").label('text'),
65-
(User.first_name + ' ' + User.last_name).label('user_name'),
66-
User.email.label('user_email'),
67-
]).select_from(Activity.__table__.join(User, User.id == Activity.user_id))
68-
69-
activity = _filter(Activity, activity)
70-
71-
u = union_all(comments, activity).alias('u')
72-
73-
return select([u]).order_by(u.c.timestamp)
74-
75-
76-
def _apply_filters(model, query, **filters):
77-
whereclause = []
78-
79-
for key, value in filters.items():
80-
if value is None:
81-
continue
82-
whereclause.append(getattr(model, key) == value)
83-
return query.where(and_(*whereclause))
84-
85-
8610
def updates_last_active(func):
8711
from . import models
8812

@@ -96,7 +20,7 @@ def new_func(*args, **kwargs):
9620
u = None
9721

9822
if u is not None:
99-
u.last_activity = flux.current_timeline.time()
23+
u.last_activity = flux.current_timeline.time() # pylint: disable=no-member
10024
models.db.session.add(u)
10125
return func(*args, **kwargs)
10226

flask_app/blueprints/api/main.py

Lines changed: 5 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from contextlib import contextmanager
22
import logbook
3-
import multiprocessing
43

54
import requests
65
from flask import abort, current_app
@@ -11,8 +10,6 @@
1110
from sqlalchemy.exc import DataError
1211

1312
from .blueprint import API
14-
from ... import activity
15-
from ... import models
1613
from ... import metrics
1714
from ...models import db, Session, Test, Comment, User, Role, Warning, Entity, TestVariation, TestMetadata
1815
from ...utils import get_current_time, statuses
@@ -100,7 +97,7 @@ def append_upcoming_tests(tests: list, session_id: int):
10097
except DataError:
10198
raise
10299

103-
@API(version=2)
100+
@API(version=3)
104101
def report_test_start(
105102
session_id: int,
106103
name: str,
@@ -111,6 +108,8 @@ def report_test_start(
111108
file_hash: (str, NoneType)=None,
112109
scm_revision: (str, NoneType)=None,
113110
scm_dirty: bool=False,
111+
scm_local_branch: (str, NoneType)=None,
112+
scm_remote_branch: (str, NoneType)=None,
114113
is_interactive: bool=False,
115114
variation: (dict, NoneType)=None,
116115
metadata: (dict, NoneType)=None,
@@ -135,6 +134,8 @@ def report_test_start(
135134
returned.status = statuses.RUNNING
136135
returned.scm_dirty = scm_dirty
137136
returned.scm_revision = scm_revision
137+
returned.scm_local_branch = scm_local_branch
138+
returned.scm_remote_branch = scm_remote_branch
138139
returned.scm = scm
139140
returned.is_interactive = is_interactive
140141
returned.file_hash = file_hash
@@ -243,27 +244,6 @@ def report_test_interrupted(id: int):
243244
_update_running_test_status(id, statuses.INTERRUPTED)
244245
db.session.commit()
245246

246-
@API(require_real_login=True)
247-
@requires_role('moderator')
248-
def toggle_archived(session_id: int):
249-
returned = _toggle_session_attribute(session_id, 'archived', activity.ACTION_ARCHIVED, activity.ACTION_UNARCHIVED)
250-
db.session.commit()
251-
return returned
252-
253-
@API(require_real_login=True)
254-
def toggle_investigated(session_id: int):
255-
_toggle_session_attribute(session_id, 'investigated', activity.ACTION_INVESTIGATED, activity.ACTION_UNINVESTIGATED)
256-
db.session.commit()
257-
258-
def _toggle_session_attribute(session_id, attr, on_action, off_action):
259-
session = Session.query.get_or_404(session_id)
260-
new_value = not getattr(session, attr)
261-
setattr(session, attr, new_value)
262-
db.session.add(session)
263-
activity.register_user_activity(on_action if new_value else off_action, session_id=session_id)
264-
return new_value
265-
266-
267247

268248
def _update_running_test_status(test_id, status, ignore_conflict=False, additional_updates=None):
269249
logbook.debug('marking test {} as {}', test_id, status)

flask_app/blueprints/api/sessions.py

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,27 @@
1-
import requests
2-
31
from flask import g, request
42
from flask_simple_api import error_abort
3+
import flux
4+
import requests
55

66
from sqlalchemy.orm.exc import NoResultFound
77
from sqlalchemy.exc import IntegrityError
88
from ...auth import get_or_create_user
99

10+
from ...search import get_orm_query_from_search_string
1011
from ...models import Session, Test, db, SessionMetadata
1112
from ...utils import get_current_time, statuses
13+
from ...utils.api_utils import requires_role
1214
from ...utils.subjects import get_or_create_subject_instance
1315
from ...utils.users import has_role
1416
from ... import metrics
1517
from .blueprint import API
1618

1719
NoneType = type(None)
1820

21+
_DEFAULT_DELETE_GRACE_PERIOD_SECONDS = 60 * 60 * 24 * 30
1922

20-
@API(version=2)
23+
24+
@API(version=3)
2125
def report_session_start(logical_id: str=None,
2226
parent_logical_id: (NoneType, str)=None,
2327
is_parent_session: bool=False,
@@ -29,6 +33,7 @@ def report_session_start(logical_id: str=None,
2933
keepalive_interval: (NoneType, int)=None,
3034
subjects: (list, NoneType)=None,
3135
infrastructure: (str, NoneType)=None,
36+
ttl_seconds: (int, NoneType)=None,
3237
):
3338
if hostname is None:
3439
hostname = request.remote_addr
@@ -44,6 +49,9 @@ def report_session_start(logical_id: str=None,
4449
user_id = g.token_user.id
4550
real_user_id = None
4651

52+
if keepalive_interval is None and ttl_seconds is not None:
53+
error_abort("Cannot specify session TTL when keepalive isn't used")
54+
4755
returned = Session(
4856
hostname=hostname,
4957
parent_logical_id=parent_logical_id,
@@ -56,10 +64,11 @@ def report_session_start(logical_id: str=None,
5664
status=statuses.RUNNING,
5765
logical_id=logical_id,
5866
keepalive_interval=keepalive_interval,
59-
next_keepalive=None if keepalive_interval is None else get_current_time() +
60-
keepalive_interval,
67+
ttl_seconds=ttl_seconds,
6168
)
6269

70+
returned.update_keepalive()
71+
6372
returned.mark_started()
6473

6574
if subjects:
@@ -117,6 +126,8 @@ def report_session_end(id: int, duration: (int, NoneType)=None, has_fatal_errors
117126
session.status = statuses.SUCCESS
118127
session.has_fatal_errors = has_fatal_errors
119128
session.in_pdb = False
129+
if session.ttl_seconds is not None:
130+
session.delete_at = flux.current_timeline.time() + session.ttl_seconds
120131
db.session.add(session)
121132
db.session.commit()
122133

@@ -143,8 +154,7 @@ def send_keepalive(session_id: int):
143154
if s.end_time is not None:
144155
return
145156
timestamp = get_current_time() + s.keepalive_interval
146-
s.next_keepalive = timestamp
147-
s.extend_timespan_to(timestamp)
157+
s.update_keepalive()
148158
for test in Test.query.filter(Test.session_id==session_id,
149159
Test.end_time == None,
150160
Test.start_time != None):
@@ -161,5 +171,33 @@ def report_session_interrupted(id: int):
161171
s.status = statuses.INTERRUPTED
162172
if s.parent:
163173
s.parent.status = statuses.INTERRUPTED
164-
db.session.add(s)
174+
db.session.commit()
175+
176+
177+
@API(require_real_login=True)
178+
@requires_role('admin')
179+
def discard_session(session_id: int, grace_period_seconds: int=_DEFAULT_DELETE_GRACE_PERIOD_SECONDS):
180+
session = Session.query.get_or_404(session_id)
181+
session.delete_at = flux.current_timeline.time() + grace_period_seconds # pylint: disable=undefined-variable
182+
db.session.commit()
183+
184+
185+
@API(require_real_login=True)
186+
@requires_role('admin')
187+
def discard_sessions_search(search_string: str, grace_period_seconds: int=_DEFAULT_DELETE_GRACE_PERIOD_SECONDS):
188+
if not search_string:
189+
error_abort('Invadlid search string')
190+
delete_at = flux.current_timeline.time() + grace_period_seconds
191+
search_query = get_orm_query_from_search_string('session', search_string).filter(Session.delete_at == None)
192+
Session.query.filter(Session.id.in_(db.session.query(search_query.subquery().c.id))).update({
193+
'delete_at': delete_at
194+
}, synchronize_session=False)
195+
db.session.commit()
196+
197+
198+
@API(require_real_login=True)
199+
@requires_role('admin')
200+
def preserve_session(session_id: int):
201+
session = Session.query.get_or_404(session_id)
202+
session.delete_at = None
165203
db.session.commit()

flask_app/blueprints/filter_configs.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
from .. import models
44
from ..utils import statuses
5-
from ..utils.filtering import ConstFilter, ToggleFilter, FilterConfig, in_, notin_
5+
from ..utils.filtering import ConstFilter, FilterConfig, in_, notin_
6+
67

78
_STATUS_FILTERS = {
89
'unsuccessful': (notin_, (statuses.SUCCESS, statuses.SKIPPED, statuses.RUNNING)),
@@ -12,12 +13,7 @@
1213

1314

1415
SESSION_FILTERS = FilterConfig({
15-
'investigated': ConstFilter(models.Session.investigated, {
16-
'not investigated': False,
17-
'investigated': True,
18-
}),
1916
'status': ConstFilter(models.Session.status, _STATUS_FILTERS),
20-
'archived': ToggleFilter(models.Session.archived, default=False),
2117
})
2218

2319
TEST_FILTERS = FilterConfig({

0 commit comments

Comments
 (0)