#!/usr/bin/python
# -*- encoding: utf-8; py-indent-offset: 4 -*-
# Check_MK mtr Plugin

# Concept:
# Read config mtr.cfg
# For every host:
# parse outstanding reports (and delete them)
# If current time > last check + config(time)//300 start new mtr in background
#	 MTR results are stored in $VARDIR/mtr_${host}.report
# return previous host data

import sys, os, re, time, glob, ConfigParser, StringIO, distutils.spawn
from unicodedata import normalize
import subprocess

# The configuration file and status file are searched in the directory named by the environment variable MTR_DIR. 
# If that is not set, MK_CONFDIR is used (should be set through check_mk_agent).
# If that is not set either, the current directory is used.

mtr_dir = os.getenv("MTR_DIR")
if mtr_dir:
	mk_confdir = mtr_dir
	mk_vardir = mtr_dir
else:
	mk_confdir = os.getenv("MK_CONFDIR") or "."
	mk_vardir = os.getenv("MK_VARDIR") or "."

config_filename = mk_confdir + "/mtr.cfg"
config_dir      = mk_confdir + "/mtr.d/*.cfg"
status_filename = mk_vardir + "/mtr.state"
report_filepre  = mk_vardir + "/mtr.report."

if '-d' in sys.argv[2:] or '--debug' in sys.argv[1:]:
	debug = True
else:
	debug = False

# See if we have mtr
mtr_prog = distutils.spawn.find_executable('mtr')
if mtr_prog == None:
	if debug:
		print "Could not find mtr binary"
	sys.exit(0)

def read_config():
	default_options = {
		'type' : 'icmp',
		'count': "10",
		'force_ipv4': "0",
		'force_ipv6': "0",
		'size': "64",
		'time': "0",
		'dns': "0",
		'port': None,
		'address': None,
		'interval': None,
		'timeout': None
	}
	if not os.path.exists(config_filename):
		if debug:
			print "Not configured, %s missing" % config_filename
		sys.exit(0)
	config = ConfigParser.SafeConfigParser(default_options)
	# Let ConfigParser figure it out
	for config_file in ( [ config_filename ] + glob.glob(config_dir)):
		try:
			if not config.read(config_file):
				print "**ERROR** Failed to parse configuration file %s!" % config_file;
		except Exception as e:
				print "**ERROR** Failed to parse config file %s: %s " % (config_file, repr(e))

	if len(config.sections()) == 0:
		print "**ERROR** Configuration defines no hosts!"
		sys.exit(0)

	return config

# structure of statusfile
# # HOST        |LASTTIME |HOPCOUNT|HOP1|Loss%|Snt|Last|Avg|Best|Wrst|StDev|HOP2|...|HOP8|...|StdDev
# www.google.com|145122481|8|192.168.1.1|0.0%|10|32.6|3.6|0.3|32.6|10.2|192.168.0.1|...|9.8
def read_status():
	status = {}
	if not os.path.exists(status_filename):
		return status

	for line in file(status_filename):
		try:
			parts = line.split('|')
			if len(parts) < 2:
				print "**ERROR** (BUG) Status has less than 2 parts: "
				print parts
				continue
			host = parts[0]
			lasttime = int(float(parts[1]))
			status[host] = {'hops': {}, 'lasttime': lasttime};
			hops = int(parts[2])
			for i in range(0, hops):
				status[host]["hops"][i+1] = {
					'hopname': parts[i*8 + 3].rstrip(),
					'loss'   : parts[i*8 + 4].rstrip(),
					'snt'    : parts[i*8 + 5].rstrip(),
					'last'   : parts[i*8 + 6].rstrip(),
					'avg'    : parts[i*8 + 7].rstrip(),
					'best'   : parts[i*8 + 8].rstrip(),
					'wrst'   : parts[i*8 + 9].rstrip(),
					'stddev' : parts[i*8 + 10].rstrip(),
				}
		except Exception as e:
			print "*ERROR** (BUG) Could not parse status line: %s, reason: %s" % (line, repr(e))
	return status

def save_status(status):
	# print "Saving status..."
	f = file(status_filename, "w")
	for host, hostdict in status.items():
		# print "Host / hostdict: %s / %s" % (host, hostdict)
		hopnum = len(hostdict["hops"].keys())
		lastreport = hostdict["lasttime"]
		hoststring = "%s|%s|%s" % (host, lastreport, hopnum)
		for hop in hostdict["hops"].keys():
			hi = hostdict["hops"][hop]
			hoststring += '|%s|%s|%s|%s|%s|%s|%s|%s' % (hi['hopname'], hi['loss'], hi['snt'], hi['last'], hi['avg'], hi['best'], hi['wrst'], hi['stddev'])
		hoststring = hoststring.rstrip()
		#print "Writing string to status: "
		#print hoststring
		#print "END Writing string to status: "
		f.write("%s\n" % hoststring)

_punct_re = re.compile(r'[\t !"#$%&\'()*\-/<=>?@\[\\\]^_`{|},.:]+')
def host_to_filename(host, delim=u'-'):
	# Get rid of gibberish chars, stolen from Django
	"""Generates an slightly worse ASCII-only slug."""
	host=unicode(host, 'UTF-8')
	result = []
	for word in _punct_re.split(host.lower()):
		word = normalize('NFKD', word).encode('ascii', 'ignore')
		if word:
			result.append(word)
	return unicode(delim.join(result))

def check_pid(pid):
	""" Check For the existence of a unix pid. """
	try:
		os.kill(pid, 0)
	except OSError:
		return False
	else:
		return True

def parse_report(host):
	reportfile = report_filepre + host_to_filename(host)
	if not os.path.exists(reportfile):
		if not host in status.keys():
			# New host
			status[host] = {'hops':{}, 'lasttime': 0}
		return
# 1451228358
# Start: Sun Dec 27 14:35:18 2015
#HOST: purple         Loss%   Snt   Last   Avg  Best  Wrst StDev
#  1.|-- 80.69.76.120    0.0%    10    0.3   0.4   0.3   0.6   0.0
#  2.|-- 80.249.209.100  0.0%    10    1.0   1.1   0.8   1.4   0.0
#  3.|-- 209.85.240.63   0.0%    10    1.3   1.7   1.1   3.6   0.5
#  4.|-- 209.85.253.242  0.0%    10    1.6   1.8   1.6   2.1   0.0
#  5.|-- 209.85.253.201  0.0%    10    4.8   5.0   4.8   5.4   0.0
#  6.|-- 216.239.56.6    0.0%    10    4.7   5.1   4.7   5.5   0.0
#  7.|-- ???            100.0    10    0.0   0.0   0.0   0.0   0.0
#  8.|-- 74.125.136.147  0.0%    10    4.5   4.6   4.3   5.2   0.0
	# See if pidfile exists and if mtr is still running
	if os.path.exists(reportfile + ".pid"):
		# Jup, see if it's running
		pid = int(file(reportfile + ".pid", 'r').readline().rstrip())
		if check_pid(pid):
			# Still running, we're done.
			if not host in status.keys():
				# New host
				status[host] = {'hops':{}, 'lasttime': 0}
			status[host]['running'] = True
			return;
		# Done running, get rid of pid file
		os.unlink(reportfile + ".pid")
	# Parse the existing report
	lines = file(reportfile).readlines()
	if len(lines) < 3:
		print "**ERROR** Report file %s has less than 3 lines, expecting at least 1 hop! Throwing away invalid report" % reportfile
		os.unlink(reportfile)
		if not host in status.keys():
			# New host
			status[host] = {'hops':{}, 'lasttime': 0}
		return;
	status[host] = {'hops':{}, 'lasttime': 0}

	hopcount = 0
	status[host]["lasttime"] = int(float(lines.pop(0)))
	while len(lines) > 0 and not lines[0].startswith("HOST:"):
		lines.pop(0)
	if len(lines) < 2: # Not enough lines
		return;
	lines.pop(0) # Get rid of HOST: header
	hopline = re.compile('^\s*\d+\.') #  10.|-- 129.250.2.147   0.0%    10  325.6 315.5 310.3 325.6   5.0
	for line in lines:
		if not hopline.match(line):
			continue; #     |  `|-- 129.250.2.159
		hopcount += 1
		parts = line.split()
		if len(parts) < 8:
			print "**ERROR** Bug parsing host/hop, line has less than 8 parts: %s" % line
			continue;
		status[host]['hops'][hopcount] = {
			'hopname': parts[1],
			'loss'   : parts[2],
			'snt'    : parts[3],
			'last'   : parts[4],
			'avg'    : parts[5],
			'best'   : parts[6],
			'wrst'   : parts[7],
			'stddev' : parts[8],
		}
	os.unlink(reportfile)

def output_report(host):
	hostdict = status[host]
	hopnum = len(hostdict["hops"].keys())
	lastreport = hostdict["lasttime"]
	hoststring = "%s|%s|%s" % (host, lastreport, hopnum)
	for hop in hostdict["hops"].keys():
		hi = hostdict["hops"][hop]
		hoststring += '|%s|%s|%s|%s|%s|%s|%s|%s' % (hi['hopname'], hi['loss'], hi['snt'], hi['last'], hi['avg'], hi['best'], hi['wrst'], hi['stddev'])
	print "%s" % hoststring

def start_mtr(host):
	options = [mtr_prog, '--report', '--report-wide']
	pingtype = config.get(host, "type")
	count = config.getint(host, "count")
	ipv4 = config.getboolean(host, "force_ipv4")
	ipv6 = config.getboolean(host, "force_ipv6")
	size = config.getint(host, "size")
	lasttime = config.getint(host, "time")
	dns = config.getboolean(host, "dns")
	port = config.get(host, "port")
	address = config.get(host, "address")
	interval = config.get(host, "interval")
	timeout = config.get(host, "timeout")

	if "running" in status[host].keys():
		if debug:
			print "MTR for host still running, not restarting MTR!"
		return

	if time.time() - status[host]["lasttime"] < lasttime:
		if debug:
			print "%s - %s = %s is smaller than %s => mtr run not needed yet." % (time.time(), status[host]["lasttime"], time.time() - status[host]["lasttime"], lasttime)
		return

	pid = os.fork()
	if pid > 0:
		# parent process, return and keep running
		return
	os.setsid()

	if pingtype == 'tcp':
		options.append("--tcp")
	if pingtype == 'udp':
		options.append("--udp")
	if not port == None:
		options.append("--port")
		options.append(str(port))
	if ipv4 == True:
		options.append("-4")
	if ipv6 == True:
		options.append("-6")
	options.append("-s")
	options.append(str(size))
	options.append("-c")
	options.append(str(count))
	if dns == False:
		options.append("--no-dns")
	if not address == None:
		options.append("--address")
		options.append(str(address))
	if not interval == None:
		options.append("-i")
		options.append(str(interval))
	if not timeout == None:
		options.append("--timeout")
		options.append(str(timeout))

	options.append(str(host))
	if debug:
		print "Startin MTR: %s" % (" ".join(options))
	reportfile = report_filepre + host_to_filename(host)
	if (os.path.exists(reportfile)):
		os.unlink(reportfile)
	report=open(reportfile, 'a+')
	report.write(str(int(time.time())) + "\n")
	report.flush()
	process = subprocess.Popen(options, stdout=report, stderr=report)
	# Write pid to report.pid
	pidfile=open(reportfile + ".pid", 'w')
	pidfile.write("%d\n" % process.pid)
	pidfile.flush()
	pidfile.close()
	os._exit(os.EX_OK)

# Parse config
print "<<<mtr>>>"
config = read_config()
status = read_status()
for host in config.sections():
	# Parse outstanding report
	parse_report(host)
	# Output last known values
	output_report(host)
	# Start new if needed
	start_mtr(host)
save_status(status)
