638d02a0a50d7f2dd209e3717e888c6a90fc7e37
1#! /usr/bin/python
2
3import argparse, logging, os, shutil, re, subprocess, sys, requests, glob, socket, sensors, datetime, time, operator, premailer, locale
4from sys import stdin
5from collections import namedtuple, defaultdict
6from shutil import copyfile
7import yaml
8import ast
9import logging.handlers
10import types
11import traceback # debugging only
12
13reload(sys)
14sys.setdefaultencoding('utf-8') # force utf-8 because anything else should die
15
16locale.setlocale(locale.LC_ALL, '') # inherit system locale
17
18scriptdir = os.path.dirname(os.path.realpath(__file__))
19
20
21diskstat = namedtuple('diskstat', ['cap', 'alloc', 'free', 'ratio'])
22drivetemp = namedtuple('drivetemp', ['path', 'name', 'temp', 'units'])
23config = {
24 'output': '~/var/www/logparse/summary.html',
25 'header': scriptdir + '/header.html',
26 'css': scriptdir + '/main.css',
27 'title': 'logparse',
28 'maxlist': 10,
29 'maxcmd': 3,
30 'resolve-domains': 'fqdn',
31 'mail': {
32 'to': '',
33 'from': '',
34 'subject': 'logparse from $hostname$'
35 },
36 'rotate': 'y',
37 'hddtemp': {
38 'drives': ['/dev/sda'],
39 'port': 7634,
40 'show-model': False,
41 },
42 'apache': {
43 'resolve-domains': '',
44 },
45 'sshd': {
46 'resolve-domains': '',
47 },
48 'smbd': {
49 'resolve-domains': '',
50 },
51 'httpd': {
52 'resolve-domains': '',
53 },
54 'du-paths': ['/', '/etc', '/home'],
55 'hostname-path': '/etc/hostname',
56 'logs': {
57 'auth': '/var/log/auth.log',
58 'cron': '/var/log/cron.log',
59 'sys': '/var/log/syslog',
60 'smb': '/var/log/samba',
61 'zfs': '/var/log/zpool.log',
62 'alloc': '/var/log/du.log',
63 'postfix': '/var/log/mail.log',
64 'httpd': '/var/log/apache2'
65 }
66}
67
68
69HTTPDSTATUS = "http://localhost/server-status"
70MAILPATH = "/mnt/andrew/temp/logparse/mail.html"
71MAILOUT = ""
72HTMLOUT = ""
73TXTOUT = ""
74VERSION = "v0.1"
75#DEG = u'\N{DEGREE SIGN}'.encode('utf-8')
76DEG = "°".encode('unicode_escape')
77CEL = "C"
78
79# Set up logging
80logging.basicConfig(level=logging.DEBUG)
81logger = logging.getLogger('logparse')
82loghandler = logging.handlers.SysLogHandler(address = '/dev/log')
83loghandler.setFormatter(logging.Formatter(fmt='logparse.py[' + str(os.getpid()) + ']: %(message)s'))
84logger.addHandler(loghandler)
85
86
87# Get arguments
88parser = argparse.ArgumentParser(description='grab logs of some common services and send them by email')
89parser.add_argument('-f', '--function', help='run a specified function with parameters (for debugging purposes',required=False)
90parser.add_argument('-t','--to', help='mail recipient (\"to\" address)',required=False)
91
92def __main__():
93 logger.info("Beginning log analysis at " + str(datenow) + ' ' + str(timenow))
94
95 loadconf(scriptdir + "/logparse.yaml")
96
97 # check if user wants to test an isolated function
98 debugfunc = parser.parse_args().function
99 if debugfunc is not None:
100 logger.debug("executing a single function: " + debugfunc)
101 eval(debugfunc)
102 sys.exit()
103
104 if not config['mail']['to']:
105 logger.info("no recipient address provided, outputting to stdout")
106 else:
107 logger.info("email will be sent to " + config['mail']['to'])
108
109 global LOCALDOMAIN
110 LOCALDOMAIN = getlocaldomain()
111
112 global pathfilter
113 global pathpattern
114 pathfilter = {"auth": config['logs']['auth'], "cron": config['logs']['cron'], "sys": config['logs']['sys'], "postfix": config['logs']['postfix'], "smb": config['logs']['smb'], "zfs": config['logs']['zfs'], "alloc": config['logs']['alloc'], "httpd": config['logs']['httpd'], "header": config['header']}
115 pathfilter = dict((re.escape(k), v) for k, v in pathfilter.iteritems())
116 pathpattern = re.compile("|".join(pathfilter.keys()))
117
118 global varfilter
119 global varpattern
120 varfilter = {"$title$": config['title'], "$date$": datenow, "$time$": timenow, "$hostname$": hostname(), "$version$": VERSION, "$css$": os.path.relpath(config['css'], os.path.dirname(config['output']))}
121 varfilter = dict((re.escape(k), v) for k, v in varfilter.iteritems())
122 varpattern = re.compile("|".join(varfilter.keys()))
123
124 global tempfile
125 tempfile = open(config['output'], 'w+')
126 tempfile.write(header(config['header']))
127 opentag('div', 1, 'main')
128 sshd()
129 sudo()
130 cron()
131 nameget()
132 httpd()
133 smbd()
134 postfix()
135 zfs()
136 temp()
137 du()
138 for tag in ['div', 'body', 'html']:
139 closetag(tag, 1)
140 tempfile.close()
141 mailprep(config['output'], MAILPATH)
142 if (config['mail']['to']):
143 logger.debug("sending email")
144 ms = subject(config['mail']['subject'])
145 cmd = "/bin/cat " + MAILPATH + " | /usr/bin/mail --debug-level=10 -a 'Content-type: text/html' -s '" + ms + "' " + config['mail']['to']
146 logger.debug(cmd)
147 subprocess.call(cmd, shell=True)
148 logger.info("sent email")
149
150
151def writetitle(title):
152 if (title == '' or '\n' in title):
153 logger.error("invalid title")
154 return
155 logger.debug("writing title for " + title)
156 tag('h2', 0, title)
157
158def writedata(subtitle, data = None): # write title and data to tempfile
159 if (subtitle == ""):
160 loggger.warning("no subtitle provided.. skipping section")
161 return
162
163 if (data == None or len(data) == 0):
164 logger.debug("no data provided.. just printing subtitle")
165 tag('p', 0, subtitle)
166 else:
167 logger.debug("received data " + str(data))
168 subtitle += ':'
169 if (len(data) == 1):
170 tag('p', 0, subtitle + ' ' + data[0])
171 else:
172 tag('p', 0, subtitle)
173 opentag('ul', 1)
174 for datum in data:
175 tag('li', 0, datum)
176 closetag('ul', 1)
177
178def opentag(tag, block = 0, id = None, cl = None): # write html opening tag
179 if (block == 1):
180 tempfile.write('\n')
181 tempfile.write('<' + tag)
182 if (id != None):
183 tempfile.write(" id='" + id + "'")
184 if (cl != None):
185 tempfile.write(" class='" + cl + "'")
186 tempfile.write('>')
187 if (block == 1):
188 tempfile.write('\n')
189
190def closetag(tag, block = 0): # write html closing tag
191 if (block == 0):
192 tempfile.write("</" + tag + ">")
193 else:
194 tempfile.write("\n</" + tag + ">\n")
195
196def tag(tag, block = 0, content = ""): # write html opening tag, content, and html closing tag
197 opentag(tag, block)
198 tempfile.write(content)
199 closetag(tag, block)
200
201def header(template): # return a parsed html header from file
202 try:
203 copyfile(config['css'], config['dest'] + '/' + os.path.basename(config['css']))
204 logger.debug("copied main.css")
205 except Exception as e:
206 logger.warning("could not copy main.css - " + str(e))
207 headercontent = open(template, 'r').read()
208 headercontent = varpattern.sub(lambda m: varfilter[re.escape(m.group(0))], headercontent)
209 return headercontent
210
211def subject(template):
212 r = varpattern.sub(lambda m: varfilter[re.escape(m.group(0))], template)
213 logger.debug("returning subject line " + r)
214 return r
215
216def hostname(): # get the hostname of current server
217 hnfile = open(config['hostname-path'], 'r')
218 hn = re.search('^(.*)\n*', hnfile.read()).group(1)
219 return hn
220
221def getlocaldomain(): # get the parent fqdn of current server
222 domain = socket.getfqdn().split('.', 1)
223 if len(domain) == 2:
224 logger.warning('Could not get domain of this server, only hostname. Please consider updating /etc/hosts')
225 return ''
226 else:
227 return domain[-1]
228
229def resolve(ip, fqdn = 'host-only'): # try to resolve an ip to hostname
230 # Possible values for fqdn:
231 # fqdn show full hostname and domain
232 # fqdn-implicit show hostname and domain unless local
233 # host-only only show hostname
234 # ip never resolve anything
235 # resolve-domains defined in individual sections of the config take priority over global config
236
237 if not fqdn:
238 fqdn = config['resolve-domains']
239
240 if fqdn == 'ip':
241 return(ip)
242
243 try:
244 socket.inet_aton(ip) # succeeds if text contains ip
245 hn = socket.gethostbyaddr(ip)[0] # resolve ip to hostname
246 if fqdn == 'fqdn-implicit' and hn.split('.', 1)[1] == LOCALDOMAIN:
247 return(hn.split('.')[0])
248 elif fqdn == 'fqdn' or fqdn == 'fqdn-implicit':
249 return(hn)
250 elif fqdn == 'host-only':
251 return(hn.split('.')[0])
252 else:
253 logger.warning("invalid value for fqdn config")
254 return(hn)
255 except socket.herror:
256 # cannot resolve ip
257 logger.debug(ip + " cannot be found, might not exist anymore")
258 return(ip)
259 except (OSError, socket.error): # socket.error for Python 2 compatibility
260 # already a hostname
261 logger.debug(ip + " is already a hostname")
262 return(ip)
263 except Exception as err:
264 logger.warning("failed to resolve hostname for " + ip + ": " + str(err))
265 return(ip) # return ip if no hostname exists
266
267def plural(noun, quantity): # return "1 noun" or "n nouns"
268 if (quantity == 1):
269 return(str(quantity) + " " + noun)
270 else:
271 return(str(quantity) + " " + noun + "s")
272
273def parsesize(num, suffix='B'): # return human-readable size from number of bytes
274 for unit in ['','Ki','Mi','Gi','Ti','Pi','Ei','Zi']:
275 if abs(num) < 1024.0:
276 return "%3.1f %s%s" % (num, unit, suffix)
277 num /= 1024.0
278 return "%.1f%s%s" % (num, 'Yi', suffix)
279
280def readlog(path = None, mode = 'r'): # read file, substituting known paths
281 if (path == None):
282 logger.error("no path provided")
283 return
284 else:
285 path = pathpattern.sub(lambda m: pathfilter[re.escape(m.group(0))], path)
286 if (os.path.isfile(path) is False):
287 logger.error(path + " does not exist")
288 return ''
289 else:
290 return open(path, mode).read()
291
292def writelog(path = None, content = "", mode = 'w'): # read file, substituting known paths
293 if (path == None or content == None):
294 logger.error("invalid usage of writelog")
295 return
296 else:
297 path = pathpattern.sub(lambda m: pathfilter[re.escape(m.group(0))], path)
298 file = open(path, mode)
299 file.write(content)
300 file.close()
301
302def getusage(path): # Get disk usage statistics
303 disk = os.statvfs(path)
304 cap = float(disk.f_bsize*disk.f_blocks) # disk capacity
305 alloc = float(disk.f_bsize*(disk.f_blocks-disk.f_bfree)) # size of path
306 free = float(disk.f_bsize*disk.f_bfree) # free space on disk (blocks, not usable space)
307 ratio = alloc / cap * 100 # percentage used
308 return diskstat(cap, alloc, free, ratio)
309
310def orderbyfreq(l): # order a list by the frequency of its elements and remove duplicates
311 temp_l = l[:]
312 l = list(set(l))
313 l = [[i, temp_l.count(i)] for i in l] # add count of each element
314 l.sort(key=lambda x:temp_l.count(x[0])) # sort by count
315 l = [i[0] + ' (' + str(i[1]) + ')' for i in l] # put element and count into string
316 l = l[::-1] # reverse
317 return l
318
319def addtag(l, tag): # add prefix and suffix tags to each item in a list
320 l2 = ['<' + tag + '>' + i + '</' + tag + '>' for i in l]
321 return l2
322
323def truncl(input, limit): # truncate list
324 if (len(input) > limit):
325 more = str(len(input) - limit)
326 output = input[:limit]
327 output.append("+ " + more + " more")
328 return(output)
329 else:
330 return(input)
331
332def mailprep(inputpath, output, *stylesheet):
333 logger.debug("converting stylesheet to inline tags")
334 old = readlog(inputpath)
335 logger.debug(config['css'])
336 pm = premailer.Premailer(old, external_styles=config['css'])
337 MAILOUT = pm.transform()
338 logger.info("converted stylesheet to inline tags")
339 file = open(output, 'w')
340 file.write(MAILOUT)
341 file.close()
342 logger.info("written to temporary mail file")
343
344
345
346#
347#
348#
349
350def sshd():
351 logger.debug("starting sshd section")
352 opentag('div', 1, 'sshd', 'section')
353 matches = re.findall('.*sshd.*Accepted publickey for .* from .*', readlog('auth')) # get all logins
354 users = [] # list of users with format [username, number of logins] for each item
355 data = []
356 num = sum(1 for x in matches) # total number of logins
357 for match in matches:
358 entry = re.search('^.*publickey\sfor\s(\w*)\sfrom\s(\S*)', match) # [('user', 'ip')]
359
360 user = entry.group(1)
361 ip = entry.group(2)
362
363 userhost = user + '@' + resolve(ip, fqdn=config['sshd']['resolve-domains'])
364 exists = [i for i, item in enumerate(users) if re.search(userhost, item[0])]
365 if (exists == []):
366 users.append([userhost, 1])
367 else:
368 users[exists[0]][1] += 1
369
370 writetitle('sshd')
371 subtitle = plural('login', num) + ' from'
372 if (len(users) == 1): # if only one user, do not display no of logins for this user
373 logger.debug("found " + str(len(matches)) + " ssh logins for user " + users[0][0])
374 subtitle += ' ' + users[0][0]
375 writedata(subtitle)
376 else:
377 for user in users:
378 data.append(user[0] + ' (' + str(user[1]) + ')')
379 if len(data) > config['maxlist']: # if there are lots of users, truncate them
380 data.append('+ ' + str(len(users) - config['maxlist'] - 1) + " more")
381 break
382 logger.debug("found " + str(len(matches)) + " ssh logins for users " + str(data))
383 writedata(subtitle, data)
384 closetag('div', 1)
385 logger.info("finished sshd section")
386
387#
388#
389#
390
391def sudo():
392 logger.debug("starting sudo section")
393 opentag('div', 1, 'sudo', 'section')
394 umatches = re.findall('.*sudo:session\): session opened.*', readlog('auth'))
395 num = sum(1 for line in umatches) # total number of sessions
396 users = []
397 data = []
398 for match in umatches:
399 user = re.search('.*session opened for user root by (\S*)\(uid=.*\)', match).group(1)
400 exists = [i for i, item in enumerate(users) if re.search(user, item[0])]
401 if (exists == []):
402 users.append([user, 1])
403 else:
404 users[exists[0]][1] += 1
405 commands = []
406 cmatches = re.findall('sudo:.*COMMAND\=(.*)', readlog('auth'))
407 for cmd in cmatches:
408 commands.append(cmd)
409# logger.debug("found the following commands: " + str(commands))
410
411 writetitle("sudo")
412 subtitle = plural("sudo session", num) + " for"
413 if (len(users) == 1):
414 logger.debug("found " + str(num) + " sudo session(s) for user " + str(users[0]))
415 subtitle += ' ' + users[0][0]
416 writedata(subtitle)
417 else:
418 for user in users:
419 data.append(user[0] + ' (' + str(user[1]) + ')')
420 logger.debug("found " + str(num) + " sudo sessions for users " + str(data))
421 writedata(subtitle, data)
422 if (len(commands) > 0):
423 commands = addtag(commands, 'code')
424 commands = orderbyfreq(commands)
425 commands = truncl(commands, config['maxcmd'])
426 writedata("top sudo commands", [c for c in commands])
427 closetag('div', 1)
428 logger.info("finished sudo section")
429
430#
431#
432#
433
434def cron():
435 logger.debug("starting cron section")
436 opentag('div', 1, 'cron', 'section')
437 matches = re.findall('.*CMD\s*\(\s*(?!.*cd)(.*)\)', readlog('cron'))
438 num = sum(1 for line in matches)
439 commands = []
440 for match in matches:
441 commands.append(str(match))
442 # commands.append([str(match)for match in matches])
443 #logger.debug("found cron command " + str(commands))
444 logger.info("found " + str(num) + " cron jobs")
445 subtitle = str(num) + " cron jobs run"
446 writetitle("cron")
447 writedata(subtitle)
448 if (matches > 0):
449 commands = addtag(commands, 'code')
450 commands = orderbyfreq(commands)
451 commands = truncl(commands, config['maxcmd'])
452 writedata("top cron commands", [c for c in commands])
453 closetag('div', 1)
454 logger.info("finished cron section")
455
456#
457#
458#
459
460def nameget():
461 logger.debug("starting nameget section")
462 opentag('div', 1, 'nameget', 'section')
463 logger.debug("reading syslog.. this may take a while")
464 syslog = readlog('sys')
465 failed = re.findall('.*nameget.*downloading of (.*) from .*failed.*', syslog)
466 n_f = sum(1 for i in failed)
467 l_f = []
468 for i in failed:
469 l_f.append(i if i else '[no destination]')
470 logger.debug("the following downloads failed: " + str(l_f))
471 succ = re.findall('.*nameget.*downloaded\s(.*)', syslog)
472 n_s = sum(1 for i in succ)
473 l_s = []
474 for i in succ:
475 l_s.append(i)
476 logger.debug("the following downloads succeeded: " + str(l_f))
477 logger.debug("found " + str(n_s) + " successful downloads, and " + str(n_f) + " failed attempts")
478 writetitle("nameget")
479 writedata(str(n_s) + " succeeded", truncl(l_s, config['maxlist']))
480 writedata(str(n_f) + " failed", truncl(l_f, config['maxlist']))
481 closetag('div', 1)
482 logger.info("finished nameget section")
483
484#
485#
486#
487
488def httpd():
489 logger.info("starting httpd section")
490 opentag('div', 1, 'httpd', 'section')
491 accesslog = readlog("httpd/access.log")
492 a = len(accesslog.split('\n'))
493 errorlog = readlog("httpd/error.log")
494 e = len(errorlog.split('\n'))
495 data_b = 0
496 ips = []
497 files = []
498 useragents = []
499 errors = []
500 notfound = []
501 unprivileged = []
502
503 for line in accesslog.split('\n'):
504 fields = re.search('^(\S*) .*GET (\/.*) HTTP/\d\.\d\" 200 (\d*) \"(.*)\".*\((.*)\;', line)
505 try:
506 ips.append(resolve(fields.group(1), fqdn=config['httpd']['resolve-domains']))
507 files.append(fields.group(2))
508 useragents.append(fields.group(5))
509 data_b += int(fields.group(3))
510 except Exception as error:
511 if type(error) is AttributeError: # this line is not an access log
512 pass
513 else:
514 logger.warning("error processing httpd access log: " + str(error))
515 traceback.print_exc()
516 logger.debug(str(data_b) + " bytes transferred")
517 data_h = parsesize(data_b)
518 writetitle("apache")
519
520 logger.debug("httpd has transferred " + str(data_b) + " bytes in response to " + str(a) + " requests with " + str(e) + " errors")
521 if (a > 0):
522 files = addtag(files, 'code')
523 files = orderbyfreq(files)
524 files = truncl(files, config['maxlist'])
525 writedata(plural(" request", a), files)
526 if (ips != None):
527 ips = addtag(ips, 'code')
528 ips = orderbyfreq(ips)
529 n_ip = str(len(ips))
530 ips = truncl(ips, config['maxlist'])
531 writedata(plural(" client", n_ip), ips)
532 if (useragents != None):
533 useragents = addtag(useragents, 'code')
534 useragents = orderbyfreq(useragents)
535 n_ua = str(len(useragents))
536 useragents = truncl(useragents, config['maxlist'])
537 writedata(plural(" device", n_ua), useragents)
538
539 writedata(data_h + " transferred")
540 writedata(plural(" error", e))
541
542 closetag('div', 1)
543 logger.info("finished httpd section")
544
545#
546#
547#
548
549def httpdsession():
550 # logger.debug("starting httpd section")
551 opentag('div', 1, 'httpd', 'section')
552 httpdlog = requests.get(HTTPDSTATUS).content
553 uptime = re.search('.*uptime: (.*)<', httpdlog).group(1)
554 uptime = re.sub(' minute[s]', 'm', uptime)
555 uptime = re.sub(' second[s]', 's', uptime)
556 uptime = re.sub(' day[s]', 's', uptime)
557 uptime = re.sub(' month[s]', 'mo', uptime)
558 accesses = re.search('.*accesses: (.*) - .*', httpdlog).group(1)
559 traffic = re.search('.*Traffic: (.*)', httpdlog).group(1)
560 return("<br /><strong>httpd session: </strong> up " + uptime + ", " + accesses + " requests, " + traffic + " transferred")
561 closetag('div', 1)
562 # logger.info("finished httpd section")
563
564#
565#
566#
567
568def smbd():
569 logger.debug("starting smbd section")
570 opentag('div', 1, 'smbd', 'section')
571 files = glob.glob(config['logs']['smb'] + "/log.*[!\.gz][!\.old]") # find list of logfiles
572 # for f in files:
573
574 # file_mod_time = os.stat(f).st_mtime
575
576 # Time in seconds since epoch for time, in which logfile can be unmodified.
577 # should_time = time.time() - (30 * 60)
578
579 # Time in minutes since last modification of file
580 # last_time = (time.time() - file_mod_time)
581 # logger.debug(last_time)
582
583 # if (file_mod_time - should_time) < args.time:
584 # print "CRITICAL: {} last modified {:.2f} minutes. Threshold set to 30 minutes".format(last_time, file, last_time)
585 # else:
586
587 # if (datetime.timedelta(datetime.datetime.now() - datetime.fromtimestamp(os.path.getmtime(f))).days > 7):
588 # files.remove(f)
589 logger.debug("found log files " + str(files))
590 n_auths = 0 # total number of logins from all users
591 sigma_auths = [] # contains users
592 output = ""
593
594 for file in files: # one log file for each client
595
596 logger.debug("looking at file " + file)
597
598 # find the machine (ip or hostname) that this file represents
599 ip = re.search('log\.(.*)', file).group(1) # get ip or hostname from file path (/var/log/samba/log.host)
600 host = resolve(ip, fqdn=config['smbd']['resolve-domains'])
601 if (host == ip and (config['smbd']['resolve-domains'] or config['resolve-domains']) != 'ip'): # if ip has disappeared, fall back to a hostname from logfile
602 newhost = re.findall('.*\]\@\[(.*)\]', readlog(file))
603 if (len(set(newhost)) == 1): # all hosts in one file should be the same
604 host = newhost[0].lower()
605
606 # count number of logins from each user-host pair
607 matches = re.findall('.*(?:authentication for user \[|connect to service .* initially as user )(\S*)(?:\] .*succeeded| \()', readlog(file))
608 for match in matches:
609 userhost = match + "@" + host
610 sigma_auths.append(userhost)
611 # exists = [i for i, item in enumerate(sigma_auths) if re.search(userhost, item[0])]
612 # if (exists == []):
613 # sigma_auths.append([userhost, 1])
614 # else:
615 # sigma_auths[exists[0]][1] += 1
616 n_auths += 1
617 writetitle("samba")
618 subtitle = plural("login", n_auths) + " from"
619 if (len(sigma_auths) == 1): # if only one user, do not display no of logins for this user
620 subtitle += ' ' + sigma_auths[0][0]
621 writedata(subtitle)
622 else: # multiple users
623 sigma_auths = orderbyfreq(sigma_auths)
624 sigma_auths = truncl(sigma_auths, config['maxlist'])
625 logger.debug("found " + str(n_auths) + " samba logins for users " + str(sigma_auths))
626 writedata(subtitle, sigma_auths)
627 closetag('div', 1)
628 logger.info("finished smbd section")
629
630#
631#
632#
633
634def postfix():
635 logger.debug("starting postfix section")
636 opentag('div', 1, 'postfix', 'section')
637 messages = re.findall('.*from\=<(.*)>, size\=(\d*),.*\n.*to=<(.*)>', readlog('postfix'))
638 r = []
639 s = []
640 size = 0
641 for message in messages:
642 r.append(message[2])
643 s.append(message[0])
644 size += int(message[1])
645 # size = sum([int(x) for x in messages])
646 size = parsesize(size)
647 n = str(len(messages))
648 writetitle("postfix")
649
650 if (len(r) > 0):
651 s = list(set(r)) # unique recipients
652 if (len(s) > 1):
653 r = orderbyfreq(r)
654 r = truncl(r, config['maxlist'])
655 writedata(n + " messages sent to", r)
656 else:
657 writedata(n + " messages sent to " + r[0])
658 else:
659 writedata(n + " messages sent")
660 writedata("total of " + size)
661 closetag('div', 1)
662 logger.info("finished postfix section")
663
664#
665#
666#
667
668def zfs():
669 logger.debug("starting zfs section")
670 opentag('div', 1, 'zfs', 'section')
671 zfslog = readlog('zfs')
672 pool = re.search('.*---\n(\w*)', zfslog).group(1)
673 scrub = re.search('.*scrub repaired (\d*).* in .*\d*h\d*m with (\d*) errors on (\S*\s)(\S*)\s(\d+\s)', zfslog)
674 iostat = re.search('.*---\n\w*\s*(\S*)\s*(\S*)\s', zfslog)
675 scrubrepairs = scruberrors = scrubdate = None
676 try:
677 scrubrepairs = scrub.group(1)
678 scruberrors = scrub.group(2)
679 scrubdate = scrub.group(3) + scrub.group(5) + scrub.group(4)
680 except Exception as e:
681 logger.debug("error getting scrub data: " + str(e))
682 alloc = iostat.group(1)
683 free = iostat.group(2)
684 writetitle("zfs")
685 if (scrubdate != None):
686 subtitle = "Scrub of " + pool + " on " + scrubdate
687 data = [scrubrepairs + " repaired", scruberrors + " errors", alloc + " used", free + " free"]
688 else:
689 subtitle = pool
690 data = [alloc + " used", free + " free"]
691 writedata(subtitle, data)
692 closetag('div', 1)
693 logger.info("finished zfs section")
694
695#
696#
697#
698
699def temp():
700 logger.debug("starting temp section")
701 opentag('div', 1, 'temp', 'section')
702
703 # cpu temp
704
705 sensors.init()
706 coretemps = []
707 pkgtemp = 0
708 systemp = 0
709 try:
710 for chip in sensors.iter_detected_chips():
711 for feature in chip:
712 if "Core" in feature.label:
713 coretemps.append([feature.label, feature.get_value()])
714 logger.debug("found core " + feature.label + " at temp " + str(feature.get_value()))
715 if "CPUTIN" in feature.label:
716 pkgtemp = str(feature.get_value())
717 logger.debug("found cpu package at temperature " + pkgtemp)
718 if "SYS" in feature.label:
719 systemp = feature.get_value()
720 logger.debug("found sys input " + feature.label + " at temp " + str(feature.get_value()))
721 core_avg = reduce(lambda x, y: x[1] + y[1], coretemps) / len(coretemps)
722 logger.debug("average cpu temp is " + str(core_avg))
723 coretemps.append(["avg", str(core_avg)])
724 coretemps.append(["pkg", pkgtemp])
725 coretemps = [x[0] + ": " + str(x[1]) + DEG + CEL for x in coretemps]
726 finally:
727 sensors.cleanup()
728
729 # drive temp
730
731 # For this to work, `hddtemp` must be running in daemon mode.
732 # Start it like this (bash): sudo hddtemp -d /dev/sda /dev/sdX...
733
734 received = ''
735 sumtemp = 0.0
736 data = ""
737 output = []
738
739 try:
740 hsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
741 hsock.connect(("localhost", int(config['hddtemp']['port'])))
742 logger.debug("tcp socket on port " + str(int(config['hddtemp']['port'])) + " opened for `hddtemp` (ensure daemon is running)")
743 hsock.sendall('') # send dummy packet and shut write conn
744 hsock.shutdown(socket.SHUT_WR)
745
746 while 1:
747 line = hsock.recv(1024)
748 if line == "": # exit on blank line
749 break
750 logger.debug("received line " + str(line))
751 data += line
752 hsock.close()
753 logger.debug("closed connection, having received " + str(sys.getsizeof(data)) + " bytes")
754
755 data = data.lstrip('|').rstrip('|') # remove leading & trailing `|`
756 drives = data.split('|' * 2) # split into drives
757
758 for drive in drives:
759 fields = drive.split('|')
760 if fields[0] in config['hddtemp']['drives']:
761 output.append(fields[0] + (' (' + fields[1] + ')' if config['hddtemp']['show-model'] else '')+ ': ' + fields[2] + DEG + fields[3])
762 sumtemp += int(fields[2])
763 logger.debug("added drive " + fields[0])
764 else:
765 logger.debug("ignoring drive " + fields[0])
766
767 hddavg = int(format(sumtemp/float(len(drives)))) + e + DEG + output[0][-1:] # use units of first drive (last character of output)
768 logger.debug("avg disk temp is " + str(hddavg))
769 output.append("avg: " + str(hddavg))
770 except Exception as ex:
771 logger.debug("failed getting hddtemps with error " + str(ex))
772 finally:
773 hsock.close()
774
775 writetitle("temperatures")
776 if (systemp != 0):
777 writedata("sys: " + str(systemp) + DEG)
778 if (coretemps != ''):
779 writedata("cores", coretemps)
780 if (config['hddtemp']['drives'] != ''):
781 writedata("disks", output)
782
783 closetag('div', 1)
784 logger.info("finished temp section")
785
786#
787#
788#
789
790def du():
791 logger.debug("starting du section")
792 opentag('div', 1, 'du', 'section')
793 out = []
794 content = readlog('alloc')
795 contentnew = ""
796 for p in config['du-paths']:
797 alloc_f = getusage(p).alloc
798 delta = None
799 try:
800 alloc_i = re.search(p + '\t(.*)\n', content).group(1)
801 delta = alloc_f - float(alloc_i)
802 except:
803 pass
804 if (delta == None):
805 out.append([p, "used " + parsesize(alloc_f)])
806 else:
807 out.append([p, "used " + parsesize(alloc_f), "delta " + parsesize(delta)])
808 contentnew += (p + '\t' + str(alloc_f) + '\n')
809 if config['rotate'] == 'y':
810 writelog('alloc', contentnew)
811
812 writetitle("du")
813 logger.debug("disk usage data is " + str(out))
814 for path in out:
815 writedata(path[0], [p for p in path[1:]])
816
817 closetag('div', 1)
818 logger.info("finished du section")
819
820#
821#
822#
823starttime = datetime.datetime.now()
824timenow = time.strftime("%H:%M:%S")
825datenow = time.strftime("%x")
826
827def loadconf(configfile):
828 try:
829 data = yaml.safe_load(open(configfile))
830 for value in data:
831 if(type(data[value]) == types.DictType):
832 for key in data[value].iteritems():
833 config[value][key[0]] = key[1]
834 else:
835 config[value] = data[value]
836 config['dest'] = os.path.dirname(config['output'])
837 if parser.parse_args().to is not None: config['mail']['to'] = parser.parse_args().to
838 except Exception as e:
839 logger.warning("error processing config: " + str(e))
840
841
842try:
843 __main__()
844finally:
845 # rotate logs using systemd logrotate
846 if parser.parse_args().function is None:
847 if (config['rotate'] == 'y'):
848 subprocess.call("/usr/sbin/logrotate -f /etc/logrotate.conf", shell=True)
849 logger.info("rotated logfiles")
850 else:
851 logger.debug("user doesn't want to rotate logs")
852 if (config['rotate'] == 's'):
853 logger.debug("Here is the output of `logrotate -d /etc/logrotate.conf` (simulated):")
854 sim = subprocess.check_output("/usr/sbin/logrotate -d /etc/logrotate.conf", shell=True)
855 logger.debug(sim)
856
857 timenow = time.strftime("%H:%M:%S")
858 datenow = time.strftime("%x")
859 logger.info("finished parsing logs at " + datetime.datetime.now().strftime("%x %H:%M:%S") + " (" + str(datetime.datetime.now() - starttime) + ")")