#!/usr/bin/python

# Copyright (C) 2009 - 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 Library General Public License as published by
# the Free Software Foundation; version 2 only
#
# 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 Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#

"""
=head1 NAME

snmp__areca_ - Munin plugin to get temperature, voltage or fan speed values
from Areca network enabled RAID controllers via SNMP.

=head1 APPLICABLE SYSTEMS

All machines with a SNMP capable ARECA raid controller.

=head1 CONFIGURATION

Make sure your Areca controller is accessible via SNMP from the munin host:
snmpwalk -v 1 -c snmp_community ip.add.re.ss

The plugin is a wildcard plugin and can thus be used to retrieve different
values depending on the name of the file.

Linking it as snmp_10.8.1.230_areca_fan would retrieve the fan speed values from
the Areca controller at 10.8.1.230.

Valid values are fan, temp and volt.

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

 [snmp_ip.add.re.ss_*]
 community private
 version 1

Then test the plugin by calling the following commands:

munin-run snmp_10.8.1.230_areca_temp config
munin-run snmp_10.8.1.230_areca_temp

Both commands should output sensible data without failing.

=head1 INTERPRETATION

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

=head1 MAGIC MARKERS

  #%# family=contrib
  #%# capabilities=

=head1 VERSION

0.0.1

=head1 BUGS

None known. If you know of any, please raise a ticket at https://trac.bawue.org/munin/wiki/areca__snmp_

=head1 AUTHOR

Andreas Thienemann <andreas@bawue.net>

=head1 LICENSE

GPLv2

=cut
"""

import pprint
import time
import sys
import re
import os
from pysnmp import v1, v2c, role, asn1

request_conf = {
	"volt" : {
		"label"  : "Voltages",
		"vlabel" : "Volt",
		"graph"  : "--base 1000 --logarithmic",
		"oid"    : ".1.3.6.1.4.1.18928.1.2.2.1.8"
	},
	"fan"  : {
		"label"  : "Fans",
		"vlabel" : "RPM",
		"graph"  : "--base 1000 -l 0",
		"oid"    : ".1.3.6.1.4.1.18928.1.2.2.1.9"
	},
	"temp" : {
		"label"  : "Temperatures",
		"vlabel" : "Celsius",
		"graph"  : "--base 1000 -l 0",
		"oid"   : ".1.3.6.1.4.1.18928.1.2.2.1.10"
	}
}

# Sanity check and parsing of the commandline
host = None
port = 161
request = None
try:
	match = re.search("^(?:|.*\/)snmp_([^_]+)_areca_(.+)$", sys.argv[0])
	host = match.group(1)
	request =  match.group(2)
	match = re.search("^([^:]+):(\d+)$", host)
	if match is not None:
		host = match.group(1)
		port = match.group(2)
except:
	pass

if host is None or request is None:
        print "# Error: Incorrect filename. Cannot parse host or request."
	sys.exit(1)

# Parse env variables
if os.getenv("community") is not None:
	community = os.getenv("community")
else:
	community = "public"
if os.getenv("version") is not None:
	version = os.getenv("version")
else:
	version = "1"


def get_data():
	# Fetch the data
	results = snmpwalk(request_conf[request]["oid"])

	# parse data
	vals = []
	for i in range(0, len(results)):
		idx, res = results[i][0].split(request_conf[request]["oid"])[1].split(".")[1:], results[i][1]
		if idx[1] == '1':
			vals.append([])
			vals[int(idx[2])-1].append(res)
		if idx[1] == '2':
			vals[int(idx[2])-1].append(res)
		if idx[1] == '3':
			if request == "volt":
				res = float(res)/1000
			vals[int(idx[2])-1].append(res)

	return vals

def snmpwalk(root):

	# Create SNMP manager object
	client = role.manager((host, port))

	# Create a SNMP request&response objects from protocol version
	# specific module.
	try:
		req = eval('v' + version).GETREQUEST()
		nextReq = eval('v' + version).GETNEXTREQUEST()
		rsp = eval('v' + version).GETRESPONSE()

	except (NameError, AttributeError):
		print '# Unsupported SNMP protocol version: %s\n%s' % (version, usage)
		sys.exit(-1)

	# Store tables headers
	head_oids = [root]

	encoded_oids = map(asn1.OBJECTID().encode, head_oids)

	result = [];

	while 1:

		# Encode OIDs, encode SNMP request message and try to send
		# it to SNMP agent and receive a response
		(answer, src) = client.send_and_receive(req.encode(community=community, encoded_oids=encoded_oids))

		# Decode SNMP response
		rsp.decode(answer)

		# Make sure response matches request (request IDs, communities, etc)
		if req != rsp:
			raise 'Unmatched response: %s vs %s' % (str(req), str(rsp))

		# Decode BER encoded Object IDs.
		oids = map(lambda x: x[0], map(asn1.OBJECTID().decode, rsp['encoded_oids']))

		# Decode BER encoded values associated with Object IDs.
		vals = map(lambda x: x[0](), map(asn1.decode, rsp['encoded_vals']))

		# Check for remote SNMP agent failure
		if rsp['error_status']:
 			# SNMP agent reports 'no such name' when walk is over
			if rsp['error_status'] == 2:
				# Switch over to GETNEXT req on error
				# XXX what if one of multiple vars fails?
				if not (req is nextReq):
					req = nextReq
					continue
				# One of the tables exceeded
				for l in oids, vals, head_oids:
					del l[rsp['error_index']-1]
			else:
				raise 'SNMP error #' + str(rsp['error_status']) + ' for OID #' + str(rsp['error_index'])

		# Exclude completed OIDs
		while 1:
			for idx in range(len(head_oids)):
				if not asn1.OBJECTID(head_oids[idx]).isaprefix(oids[idx]):
					# One of the tables exceeded
					for l in oids, vals, head_oids:
						del l[idx]
					break
			else:
				break

		if not head_oids:
			return result

		# Print out results
		for (oid, val) in map(None, oids, vals):
			result.append((oid, str(val)))
			# print oid + ' ---> ' + str(val)

		# BER encode next SNMP Object IDs to query
		encoded_oids = map(asn1.OBJECTID().encode, oids)

		# Update request object
		req['request_id'] = req['request_id'] + 1

		# Switch over GETNEXT PDU for if not done
		if not (req is nextReq):
			req = nextReq

	raise "error"


def print_config():
	print "graph_title " + request_conf[request]["label"]
	print "graph_vlabel " + request_conf[request]["vlabel"]
	print "graph_args " + request_conf[request]["graph"]
	print "graph_category sensors"
	print "host_name", host
	for dataset in get_data():
		if request == "volt":
			if dataset[1] == "Battery Status":
				continue
			else:
				print request + dataset[0] + ".label", dataset[1]
				ref_val = float(dataset[1].split()[-1][:-1])
				print request + dataset[0] + ".warning", str(ref_val * 0.95) + ":" + str(ref_val * 1.05)
				print request + dataset[0] + ".critical", str(ref_val * 0.80) + ":" + str(ref_val * 1.20)
		if request == "temp":
			print request + dataset[0] + ".label", dataset[1]
			if dataset[1].startswith("CPU"):
				print request + dataset[0] + ".warning", 55
				print request + dataset[0] + ".critical", 60
			if dataset[1].startswith("System"):
				print request + dataset[0] + ".warning", 40
				print request + dataset[0] + ".critical", 45
		if request == "fan":
			print request + dataset[0] + ".label", dataset[1]
			print request + dataset[0] + ".warning", 2400
			print request + dataset[0] + ".critical", 2000

	sys.exit(0)


if "config" in sys.argv[1:]:
	print_config()
elif "snmpconf" in sys.argv[1:]:
	print "require 1.3.6.1.4.1.18928.1.2.2.1.8.1.1"
	sys.exit(0)
else:
	for dataset in get_data():
		# Filter Battery Status (255 == Not installed)
		if request == "volt" and dataset[1] == "Battery Status":
			continue
		print request + dataset[0] + ".value", dataset[2]
	sys.exit(0)
