#!/usr/bin/env python
from __future__ import with_statement
import os
import re
import sys
import argparse
from sys import exit

OK = 0
WARN = 1
CRIT = 2
UNKN = 3

class NoBond(Exception):
    def __init__(self, name):
        self.name = name
        self.message = "Can't find bond '%s'" % self.name

class Bond(object):
    def __init__(self, name="bond0"):
        self.name = name
        self.__interfaces = {}
        self.number_of_ports = None
        self.transmit_hash = "No Hash Policy"
        self.churned_state = ''
    def add_interface(self, name, status):
        self.__interfaces[name] = status

    def interfaces(self):
        return self.__interfaces.keys()

    def bad_interfaces(self):
        return [interface for (interface,status) in self.__interfaces.iteritems() if status.lower() != "up"]

    def good_interfaces(self):
        return [interface for (interface,status) in self.__interfaces.iteritems() if status.lower() == "up"]

    def __str__(self):
        return self.name

    def __repr__(self):
        return "<bond: '%s'>" % self.name

def parse_bondfile(bond_name):
    """
    Takes a bond to check.
    Returns None if the bond can't be read.
    Else it returns a hash of various data
    """
    bond = Bond(bond_name)
    current_interface = bond_name
    bonding_file=os.path.join("/proc/net/bonding/", bond_name)
    if not os.path.isfile(bonding_file):
        raise NoBond(bond_name)

    with open(bonding_file, "r") as f:
        for line in f.readlines():
            if line.startswith("Bonding Mode: "):
                bond.bonding_mode=line.replace("Bonding Mode: ", "").rstrip('\n')

            elif line.startswith("Slave Interface: "):
                current_interface = line.replace("Slave Interface: ", "").rstrip('\n')

            elif line.startswith("Transmit Hash Policy: "):
                bond.transmit_hash = line.replace("Transmit Hash Policy: ", "").rstrip('\n')

            elif line.startswith("MII Status: "):
                status = line.replace("MII Status: ", "").rstrip('\n')
                if not current_interface.startswith("bond"):
                    bond.add_interface(current_interface, status)
                else:
                    bond.status = status
           
            #Handle Xenial
            elif " Churn State: " in line:
                #grabs the status of the Actor or Partner Churn State
                churn_string = line.split(' ')
                if churn_string[3].rstrip('\n') == "churned":
                    bond.churned_state = "%(role_type)s on %(interface)s\n" % \
                        {'role_type': churn_string[0], \
                        'interface': current_interface }
                    churned_interface = current_interface

            #if churned, return the count for each role for informative alerting
            elif (" Churned Count:" in line) and (bond.churned_state) \
                and current_interface == churned_interface:
                bond.churned_state += line

            elif re.match(r'^[ \t]+Number of ports: ', line):
                bond.number_of_ports = int(re.sub(r'^[ \t]+Number of ports: ', '', line).rstrip())
    return bond
    

def find_bonds():
    """
    Return a list of bonds found on the local machine
    """
    try:
        return os.listdir("/proc/net/bonding/")
    except:
        return []

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('-i', '--interface', metavar='INTERFACE', dest='check_interface', default=None)
    args=parser.parse_args()

    if args.check_interface:
        bonds = [args.check_interface]
    else:
        bonds = find_bonds()
        if len(bonds) < 1:
            print "CRIT: No bonds available"
            exit(CRIT)

    results={}
    for bond in bonds:
        try: 
            results[bond] = parse_bondfile(bond)
        except NoBond, e:
            print "CRIT: Can't find bond: '%s'" % (e.name)
            exit(CRIT)

    critical_messages = []
    warning_messages = []
    ok_messages = []

    # Now we check for various errors. If the error is found we build a message and add it to the list.
    # At the end we combine all of the messages into one message and print it out.

    for name, bond in results.iteritems():
        if bond.status.lower() != 'up':
            critical_messages.append("%(name)s: Bond is down" %
                    {"name": name})

        if bond.bad_interfaces():
            critical_messages.append("%(name)s: %(interfaces)s down - Bond Mode: '%(mode)s' - Transmit Hash: '%(hash)s'" % \
                    {"name": name, "mode": bond.bonding_mode, "hash": bond.transmit_hash,
                        "interfaces": ", ".join(sorted(bond.bad_interfaces())),
                        })

        if bond.transmit_hash != "layer3+4 (1)" and bond.transmit_hash != "No Hash Policy":
            critical_messages.append("%(name)s: Invalid Transmit Hash: '%(hash)s'" % \
                    {"name": name, "hash": bond.transmit_hash,
                            })
        if bond.churned_state:
            critical_messages.append("%(name)s: Interface is in Churned state: %(churned)s " % \
                {"name": name, "churned": bond.churned_state,
                            })
        if bond.number_of_ports < 2:
            critical_messages.append("%(name)s: only has 1 interface configured" %
                    {"name": name})
        # number_of_ports is None on non-802.3ad protocolos, so it shouldn't be checked
        # Only alert if the number_of_ports in the bond is less than the number of good interfaces
        if bond.number_of_ports and bond.number_of_ports != len(bond.interfaces()):
            critical_messages.append("%(name)s: Interfaces missing from bond: %(ports)d of %(good)d" % \
                    {"name": name, "ports": bond.number_of_ports, "good": len(bond.interfaces()),
                        })

        ok_messages.append("%(name)s: Bond Mode: '%(mode)s'; Transmit Hash: '%(hash)s'" % \
                {"name": name, "mode": bond.bonding_mode, "hash": bond.transmit_hash,
                        })

    if critical_messages:
        print "CRIT %s" % "; ".join(critical_messages)
        exit(CRIT)
    elif warning_messages:
        print "WARN %s" % "; ".join(warning_messages)
        exit(WARN)
    else:
        print "OK %s" % "; ".join(ok_messages)
        exit(OK)

if __name__ == "__main__":
    main()
