Skip to content

Commit 2f1cc1a

Browse files
committed
feat(notifications): add hardware_summary action
Add new hardware_summary action in the notifications management command. This action generates hardware reports based on the subscription file. Signed-off-by: Yushan Li <[email protected]>
1 parent 9efa32f commit 2f1cc1a

File tree

11 files changed

+438
-1
lines changed

11 files changed

+438
-1
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Add a subscription file for your hardware platform by following this format.
2+
# Place the file under: dashboard/backend/data/notifications/subscriptions
3+
# The file name must end with _hardware.yml or _hardware.yaml
4+
5+
qcs9100-ride: # The hardware platform name
6+
origin: maestro
7+
default_recipients:
8+
- Recipient One <[email protected]>
9+
- Recipient Two <[email protected]>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
qcs615-ride:
2+
origin: maestro
3+
default_recipients:
4+
- Trilok Soni <[email protected]>
5+
- Shiraz Hashim <[email protected]>
6+
- Yogesh Lal <[email protected]>
7+
- Yijie Yang <[email protected]>
8+
- Yushan Li <[email protected]>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
qcs6490-rb3gen2:
2+
origin: maestro
3+
default_recipients:
4+
- Trilok Soni <[email protected]>
5+
- Shiraz Hashim <[email protected]>
6+
- Yogesh Lal <[email protected]>
7+
- Yijie Yang <[email protected]>
8+
- Yushan Li <[email protected]>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
qcs8300-ride:
2+
origin: maestro
3+
default_recipients:
4+
- Trilok Soni <[email protected]>
5+
- Shiraz Hashim <[email protected]>
6+
- Yogesh Lal <[email protected]>
7+
- Yijie Yang <[email protected]>
8+
- Yushan Li <[email protected]>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
qcs9100-ride:
2+
origin: maestro
3+
default_recipients:
4+
- Trilok Soni <[email protected]>
5+
- Shiraz Hashim <[email protected]>
6+
- Yogesh Lal <[email protected]>
7+
- Yijie Yang <[email protected]>
8+
- Yushan Li <[email protected]>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
x1e80100:
2+
origin: maestro
3+
default_recipients:
4+
- Trilok Soni <[email protected]>
5+
- Shiraz Hashim <[email protected]>
6+
- Yogesh Lal <[email protected]>
7+
- Yijie Yang <[email protected]>
8+
- Yushan Li <[email protected]>
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
from kernelCI_app.typeModels.common import StatusCount
2+
from kernelCI_app.typeModels.hardwareListing import HardwareItem
3+
4+
5+
def sanitize_hardware(
6+
hardware: dict,
7+
) -> HardwareItem:
8+
"""Sanitizes a HardwareItem that was returned by a 'hardwarelisting-like' query
9+
10+
Returns a HardwareItem object"""
11+
hardware_name = hardware["hardware"]
12+
platform = hardware["platform"]
13+
14+
build_status_summary = StatusCount(
15+
PASS=hardware["pass_builds"],
16+
FAIL=hardware["fail_builds"],
17+
NULL=hardware["null_builds"],
18+
ERROR=hardware["error_builds"],
19+
MISS=hardware["miss_builds"],
20+
DONE=hardware["done_builds"],
21+
SKIP=hardware["skip_builds"],
22+
)
23+
24+
test_status_summary = StatusCount(
25+
PASS=hardware["pass_tests"],
26+
FAIL=hardware["fail_tests"],
27+
NULL=hardware["null_tests"],
28+
ERROR=hardware["error_tests"],
29+
MISS=hardware["miss_tests"],
30+
DONE=hardware["done_tests"],
31+
SKIP=hardware["skip_tests"],
32+
)
33+
34+
boot_status_summary = StatusCount(
35+
PASS=hardware["pass_boots"],
36+
FAIL=hardware["fail_boots"],
37+
NULL=hardware["null_boots"],
38+
ERROR=hardware["error_boots"],
39+
MISS=hardware["miss_boots"],
40+
DONE=hardware["done_boots"],
41+
SKIP=hardware["skip_boots"],
42+
)
43+
44+
return HardwareItem(
45+
hardware=hardware_name,
46+
platform=platform,
47+
test_status_summary=test_status_summary,
48+
boot_status_summary=boot_status_summary,
49+
build_status_summary=build_status_summary,
50+
)

backend/kernelCI_app/management/commands/helpers/summary.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
type TreeKey = tuple[str, str, str]
2020
"""A tuple (branch, giturl, origin)"""
2121

22+
type HardwareKey = tuple[str, str]
23+
"""A tuple (hardware, origin)"""
24+
2225
type ReportConfigs = list[dict[str, Any]]
2326
"""A list of dictionaries containing the definition/configuration of a report"""
2427

@@ -163,3 +166,59 @@ def get_build_issues_from_checkout(
163166
result_checkout_issues[checkout_id].append(checkout_issue)
164167

165168
return result_checkout_issues, checkout_builds_without_issues
169+
170+
171+
def process_hardware_submissions_files(
172+
*,
173+
base_dir: Optional[str] = None,
174+
signup_folder: Optional[str] = None,
175+
hardware_origins: Optional[list[str]] = None,
176+
) -> tuple[set[HardwareKey], dict[HardwareKey, ReportConfigs]]:
177+
"""Processes all hardware submission files and returns the set of HardwareKey and
178+
the dict linking each hardware to its report props"""
179+
(base_dir, signup_folder) = _assign_default_folders(
180+
base_dir=base_dir, signup_folder=signup_folder
181+
)
182+
183+
hardware_key_set: set[HardwareKey] = set()
184+
hardware_prop_map: dict[HardwareKey, ReportConfigs] = {}
185+
"""Example:
186+
hardware_prop_map[(imx6q-sabrelite, maestro)] = [
187+
{
188+
default_recipients: []
189+
},
190+
...
191+
]
192+
"""
193+
194+
full_path = os.path.join(base_dir, signup_folder)
195+
for filename in os.listdir(full_path):
196+
"""Example:
197+
Filename: imx6q_hardware.yaml
198+
199+
imx6q-sabrelite:
200+
origin: maestro
201+
default_recipients:
202+
- Recipient One <[email protected]>
203+
- Recipient Two <[email protected]>
204+
"""
205+
if filename.endswith("_hardware.yaml") or filename.endswith("_hardware.yml"):
206+
file_path = os.path.join(signup_folder, filename)
207+
file_data = read_yaml_file(base_dir=base_dir, file=file_path)
208+
for hardware_name, hardware_values in file_data.items():
209+
origin = hardware_values.get("origin", DEFAULT_ORIGIN)
210+
if hardware_origins is not None and origin not in hardware_origins:
211+
continue
212+
default_recipients = hardware_values.get("default_recipients", [])
213+
214+
hardware_key = (hardware_name, origin)
215+
hardware_key_set.add(hardware_key)
216+
if hardware_prop_map.get(hardware_key) is None:
217+
hardware_prop_map[hardware_key] = []
218+
hardware_prop_map[hardware_key].append({"default_recipients": default_recipients})
219+
else:
220+
log_message(
221+
f"Skipping file {filename} on loading summary files. Not a hardware yaml file."
222+
)
223+
224+
return hardware_key_set, hardware_prop_map

backend/kernelCI_app/management/commands/notifications.py

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import sys
66

77
from collections import defaultdict
8-
from datetime import datetime, timezone
8+
from datetime import datetime, timezone, timedelta
99
from types import SimpleNamespace
1010
from urllib.parse import quote_plus
1111

@@ -29,6 +29,7 @@
2929
TreeKey,
3030
get_build_issues_from_checkout,
3131
process_submissions_files,
32+
process_hardware_submissions_files,
3233
)
3334
from kernelCI_app.queries.notifications import (
3435
get_checkout_summary_data,
@@ -55,6 +56,9 @@
5556
from kernelCI_cache.typeModels.databases import PossibleIssueType
5657
from kernelCI_app.utils import group_status
5758

59+
from kernelCI_app.queries.hardware import get_hardware_summary_data, get_hardware_listing_data_bulk
60+
from kernelCI_app.helpers.hardwares import sanitize_hardware
61+
5862
KERNELCI_RESULTS = "[email protected]"
5963
KERNELCI_REPLYTO = "[email protected]"
6064
REGRESSIONS_LIST = "[email protected]"
@@ -723,6 +727,93 @@ def generate_test_report(*, service, test_id, email_args, signup_folder):
723727
)
724728

725729

730+
def generate_hardware_summary_report(
731+
*,
732+
service,
733+
hardware_origins: Optional[list[str]],
734+
email_args,
735+
signup_folder: Optional[str] = None,
736+
):
737+
"""Generate monthly hardware reports for hardware submission file."""
738+
739+
now = datetime.now(timezone.utc)
740+
start_date = now - timedelta(days=30)
741+
end_date = now
742+
743+
# process the hardware submission files
744+
hardware_key_set, hardware_prop_map = process_hardware_submissions_files(
745+
signup_folder=signup_folder,
746+
hardware_origins=hardware_origins,
747+
)
748+
749+
# get detailed data for all hardware
750+
hardwares_data_raw = get_hardware_summary_data(
751+
keys=list(hardware_key_set),
752+
start_date=start_date,
753+
end_date=end_date,
754+
)
755+
hardwares_data_dict = defaultdict(list)
756+
for raw in hardwares_data_raw:
757+
try:
758+
environment_misc = json.loads(raw.get("environment_misc", "{}"))
759+
except json.JSONDecodeError:
760+
print(f'Error decoding JSON for key: {raw.get("environment_misc")}')
761+
continue
762+
hardware_id = environment_misc.get("platform")
763+
origin = raw.get("test_origin")
764+
key = (hardware_id, origin)
765+
hardwares_data_dict[key].append(raw)
766+
767+
# get the total build/boot/test counts for each hardware
768+
hardwares_list_raw = get_hardware_listing_data_bulk(
769+
keys=list(hardware_key_set),
770+
start_date=start_date,
771+
end_date=end_date,
772+
)
773+
774+
# Iterate through each hardware record to render report, extract recipient, send email
775+
for (hardware_id, origin), hardware_data in hardwares_data_dict.items():
776+
hardware_raw = next(
777+
(row for row in hardwares_list_raw if row.get("platform") == hardware_id),
778+
None
779+
)
780+
781+
hardware_item = sanitize_hardware(hardware_raw)
782+
build_status_group = group_status(hardware_item.build_status_summary)
783+
boot_status_group = group_status(hardware_item.boot_status_summary)
784+
test_status_group = group_status(hardware_item.test_status_summary)
785+
786+
# render the template
787+
template = setup_jinja_template("hardware_report.txt.j2")
788+
report = {}
789+
report["content"] = template.render(
790+
hardware_id=hardware_id,
791+
hardware_data=hardware_data,
792+
build_status_group=build_status_group,
793+
boot_status_group=boot_status_group,
794+
test_status_group=test_status_group
795+
)
796+
report["title"] = f"hardware {hardware_id} summary - {now.strftime("%Y-%m-%d %H:%M %Z")}"
797+
798+
# extract recipient
799+
for (hardware_id, origin), report_configs in hardware_prop_map.items():
800+
for hardware_report in report_configs:
801+
recipients = process_submission_options(
802+
default_recipients=hardware_report.get("default_recipients", []),
803+
specific_recipients=hardware_report.get("recipients", []),
804+
options=hardware_report.get("options", []),
805+
)
806+
807+
# send email
808+
send_email_report(
809+
service=service,
810+
report=report,
811+
email_args=email_args,
812+
signup_folder=signup_folder,
813+
recipients=recipients
814+
)
815+
816+
726817
def run_fake_report(*, service, email_args):
727818
report = {}
728819
report["content"] = "Testing the email sending path..."
@@ -787,6 +878,7 @@ def add_arguments(self, parser):
787878
"summary",
788879
"fake_report",
789880
"test_report",
881+
"hardware_summary",
790882
],
791883
help="Action to perform: new_issues, issue_report, summary, fake_report, or test_report",
792884
)
@@ -841,6 +933,13 @@ def add_arguments(self, parser):
841933
help="Add recipients for the given tree name (fake_report only)",
842934
)
843935

936+
# hardware summary report specific arguments
937+
parser.add_argument(
938+
"--hardware-origins",
939+
type=lambda s: [origin.strip() for origin in s.split(",")],
940+
help="Limit hardware summary to specific origins (hardware summary only, comma-separated list)",
941+
)
942+
844943
def handle(self, *args, **options):
845944
# Setup connections
846945
service = smtp_setup_connection()
@@ -929,3 +1028,13 @@ def handle(self, *args, **options):
9291028
self.stdout.write(
9301029
self.style.SUCCESS(f"Test report generated for test {test_id}")
9311030
)
1031+
1032+
elif action == "hardware_summary":
1033+
email_args.update = options.get("update_storage", False)
1034+
hardware_origins = options.get("hardware_origins")
1035+
generate_hardware_summary_report(
1036+
service=service,
1037+
signup_folder=signup_folder,
1038+
email_args=email_args,
1039+
hardware_origins=hardware_origins,
1040+
)
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{% extends "base.txt" %}
2+
{% block header %}{% endblock %}
3+
{% block content %}
4+
Hello,
5+
6+
Status summary for {{ hardware_id }}
7+
8+
Builds:{{ "\t" }}{{ "{:>5}".format(build_status_group["success"]) }} ✅
9+
{{- "{:>5}".format(build_status_group["failed"]) }} ❌
10+
{{- "{:>5}".format(build_status_group["inconclusive"]) }} ⚠️
11+
Boots: {{ "\t" }}{{ "{:>5}".format(boot_status_group["success"]) }} ✅
12+
{{- "{:>5}".format(boot_status_group["failed"]) }} ❌
13+
{{- "{:>5}".format(boot_status_group["inconclusive"]) }} ⚠️
14+
Tests: {{ "\t" }}{{ "{:>5}".format(test_status_group["success"]) }} ✅
15+
{{- "{:>5}".format(test_status_group["failed"]) }} ❌
16+
{{- "{:>5}".format(test_status_group["inconclusive"]) }} ⚠️
17+
-------------
18+
{% for data in hardware_data %}
19+
#kernelci test {{ data["id"] }}
20+
- Status: {{ data["status"] }}
21+
- Comment: {{ data["comment"] }}
22+
- Starttime: {{ data["start_time"] }}
23+
- Tree: {{ data["build__checkout__tree_name"] }}/{{ data["build__checkout__git_repository_branch"] }}
24+
- Origin: {{ data["test_origin"] }}
25+
- Test_lab: {{ data["runtime"] }}
26+
- Commit: {{ data["build__checkout__git_commit_hash"] }}
27+
- Dashboard: https://dashboard.kernelci.org/test/{{ data["id"] }}
28+
-------------
29+
{% endfor %}
30+
{%- endblock -%}

0 commit comments

Comments
 (0)