Skip to content

Commit e37285a

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 e37285a

File tree

13 files changed

+467
-1
lines changed

13 files changed

+467
-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]>

backend/kernelCI/settings.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,16 @@ def get_json_env_var(name, default):
169169
"--summary-origins=maestro",
170170
],
171171
),
172+
(
173+
"33 3 * * 1",
174+
"django.core.management.call_command",
175+
[
176+
"notifications",
177+
"--action=hardware_summary",
178+
"--send",
179+
"--yes",
180+
],
181+
),
172182
]
173183

174184
# Email settings for SMTP backend
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: 61 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,61 @@ 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, dict[str, Any]]]:
177+
"""
178+
Processes all hardware submission files and returns the set of HardwareKey
179+
and the dict linking each hardware to its default recipients
180+
"""
181+
(base_dir, signup_folder) = _assign_default_folders(
182+
base_dir=base_dir, signup_folder=signup_folder
183+
)
184+
185+
hardware_key_set: set[HardwareKey] = set()
186+
hardware_prop_map: dict[HardwareKey, dict[str, Any]] = {}
187+
"""Example:
188+
hardware_prop_map[(imx6q-sabrelite, maestro)] = {
189+
"default_recipients": [
190+
"Recipient One <[email protected]>",
191+
"Recipient Two <[email protected]>"
192+
]
193+
}
194+
"""
195+
196+
full_path = os.path.join(base_dir, signup_folder)
197+
for filename in os.listdir(full_path):
198+
"""Example:
199+
Filename: imx6q_hardware.yaml
200+
201+
imx6q-sabrelite:
202+
origin: maestro
203+
default_recipients:
204+
- Recipient One <[email protected]>
205+
- Recipient Two <[email protected]>
206+
"""
207+
if filename.endswith("_hardware.yaml") or filename.endswith("_hardware.yml"):
208+
file_path = os.path.join(signup_folder, filename)
209+
file_data = read_yaml_file(base_dir=base_dir, file=file_path)
210+
for hardware_name, hardware_values in file_data.items():
211+
origin = hardware_values.get("origin", DEFAULT_ORIGIN)
212+
if hardware_origins is not None and origin not in hardware_origins:
213+
continue
214+
default_recipients = hardware_values.get("default_recipients", [])
215+
216+
hardware_key = (hardware_name, origin)
217+
hardware_key_set.add(hardware_key)
218+
hardware_prop_map[hardware_key] = {
219+
"default_recipients": default_recipients
220+
}
221+
else:
222+
log_message(
223+
f"Skipping file {filename} on loading summary files. Not a hardware yaml file."
224+
)
225+
226+
return hardware_key_set, hardware_prop_map

backend/kernelCI_app/management/commands/notifications.py

Lines changed: 121 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,12 @@
5556
from kernelCI_cache.typeModels.databases import PossibleIssueType
5657
from kernelCI_app.utils import group_status
5758

59+
from kernelCI_app.queries.hardware import (
60+
get_hardware_summary_data,
61+
get_hardware_listing_data_bulk,
62+
)
63+
from kernelCI_app.helpers.hardwares import sanitize_hardware
64+
5865
KERNELCI_RESULTS = "[email protected]"
5966
KERNELCI_REPLYTO = "[email protected]"
6067
REGRESSIONS_LIST = "[email protected]"
@@ -723,6 +730,101 @@ def generate_test_report(*, service, test_id, email_args, signup_folder):
723730
)
724731

725732

733+
def generate_hardware_summary_report(
734+
*,
735+
service,
736+
hardware_origins: Optional[list[str]],
737+
email_args,
738+
signup_folder: Optional[str] = None,
739+
):
740+
"""
741+
Generate weekly hardware reports for hardware submission file.
742+
Emails are sent only for hardware with failed tests.
743+
"""
744+
745+
now = datetime.now(timezone.utc)
746+
start_date = now - timedelta(days=7)
747+
end_date = now
748+
749+
# process the hardware submission files
750+
hardware_key_set, hardware_prop_map = process_hardware_submissions_files(
751+
signup_folder=signup_folder,
752+
hardware_origins=hardware_origins,
753+
)
754+
755+
# get detailed data for all hardware
756+
hardwares_data_raw = get_hardware_summary_data(
757+
keys=list(hardware_key_set),
758+
start_date=start_date,
759+
end_date=end_date,
760+
)
761+
762+
if not hardwares_data_raw:
763+
print("No data for hardware summary")
764+
return
765+
766+
hardwares_data_dict = defaultdict(list)
767+
for raw in hardwares_data_raw:
768+
try:
769+
environment_misc = json.loads(raw.get("environment_misc", "{}"))
770+
misc = json.loads(raw.get("misc", "{}"))
771+
except json.JSONDecodeError:
772+
print(f'Error decoding JSON for key: {raw.get("environment_misc")}')
773+
continue
774+
hardware_id = environment_misc.get("platform")
775+
raw["job_id"] = environment_misc.get("job_id")
776+
raw["runtime"] = misc.get("runtime")
777+
origin = raw.get("test_origin")
778+
key = (hardware_id, origin)
779+
hardwares_data_dict[key].append(raw)
780+
781+
# get the total build/boot/test counts for each hardware
782+
hardwares_list_raw = get_hardware_listing_data_bulk(
783+
keys=list(hardware_key_set),
784+
start_date=start_date,
785+
end_date=end_date,
786+
)
787+
788+
# Iterate through each hardware record to render report, extract recipient, send email
789+
for (hardware_id, origin), hardware_data in hardwares_data_dict.items():
790+
hardware_raw = next(
791+
(row for row in hardwares_list_raw if row.get("platform") == hardware_id),
792+
None,
793+
)
794+
795+
hardware_item = sanitize_hardware(hardware_raw)
796+
build_status_group = group_status(hardware_item.build_status_summary)
797+
boot_status_group = group_status(hardware_item.boot_status_summary)
798+
test_status_group = group_status(hardware_item.test_status_summary)
799+
800+
# render the template
801+
template = setup_jinja_template("hardware_report.txt.j2")
802+
report = {}
803+
report["content"] = template.render(
804+
hardware_id=hardware_id,
805+
hardware_data=hardware_data,
806+
build_status_group=build_status_group,
807+
boot_status_group=boot_status_group,
808+
test_status_group=test_status_group,
809+
)
810+
report["title"] = (
811+
f"hardware {hardware_id} summary - {now.strftime("%Y-%m-%d %H:%M %Z")}"
812+
)
813+
814+
# extract recipient
815+
hardware_report = hardware_prop_map.get((hardware_id, origin), {})
816+
recipients = hardware_report.get("default_recipients", [])
817+
818+
# send email
819+
send_email_report(
820+
service=service,
821+
report=report,
822+
email_args=email_args,
823+
signup_folder=signup_folder,
824+
recipients=recipients,
825+
)
826+
827+
726828
def run_fake_report(*, service, email_args):
727829
report = {}
728830
report["content"] = "Testing the email sending path..."
@@ -787,6 +889,7 @@ def add_arguments(self, parser):
787889
"summary",
788890
"fake_report",
789891
"test_report",
892+
"hardware_summary",
790893
],
791894
help="Action to perform: new_issues, issue_report, summary, fake_report, or test_report",
792895
)
@@ -841,6 +944,13 @@ def add_arguments(self, parser):
841944
help="Add recipients for the given tree name (fake_report only)",
842945
)
843946

947+
# hardware summary report specific arguments
948+
parser.add_argument(
949+
"--hardware-origins",
950+
type=lambda s: [origin.strip() for origin in s.split(",")],
951+
help="Limit hardware summary to specific origins (hardware summary only, comma-separated list)",
952+
)
953+
844954
def handle(self, *args, **options):
845955
# Setup connections
846956
service = smtp_setup_connection()
@@ -929,3 +1039,13 @@ def handle(self, *args, **options):
9291039
self.stdout.write(
9301040
self.style.SUCCESS(f"Test report generated for test {test_id}")
9311041
)
1042+
1043+
elif action == "hardware_summary":
1044+
email_args.update = options.get("update_storage", False)
1045+
hardware_origins = options.get("hardware_origins")
1046+
generate_hardware_summary_report(
1047+
service=service,
1048+
signup_folder=signup_folder,
1049+
email_args=email_args,
1050+
hardware_origins=hardware_origins,
1051+
)

0 commit comments

Comments
 (0)