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