1#!/usr/bin/env python3 2# -*- coding: utf-8 -*- 3 4import subprocess, csv, argparse, tempfile 5from datetime import datetime, timedelta 6from collections import Counter 7import matplotlib.pyplot as plt 8import matplotlib.dates as mdates 9import dateutil.parser 10 11 12def get_arguments(): 13 """Get arguments from command line""" 14 15 parser = argparse.ArgumentParser() 16 parser.add_argument( 17 '-b', '--bdate', 18 dest='bdate', 19 action='store', 20 help='begin date to plot, format:"d-m-Y"', 21 type=str 22 ) 23 parser.add_argument( 24 '-e', '--edate', 25 dest='edate', 26 action='store', 27 help='end date to plot, format:"d-m-Y" (default today)', 28 type=str 29 ) 30 parser.add_argument( 31 '-f', '--filedb', 32 dest='dbfile', 33 default=None, 34 action='store', 35 help='database file' 36 ) 37 parser.add_argument( 38 '-H', '--height', 39 dest='height', 40 default=13, 41 action='store', 42 help='window height in cm (default 13)', 43 type=int 44 ) 45 parser.add_argument( 46 '-p', '--pastdays', 47 dest='pdays', 48 default=7, 49 action='store', 50 help='past days before edate to plot (default is 7)', 51 type=int 52 ) 53 parser.add_argument( 54 '-W', '--width', 55 dest='width', 56 default=17, 57 action='store', 58 help='window width in cm (default 17)', 59 type=int 60 ) 61 parser.add_argument( 62 '-x', 63 dest='report_pie', 64 action='store_true', 65 default=False, 66 help='swich to pie report with accumulated hours' 67 ) 68 arg = parser.parse_args() 69 return arg 70 71 72def date_check(arg): 73 """Check and clean dates""" 74 75 # Set user provided or default end date 76 if arg.edate: 77 end_date = dateutil.parser.parse(arg.edate) 78 else: 79 end_date = datetime.today() 80 print('Default end:\tnow') 81 82 # Set user provided or default begind date. Days ago... 83 if arg.bdate: 84 begin_date = dateutil.parser.parse(arg.bdate) 85 else: 86 begin_date = end_date - timedelta(days=arg.pdays) 87 print('Default begin:\tsince ' + str(arg.pdays) + ' days ago') 88 89 90 # Adjust date to the start or end time range and set the format 91 begin_date = begin_date.replace(hour=0, minute=0, second=0).strftime("%d-%b-%Y %H:%M:%S") 92 end_date = end_date.replace(hour=23, minute=59, second=59).strftime("%d-%b-%Y %H:%M:%S") 93 94 print('Begin datetime:\t' + str(begin_date)) 95 print('End datetime:\t' + str(end_date)) 96 97 return([begin_date, end_date]) 98 99 100 101def main(): 102 """Core logic""" 103 104 arg = get_arguments() 105 date_limits = date_check(arg) 106 107 ftmp = tempfile.NamedTemporaryFile().name # For Tuptime csv file 108 tst = {'up': [], 'down': [], 'down_ok': [], 'down_bad': []} # Store events list on range 109 110 # Get datetime objects from date limits in timestamp format 111 tsince = str(int(dateutil.parser.parse(date_limits[0]).timestamp())) 112 tuntil = str(int(dateutil.parser.parse(date_limits[1]).timestamp())) 113 114 # Query tuptime for every (since, until) and save output to a file 115 with open(ftmp, "wb", 0) as out: 116 if arg.dbfile: # If a file is passed, avoid update it 117 subprocess.call(["tuptime", "-tsc", "--tsince", tsince, "--tuntil", tuntil, "-f", arg.dbfile, "-n"], stdout=out) 118 else: 119 subprocess.call(["tuptime", "-tsc", "--tsince", tsince, "--tuntil", tuntil], stdout=out) 120 121 # Parse csv file 122 with open(ftmp) as csv_file: 123 csv_reader = csv.reader(csv_file, delimiter=',') 124 125 for row in csv_reader: 126 if row[0] == 'No.': 127 continue 128 129 #print('Startup T.: ' + row[1]) 130 #print('Uptime: ' + row[2]) 131 #print('Shutdown T.: ' + row[3]) 132 #print('End: ' + row[4]) 133 #print('Downtime: ' + row[5]) 134 135 if row[1] != '': 136 tst['up'].append(row[1]) 137 138 if row[3] != '': 139 if row[4] == 'BAD': 140 tst['down_bad'].append(row[3]) 141 else: 142 tst['down_ok'].append(row[3]) 143 144 # Set whole downtimes and convert to datetime object 145 tst['down'] = tst['down_ok'] + tst['down_bad'] 146 for state in tst: 147 tst[state] = [datetime.fromtimestamp(int(elem)) for elem in tst[state]] 148 149 if arg.report_pie: 150 pie = {'up': [], 'down': []} # One plot for each type 151 152 # From datetime, get only hour 153 for elem in tst['up']: pie['up'].append(str(elem.hour)) 154 for elem in tst['down']: pie['down'].append(str(elem.hour)) 155 156 # Count elements on list or set '0' if emtpy. Get list with items 157 pie['up'] = dict(Counter(pie['up'])).items() if pie['up'] else [('0', 0)] 158 pie['down'] = dict(Counter(pie['down'])).items() if pie['down'] else [('0', 0)] 159 160 # Values ordered by first element on list that was key on source dict 161 pie['up'] = sorted(pie['up'], key=lambda ordr: int(ordr[0])) 162 pie['down'] = sorted(pie['down'], key=lambda ordr: int(ordr[0])) 163 164 # Set two plots and their frame size 165 _, axs = plt.subplots(1, 2, figsize=((arg.width / 2.54), (arg.height / 2.54))) 166 167 # Set values for each pie plot 168 axs[0].pie([v[1] for v in pie['up']], labels=[k[0].rjust(2, '0') + str('h') for k in pie['up']], 169 autopct=lambda p : '{:.1f}%\n{:,.0f}'.format(p,p * sum([v[1] for v in pie['up']])/100), 170 startangle=90, counterclock=False, 171 textprops={'fontsize': 8}, wedgeprops={'alpha':0.85}) 172 axs[0].set(aspect="equal", title='Startup') 173 174 axs[1].pie([v[1] for v in pie['down']], labels=[str(k[0]).rjust(2, '0') + str('h') for k in pie['down']], 175 autopct=lambda p : '{:.1f}%\n{:,.0f}'.format(p,p * sum([v[1] for v in pie['down']])/100), 176 startangle=90, counterclock=False, 177 textprops={'fontsize': 8}, wedgeprops={'alpha':0.85}) 178 axs[1].set(aspect="equal", title='Shutdown') 179 180 plt.suptitle("Events per Hours in Range", fontsize=14) 181 182 else: 183 # Reset date allows position circles inside the same 00..24 range on y-axis 184 scatt_y = {'up': [], 'down_ok': [], 'down_bad': []} 185 scatt_y['up'] = [elem.replace(year=1970, month=1, day=1) for elem in tst['up']] 186 scatt_y['down_ok'] = [elem.replace(year=1970, month=1, day=1) for elem in tst['down_ok']] 187 scatt_y['down_bad'] = [elem.replace(year=1970, month=1, day=1) for elem in tst['down_bad']] 188 189 # Reset hour allows position circles straight over the date tick on x-axis 190 scatt_x = {'up': [], 'down_ok': [], 'down_bad': []} 191 scatt_x['up'] = [elem.replace(hour=00, minute=00, second=00) for elem in tst['up']] 192 scatt_x['down_ok'] = [elem.replace(hour=00, minute=00, second=00) for elem in tst['down_ok']] 193 scatt_x['down_bad'] = [elem.replace(hour=00, minute=00, second=00) for elem in tst['down_bad']] 194 195 # Set width and height from inches to cm 196 plt.figure(figsize=((arg.width / 2.54), (arg.height / 2.54))) 197 198 # Set scatter plot values 199 plt.scatter(scatt_x['up'], scatt_y['up'], s=200, color='forestgreen', edgecolors='white', alpha=0.85, marker="X", label='Up') 200 plt.scatter(scatt_x['down_ok'], scatt_y['down_ok'], s=200, color='grey', edgecolors='white', alpha=0.85, marker="X", label='Down ok') 201 plt.scatter(scatt_x['down_bad'], scatt_y['down_bad'], s=200, color='black', edgecolors='white', alpha=0.85, marker="X", label='Down bad') 202 203 # Format axes: 204 plt.gcf().autofmt_xdate() 205 axs = plt.gca() 206 207 # X as days and defined limits with their margin 208 axs.xaxis.set_major_formatter(mdates.DateFormatter('%d-%b-%y')) 209 axs.xaxis.set_major_locator(mdates.DayLocator()) 210 plt.xlim(datetime.strptime(date_limits[0], '%d-%b-%Y %H:%M:%S') - timedelta(hours=4), 211 datetime.strptime(date_limits[1], '%d-%b-%Y %H:%M:%S') - timedelta(hours=20)) 212 213 # Y as 24 hours range 214 axs.yaxis.set_major_formatter(mdates.DateFormatter('%H:%M')) 215 axs.yaxis.set_major_locator(mdates.HourLocator(byhour=range(0, 24, 2))) 216 plt.ylim([datetime(1970, 1, 1, 00, 00), datetime(1970, 1, 1, 23, 59, 59)]) 217 218 axs.set_axisbelow(True) 219 axs.invert_yaxis() 220 plt.grid(True) 221 plt.ylabel('Day Time') 222 plt.title('Events on Time by Day') 223 plt.margins(y=0, x=0.01) 224 plt.grid(color='lightgrey', linestyle='--', linewidth=0.9, axis='y') 225 plt.legend() 226 227 plt.tight_layout() 228 cfig = plt.get_current_fig_manager() 229 cfig.canvas.set_window_title("Tuptime") 230 plt.show() 231 232if __name__ == "__main__": 233 main() 234