#!/usr/bin/python
#
#    Copyright (C) 2011,2012  Andreas Thienemann <andreas@bawue.net>
#
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
#

"""
=head1 NAME

freeipmi_ - Munin plugin to retreive temperature and fan speed measurements
from a local machine via IPMI.

=head1 APPLICABLE SYSTEMS

All machines with an IPMI capable baseboard management controller.

=head1 CONFIGURATION

On most supported systems this plugin works nearly out of the box as long as
both Python and the freeipmi binaries in a semi-recent version are installed.

If the machine works out of the box can be tested by calling bmc-info.
If there's text output, a bmc card was detected. If there's an entry for
"Sensor Device" visible in the "Additional Device Support" entry you're good.

If you get a "ipmi_cmd_get_device_id: driver timeout" message you have most
likely no bmc to query.

In certain cases however bmc-info will just seem to hang for quite some time.
In this case, autodetection does not work because the smbios table has
incorrect information. One system known to experience this problem is the
HP Proliant Microserver.

Adding env.freeipmi_args "--no-probing --driver-type=KCS --driver-address=0xca2 --register-spacing=1"
to the munin plugin configuration will make the plugin work. This is the
specific line for the HP Proliant Microserver mentioned above. Your mileage
may vary.

Basic configuration for every system is that the plugin needs to be called as root.

Add the following to your /etc/munin/plugin-conf.d/freeipmi:

 [freeipmi_*]
 user root

=head1 INTERPRETATION

The plugin shows the temperature in Celsius or the fanspeed in rotations per minute.

=head1 MAGIC MARKERS

  #%# family=contrib
  #%# capabilities=autoconf suggest

=head1 VERSION

0.0.1

=head1 BUGS

Only local support for now. Remote could be hacked in via freeipmi_args for now.

=head1 AUTHOR

Andreas Thienemann <andreas@bawue.net>

=head1 LICENSE

GPLv3+

=cut
"""

import subprocess
import sys
import os
import re
import pprint

# Parse some environment variables
if os.getenv("freeipmi_args") is not None:
    freeipmi_args = " %s" % (os.getenv("freeipmi_args"))
else:
    freeipmi_args = ""

# We are a wildcard plugin, figure out whether we are called for temp or fan
if sys.argv[0].split("_")[1] == "temp":
    mode = "Temperature"
elif sys.argv[0].split("_")[1] == "fan":
    mode = "Fan"
else:
    mode = None

def whereis(prog):
    """Check if prog can be found in the path and if yes, return the full pathname"""
    prog = os.path.basename(prog)
    for dir in os.getenv("PATH").split(":"):
        for root, dirs, files in os.walk(dir):
            if prog in files:
                return os.path.join(dir, prog)
    return None

def normalize_sensor(name):
    name = name.lower().replace("-","M").replace("+","P")
    name = re.sub("[^a-z0-9A-Z]","_", name)
    return name

# Code sniplet from Philipp Keller
# http://code.pui.ch/2007/02/19/set-timeout-for-a-shell-command-in-python/
def timeout_command(command, timeout):
    """call shell-command and either return its output or kill it
    if it doesn't normally exit within timeout seconds and return None"""
    import subprocess, datetime, os, time, signal
    start = datetime.datetime.now()
    process = subprocess.Popen(command.split(), stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    while process.poll() is None:
        time.sleep(0.1)
        now = datetime.datetime.now()
        if (now - start).seconds> timeout:
            os.kill(process.pid, signal.SIGKILL)
            os.waitpid(-1, os.WNOHANG)
            return None
    return process.stdout.read()

def bmc_detect():
    """Check whether there's a baseboard management controller we can query."""
    if whereis("bmc-info") is None:
        print "no (bmc-info not found in path. Please install FreeIPMI.)"
        sys.exit(0)
    else:
        out = timeout_command("bmc-info%s" % (freeipmi_args), 2)
        if out is not None and "[Sensor Device]" in out:
            print "yes"
            sys.exit(0)
        else:
            print "no (no supported bmc found)"
            sys.exit(0)

def read_sensors():
    """Return all sensor data as a dict"""
    out = timeout_command("ipmi-sensors --verbose%s" % (freeipmi_args), 2)
    sensors = dict()
    sensor = dict()
    sensor_id = None
    for line in out.split("\n"):
        if ":" in line:
            k,v = line.split(": ")
            if k == "Record ID":
                sensor = dict()
                sensor_id = int(v)
                sensor[k] = v
            else:
                sensor[k] = v
        else:
            sensors[sensor_id] = sensor
    return sensors

def print_config():
    """Return configuration arguments for munin"""
    print "graph_title FreeIPMI Sensors: %s" % (mode)
    if mode == "Fan":
        print "graph_vlabel RPM"
        print "graph_info This graph shows the RPMs of the fans as reported by IPMI"
    elif mode == "Temperature":
        print "graph_vlabel Degrees C"
        print "graph_info This graph shows the temperatures as reported by IPMI"
    print "graph_category sensors"
    sensors = read_sensors()

    for id in sorted(sensors):
        if sensors[id]["Group Name"] == mode:
            label = normalize_sensor(sensors[id]["Sensor Name"])
            for n in ["Normal Max.", "Normal Min.", "Sensor Reading", "Lower Critical Threshold", "Upper Critical Threshold", "Lower Non-Critical Threshold", "Upper Non-Critical Threshold"]:
                sensors[id][n] = sensors[id][n].replace("NA","")
                sensors[id][n] = sensors[id][n].split('.')[0]

            print "%s.label %s" % (label, label)
            print "%s.warning %s:%s" % (label, sensors[id]["Lower Non-Critical Threshold"], sensors[id]["Upper Non-Critical Threshold"])
            print "%s.critical %s:%s" % (label, sensors[id]["Lower Critical Threshold"], sensors[id]["Upper Critical Threshold"])
            print "%s.graph_args --base 1000 -l 0" % (label)
            print "%s.graph_scale no" % (label)
#            pprint.pprint(sensors[id])
    sys.exit(0)

def fetch():
    sensors = read_sensors()

    for id in sorted(sensors):
        if sensors[id]["Group Name"] == mode:
            label = normalize_sensor(sensors[id]["Sensor Name"])
            print "%s.value %s" % (label, sensors[id]["Sensor Reading"].split(".")[0])
    sys.exit(0)


if "config" in sys.argv[1:]:
    print_config()

elif "autoconf" in sys.argv[1:]:
    bmc_detect()

elif "suggest" in sys.argv[1:]:
    sensors = read_sensors()
    fan, temperature = [0, 0]
    for id in sensors:
        if sensors[id]["Group Name"] == "Fan":
            fan += 1
        elif sensors[id]["Group Name"] == "Temperature":
            temperature += 1
    if fan > 0:
        print "fan"
    if temperature > 0:
        print "temp"
    sys.exit(0)

else:
    fetch()
