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) + ")")