argus-alm 0.12.2__py3-none-any.whl → 0.12.4b1__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.
Files changed (40) hide show
  1. argus/backend/cli.py +1 -1
  2. argus/backend/controller/admin_api.py +26 -0
  3. argus/backend/controller/api.py +26 -1
  4. argus/backend/controller/main.py +21 -0
  5. argus/backend/controller/testrun_api.py +132 -1
  6. argus/backend/controller/view_api.py +162 -0
  7. argus/backend/models/web.py +16 -0
  8. argus/backend/plugins/core.py +28 -5
  9. argus/backend/plugins/driver_matrix_tests/controller.py +39 -0
  10. argus/backend/plugins/driver_matrix_tests/model.py +252 -4
  11. argus/backend/plugins/driver_matrix_tests/raw_types.py +27 -0
  12. argus/backend/plugins/driver_matrix_tests/service.py +18 -0
  13. argus/backend/plugins/driver_matrix_tests/udt.py +14 -13
  14. argus/backend/plugins/generic/model.py +6 -3
  15. argus/backend/plugins/loader.py +2 -2
  16. argus/backend/plugins/sct/controller.py +31 -0
  17. argus/backend/plugins/sct/plugin.py +2 -1
  18. argus/backend/plugins/sct/service.py +101 -3
  19. argus/backend/plugins/sct/testrun.py +8 -2
  20. argus/backend/plugins/sct/types.py +18 -0
  21. argus/backend/plugins/sct/udt.py +6 -0
  22. argus/backend/plugins/sirenada/model.py +1 -1
  23. argus/backend/service/argus_service.py +116 -11
  24. argus/backend/service/build_system_monitor.py +37 -7
  25. argus/backend/service/jenkins_service.py +176 -1
  26. argus/backend/service/release_manager.py +14 -0
  27. argus/backend/service/stats.py +179 -21
  28. argus/backend/service/testrun.py +44 -5
  29. argus/backend/service/views.py +258 -0
  30. argus/backend/template_filters.py +7 -0
  31. argus/backend/util/common.py +14 -2
  32. argus/client/driver_matrix_tests/cli.py +110 -0
  33. argus/client/driver_matrix_tests/client.py +56 -193
  34. argus/client/sct/client.py +34 -0
  35. argus_alm-0.12.4b1.dist-info/METADATA +129 -0
  36. {argus_alm-0.12.2.dist-info → argus_alm-0.12.4b1.dist-info}/RECORD +39 -36
  37. {argus_alm-0.12.2.dist-info → argus_alm-0.12.4b1.dist-info}/WHEEL +1 -1
  38. {argus_alm-0.12.2.dist-info → argus_alm-0.12.4b1.dist-info}/entry_points.txt +1 -0
  39. argus_alm-0.12.2.dist-info/METADATA +0 -206
  40. {argus_alm-0.12.2.dist-info → argus_alm-0.12.4b1.dist-info}/LICENSE +0 -0
@@ -1,8 +1,12 @@
1
1
  from dataclasses import dataclass
2
2
  from datetime import datetime
3
3
  from functools import reduce
4
+ import logging
5
+ from pprint import pformat
4
6
  import re
7
+ from typing import Literal, TypedDict
5
8
  from uuid import UUID
9
+ from xml.etree import ElementTree
6
10
  from cassandra.cqlengine import columns
7
11
  from argus.backend.db import ScyllaCluster
8
12
  from argus.backend.models.web import ArgusRelease
@@ -12,6 +16,8 @@ from argus.backend.plugins.driver_matrix_tests.raw_types import RawMatrixTestRes
12
16
  from argus.backend.util.enums import TestStatus
13
17
 
14
18
 
19
+ LOGGER = logging.getLogger(__name__)
20
+
15
21
  class DriverMatrixPluginError(Exception):
16
22
  pass
17
23
 
@@ -26,6 +32,65 @@ class DriverMatrixRunSubmissionRequest():
26
32
  matrix_results: list[RawMatrixTestResult]
27
33
 
28
34
 
35
+ @dataclass(init=True, repr=True, frozen=True)
36
+ class DriverMatrixRunSubmissionRequestV2():
37
+ schema_version: str
38
+ run_id: str
39
+ job_name: str
40
+ job_url: str
41
+
42
+
43
+ TestTypeType = Literal['java', 'cpp', 'python', 'gocql']
44
+
45
+
46
+ class AdaptedXUnitData(TypedDict):
47
+ timestamp: str
48
+
49
+
50
+ def python_driver_matrix_adapter(xml: ElementTree.ElementTree) -> AdaptedXUnitData:
51
+ testsuites = list(xml.getroot().iter("testsuite"))
52
+
53
+ return {
54
+ "timestamp": testsuites[0].attrib.get("timestamp"),
55
+ }
56
+
57
+
58
+ def java_driver_matrix_adapter(xml: ElementTree.ElementTree) -> AdaptedXUnitData:
59
+ testsuites = xml.getroot()
60
+ ts_now = datetime.utcnow().timestamp()
61
+ try:
62
+ time_taken = float(testsuites.attrib.get("time"))
63
+ except ValueError:
64
+ time_taken = 0.0
65
+
66
+ timestamp = datetime.utcfromtimestamp(ts_now - time_taken).isoformat()
67
+
68
+ return {
69
+ "timestamp": timestamp,
70
+ }
71
+
72
+
73
+ def cpp_driver_matrix_adapter(xml: ElementTree.ElementTree) -> AdaptedXUnitData:
74
+ testsuites = xml.getroot()
75
+
76
+ return {
77
+ "timestamp": testsuites.attrib.get("timestamp"),
78
+ }
79
+
80
+
81
+ def gocql_driver_matrix_adapter(xml: ElementTree.ElementTree) -> AdaptedXUnitData:
82
+ testsuites = list(xml.getroot().iter("testsuite"))
83
+
84
+ return {
85
+ "timestamp": testsuites[0].attrib.get("timestamp"),
86
+ }
87
+
88
+
89
+ def generic_adapter(xml: ElementTree.ElementTree) -> AdaptedXUnitData:
90
+ return {
91
+ "timestamp": datetime.utcnow().isoformat()
92
+ }
93
+
29
94
  class DriverTestRun(PluginModelBase):
30
95
  _plugin_name = "driver-matrix-tests"
31
96
  __table_name__ = "driver_test_run"
@@ -33,18 +98,28 @@ class DriverTestRun(PluginModelBase):
33
98
  test_collection = columns.List(value_type=columns.UserDefinedType(user_type=TestCollection))
34
99
  environment_info = columns.List(value_type=columns.UserDefinedType(user_type=EnvironmentInfo))
35
100
 
101
+ _no_upstream = ["rust"]
102
+
103
+ _TEST_ADAPTERS = {
104
+ "java": java_driver_matrix_adapter,
105
+ "cpp": cpp_driver_matrix_adapter,
106
+ "python": python_driver_matrix_adapter,
107
+ "gocql": gocql_driver_matrix_adapter,
108
+ }
109
+
36
110
 
37
111
  _artifact_fnames = {
38
112
  "cpp": r"TEST-(?P<driver_name>[\w]*)-(?P<version>[\d\.-]*)",
39
113
  "gocql": r"xunit\.(?P<driver_name>[\w]*)\.(?P<proto>v\d)\.(?P<version>[v\d\.]*)",
40
114
  "python": r"pytest\.(?P<driver_name>[\w]*)\.(?P<proto>v\d)\.(?P<version>[\d\.]*)",
41
115
  "java": r"TEST-(?P<version>[\d\.\w-]*)",
116
+ "rust": r"(?P<driver_name>rust)_results_v(?P<version>[\d\w\-.]*)",
42
117
  }
43
118
 
44
119
  @classmethod
45
120
  def _stats_query(cls) -> str:
46
121
  return ("SELECT id, test_id, group_id, release_id, status, start_time, build_job_url, build_id, "
47
- f"assignee, end_time, investigation_status, heartbeat, scylla_version FROM {cls.table_name()} WHERE release_id = ?")
122
+ f"assignee, end_time, investigation_status, heartbeat, scylla_version FROM {cls.table_name()} WHERE build_id IN ? PER PARTITION LIMIT 15")
48
123
 
49
124
  @classmethod
50
125
  def get_distinct_product_versions(cls, release: ArgusRelease) -> list[str]:
@@ -76,6 +151,173 @@ class DriverTestRun(PluginModelBase):
76
151
 
77
152
  @classmethod
78
153
  def submit_run(cls, request_data: dict) -> 'DriverTestRun':
154
+ if request_data["schema_version"] == "v2":
155
+ req = DriverMatrixRunSubmissionRequestV2(**request_data)
156
+ else:
157
+ return cls.submit_matrix_run(request_data)
158
+
159
+ run = cls()
160
+ run.id = req.run_id
161
+ run.build_id = req.job_name
162
+ run.build_job_url = req.job_url
163
+ run.start_time = datetime.utcnow()
164
+ run.assign_categories()
165
+ try:
166
+ run.assignee = run.get_scheduled_assignee()
167
+ except Exception: # pylint: disable=broad-except
168
+ run.assignee = None
169
+
170
+ run.status = TestStatus.CREATED.value
171
+ run.save()
172
+ return run
173
+
174
+ @classmethod
175
+ def submit_driver_result(cls, run_id: UUID, driver_name: str, driver_type: TestTypeType, xml_data: str):
176
+ run: DriverTestRun = cls.get(id=run_id)
177
+
178
+ collection = run.parse_result_xml(driver_name, xml_data, driver_type)
179
+ run.test_collection.append(collection)
180
+
181
+ if run.status == TestStatus.CREATED:
182
+ run.status = TestStatus.RUNNING.value
183
+
184
+ run.save()
185
+ return run
186
+
187
+
188
+ @classmethod
189
+ def submit_driver_failure(cls, run_id: UUID, driver_name: str, driver_type: TestTypeType, fail_message: str):
190
+ run: DriverTestRun = cls.get(id=run_id)
191
+
192
+ collection = TestCollection()
193
+ collection.failures = 1
194
+ collection.failure_message = fail_message
195
+ collection.name = driver_name
196
+ driver_info = run.get_driver_info(driver_name, driver_type)
197
+ collection.driver = driver_info.get("driver_name")
198
+ collection.tests_total = 1
199
+ run.test_collection.append(collection)
200
+
201
+ if run.status == TestStatus.CREATED:
202
+ run.status = TestStatus.RUNNING.value
203
+
204
+ run.save()
205
+ return run
206
+
207
+ @classmethod
208
+ def submit_env_info(cls, run_id: UUID, env_data: str):
209
+ run: DriverTestRun = cls.get(id=run_id)
210
+ env = run.parse_build_environment(env_data)
211
+
212
+ for key, value in env.items():
213
+ env_info = EnvironmentInfo()
214
+ env_info.key = key
215
+ env_info.value = value
216
+ run.environment_info.append(env_info)
217
+
218
+ run.scylla_version = env.get("scylla-version")
219
+
220
+ run.save()
221
+ return run
222
+
223
+ def parse_build_environment(self, raw_env: str) -> dict[str, str]:
224
+ result = {}
225
+ for line in raw_env.split("\n"):
226
+ if not line:
227
+ continue
228
+ LOGGER.debug("ENV: %s", line)
229
+ key, val = line.split(": ")
230
+ result[key] = val.strip()
231
+
232
+ return result
233
+
234
+ def get_test_cases(self, cases: list[ElementTree.Element]) -> list[TestCase]:
235
+ result = []
236
+ for raw_case in cases:
237
+ children = list(raw_case.findall("./*"))
238
+ if len(children) > 0:
239
+ status = children[0].tag
240
+ message = f"{children[0].attrib.get('message', 'no-message')} ({children[0].attrib.get('type', 'no-type')})"
241
+ else:
242
+ status = "passed"
243
+ message = ""
244
+
245
+ case = TestCase()
246
+ case.name = raw_case.attrib["name"]
247
+ case.status = status
248
+ case.time = float(raw_case.attrib.get("time", 0.0))
249
+ case.classname = raw_case.attrib.get("classname", "")
250
+ case.message = message
251
+ result.append(case)
252
+
253
+ return result
254
+
255
+ def get_driver_info(self, xml_name: str, test_type: TestTypeType) -> dict[str, str]:
256
+ if test_type == "cpp":
257
+ filename_re = r"TEST-(?P<driver_name>[\w]*)-(?P<version>[\d\.]*)(\.xml)?"
258
+ else:
259
+ filename_re = r"(?P<name>[\w]*)\.(?P<driver_name>[\w]*)\.(?P<proto>v\d)\.(?P<version>[\d\.]*)(\.xml)?"
260
+
261
+ match = re.match(filename_re, xml_name)
262
+
263
+ return match.groupdict() if match else {}
264
+
265
+ def get_passed_count(self, suite_attribs: dict[str, str]) -> int:
266
+ if (pass_count := suite_attribs.get("passed")):
267
+ return int(pass_count)
268
+ total = int(suite_attribs.get("tests", 0))
269
+ errors = int(suite_attribs.get("errors", 0))
270
+ skipped = int(suite_attribs.get("skipped", 0))
271
+ failures = int(suite_attribs.get("failures", 0))
272
+
273
+ return total - errors - skipped - failures
274
+
275
+ def parse_result_xml(self, name: str, xml_data: str, test_type: TestTypeType) -> TestCollection:
276
+ xml: ElementTree.ElementTree = ElementTree.ElementTree(ElementTree.fromstring(xml_data))
277
+ LOGGER.debug("%s", pformat(xml))
278
+ testsuites = xml.getroot()
279
+ adapted_data = self._TEST_ADAPTERS.get(test_type, generic_adapter)(xml)
280
+
281
+ driver_info = self.get_driver_info(name, test_type)
282
+ test_collection = TestCollection()
283
+ test_collection.timestamp = datetime.fromisoformat(adapted_data["timestamp"][0:-1] if adapted_data["timestamp"][-1] == "Z" else adapted_data["timestamp"])
284
+ test_collection.name = name
285
+ test_collection.driver = driver_info.get("driver_name")
286
+ test_collection.tests_total = 0
287
+ test_collection.failures = 0
288
+ test_collection.errors = 0
289
+ test_collection.disabled = 0
290
+ test_collection.skipped = 0
291
+ test_collection.passed = 0
292
+ test_collection.time = 0.0
293
+ test_collection.suites = []
294
+
295
+ for xml_suite in testsuites.iter("testsuite"):
296
+ suite = TestSuite()
297
+ suite.name = xml_suite.attrib["name"]
298
+ suite.tests_total = int(xml_suite.attrib.get("tests", 0))
299
+ suite.failures = int(xml_suite.attrib.get("failures", 0))
300
+ suite.disabled = int(0)
301
+ suite.passed = self.get_passed_count(xml_suite.attrib)
302
+ suite.skipped = int(xml_suite.attrib.get("skipped", 0))
303
+ suite.errors = int(xml_suite.attrib.get("errors", 0))
304
+ suite.time = float(xml_suite.attrib["time"])
305
+ suite.cases = self.get_test_cases(xml_suite.findall("testcase"))
306
+
307
+ test_collection.suites.append(suite)
308
+ test_collection.tests_total += suite.tests_total
309
+ test_collection.failures += suite.failures
310
+ test_collection.errors += suite.errors
311
+ test_collection.disabled += suite.disabled
312
+ test_collection.skipped += suite.skipped
313
+ test_collection.passed += suite.passed
314
+ test_collection.time += suite.time
315
+
316
+ return test_collection
317
+
318
+ @classmethod
319
+ def submit_matrix_run(cls, request_data):
320
+ # Legacy method
79
321
  req = DriverMatrixRunSubmissionRequest(**request_data)
80
322
  run = cls()
81
323
  run.id = req.run_id # pylint: disable=invalid-name
@@ -144,21 +386,26 @@ class DriverTestRun(PluginModelBase):
144
386
  return []
145
387
 
146
388
  def _determine_run_status(self):
389
+ for collection in self.test_collection:
390
+ # patch failure
391
+ if collection.failure_message:
392
+ return TestStatus.FAILED
393
+
147
394
  if len(self.test_collection) < 2:
148
395
  return TestStatus.FAILED
149
396
 
150
397
  driver_types = {collection.driver for collection in self.test_collection}
151
- if len(driver_types) <= 1:
398
+ if len(driver_types) <= 1 and not any(driver for driver in self._no_upstream if driver in driver_types):
152
399
  return TestStatus.FAILED
153
400
 
154
- failure_count = reduce(lambda acc, val: acc + (val.failures + val.errors), self.test_collection, 0)
401
+ failure_count = reduce(lambda acc, val: acc + (val.failures or 0 + val.errors or 0), self.test_collection, 0)
155
402
  if failure_count > 0:
156
403
  return TestStatus.FAILED
157
404
 
158
405
  return TestStatus.PASSED
159
406
 
160
407
  def change_status(self, new_status: TestStatus):
161
- raise DriverMatrixPluginError("This method is obsolete. Status is now determined on submission.")
408
+ self.status = new_status
162
409
 
163
410
  def get_events(self) -> list:
164
411
  return []
@@ -168,6 +415,7 @@ class DriverTestRun(PluginModelBase):
168
415
 
169
416
  def finish_run(self, payload: dict = None):
170
417
  self.end_time = datetime.utcnow()
418
+ self.status = self._determine_run_status().value
171
419
 
172
420
  def submit_logs(self, logs: list[dict]):
173
421
  pass
@@ -1,4 +1,6 @@
1
+ from dataclasses import dataclass
1
2
  from typing import TypedDict
3
+ from uuid import UUID
2
4
 
3
5
 
4
6
  class RawMatrixTestCase(TypedDict):
@@ -33,3 +35,28 @@ class RawMatrixTestResult(TypedDict):
33
35
  time: float
34
36
  timestamp: str
35
37
  suites: list[RawMatrixTestSuite]
38
+
39
+
40
+ @dataclass(init=True, frozen=True)
41
+ class DriverMatrixSubmitResultRequest():
42
+ schema_version: str
43
+ run_id: UUID
44
+ driver_type: str
45
+ driver_name: str
46
+ raw_xml: str
47
+
48
+
49
+ @dataclass(init=True, frozen=True)
50
+ class DriverMatrixSubmitFailureRequest():
51
+ schema_version: str
52
+ run_id: UUID
53
+ driver_type: str
54
+ driver_name: str
55
+ failure_reason: str
56
+
57
+
58
+ @dataclass(init=True, frozen=True)
59
+ class DriverMatrixSubmitEnvRequest():
60
+ schema_version: str
61
+ run_id: UUID
62
+ raw_env: str
@@ -1,8 +1,13 @@
1
+ import base64
2
+ import logging
3
+ from uuid import UUID
1
4
  from argus.backend.db import ScyllaCluster
2
5
  from argus.backend.models.web import ArgusRelease, ArgusTest
3
6
  from argus.backend.plugins.driver_matrix_tests.model import DriverTestRun
4
7
 
5
8
 
9
+ LOGGER = logging.getLogger(__name__)
10
+
6
11
  class DriverMatrixService:
7
12
  def tested_versions_report(self, build_id: str) -> dict:
8
13
  db = ScyllaCluster.get()
@@ -40,3 +45,16 @@ class DriverMatrixService:
40
45
  "versions": version_map,
41
46
  }
42
47
  return response
48
+
49
+ def submit_driver_result(self, run_id: UUID | str, driver_name: str, driver_type: str, raw_xml: str) -> bool:
50
+ xml_data = base64.decodebytes(bytes(raw_xml, encoding="utf-8"))
51
+ DriverTestRun.submit_driver_result(UUID(run_id), driver_name, driver_type, xml_data)
52
+ return True
53
+
54
+ def submit_driver_failure(self, run_id: UUID | str, driver_name: str, driver_type: str, failure_reason: str) -> bool:
55
+ DriverTestRun.submit_driver_failure(UUID(run_id), driver_name, driver_type, failure_reason)
56
+ return True
57
+
58
+ def submit_env_info(self, run_id: UUID | str, raw_env: str) -> bool:
59
+ DriverTestRun.submit_env_info(UUID(run_id), raw_env)
60
+ return True
@@ -12,12 +12,12 @@ class TestCase(UserType):
12
12
 
13
13
  class TestSuite(UserType):
14
14
  name = columns.Text()
15
- tests_total = columns.Integer()
16
- failures = columns.Integer()
17
- disabled = columns.Integer()
18
- skipped = columns.Integer()
19
- passed = columns.Integer()
20
- errors = columns.Integer()
15
+ tests_total = columns.Integer(default=lambda: 0)
16
+ failures = columns.Integer(default=lambda: 0)
17
+ disabled = columns.Integer(default=lambda: 0)
18
+ skipped = columns.Integer(default=lambda: 0)
19
+ passed = columns.Integer(default=lambda: 0)
20
+ errors = columns.Integer(default=lambda: 0)
21
21
  time = columns.Float()
22
22
  cases = columns.List(value_type=columns.UserDefinedType(user_type=TestCase))
23
23
 
@@ -25,14 +25,15 @@ class TestSuite(UserType):
25
25
  class TestCollection(UserType):
26
26
  name = columns.Text()
27
27
  driver = columns.Text()
28
- tests_total = columns.Integer()
29
- failures = columns.Integer()
30
- disabled = columns.Integer()
31
- skipped = columns.Integer()
32
- passed = columns.Integer()
33
- errors = columns.Integer()
28
+ tests_total = columns.Integer(default=lambda: 0)
29
+ failure_message = columns.Text()
30
+ failures = columns.Integer(default=lambda: 0)
31
+ disabled = columns.Integer(default=lambda: 0)
32
+ skipped = columns.Integer(default=lambda: 0)
33
+ passed = columns.Integer(default=lambda: 0)
34
+ errors = columns.Integer(default=lambda: 0)
34
35
  timestamp = columns.DateTime()
35
- time = columns.Float()
36
+ time = columns.Float(default=lambda: 0.0)
36
37
  suites = columns.List(value_type=columns.UserDefinedType(user_type=TestSuite))
37
38
 
38
39
 
@@ -1,4 +1,5 @@
1
1
  from datetime import datetime
2
+ import re
2
3
  from uuid import UUID
3
4
  from cassandra.cqlengine import columns
4
5
  from cassandra.cqlengine.models import Model
@@ -24,7 +25,7 @@ class GenericRun(PluginModelBase):
24
25
  @classmethod
25
26
  def _stats_query(cls) -> str:
26
27
  return ("SELECT id, test_id, group_id, release_id, status, start_time, build_job_url, build_id, "
27
- f"assignee, end_time, investigation_status, heartbeat, scylla_version FROM {cls.table_name()} WHERE release_id = ?")
28
+ f"assignee, end_time, investigation_status, heartbeat, scylla_version FROM {cls.table_name()} WHERE build_id IN ? PER PARTITION LIMIT 15")
28
29
 
29
30
  @classmethod
30
31
  def get_distinct_product_versions(cls, release: ArgusRelease, cluster: ScyllaCluster = None) -> list[str]:
@@ -37,8 +38,10 @@ class GenericRun(PluginModelBase):
37
38
  return sorted(list(unique_versions), reverse=True)
38
39
 
39
40
  def submit_product_version(self, version: str):
40
- self.scylla_version = version
41
- self.set_product_version(version)
41
+ pattern = re.compile(r"((?P<short>[\w.~]+)-(?P<build>(0\.)?(?P<date>[0-9]{8,8})\.(?P<commit>\w+).*))")
42
+ if match := pattern.search(version):
43
+ self.scylla_version = match.group("short")
44
+ self.set_full_version(version)
42
45
 
43
46
  @classmethod
44
47
  def load_test_run(cls, run_id: UUID) -> 'GenericRun':
@@ -32,8 +32,8 @@ def plugin_loader() -> dict[str, PluginInfoBase]:
32
32
  AVAILABLE_PLUGINS = plugin_loader()
33
33
 
34
34
 
35
- def all_plugin_models() -> list[PluginModelBase]:
36
- return [model for plugin in AVAILABLE_PLUGINS.values() for model in plugin.all_models]
35
+ def all_plugin_models(include_all=False) -> list[PluginModelBase]:
36
+ return [model for plugin in AVAILABLE_PLUGINS.values() for model in plugin.all_models if issubclass(model, PluginModelBase) or include_all]
37
37
 
38
38
 
39
39
  def all_plugin_types():
@@ -81,6 +81,17 @@ def sct_resource_update_shards(run_id: str, resource_name: str):
81
81
  }
82
82
 
83
83
 
84
+ @bp.route("/<string:run_id>/resource/<string:resource_name>/update", methods=["POST"])
85
+ @api_login_required
86
+ def sct_resource_update(run_id: str, resource_name: str):
87
+ payload = get_payload(request)
88
+ result = SCTService.update_resource(run_id=run_id, resource_name=resource_name, update_data=payload["update_data"])
89
+ return {
90
+ "status": "ok",
91
+ "response": result
92
+ }
93
+
94
+
84
95
  @bp.route("/<string:run_id>/nemesis/submit", methods=["POST"])
85
96
  @api_login_required
86
97
  def sct_nemesis_submit(run_id: str):
@@ -152,3 +163,23 @@ def sct_get_kernel_report(release_name: str):
152
163
  "status": "ok",
153
164
  "response": result
154
165
  }
166
+
167
+
168
+ @bp.route("/<string:run_id>/junit/submit", methods=["POST"])
169
+ @api_login_required
170
+ def sct_submit_junit_report(run_id: str):
171
+ payload = get_payload(request)
172
+ result = SCTService.junit_submit(run_id, payload["file_name"], payload["content"])
173
+ return {
174
+ "status": "ok",
175
+ "response": result
176
+ }
177
+
178
+ @bp.route("/<string:run_id>/junit/get_all", methods=["GET"])
179
+ @api_login_required
180
+ def sct_get_junit_reports(run_id: str):
181
+ result = SCTService.junit_get_all(run_id)
182
+ return {
183
+ "status": "ok",
184
+ "response": result
185
+ }
@@ -1,6 +1,6 @@
1
1
  from flask import Blueprint
2
2
 
3
- from argus.backend.plugins.sct.testrun import SCTTestRun
3
+ from argus.backend.plugins.sct.testrun import SCTJunitReports, SCTTestRun
4
4
  from argus.backend.plugins.sct.controller import bp as sct_bp
5
5
  from argus.backend.plugins.core import PluginInfoBase, PluginModelBase
6
6
  from argus.backend.plugins.sct.udt import (
@@ -23,6 +23,7 @@ class PluginInfo(PluginInfoBase):
23
23
  controller: Blueprint = sct_bp
24
24
  all_models = [
25
25
  SCTTestRun,
26
+ SCTJunitReports,
26
27
  ]
27
28
  all_types = [
28
29
  NemesisRunInfo,
@@ -1,12 +1,16 @@
1
+ import base64
1
2
  from dataclasses import dataclass
3
+ from datetime import datetime
2
4
  from functools import reduce
3
5
  import logging
4
6
  import math
7
+ import re
5
8
  from time import time
9
+ from xml.etree import ElementTree
6
10
  from flask import g
7
11
  from argus.backend.models.web import ArgusEventTypes
8
- from argus.backend.plugins.sct.testrun import SCTTestRun, SubtestType
9
- from argus.backend.plugins.sct.types import GeminiResultsRequest, PerformanceResultsRequest
12
+ from argus.backend.plugins.sct.testrun import SCTJunitReports, SCTTestRun, SubtestType
13
+ from argus.backend.plugins.sct.types import GeminiResultsRequest, PerformanceResultsRequest, ResourceUpdateRequest
10
14
  from argus.backend.plugins.sct.udt import (
11
15
  CloudInstanceDetails,
12
16
  CloudResource,
@@ -60,7 +64,8 @@ class SCTService:
60
64
  run: SCTTestRun = SCTTestRun.get(id=run_id)
61
65
  for package_dict in packages:
62
66
  package = PackageVersion(**package_dict)
63
- run.packages.append(package)
67
+ if package not in run.packages:
68
+ run.packages.append(package)
64
69
  run.save()
65
70
  except SCTTestRun.DoesNotExist as exception:
66
71
  LOGGER.error("Run %s not found for SCTTestRun", run_id)
@@ -277,6 +282,33 @@ class SCTService:
277
282
 
278
283
  return "updated"
279
284
 
285
+ @staticmethod
286
+ def update_resource(run_id: str, resource_name: str, update_data: ResourceUpdateRequest) -> str:
287
+ try:
288
+ fields_updated = {}
289
+ run: SCTTestRun = SCTTestRun.get(id=run_id)
290
+ resource = next(res for res in run.get_resources() if res.name == resource_name)
291
+ instance_info = update_data.pop("instance_info", None)
292
+ resource.state = ResourceState(update_data.get("state", resource.state)).value
293
+ if instance_info:
294
+ resource_instance_info = resource.get_instance_info()
295
+ for k, v in instance_info.items():
296
+ if k in resource_instance_info.keys():
297
+ resource_instance_info[k] = v
298
+ fields_updated[k] = v
299
+ run.save()
300
+ except StopIteration as exception:
301
+ LOGGER.error("Resource %s not found in run %s", resource_name, run_id)
302
+ raise SCTServiceException("Resource not found", resource_name) from exception
303
+ except SCTTestRun.DoesNotExist as exception:
304
+ LOGGER.error("Run %s not found for SCTTestRun", run_id)
305
+ raise SCTServiceException("Run not found", run_id) from exception
306
+
307
+ return {
308
+ "state": "updated",
309
+ "fields": fields_updated
310
+ }
311
+
280
312
  @staticmethod
281
313
  def terminate_resource(run_id: str, resource_name: str, reason: str) -> str:
282
314
  try:
@@ -348,6 +380,8 @@ class SCTService:
348
380
  wrapper = EventsBySeverity(severity=event.severity,
349
381
  event_amount=event.total_events, last_events=event.messages)
350
382
  run.get_events().append(wrapper)
383
+ coredumps = SCTService.locate_coredumps(run, run.get_events())
384
+ run.submit_logs(coredumps)
351
385
  run.save()
352
386
  except SCTTestRun.DoesNotExist as exception:
353
387
  LOGGER.error("Run %s not found for SCTTestRun", run_id)
@@ -355,6 +389,39 @@ class SCTService:
355
389
 
356
390
  return "added"
357
391
 
392
+ @staticmethod
393
+ def locate_coredumps(run: SCTTestRun, events: list[EventsBySeverity]) -> list[dict]:
394
+ flat_messages: list[str] = []
395
+ links = []
396
+ for es in events:
397
+ flat_messages.extend(es.last_events)
398
+ coredump_events = filter(lambda v: "coredumpevent" in v.lower(), flat_messages)
399
+ for idx, event in enumerate(coredump_events):
400
+ core_pattern = r"corefile_url=(?P<url>.+)$"
401
+ ts_pattern = r"^(?P<ts>\d{4}-\d{2}-\d{2} ([\d:]*)\.\d{3})"
402
+ node_name_pattern = r"node=(?P<name>.+)$"
403
+ core_url_match = re.search(core_pattern, event, re.MULTILINE)
404
+ node_name_match = re.search(node_name_pattern, event, re.MULTILINE)
405
+ ts_match = re.search(ts_pattern, event)
406
+ if core_url_match:
407
+ node_name = node_name_match.group("name") if node_name_match else f"unknown-node-{idx}"
408
+ split_name = node_name.split(" ")
409
+ node_name = split_name[1] if len(split_name) >= 2 else node_name
410
+ url = core_url_match.group("url")
411
+ timestamp_component = ""
412
+ if ts_match:
413
+ try:
414
+ timestamp = datetime.fromisoformat(ts_match.group("ts"))
415
+ timestamp_component = timestamp.strftime("-%Y-%m-%d_%H-%M-%S")
416
+ except ValueError:
417
+ pass
418
+ log_link = {
419
+ "log_name": f"core.scylla-{node_name}{timestamp_component}.gz",
420
+ "log_link": url
421
+ }
422
+ links.append(log_link)
423
+ return links
424
+
358
425
  @staticmethod
359
426
  def get_scylla_version_kernels_report(release_name: str):
360
427
  all_release_runs = SCTTestRun.get_version_data_for_release(release_name=release_name)
@@ -391,3 +458,34 @@ class SCTService:
391
458
  "versions": kernels_by_version,
392
459
  "metadata": kernel_metadata
393
460
  }
461
+
462
+ @staticmethod
463
+ def junit_submit(run_id: str, file_name: str, content: str) -> bool:
464
+ try:
465
+ report = SCTJunitReports.get(test_id=run_id, file_name=file_name)
466
+ if report:
467
+ raise SCTServiceException(f"Report {file_name} already exists.", file_name)
468
+ except SCTJunitReports.DoesNotExist:
469
+ pass
470
+ report = SCTJunitReports()
471
+ report.test_id = run_id
472
+ report.file_name = file_name
473
+
474
+ xml_content = str(base64.decodebytes(bytes(content, encoding="utf-8")), encoding="utf-8")
475
+ try:
476
+ _ = ElementTree.fromstring(xml_content)
477
+ except Exception:
478
+ raise SCTServiceException(f"Malformed JUnit report submitted")
479
+
480
+ report.report = xml_content
481
+ report.save()
482
+
483
+ return True
484
+
485
+ @staticmethod
486
+ def junit_get_all(run_id: str) -> list[SCTJunitReports]:
487
+ return list(SCTJunitReports.filter(test_id=run_id).all())
488
+
489
+ @staticmethod
490
+ def junit_get_single(run_id: str, file_name: str) -> SCTJunitReports:
491
+ return SCTJunitReports.get(test_id=run_id, file_name=file_name)