From 701d647a913b058a214d6d726dcb31c849511d0d Mon Sep 17 00:00:00 2001 From: 2Shirt <2xShirt@gmail.com> Date: Wed, 22 Jan 2020 19:19:54 -0700 Subject: [PATCH] Added I/O Benchmark PNG graph sections --- scripts/wk/cfg/net.py | 2 + scripts/wk/graph.py | 117 +++++++++++++++++++++++++++++++++++++++++ scripts/wk/hw/diags.py | 20 +++++-- 3 files changed, 136 insertions(+), 3 deletions(-) diff --git a/scripts/wk/cfg/net.py b/scripts/wk/cfg/net.py index 1bc4ac67..821d275b 100644 --- a/scripts/wk/cfg/net.py +++ b/scripts/wk/cfg/net.py @@ -28,6 +28,8 @@ CRASH_SERVER = { 'Pass': '', 'Headers': {'X-Requested-With': 'XMLHttpRequest'}, } +# Misc +IMGUR_CLIENT_ID='3d1ee1d38707b85' if __name__ == '__main__': diff --git a/scripts/wk/graph.py b/scripts/wk/graph.py index 1bcb9c27..a3d09914 100644 --- a/scripts/wk/graph.py +++ b/scripts/wk/graph.py @@ -2,11 +2,24 @@ # pylint: disable=bad-whitespace # vim: sts=2 sw=2 ts=2 +import base64 +import json import logging +import math +import pathlib +import time +import Gnuplot +import requests + +from wk.cfg.net import BENCHMARK_SERVER, IMGUR_CLIENT_ID from wk.std import color_string +# Hack to hide X11 error when running in CLI mode +Gnuplot.gp.GnuplotOpts.default_term = 'xterm' + + # STATIC VARIABLES LOG = logging.getLogger(__name__) GRAPH_HORIZONTAL = ('▁', '▂', '▃', '▄', '▅', '▆', '▇', '█') @@ -34,6 +47,48 @@ THRESH_GREAT = 750 * 1024**2 # Functions +def export_io_graph(disk, log_dir): + """Exports PNG graph using gnuplot, returns pathlib.Path obj.""" + read_rates = disk.tests['Disk I/O Benchmark'].read_rates + + # Safety check + if not read_rates: + raise RuntimeError(f'No read rates for {disk.path}') + + # Prep + max_rate = max(read_rates) / (1024**2) + max_rate = max(800, max_rate) + out_path = pathlib.Path(f'{log_dir}/{disk.path.name}_iobenchmark.png') + plot_data = out_path.with_suffix('.data') + + # Adjust Y-axis range to either 1000 or roughly max rate + 200 + ## Round up to the nearest 100 and then add 200 + y_range = (math.ceil(max_rate/100)*100) + 200 + + # Save plot data to file for Gnuplot + with open(plot_data, 'w') as _f: + for i, rate in enumerate(read_rates): + percent = (i+1) / len(read_rates) * 100 + rate = int(rate / (1024**2)) + _f.write(f'{percent:0.1f} {rate}\n') + + # Run gnuplot commands + _g = Gnuplot.Gnuplot() + _g('reset') + _g(f'set output "{out_path}"') + _g('set terminal png large size 660,300 truecolor font "Noto Sans,11"') + _g('set title "I/O Benchmark"') + _g(f'set yrange [0:{y_range}]') + _g('set style data lines') + _g(f'plot "{plot_data}" title "{disk.description.replace("_", " ")}"') + + # Cleanup + _g.close() + del _g + + return out_path + + def generate_horizontal_graph(rate_list, graph_width=40, oneline=False): """Generate horizontal graph from rate_list, returns list.""" graph = ['', '', '', ''] @@ -111,6 +166,68 @@ def merge_rates(rates, graph_width=40): return merged_rates +def upload_to_imgur(image_path): + """Upload image to Imgur and return image url as str.""" + image_data = None + image_link = None + + # Bail early + if not image_path: + raise RuntimeError(f'Invalid image path: {image_path}') + + # Read image file and convert to base64 then convert to str + with open(image_path, 'rb') as _f: + image_data = base64.b64encode(_f.read()).decode() + + # POST image + url = 'https://api.imgur.com/3/image' + boundary = '----WebKitFormBoundary7MA4YWxkTrZu0gW' + payload = ( + f'--{boundary}\r\nContent-Disposition: form-data; ' + f'name="image"\r\n\r\n{image_data}\r\n--{boundary}--' + ) + headers = { + 'content-type': f'multipart/form-data; boundary={boundary}', + 'Authorization': f'Client-ID {IMGUR_CLIENT_ID}', + } + response = requests.request('POST', url, data=payload, headers=headers) + + # Return image link + if response.ok: + json_data = json.loads(response.text) + image_link = json_data['data']['link'] + return image_link + + +def upload_to_nextcloud(image_path, ticket_number, dev_name): + """Upload image to Nextcloud server and return folder url as str.""" + image_data = None + + # Bail early + if not image_path: + raise RuntimeError(f'Invalid image path: {image_path}') + + # Read image file and convert to base64 + with open(image_path, 'rb') as _f: + image_data = _f.read() + + # PUT image + url = ( + f'{BENCHMARK_SERVER["Url"]}/' + f'{ticket_number}_iobenchmark' + f'_{dev_name}_{time.strftime("%Y-%m-%d_%H%M_%z")}.png' + ) + requests.put( + url, + data=image_data, + headers = {'X-Requested-With': 'XMLHttpRequest'}, + auth = (BENCHMARK_SERVER['User'], BENCHMARK_SERVER['Pass']), + ) + + # Return folder link + return BENCHMARK_SERVER['Short Url'] + + def vertical_graph_line(percent, rate, scale=32): """Build colored graph string using thresholds, returns str.""" color_bar = None diff --git a/scripts/wk/hw/diags.py b/scripts/wk/hw/diags.py index e596b554..6fccb3e0 100644 --- a/scripts/wk/hw/diags.py +++ b/scripts/wk/hw/diags.py @@ -748,7 +748,7 @@ def cpu_mprime_test(state, test_objects): std.print_info('Posting results to osTicket...') test_cooling_obj.cpu_max_temp = sensors.cpu_max_temp() state.ost.post_response( - ost_build_report(state.cpu, 'CPU'), + ost_build_report(state.cpu, 'CPU', state), color='Diags FAIL' if state.cpu.any_test_failed() else 'Diags', ) @@ -860,6 +860,9 @@ def disk_io_benchmark(state, test_objects, skip_usb=True): # Check results check_io_benchmark_results(test_obj, read_rates, IO_GRAPH_WIDTH) + # osTicket + test_obj.read_rates = read_rates + # Run benchmarks state.update_top_pane( f'Disk I/O Benchmark{"s" if len(test_objects) > 1 else ""}', @@ -1228,7 +1231,8 @@ def network_test(): std.pause('Press Enter to return to main menu...') -def ost_build_report(dev, dev_type): +def ost_build_report(dev, dev_type, state): + # pylint: disable=too-many-branches """Build report for posting to osTicket, returns str.""" report = [] @@ -1279,6 +1283,16 @@ def ost_build_report(dev, dev_type): ) else: report.extend(ost_convert_report(test.report, start_index=1)) + if name == 'Disk I/O Benchmark': + try: + image_path = graph.export_io_graph(dev, state.log_dir) + imgur_url = graph.upload_to_imgur(image_path) + nextcloud_url = graph.upload_to_nextcloud( + image_path, state.ost.ticket_id, dev.path.name) + report.append(f'Imgur: {imgur_url}') + report.append(f'Nextcloud: {nextcloud_url}') + except (AttributeError, RuntimeError): + report.append('Error(s) exporting graph') # Spacer report.append('') @@ -1391,7 +1405,7 @@ def ost_post_disk_results(state): std.print_info('Posting results to osTicket...') for disk in state.disks: state.ost.post_response( - ost_build_report(disk, 'Disk'), + ost_build_report(disk, 'Disk', state), color='Diags FAIL' if disk.any_test_failed() else 'Diags', )