diff --git a/.bin/Scripts/functions/hw_diags.py b/.bin/Scripts/functions/hw_diags.py index 3b8bd354..3c7df905 100644 --- a/.bin/Scripts/functions/hw_diags.py +++ b/.bin/Scripts/functions/hw_diags.py @@ -1,9 +1,23 @@ # Wizard Kit: Functions - HW Diagnostics +import base64 +import Gnuplot import json +import math +import mysql.connector as mariadb +import requests import time from functions.common import * +from numpy import * + +# Database connection +ost_db = { + 'Connection': None, + 'Cursor': None, + 'Errors': False, + 'Tunnel': None, + } # STATIC VARIABLES ATTRIBUTES = { @@ -29,13 +43,14 @@ ATTRIBUTES = { } IO_VARS = { 'Block Size': 512*1024, - 'Chunk Size': 16*1024**2, + 'Chunk Size': 32*1024**2, 'Minimum Dev Size': 8*1024**3, 'Minimum Test Size': 10*1024**3, 'Alt Test Size Factor': 0.01, 'Progress Refresh Rate': 5, - 'Scale 16': [2**(0.6*x)+(16*x) for x in range(1,17)], - 'Scale 32': [2**(0.6*x/2)+(16*x/2) for x in range(1,33)], + 'Scale 8': [2**(0.56*(x+1))+(16*(x+1)) for x in range(8)], + 'Scale 16': [2**(0.56*(x+1))+(16*(x+1)) for x in range(16)], + 'Scale 32': [2**(0.56*(x+1)/2)+(16*(x+1)/2) for x in range(32)], 'Threshold Fail': 65*1024**2, 'Threshold Warn': 135*1024**2, 'Threshold Great': 750*1024**2, @@ -51,6 +66,16 @@ IO_VARS = { '███▏', '███▎', '███▍', '███▌', '███▋', '███▊', '███▉', '████'), } +OST_STAFF_ID = '23' +OST_STAFF_NAME = 'Wizard Kit' +OST_SQL_SET_HOLD = "UPDATE `{db_name}`.`ost_ticket` SET `hold` = '{hold_type}' WHERE `ost_ticket`.`ticket_id` = {ticket_id};" +OST_SQL_SET_FLAG = "UPDATE `{db_name}`.`ost_ticket` SET `{flag}` = '{value}' WHERE `ost_ticket`.`ticket_id` = {ticket_id};" +OST_SQL_POST_REPLY = ("INSERT INTO `{db_name}`.`ost_ticket_response` (ticket_id, staff_id, staff_name, response, created) " + "VALUES ('{ticket_id}', '{staff_id}', '{staff_name}', '{response}', '{created}');") +OST_DRIVE_FLAG = 'zHDTune' +OST_DRIVE_PASSED = 1 +OST_DRIVE_FAILED = 2 +OST_NEEDS_ATTENTION = 4 TESTS = { 'Prime95': { 'Enabled': False, @@ -68,17 +93,90 @@ TESTS = { }, 'iobenchmark': { 'Enabled': False, + 'Data': {}, 'Results': {}, 'Status': {}, }, } -def generate_horizontal_graph(rates): +def connect_to_db(): + """Connect to osTicket database via SSH tunnel.""" + cmd = [ + 'ssh', '-N', '-p', SSH_PORT, '-L3306:127.0.0.1:3306', + '{user}@{host}'.format(user=SSH_USER, host=DB_HOST), + ] + + # Establish SSH tunnel unless one already exists + if not ost_db['Tunnel']: + ost_db['Tunnel'] = popen_program(cmd) + + # Establish SQL connection (try a few times in case SSH is slow) + for x in range(5): + sleep(3) + try: + ost_db['Connection'] = mariadb.connect( + user=DB_USER, password=DB_PASS, database=DB_NAME) + except: + # Just try again + pass + else: + break + ost_db['Cursor'] = ost_db['Connection'].cursor() + ost_db['Errors'] = False + +def disconnect_from_db(): + """Disconnect from SQL DB and close SSH tunnel.""" + if ost_db['Cursor']: + ost_db['Cursor'].close() + if ost_db['Connection']: + ost_db['Connection'].close() + if ost_db['Tunnel']: + ost_db['Tunnel'].kill() + +def export_png_graph(name, dev): + """Exports PNG graph using gnuplot, returns file path as str.""" + max_rate = max(TESTS['iobenchmark']['Data'][name]['Read Rates']) + max_rate /= (1024**2) + max_rate = max(800, max_rate) + out_path = '{}/iobenchmark-{}.png'.format(global_vars['LogDir'], name) + plot_data = '{}/iobenchmark-{}-raw.log'.format(global_vars['LogDir'], name) + + # 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 + + # Run gnuplot commands + g = Gnuplot.Gnuplot() + g('reset') + g('set output "{}"'.format(out_path)) + g('set terminal png large size 660,300 truecolor font "Noto Sans,11"') + g('set title "I/O Benchmark"') + g('set yrange [0:{}]'.format(y_range)) + g('set style data lines') + g('plot "{data}" title "{size} ({tran}) {model} {serial}"'.format( + data=plot_data, + size=dev['lsblk'].get('size', '???b'), + tran=dev['lsblk'].get('tran', '???'), + model=dev['lsblk'].get('model', 'Unknown Model'), + serial=dev['lsblk'].get('serial', 'Unknown Serial'), + )) + + # Cleanup + g.close() + del(g) + + return out_path + +def generate_horizontal_graph(rates, oneline=False): """Generate two-line horizontal graph from rates, returns str.""" - line_top = '' - line_bottom = '' + line_1 = '' + line_2 = '' + line_3 = '' + line_4 = '' for r in rates: - step = get_graph_step(r, scale=16) + step = get_graph_step(r, scale=32) + if oneline: + step = get_graph_step(r, scale=8) # Set color r_color = COLORS['CLEAR'] @@ -90,15 +188,35 @@ def generate_horizontal_graph(rates): r_color = COLORS['GREEN'] # Build graph - if step < 8: - line_top += ' ' - line_bottom += '{}{}'.format(r_color, IO_VARS['Graph Horizontal'][step]) + full_block = '{}{}'.format(r_color, IO_VARS['Graph Horizontal'][-1]) + if step >= 24: + line_1 += '{}{}'.format(r_color, IO_VARS['Graph Horizontal'][step-24]) + line_2 += full_block + line_3 += full_block + line_4 += full_block + elif step >= 16: + line_1 += ' ' + line_2 += '{}{}'.format(r_color, IO_VARS['Graph Horizontal'][step-16]) + line_3 += full_block + line_4 += full_block + elif step >= 8: + line_1 += ' ' + line_2 += ' ' + line_3 += '{}{}'.format(r_color, IO_VARS['Graph Horizontal'][step-8]) + line_4 += full_block else: - line_top += '{}{}'.format(r_color, IO_VARS['Graph Horizontal'][step-8]) - line_bottom += '{}{}'.format(r_color, IO_VARS['Graph Horizontal'][-1]) - line_top += COLORS['CLEAR'] - line_bottom += COLORS['CLEAR'] - return '{}\n{}'.format(line_top, line_bottom) + line_1 += ' ' + line_2 += ' ' + line_3 += ' ' + line_4 += '{}{}'.format(r_color, IO_VARS['Graph Horizontal'][step]) + line_1 += COLORS['CLEAR'] + line_2 += COLORS['CLEAR'] + line_3 += COLORS['CLEAR'] + line_4 += COLORS['CLEAR'] + if oneline: + return line_4 + else: + return '\n'.join([line_1, line_2, line_3, line_4]) def get_graph_step(rate, scale=16): """Get graph step based on rate and scale, returns int.""" @@ -112,6 +230,30 @@ def get_graph_step(rate, scale=16): break return step +def get_osticket_number(): + """Get ticket number and confirm with name from osTicket DB.""" + ticket_number = None + if not ost_db['Cursor']: + # No DB access, return None to disable integration + return None + while ticket_number is None: + print_standard(' ') + _input = input('Enter ticket number (or leave blank to disable): ') + if re.match(r'^\s*$', _input): + if ask('Disable osTicket integration for this run?'): + return None + else: + continue + if not re.match(r'^([0-9]+)$', _input): + continue + _name = osticket_get_ticket_name(_input) + if _name: + print_standard('You have selected ticket #{} {}'.format( + _input, _name)) + if ask('Is this correct?'): + ticket_number = _input + return ticket_number + def get_read_rate(s): """Get read rate in bytes/s from dd progress output.""" real_rate = None @@ -200,15 +342,22 @@ def menu_diags(*args): spacer = '──────────────────────────') if selection.isnumeric(): if diag_modes[int(selection)-1]['Name'] != 'Quick drive test': + clear_screen() + print_standard(' ') + try_and_print( + message='Connecting to osTicket database...', + function=connect_to_db, + width=40) # Save log for non-quick tests - ticket_number = get_ticket_number() - global_vars['LogDir'] = '{}/Logs/{}'.format( + ticket_number = get_osticket_number() + global_vars['LogDir'] = '{}/Logs/{}_{}'.format( global_vars['Env']['HOME'], - ticket_number if ticket_number else global_vars['Date-Time']) + ticket_number, + global_vars['Date-Time']) os.makedirs(global_vars['LogDir'], exist_ok=True) global_vars['LogFile'] = '{}/Hardware Diagnostics.log'.format( global_vars['LogDir']) - run_tests(diag_modes[int(selection)-1]['Tests']) + run_tests(diag_modes[int(selection)-1]['Tests'], ticket_number) elif selection == 'A': run_program(['hw-diags-audio'], check=False, pipe=False) pause('Press Enter to return to main menu... ') @@ -230,7 +379,277 @@ def menu_diags(*args): elif selection == 'Q': break -def run_badblocks(): + # Done + disconnect_from_db() + +def osticket_get_ticket_name(ticket_id): + """Lookup ticket and return name as str.""" + ticket_name = 'Unknown' + if not ticket_id: + raise GenericError + if not ost_db['Cursor']: + # Skip section + return + + # Set command + sql_cmd = "SELECT name FROM `ost_ticket` WHERE `ticket_id` = {}".format( + ticket_id) + + # Run command + try: + ost_db['Cursor'].execute(sql_cmd) + for name in ost_db['Cursor']: + ticket_name = name[0] + return ticket_name + except: + ost_db['Errors'] = True + +def osticket_needs_attention(ticket_id): + """[DISABLED] Marks the ticket as "NEEDS ATTENTION" in osTicket.""" + return # This function has been DISABLED due to a repurposing of that flag + if not ticket_id: + raise GenericError + if not ost_db['Cursor']: + # Skip section + return + + # Set command + sql_cmd = OST_SQL_SET_HOLD.format( + db_name=DB_NAME, + hold_type=OST_NEEDS_ATTENTION, + ticket_id=ticket_id) + + # Run command + try: + ost_db['Cursor'].execute(sql_cmd) + except: + ost_db['Errors'] = True + +def osticket_post_reply(ticket_id, response): + """Post a reply to a ticket in osTicket.""" + if not ticket_id: + raise GenericError + if not ost_db['Cursor']: + # Skip section + return + + # Set command + sql_cmd = OST_SQL_POST_REPLY.format( + db_name=DB_NAME, + ticket_id=ticket_id, + staff_id=OST_STAFF_ID, + staff_name=OST_STAFF_NAME, + response=response, + created=time.strftime("%Y-%m-%d %H:%M:%S")) + + # Run command + try: + ost_db['Cursor'].execute(sql_cmd) + except: + ost_db['Errors'] = True + +def osticket_set_drive_result(ticket_id, passed): + """Marks the pass/fail box for the drive(s) in osTicket.""" + if not ticket_id: + raise GenericError + if not ost_db['Cursor']: + # Skip section + return + + # Set command + sql_cmd = OST_SQL_SET_FLAG.format( + db_name=DB_NAME, + flag=OST_DRIVE_FLAG, + value=OST_DRIVE_PASSED if passed else OST_DRIVE_FAILED, + ticket_id=ticket_id) + + # Run command + try: + ost_db['Cursor'].execute(sql_cmd) + except: + ost_db['Errors'] = True + +def pad_with_dots(s, left_pad=True): + """Replace ' ' padding with '..' for osTicket posts.""" + s = str(s).replace(' ', '..') + if '.' in s: + if left_pad: + s = '.' + s + else: + s = s + '.' + return s + +def post_drive_results(ticket_number): + """Post drive test results to osTicket.""" + tested = False + + # Check if test(s) were run + for t in ['NVMe/SMART', 'badblocks', 'iobenchmark']: + tested |= TESTS[t]['Enabled'] + if not tested or TESTS['NVMe/SMART']['Quick']: + # No tests were run so no post necessary + return + + # Build reports for all tested devices + for name, dev in sorted(TESTS['NVMe/SMART']['Devices'].items()): + dev_failed = False + dev_passed = True + dev_unknown = False + report = [] + + # Check all test results for dev + for t in ['NVMe/SMART', 'badblocks', 'iobenchmark']: + if not TESTS[t]['Enabled']: + continue + status = TESTS[t]['Status'].get(name, 'Unknown') + dev_failed |= status == 'NS' + dev_passed &= status == 'CS' + dev_unknown |= status in ('Working', 'Unknown') + if not dev_failed and not dev_passed and not dev_unknown: + # Assuming drive was skipped so no reply is needed + continue + + # Start drive report + if dev_failed: + report.append('Drive hardware diagnostics tests: FAILED') + elif dev_unknown: + report.append('Drive hardware diagnostics tests: UNKNOWN') + elif dev_passed: + report.append('Drive hardware diagnostics tests: Passed') + report.append('') + + # Drive description + report.append('{size} ({tran}) {model} {serial}'.format( + size=dev['lsblk'].get('size', '???b'), + tran=dev['lsblk'].get('tran', '???'), + model=dev['lsblk'].get('model', 'Unknown Model'), + serial=dev['lsblk'].get('serial', 'Unknown Serial'), + )) + report.append('') + + # Warnings (if any) + if dev.get('NVMe Disk', False): + if dev['Quick Health Ok']: + report.append('WARNING: NVMe support is still experimental') + else: + report.append('ERROR: NVMe disk is reporting critical warnings') + report.append('') + elif not dev['SMART Support']: + report.append('ERROR: Unable to retrieve SMART data') + report.append('') + elif not dev['SMART Pass']: + report.append('ERROR: SMART overall-health assessment result: FAILED') + report.append('') + + # NVMe/SMART Attributes + if dev.get('NVMe Disk', False): + report.append('NVMe Attributes:') + for attrib in sorted(ATTRIBUTES['NVMe'].keys()): + if attrib in dev['nvme-cli']: + report.append('{attrib:30}{value}'.format( + attrib=attrib, + value=dev['nvme-cli'][attrib], + )) + report[-1] = report[-1].strip().replace(' ', '.') + report[-1] = report[-1].replace('_', ' ') + elif dev['smartctl'].get('ata_smart_attributes', None): + report.append('SMART Attributes:') + s_table = dev['smartctl'].get('ata_smart_attributes', {}).get( + 'table', {}) + s_table = {a.get('id', 'Unknown'): a for a in s_table} + for attrib in sorted(ATTRIBUTES['SMART'].keys()): + if attrib in s_table: + # Pad attributewith dots for osTicket + hex_str = str(hex(int(attrib))).upper()[2:] + hex_str = pad_with_dots('{:>2}'.format(hex_str)) + dec_str = pad_with_dots('{:>3}'.format(attrib)) + val_str = '{:<20}'.format(s_table[attrib]['raw']['string']) + val_str = pad_with_dots(val_str, left_pad=False) + report.append('{:>2}/{:>3}: {} ({})'.format( + hex_str, + dec_str, + val_str, + s_table[attrib]['name'], + )) + report[-1] = report[-1].replace('_', ' ') + report.append('') + + # badblocks + bb_status = TESTS['badblocks']['Status'].get(name, None) + if TESTS['badblocks']['Enabled'] and bb_status not in ['Denied', 'Skipped']: + report.append('badblocks:') + bb_result = TESTS['badblocks']['Results'].get( + name, + 'ERROR: Failed to read log.') + for line in bb_result.splitlines(): + line = line.strip() + if not line: + continue + if re.search('Pass completed', line, re.IGNORECASE): + line = re.sub( + r'Pass completed,?\s+', + r'', + line, + re.IGNORECASE) + report.append(line) + report.append('') + + # I/O Benchmark + io_status = TESTS['iobenchmark']['Status'].get(name, None) + if TESTS['iobenchmark']['Enabled'] and io_status not in ['Denied', 'Skipped']: + one_line_graph = generate_horizontal_graph( + rates=TESTS['iobenchmark']['Data'][name]['Merged Rates'], + oneline=True) + for c in COLORS.values(): + one_line_graph = one_line_graph.replace(c, '') + report.append('I/O Benchmark:') + report.append(one_line_graph) + report.append('{}'.format( + TESTS['iobenchmark']['Data'][name]['Avg/Min/Max'])) + + # Export PNG + try: + png_path = export_png_graph(name, dev) + except: + png_path = None + + # imgur + try: + url = upload_to_imgur(png_path) + report.append('Imgur: {}'.format(url)) + except: + # Oh well + pass + + # Nextcloud + try: + url = upload_to_nextcloud(png_path, ticket_number, name) + report.append('Nextcloud: {}'.format(url)) + except: + # Oh well + pass + + # Post reply for drive + osticket_post_reply( + ticket_id=ticket_number, + response='\n'.join(report)) + + # Mark ticket HDD/SSD pass/fail checkbox (as needed) + if dev_failed: + osticket_set_drive_result( + ticket_id=ticket_number, + passed=False) + elif dev_unknown: + pass + elif dev_passed: + osticket_set_drive_result( + ticket_id=ticket_number, + passed=True) + + # Mark ticket as NEEDS ATTENTION + osticket_needs_attention(ticket_id=ticket_number) + +def run_badblocks(ticket_number): """Run a read-only test for all detected disks.""" aborted = False clear_screen() @@ -292,7 +711,7 @@ def run_badblocks(): run_program('tmux kill-pane -a'.split(), check=False) pass -def run_iobenchmark(): +def run_iobenchmark(ticket_number): """Run a read-only test for all detected disks.""" aborted = False clear_screen() @@ -385,14 +804,16 @@ def run_iobenchmark(): # Run dd read tests offset = 0 - read_rates = [] + TESTS['iobenchmark']['Data'][name] = { + 'Graph': [], + 'Read Rates': []} for i in range(test_chunks): i += 1 s = skip_count c = int(IO_VARS['Chunk Size'] / IO_VARS['Block Size']) if skip_extra and i % skip_extra == 0: s += 1 - cmd = 'sudo dd bs={b} skip={s} count={c} if=/dev/{n} of={o}'.format( + cmd = 'sudo dd bs={b} skip={s} count={c} if=/dev/{n} of={o} iflag=direct'.format( b=IO_VARS['Block Size'], s=offset+s, c=c, @@ -400,12 +821,18 @@ def run_iobenchmark(): o='/dev/null') result = run_program(cmd.split()) result_str = result.stderr.decode().replace('\n', '') - read_rates.append(get_read_rate(result_str)) + cur_rate = get_read_rate(result_str) + TESTS['iobenchmark']['Data'][name]['Read Rates'].append( + cur_rate) + TESTS['iobenchmark']['Data'][name]['Graph'].append( + '{percent:0.1f} {rate}'.format( + percent=i/test_chunks*100, + rate=int(cur_rate/(1024**2)))) if i % IO_VARS['Progress Refresh Rate'] == 0: # Update vertical graph update_io_progress( percent=i/test_chunks*100, - rate=read_rates[-1], + rate=cur_rate, progress_file=progress_file) # Update offset offset += s + c @@ -415,24 +842,29 @@ def run_iobenchmark(): run_program(['tmux', 'kill-pane', '-t', bottom_pane]) # Build report - h_graph_rates = [] + avg_min_max = 'Average read speed: {:3.1f} MB/s (Min: {:3.1f}, Max: {:3.1f})'.format( + sum(TESTS['iobenchmark']['Data'][name]['Read Rates'])/len( + TESTS['iobenchmark']['Data'][name]['Read Rates'])/(1024**2), + min(TESTS['iobenchmark']['Data'][name]['Read Rates'])/(1024**2), + max(TESTS['iobenchmark']['Data'][name]['Read Rates'])/(1024**2)) + TESTS['iobenchmark']['Data'][name]['Avg/Min/Max'] = avg_min_max + TESTS['iobenchmark']['Data'][name]['Merged Rates'] = [] pos = 0 width = int(test_chunks / IO_VARS['Graph Horizontal Width']) for i in range(IO_VARS['Graph Horizontal Width']): # Append average rate for WIDTH number of rates to new array - h_graph_rates.append(sum(read_rates[pos:pos+width])/width) + TESTS['iobenchmark']['Data'][name]['Merged Rates'].append(sum( + TESTS['iobenchmark']['Data'][name]['Read Rates'][pos:pos+width])/width) pos += width - report = generate_horizontal_graph(h_graph_rates) - report += '\nRead speed: {:3.1f} MB/s (Min: {:3.1f}, Max: {:3.1f})'.format( - sum(read_rates)/len(read_rates)/(1024**2), - min(read_rates)/(1024**2), - max(read_rates)/(1024**2)) + report = generate_horizontal_graph( + TESTS['iobenchmark']['Data'][name]['Merged Rates']) + report += '\n{}'.format(avg_min_max) TESTS['iobenchmark']['Results'][name] = report # Set CS/NS - if min(read_rates) <= IO_VARS['Threshold Fail']: + if min(TESTS['iobenchmark']['Data'][name]['Read Rates']) <= IO_VARS['Threshold Fail']: TESTS['iobenchmark']['Status'][name] = 'NS' - elif min(read_rates) <= IO_VARS['Threshold Warn']: + elif min(TESTS['iobenchmark']['Data'][name]['Read Rates']) <= IO_VARS['Threshold Warn']: TESTS['iobenchmark']['Status'][name] = 'Unknown' else: TESTS['iobenchmark']['Status'][name] = 'CS' @@ -441,15 +873,14 @@ def run_iobenchmark(): dest_filename = '{}/iobenchmark-{}.log'.format(global_vars['LogDir'], name) shutil.move(progress_file, dest_filename) with open(dest_filename.replace('.', '-raw.'), 'a') as f: - for rate in read_rates: - f.write('{} MB/s\n'.format(rate/(1024**2))) + f.write('\n'.join(TESTS['iobenchmark']['Data'][name]['Graph'])) update_progress() # Done run_program('tmux kill-pane -a'.split(), check=False) pass -def run_mprime(): +def run_mprime(ticket_number): """Run Prime95 for MPRIME_LIMIT minutes while showing the temps.""" aborted = False print_log('\nStart Prime95 test') @@ -469,8 +900,10 @@ def run_mprime(): try: for i in range(int(MPRIME_LIMIT)): clear_screen() - print_standard('Running Prime95 ({} minutes left)'.format( - int(MPRIME_LIMIT)-i)) + min_left = int(MPRIME_LIMIT) - i + print_standard('Running Prime95 ({} minute{} left)'.format( + min_left, + 's' if min_left != 1 else '')) print_warning('If running too hot, press CTRL+c to abort the test') sleep(60) except KeyboardInterrupt: @@ -540,10 +973,45 @@ def run_mprime(): TESTS['Prime95']['Status'] = 'Unknown' update_progress() + # Build osTicket report + if TESTS['Prime95']['Status'] not in ['Unknown', 'Aborted']: + report = ['System {} Prime95 testing.'.format( + 'FAILED' if TESTS['Prime95']['NS'] else 'passed')] + report.append('') + report.append('Prime95 log:') + log_path = '{}/prime.log'.format(global_vars['LogDir']) + try: + with open(log_path, 'r') as f: + for line in f.readlines(): + line = line.strip() + r = re.search('(completed \d+ tests.*)', line, re.IGNORECASE) + if r: + report.append(r.group(1)) + except: + report.append('ERROR: Failed to read log.') + report.append('') + report.append('Final temps:') + log_path = '{}/Final Temps.log'.format(global_vars['LogDir']) + try: + with open(log_path, 'r') as f: + for line in f.readlines(): + line = line.strip() + if not line: + # Stop after CPU temp(s) + break + report.append(line) + except: + report.append('ERROR: Failed to read log.') + + # Upload osTicket report + osticket_post_reply( + ticket_id=ticket_number, + response='\n'.join(report)) + # Done run_program('tmux kill-pane -a'.split()) -def run_nvme_smart(): +def run_nvme_smart(ticket_number): """Run the built-in NVMe or SMART test for all detected disks.""" aborted = False clear_screen() @@ -638,10 +1106,12 @@ def run_nvme_smart(): # Done run_program('tmux kill-pane -a'.split(), check=False) -def run_tests(tests): +def run_tests(tests, ticket_number=None): """Run selected hardware test(s).""" - print_log('Starting Hardware Diagnostics') - print_log('\nRunning tests: {}'.format(', '.join(tests))) + clear_screen() + print_standard('Starting Hardware Diagnostics') + print_standard(' ') + print_standard('Running tests: {}'.format(', '.join(tests))) # Enable selected tests for t in ['Prime95', 'NVMe/SMART', 'badblocks', 'iobenchmark']: TESTS[t]['Enabled'] = t in tests @@ -650,7 +1120,6 @@ def run_tests(tests): # Initialize if TESTS['NVMe/SMART']['Enabled'] or TESTS['badblocks']['Enabled'] or TESTS['iobenchmark']['Enabled']: print_standard(' ') - print_standard('Scanning disks...') scan_disks() update_progress() @@ -658,18 +1127,19 @@ def run_tests(tests): mprime_aborted = False if TESTS['Prime95']['Enabled']: try: - run_mprime() + run_mprime(ticket_number) except GenericError: mprime_aborted = True if not mprime_aborted: if TESTS['NVMe/SMART']['Enabled']: - run_nvme_smart() + run_nvme_smart(ticket_number) if TESTS['badblocks']['Enabled']: - run_badblocks() + run_badblocks(ticket_number) if TESTS['iobenchmark']['Enabled']: - run_iobenchmark() + run_iobenchmark(ticket_number) # Show results + post_drive_results(ticket_number) show_results() # Open log @@ -683,7 +1153,6 @@ def run_tests(tests): def scan_disks(full_paths=False, only_path=None): """Scan for disks eligible for hardware testing.""" - clear_screen() # Get eligible disk list cmd = ['lsblk', '-J', '-O'] @@ -772,7 +1241,7 @@ def scan_disks(full_paths=False, only_path=None): if ask('Run tests on this device anyway?'): TESTS['NVMe/SMART']['Status'][dev_name] = 'OVERRIDE' else: - TESTS['NVMe/SMART']['Status'][dev_name] = 'NS' + TESTS['NVMe/SMART']['Status'][dev_name] = 'Skipped' TESTS['badblocks']['Status'][dev_name] = 'Denied' TESTS['iobenchmark']['Status'][dev_name] = 'Denied' print_standard(' ') # In case there's more than one "OVERRIDE" disk @@ -1020,6 +1489,64 @@ def update_progress(): with open(TESTS['Progress Out'], 'w') as f: f.writelines(output) +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 GenericError + + # 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 = ('--{boundary}\r\nContent-Disposition: form-data; ' + 'name="image"\r\n\r\n{data}\r\n--{boundary}--') + payload = payload.format(boundary=boundary, data=image_data) + headers = { + 'content-type': 'multipart/form-data; boundary={}'.format(boundary), + 'Authorization': 'Client-ID {}'.format(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 GenericError + + # Read image file and convert to base64 + with open(image_path, 'rb') as f: + image_data = f.read() + + # PUT image + url = '{base_url}/{ticket_number}_iobenchmark_{dev_name}_{date}.png'.format( + base_url=BENCHMARK_SERVER['Url'], + ticket_number=ticket_number, + dev_name=dev_name, + date=global_vars.get('Date-Time', 'Unknown Date-Time')) + 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'] + if __name__ == '__main__': print("This file is not meant to be called directly.") diff --git a/.bin/Scripts/settings/main.py b/.bin/Scripts/settings/main.py index 22e6f955..13ed594b 100644 --- a/.bin/Scripts/settings/main.py +++ b/.bin/Scripts/settings/main.py @@ -3,7 +3,7 @@ # Features ENABLED_OPEN_LOGS = False ENABLED_TICKET_NUMBERS = False -ENABLED_UPLOAD_DATA = False +ENABLED_UPLOAD_DATA = True # STATIC VARIABLES (also used by BASH and BATCH files) ## NOTE: There are no spaces around the = for easier parsing in BASH and BATCH @@ -12,6 +12,15 @@ ARCHIVE_PASSWORD='Sorted1201' KIT_NAME_FULL='1201-WizardKit' KIT_NAME_SHORT='1201' SUPPORT_MESSAGE='Please let support know by opening an issue on Gogs' +# osTicket +DB_HOST='osticket.1201.com' +DB_NAME='osticket' +DB_USER='wizardkit' +DB_PASS='U9bJnF9eamVkfsVw' +SSH_PORT='22' +SSH_USER='sql_tunnel' +# imgur +IMGUR_CLIENT_ID='3d1ee1d38707b85' # Live Linux MPRIME_LIMIT='7' # of minutes to run Prime95 during hw-diags ROOT_PASSWORD='1201 loves computers!' @@ -41,17 +50,24 @@ BACKUP_SERVERS = [ 'RW-Pass': '1201 loves computers!', }, ] +BENCHMARK_SERVER = { + 'Name': 'Nextcloud', + 'Short Url': 'https://1201north.ddns.net:8001/index.php/f/27892', + 'Url': 'https://1201north.ddns.net:8001/public.php/webdav/Benchmarks', + 'User': 'RAE7ajRk25CBnW6', + 'Pass': '', +} CRASH_SERVER = { - 'Name': "2Shirt's Nextcloud Server", - 'Url': 'https://nextcloud.2shirt.work/public.php/webdav/WK_Issues', - 'User': 'hgoTCWIL28oGWqJ', + 'Name': 'Nextcloud', + 'Url': 'https://1201north.ddns.net:8001/public.php/webdav/WizardKit_Issues', + 'User': 'LoQ97J3r6CFGT2T', 'Pass': '', } OFFICE_SERVER = { 'IP': OFFICE_SERVER_IP, 'Name': 'Anaconda', 'Mounted': False, - 'Share': 'Office', + 'Share': r'Public\Office\MS Office', 'User': 'cx', 'Pass': 'cx', 'RW-User': 'backup', @@ -61,7 +77,7 @@ QUICKBOOKS_SERVER = { 'IP': QUICKBOOKS_SERVER_IP, 'Name': 'Anaconda', 'Mounted': False, - 'Share': 'QuickBooks', + 'Share': r'Public\QuickBooks', 'User': 'cx', 'Pass': 'cx', 'RW-User': 'backup', @@ -71,7 +87,7 @@ WINDOWS_SERVER = { 'IP': '10.11.1.20', 'Name': 'Anaconda', 'Mounted': False, - 'Share': 'Windows', + 'Share': r'Public\Windows', 'User': 'cx', 'Pass': 'cx', 'RW-User': 'backup', diff --git a/.linux_items/include/airootfs/etc/ca-certificates/trust-source/anchors/1201_Root_CA.crt b/.linux_items/include/airootfs/etc/ca-certificates/trust-source/anchors/1201_Root_CA.crt new file mode 100644 index 00000000..7d8ae206 --- /dev/null +++ b/.linux_items/include/airootfs/etc/ca-certificates/trust-source/anchors/1201_Root_CA.crt @@ -0,0 +1,36 @@ +-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- diff --git a/.linux_items/include/airootfs/etc/skel/.ssh/known_hosts b/.linux_items/include/airootfs/etc/skel/.ssh/known_hosts new file mode 100644 index 00000000..b2cbbaee --- /dev/null +++ b/.linux_items/include/airootfs/etc/skel/.ssh/known_hosts @@ -0,0 +1,2 @@ +osticket.1201.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJDDXtNvh4Vd3q3qZkZbIcnDWWO +fJPZb6LVCFptr4awYjlZNL5ieWIUW080IUgtnzWNR7UvetQRtGDsyGu65L+4= diff --git a/.linux_items/packages/live_add b/.linux_items/packages/live_add index ea784699..7ac1faf5 100644 --- a/.linux_items/packages/live_add +++ b/.linux_items/packages/live_add @@ -43,6 +43,7 @@ libinput linux-firmware lm_sensors lzip +mariadb-clients mdadm mediainfo mesa-demos @@ -62,6 +63,8 @@ p7zip papirus-icon-theme progsreiserfs python +python-gnuplot +python-mysql-connector python-psutil python-requests qemu-guest-agent diff --git a/Build Linux b/Build Linux index af01c928..c65ffed0 100755 --- a/Build Linux +++ b/Build Linux @@ -253,6 +253,10 @@ function update_live_env() { echo "ln -sf '/usr/share/zoneinfo/$LINUX_TIME_ZONE' '/etc/localtime'" >> "$LIVE_DIR/airootfs/root/customize_airootfs.sh" echo 'sed -i "s/#FallbackNTP/NTP/" /etc/systemd/timesyncd.conf' >> "$LIVE_DIR/airootfs/root/customize_airootfs.sh" + # Trust root CA(s) + echo "trust extract" >> "$LIVE_DIR/airootfs/root/customize_airootfs.sh" + echo "trust extract-compat" >> "$LIVE_DIR/airootfs/root/customize_airootfs.sh" + # udevil fix echo "mkdir /media" >> "$LIVE_DIR/airootfs/root/customize_airootfs.sh"