#!/usr/bin/python

"""
: << =cut

=head1 NAME

ceph osd stats by BTG

=head1 NOTES

I have no idea what I'm doing here

Usage:

$0 config
	show graphs that should be generated

$0
	show the data for the graphs

Place this in /etc/munin/plugins/ and munin should find it.

You may need to add "timeout 240" to /etc/munin/munin-node.conf and restart

=head1 AUTHOR

Jort Bloem

=head1 EXCUSES

This is one of the first programs I wrote in Python. I got carried away
by Python's powerful one-line commands. Just because you can, doesn't
mean you should. 

This program needs a rewrite, and if there were any problems with it,
I probably would.

=head1 LICENSE

Copyright (C) 2013 Business Technology Group (btg.co.nz)

Permission is granted to copy, distribute and/or modify this document
under the terms of the GNU Free Documentation License, Version 1.3
or any later version published by the Free Software Foundation;
with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts.
A copy of the license is included in the section entitled "GNU
Free Documentation License".

If you wish to use this code on any other terms, please contact us
with your needs, we'll do what we can. This may not include payment.
We don't bite.

=head1 MAGIC MARKERS

 #%# capabilities=config

=cut
"""

import socket,os,json,sys,re,glob,itertools

# this overides config values for specific graphs:
# "graphname.osd1":{"graph_title":"This is the graph for OSD 1!"}
# "graphname.osd*":{"graph_title":"This is one of the 'dig deeper' graphs!"}
# "graphname":{"graph_title":"This is my great graph!"}
#
# "graphname" overrides defaults, both for graphname and graphname.osd*
# "graphname.osd*" overrides graphname and defaults (for the sub-graphs), but cannot have the OSD number in it.
# "graphname.osd1" (or .osd2, or .osd3...) is the final setting for osd1 - may be needed if you want the number of the osd in it.
# if "graphname.osd*" and "graphname.osd1" are not used, all the settings will be the same as the parent graph,
#  except the title will have " - OSD 1" (or whichever osd it is) appended.
#
# Alternatively, "graph":None will hide a graph.

settings_graph={
#	"osd_opq":{"graph_title":"test"},
}


### BUG REPORT TODO README!
# If subgraph is true, then graphs for osd* are created individually.
# This causes the master (client) munin to take so long to update rrds that timeouts happen.
# Solutions:
# 1: set subgraph to false.
# 2: increase the timeout, wherever that is
# 3: return config, but no data, for individual graphs, so rrds aren't updated, then link individual rrds to their parent rrd.
# 4: change munin so which rrd file is used can be overridden
#
# option 1: add "timeout 240" to /etc/munin/munin-node.conf and restart

subgraphs=True

def read_osd(filename):
	for loop in range(10):
		try:
			s=socket.socket(socket.AF_UNIX,socket.SOCK_STREAM)
			s.connect(filename)
			s.send("{\"prefix\": \"perf dump\"}\0")
			result=s.recv(102400)
			result=result[4:]
			return json.loads(result)
		except:
			pass
		
	return None

def osd_list():
	result={}
	for osd in glob.glob("/var/run/ceph/ceph-osd.*.asok"):
		data=read_osd(osd)
		if data:
			result[osd.split(".")[1]]=data
	return result

def collapse_one(inputdict,newkeyformat="%s_%s"):
	"""map inputdict["a"]["b"]=val to outdict["a_b"]=val"""
	outdict={}
	for outer in inputdict.items():
		#print "outer => %s %s" % outer
		for inner in outer[1].items():
			outdict[newkeyformat % (outer[0],inner[0])]=inner[1]
	return outdict

def sortlist(listtosort):
	listtosort=list(listtosort)
	listtosort.sort()
	return listtosort

# get and tidy osd_list, get derived keys.
data=osd_list()
osds=[key for key in data.keys() if data[key]!=None]
osds.sort()
for key in osds:
	data[key]=collapse_one(data[key])

graphlist=[item[1].keys() for item in data.items()]+settings_graph.keys()
graphlist=list(set(itertools.chain(*graphlist)))

if (sys.argv.__len__()>1) and (sys.argv[1]=="config"):
	for graph in graphlist:
		if (graph not in settings_graph):
			graphsettings={}
		elif settings_graph[graph]==None:
			continue
		else:
			graphsettings=settings_graph[graph]
		gr_simple=graph.replace("-","_").replace(":","_")
		gr_pretty=graph.replace("_"," ").title()
		gr=graph.replace("-","_").replace(":","_")
		graphdefaults={"graph_title":gr_pretty,"graph_vlabel":gr_pretty,"graph_category":"osd"}
		graphsettings=dict(graphdefaults.items()+graphsettings.items())
		print "multigraph %s" % (gr_simple)
		print "\n".join(["%s %s" % setting for setting in graphsettings.items()])
		for osd in sortlist(data.keys()):
			print "osd%s_%s.label osd %s" % (osd,gr_simple,osd)
		if subgraphs:
			for osd in sortlist(data.keys()):
				print "multigraph %s.osd%s" % (gr_simple,osd)
				thisrecord=dict(graphsettings.items()+[("graph_title","%s - OSD %s" % (graphsettings["graph_title"],osd),)])
				#print thisrecord
				if ("%s.osd*" % (graph) in settings_graph):
					thisrecord=dict(thisrecord.items()+settings_graph["%s.osd%s" % (graph,osd)].items())
				if ("%s.osd%s" % (graph,osd) in settings_graph):
					thisrecord=dict(thisrecord.items()+settings_graph["%s.osd%s" % (graph,osd)].items())
				print "\n".join(["%s %s" % setting for setting in thisrecord.items()])
				print "osd%s_%s.label osd %s" % (osd,gr_simple,osd)
else:
	for graph in graphlist:
		gr=graph.replace("-","_").replace(":","_")
		print "multigraph %s" % gr
		for osd in osds:
			if type(data[osd][graph])==dict:
				if data[osd][graph]["avgcount"]==0:
					data[osd][graph]="NaN"
				else:
					data[osd][graph]=float(data[osd][graph]["sum"])/float(data[osd][graph]["avgcount"])
		for osd in osds:
			value=data[osd][graph]
			print "osd%s_%s.value %s" % (osd,gr,data[osd][graph])
		if subgraphs:
			for osd in osds:
				print "multigraph %s.osd%s" % (gr,osd)
				print "osd%s_%s.value %s" % (osd,gr,data[osd][graph])

