#!/usr/bin/env python

# -*- coding: utf-8 -*-
from __future__ import print_function
from abc import ABCMeta, abstractmethod
import os
import sys
import re
import inspect
import multiprocessing
import subprocess
from collections import defaultdict

###############################################################################
### CORE
###############################################################################

OK = "OK"
WARN = "WARN"
CRIT = "CRIT"
SKIP = "SKIP"
FAIL = "FAIL"

class Result(object):
    def __init__(self, status=OK, data=None):
        self.status = status
        self.data = data

    def __repr__(self):
        return '{} {}'.format(self.status, self.data)


class Runner(list):
    def __init__(self):
        self._fail = os.getenv("DIAG_FAIL")
        self._silent = os.getenv("DIAG_SILENT")

    def add_check(self, c):
        self.append(c)

    def run(self):
        for c in self:
            self.run_check(c)

    def run_check(self, c):
        try:
            r = c.run()
        except Exception, e:
            print(c, Result(FAIL, str(e)))
            if self._fail:
                raise
        else:
            if not self._silent or r.status != 'OK':
                print(c, r)


class Module(object):
    __metaclass__ = ABCMeta

    @abstractmethod
    def run(self):
        pass

    def __repr__(self):
        return type(self).__name__

class HostModule(Module):
    pass

class InstanceModule(Module):
    itype = "instance_name"

    def __init__(self, instance):
        self.instance = instance
        self.port = instance.split(':')[1]

    def __repr__(self):
        return "{}@{}".format(type(self).__name__, self.port)

    @property
    def program(self):
        return "srch-{}-{}".format(self.itype, self.port)

class UpperModule(InstanceModule):
    itype = "upper"

    @property
    def program(self):
        return "justapache"

class NoapacheModule(InstanceModule):
    itype = "noapache"

class MMetaModule(InstanceModule):
    itype = "mmeta"

class IntModule(InstanceModule):
    itype = "int"

class BaseModule(InstanceModule):
    itype = "base"

###############################################################################
### UTILS
###############################################################################

class BSConfigException(Exception):
    def __init__(self, cmd, code=None, stdout=None, stderr=None, exc=None):
        self.cmd = cmd
        self.code = code
        self.stdout, self.stderr = stdout, stderr
        self.exc = exc

    def __str__(self):
        if self.exc:
            return "BSConfigException: {}: {}".format(self.cmd, self.exc)
        return "BSConfigException: {}: {} {} {}".format(self.cmd, self.code, self.stdout, self.stderr)


class BSConfig(object):
    def __init__(self, exe="/usr/bin/bsconfig"):
        self.exe = exe

    def listtags(self):
        code, out, err = self.run("listtags", "--yasm-format")
        return out

    def run(self, *args):
        cmd = [self.exe] + list(args)
        try:
            code, out, err = call(cmd)
        except Exception, e:
            raise BSConfigException(cmd, exc=e)
        if code != 0:
            raise BSConfigException(cmd, code, out, err)

        return code, out, err


class CalledProcessError(Exception):
    def __init__(self, code, cmd, stdout=None, stderr=None):
        self.code = code
        self.cmd = cmd
        self.stdout = stdout
        self.stderr = stderr

    def __str__(self):
        return "Command '{}' returned non-zero exit status {} (stdout={}, stderr={})".format(
            self.cmd, self.code, self.stdout, self.stderr)

def call(cmd):
    p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    out, err = p.communicate()
    return p.returncode, out, err

def get_output(cmd):
    """like subprocess.check_stdout"""
    code, out, err = call(cmd)
    if code != 0:
        raise CalledProcessError(code, cmd, out, err)
    return out


def error(*msg):
    print("ERR: ", *msg, file=sys.stderr)

def get_bsconfig_itags():
    "Returns parsed bsconfig listtags output"
    bsconfig = BSConfig()
    try:
        tags = bsconfig.listtags()
    except BSConfigException, e:
        error(e)
        return {}
    else:
        itags = {}
        for line in tags.splitlines():
            tags = line.split()
            instace, conf = tags[0].split('@')
            itags[(instace, conf)] = tags[1:]
        return itags

def get_pids(program, pidof='/bin/pidof'):
    cmd = [pidof] + [program]
    code, out, err = call(cmd)
    if code == 1:
        return []
    return [int(pid) for pid in out.split()]

RE_LINK_META = re.compile(r"(?P<number>\d+): (?P<device>[@\w]+): <.*> mtu (?P<mtu>\d+) qdisc (?P<qdisc>\w+) state (?P<state>\w+)")
def ip_link_show(dev=None, ip='/bin/ip'):
    cmd = [ip, 'link', 'show']
    if dev:
        cmd.append(dev)
    links = {}
    out = get_output(cmd)
    meta = None
    for idx, line in enumerate(out.splitlines()):
        if idx % 2:
            m = RE_LINK_META.match(meta).groupdict()
            links[m['device']] = m
        else:
            meta = line
    return links

def ethtool(dev, ethtool='/sbin/ethtool'):
    cmd = [ethtool, dev]
    out = get_output(cmd)
    r = {}
    for line in out.splitlines():
        x = line.split()
        if x[0].startswith('Speed'):
            r['speed'] = x[1]
        elif x[0].startswith('Duplex'):
            r['duplex'] = x[1]
    return r


###############################################################################
### MODULES
###############################################################################

class Memory(HostModule):
    """Check free memory"""
    def __init__(self, meminfo="/proc/meminfo"):
        self.meminfo = meminfo

    def run(self):
        meminfo = self._process_meminfo()
        real_free = meminfo["MemFree"] + meminfo["Cached"]
        total = meminfo["MemTotal"]
        free_pct = float(real_free) / total

        status = OK
        if free_pct < 0.10:
            status = WARN
        if free_pct < 0.05:
            status = CRIT

        return Result(status, data={"free": real_free, "free_pct": free_pct})


    def _process_meminfo(self):
        """Reads /proc/meminfo and returns dict with statistics (MemTotal,
        MemFree, etc.). Units in kB"""
        res = {}
        with file(self.meminfo) as meminfo:
            for line in meminfo:
                data = line.split()
                # strip ending ':'
                key = data[0][:-1]
                value = int(data[1])
                res[key] = value
        return res


class LA(HostModule):
    """Check LA"""
    def __init__(self, la_func=os.getloadavg, ncpu_func=multiprocessing.cpu_count):
        self.la_func = la_func
        self.ncpu_func = ncpu_func

    def run(self):
        n = self.ncpu_func()
        la = self.la_func()

        res = OK
        if la[0] > 1.5 * n:
            res = WARN
        if la[0] > 2 * n:
            res = CRIT

        return Result(res, data=la)


class DiskUsage(HostModule):
    """Check disk usage"""
    def __init__(self, exe="/bin/df"):
        self.exe = exe

    def run(self):
        out = get_output([self.exe, '-k'])
        res, data = OK, []

        # Filesystem 1K-blocks Used Available Use% Mounted on
        lines = out.splitlines()
        for line in lines[1:]:
            fields = line.split()
            avail = int(fields[3])
            pct = int(fields[4].strip('%'))
            if pct >= 98:
                data.append(line)
                res = CRIT
            elif pct > 90 and avail < 15728640: # > 90% and < 15GB
                data.append(line)
                if res != CRIT:
                    res = WARN

        return Result(res, data=data)


class DiskInodesUsage(HostModule):
    """Check disk inodes usage"""
    def __init__(self, exe="/bin/df"):
        self.exe = exe

    def run(self):
        out = get_output([self.exe, '-i'])
        res, data = OK, []

        # Filesystem Inodes IUsed IFree IUse% Mounted on
        lines = out.splitlines()
        for line in lines[1:]:
            fields = line.split()
            pct = int(fields[4].strip('%'))
            if pct >= 98:
                data.append(line)
                res = CRIT
            elif pct >= 90:
                data.append(line)
                if res != CRIT:
                    res = WARN

        return Result(res, data=data)


class Link100Mb(HostModule):
    """Checks 100Mb links"""
    def run(self):
        links = ip_link_show()
        for link in links.values():
            dev = link['device']
            if dev.startswith('vlan'):
                continue
            if link['state'] == 'UP':
                s = ethtool(dev)
                if s['speed'] == "100Mb/s":
                    return Result(CRIT, data={'device': dev, speed: s['speed']})
        return Result()

class LinkMTU1500(HostModule):
    """Checks links with mtu 1500"""
    def run(self):
        links = ip_link_show()
        for link in links.values():
            if link['state'] == 'UP' and link['mtu'] == '1500':
                return Result(CRIT, data={'device': link['device'], 'mtu': link['mtu']})
        return Result()


###############################################################################

class ProcessCheck(InstanceModule):
    @abstractmethod
    def __init__(self, minprocs=1, maxprocs=1):
        self.minprocs = minprocs
        self.maxprocs = maxprocs

    def run(self):
        pids = get_pids(self.program)
        res = OK
        if not pids:
            res = CRIT
        elif len(pids) < self.minprocs:
            res = WARN
        elif self.maxprocs and len(pids) > self.maxprocs:
            res = WARN

        return Result(res, pids)

class CrashesCheck(InstanceModule):
    @abstractmethod
    def __init__(self, maxcraches=1, backtime=15*60):
        pass

###############################################################################

class UpperProcessCheck(ProcessCheck, UpperModule):
    """UPPER: check upperapache2 processes"""
    def __init__(self, instance):
        UpperModule.__init__(self, instance)
        ProcessCheck.__init__(self, minprocs=8, maxprocs=200)

###############################################################################

class NoapacheProcessCheck(ProcessCheck, NoapacheModule):
    """NOAPACHE: check srch-noapache-$PORT process"""
    def __init__(self, instance):
        NoapacheModule.__init__(self, instance)
        ProcessCheck.__init__(self)

###############################################################################

class MMetaProcessCheck(ProcessCheck, MMetaModule):
    """MMETA: check srch-mmeta-$PORT process"""
    def __init__(self, instance):
        MMetaModule.__init__(self, instance)
        ProcessCheck.__init__(self)

###############################################################################

class IntProcessCheck(ProcessCheck, IntModule):
    """INT: check srch-int-$PORT process"""
    def __init__(self, instance):
        IntModule.__init__(self, instance)
        ProcessCheck.__init__(self)

###############################################################################

class BaseProcessCheck(ProcessCheck, BaseModule):
    """BASE: check srch-base-$PORT process"""
    def __init__(self, instance):
        BaseModule.__init__(self, instance)
        ProcessCheck.__init__(self)

###############################################################################

def main():
    clsmembers = inspect.getmembers(sys.modules[__name__], inspect.isclass)
    itags = get_bsconfig_itags()

    r = Runner()
    by_instance = defaultdict(list)

    for name, cls in clsmembers:
        if inspect.isabstract(cls):
            continue
        elif issubclass(cls, HostModule):
            r.add_check(cls())
        elif issubclass(cls, UpperModule):
            by_instance['a_itype_upper'].append(cls)
        elif issubclass(cls, NoapacheModule):
            by_instance['a_itype_noapache'].append(cls)
        elif issubclass(cls, MMetaModule):
            by_instance['a_itype_mmeta'].append(cls)
        elif issubclass(cls, IntModule):
            by_instance['a_itype_int'].append(cls)
        elif issubclass(cls, BaseModule):
            by_instance['a_itype_base'].append(cls)

    for instance, tags in itags.iteritems():
        for tag in tags:
            if tag.startswith('a_itype_'):
                for check in by_instance[tag]:
                    r.add_check(check(instance[0]))
                break

    r.run()


if __name__ == '__main__':
    main()
