argus-alm 0.12.9__py3-none-any.whl → 0.13.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- argus/client/base.py +1 -1
- argus/client/driver_matrix_tests/cli.py +2 -2
- argus/client/driver_matrix_tests/client.py +1 -1
- argus/client/generic/cli.py +2 -2
- argus/client/generic_result.py +3 -2
- argus/client/sct/client.py +3 -3
- argus/client/sirenada/client.py +1 -1
- {argus_alm-0.12.9.dist-info → argus_alm-0.13.0.dist-info}/METADATA +2 -4
- argus_alm-0.13.0.dist-info/RECORD +20 -0
- argus/backend/.gitkeep +0 -0
- argus/backend/cli.py +0 -41
- argus/backend/controller/__init__.py +0 -0
- argus/backend/controller/admin.py +0 -20
- argus/backend/controller/admin_api.py +0 -354
- argus/backend/controller/api.py +0 -529
- argus/backend/controller/auth.py +0 -67
- argus/backend/controller/client_api.py +0 -108
- argus/backend/controller/main.py +0 -274
- argus/backend/controller/notification_api.py +0 -72
- argus/backend/controller/notifications.py +0 -13
- argus/backend/controller/team.py +0 -126
- argus/backend/controller/team_ui.py +0 -18
- argus/backend/controller/testrun_api.py +0 -482
- argus/backend/controller/view_api.py +0 -162
- argus/backend/db.py +0 -100
- argus/backend/error_handlers.py +0 -21
- argus/backend/events/event_processors.py +0 -34
- argus/backend/models/__init__.py +0 -0
- argus/backend/models/result.py +0 -138
- argus/backend/models/web.py +0 -389
- argus/backend/plugins/__init__.py +0 -0
- argus/backend/plugins/core.py +0 -225
- argus/backend/plugins/driver_matrix_tests/controller.py +0 -63
- argus/backend/plugins/driver_matrix_tests/model.py +0 -421
- argus/backend/plugins/driver_matrix_tests/plugin.py +0 -22
- argus/backend/plugins/driver_matrix_tests/raw_types.py +0 -62
- argus/backend/plugins/driver_matrix_tests/service.py +0 -60
- argus/backend/plugins/driver_matrix_tests/udt.py +0 -42
- argus/backend/plugins/generic/model.py +0 -79
- argus/backend/plugins/generic/plugin.py +0 -16
- argus/backend/plugins/generic/types.py +0 -13
- argus/backend/plugins/loader.py +0 -40
- argus/backend/plugins/sct/controller.py +0 -185
- argus/backend/plugins/sct/plugin.py +0 -38
- argus/backend/plugins/sct/resource_setup.py +0 -178
- argus/backend/plugins/sct/service.py +0 -491
- argus/backend/plugins/sct/testrun.py +0 -272
- argus/backend/plugins/sct/udt.py +0 -101
- argus/backend/plugins/sirenada/model.py +0 -113
- argus/backend/plugins/sirenada/plugin.py +0 -17
- argus/backend/service/admin.py +0 -27
- argus/backend/service/argus_service.py +0 -688
- argus/backend/service/build_system_monitor.py +0 -188
- argus/backend/service/client_service.py +0 -122
- argus/backend/service/event_service.py +0 -18
- argus/backend/service/jenkins_service.py +0 -240
- argus/backend/service/notification_manager.py +0 -150
- argus/backend/service/release_manager.py +0 -230
- argus/backend/service/results_service.py +0 -317
- argus/backend/service/stats.py +0 -540
- argus/backend/service/team_manager_service.py +0 -83
- argus/backend/service/testrun.py +0 -559
- argus/backend/service/user.py +0 -307
- argus/backend/service/views.py +0 -258
- argus/backend/template_filters.py +0 -27
- argus/backend/tests/__init__.py +0 -0
- argus/backend/tests/argus_web.test.yaml +0 -39
- argus/backend/tests/conftest.py +0 -44
- argus/backend/tests/results_service/__init__.py +0 -0
- argus/backend/tests/results_service/test_best_results.py +0 -70
- argus/backend/util/common.py +0 -65
- argus/backend/util/config.py +0 -38
- argus/backend/util/encoders.py +0 -41
- argus/backend/util/logsetup.py +0 -81
- argus/backend/util/module_loaders.py +0 -30
- argus/backend/util/send_email.py +0 -91
- argus/client/generic_result_old.py +0 -143
- argus/db/.gitkeep +0 -0
- argus/db/argus_json.py +0 -14
- argus/db/cloud_types.py +0 -125
- argus/db/config.py +0 -135
- argus/db/db_types.py +0 -139
- argus/db/interface.py +0 -370
- argus/db/testrun.py +0 -740
- argus/db/utils.py +0 -15
- argus_alm-0.12.9.dist-info/RECORD +0 -96
- /argus/{backend → common}/__init__.py +0 -0
- /argus/{backend/util → common}/enums.py +0 -0
- /argus/{backend/plugins/sct/types.py → common/sct_types.py} +0 -0
- /argus/{backend/plugins/sirenada/types.py → common/sirenada_types.py} +0 -0
- {argus_alm-0.12.9.dist-info → argus_alm-0.13.0.dist-info}/LICENSE +0 -0
- {argus_alm-0.12.9.dist-info → argus_alm-0.13.0.dist-info}/WHEEL +0 -0
- {argus_alm-0.12.9.dist-info → argus_alm-0.13.0.dist-info}/entry_points.txt +0 -0
argus/backend/service/user.py
DELETED
|
@@ -1,307 +0,0 @@
|
|
|
1
|
-
from datetime import datetime
|
|
2
|
-
import functools
|
|
3
|
-
import hashlib
|
|
4
|
-
import os
|
|
5
|
-
import base64
|
|
6
|
-
from uuid import UUID
|
|
7
|
-
from time import time
|
|
8
|
-
from hashlib import sha384
|
|
9
|
-
|
|
10
|
-
from flask import current_app, flash, g, redirect, request, session, url_for
|
|
11
|
-
import requests
|
|
12
|
-
from werkzeug.security import generate_password_hash, check_password_hash
|
|
13
|
-
|
|
14
|
-
from argus.backend.db import ScyllaCluster
|
|
15
|
-
from argus.backend.error_handlers import APIException
|
|
16
|
-
from argus.backend.models.web import User, UserOauthToken, UserRoles, WebFileStorage
|
|
17
|
-
from argus.backend.util.common import FlaskView
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class UserServiceException(Exception):
|
|
21
|
-
pass
|
|
22
|
-
|
|
23
|
-
class GithubOrganizationMissingError(Exception):
|
|
24
|
-
pass
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
class UserService:
|
|
28
|
-
# pylint: disable=no-self-use
|
|
29
|
-
def __init__(self) -> None:
|
|
30
|
-
self.cluster = ScyllaCluster.get()
|
|
31
|
-
self.session = self.cluster.session
|
|
32
|
-
|
|
33
|
-
@staticmethod
|
|
34
|
-
def check_roles(roles: list[UserRoles] | UserRoles, user: User) -> bool:
|
|
35
|
-
if not user:
|
|
36
|
-
return False
|
|
37
|
-
if isinstance(roles, str):
|
|
38
|
-
return roles in user.roles
|
|
39
|
-
elif isinstance(roles, list):
|
|
40
|
-
for role in roles:
|
|
41
|
-
if role in user.roles:
|
|
42
|
-
return True
|
|
43
|
-
return False
|
|
44
|
-
|
|
45
|
-
def github_callback(self, req_code: str) -> dict | None:
|
|
46
|
-
if "gh" not in current_app.config.get("LOGIN_METHODS", []):
|
|
47
|
-
raise UserServiceException("Github Login is disabled")
|
|
48
|
-
# pylint: disable=too-many-locals
|
|
49
|
-
oauth_response = requests.post(
|
|
50
|
-
"https://github.com/login/oauth/access_token",
|
|
51
|
-
headers={
|
|
52
|
-
"Accept": "application/json",
|
|
53
|
-
},
|
|
54
|
-
params={
|
|
55
|
-
"code": req_code,
|
|
56
|
-
"client_id": current_app.config.get("GITHUB_CLIENT_ID"),
|
|
57
|
-
"client_secret": current_app.config.get("GITHUB_CLIENT_SECRET"),
|
|
58
|
-
}
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
oauth_data = oauth_response.json()
|
|
62
|
-
|
|
63
|
-
user_info = requests.get(
|
|
64
|
-
"https://api.github.com/user",
|
|
65
|
-
headers={
|
|
66
|
-
"Accept": "application/json",
|
|
67
|
-
"Authorization": f"token {oauth_data.get('access_token')}"
|
|
68
|
-
}
|
|
69
|
-
).json()
|
|
70
|
-
email_info = requests.get(
|
|
71
|
-
"https://api.github.com/user/emails",
|
|
72
|
-
headers={
|
|
73
|
-
"Accept": "application/json",
|
|
74
|
-
"Authorization": f"token {oauth_data.get('access_token')}"
|
|
75
|
-
}
|
|
76
|
-
).json()
|
|
77
|
-
|
|
78
|
-
organizations = requests.get(
|
|
79
|
-
"https://api.github.com/user/orgs",
|
|
80
|
-
headers={
|
|
81
|
-
"Accept": "application/json",
|
|
82
|
-
"Authorization": f"token {oauth_data.get('access_token')}"
|
|
83
|
-
}
|
|
84
|
-
).json()
|
|
85
|
-
|
|
86
|
-
temp_password = None
|
|
87
|
-
required_organizations = current_app.config.get("GITHUB_REQUIRED_ORGANIZATIONS")
|
|
88
|
-
if required_organizations:
|
|
89
|
-
logins = set([org["login"] for org in organizations]) # pylint: disable=consider-using-set-comprehension
|
|
90
|
-
required_organizations = set(required_organizations)
|
|
91
|
-
if len(logins.intersection(required_organizations)) == 0:
|
|
92
|
-
raise GithubOrganizationMissingError(
|
|
93
|
-
"Not a member of a required organization or missing organization scope")
|
|
94
|
-
|
|
95
|
-
try:
|
|
96
|
-
user = User.get(username=user_info.get("login"))
|
|
97
|
-
except User.DoesNotExist:
|
|
98
|
-
user = User()
|
|
99
|
-
user.username = user_info.get("login")
|
|
100
|
-
# pick only scylladb.com emails
|
|
101
|
-
scylla_email = next(iter([email.get("email") for email in email_info if email.get("email").endswith("@scylladb.com")]), None)
|
|
102
|
-
primary_email = next(iter([email.get("email") for email in email_info if email.get("primary") and email.get("verified")]), None)
|
|
103
|
-
user.email = scylla_email or primary_email
|
|
104
|
-
user.full_name = user_info.get("name", user_info.get("login"))
|
|
105
|
-
user.registration_date = datetime.utcnow()
|
|
106
|
-
user.roles = ["ROLE_USER"]
|
|
107
|
-
temp_password = base64.encodebytes(
|
|
108
|
-
os.urandom(48)).decode("ascii").strip()
|
|
109
|
-
user.password = generate_password_hash(temp_password)
|
|
110
|
-
|
|
111
|
-
avatar_url: str = user_info.get("avatar_url")
|
|
112
|
-
avatar = requests.get(avatar_url).content
|
|
113
|
-
avatar_name = avatar_url.split("/")[-1]
|
|
114
|
-
filename, filepath = self.save_profile_picture_to_disk(avatar_name, avatar, user.username)
|
|
115
|
-
|
|
116
|
-
web_file = WebFileStorage()
|
|
117
|
-
web_file.filename = filename
|
|
118
|
-
web_file.filepath = filepath
|
|
119
|
-
web_file.save()
|
|
120
|
-
user.picture_id = web_file.id
|
|
121
|
-
user.save()
|
|
122
|
-
|
|
123
|
-
try:
|
|
124
|
-
tokens = list(UserOauthToken.filter(user_id=user.id).all())
|
|
125
|
-
github_token = [
|
|
126
|
-
token for token in tokens if token["kind"] == "github"][0]
|
|
127
|
-
github_token.token = oauth_data.get('access_token')
|
|
128
|
-
github_token.save()
|
|
129
|
-
except (UserOauthToken.DoesNotExist, IndexError):
|
|
130
|
-
github_token = UserOauthToken()
|
|
131
|
-
github_token.kind = "github"
|
|
132
|
-
github_token.user_id = user.id
|
|
133
|
-
github_token.token = oauth_data.get('access_token')
|
|
134
|
-
github_token.save()
|
|
135
|
-
|
|
136
|
-
redirect_target = session.get("redirect_target")
|
|
137
|
-
session.clear()
|
|
138
|
-
session["user_id"] = str(user.id)
|
|
139
|
-
session["redirect_target"] = redirect_target
|
|
140
|
-
if temp_password:
|
|
141
|
-
return {
|
|
142
|
-
"password": temp_password,
|
|
143
|
-
"first_login": True
|
|
144
|
-
}
|
|
145
|
-
return None
|
|
146
|
-
|
|
147
|
-
def get_users(self) -> dict:
|
|
148
|
-
users = User.all()
|
|
149
|
-
return {str(user.id): user.to_json() for user in users}
|
|
150
|
-
|
|
151
|
-
def get_users_privileged(self) -> dict:
|
|
152
|
-
users = User.all()
|
|
153
|
-
users = {str(user.id): dict(user.items()) for user in users}
|
|
154
|
-
for user in users.values():
|
|
155
|
-
user.pop("password")
|
|
156
|
-
user.pop("api_token")
|
|
157
|
-
|
|
158
|
-
return users
|
|
159
|
-
|
|
160
|
-
def generate_token(self, user: User):
|
|
161
|
-
token_digest = f"{user.username}-{int(time())}-{base64.encodebytes(os.urandom(128)).decode(encoding='utf-8')}"
|
|
162
|
-
new_token = base64.encodebytes(sha384(token_digest.encode(encoding="utf-8")
|
|
163
|
-
).digest()).decode(encoding="utf-8").strip()
|
|
164
|
-
user.api_token = new_token
|
|
165
|
-
user.save()
|
|
166
|
-
return new_token
|
|
167
|
-
|
|
168
|
-
def update_email(self, user: User, new_email: str):
|
|
169
|
-
user.email = new_email
|
|
170
|
-
user.save()
|
|
171
|
-
|
|
172
|
-
return True
|
|
173
|
-
|
|
174
|
-
def toggle_admin(self, user_id: str):
|
|
175
|
-
user: User = User.get(id=user_id)
|
|
176
|
-
|
|
177
|
-
if user.id == g.user.id:
|
|
178
|
-
raise UserServiceException("Cannot toggle admin role from yourself.")
|
|
179
|
-
|
|
180
|
-
is_admin = UserService.check_roles(UserRoles.Admin, user)
|
|
181
|
-
|
|
182
|
-
if is_admin:
|
|
183
|
-
user.roles.remove(UserRoles.Admin)
|
|
184
|
-
else:
|
|
185
|
-
user.set_as_admin()
|
|
186
|
-
|
|
187
|
-
user.save()
|
|
188
|
-
return True
|
|
189
|
-
|
|
190
|
-
def delete_user(self, user_id: str):
|
|
191
|
-
user: User = User.get(id=user_id)
|
|
192
|
-
if user.id == g.user.id:
|
|
193
|
-
raise UserServiceException("Cannot delete user that you are logged in as.")
|
|
194
|
-
|
|
195
|
-
if user.is_admin():
|
|
196
|
-
raise UserServiceException("Cannot delete admin users. Unset admin flag before deleting")
|
|
197
|
-
|
|
198
|
-
user.delete()
|
|
199
|
-
|
|
200
|
-
return True
|
|
201
|
-
|
|
202
|
-
def update_password(self, user: User, old_password: str, new_password: str, force = False):
|
|
203
|
-
if not check_password_hash(user.password, old_password) and not force:
|
|
204
|
-
raise UserServiceException("Incorrect old password")
|
|
205
|
-
|
|
206
|
-
if not new_password:
|
|
207
|
-
raise UserServiceException("Empty new password")
|
|
208
|
-
|
|
209
|
-
if len(new_password) < 5:
|
|
210
|
-
raise UserServiceException("New password is too short")
|
|
211
|
-
|
|
212
|
-
user.password = generate_password_hash(new_password)
|
|
213
|
-
user.save()
|
|
214
|
-
|
|
215
|
-
return True
|
|
216
|
-
|
|
217
|
-
def update_name(self, user: User, new_name: str):
|
|
218
|
-
user.full_name = new_name
|
|
219
|
-
user.save()
|
|
220
|
-
|
|
221
|
-
def save_profile_picture_to_disk(self, original_filename: str, filedata: bytes, suffix: str):
|
|
222
|
-
filename_fragment = hashlib.sha256(os.urandom(64)).hexdigest()[:10]
|
|
223
|
-
filename = f"profile_{suffix}_{filename_fragment}"
|
|
224
|
-
filepath = f"storage/profile_pictures/{filename}"
|
|
225
|
-
with open(filepath, "wb") as file:
|
|
226
|
-
file.write(filedata)
|
|
227
|
-
|
|
228
|
-
return original_filename, filepath
|
|
229
|
-
|
|
230
|
-
def update_profile_picture(self, filename: str, filepath: str):
|
|
231
|
-
web_file = WebFileStorage()
|
|
232
|
-
web_file.filename = filename
|
|
233
|
-
web_file.filepath = filepath
|
|
234
|
-
web_file.save()
|
|
235
|
-
|
|
236
|
-
try:
|
|
237
|
-
if old_picture_id := g.user.picture_id:
|
|
238
|
-
old_file = WebFileStorage.get(id=old_picture_id)
|
|
239
|
-
os.unlink(old_file.filepath)
|
|
240
|
-
old_file.delete()
|
|
241
|
-
except Exception as exc: # pylint: disable=broad-except
|
|
242
|
-
print(exc)
|
|
243
|
-
|
|
244
|
-
g.user.picture_id = web_file.id
|
|
245
|
-
g.user.save()
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
def login_required(view: FlaskView):
|
|
249
|
-
@functools.wraps(view)
|
|
250
|
-
def wrapped_view(*args, **kwargs):
|
|
251
|
-
if g.user is None and not getattr(view, "api_view", False):
|
|
252
|
-
flash(message='Unauthorized, please login', category='error')
|
|
253
|
-
session["redirect_target"] = request.full_path
|
|
254
|
-
return redirect(url_for('auth.login'))
|
|
255
|
-
elif g.user is None and getattr(view, "api_view", True):
|
|
256
|
-
return {
|
|
257
|
-
"status": "error",
|
|
258
|
-
"message": "Authorization required"
|
|
259
|
-
}, 403
|
|
260
|
-
|
|
261
|
-
return view(*args, **kwargs)
|
|
262
|
-
|
|
263
|
-
return wrapped_view
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
def api_login_required(view: FlaskView):
|
|
267
|
-
view.api_view = True
|
|
268
|
-
return login_required(view)
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
def check_roles(needed_roles: list[str] | str = None):
|
|
272
|
-
def inner(view: FlaskView):
|
|
273
|
-
@functools.wraps(view)
|
|
274
|
-
def wrapped_view(*args, **kwargs):
|
|
275
|
-
if not UserService.check_roles(needed_roles, g.user):
|
|
276
|
-
flash(message='Not authorized to access this area', category='error')
|
|
277
|
-
return redirect(url_for('main.home'))
|
|
278
|
-
|
|
279
|
-
return view(*args, **kwargs)
|
|
280
|
-
|
|
281
|
-
return wrapped_view
|
|
282
|
-
return inner
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
def load_logged_in_user():
|
|
286
|
-
user_id = session.get('user_id')
|
|
287
|
-
auth_header = request.headers.get("Authorization")
|
|
288
|
-
|
|
289
|
-
if user_id:
|
|
290
|
-
try:
|
|
291
|
-
g.user = User.get(id=UUID(user_id)) # pylint: disable=assigning-non-slot
|
|
292
|
-
return
|
|
293
|
-
except User.DoesNotExist:
|
|
294
|
-
session.clear()
|
|
295
|
-
|
|
296
|
-
if auth_header:
|
|
297
|
-
try:
|
|
298
|
-
auth_schema, *auth_data = auth_header.split()
|
|
299
|
-
if auth_schema == "token":
|
|
300
|
-
token = auth_data[0]
|
|
301
|
-
g.user = User.get(api_token=token)
|
|
302
|
-
return
|
|
303
|
-
except IndexError as exception:
|
|
304
|
-
raise APIException("Malformed authorization header") from exception
|
|
305
|
-
except User.DoesNotExist as exception:
|
|
306
|
-
raise APIException("User not found for supplied token") from exception
|
|
307
|
-
g.user = None # pylint: disable=assigning-non-slot
|
argus/backend/service/views.py
DELETED
|
@@ -1,258 +0,0 @@
|
|
|
1
|
-
import datetime
|
|
2
|
-
import logging
|
|
3
|
-
import re
|
|
4
|
-
from functools import partial, reduce
|
|
5
|
-
from typing import Any, Callable, TypedDict
|
|
6
|
-
from urllib.parse import unquote
|
|
7
|
-
from uuid import UUID
|
|
8
|
-
|
|
9
|
-
from cassandra.cqlengine.models import Model
|
|
10
|
-
from argus.backend.models.web import ArgusGroup, ArgusRelease, ArgusTest, ArgusUserView, User
|
|
11
|
-
from argus.backend.plugins.loader import all_plugin_models
|
|
12
|
-
from argus.backend.util.common import chunk, current_user
|
|
13
|
-
|
|
14
|
-
LOGGER = logging.getLogger(__name__)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class UserViewException(Exception):
|
|
18
|
-
pass
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class ViewUpdateRequest(TypedDict):
|
|
22
|
-
name: str
|
|
23
|
-
description: str
|
|
24
|
-
display_name: str
|
|
25
|
-
tests: list[str]
|
|
26
|
-
widget_settings: str
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class UserViewService:
|
|
30
|
-
ADD_ALL_ID = UUID("db6f33b2-660b-4639-ba7f-79725ef96616")
|
|
31
|
-
def create_view(self, name: str, items: list[str], widget_settings: str, description: str = None, display_name: str = None) -> ArgusUserView:
|
|
32
|
-
try:
|
|
33
|
-
name_check = ArgusUserView.get(name=name)
|
|
34
|
-
raise UserViewException(f"View with name {name} already exists: {name_check.id}", name, name_check, name_check.id)
|
|
35
|
-
except ArgusUserView.DoesNotExist:
|
|
36
|
-
pass
|
|
37
|
-
view = ArgusUserView()
|
|
38
|
-
view.name = name
|
|
39
|
-
view.display_name = display_name or name
|
|
40
|
-
view.description = description
|
|
41
|
-
view.widget_settings = widget_settings
|
|
42
|
-
view.tests = []
|
|
43
|
-
for entity in items:
|
|
44
|
-
entity_type, entity_id = entity.split(":")
|
|
45
|
-
match (entity_type):
|
|
46
|
-
case "release":
|
|
47
|
-
view.tests.extend(t.id for t in ArgusTest.filter(release_id=entity_id).all())
|
|
48
|
-
view.release_ids.append(entity_id)
|
|
49
|
-
case "group":
|
|
50
|
-
view.tests.extend(t.id for t in ArgusTest.filter(group_id=entity_id).all())
|
|
51
|
-
view.group_ids.append(entity_id)
|
|
52
|
-
case "test":
|
|
53
|
-
view.tests.append(entity_id)
|
|
54
|
-
view.user_id = current_user().id
|
|
55
|
-
|
|
56
|
-
view.save()
|
|
57
|
-
return view
|
|
58
|
-
|
|
59
|
-
@staticmethod
|
|
60
|
-
def index_mapper(item: Model, type = "test"):
|
|
61
|
-
mapped = dict(item)
|
|
62
|
-
mapped["type"] = type
|
|
63
|
-
return mapped
|
|
64
|
-
|
|
65
|
-
def test_lookup(self, query: str):
|
|
66
|
-
def check_visibility(entity: dict):
|
|
67
|
-
if not entity["enabled"]:
|
|
68
|
-
return False
|
|
69
|
-
if entity.get("group") and not entity["group"]["enabled"]:
|
|
70
|
-
return False
|
|
71
|
-
if entity.get("release") and not entity["release"]["enabled"]:
|
|
72
|
-
return False
|
|
73
|
-
return True
|
|
74
|
-
|
|
75
|
-
def facet_extraction(query: str) -> str:
|
|
76
|
-
extractor = re.compile(r"(?:(?P<name>(?:release|group|type)):(?P<value>\"?[\w\d\.\-]*\"?))")
|
|
77
|
-
facets = re.findall(extractor, query)
|
|
78
|
-
|
|
79
|
-
return (re.sub(extractor, "", query).strip(), facets)
|
|
80
|
-
|
|
81
|
-
def type_facet_filter(item: dict, key: str, facet_query: str):
|
|
82
|
-
entity_type: str = item[key]
|
|
83
|
-
return facet_query.lower() == entity_type
|
|
84
|
-
|
|
85
|
-
def facet_filter(item: dict, key: str, facet_query: str):
|
|
86
|
-
if entity := item.get(key):
|
|
87
|
-
name: str = entity.get("pretty_name") or entity.get("name")
|
|
88
|
-
return facet_query.lower() in name.lower() if name else False
|
|
89
|
-
return False
|
|
90
|
-
|
|
91
|
-
def facet_wrapper(query_func: Callable[[dict], bool], facet_query: str, facet_type: str) -> bool:
|
|
92
|
-
def inner(item: dict, query: str):
|
|
93
|
-
return query_func(item, query) and facet_funcs[facet_type](item, facet_type, facet_query)
|
|
94
|
-
return inner
|
|
95
|
-
|
|
96
|
-
facet_funcs = {
|
|
97
|
-
"type": type_facet_filter,
|
|
98
|
-
"release": facet_filter,
|
|
99
|
-
"group": facet_filter,
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
def index_searcher(item, query: str):
|
|
103
|
-
name: str = item["pretty_name"] or item["name"]
|
|
104
|
-
return unquote(query).lower() in name.lower() if query else True
|
|
105
|
-
|
|
106
|
-
text_query, facets = facet_extraction(query)
|
|
107
|
-
search_func = index_searcher
|
|
108
|
-
for facet, value in facets:
|
|
109
|
-
if facet in facet_funcs.keys():
|
|
110
|
-
search_func = facet_wrapper(query_func=search_func, facet_query=value, facet_type=facet)
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
all_tests = ArgusTest.objects().limit(None)
|
|
114
|
-
all_releases = ArgusRelease.objects().limit(None)
|
|
115
|
-
all_groups = ArgusGroup.objects().limit(None)
|
|
116
|
-
release_by_id = {release.id: partial(self.index_mapper, type="release")(release) for release in all_releases}
|
|
117
|
-
group_by_id = {group.id: partial(self.index_mapper, type="group")(group) for group in all_groups}
|
|
118
|
-
index = [self.index_mapper(t) for t in all_tests]
|
|
119
|
-
index = [*release_by_id.values(), *group_by_id.values(), *index]
|
|
120
|
-
for item in index:
|
|
121
|
-
item["group"] = group_by_id.get(item.get("group_id"))
|
|
122
|
-
item["release"] = release_by_id.get(item.get("release_id"))
|
|
123
|
-
|
|
124
|
-
results = filter(partial(search_func, query=text_query), index)
|
|
125
|
-
|
|
126
|
-
return [{ "id": self.ADD_ALL_ID, "name": "Add all...", "type": "special" }, *list(res for res in results if check_visibility(res))]
|
|
127
|
-
|
|
128
|
-
def update_view(self, view_id: str | UUID, update_data: ViewUpdateRequest) -> bool:
|
|
129
|
-
view: ArgusUserView = ArgusUserView.get(id=view_id)
|
|
130
|
-
if view.user_id != current_user().id and not current_user().is_admin():
|
|
131
|
-
raise UserViewException("Unable to modify other users' views")
|
|
132
|
-
for key in ["user_id", "id"]:
|
|
133
|
-
update_data.pop(key, None)
|
|
134
|
-
items = update_data.pop("items")
|
|
135
|
-
for k, value in update_data.items():
|
|
136
|
-
view[k] = value
|
|
137
|
-
view.tests = []
|
|
138
|
-
view.release_ids = []
|
|
139
|
-
view.group_ids = []
|
|
140
|
-
for entity in items:
|
|
141
|
-
entity_type, entity_id = entity.split(":")
|
|
142
|
-
match (entity_type):
|
|
143
|
-
case "release":
|
|
144
|
-
view.tests.extend(t.id for t in ArgusTest.filter(release_id=entity_id).all())
|
|
145
|
-
view.release_ids.append(entity_id)
|
|
146
|
-
case "group":
|
|
147
|
-
view.tests.extend(t.id for t in ArgusTest.filter(group_id=entity_id).all())
|
|
148
|
-
view.group_ids.append(entity_id)
|
|
149
|
-
case "test":
|
|
150
|
-
view.tests.append(entity_id)
|
|
151
|
-
view.last_updated = datetime.datetime.utcnow()
|
|
152
|
-
view.save()
|
|
153
|
-
return True
|
|
154
|
-
|
|
155
|
-
def delete_view(self, view_id: str | UUID) -> bool:
|
|
156
|
-
view = ArgusUserView.get(id=view_id)
|
|
157
|
-
if view.user_id != current_user().id and not current_user().is_admin():
|
|
158
|
-
raise UserViewException("Unable to modify other users' views")
|
|
159
|
-
view.delete()
|
|
160
|
-
|
|
161
|
-
return True
|
|
162
|
-
|
|
163
|
-
def get_view(self, view_id: str | UUID) -> ArgusUserView:
|
|
164
|
-
view: ArgusUserView = ArgusUserView.get(id=view_id)
|
|
165
|
-
if datetime.datetime.utcnow() - (view.last_updated or datetime.datetime.fromtimestamp(0)) > datetime.timedelta(hours=1):
|
|
166
|
-
self.refresh_stale_view(view)
|
|
167
|
-
return view
|
|
168
|
-
|
|
169
|
-
def get_view_by_name(self, view_name: str) -> ArgusUserView:
|
|
170
|
-
view: ArgusUserView = ArgusUserView.get(name=view_name)
|
|
171
|
-
if datetime.datetime.utcnow() - (view.last_updated or datetime.datetime.fromtimestamp(0)) > datetime.timedelta(hours=1):
|
|
172
|
-
self.refresh_stale_view(view)
|
|
173
|
-
return view
|
|
174
|
-
|
|
175
|
-
def get_all_views(self, user: User | None = None) -> list[ArgusUserView]:
|
|
176
|
-
if user:
|
|
177
|
-
return list(ArgusUserView.filter(user_id=user.id).all())
|
|
178
|
-
return list(ArgusUserView.filter().all())
|
|
179
|
-
|
|
180
|
-
def resolve_view_tests(self, view_id: str | UUID) -> list[ArgusTest]:
|
|
181
|
-
view = ArgusUserView.get(id=view_id)
|
|
182
|
-
return self.resolve_tests_by_id(view.tests)
|
|
183
|
-
|
|
184
|
-
def resolve_tests_by_id(self, test_ids: list[str | UUID]) -> list[ArgusTest]:
|
|
185
|
-
tests = []
|
|
186
|
-
for batch in chunk(test_ids):
|
|
187
|
-
tests.extend(ArgusTest.filter(id__in=batch).all())
|
|
188
|
-
|
|
189
|
-
return tests
|
|
190
|
-
|
|
191
|
-
def batch_resolve_entity(self, entity: Model, param_name: str, entity_ids: list[UUID]) -> list[Model]:
|
|
192
|
-
result = []
|
|
193
|
-
for batch in chunk(entity_ids):
|
|
194
|
-
result.extend(entity.filter(**{f"{param_name}__in": batch}).allow_filtering().all())
|
|
195
|
-
return result
|
|
196
|
-
|
|
197
|
-
def refresh_stale_view(self, view: ArgusUserView):
|
|
198
|
-
view.tests = [test.id for test in self.resolve_view_tests(view.id)]
|
|
199
|
-
all_tests = set(view.tests)
|
|
200
|
-
all_tests.update(test.id for test in self.batch_resolve_entity(ArgusTest, "group_id", view.group_ids))
|
|
201
|
-
all_tests.update(test.id for test in self.batch_resolve_entity(ArgusTest, "release_id", view.release_ids))
|
|
202
|
-
view.tests = list(all_tests)
|
|
203
|
-
view.last_updated = datetime.datetime.utcnow()
|
|
204
|
-
view.save()
|
|
205
|
-
|
|
206
|
-
return view
|
|
207
|
-
|
|
208
|
-
def resolve_releases_for_tests(self, tests: list[ArgusTest]):
|
|
209
|
-
releases = []
|
|
210
|
-
unique_release_ids = reduce(lambda releases, test: releases.add(test.release_id) or releases, tests, set())
|
|
211
|
-
for batch in chunk(unique_release_ids):
|
|
212
|
-
releases.extend(ArgusRelease.filter(id__in=batch).all())
|
|
213
|
-
|
|
214
|
-
return releases
|
|
215
|
-
|
|
216
|
-
def resolve_groups_for_tests(self, tests: list[ArgusTest]):
|
|
217
|
-
releases = []
|
|
218
|
-
unique_release_ids = reduce(lambda groups, test: groups.add(test.group_id) or groups, tests, set())
|
|
219
|
-
for batch in chunk(unique_release_ids):
|
|
220
|
-
releases.extend(ArgusGroup.filter(id__in=batch).all())
|
|
221
|
-
|
|
222
|
-
return releases
|
|
223
|
-
|
|
224
|
-
def get_versions_for_view(self, view_id: str | UUID) -> list[str]:
|
|
225
|
-
tests = self.resolve_view_tests(view_id)
|
|
226
|
-
unique_versions = {ver for plugin in all_plugin_models()
|
|
227
|
-
for ver in plugin.get_distinct_versions_for_view(tests=tests)}
|
|
228
|
-
|
|
229
|
-
return sorted(list(unique_versions), reverse=True)
|
|
230
|
-
|
|
231
|
-
def resolve_view_for_edit(self, view_id: str | UUID) -> dict:
|
|
232
|
-
view: ArgusUserView = ArgusUserView.get(id=view_id)
|
|
233
|
-
resolved = dict(view)
|
|
234
|
-
view_groups = self.batch_resolve_entity(ArgusGroup, "id", view.group_ids)
|
|
235
|
-
view_releases = self.batch_resolve_entity(ArgusRelease, "id", view.release_ids)
|
|
236
|
-
view_tests = self.resolve_view_tests(view.id)
|
|
237
|
-
all_groups = { group.id: partial(self.index_mapper, type="group")(group) for group in self.resolve_releases_for_tests(view_tests) }
|
|
238
|
-
all_releases ={ release.id: partial(self.index_mapper, type="release")(release) for release in self.resolve_releases_for_tests(view_tests) }
|
|
239
|
-
entities_by_id = {
|
|
240
|
-
entity.id: partial(self.index_mapper, type="release" if isinstance(entity, ArgusRelease) else "group")(entity)
|
|
241
|
-
for container in [view_releases, view_groups]
|
|
242
|
-
for entity in container
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
items = []
|
|
246
|
-
for test in view_tests:
|
|
247
|
-
if not (entities_by_id.get(test.group_id) or entities_by_id.get(test.release_id)):
|
|
248
|
-
item = dict(test)
|
|
249
|
-
item["type"] = "test"
|
|
250
|
-
items.append(item)
|
|
251
|
-
|
|
252
|
-
items = [*entities_by_id.values(), *items]
|
|
253
|
-
for entity in items:
|
|
254
|
-
entity["group"] = all_groups.get(entity.get("group_id"), {}).get("pretty_name") or all_groups.get(entity.get("group_id"), {}).get("name")
|
|
255
|
-
entity["release"] = all_releases.get(entity.get("release_id"), {}).get("pretty_name") or all_releases.get(entity.get("release_id"), {}).get("name")
|
|
256
|
-
|
|
257
|
-
resolved["items"] = items
|
|
258
|
-
return resolved
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
from functools import partial
|
|
2
|
-
from datetime import datetime
|
|
3
|
-
|
|
4
|
-
from argus.backend.models.web import User
|
|
5
|
-
from argus.backend.util.module_loaders import is_filter, export_functions
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
export_filters = partial(export_functions, module_name=__name__, attr="is_filter")
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
@is_filter("from_timestamp")
|
|
12
|
-
def from_timestamp_filter(timestamp: int):
|
|
13
|
-
return datetime.utcfromtimestamp(timestamp)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
@is_filter("safe_user")
|
|
17
|
-
def safe_user(user: User):
|
|
18
|
-
user_dict = dict(user.items())
|
|
19
|
-
del user_dict["password"]
|
|
20
|
-
return user_dict
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
@is_filter("formatted_date")
|
|
24
|
-
def formatted_date(date: datetime | None):
|
|
25
|
-
if date:
|
|
26
|
-
return date.strftime("%d/%m/%Y %H:%M:%S")
|
|
27
|
-
return "#unknown"
|
argus/backend/tests/__init__.py
DELETED
|
File without changes
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
# BASE URL FOR ARGUS APPLICATION
|
|
2
|
-
BASE_URL: "https://argus.scylladb.com"
|
|
3
|
-
# Main DB Cluster contact points
|
|
4
|
-
SCYLLA_CONTACT_POINTS:
|
|
5
|
-
- 172.18.0.2
|
|
6
|
-
# Username
|
|
7
|
-
SCYLLA_USERNAME: cassandra
|
|
8
|
-
# Password
|
|
9
|
-
SCYLLA_PASSWORD: cassandra
|
|
10
|
-
# Default keyspace (can be created using 'create-keyspace' command with RF set to the number of contact points)
|
|
11
|
-
SCYLLA_KEYSPACE_NAME: test_argus
|
|
12
|
-
# Replication factor used - if set, will override contact_points as amount of nodes for replication
|
|
13
|
-
# SCYLLA_REPLICATION_FACTOR: 3
|
|
14
|
-
LOGIN_METHODS:
|
|
15
|
-
- gh
|
|
16
|
-
# Application log level
|
|
17
|
-
APP_LOG_LEVEL: INFO
|
|
18
|
-
# Secret key used to match session data
|
|
19
|
-
SECRET_KEY: MUSTBEUNIQUE1
|
|
20
|
-
# Client ID of a github oauth application
|
|
21
|
-
GITHUB_CLIENT_ID: not_set
|
|
22
|
-
# Scopes used for Github Application:
|
|
23
|
-
# GITHUB_SCOPES: 'user:email read:user read:org repo'
|
|
24
|
-
# Client secret of a github oauth application
|
|
25
|
-
GITHUB_CLIENT_SECRET: not_set
|
|
26
|
-
# Github personal access token
|
|
27
|
-
GITHUB_ACCESS_TOKEN: unknown
|
|
28
|
-
# List of required organization names (Comment out to disable organization requirement)
|
|
29
|
-
GITHUB_REQUIRED_ORGANIZATIONS:
|
|
30
|
-
# at least one is required for user to successfully authenticate
|
|
31
|
-
BUILD_SYSTEM_FILTERED_PREFIXES:
|
|
32
|
-
- prefixToExclude
|
|
33
|
-
JENKINS_URL: https://jenkins.scylladb.com
|
|
34
|
-
JENKINS_USER: not_set
|
|
35
|
-
JENKINS_API_TOKEN_NAME: not_set
|
|
36
|
-
JENKINS_API_TOKEN: not_set
|
|
37
|
-
JENKINS_MONITORED_RELEASES:
|
|
38
|
-
- not_set
|
|
39
|
-
|
argus/backend/tests/conftest.py
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
from unittest.mock import patch
|
|
4
|
-
|
|
5
|
-
from _pytest.fixtures import fixture
|
|
6
|
-
|
|
7
|
-
from argus.backend.cli import sync_models
|
|
8
|
-
from argus.backend.db import ScyllaCluster
|
|
9
|
-
from argus.backend.service.client_service import ClientService
|
|
10
|
-
from argus.backend.service.release_manager import ReleaseManagerService
|
|
11
|
-
from argus.backend.util.config import Config
|
|
12
|
-
import logging
|
|
13
|
-
os.environ['CQLENG_ALLOW_SCHEMA_MANAGEMENT'] = '1'
|
|
14
|
-
logging.getLogger('cassandra').setLevel(logging.WARNING)
|
|
15
|
-
logging.getLogger('cassandra.connection').setLevel(logging.WARNING)
|
|
16
|
-
logging.getLogger('cassandra.pool').setLevel(logging.WARNING)
|
|
17
|
-
logging.getLogger('cassandra.cluster').setLevel(logging.WARNING)
|
|
18
|
-
|
|
19
|
-
def truncate_all_tables(session):
|
|
20
|
-
for table in session.cluster.metadata.keyspaces[session.keyspace].tables:
|
|
21
|
-
session.execute(f"TRUNCATE {table}")
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
@fixture(autouse=True, scope='session')
|
|
25
|
-
def argus_db():
|
|
26
|
-
Config.CONFIG_PATHS = [Path(__file__).parent / "argus_web.test.yaml"]
|
|
27
|
-
config = Config.load_yaml_config()
|
|
28
|
-
database = ScyllaCluster.get(config)
|
|
29
|
-
session = database.cluster.connect(keyspace=config["SCYLLA_KEYSPACE_NAME"])
|
|
30
|
-
ScyllaCluster.get_session = lambda: session # monkey patching to escape need for flask app context
|
|
31
|
-
|
|
32
|
-
sync_models()
|
|
33
|
-
truncate_all_tables(database.session)
|
|
34
|
-
yield database
|
|
35
|
-
database.shutdown()
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
@fixture(autouse=True, scope='session')
|
|
39
|
-
def release_manager_service(argus_db):
|
|
40
|
-
return ReleaseManagerService()
|
|
41
|
-
|
|
42
|
-
@fixture(autouse=True, scope='session')
|
|
43
|
-
def client_service(argus_db):
|
|
44
|
-
return ClientService()
|
|
File without changes
|