argus-alm 0.10.1b4__tar.gz → 0.11.0__tar.gz

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 (75) hide show
  1. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/PKG-INFO +2 -4
  2. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/backend/controller/api.py +0 -2
  3. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/backend/controller/testrun_api.py +12 -0
  4. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/backend/plugins/core.py +18 -0
  5. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/backend/plugins/sct/testrun.py +6 -2
  6. argus_alm-0.11.0/argus/backend/plugins/sirenada/model.py +113 -0
  7. argus_alm-0.11.0/argus/backend/plugins/sirenada/plugin.py +17 -0
  8. argus_alm-0.11.0/argus/backend/plugins/sirenada/types.py +35 -0
  9. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/backend/service/argus_service.py +18 -4
  10. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/backend/service/stats.py +26 -3
  11. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/backend/service/testrun.py +50 -7
  12. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/client/base.py +2 -1
  13. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/client/driver_matrix_tests/client.py +6 -6
  14. argus_alm-0.11.0/argus/client/sirenada/client.py +151 -0
  15. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/pyproject.toml +4 -6
  16. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/setup.py +4 -4
  17. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/LICENSE +0 -0
  18. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/README.md +0 -0
  19. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/__init__.py +0 -0
  20. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/backend/.gitkeep +0 -0
  21. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/backend/__init__.py +0 -0
  22. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/backend/cli.py +0 -0
  23. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/backend/controller/__init__.py +0 -0
  24. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/backend/controller/admin.py +0 -0
  25. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/backend/controller/admin_api.py +0 -0
  26. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/backend/controller/auth.py +0 -0
  27. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/backend/controller/client_api.py +0 -0
  28. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/backend/controller/main.py +0 -0
  29. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/backend/controller/notification_api.py +0 -0
  30. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/backend/controller/notifications.py +0 -0
  31. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/backend/controller/team.py +0 -0
  32. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/backend/controller/team_ui.py +0 -0
  33. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/backend/db.py +0 -0
  34. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/backend/error_handlers.py +0 -0
  35. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/backend/events/event_processors.py +0 -0
  36. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/backend/models/__init__.py +0 -0
  37. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/backend/models/web.py +0 -0
  38. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/backend/plugins/__init__.py +0 -0
  39. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/backend/plugins/driver_matrix_tests/controller.py +0 -0
  40. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/backend/plugins/driver_matrix_tests/model.py +0 -0
  41. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/backend/plugins/driver_matrix_tests/plugin.py +0 -0
  42. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/backend/plugins/driver_matrix_tests/raw_types.py +0 -0
  43. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/backend/plugins/driver_matrix_tests/udt.py +0 -0
  44. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/backend/plugins/loader.py +0 -0
  45. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/backend/plugins/sct/controller.py +0 -0
  46. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/backend/plugins/sct/plugin.py +0 -0
  47. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/backend/plugins/sct/resource_setup.py +0 -0
  48. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/backend/plugins/sct/service.py +0 -0
  49. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/backend/plugins/sct/udt.py +0 -0
  50. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/backend/service/admin.py +0 -0
  51. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/backend/service/build_system_monitor.py +0 -0
  52. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/backend/service/client_service.py +0 -0
  53. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/backend/service/notification_manager.py +0 -0
  54. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/backend/service/release_manager.py +0 -0
  55. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/backend/service/team_manager_service.py +0 -0
  56. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/backend/service/user.py +0 -0
  57. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/backend/template_filters.py +0 -0
  58. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/backend/util/common.py +0 -0
  59. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/backend/util/config.py +0 -0
  60. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/backend/util/encoders.py +0 -0
  61. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/backend/util/enums.py +0 -0
  62. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/backend/util/logsetup.py +0 -0
  63. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/backend/util/module_loaders.py +0 -0
  64. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/backend/util/send_email.py +0 -0
  65. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/client/__init__.py +0 -0
  66. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/client/sct/client.py +0 -0
  67. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/client/sct/types.py +0 -0
  68. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/db/.gitkeep +0 -0
  69. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/db/argus_json.py +0 -0
  70. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/db/cloud_types.py +0 -0
  71. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/db/config.py +0 -0
  72. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/db/db_types.py +0 -0
  73. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/db/interface.py +0 -0
  74. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/db/testrun.py +0 -0
  75. {argus_alm-0.10.1b4 → argus_alm-0.11.0}/argus/db/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: argus-alm
3
- Version: 0.10.1b4
3
+ Version: 0.11.0
4
4
  Summary: Argus
5
5
  Home-page: https://github.com/scylladb/argus
6
6
  License: Apache-2.0
@@ -11,9 +11,7 @@ Classifier: License :: OSI Approved :: Apache Software License
11
11
  Classifier: Programming Language :: Python :: 3
12
12
  Classifier: Programming Language :: Python :: 3.10
13
13
  Classifier: Programming Language :: Python :: 3.11
14
- Requires-Dist: PyYAML (>=5.4.1,<6.0.0)
15
- Requires-Dist: pydantic (>=1.8.2,<2.0.0)
16
- Requires-Dist: scylla-driver (>=3.24.8,<4.0.0)
14
+ Requires-Dist: requests (>=2.26.0,<3.0.0)
17
15
  Project-URL: Repository, https://github.com/scylladb/argus
18
16
  Description-Content-Type: text/markdown
19
17
 
@@ -332,8 +332,6 @@ def set_test_plugin(test_id: str):
332
332
  payload = get_payload(request)
333
333
 
334
334
  current_user: User = g.user
335
- if not current_user.is_admin():
336
- raise Exception("Cannot set plugin as a non-administrator")
337
335
  test: ArgusTest = ArgusTest.get(id=UUID(test_id))
338
336
  test.plugin_name = payload["plugin_name"]
339
337
  test.save()
@@ -257,3 +257,15 @@ def test_run_delete_comment(test_id: str, run_id: str, comment_id: str):
257
257
  "status": "ok",
258
258
  "response": result
259
259
  }
260
+
261
+
262
+ @bp.route("/terminate_stuck_runs", methods=["POST"])
263
+ @api_login_required
264
+ def sct_terminate_stuck_runs():
265
+ result = TestRunService().terminate_stuck_runs()
266
+ return {
267
+ "status": "ok",
268
+ "response": {
269
+ "total": result
270
+ }
271
+ }
@@ -122,6 +122,24 @@ class PluginModelBase(Model):
122
122
 
123
123
  return list(rows)
124
124
 
125
+ @classmethod
126
+ def get_run_meta_by_build_id(cls, build_id: str, limit: int = 10):
127
+ cluster = ScyllaCluster.get()
128
+ query = cluster.prepare("SELECT id, test_id, group_id, release_id, status, start_time, build_job_url, build_id, "
129
+ f"assignee, end_time, investigation_status, heartbeat FROM {cls.table_name()} WHERE build_id = ? LIMIT ?")
130
+ rows = cluster.session.execute(query=query, parameters=(build_id, limit))
131
+
132
+ return list(rows)
133
+
134
+ @classmethod
135
+ def get_run_meta_by_run_id(cls, run_id: UUID | str):
136
+ cluster = ScyllaCluster.get()
137
+ query = cluster.prepare("SELECT id, test_id, group_id, release_id, status, start_time, build_job_url, build_id, "
138
+ f"assignee, end_time, investigation_status, heartbeat FROM {cls.table_name()} WHERE id = ?")
139
+ rows = cluster.session.execute(query=query, parameters=(run_id,))
140
+
141
+ return list(rows)
142
+
125
143
  @classmethod
126
144
  def load_test_run(cls, run_id: UUID) -> 'PluginModelBase':
127
145
  raise NotImplementedError()
@@ -4,6 +4,7 @@ from dataclasses import dataclass
4
4
  from uuid import UUID
5
5
 
6
6
  from cassandra.cqlengine import columns
7
+ from cassandra.cqlengine.models import _DoesNotExist
7
8
  from argus.backend.db import ScyllaCluster
8
9
  from argus.backend.models.web import ArgusRelease
9
10
  from argus.backend.plugins.core import PluginModelBase
@@ -44,7 +45,7 @@ class SCTTestRunSubmissionRequest():
44
45
 
45
46
 
46
47
  class SCTTestRun(PluginModelBase):
47
- __table_name__ = "test_runs_v8"
48
+ __table_name__ = "sct_test_run"
48
49
  _plugin_name = "scylla-cluster-tests"
49
50
 
50
51
  # Test Details
@@ -105,7 +106,10 @@ class SCTTestRun(PluginModelBase):
105
106
  run = cls()
106
107
  run.build_id = req.job_name
107
108
  run.assign_categories()
108
- run.assignee = run.get_scheduled_assignee()
109
+ try:
110
+ run.assignee = run.get_scheduled_assignee()
111
+ except _DoesNotExist:
112
+ run.assignee = None
109
113
  run.start_time = datetime.utcnow()
110
114
  run.id = UUID(req.run_id) # pylint: disable=invalid-name
111
115
  run.scm_revision_id = req.commit_id
@@ -0,0 +1,113 @@
1
+ from datetime import datetime
2
+ from uuid import UUID, uuid4
3
+ from cassandra.cqlengine import columns
4
+ from cassandra.cqlengine.usertype import UserType
5
+ from cassandra.cqlengine.models import Model
6
+ from argus.backend.db import ScyllaCluster
7
+ from argus.backend.models.web import ArgusRelease
8
+ from argus.backend.plugins.core import PluginModelBase
9
+ from argus.backend.plugins.sirenada.types import RawSirenadaRequest, SirenadaPluginException
10
+ from argus.backend.util.enums import TestStatus
11
+
12
+
13
+ class SirenadaTest(UserType):
14
+ test_name = columns.Text()
15
+ class_name = columns.Text()
16
+ file_name = columns.Text()
17
+ browser_type = columns.Text()
18
+ cluster_type = columns.Text()
19
+ status = columns.Text()
20
+ duration = columns.Float()
21
+ message = columns.Text()
22
+ start_time = columns.DateTime()
23
+ stack_trace = columns.Text()
24
+ screenshot_file = columns.Text()
25
+ s3_folder_id = columns.Text()
26
+ requests_file = columns.Text()
27
+ sirenada_test_id = columns.Text()
28
+ sirenada_user = columns.Text()
29
+ sirenada_password = columns.Text()
30
+
31
+
32
+
33
+ class SirenadaRun(PluginModelBase):
34
+ _plugin_name = "sirenada"
35
+ __table_name__ = "sirenada_run"
36
+ logs = columns.Map(key_type=columns.Text(), value_type=columns.Text())
37
+ # TODO: Legacy field name, should be renamed to product_version and abstracted
38
+ scylla_version = columns.Text()
39
+ region = columns.Text()
40
+ sirenada_test_ids = columns.List(value_type=columns.Text())
41
+ s3_folder_ids = columns.List(value_type=columns.Tuple(columns.Text(), columns.Text()))
42
+ browsers = columns.List(value_type=columns.Text())
43
+ clusters = columns.List(value_type=columns.Text())
44
+ sct_test_id = columns.UUID()
45
+ results = columns.List(value_type=columns.UserDefinedType(user_type=SirenadaTest))
46
+
47
+ @classmethod
48
+ def _stats_query(cls) -> str:
49
+ return ("SELECT id, test_id, group_id, release_id, status, start_time, build_job_url, build_id, "
50
+ f"assignee, end_time, investigation_status, heartbeat, scylla_version FROM {cls.table_name()} WHERE release_id = ?")
51
+
52
+ @classmethod
53
+ def get_distinct_product_versions(cls, release: ArgusRelease, cluster: ScyllaCluster = None) -> list[str]:
54
+ if not cluster:
55
+ cluster = ScyllaCluster.get()
56
+ statement = cluster.prepare(f"SELECT scylla_version FROM {cls.table_name()} WHERE release_id = ?")
57
+ rows = cluster.session.execute(query=statement, parameters=(release.id,))
58
+ unique_versions = {r["scylla_version"] for r in rows if r["scylla_version"]}
59
+
60
+ return sorted(list(unique_versions), reverse=True)
61
+
62
+ def submit_product_version(self, version: str):
63
+ self.scylla_version = version
64
+
65
+ def submit_logs(self, logs: dict[str, str]):
66
+ raise SirenadaPluginException("Log submission is not supported for Sirenada")
67
+
68
+ def finish_run(self):
69
+ raise SirenadaPluginException("Sirenada runs do not need finalization")
70
+
71
+ @classmethod
72
+ def load_test_run(cls, run_id: UUID) -> 'SirenadaRun':
73
+ return cls.get(id=run_id)
74
+
75
+ @classmethod
76
+ def submit_run(cls, request_data: RawSirenadaRequest) -> 'SirenadaRun':
77
+ try:
78
+ run = cls.get(id=UUID(request_data["run_id"]))
79
+ except cls.DoesNotExist:
80
+ run = cls()
81
+ run.id = request_data["run_id"]
82
+ run.build_id = request_data["build_id"]
83
+ run.start_time = datetime.utcnow()
84
+ run.assign_categories()
85
+ run.build_job_url = request_data["build_job_url"]
86
+ run.region = request_data["region"]
87
+ run.status = TestStatus.PASSED.value
88
+ try:
89
+ run.assignee = run.get_scheduled_assignee()
90
+ except Model.DoesNotExist:
91
+ run.assignee = None
92
+
93
+ for raw_case in request_data["results"]:
94
+ case = SirenadaTest(**raw_case)
95
+ if case.status in ["failed", "error"] and run.status not in [TestStatus.FAILED.value, TestStatus.ABORTED.value]:
96
+ run.status = TestStatus.FAILED.value
97
+ run.results.append(case)
98
+
99
+ if case.sirenada_test_id not in run.sirenada_test_ids:
100
+ run.sirenada_test_ids.append(case.sirenada_test_id)
101
+
102
+ if case.browser_type not in run.browsers:
103
+ run.browsers.append(case.browser_type)
104
+
105
+ if case.cluster_type not in run.clusters:
106
+ run.clusters.append(case.cluster_type)
107
+
108
+ if (case.s3_folder_id, case.sirenada_test_id) not in run.s3_folder_ids and case.s3_folder_id:
109
+ run.s3_folder_ids.append((case.s3_folder_id, case.sirenada_test_id))
110
+
111
+ run.save()
112
+
113
+ return run
@@ -0,0 +1,17 @@
1
+ from flask import Blueprint
2
+
3
+ from argus.backend.plugins.core import PluginInfoBase, PluginModelBase
4
+ from argus.backend.plugins.sirenada.model import SirenadaRun, SirenadaTest
5
+
6
+
7
+ class PluginInfo(PluginInfoBase):
8
+ # pylint: disable=too-few-public-methods
9
+ name: str = "sirenada"
10
+ model: PluginModelBase = SirenadaRun
11
+ controller: Blueprint = None
12
+ all_models = [
13
+ SirenadaRun
14
+ ]
15
+ all_types = [
16
+ SirenadaTest
17
+ ]
@@ -0,0 +1,35 @@
1
+ from typing import TypedDict
2
+
3
+
4
+ class RawSirenadaTestCase(TypedDict):
5
+ test_name: str
6
+ class_name: str
7
+ file_name: str
8
+ browser_type: str
9
+ cluster_type: str
10
+ status: str
11
+ duration: float
12
+ message: str
13
+ start_time: str
14
+ stack_trace: str
15
+ screenshot_file: str
16
+ s3_folder_id: str
17
+ requests_file: str
18
+ sirenada_test_id: str
19
+ sirenada_user: str
20
+ sirenada_password: str
21
+
22
+
23
+ class RawSirenadaRequest(TypedDict):
24
+ build_id: str
25
+ build_job_url: str
26
+ run_id: str # UUID
27
+ region: list[str]
28
+ browsers: list[str]
29
+ clusters: list[str]
30
+ sct_test_id: str
31
+ results: list[RawSirenadaTestCase]
32
+
33
+
34
+ class SirenadaPluginException(Exception):
35
+ pass
@@ -24,6 +24,7 @@ from argus.backend.models.web import (
24
24
  User,
25
25
  )
26
26
  from argus.backend.events.event_processors import EVENT_PROCESSORS
27
+ from argus.backend.service.testrun import TestRunService
27
28
 
28
29
  LOGGER = logging.getLogger(__name__)
29
30
 
@@ -418,6 +419,19 @@ class ArgusService:
418
419
  groups = ArgusScheduleGroup.filter(schedule_id=schedule.id).all()
419
420
  assignees = ArgusScheduleAssignee.filter(schedule_id=schedule.id).all()
420
421
 
422
+ full_schedule = dict(schedule)
423
+ full_schedule["tests"] = [test.test_id for test in tests]
424
+ full_schedule["groups"] = [group.group_id for group in groups]
425
+ full_schedule["assignees"] = [assignee.assignee for assignee in assignees]
426
+
427
+ schedule_user = User.get(id=assignees[0].assignee)
428
+
429
+ jobs_for_schedule = self.get_jobs_for_user(user=schedule_user, ignore_time=True, schedules=[full_schedule])
430
+
431
+ service = TestRunService()
432
+ for job in jobs_for_schedule:
433
+ service.change_run_assignee(test_id=job["test_id"], run_id=job["id"], new_assignee=None)
434
+
421
435
  for entities in [tests, groups, assignees]:
422
436
  for entity in entities:
423
437
  entity.delete()
@@ -517,15 +531,15 @@ class ArgusService:
517
531
 
518
532
  return response
519
533
 
520
- def get_jobs_for_user(self, user: User):
534
+ def get_jobs_for_user(self, user: User, ignore_time: bool = False, schedules: list[dict] = None):
521
535
  runs = [run for plugin in all_plugin_models() for run in plugin.get_jobs_assigned_to_user(user=user)]
522
- schedules = self.get_schedules_for_user(user)
536
+ schedules = self.get_schedules_for_user(user) if not schedules else schedules
523
537
  valid_runs = []
524
538
  today = datetime.datetime.now()
525
539
  month_ago = today - datetime.timedelta(days=30)
526
540
  for run in runs:
527
541
  run_date = run["start_time"]
528
- if user.id == run["assignee"] and run_date >= month_ago:
542
+ if user.id == run["assignee"] and run_date >= month_ago and not ignore_time:
529
543
  valid_runs.append(run)
530
544
  continue
531
545
  for schedule in schedules:
@@ -545,7 +559,7 @@ class ArgusService:
545
559
  break
546
560
  return valid_runs
547
561
 
548
- def get_schedules_for_user(self, user: User):
562
+ def get_schedules_for_user(self, user: User) -> list[dict]:
549
563
  all_assigned_schedules = ArgusScheduleAssignee.filter(assignee=user.id).all()
550
564
  schedule_keys = [(schedule_assignee.release_id, schedule_assignee.schedule_id)
551
565
  for schedule_assignee in all_assigned_schedules]
@@ -3,6 +3,7 @@ import logging
3
3
  from datetime import datetime
4
4
  from typing import TypedDict
5
5
  from uuid import UUID
6
+
6
7
  from argus.backend.plugins.loader import all_plugin_models
7
8
  from argus.backend.util.common import get_build_number
8
9
  from argus.backend.util.enums import TestStatus, TestInvestigationStatus
@@ -92,16 +93,28 @@ class ReleaseStats:
92
93
  self.all_tests = []
93
94
 
94
95
  def to_dict(self) -> dict:
96
+ converted_groups = {str(group.group.id): group.to_dict() for group in self.groups}
97
+ aggregated_investigation_status = {}
98
+ for group in converted_groups.values():
99
+ for investigation_status in TestInvestigationStatus:
100
+ current_status = aggregated_investigation_status.get(investigation_status.value, {})
101
+ result = {
102
+ status.value: current_status.get(status.value, 0) + group.get(investigation_status.value, {}).get(status, 0)
103
+ for status in TestStatus
104
+ }
105
+ aggregated_investigation_status[investigation_status.value] = result
106
+
95
107
  return {
96
108
  "release": dict(self.release.items()),
97
- "groups": {str(group.group.id): group.to_dict() for group in self.groups},
109
+ "groups": converted_groups,
98
110
  "total": self.total_tests,
99
111
  **self.status_map,
100
112
  "disabled": not self.release.enabled,
101
113
  "perpetual": self.release.perpetual,
102
114
  "lastStatus": self.last_investigation_status,
103
115
  "lastInvestigationStatus": self.last_investigation_status,
104
- "hasBugReport": self.has_bug_report
116
+ "hasBugReport": self.has_bug_report,
117
+ **aggregated_investigation_status
105
118
  }
106
119
 
107
120
  def collect(self, rows: list[TestRunStatRow], limited=False, force=False) -> None:
@@ -144,6 +157,15 @@ class GroupStats:
144
157
  self.tests: list[TestStats] = []
145
158
 
146
159
  def to_dict(self) -> dict:
160
+ converted_tests = {str(test.test.id): test.to_dict() for test in self.tests}
161
+ investigation_progress = {}
162
+ for test in converted_tests.values():
163
+ progress_for_status = investigation_progress.get(test["investigation_status"], {})
164
+ status_count = progress_for_status.get(test["status"], 0)
165
+ status_count += 1
166
+ progress_for_status[test["status"]] = status_count
167
+ investigation_progress[test["investigation_status"]] = progress_for_status
168
+
147
169
  return {
148
170
  "group": dict(self.group.items()),
149
171
  "total": self.total_tests,
@@ -151,7 +173,8 @@ class GroupStats:
151
173
  "lastStatus": self.last_status,
152
174
  "lastInvestigationStatus": self.last_investigation_status,
153
175
  "disabled": self.disabled,
154
- "tests": {str(test.test.id): test.to_dict() for test in self.tests}
176
+ "tests": converted_tests,
177
+ **investigation_progress
155
178
  }
156
179
 
157
180
  def collect(self, limited=False):
@@ -1,4 +1,4 @@
1
- from datetime import datetime
1
+ from datetime import datetime, timedelta
2
2
  import json
3
3
  import logging
4
4
  import re
@@ -34,6 +34,10 @@ from argus.backend.util.enums import TestInvestigationStatus, TestStatus
34
34
  LOGGER = logging.getLogger(__name__)
35
35
 
36
36
 
37
+ class TestRunServiceException(Exception):
38
+ pass
39
+
40
+
37
41
  class TestRunService:
38
42
  ASSIGNEE_PLACEHOLDER = "none-none-none"
39
43
 
@@ -64,14 +68,14 @@ class TestRunService:
64
68
  if not plugin:
65
69
  return []
66
70
 
67
- last_runs: list[PluginModelBase] = list(plugin.model.filter(build_id=test.build_system_id).limit(limit).all())
68
- last_runs_ids = [run.id for run in last_runs]
71
+ last_runs: list[dict] = plugin.model.get_run_meta_by_build_id(build_id=test.build_system_id, limit=limit)
72
+ last_runs_ids = [run["id"] for run in last_runs]
69
73
  for added_run in additional_runs:
70
74
  if added_run not in last_runs_ids:
71
- last_runs.append(plugin.model.get(id=added_run))
75
+ last_runs.extend(plugin.model.get_run_meta_by_run_id(run_id=added_run))
72
76
 
73
77
  for row in last_runs:
74
- setattr(row, "build_number", get_build_number(build_job_url=row.build_job_url))
78
+ row["build_number"] = get_build_number(build_job_url=row["build_job_url"])
75
79
 
76
80
  return last_runs
77
81
 
@@ -91,7 +95,10 @@ class TestRunService:
91
95
  return response
92
96
 
93
97
  def change_run_status(self, test_id: UUID, run_id: UUID, new_status: TestStatus):
94
- test = ArgusTest.get(id=test_id)
98
+ try:
99
+ test = ArgusTest.get(id=test_id)
100
+ except ArgusTest.DoesNotExist as exc:
101
+ raise TestRunServiceException("Test entity does not exist for provided test_id", test_id) from exc
95
102
  plugin = self.get_plugin(plugin_name=test.plugin_name)
96
103
  run: PluginModelBase = plugin.model.get(id=run_id)
97
104
  old_status = run.status
@@ -384,7 +391,7 @@ class TestRunService:
384
391
  response = []
385
392
  for issue in all_issues:
386
393
  runs = runs_by_issue.get(issue, [])
387
- runs.append(issue.run_id)
394
+ runs.append({"test_id": issue.test_id, "run_id": issue.run_id})
388
395
  runs_by_issue[issue] = runs
389
396
 
390
397
  for issue, runs in runs_by_issue.items():
@@ -419,3 +426,39 @@ class TestRunService:
419
426
  return {
420
427
  "deleted": issue_id
421
428
  }
429
+
430
+ def terminate_stuck_runs(self):
431
+ sct = AVAILABLE_PLUGINS.get("scylla-cluster-tests").model
432
+ now = datetime.utcnow()
433
+ stuck_period = now - timedelta(minutes=45)
434
+ stuck_runs_running = sct.filter(heartbeat__lt=int(
435
+ stuck_period.timestamp()), status=TestStatus.RUNNING.value).allow_filtering().all()
436
+ stuck_runs_created = sct.filter(heartbeat__lt=int(
437
+ stuck_period.timestamp()), status=TestStatus.CREATED.value).allow_filtering().all()
438
+
439
+ all_stuck_runs = [*stuck_runs_running, *stuck_runs_created]
440
+ LOGGER.info("Found %s stuck runs", len(all_stuck_runs))
441
+
442
+ for run in all_stuck_runs:
443
+ LOGGER.info("Will set %s as ABORTED", run.id)
444
+ old_status = run.status
445
+ run.status = TestStatus.ABORTED.value
446
+ run.save()
447
+
448
+ self.create_run_event(
449
+ kind=ArgusEventTypes.TestRunStatusChanged,
450
+ body={
451
+ "message": "Run was automatically terminated due to not responding for more than 45 minutes "
452
+ "(Status changed from {old_status} to {new_status}) by {username}",
453
+ "old_status": old_status,
454
+ "new_status": run.status,
455
+ "username": g.user.username
456
+ },
457
+ user_id=g.user.id,
458
+ run_id=run.id,
459
+ release_id=run.release_id,
460
+ group_id=run.group_id,
461
+ test_id=run.test_id
462
+ )
463
+
464
+ return len(all_stuck_runs)
@@ -59,9 +59,10 @@ class ArgusClient:
59
59
  response_data: JSON = response.json()
60
60
  LOGGER.debug("API Response: %s", response_data)
61
61
  if response_data.get("status") != "ok":
62
+ exc_args = response_data["response"]["arguments"]
62
63
  raise ArgusClientError(
63
64
  f"API Error encountered using endpoint: {response.request.method} {response.request.path_url}",
64
- response_data["response"]["arguments"][0],
65
+ exc_args[0] if len(exc_args) > 0 else response_data.get("response", {}).get("exception", "#NoMessage"),
65
66
  )
66
67
 
67
68
  def get_url_for_endpoint(self, endpoint: str, location_params: dict[str, str] | None) -> str:
@@ -1,6 +1,6 @@
1
1
  from functools import reduce
2
2
  from datetime import datetime
3
- from typing import TypedDict
3
+ from typing import TypedDict, Literal
4
4
  import re
5
5
  import logging
6
6
  from uuid import UUID
@@ -124,7 +124,7 @@ class ArgusDriverMatrixClient(ArgusClient):
124
124
 
125
125
  return total - errors - skipped - failures
126
126
 
127
- def parse_result_xml(self, xml_path: Path, test_type: str, env: dict[str, str] = None) -> RawMatrixTestResult:
127
+ def parse_result_xml(self, xml_path: Path, test_type: Literal['java', 'cpp', 'python']) -> RawMatrixTestResult:
128
128
  with xml_path.open(mode="rt", encoding="utf-8") as xml_file:
129
129
  xml = ElementTree.parse(source=xml_file)
130
130
  LOGGER.info("%s", pformat(xml))
@@ -171,18 +171,18 @@ class ArgusDriverMatrixClient(ArgusClient):
171
171
  "suites": all_suites
172
172
  }
173
173
 
174
- def get_results(self, result_path: str, test_type: str, env: dict[str, str] = None) -> list[RawMatrixTestResult]:
174
+ def get_results(self, result_path: str, test_type: Literal['java', 'cpp', 'python']) -> list[RawMatrixTestResult]:
175
175
  xmls = glob(f"{result_path}/**/*.xml", recursive=True)
176
176
  LOGGER.info("Will use following XMLs: %s", pformat(xmls))
177
177
  results = []
178
178
  for xml in xmls:
179
- result = self.parse_result_xml(Path(xml), test_type, env=env)
179
+ result = self.parse_result_xml(Path(xml), test_type)
180
180
  results.append(result)
181
181
  return results
182
182
 
183
- def submit(self, build_id: str, build_url: str, env_path: str, result_path: str, test_type: str):
183
+ def submit(self, build_id: str, build_url: str, env_path: str, result_path: str, test_type: Literal['java', 'cpp', 'python']):
184
184
  env = self.parse_build_environment(env_path)
185
- results = self.get_results(result_path, test_type, env)
185
+ results = self.get_results(result_path, test_type)
186
186
 
187
187
  self.submit_driver_matrix_run(job_name=build_id, job_url=build_url, test_environment=env, results=results)
188
188
  failures = reduce(lambda total, coll: (coll["failures"] + coll["errors"]) + total, results, 0)
@@ -0,0 +1,151 @@
1
+ import json
2
+ import logging
3
+ from glob import glob, escape
4
+ from pathlib import Path
5
+ from typing import TypedDict
6
+ from xml.etree import ElementTree
7
+
8
+ from argus.backend.plugins.sirenada.types import RawSirenadaRequest, RawSirenadaTestCase
9
+
10
+ from argus.client.base import ArgusClient
11
+
12
+
13
+ LOGGER = logging.getLogger(__name__)
14
+
15
+ class SirenadaEnv(TypedDict):
16
+ SIRENADA_JOB_ID: str
17
+ SIRENADA_BROWSER: str
18
+ SIRENADA_REGION: str
19
+ SIRENADA_CLUSTER: str
20
+ WORKSPACE: str
21
+ BUILD_NUMBER: str
22
+ BUILD_URL: str
23
+ JOB_NAME: str
24
+
25
+
26
+ class TestCredentials(TypedDict):
27
+ SIRENADA_TEST_ID: str
28
+ SIRENADA_USER_NAME: str
29
+ SIRENADA_USER_PASS: str
30
+ SIRENADA_OTP_SECRET: str
31
+ ClusterID: str
32
+ region: str
33
+
34
+ class ArgusSirenadaClient(ArgusClient):
35
+ test_type = "sirenada"
36
+ schema_version: None = "v1"
37
+ _junit_xml_filename = "junit_results.xml"
38
+ _credentials_filename = "test_credentials.json"
39
+ _bucket_url_template = "https://sirenada-results.s3.us-east-1.amazonaws.com/{s3_id}/{test_results_dir}/{filename}"
40
+ TEST_TAG_MAPPING = {
41
+ "failure": "failed",
42
+ "error": "aborted",
43
+ "skipped": "skipped"
44
+ }
45
+
46
+ def __init__(self, auth_token: str, base_url: str, api_version="v1") -> None:
47
+ self.results_path: Path | None = None
48
+ super().__init__(auth_token, base_url, api_version)
49
+
50
+ def _verify_required_files_exist(self, results_path: Path):
51
+ assert (results_path / self._junit_xml_filename).exists(), "Missing jUnit XML results file!"
52
+ assert (results_path / self._credentials_filename).exists(), "Missing test credentials file!"
53
+
54
+ @staticmethod
55
+ def _verify_env(env: SirenadaEnv):
56
+ for key in SirenadaEnv.__annotations__.keys():
57
+ assert env.get(key), f"Missing required key {key} in the environment"
58
+
59
+ def _set_files_for_failed_test(self, test_case: RawSirenadaTestCase):
60
+ test_results_dir = f"sirenada-{test_case['sirenada_test_id']}"
61
+ results_dir: Path = self.results_path / test_results_dir
62
+
63
+ just_class_name = test_case.get("class_name").split(".")[-1]
64
+ requests_pattern = f"{just_class_name}-{test_case.get('test_name')}.requests.json"
65
+ screenshot_pattern = f"{just_class_name}-{escape(test_case.get('test_name'))}-*.png"
66
+
67
+ if not results_dir.exists():
68
+ return
69
+
70
+ if not (s3_bucket_id_path := (results_dir / "s3_id")).exists():
71
+ return
72
+
73
+ with open(s3_bucket_id_path, "rt") as s3_id_file:
74
+ test_case["s3_folder_id"] = s3_id_file.read().strip()
75
+
76
+ test_case["requests_file"] = self._bucket_url_template.format(
77
+ s3_id=test_case.get("s3_folder_id"),
78
+ test_results_dir=test_results_dir,
79
+ filename=requests_pattern,
80
+ )
81
+
82
+ screenshots = glob(pathname=screenshot_pattern, root_dir=results_dir)
83
+ if len(screenshots) > 0:
84
+ test_case["screenshot_file"] = self._bucket_url_template.format(
85
+ s3_id=test_case.get("s3_folder_id"),
86
+ test_results_dir=test_results_dir,
87
+ filename=screenshots[0],
88
+ )
89
+
90
+ def _set_case_status(self, raw_case: RawSirenadaTestCase, junit_case: ElementTree.Element):
91
+ subelements = list(junit_case.iter())
92
+ if len(subelements) == 2:
93
+ status_node = subelements[1]
94
+ raw_case["status"] = self.TEST_TAG_MAPPING.get(status_node.tag, "failed")
95
+ raw_case["message"] = status_node.get("message", "#NO_MESSAGE")
96
+ raw_case["stack_trace"] = status_node.text
97
+ self._set_files_for_failed_test(raw_case)
98
+ else:
99
+ raw_case["status"] = "passed"
100
+
101
+ def _parse_junit_results(self, junit_xml: Path, credentials: TestCredentials, env: SirenadaEnv) -> list[RawSirenadaTestCase]:
102
+ etree = ElementTree.parse(source=junit_xml)
103
+ cases = list(etree.iter("testcase"))
104
+ assert len(cases) > 0, "No test cases found in test results XML."
105
+
106
+ parsed_cases: list[RawSirenadaTestCase] = []
107
+ for case in cases:
108
+ raw_case: RawSirenadaTestCase = {}
109
+ raw_case["class_name"] = case.get("classname", "#NO_CLASSNAME")
110
+ raw_case["file_name"] = case.get("file", "#NO_FILE")
111
+ raw_case["test_name"] = case.get("name", "#NO_TEST_NAME")
112
+ raw_case["browser_type"] = env.get("SIRENADA_BROWSER")
113
+ raw_case["cluster_type"] = env.get("SIRENADA_CLUSTER")
114
+ raw_case["duration"] = float(case.attrib.get("time", "0.0"))
115
+ raw_case["sirenada_test_id"] = credentials.get("SIRENADA_TEST_ID")
116
+ raw_case["sirenada_user"] = credentials.get("SIRENADA_USER_NAME")
117
+ raw_case["sirenada_password"] = credentials.get("SIRENADA_USER_PASS")
118
+ self._set_case_status(raw_case, case)
119
+ parsed_cases.append(raw_case)
120
+
121
+ return parsed_cases
122
+
123
+ @staticmethod
124
+ def _read_credentials(credentials_path: Path) -> TestCredentials:
125
+ with open(credentials_path, "rt") as creds_file:
126
+ credentials: TestCredentials = json.load(creds_file)
127
+
128
+ assert credentials.get("SIRENADA_TEST_ID"), "Credentials are missing required field: SIRENADA_TEST_ID"
129
+ return credentials
130
+
131
+ def submit_sirenada_run(self, env: SirenadaEnv, test_results_path: Path):
132
+ self.results_path = test_results_path
133
+ try:
134
+ self._verify_env(env)
135
+ self._verify_required_files_exist(self.results_path)
136
+ except AssertionError as exc:
137
+ LOGGER.critical("%s", exc.args[0])
138
+ raise exc
139
+
140
+ credentials = self._read_credentials(self.results_path / self._credentials_filename)
141
+ results = self._parse_junit_results(junit_xml=self.results_path / self._junit_xml_filename, credentials=credentials, env=env)
142
+ request_body: RawSirenadaRequest = {}
143
+
144
+ request_body["run_id"] = env.get("SIRENADA_JOB_ID")
145
+ request_body["build_id"] = env.get("JOB_NAME")
146
+ request_body["build_job_url"] = env.get("BUILD_URL")
147
+ request_body["region"] = env.get("SIRENADA_REGION")
148
+ request_body["results"] = results
149
+
150
+ response = self.submit_run(run_type=self.test_type, run_body=request_body)
151
+ self.check_response(response)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "argus-alm"
3
- version = "0.10.1b4"
3
+ version = "0.11.0"
4
4
  description = "Argus"
5
5
  authors = ["Alexey Kartashov <alexey.kartashov@scylladb.com>"]
6
6
  license = "Apache-2.0"
@@ -15,19 +15,17 @@ optional = true
15
15
  optional = true
16
16
 
17
17
  [tool.poetry.dependencies]
18
+ requests = "^2.26.0"
18
19
  python = "^3.10"
19
- pydantic = "^1.8.2"
20
- PyYAML = "^5.4.1"
21
- scylla-driver = "^3.24.8"
22
-
23
20
 
24
21
  [tool.poetry.group.web-backend.dependencies]
22
+ PyYAML = "^5.4.1"
23
+ scylla-driver = "^3.24.8"
25
24
  Flask = "2.0.2"
26
25
  Flask-WTF = "1.0.0"
27
26
  Flask-Login = "0.5.0"
28
27
  humanize = "3.13.1"
29
28
  python-magic = "0.4.24"
30
- requests = "2.26.0"
31
29
  uwsgi = "2.0.20"
32
30
  python-jenkins = "^1.7.0"
33
31
  python-slugify = "^6.1.1"
@@ -10,24 +10,24 @@ packages = \
10
10
  'argus.backend.plugins',
11
11
  'argus.backend.plugins.driver_matrix_tests',
12
12
  'argus.backend.plugins.sct',
13
+ 'argus.backend.plugins.sirenada',
13
14
  'argus.backend.service',
14
15
  'argus.backend.util',
15
16
  'argus.client',
16
17
  'argus.client.driver_matrix_tests',
17
18
  'argus.client.sct',
19
+ 'argus.client.sirenada',
18
20
  'argus.db']
19
21
 
20
22
  package_data = \
21
23
  {'': ['*']}
22
24
 
23
25
  install_requires = \
24
- ['PyYAML>=5.4.1,<6.0.0',
25
- 'pydantic>=1.8.2,<2.0.0',
26
- 'scylla-driver>=3.24.8,<4.0.0']
26
+ ['requests>=2.26.0,<3.0.0']
27
27
 
28
28
  setup_kwargs = {
29
29
  'name': 'argus-alm',
30
- 'version': '0.10.1b4',
30
+ 'version': '0.11.0',
31
31
  'description': 'Argus',
32
32
  'long_description': '# Argus\n\n## Description\n\nArgus is a test tracking system intended to provide observability into automated test pipelines which use long-running resources. It allows observation of a test status, its events and its allocated resources. It also allows easy comparison between particular runs of a specific test.\n\n## Installation notes\n\n### Prerequisites\n\n- Python >=3.10.0 (system-wide or pyenv)\n\n- NodeJS >=16 (with npm)\n\n- Yarn (can be installed globally with `npm -g install yarn`)\n\n- nginx\n\n- poetry >=1.2.0b1\n\n### From source\n\n#### Production\n\nPerform the following steps:\n\nCreate a user that will be used by uwsgi:\n\n```bash\nuseradd -m -s /bin/bash argus\nsudo -iu argus\n```\n\n(Optional) Install pyenv and create a virtualenv for this user:\n\n```bash\npyenv install 3.10.0\npyenv virtualenv argus\npyenv activate argus\n```\n\nClone the project into a directory somewhere where user has full write permissions\n\n```bash\ngit clone https://github.com/scylladb/argus ~/app\ncd ~/app\n```\n\nInstall project dependencies:\n\n```bash\npoetry install --with default,dev,web-backend,docker-image\nyarn install\n```\n\nCompile frontend files from `/frontend` into `/public/dist`\n\n```bash\nyarn webpack\n```\n\nCreate a `argus.local.yaml` configuration file (used to configure database connection) and a `argus_web.yaml` (used for webapp secrets) in your application install directory.\n\n```bash\ncp argus_web.example.yaml argus_web.yaml\ncp argus.yaml argus.local.yaml\n```\n\nOpen `argus.local.yaml` and add the database connection information (contact_points, user, password and keyspace name).\n\nOpen `argus_web.yaml` and change the `SECRET_KEY` value to something secure, like a sha512 digest of random bytes. Fill out GITHUB_* variables with their respective values.\n\nCopy nginx configuration file from `docs/configs/argus.nginx.conf` to nginx virtual hosts directory:\n\nUbuntu:\n\n```bash\nsudo cp docs/configs/argus.nginx.conf /etc/nginx/sites-available/argus\nsudo ln -s /etc/nginx/sites-enabled/argus /etc/nginx/sites-available/argus\n```\n\nRHEL/Centos/Alma/Fedora:\n\n```bash\nsudo cp docs/configs/argus.nginx.conf /etc/nginx/conf.d/argus.conf\n```\n\nAdjust the webhost settings in that file as necessary, particularly `listen` and `server_name` directives.\n\nCopy systemd service file from `docs/config/argus.service` to `/etc/systemd/system` directory:\n\n```bash\nsudo cp docs/config/argus.service /etc/systemd/system\n```\n\nOpen it and adjust the path to the `start_argus.sh` script in the `ExecStart=` directive and the user/group, then reload systemd daemon configuration and enable (and optionally start) the service.\n\nWARNING: `start_argus.sh` assumes pyenv is installed into `~/.pyenv`\n\n```bash\nsudo systemctl daemon-reload\nsudo systemctl enable --now argus.service\n```\n\n#### Development\n\nClone the project into a directory somewhere\n\n```bash\ngit clone https://github.com/scylladb/argus\ncd argus\n```\n\nInstall project dependencies:\n\n```bash\npoetry install --with default,dev,web-backend,docker-image\nyarn install\n```\n\nCompile frontend files from `/frontend` into `/public/dist`. Add --watch to recompile files on change.\n\n```bash\nyarn webpack --watch\n```\n##### Configuration\nCreate a `argus.local.yaml` configuration file (used to configure database connection) and a `argus_web.yaml` (used for webapp secrets) in your application install directory.\n\nSee `Production` section for more details.\nTo configure Github authentication follow steps:\n1. Authorize OAuth App\n 1. go to your Account Settings (top right corner) -> Developer settings (left pane) -> OAuth Apps\n 2. Click Create New OAuth App button\n 3. Fill the fields (app name: `argus-dev`, homepage URL `http://localhost:5000`, Auth callback URL: `http://localhost:5000/profile/oauth/github`)\n 4. Confirm and get the tokens/ids required for config\n2. Create Jenkins token for your account\n 1. Go to `Configure` in top right corner\n 2. Click `Add new Token`\n 3. Get it and paste to config to `JENKINS_API_TOKEN` param\n##### Database Initialization\n\nYou can initialize a scylla cluster in any way you like, either using docker image with docker-compose or using cassandra cluster manager. You will need to create the keyspace manually before you can sync database models.\n\nCreate keyspace according to your configuration.\ne.g. (need to test if it works with RF=1 if not, make it 3)\n```\nCREATE KEYSPACE argus WITH replication = {\'class\': \'SimpleStrategy\', \'replication_factor\' : 1}\n```\n\nInitial sync can be done as follows:\n\n```py\nfrom argus.backend.db import ScyllaCluster\nfrom argus.db.testrun import TestRun\ndb = ScyllaCluster.get()\n\ndb.sync_models() # Syncronizes Object Mapper models\nTestRun.init_own_table() # Syncronizes TestRun table (separate from python-driver Object Mapper)\n\n```\n\nYou can also use `flask sync-models` afterwards during development when making small changes to models.\n\nIt is recommended to set up jenkins api key and run `flask scan-jenkins` afterwards to get basic release/group/test structure.\n\nThere are scripts in `./scripts` directory that can be used to download data from production, upload them into your dev db and fix their relations to other models in your instance of the application. Specifically, `download_runs_from_prod.py` requires additional config, `argus.local.prod.yaml` which is the config used to connect to the production cluster. The scripts are split to prevent mistakes and accidentally affecting production cluster.\n\n##### Configuration\n\nCreate a `argus.local.yaml` configuration file (used to configure database connection) and a `argus_web.yaml` (used for webapp secrets) in your application install directory.\n\n```bash\ncp argus_web.example.yaml argus_web.yaml\ncp argus.yaml argus.local.yaml\n```\n\nOpen `argus.local.yaml` and add the database connection information (contact_points, user, password and keyspace name).\n\nOpen `argus_web.yaml` and change the `SECRET_KEY` value to something secure, like a sha512 digest of random bytes. Fill out GITHUB_* and JENKINS_* variables with their respective values.\n\nRun the application from CLI using:\n\n```bash\nFLASK_ENV="development" FLASK_APP="argus_backend:start_server" FLASK_DEBUG=1 CQLENG_ALLOW_SCHEMA_MANAGEMENT=1 flask run\n```\n\nOmit `FLASK_DEBUG` if running your own debugger (pdb, pycharm, vscode)\n',
33
33
  'author': 'Alexey Kartashov',
File without changes
File without changes