#!/usr/bin/env python
#%# family=auto
#%# capabilities=autoconf

"""
Munin plugin that monitors the greyfix triplet database.
Author: Tom Hendrikx <tom@whyscream.net> 2011-08-05

This plugin monitors the Greyfix greylisting tool for Postfix.
It creates a nice overview of the number of triplets in the greyfix database,
dividing the database population in configurable ages.

For more information about greyfix: http://trac.kim-minh.com/greyfix/

Installation
============

To install, create a symlink to the plugin in the munin plugin directory:
$ ln -s /path/to/plugin/greyfix /etc/munin/plugins/greyfix

Configuration
=============

There are some settings that can be tweaked by adding statements to the 
munin-node config:

[greyfix]
# run plugin as the same user as postfix does
user nobody
# path to greyfix binary (default: /usr/sbin/greyfix)
env.greyfix /usr/local/sbin/greyfix
# the length of each graph step in days (default: 7)
env.step_size 3
# the number of steps to graph (default: 11)
env.num_steps 47
# graph the greylisted triplets separate from the whitelisted ones (default: yes)
env.greylist_step no

Please note that the last step has no end date, so it includes all triplets 
older than the second last step. I.e., the defaults (as named above) create a 
graph that shows 10 steps of one week each, and one last step for everything 
older than 10 weeks. Also, the separate greylist step is not considered 
when applying num_steps.

"""

# Settings: all of these can be redefined in the munin-node config
settings = {
	'greyfix': '/usr/sbin/greyfix',
	'step_size': 7,
	'num_steps': 11,
	'greylist_step': 'yes'
}


import os
import sys
import subprocess
import datetime


def greyfix_parse_triplets():
	"""Parse output of greyfix --dump-triplets into something that we can use.

	The output of the command has the following columns:
	sender_ip, sender_email, recipient_email, timestamp_first_seen, timestamp_last_seen, block_count, pass_count

	Also see http://groups.google.com/group/greyfix/browse_thread/thread/510687a9ed94fc2c"""

	greyfix = subprocess.Popen(args=[ settings['greyfix'], '--dump-triplets'], stdout=subprocess.PIPE)
	stdout = greyfix.communicate()[0]
	if greyfix.returncode > 0:
		print '# greyfix exited with exit code %i' % (greyfix.returncode)
		sys.exit(greyfix.returncode)
	
	triplets = []
	for line in stdout.split("\n"):
		triplet = line.split("\t")
		if len(triplet) == 7:
			triplet[3] = datetime.datetime.strptime(triplet[3], '%c')
			triplet[4] = datetime.datetime.strptime(triplet[4], '%c')
			triplet[5] = int(triplet[5])
			triplet[6] = int(triplet[6])
			triplets.append(triplet)

	return triplets


def convert_step_to_days(step):
	"""Compute the days that are contained in a step, according to the configuration"""

	start = settings['step_size'] * step
	end = (settings['step_size'] * (step + 1)) - 1
	if step >= (settings['num_steps'] -1):
		return (start, '')
	else:
		return (start, end)


def print_fetch():
	"""Generates and prints the values as retrieved from greyfix."""

	triplets = greyfix_parse_triplets()
	now = datetime.datetime.now()
	steps = [0 for i in range(0, settings['num_steps'])]
	greylist_step = 0

	for triplet in triplets:
		if settings['greylist_step'] == 'yes' and triplet[6] == 0:
			greylist_step = greylist_step +1
			continue;

		delta = now - triplet[3]
		step = delta.days / settings['step_size']
		step = min(step, settings['num_steps'] -1)
		# count the number of triplets in a group
		steps[ step ] = steps[ step ] +1

	# print result counts for each group
	if settings['greylist_step'] == 'yes':
		print 'gl.value %i' % greylist_step
	for step, count in enumerate(steps):
		fieldname = 'd%s_%s' % convert_step_to_days(step)
		print '%s.value %i' % (fieldname, count)


def print_config():
	"""Generates and prints a munin config for a given chart."""

	print 'graph_title Greyfix triplets by age'
	print 'graph_vlabel Number of triplets'
	print 'graph_info The amount of triplets in the greyfix database with a certain age'
	print 'graph_category mail'
	print 'graph_total All triplets'
	print 'graph_args --lower-limit 0 --base 1000'

	if settings['greylist_step'] == 'yes':
		print 'gl.label Greylisted'
		print 'gl.info The number of greylisted triplets. These did not have a single pass (yet)'
		print 'gl.draw AREASTACK'
		print 'gl.colour aaaaaa'

	steps = range(0, settings['num_steps'])
	for step in steps:
		days = convert_step_to_days(step)

		fieldname = 'd%s_%s' % days
		label = 'Whitelisted for %s - %s days' % (days[0], days[1] if days[1] != '' else '...')
		info = 'The number of whitelisted triplets that is between %s and %s days old' % (days[0], days[1] if days[1] != '' else '...')

		print '%s.label %s' % (fieldname, label)
		print '%s.info %s' % (fieldname, info)
		print '%s.draw AREASTACK' % fieldname


if __name__ == '__main__':

	# read settings from config file / environment
	for key in settings.iterkeys():
		if key in os.environ:
			if os.environ[ key ].isdigit():
				settings[ key ] = int(os.environ[ key ])
			else:
				settings[ key ] = os.environ[ key ]
			#print '# setting %s updated to: %s' % (key, settings[ key ])

	commands = ['fetch', 'autoconf', 'config']
	if len(sys.argv) > 1:
		command = sys.argv[1]
	else:
		command = commands[0]

	if command not in commands:
		print '# command %s unknown, choose one of: %s' % (command, commands)
		sys.exit(1)

	if command == 'fetch':
		print_fetch()
	elif command == 'config':
		print_config()
	elif command == 'autoconf':
		if os.path.exists( settings['greyfix'] ):
			print 'yes'
		else:
			print 'no (binary not found at %s)' % settings['greyfix']
