Skip to content

Commit cfb51c6

Browse files
Test Module Report - TLS Module (#1419) (#1423)
* Test Module Report - TLS Module (#1419) * Generate html report for tls module --------- Co-authored-by: Aliaksandr Nikitsin <aliaksandrn@google.com> * Adds html report * Fix pylint * Adds html report * Adds html report * change module reports extension to html * rename module report file * rename report * remane report * pylint * fix dns module report --------- Co-authored-by: Aliaksandr Nikitsin <aliaksandrn@google.com>
1 parent 29095ba commit cfb51c6

File tree

11 files changed

+264
-27
lines changed

11 files changed

+264
-27
lines changed

framework/python/src/test_orc/test_orchestrator.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import copy
1616
import os
1717
import json
18+
import pathlib
1819
import re
1920
import time
2021
import shutil
@@ -234,6 +235,8 @@ def _write_reports(self, test_report):
234235

235236
util.run_command(f"chown -R {self._host_user} {out_dir}")
236237

238+
self._cleanup_modules_html_reports(out_dir)
239+
237240
def _generate_report(self):
238241

239242
device = self.get_session().get_target_device()
@@ -271,6 +274,27 @@ def _generate_report(self):
271274

272275
return report
273276

277+
def _cleanup_modules_html_reports(self, out_dir):
278+
"""Cleans up any HTML reports generated by test modules to save space."""
279+
280+
for module in self._test_modules:
281+
module_template_path = os.path.join(
282+
os.path.join(out_dir, module.name),
283+
f"{module.name}_report.j2.html")
284+
module_report_path = os.path.join(
285+
os.path.join(out_dir, module.name),
286+
f"{module.name}_report.jinja2")
287+
try:
288+
if os.path.exists(module_template_path):
289+
os.remove(module_template_path)
290+
LOGGER.debug(f"Removed module template: {module_template_path}")
291+
if os.path.exists(module_report_path):
292+
p = pathlib.Path(module_report_path)
293+
p.rename(p.with_suffix(".html"))
294+
except Exception as e:
295+
LOGGER.error(f"Error {module_template_path}: {e}")
296+
297+
274298
def _cleanup_old_test_results(self, device):
275299

276300
if device.max_device_reports is not None:

modules/test/base/base.Dockerfile

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,29 @@ ENV PATH="/opt/venv/bin:$PATH"
8484
ENV REPORT_TEMPLATE_PATH=/testrun/resources
8585
# Jinja base template
8686
ENV BASE_TEMPLATE_FILE=module_report_base.jinja2
87+
# Jinja preview template
88+
ENV BASE_TEMPLATE_PREVIEW_FILE=module_report_base_preview.jinja2
89+
# Jinja base template
90+
ENV BASE_TEMPLATE_STYLED_FILE=module_report_styled.jinja2
91+
# Styles
92+
ENV CSS_FILE=test_report_styles.css
93+
# ICON
94+
ENV LOGO_FILE=testrun.png
8795

8896
# Copy base template
8997
COPY resources/report/$BASE_TEMPLATE_FILE $REPORT_TEMPLATE_PATH/
9098

99+
# Copy base preview template
100+
COPY resources/report/$BASE_TEMPLATE_PREVIEW_FILE $REPORT_TEMPLATE_PATH/
101+
102+
# Copy base template (with styles)
103+
COPY resources/report/$BASE_TEMPLATE_STYLED_FILE $REPORT_TEMPLATE_PATH/
104+
105+
# Copy styles
106+
COPY resources/report/$CSS_FILE $REPORT_TEMPLATE_PATH/
107+
108+
# Copy icon
109+
COPY resources/report/$LOGO_FILE $REPORT_TEMPLATE_PATH/
110+
91111
# Start the test module
92112
ENTRYPOINT [ "/testrun/bin/start" ]

modules/test/base/python/src/test_module.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,15 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414
"""Base class for all core test module functions"""
15+
16+
import base64
1517
import json
1618
import logger
1719
import os
1820
import util
1921
from datetime import datetime
2022
import traceback
23+
from jinja2 import Environment, FileSystemLoader, BaseLoader
2124

2225
from common.statuses import TestResult
2326

@@ -44,6 +47,12 @@ def __init__(self,
4447
self._device_test_pack = json.loads(os.environ.get('DEVICE_TEST_PACK', ''))
4548
self._report_template_folder = os.environ.get('REPORT_TEMPLATE_PATH')
4649
self._base_template_file=os.environ.get('BASE_TEMPLATE_FILE')
50+
self._base_template_file_preview=os.environ.get(
51+
'BASE_TEMPLATE_PREVIEW_FILE'
52+
)
53+
self._base_template_styled_file=os.environ.get('BASE_TEMPLATE_STYLED_FILE')
54+
self._css_file=os.environ.get('CSS_FILE')
55+
self._logo_file=os.environ.get('LOGO_FILE')
4756
self._log_level = os.environ.get('LOG_LEVEL', None)
4857
self._add_logger(log_name=log_name)
4958
self._config = self._read_config(
@@ -229,3 +238,38 @@ def _get_device_ipv4(self):
229238
if text:
230239
return text.split('\n')[0]
231240
return None
241+
242+
def _render_styled_report(self, jinja_report, result_path):
243+
# Report styles
244+
with open(os.path.join(self._report_template_folder,
245+
self._css_file),
246+
'r',
247+
encoding='UTF-8'
248+
) as style_file:
249+
styles = style_file.read()
250+
251+
# Load Testrun logo to base64
252+
with open(os.path.join(self._report_template_folder,
253+
self._logo_file), 'rb') as f:
254+
logo = base64.b64encode(f.read()).decode('utf-8')
255+
256+
loader=FileSystemLoader(self._report_template_folder)
257+
template = Environment(
258+
loader=loader,
259+
trim_blocks=True,
260+
lstrip_blocks=True
261+
).get_template(self._base_template_styled_file)
262+
263+
module_template = Environment(loader=BaseLoader()
264+
).from_string(jinja_report).render(
265+
title = 'Testrun report',
266+
logo=logo,
267+
)
268+
269+
report_jinja_styled = template.render(
270+
template=module_template,
271+
styles=styles
272+
)
273+
# Write the styled content to a file
274+
with open(result_path, 'w', encoding='utf-8') as file:
275+
file.write(report_jinja_styled)

modules/test/dns/python/src/dns_module.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
LOG_NAME = 'test_dns'
2424
MODULE_REPORT_FILE_NAME = 'dns_report.j2.html'
25+
MODULE_REPORT_STYLED_FILE_NAME = 'dns_report.jinja2'
2526
DNS_SERVER_CAPTURE_FILE = '/runtime/network/dns.pcap'
2627
STARTUP_CAPTURE_FILE = '/runtime/device/startup.pcap'
2728
MONITOR_CAPTURE_FILE = '/runtime/device/monitor.pcap'
@@ -161,6 +162,7 @@ def generate_module_report(self):
161162
pages_content.append(current_page_rows)
162163

163164
report_html = ''
165+
report_jinja_preview = ''
164166
if not pages_content:
165167
pages_content = [[]]
166168

@@ -175,9 +177,23 @@ def generate_module_report(self):
175177
module_data=page_rows
176178
)
177179
report_html += page_html
180+
page_html = template.render(
181+
base_template=self._base_template_file_preview,
182+
module_header=module_header_repr,
183+
summary_headers=summary_headers,
184+
summary_data=summary_data,
185+
module_data_headers=module_data_headers,
186+
module_data=page_rows
187+
)
188+
report_jinja_preview += page_html
178189

179190
LOGGER.debug('Module report:\n' + report_html)
180191

192+
# Generate styled report for a preview
193+
jinja_path_styled = os.path.join(
194+
self._results_dir, MODULE_REPORT_STYLED_FILE_NAME)
195+
self._render_styled_report(report_jinja_preview, jinja_path_styled)
196+
181197
# Use os.path.join to create the complete file path
182198
report_path = os.path.join(self._results_dir, MODULE_REPORT_FILE_NAME)
183199

@@ -219,7 +235,6 @@ def extract_dns_data(self):
219235
qname = dns_layer.qd.qname.decode() if dns_layer.qd.qname else 'N/A'
220236
else:
221237
qname = 'N/A'
222-
223238
resolved_ip = 'N/A'
224239
# If it's a response packet, extract the resolved IP address
225240
# from the answer section
@@ -237,14 +252,15 @@ def extract_dns_data(self):
237252
elif answer.type == 28: # Indicates AAAA record (IPv6 address)
238253
resolved_ip = answer.rdata # Extract IPv6 address
239254
break # Stop after finding the first valid resolved IP
240-
255+
qname = qname.rstrip('.') if (isinstance(qname, str)
256+
and qname.endswith('.')) else qname
241257
dns_data.append({
242258
'Timestamp': float(packet.time), # Timestamp of the DNS packet
243259
'Source': source_ip,
244260
'Destination': destination_ip,
245261
'ResolvedIP': resolved_ip, # Adding the resolved IP address
246262
'Type': dns_type,
247-
'Data': qname[:-1]
263+
'Data': qname,
248264
})
249265

250266
# Filter unique entries based on 'Timestamp'

modules/test/ntp/python/src/ntp_module.py

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
LOG_NAME = 'test_ntp'
2525
MODULE_REPORT_FILE_NAME = 'ntp_report.j2.html'
26+
MODULE_REPORT_STYLED_FILE_NAME = 'ntp_report.jinja2'
2627
NTP_SERVER_CAPTURE_FILE = '/runtime/network/ntp.pcap'
2728
STARTUP_CAPTURE_FILE = '/runtime/device/startup.pcap'
2829
MONITOR_CAPTURE_FILE = '/runtime/device/monitor.pcap'
@@ -142,7 +143,7 @@ def generate_module_report(self):
142143

143144
# Generate the HTML table with the count column
144145
for (src, dst, typ,
145-
version), avg_diff in average_time_between_requests.items():
146+
version), avg_diff in average_time_between_requests.items():
146147
cnt = len(timestamps[(src, dst, typ, version)])
147148

148149
# Sync Average only applies to client requests
@@ -169,23 +170,42 @@ def generate_module_report(self):
169170
rows_on_page = ((page_useful_space) // row_height) - 1
170171
start = 0
171172
report_html = ''
173+
report_html_styled = ''
172174
for page in range(pages + 1):
173175
end = start + min(len(module_table_data), rows_on_page)
174176
module_header_repr = module_header if page == 0 else None
175-
page_html = template.render(base_template=self._base_template_file,
176-
module_header=module_header_repr,
177-
summary_headers=summary_headers,
178-
summary_data=summary_data,
179-
module_data_headers=module_data_headers,
180-
module_data=module_table_data[start:end])
177+
page_html = template.render(
178+
base_template=self._base_template_file,
179+
module_header=module_header_repr,
180+
summary_headers=summary_headers,
181+
summary_data=summary_data,
182+
module_data_headers=module_data_headers,
183+
module_data=module_table_data[start:end]
184+
)
185+
page_html_styled = template.render(
186+
base_template=self._base_template_file_preview,
187+
module_header=module_header_repr,
188+
summary_headers=summary_headers,
189+
summary_data=summary_data,
190+
module_data_headers=module_data_headers,
191+
module_data=module_table_data[start:end]
192+
)
181193
report_html += page_html
194+
report_html_styled += page_html_styled
182195
start = end
183196

184197
LOGGER.debug('Module report:\n' + report_html)
185198

186199
# Use os.path.join to create the complete file path
187200
report_path = os.path.join(self._results_dir, MODULE_REPORT_FILE_NAME)
188201

202+
# Use os.path.join to create the complete file path for styled report
203+
report_path_styled = os.path.join(
204+
self._results_dir, MODULE_REPORT_STYLED_FILE_NAME
205+
)
206+
# Generate the styled report for preview
207+
self._render_styled_report(report_html_styled, report_path_styled)
208+
189209
# Write the content to a file
190210
with open(report_path, 'w', encoding='utf-8') as file:
191211
file.write(report_html)

modules/test/ntp/python/src/ntp_white_list.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Module to resolve NTP whitelist domains to IP addresses asynchronously."""
22

33
import asyncio
4+
import concurrent.futures
45
import dns.asyncresolver
56
from logging import Logger
67

@@ -99,8 +100,18 @@ def _get_ntp_whitelist_ips(
99100
semaphore_limit: int = 50,
100101
timeout: int = 30
101102
) -> set[str]:
102-
return asyncio.run(
103-
self._get_ips_whitelist(self.config, semaphore_limit, timeout))
103+
# Always run in a separate thread to ensure we have a clean event loop
104+
def run_in_thread():
105+
new_loop = asyncio.new_event_loop()
106+
asyncio.set_event_loop(new_loop)
107+
try:
108+
return new_loop.run_until_complete(
109+
self._get_ips_whitelist(self.config, semaphore_limit, timeout))
110+
finally:
111+
new_loop.close()
112+
113+
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
114+
return executor.submit(run_in_thread).result()
104115

105116
# Check if an IP is whitelisted
106117
def is_ip_whitelisted(self, ip: str) -> bool:

modules/test/services/python/src/services_module.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
LOG_NAME = 'test_services'
2525
MODULE_REPORT_FILE_NAME = 'services_report.j2.html'
26+
MODULE_REPORT_STYLED_FILE_NAME = 'services_report.jinja2'
2627
NMAP_SCAN_RESULTS_SCAN_FILE = 'services_scan_results.json'
2728
LOGGER = None
2829
REPORT_TEMPLATE_FILE = 'report_template.jinja2'
@@ -129,9 +130,22 @@ def generate_module_report(self):
129130
module_data_headers=module_data_headers,
130131
module_data=module_data,
131132
)
133+
html_content_preview = template.render(
134+
base_template=self._base_template_file_preview,
135+
module_header=module_header,
136+
summary_headers=summary_headers,
137+
summary_data=summary_data,
138+
module_data_headers=module_data_headers,
139+
module_data=module_data,
140+
)
132141

133142
LOGGER.debug('Module report:\n' + html_content)
134143

144+
# Generate styled report for a preview
145+
jinja_path_styled = os.path.join(
146+
self._results_dir, MODULE_REPORT_STYLED_FILE_NAME)
147+
self._render_styled_report(html_content_preview, jinja_path_styled)
148+
135149
# Use os.path.join to create the complete file path
136150
report_path = os.path.join(self._results_dir, MODULE_REPORT_FILE_NAME)
137151

0 commit comments

Comments
 (0)