cd1e632b4bfa3a748b43dcefebca17f19767eb17
1#! /usr/bin/python
2
3import argparse, logging, os, shutil, re, subprocess, sys, requests, glob, socket, sensors, datetime, time, operator, premailer
4from sys import stdin
5from collections import namedtuple, defaultdict
6
7diskstat = namedtuple('diskstat', ['cap', 'alloc', 'free', 'ratio'])
8drivetemp = namedtuple('drivetemp', ['name', 'temp', 'units'])
9
10
11AUTHPATH = "/var/log/auth.log"
12CRONPATH = "/var/log/cron.log"
13SYSPATH = "/var/log/syslog"
14SMBDDIR = "/var/log/samba"
15ZFSPATH = "/var/log/zpool.log"
16ALLOCPATH = "/tmp/alloc"
17POSTFIXPATH = "/var/log/mail.log"
18HTTPDSTATUS = "http://localhost/server-status"
19HTTPDDIR = "/var/log/apache2"
20HOSTNAMEPATH = "/etc/hostname"
21DUPATHS = ["/home/andrew", "/mnt/andrew"]
22HDDTEMPS = ["/dev/sda", "/dev/sdc", "/dev/sdd", "/dev/sde"]
23HDDTEMPPORT = 7634
24SUMMARYPATH = "/mnt/andrew/temp/logparse-test.html"
25OUTPUTPATH = "/mnt/andrew/temp/logparse-test2.html"
26MAILPATH = "/mnt/andrew/temp/log-parse-test-3.html"
27HEADERPATH = "header.html"
28STYLEPATH = "main.css"
29MAILOUT = ""
30HTMLOUT = ""
31TXTOUT = ""
32TITLE = "logparse"
33MAXLIST = 10
34CMDNO = 3
35MAILSUBJECT = "logparse from $hostname$"
36VERSION = "v0.1"
37# DEG = u'\N{DEGREE SIGN}'.encode('utf-8')
38DEG = 'C'
39
40# Set up logging
41logging.basicConfig(level=logging.DEBUG)
42logger = logging.getLogger('logparse')
43
44# Get arguments
45parser = argparse.ArgumentParser(description='grab logs of some common services and send them by email')
46parser.add_argument('-t','--to', help='mail recipient (\"to\" address)',required=False)
47to = parser.parse_args().to
48
49def __main__():
50 logger.info("Beginning log analysis at " + str(timenow))
51 if (to == None):
52 logger.info("no recipient address provided, outputting to stdout")
53 else:
54 logger.info("email will be sent to " + to)
55
56 global tempfile
57 tempfile = open(SUMMARYPATH, 'w+')
58 tempfile.write(header(HEADERPATH))
59 opentag('div', 1, 'main')
60 sshd()
61 sudo()
62 cron()
63 nameget()
64 httpd()
65 smbd()
66 postfix()
67 zfs()
68 temp()
69 du()
70 for tag in ['div', 'body', 'html']:
71 closetag(tag, 1)
72 tempfile.close()
73 mailprep(SUMMARYPATH, MAILPATH)
74 if (to != None):
75 logger.debug("sending email")
76 ms = subject(MAILSUBJECT)
77 cmd = "cat " + MAILPATH + " | mail --debug-level=10 -a 'Content-type: text/html' -s '" + ms + "' " + to
78 logger.debug(cmd)
79 subprocess.call(cmd, shell=True)
80 logger.info("sent email")
81
82
83def writetitle(title):
84 if (title == '' or '\n' in title):
85 logger.error("invalid title")
86 return
87 logger.debug("writing title for " + title)
88 tag('h2', 0, title)
89
90def writedata(subtitle, data = None): # write title and data to tempfile
91 if (subtitle == ""):
92 loggger.warning("no subtitle provided.. skipping section")
93 return
94
95 tag('p', 0, subtitle)
96 if (data == None):
97 logger.debug("no data provided.. just printing subtitle")
98 else:
99 logger.debug("received data " + str(data))
100 opentag('ul', 1)
101 for datum in data:
102 logger.debug("printing datum " + datum)
103 tag('li', 0, datum)
104 closetag('ul', 1)
105
106def opentag(tag, block = 0, id = None, cl = None): # write html opening tag
107 if (block == 1):
108 tempfile.write('\n')
109 tempfile.write('<' + tag)
110 if (id != None):
111 tempfile.write(" id='" + id + "'")
112 if (cl != None):
113 tempfile.write(" class='" + cl + "'")
114 tempfile.write('>')
115 if (block == 1):
116 tempfile.write('\n')
117
118def closetag(tag, block = 0): # write html closing tag
119 if (block == 0):
120 tempfile.write("</" + tag + ">")
121 else:
122 tempfile.write("\n</" + tag + ">\n")
123
124def tag(tag, block = 0, content = ""): # write html opening tag, content, and html closing tag
125 opentag(tag, block)
126 tempfile.write(content)
127 closetag(tag, block)
128
129def header(template): # return a parsed html header from file
130 headercontent = open(template, 'r').read()
131 headercontent = varpattern.sub(lambda m: varfilter[re.escape(m.group(0))], headercontent)
132 return headercontent
133
134def subject(template):
135 r = varpattern.sub(lambda m: varfilter[re.escape(m.group(0))], template)
136 logger.debug("returning subject line " + r)
137 return r
138
139def hostname(): # get the hostname
140 hnfile = open(HOSTNAMEPATH, 'r')
141 hn = re.search('^(.*)\n*', hnfile.read()).group(1)
142 return hn
143
144def resolve(ip): # try to resolve an ip to hostname
145 logger.debug("trying to resolve ip " + ip)
146 try:
147 socket.inet_aton(ip) # succeeds if text contains ip
148 hn = socket.gethostbyaddr(ip)[0].split(".")[0] # resolve ip to hostname
149 logger.debug("found hostname " + hn)
150 return(hn)
151 except:
152 logger.debug("failed to resolve hostname for " + ip)
153 return(ip) # return ip if no hostname exists
154
155def plural(noun, quantity): # return "1 noun" or "n nouns"
156 if (quantity == 1):
157 return(str(quantity) + " " + noun)
158 else:
159 return(str(quantity) + " " + noun + "s")
160
161def parsesize(num, suffix='B'): # return human-readable size from number of bytes
162 for unit in ['','Ki','Mi','Gi','Ti','Pi','Ei','Zi']:
163 if abs(num) < 1024.0:
164 return "%3.1f %s%s" % (num, unit, suffix)
165 num /= 1024.0
166 return "%.1f%s%s" % (num, 'Yi', suffix)
167
168def readlog(path = None, mode = 'r'): # read file, substituting known paths
169 if (path == None):
170 logger.error("no path provided")
171 return
172 else:
173 path = pathpattern.sub(lambda m: pathfilter[re.escape(m.group(0))], path)
174 return open(path, mode).read()
175
176def writelog(path = None, content = "", mode = 'w'): # read file, substituting known paths
177 if (path == None or content == None):
178 logger.error("invalid usage of writelog")
179 return
180 else:
181 path = pathpattern.sub(lambda m: pathfilter[re.escape(m.group(0))], path)
182 file = open(path, mode)
183 file.write(content)
184 file.close()
185
186def getusage(path): # Get disk usage statistics
187 disk = os.statvfs(path)
188 cap = float(disk.f_bsize*disk.f_blocks) # disk capacity
189 alloc = float(disk.f_bsize*(disk.f_blocks-disk.f_bfree)) # size of path
190 free = float(disk.f_bsize*disk.f_bfree) # free space on disk (blocks, not usable space)
191 ratio = alloc / cap * 100 # percentage used
192 return diskstat(cap, alloc, free, ratio)
193
194def orderbyfreq(l): # order a list by the frequency of its elements and remove duplicates
195 temp_l = l[:]
196 l = list(set(l))
197 l.sort(key=lambda x:temp_l.count(x))
198 return l
199
200def addtag(l, tag): # add prefix and suffix tags to each item in a list
201 l2 = ['<' + tag + '>' + i + '</' + tag + '>' for i in l]
202 return l2
203
204def truncl(input, limit): # truncate list
205 if (len(input) > limit):
206 more = str(len(input) - limit)
207 output = input[-limit:]
208 output.append("+ " + more + " more")
209 return(output)
210 else:
211 return(input)
212
213def mailprep(inputpath, outputpath, *stylesheet):
214 logger.debug("converting stylesheet to inline tags")
215 old = readlog(inputpath)
216 pm = premailer.Premailer(old, external_styles=STYLEPATH)
217 MAILOUT = pm.transform()
218 logger.info("converted stylesheet to inline tags")
219 file = open(outputpath, 'w')
220 file.write(MAILOUT)
221 file.close()
222 logger.info("written to temporary mail file")
223
224
225
226#
227#
228#
229
230def sshd():
231 logger.debug("starting sshd section")
232 opentag('div', 1, 'sshd', 'section')
233 matches = re.findall('.*sshd.*Accepted publickey for .* from .*', readlog('auth')) # get all logins
234 users = [] # list of users with format [username, number of logins] for each item
235 data = []
236 num = sum(1 for x in matches) # total number of logins
237 for match in matches:
238 entry = re.search('^.*publickey\sfor\s(\w*)\sfrom\s(\S*)', match) # [('user', 'ip')]
239
240 user = entry.group(1)
241 ip = entry.group(2)
242
243 userhost = user + '@' + resolve(ip)
244 exists = [i for i, item in enumerate(users) if re.search(userhost, item[0])]
245 if (exists == []):
246 users.append([userhost, 1])
247 else:
248 users[exists[0]][1] += 1
249
250 writetitle('sshd')
251 subtitle = plural('login', num) + ' from'
252 if (len(users) == 1): # if only one user, do not display no of logins for this user
253 logger.debug("found " + str(len(matches)) + " ssh logins for user " + users[0][0])
254 subtitle += ' ' + users[0][0]
255 writedata(subtitle)
256 else:
257 subtitle += ':'
258 for user in users:
259 data.append(user[0] + ' (' + str(user[1]) + ')')
260 if len(data) > MAXLIST: # if there are lots of users, truncate them
261 data.append('+ ' + str(len(users) - MAXLIST - 1) + " more")
262 break
263 logger.debug("found " + str(len(matches)) + " ssh logins for users " + str(data))
264 writedata(subtitle, data)
265 closetag('div', 1)
266 logger.info("finished sshd section")
267
268#
269#
270#
271
272def sudo():
273 logger.debug("starting sudo section")
274 opentag('div', 1, 'sudo', 'section')
275 umatches = re.findall('.*sudo:session\): session opened.*', readlog('auth'))
276 num = sum(1 for line in umatches) # total number of sessions
277 users = []
278 data = []
279 for match in umatches:
280 user = re.search('.*session opened for user root by (\S*)\(uid=.*\)', match).group(1)
281 exists = [i for i, item in enumerate(users) if re.search(user, item[0])]
282 if (exists == []):
283 users.append([user, 1])
284 else:
285 users[exists[0]][1] += 1
286 commands = []
287 cmatches = re.findall('sudo:.*COMMAND\=(.*)', readlog('auth'))
288 for cmd in cmatches:
289 commands.append(cmd)
290 logger.debug("found the following commands: " + str(commands))
291 # temp_cmd=commands[:]
292 # commands = list(set(commands))
293 # commands.sort(key=lambda x:temp_cmd.count(x))
294 commands = orderbyfreq(commands)
295 logger.debug("top 3 sudo commands: " + str(commands[-3:]))
296
297 writetitle("sudo")
298 subtitle = plural("sudo session", num) + " for"
299 if (len(users) == 1):
300 logger.debug("found " + str(num) + " sudo session(s) for user " + str(users[0]))
301 subtitle += ' ' + users[0][0]
302 writedata(subtitle)
303 else:
304 subtitle += ':'
305 for user in users:
306 data.append(user[0] + ' (' + str(user[1]) + ')')
307 if len(data) > 3:
308 data.append('+ ' + str(len(users) - 2) + " more")
309 break
310 logger.debug("found " + str(len(matches)) + " sudo sessions for users " + str(data))
311 writedata(subtitle, data)
312 if (len(commands) > 0):
313 commands = addtag(commands, 'code')
314 commands = truncl(commands, CMDNO)
315 writedata("top sudo commands", [c for c in commands])
316 closetag('div', 1)
317 logger.info("finished sudo section")
318
319#
320#
321#
322
323def cron():
324 logger.debug("starting cron section")
325 opentag('div', 1, 'cron', 'section')
326 matches = re.findall('.*CMD\s*\(\s*(?!.*cd)(.*)\)', readlog('cron'))
327 num = sum(1 for line in matches)
328 commands = []
329 for match in matches:
330 commands.append(str(match))
331 # commands.append([str(match)for match in matches])
332 logger.debug("found cron command " + str(commands))
333 logger.info("found " + str(num) + " cron jobs")
334 subtitle = str(num) + " cron jobs run"
335 writetitle("cron")
336 writedata(subtitle)
337 if (matches > 0):
338 commands = orderbyfreq(commands)
339 commands = addtag(commands, 'code')
340 commands = truncl(commands, CMDNO)
341 writedata("top cron commands", [c for c in commands])
342 closetag('div', 1)
343 logger.info("finished cron section")
344
345#
346#
347#
348
349def nameget():
350 logger.debug("starting nameget section")
351 opentag('div', 1, 'nameget', 'section')
352 syslog = readlog('sys')
353 failed = re.findall('.*nameget.*downloading of (.*) from .*failed.*', syslog)
354 n_f = sum(1 for i in failed)
355 l_f = []
356 for i in failed:
357 l_f.append(i)
358 logger.debug("the following downloads failed: " + str(l_f))
359 succ = re.findall('.*nameget.*downloaded.*', syslog)
360 n_s = sum(1 for i in succ)
361 l_s = []
362 for i in succ:
363 l_s.append(i)
364 logger.debug("the following downloads succeeded: " + str(l_f))
365 logger.debug("found " + str(n_s) + " successful downloads, and " + str(n_f) + " failed attempts")
366 writetitle("nameget")
367 writedata(str(n_s) + " succeeded", truncl(orderbyfreq(l_s), CMDNO))
368 writedata(str(n_f) + " failed", truncl(orderbyfreq(l_f), CMDNO))
369 closetag('div', 1)
370 logger.info("finished nameget section")
371
372#
373#
374#
375
376def httpd():
377 logger.info("starting httpd section")
378 opentag('div', 1, 'httpd', 'section')
379 accesslog = readlog("httpd/access.log")
380 a = len(accesslog)
381 errorlog = readlog("httpd/error.log")
382 e = len(errorlog)
383 data_b = 0
384
385 for line in accesslog.split('\n'):
386 try:
387 data_b += int(re.search('.*HTTP/\d\.\d\" 200 (\d*) ', line).group(1))
388 except Exception as error:
389 if type(error) is AttributeError:
390 pass
391 else:
392 logger.warning("error processing httpd access log: " + str(error))
393 data_h = parsesize(data_b)
394
395 logger.debug("httpd has transferred " + str(data_b) + " bytes in response to " + str(a) + " requests with " + str(e) + " errors")
396
397 writetitle("apache")
398 writedata(data_h + " transferred")
399 writedata(str(a) + " requests")
400 writedata(str(e) + " errors")
401
402 closetag('div', 1)
403 logger.info("finished httpd section")
404
405#
406#
407#
408
409def httpdsession():
410 # logger.debug("starting httpd section")
411 opentag('div', 1, 'httpd', 'section')
412 httpdlog = requests.get(HTTPDSTATUS).content
413 uptime = re.search('.*uptime: (.*)<', httpdlog).group(1)
414 uptime = re.sub(' minute[s]', 'm', uptime)
415 uptime = re.sub(' second[s]', 's', uptime)
416 uptime = re.sub(' day[s]', 's', uptime)
417 uptime = re.sub(' month[s]', 'mo', uptime)
418 accesses = re.search('.*accesses: (.*) - .*', httpdlog).group(1)
419 traffic = re.search('.*Traffic: (.*)', httpdlog).group(1)
420 return("<br /><strong>httpd session: </strong> up " + uptime + ", " + accesses + " requests, " + traffic + " transferred")
421 closetag('div', 1)
422 # logger.info("finished httpd section")
423
424#
425#
426#
427
428def smbd():
429 logger.debug("starting smbd section")
430 opentag('div', 1, 'smbd', 'section')
431 files = glob.glob(SMBDDIR + "/log.*[!\.gz][!\.old]") # find list of logfiles
432 n_auths = 0 # total number of logins from all users
433 sigma_auths = [] # contains users and their respective no. of logins
434 output = ""
435
436 for file in files: # one log file for each client
437
438 # find the machine (ip or hostname) that this file represents
439 ip = re.search('log\.(.*)', file).group(1) # get ip or hostname from file path (/var/log/samba/log.host)
440 host = resolve(ip)
441
442 # count number of logins from each user
443 matches = re.findall('.*sam authentication for user \[(.*)\] succeeded.*', readlog(file))
444 for match in matches:
445 userhost = match + "@" + host
446 exists = [i for i, item in enumerate(sigma_auths) if re.search(userhost, item[0])]
447 if (exists == []):
448 sigma_auths.append([userhost, 1])
449 else:
450 sigma_auths[exists[0]][1] += 1
451 n_auths += 1
452 writetitle("samba")
453 subtitle = plural("login", n_auths) + " from"
454 data = []
455 if (len(sigma_auths) == 1): # if only one user, do not display no of logins for this user
456 subtitle += ' ' + sigma_auths[0][0]
457 writedata(subtitle)
458 else: # multiple users
459 subtitle += ':'
460 for x in sigma_auths:
461 data.append((str(x[0])) + " (" + str(x[1]) + ")")
462 if len(data) > MAXLIST: # if many users, truncate them
463 data.append('+ ' + str(len(sigma_auths) - MAXLIST - 1) + " more")
464 break
465 logger.debug("found " + str(n_auths) + " samba logins for users " + str(sigma_auths))
466 writedata(subtitle, data)
467 closetag('div', 1)
468 logger.info("finished smbd section")
469
470#
471#
472#
473
474def postfix():
475 logger.debug("starting postfix section")
476 opentag('div', 1, 'postfix', 'section')
477 messages = re.findall('.*from\=<.*>, size\=(\d*),.*\n.*\n.*\: removed\n.*', readlog('postfix'))
478 size = sum([int(x) for x in messages])
479 size = parsesize(size)
480 n = str(len(messages))
481 writetitle("postfix")
482 writedata(n + " messages sent")
483 writedata("total of " + size)
484 closetag('div', 1)
485 logger.info("finished postfix section")
486
487#
488#
489#
490
491def zfs():
492 logger.debug("starting zfs section")
493 opentag('div', 1, 'zfs', 'section')
494 zfslog = readlog('zfs')
495 logger.debug("got zfs logfile\n" + zfslog + "---end log---")
496 pool = re.search('.*---\n(\w*)', zfslog).group(1)
497 scrub = re.search('.*scrub repaired (\d*) in \d*h\d*m with (\d*) errors on (\S*\s)(\S*)\s(\d+\s)', zfslog)
498 iostat = re.search('.*---\n\w*\s*(\S*)\s*(\S*)\s', zfslog)
499 scrubrepairs = scrub.group(1)
500 scruberrors = scrub.group(2)
501 scrubdate = scrub.group(3) + scrub.group(5) + scrub.group(4)
502 alloc = iostat.group(1)
503 free = iostat.group(2)
504 writetitle("zfs")
505 subtitle = "Scrub on " + scrubdate + ": "
506 data = [scrubrepairs + " repaired", scruberrors + " errors", alloc + " used", free + " free"]
507 writedata(subtitle, data)
508 closetag('div', 1)
509 logger.info("finished zfs section")
510
511#
512#
513#
514
515def temp():
516 logger.debug("starting temp section")
517 opentag('div', 1, 'temp', 'section')
518 sensors.init()
519 coretemps = []
520 pkgtemp = 0
521 systemp = 0
522 try:
523 print(sensors.iter_detected_chips())
524 for chip in sensors.iter_detected_chips():
525 for feature in chip:
526 if "Core" in feature.label:
527 coretemps.append([feature.label, feature.get_value()])
528 logger.debug("found core " + feature.label + " at temp " + str(feature.get_value()))
529 if "CPUTIN" in feature.label:
530 pkgtemp = str(feature.get_value())
531 logger.debug("found cpu package at temperature " + pkgtemp)
532 if "SYS" in feature.label:
533 systemp = feature.get_value()
534 logger.debug("found sys input " + feature.label + " at temp " + str(feature.get_value()))
535 core_avg = reduce(lambda x, y: x[1] + y[1], coretemps) / len(coretemps)
536 logger.debug("average cpu temp is " + str(core_avg))
537 coretemps.append(["avg", str(core_avg)])
538 coretemps.append(["pkg", pkgtemp])
539 coretemps = [x[0] + ": " + str(x[1]) + DEG for x in coretemps]
540 finally:
541 sensors.cleanup()
542
543 # For this to work, `hddtemp` must be running in daemon mode.
544 # Start it like this (bash): sudo hddtemp -d /dev/sda /dev/sdX...
545 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
546 s.connect(('localhost',HDDTEMPPORT))
547 output = s.recv(4096)
548 output += s.recv(4096)
549 s.close()
550 hddtemps = []
551 for drive in re.split('\|{2}', output):
552 try:
553 fields = re.search('\|*(/dev/sd.)\|.*\|(\d+)\|(.)', drive)
554 name = fields.group(1)
555 temp = float(fields.group(2))
556 units = fields.group(3)
557 hddtemps.append(drivetemp(name, temp, units))
558 except:
559 pass
560 hddtotal = 0
561 data = []
562 for drive in hddtemps:
563 data.append(drive.name + ': ' + str(drive.temp) + drive.units)
564 logger.debug("found disk " + drive.name + " at " + str(drive.temp))
565 hddtotal += drive.temp
566 logger.debug("found " + str(len(hddtemps)) + " disks")
567 logger.debug("sum of disk temps is " + str(hddtotal))
568 hddavg = hddtotal/float(len(hddtemps))
569 logger.debug("avg disk temp is " + str(hddavg))
570 data.append("avg: " + str(hddavg))
571 writetitle("temperatures")
572 if (systemp != 0):
573 writedata("sys: " + str(systemp) + DEG)
574 if (coretemps != ''):
575 writedata("cores", coretemps)
576 if (hddtemps != ''):
577 writedata("disks", data)
578
579 closetag('div', 1)
580 logger.info("finished temp section")
581
582#
583#
584#
585
586def du():
587 logger.debug("starting du section")
588 opentag('div', 1, 'du', 'section')
589 out = []
590 content = readlog('alloc')
591 contentnew = ""
592 for p in DUPATHS:
593 alloc_f = getusage(p).alloc
594 delta = None
595 try:
596 alloc_i = re.search(p + '\t(.*)\n', content).group(1)
597 delta = alloc_f - float(alloc_i)
598 except:
599 pass
600 logger.debug("delta is " + str(delta))
601 if (delta == None):
602 out.append([p, "used " + parsesize(alloc_f)])
603 else:
604 out.append([p, "used " + parsesize(alloc_f), "delta " + parsesize(delta)])
605 contentnew += (p + '\t' + str(alloc_f) + '\n')
606 writelog('alloc', contentnew)
607
608 writetitle("du")
609 logger.debug("disk usage data is " + str(out))
610 for path in out:
611 writedata(path[0], [p for p in path[1:]])
612
613 closetag('div', 1)
614 logger.info("finished du section")
615
616#
617#
618#
619
620timenow = time.strftime("%H:%M:%S")
621datenow = time.strftime("%x")
622
623pathfilter = {"auth": AUTHPATH, "cron": CRONPATH, "sys": SYSPATH, "postfix": POSTFIXPATH, "smb": SMBDDIR, "zfs": ZFSPATH, "alloc": ALLOCPATH, "httpd": HTTPDDIR, "header": HEADERPATH}
624pathfilter = dict((re.escape(k), v) for k, v in pathfilter.iteritems())
625pathpattern = re.compile("|".join(pathfilter.keys()))
626
627varfilter = {"$title$": TITLE, "$date$": datenow, "$time$": timenow, "$hostname$": hostname(), "$version$": VERSION}
628varfilter = dict((re.escape(k), v) for k, v in varfilter.iteritems())
629varpattern = re.compile("|".join(varfilter.keys()))
630
631
632__main__()