argus-alm 0.12.10__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/sct/client.py +3 -3
- argus/client/sirenada/client.py +1 -1
- {argus_alm-0.12.10.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.10.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.10.dist-info → argus_alm-0.13.0.dist-info}/LICENSE +0 -0
- {argus_alm-0.12.10.dist-info → argus_alm-0.13.0.dist-info}/WHEEL +0 -0
- {argus_alm-0.12.10.dist-info → argus_alm-0.13.0.dist-info}/entry_points.txt +0 -0
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
from abc import ABC, abstractmethod
|
|
3
|
-
import jenkins
|
|
4
|
-
import click
|
|
5
|
-
import re
|
|
6
|
-
from flask import current_app
|
|
7
|
-
from flask.cli import with_appcontext
|
|
8
|
-
|
|
9
|
-
from argus.backend.db import ScyllaCluster
|
|
10
|
-
from argus.backend.models.web import ArgusRelease, ArgusGroup, ArgusTest, ArgusTestException
|
|
11
|
-
from argus.backend.service.release_manager import ReleaseManagerService
|
|
12
|
-
|
|
13
|
-
LOGGER = logging.getLogger(__name__)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class ArgusTestsMonitor(ABC):
|
|
17
|
-
BUILD_SYSTEM_FILTERED_PREFIXES = [
|
|
18
|
-
|
|
19
|
-
]
|
|
20
|
-
|
|
21
|
-
def __init__(self) -> None:
|
|
22
|
-
self._cluster = ScyllaCluster.get()
|
|
23
|
-
self._existing_releases = list(ArgusRelease.objects().limit(None))
|
|
24
|
-
self._existing_groups = list(ArgusGroup.objects().limit(None))
|
|
25
|
-
self._existing_tests = list(ArgusTest.objects().limit(None))
|
|
26
|
-
self._filtered_groups: list[str] = self.BUILD_SYSTEM_FILTERED_PREFIXES
|
|
27
|
-
|
|
28
|
-
def create_release(self, release_name):
|
|
29
|
-
# pylint: disable=no-self-use
|
|
30
|
-
release = ArgusRelease()
|
|
31
|
-
release.name = release_name
|
|
32
|
-
release.save()
|
|
33
|
-
|
|
34
|
-
return release
|
|
35
|
-
|
|
36
|
-
def create_group(self, release: ArgusRelease, group_name: str, build_id: str, group_pretty_name: str | None = None):
|
|
37
|
-
# pylint: disable=no-self-use
|
|
38
|
-
group = ArgusGroup()
|
|
39
|
-
group.release_id = release.id
|
|
40
|
-
group.name = group_name
|
|
41
|
-
group.build_system_id = build_id
|
|
42
|
-
if group_pretty_name:
|
|
43
|
-
group.pretty_name = group_pretty_name
|
|
44
|
-
group.save()
|
|
45
|
-
|
|
46
|
-
return group
|
|
47
|
-
|
|
48
|
-
def create_test(self, release: ArgusRelease, group: ArgusGroup,
|
|
49
|
-
test_name: str, build_id: str, build_url: str) -> ArgusTest:
|
|
50
|
-
# pylint: disable=no-self-use
|
|
51
|
-
test = ArgusTest()
|
|
52
|
-
test.name = test_name
|
|
53
|
-
test.group_id = group.id
|
|
54
|
-
test.release_id = release.id
|
|
55
|
-
test.build_system_id = build_id
|
|
56
|
-
test.build_system_url = build_url
|
|
57
|
-
test.validate_build_system_id()
|
|
58
|
-
test.save()
|
|
59
|
-
ReleaseManagerService().move_test_runs(test)
|
|
60
|
-
|
|
61
|
-
return test
|
|
62
|
-
|
|
63
|
-
@abstractmethod
|
|
64
|
-
def collect(self):
|
|
65
|
-
raise NotImplementedError()
|
|
66
|
-
|
|
67
|
-
def check_filter(self, group_name: str) -> bool:
|
|
68
|
-
for prefix in self._filtered_groups:
|
|
69
|
-
if prefix.lower() in group_name.lower():
|
|
70
|
-
return False
|
|
71
|
-
|
|
72
|
-
return True
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
class JenkinsMonitor(ArgusTestsMonitor):
|
|
76
|
-
|
|
77
|
-
BUILD_SYSTEM_FILTERED_PREFIXES = [
|
|
78
|
-
"releng",
|
|
79
|
-
]
|
|
80
|
-
|
|
81
|
-
JENKINS_MONITORED_RELEASES = [
|
|
82
|
-
r"^scylla-master$",
|
|
83
|
-
r"^scylla-staging$",
|
|
84
|
-
r"^scylla-\d+\.\d+$",
|
|
85
|
-
r"^manager-3.\d+$",
|
|
86
|
-
r"^scylla-operator/operator-master$",
|
|
87
|
-
r"^scylla-operator/operator-\d+.\d+$",
|
|
88
|
-
r"^scylla-enterprise$",
|
|
89
|
-
r"^enterprise-20\d{2}\.\d+$",
|
|
90
|
-
r"^siren-tests$",
|
|
91
|
-
]
|
|
92
|
-
|
|
93
|
-
def __init__(self) -> None:
|
|
94
|
-
super().__init__()
|
|
95
|
-
self._jenkins = jenkins.Jenkins(url=current_app.config["JENKINS_URL"],
|
|
96
|
-
username=current_app.config["JENKINS_USER"],
|
|
97
|
-
password=current_app.config["JENKINS_API_TOKEN"])
|
|
98
|
-
self._monitored_releases = self.JENKINS_MONITORED_RELEASES
|
|
99
|
-
|
|
100
|
-
def _check_release_name(self, release_name: str):
|
|
101
|
-
return any(re.match(pattern, release_name, re.IGNORECASE) for pattern in self._monitored_releases)
|
|
102
|
-
|
|
103
|
-
def collect(self):
|
|
104
|
-
click.echo("Collecting new tests from jenkins")
|
|
105
|
-
all_jobs = self._jenkins.get_all_jobs()
|
|
106
|
-
all_monitored_folders = [job for job in all_jobs if self._check_release_name(job["fullname"])]
|
|
107
|
-
LOGGER.info("Will collect %s", [f["fullname"] for f in all_monitored_folders])
|
|
108
|
-
|
|
109
|
-
for release in all_monitored_folders:
|
|
110
|
-
LOGGER.info("Processing release %s", release["name"])
|
|
111
|
-
try:
|
|
112
|
-
saved_release = ArgusRelease.get(name=release["name"])
|
|
113
|
-
LOGGER.info("Release %s exists", release["name"])
|
|
114
|
-
except ArgusRelease.DoesNotExist:
|
|
115
|
-
LOGGER.warning("Release %s does not exist, creating...", release["name"])
|
|
116
|
-
saved_release = self.create_release(release["name"])
|
|
117
|
-
self._existing_releases.append(saved_release)
|
|
118
|
-
|
|
119
|
-
try:
|
|
120
|
-
groups = self.collect_groups_for_release(release["jobs"])
|
|
121
|
-
except KeyError:
|
|
122
|
-
LOGGER.error("Empty release!\n %s", release)
|
|
123
|
-
continue
|
|
124
|
-
folder_stack = [dict(parent_name="", parent_display_name="", group=g) for g in reversed(groups)]
|
|
125
|
-
root_folder = {
|
|
126
|
-
"parent_name": "",
|
|
127
|
-
"parent_display_name": "",
|
|
128
|
-
"group": {
|
|
129
|
-
"name": f"{release['fullname']}-root",
|
|
130
|
-
"displayName": "-- root directory --",
|
|
131
|
-
"fullname": release["fullname"],
|
|
132
|
-
"jobs": self.collect_root_folder_jobs(release["jobs"]),
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
folder_stack.append(root_folder)
|
|
136
|
-
while len(folder_stack) != 0:
|
|
137
|
-
group_dict = folder_stack.pop()
|
|
138
|
-
group = group_dict["group"]
|
|
139
|
-
LOGGER.info("Processing group %s for release %s", group["name"], saved_release.name)
|
|
140
|
-
try:
|
|
141
|
-
group_name = group["name"] if not group_dict["parent_name"] else f"{group_dict['parent_name']}-{group['name']}"
|
|
142
|
-
saved_group = filter(lambda g: g.build_system_id == group["fullname"], self._existing_groups)
|
|
143
|
-
saved_group = next(saved_group)
|
|
144
|
-
LOGGER.info("Group %s already exists. (id: %s)", saved_group.build_system_id, saved_group.id)
|
|
145
|
-
except StopIteration:
|
|
146
|
-
LOGGER.warning(
|
|
147
|
-
"Group %s for release %s doesn't exist, creating...", group_name, saved_release.name)
|
|
148
|
-
try:
|
|
149
|
-
display_name = group.get("displayName", self._jenkins.get_job_info(name=group["fullname"])["displayName"])
|
|
150
|
-
display_name = display_name if not group_dict[
|
|
151
|
-
"parent_display_name"] else f"{group_dict['parent_display_name']} - {display_name}"
|
|
152
|
-
except Exception:
|
|
153
|
-
display_name = None
|
|
154
|
-
|
|
155
|
-
saved_group = self.create_group(saved_release, group_name, group["fullname"], display_name)
|
|
156
|
-
self._existing_groups.append(saved_group)
|
|
157
|
-
|
|
158
|
-
for job in group["jobs"]:
|
|
159
|
-
LOGGER.info("Processing job %s for release %s and group %s",
|
|
160
|
-
job["fullname"], saved_group.name, saved_release.name)
|
|
161
|
-
saved_test = None
|
|
162
|
-
if "Folder" in job["_class"]:
|
|
163
|
-
folder_stack.append(dict(parent_name=saved_group.name,
|
|
164
|
-
parent_display_name=saved_group.pretty_name, group=job))
|
|
165
|
-
if "WorkflowJob" in job["_class"]:
|
|
166
|
-
try:
|
|
167
|
-
saved_test = filter(lambda t: t.build_system_id == job["fullname"], self._existing_tests)
|
|
168
|
-
saved_test = next(saved_test)
|
|
169
|
-
LOGGER.info("Test %s already exists. (id: %s)", saved_test.build_system_id, saved_test.id)
|
|
170
|
-
except StopIteration:
|
|
171
|
-
LOGGER.warning("Test %s for release %s (group %s) doesn't exist, creating...",
|
|
172
|
-
job["name"], saved_release.name, saved_group.name)
|
|
173
|
-
try:
|
|
174
|
-
saved_test = self.create_test(
|
|
175
|
-
saved_release, saved_group, job["name"], job["fullname"], job["url"])
|
|
176
|
-
self._existing_tests.append(saved_test)
|
|
177
|
-
except ArgusTestException:
|
|
178
|
-
LOGGER.error("Unable to create test for build_id %s", job["fullname"], exc_info=True)
|
|
179
|
-
|
|
180
|
-
def collect_groups_for_release(self, jobs):
|
|
181
|
-
# pylint: disable=no-self-use
|
|
182
|
-
groups = [folder for folder in jobs if "Folder" in folder["_class"]]
|
|
183
|
-
groups = [group for group in groups if self.check_filter(group["name"])]
|
|
184
|
-
|
|
185
|
-
return groups
|
|
186
|
-
|
|
187
|
-
def collect_root_folder_jobs(self, jobs):
|
|
188
|
-
return [job for job in jobs if "WorkflowJob" in job["_class"]]
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import operator
|
|
2
|
-
from dataclasses import asdict, is_dataclass
|
|
3
|
-
from datetime import datetime, timezone
|
|
4
|
-
from functools import partial
|
|
5
|
-
from uuid import UUID
|
|
6
|
-
|
|
7
|
-
from argus.backend.db import ScyllaCluster
|
|
8
|
-
from argus.backend.models.result import ArgusGenericResultMetadata, ArgusGenericResultData
|
|
9
|
-
from argus.backend.plugins.core import PluginModelBase
|
|
10
|
-
from argus.backend.plugins.loader import AVAILABLE_PLUGINS
|
|
11
|
-
from argus.backend.service.results_service import ResultsService, Cell
|
|
12
|
-
from argus.backend.util.enums import TestStatus
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class ClientException(Exception):
|
|
16
|
-
pass
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class ClientService:
|
|
20
|
-
PLUGINS = {name: plugin.model for name, plugin in AVAILABLE_PLUGINS.items()}
|
|
21
|
-
|
|
22
|
-
def __init__(self) -> None:
|
|
23
|
-
self.cluster = ScyllaCluster.get()
|
|
24
|
-
|
|
25
|
-
def get_model(self, run_type: str) -> PluginModelBase:
|
|
26
|
-
cls = self.PLUGINS.get(run_type)
|
|
27
|
-
if not cls:
|
|
28
|
-
raise ClientException(f"Unsupported run type: {run_type}", run_type)
|
|
29
|
-
return cls
|
|
30
|
-
|
|
31
|
-
def submit_run(self, run_type: str, request_data: dict) -> str:
|
|
32
|
-
model = self.get_model(run_type)
|
|
33
|
-
model.submit_run(request_data=request_data)
|
|
34
|
-
|
|
35
|
-
return "Created"
|
|
36
|
-
|
|
37
|
-
def get_run(self, run_type: str, run_id: str):
|
|
38
|
-
model = self.get_model(run_type)
|
|
39
|
-
try:
|
|
40
|
-
run = model.get(id=run_id)
|
|
41
|
-
except model.DoesNotExist:
|
|
42
|
-
return None
|
|
43
|
-
return run
|
|
44
|
-
|
|
45
|
-
def heartbeat(self, run_type: str, run_id: str) -> int:
|
|
46
|
-
model = self.get_model(run_type)
|
|
47
|
-
run = model.load_test_run(UUID(run_id))
|
|
48
|
-
run.update_heartbeat()
|
|
49
|
-
run.save()
|
|
50
|
-
return run.heartbeat
|
|
51
|
-
|
|
52
|
-
def get_run_status(self, run_type: str, run_id: str) -> str:
|
|
53
|
-
model = self.get_model(run_type)
|
|
54
|
-
run = model.load_test_run(UUID(run_id))
|
|
55
|
-
return run.status
|
|
56
|
-
|
|
57
|
-
def update_run_status(self, run_type: str, run_id: str, new_status: str) -> str:
|
|
58
|
-
model = self.get_model(run_type)
|
|
59
|
-
run = model.load_test_run(UUID(run_id))
|
|
60
|
-
run.change_status(new_status=TestStatus(new_status))
|
|
61
|
-
run.save()
|
|
62
|
-
|
|
63
|
-
return run.status
|
|
64
|
-
|
|
65
|
-
def submit_product_version(self, run_type: str, run_id: str, version: str) -> str:
|
|
66
|
-
model = self.get_model(run_type)
|
|
67
|
-
run = model.load_test_run(UUID(run_id))
|
|
68
|
-
run.submit_product_version(version)
|
|
69
|
-
run.save()
|
|
70
|
-
|
|
71
|
-
return "Submitted"
|
|
72
|
-
|
|
73
|
-
def submit_logs(self, run_type: str, run_id: str, logs: list[dict]) -> str:
|
|
74
|
-
model = self.get_model(run_type)
|
|
75
|
-
run = model.load_test_run(UUID(run_id))
|
|
76
|
-
run.submit_logs(logs)
|
|
77
|
-
run.save()
|
|
78
|
-
|
|
79
|
-
return "Submitted"
|
|
80
|
-
|
|
81
|
-
def finish_run(self, run_type: str, run_id: str, payload: dict | None = None) -> str:
|
|
82
|
-
model = self.get_model(run_type)
|
|
83
|
-
run = model.load_test_run(UUID(run_id))
|
|
84
|
-
run.finish_run(payload)
|
|
85
|
-
run.save()
|
|
86
|
-
|
|
87
|
-
return "Finalized"
|
|
88
|
-
|
|
89
|
-
def submit_results(self, run_type: str, run_id: str, results: dict) -> dict[str, str]:
|
|
90
|
-
model = self.get_model(run_type)
|
|
91
|
-
try:
|
|
92
|
-
run = model.load_test_run(UUID(run_id))
|
|
93
|
-
except model.DoesNotExist:
|
|
94
|
-
return {"status": "error", "response": {
|
|
95
|
-
"exception": "DoesNotExist",
|
|
96
|
-
"arguments": [run_id]
|
|
97
|
-
}}
|
|
98
|
-
table_name = results["meta"]["name"]
|
|
99
|
-
results_service = ResultsService()
|
|
100
|
-
cells = [Cell(**cell) for cell in results["results"]]
|
|
101
|
-
table_metadata = results_service.get_table_metadata(test_id=run.test_id, table_name=table_name)
|
|
102
|
-
if table_metadata:
|
|
103
|
-
table_metadata = table_metadata.update_if_changed(results["meta"])
|
|
104
|
-
else:
|
|
105
|
-
table_metadata = ArgusGenericResultMetadata(test_id=run.test_id, **results["meta"])
|
|
106
|
-
table_metadata.save()
|
|
107
|
-
if results.get("sut_timestamp", 0) == 0:
|
|
108
|
-
results["sut_timestamp"] = run.sut_timestamp() # automatic sut_timestamp
|
|
109
|
-
results["sut_timestamp"] = datetime.fromtimestamp(results["sut_timestamp"])
|
|
110
|
-
best_results = results_service.update_best_results(test_id=run.test_id, table_name=table_name, table_metadata=table_metadata,
|
|
111
|
-
cells=cells, run_id=run_id)
|
|
112
|
-
table_name = results["meta"]["name"]
|
|
113
|
-
sut_timestamp = results["sut_timestamp"]
|
|
114
|
-
for cell in cells:
|
|
115
|
-
cell.update_cell_status_based_on_rules(table_metadata, best_results)
|
|
116
|
-
ArgusGenericResultData(test_id=run.test_id,
|
|
117
|
-
run_id=run.id,
|
|
118
|
-
name=table_name,
|
|
119
|
-
sut_timestamp=sut_timestamp,
|
|
120
|
-
**asdict(cell)
|
|
121
|
-
).save()
|
|
122
|
-
return {"status": "ok", "message": "Results submitted"}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
from datetime import datetime
|
|
2
|
-
import json
|
|
3
|
-
from argus.backend.models.web import ArgusEvent, ArgusEventTypes
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class EventService:
|
|
7
|
-
@staticmethod
|
|
8
|
-
def create_run_event(kind: ArgusEventTypes, body: dict, user_id=None, run_id=None, release_id=None, group_id=None, test_id=None):
|
|
9
|
-
event = ArgusEvent()
|
|
10
|
-
event.release_id = release_id
|
|
11
|
-
event.group_id = group_id
|
|
12
|
-
event.test_id = test_id
|
|
13
|
-
event.user_id = user_id
|
|
14
|
-
event.run_id = run_id
|
|
15
|
-
event.body = json.dumps(body, ensure_ascii=True, separators=(',', ':'))
|
|
16
|
-
event.kind = kind.value
|
|
17
|
-
event.created_at = datetime.utcnow()
|
|
18
|
-
event.save()
|
|
@@ -1,240 +0,0 @@
|
|
|
1
|
-
import re
|
|
2
|
-
import requests
|
|
3
|
-
from typing import Any, TypedDict
|
|
4
|
-
from uuid import UUID
|
|
5
|
-
import xml.etree.ElementTree as ET
|
|
6
|
-
import jenkins
|
|
7
|
-
import logging
|
|
8
|
-
|
|
9
|
-
from flask import current_app, g
|
|
10
|
-
|
|
11
|
-
from argus.backend.models.web import ArgusGroup, ArgusRelease, ArgusTest, UserOauthToken
|
|
12
|
-
|
|
13
|
-
LOGGER = logging.getLogger(__name__)
|
|
14
|
-
GITHUB_REPO_RE = r"(?P<http>^https?:\/\/(www\.)?github\.com\/(?P<user>[\w\d\-]+)\/(?P<repo>[\w\d\-]+)(\.git)?$)|(?P<ssh>git@github\.com:(?P<ssh_user>[\w\d\-]+)\/(?P<ssh_repo>[\w\d\-]+)(\.git)?)"
|
|
15
|
-
|
|
16
|
-
class Parameter(TypedDict):
|
|
17
|
-
_class: str
|
|
18
|
-
name: str
|
|
19
|
-
description: str
|
|
20
|
-
value: Any
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
class JenkinsServiceError(Exception):
|
|
24
|
-
pass
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
class JenkinsService:
|
|
28
|
-
RESERVED_PARAMETER_NAME = "requested_by_user"
|
|
29
|
-
|
|
30
|
-
SETTINGS_CONFIG_MAP = {
|
|
31
|
-
"scylla-cluster-tests": {
|
|
32
|
-
"gitRepo": "*//scm/userRemoteConfigs/hudson.plugins.git.UserRemoteConfig/url",
|
|
33
|
-
"gitBranch": "*//scm/branches/hudson.plugins.git.BranchSpec/name",
|
|
34
|
-
"pipelineFile": "*//scriptPath",
|
|
35
|
-
},
|
|
36
|
-
"driver-matrix-tests": {
|
|
37
|
-
"gitRepo": "*//scm/userRemoteConfigs/hudson.plugins.git.UserRemoteConfig/url",
|
|
38
|
-
"gitBranch": "*//scm/branches/hudson.plugins.git.BranchSpec/name",
|
|
39
|
-
"pipelineFile": "*//scriptPath",
|
|
40
|
-
},
|
|
41
|
-
"sirenada": {
|
|
42
|
-
"gitRepo": "*//scm/userRemoteConfigs/hudson.plugins.git.UserRemoteConfig/url",
|
|
43
|
-
"gitBranch": "*//scm/branches/hudson.plugins.git.BranchSpec/name",
|
|
44
|
-
"pipelineFile": "*//scriptPath",
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
def __init__(self) -> None:
|
|
49
|
-
self._jenkins = jenkins.Jenkins(url=current_app.config["JENKINS_URL"],
|
|
50
|
-
username=current_app.config["JENKINS_USER"],
|
|
51
|
-
password=current_app.config["JENKINS_API_TOKEN"])
|
|
52
|
-
|
|
53
|
-
def retrieve_job_parameters(self, build_id: str, build_number: int) -> list[Parameter]:
|
|
54
|
-
job_info = self._jenkins.get_build_info(name=build_id, number=build_number)
|
|
55
|
-
raw_config = self._jenkins.get_job_config(name=build_id)
|
|
56
|
-
config = ET.fromstring(raw_config)
|
|
57
|
-
parameter_defs = config.find("*//parameterDefinitions")
|
|
58
|
-
if parameter_defs:
|
|
59
|
-
descriptions = {
|
|
60
|
-
define.findtext("name"): f"{define.findtext('description', '')}" + f" (default: <span class=\"fw-bold\">{define.findtext('defaultValue')}</span>)" if define.findtext('defaultValue') else ""
|
|
61
|
-
for define in parameter_defs.iterfind("hudson.model.StringParameterDefinition")
|
|
62
|
-
}
|
|
63
|
-
else:
|
|
64
|
-
descriptions = {}
|
|
65
|
-
params = next(a for a in job_info["actions"] if a.get("_class", "#NONE") == "hudson.model.ParametersAction")["parameters"]
|
|
66
|
-
params = [param for param in params if param["name"] != self.RESERVED_PARAMETER_NAME]
|
|
67
|
-
for idx, param in enumerate(params):
|
|
68
|
-
params[idx]["description"] = descriptions.get(param["name"], "")
|
|
69
|
-
|
|
70
|
-
return params
|
|
71
|
-
|
|
72
|
-
def get_releases_for_clone(self, test_id: str):
|
|
73
|
-
test_id = UUID(test_id)
|
|
74
|
-
# TODO: Filtering based on origin location / user preferences
|
|
75
|
-
_: ArgusTest = ArgusTest.get(id=test_id)
|
|
76
|
-
|
|
77
|
-
releases = list(ArgusRelease.all())
|
|
78
|
-
|
|
79
|
-
return sorted(releases, key=lambda r: r.pretty_name if r.pretty_name else r.name)
|
|
80
|
-
|
|
81
|
-
def get_groups_for_release(self, release_id: str):
|
|
82
|
-
groups = list(ArgusGroup.filter(release_id=release_id).all())
|
|
83
|
-
|
|
84
|
-
return sorted(groups, key=lambda g: g.pretty_name if g.pretty_name else g.name)
|
|
85
|
-
|
|
86
|
-
def _verify_sct_settings(self, new_settings: dict[str, str]) -> tuple[bool, str]:
|
|
87
|
-
if not (match := re.match(GITHUB_REPO_RE, new_settings["gitRepo"])):
|
|
88
|
-
return (False, "Repository doesn't conform to GitHub schema")
|
|
89
|
-
|
|
90
|
-
git_info = match.groupdict()
|
|
91
|
-
if git_info.get("ssh"):
|
|
92
|
-
repo = git_info["ssh_repo"]
|
|
93
|
-
user = git_info["ssh_user"]
|
|
94
|
-
else:
|
|
95
|
-
repo = git_info["repo"]
|
|
96
|
-
user = git_info["user"]
|
|
97
|
-
|
|
98
|
-
user_tokens = UserOauthToken.filter(user_id=g.user.id).all()
|
|
99
|
-
token = None
|
|
100
|
-
for tok in user_tokens:
|
|
101
|
-
if tok.kind == "github":
|
|
102
|
-
token = tok.token
|
|
103
|
-
break
|
|
104
|
-
if not token:
|
|
105
|
-
raise JenkinsServiceError("Github token not found")
|
|
106
|
-
|
|
107
|
-
response = requests.get(
|
|
108
|
-
url=f"https://api.github.com/repos/{user}/{repo}/contents/{new_settings['pipelineFile']}?ref={new_settings['gitBranch']}",
|
|
109
|
-
headers={
|
|
110
|
-
"Accept": "application/vnd.github+json",
|
|
111
|
-
"Authorization": f"Bearer {token}",
|
|
112
|
-
}
|
|
113
|
-
)
|
|
114
|
-
|
|
115
|
-
if response.status_code == 404:
|
|
116
|
-
return (False, f"Pipeline file not found in the <a href=\"https://github.com/{user}/{repo}/tree/{new_settings['gitBranch']}\"> target repository</a>, please check the repository before continuing")
|
|
117
|
-
|
|
118
|
-
if response.status_code == 403:
|
|
119
|
-
return (True, "No access to this repository using your token. The pipeline file cannot be verified.")
|
|
120
|
-
|
|
121
|
-
if response.status_code == 200:
|
|
122
|
-
return (True, "")
|
|
123
|
-
|
|
124
|
-
return (False, "Generic Error")
|
|
125
|
-
|
|
126
|
-
def verify_job_settings(self, build_id: str, new_settings: dict[str, str]) -> tuple[bool, str]:
|
|
127
|
-
PLUGIN_MAP = {
|
|
128
|
-
"scylla-cluster-tests": self._verify_sct_settings,
|
|
129
|
-
# for now they match
|
|
130
|
-
"sirenada": self._verify_sct_settings,
|
|
131
|
-
"driver-matrix-tests": self._verify_sct_settings,
|
|
132
|
-
}
|
|
133
|
-
test: ArgusTest = ArgusTest.get(build_system_id=build_id)
|
|
134
|
-
plugin_name = test.plugin_name
|
|
135
|
-
|
|
136
|
-
validated, message = PLUGIN_MAP.get(plugin_name, lambda _: (True, ""))(new_settings)
|
|
137
|
-
|
|
138
|
-
return {
|
|
139
|
-
"validated": validated,
|
|
140
|
-
"message": message,
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
def get_advanced_settings(self, build_id: str):
|
|
144
|
-
test: ArgusTest = ArgusTest.get(build_system_id=build_id)
|
|
145
|
-
plugin_name = test.plugin_name
|
|
146
|
-
|
|
147
|
-
if not (plugin_settings := self.SETTINGS_CONFIG_MAP.get(plugin_name)):
|
|
148
|
-
return {}
|
|
149
|
-
|
|
150
|
-
settings = {}
|
|
151
|
-
raw_config = self._jenkins.get_job_config(name=build_id)
|
|
152
|
-
config = ET.fromstring(raw_config)
|
|
153
|
-
|
|
154
|
-
for setting, xpath in plugin_settings.items():
|
|
155
|
-
value = config.find(xpath)
|
|
156
|
-
settings[setting] = value.text
|
|
157
|
-
|
|
158
|
-
return settings
|
|
159
|
-
|
|
160
|
-
def adjust_job_settings(self, build_id: str, plugin_name: str, settings: dict[str, str]):
|
|
161
|
-
xpath_map = self.SETTINGS_CONFIG_MAP.get(plugin_name)
|
|
162
|
-
if not xpath_map:
|
|
163
|
-
return
|
|
164
|
-
|
|
165
|
-
config = self._jenkins.get_job_config(name=build_id)
|
|
166
|
-
xml = ET.fromstring(config)
|
|
167
|
-
for setting, value in settings.items():
|
|
168
|
-
element = xml.find(xpath_map[setting])
|
|
169
|
-
element.text = value
|
|
170
|
-
|
|
171
|
-
adjusted_config = ET.tostring(xml, encoding="unicode")
|
|
172
|
-
self._jenkins.reconfig_job(name=build_id, config_xml=adjusted_config)
|
|
173
|
-
|
|
174
|
-
def clone_job(self, current_test_id: str, new_name: str, target: str, group: str, advanced_settings: bool | dict[str, str]):
|
|
175
|
-
cloned_test: ArgusTest = ArgusTest.get(id=current_test_id)
|
|
176
|
-
target_release: ArgusRelease = ArgusRelease.get(id=target)
|
|
177
|
-
target_group: ArgusGroup = ArgusGroup.get(id=group)
|
|
178
|
-
|
|
179
|
-
if target_group.id == cloned_test.id and new_name == cloned_test.name:
|
|
180
|
-
raise JenkinsServiceError("Unable to clone: source and destination are the same")
|
|
181
|
-
|
|
182
|
-
if not target_group.build_system_id:
|
|
183
|
-
raise JenkinsServiceError("Unable to clone: target group is missing jenkins folder path")
|
|
184
|
-
|
|
185
|
-
jenkins_new_build_id = f"{target_group.build_system_id}/{new_name}"
|
|
186
|
-
|
|
187
|
-
new_test = ArgusTest()
|
|
188
|
-
new_test.name = new_name
|
|
189
|
-
new_test.build_system_id = jenkins_new_build_id
|
|
190
|
-
new_test.group_id = target_group.id
|
|
191
|
-
new_test.release_id = target_release.id
|
|
192
|
-
new_test.plugin_name = cloned_test.plugin_name
|
|
193
|
-
|
|
194
|
-
old_config = self._jenkins.get_job_config(name=cloned_test.build_system_id)
|
|
195
|
-
LOGGER.info(old_config)
|
|
196
|
-
xml = ET.fromstring(old_config)
|
|
197
|
-
display_name = xml.find("displayName")
|
|
198
|
-
if display_name:
|
|
199
|
-
display_name.text = new_name
|
|
200
|
-
new_config = ET.tostring(xml, encoding="unicode")
|
|
201
|
-
self._jenkins.create_job(name=jenkins_new_build_id, config_xml=new_config)
|
|
202
|
-
new_job_info = self._jenkins.get_job_info(name=jenkins_new_build_id)
|
|
203
|
-
new_test.build_system_url = new_job_info["url"]
|
|
204
|
-
new_test.save()
|
|
205
|
-
|
|
206
|
-
if advanced_settings:
|
|
207
|
-
self.adjust_job_settings(build_id=jenkins_new_build_id, plugin_name=new_test.plugin_name, settings=advanced_settings)
|
|
208
|
-
|
|
209
|
-
return {
|
|
210
|
-
"new_job": new_job_info,
|
|
211
|
-
"new_entity": new_test,
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
def clone_build_job(self, build_id: str, params: dict[str, str]):
|
|
215
|
-
queue_item = self.build_job(build_id=build_id, params=params)
|
|
216
|
-
return {
|
|
217
|
-
"queueItem": queue_item,
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
def build_job(self, build_id: str, params: dict, user_override: str = None):
|
|
221
|
-
queue_number = self._jenkins.build_job(build_id, {
|
|
222
|
-
**params,
|
|
223
|
-
# use the user's email as the default value for the requested by user parameter,
|
|
224
|
-
# so it would align with how SCT default works, on runs not trigger by argus
|
|
225
|
-
self.RESERVED_PARAMETER_NAME: g.user.email.split('@')[0] if not user_override else user_override
|
|
226
|
-
})
|
|
227
|
-
return queue_number
|
|
228
|
-
|
|
229
|
-
def get_queue_info(self, queue_item: int):
|
|
230
|
-
build_info = self._jenkins.get_queue_item(queue_item)
|
|
231
|
-
LOGGER.info("%s", build_info)
|
|
232
|
-
executable = build_info.get("executable")
|
|
233
|
-
if executable:
|
|
234
|
-
return executable
|
|
235
|
-
else:
|
|
236
|
-
return {
|
|
237
|
-
"why": build_info["why"],
|
|
238
|
-
"inQueueSince": build_info["inQueueSince"],
|
|
239
|
-
"taskUrl": build_info["task"]["url"],
|
|
240
|
-
}
|