#!/usr/bin/python
#
# Copyright (C) 2014 Red Hat.
#
# 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
# Iostat Software Foundation; either version 2 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.
#
# pylint: disable=C0103,R0914,R0902
""" Display disk and device-mapper I/O statistics """

import sys
from pcp import pmapi, pmcc
from cpmapi import PM_TYPE_U64, PM_CONTEXT_ARCHIVE, PM_SPACE_KBYTE

IOSTAT_SD_METRICS = [ "disk.dev.read", "disk.dev.read_bytes",
	         "disk.dev.write",  "disk.dev.write_bytes",
		 "disk.dev.read_merge", "disk.dev.write_merge",
		 "disk.dev.blkread", "disk.dev.blkwrite",
		 "disk.dev.read_rawactive", "disk.dev.write_rawactive",
		 "disk.dev.avactive"]

IOSTAT_DM_METRICS = [ "disk.dm.read", "disk.dm.read_bytes",
	         "disk.dm.write",  "disk.dm.write_bytes",
		 "disk.dm.read_merge", "disk.dm.write_merge",
		 "disk.dm.blkread", "disk.dm.blkwrite",
		 "disk.dm.read_rawactive", "disk.dm.write_rawactive",
		 "disk.dm.avactive"]

class IostatReport(pmcc.MetricGroupPrinter):
    Hcount = 0
    def timeStampDelta(self, group):
	c = 1000000.0 * group.timestamp.tv_sec + group.timestamp.tv_usec
	p = 1000000.0 * group.prevTimestamp.tv_sec + group.prevTimestamp.tv_usec
	return (c - p) / 1000000.0

    def instlist(self, group, name):
	return dict(map(lambda x: (x[1], x[2]), group[name].netValues)).keys()

    def curVals(self, group, name):
	return dict(map(lambda x: (x[1], x[2]), group[name].netValues))

    def prevVals(self, group, name):
	return dict(map(lambda x: (x[1], x[2]), group[name].netPrevValues))

    def report(self, manager):
	if "dm" in IostatOptions.xflag:
	    subtree = "disk.dm"
	else:
	    subtree = "disk.dev"
	group = manager["iostat"]

	if group[subtree + ".read_merge"].netPrevValues == None:
	    # need two fetches to report rate converted counter metrics
	    return

	instlist = self.instlist(group, subtree + ".read")
	dt = self.timeStampDelta(group)
	timestamp = group.contextCache.pmCtime(long(group.timestamp)).rstrip()

	c_rrqm = self.curVals(group, subtree + ".read_merge")
	p_rrqm = self.prevVals(group, subtree + ".read_merge")

	c_wrqm = self.curVals(group, subtree + ".write_merge")
	p_wrqm = self.prevVals(group, subtree + ".write_merge")

	c_r = self.curVals(group, subtree + ".read")
	p_r = self.prevVals(group, subtree + ".read")

	c_w = self.curVals(group, subtree + ".write")
	p_w = self.prevVals(group, subtree + ".write")

	c_rkb = self.curVals(group, subtree + ".read_bytes")
	p_rkb = self.prevVals(group, subtree + ".read_bytes")

	c_wkb = self.curVals(group, subtree + ".write_bytes")
	p_wkb = self.prevVals(group, subtree + ".write_bytes")

	c_ractive = self.curVals(group, subtree + ".read_rawactive")
	p_ractive = self.prevVals(group, subtree + ".read_rawactive")

	c_wactive = self.curVals(group, subtree + ".write_rawactive")
	p_wactive = self.prevVals(group, subtree + ".write_rawactive")

	c_avactive = self.curVals(group, subtree + ".avactive")
	p_avactive = self.prevVals(group, subtree + ".avactive")

	# check availability
	if p_rrqm == {} or p_wrqm == {} or p_r == {} or p_w == {} or p_rkb == {} \
	   or p_wkb == {} or p_ractive == {} or p_wactive == {} or p_avactive == {}:
	    # no values (near start of archive?)
	    return

	if "h" not in IostatOptions.xflag:
	    self.Hcount += 1
	    if self.Hcount == 24:
		self.Hcount = 1
	    if self.Hcount == 1:
		if "t" in IostatOptions.xflag:
		    heading = ('# Timestamp', 'Device', 'rrqm/s', 'wrqm/s', 'r/s', 'w/s', 'rkB/s', 'wkB/s',
			       'avgrq-sz', 'avgqu-sz', 'await', 'r_await', 'w_await', '%util')
		    print "%-24s %-12s %7s %7s %6s %6s %8s %8s %8s %8s %7s %7s %7s %5s" % heading
		else:
		    heading = ('# Device', 'rrqm/s', 'wrqm/s', 'r/s', 'w/s', 'rkB/s', 'wkB/s',
			       'avgrq-sz', 'avgqu-sz', 'await', 'r_await', 'w_await', '%util')
		    print "%-12s %7s %7s %6s %6s %8s %8s %8s %8s %7s %7s %7s %5s" % heading

	for inst in instlist:
	    # basic stats
	    rrqm = (c_rrqm[inst] - p_rrqm[inst]) / dt
	    wrqm = (c_wrqm[inst] - p_wrqm[inst]) / dt
	    r = (c_r[inst] - p_r[inst]) / dt
	    w = (c_w[inst] - p_w[inst]) / dt
	    rkb = (c_rkb[inst] - p_rkb[inst]) / dt
	    wkb = (c_wkb[inst] - p_wkb[inst]) / dt

	    # totals
	    tot_rios = (float)(c_r[inst] - p_r[inst])
	    tot_wios = (float)(c_w[inst] - p_w[inst])
	    tot_ios = (float)(tot_rios + tot_wios)

	    # total active time in seconds (same units as dt)
	    tot_active = (float)(c_avactive[inst] - p_avactive[inst]) / 1000.0

	    avgrqsz = avgqsz = await = r_await = w_await = util = 0.0

	    # average request size units are KB (sysstat reports in units of sectors)
	    if tot_ios:
		avgrqsz = (float)((c_rkb[inst] - p_rkb[inst]) + (c_wkb[inst] - p_wkb[inst])) / tot_ios

	    # average queue length
	    avgqsz = (float)((c_ractive[inst] - p_ractive[inst]) + (c_wactive[inst] - p_wactive[inst])) / dt / 1000.0

	    # await, r_await, w_await
	    if tot_ios:
		await = ((c_ractive[inst] - p_ractive[inst]) + (c_wactive[inst] - p_wactive[inst])) / tot_ios

	    if tot_rios:
		r_await = (c_ractive[inst] - p_ractive[inst]) / tot_rios

	    if tot_wios:
		w_await = (c_wactive[inst] - p_wactive[inst]) / tot_wios

	    # device utilization (percentage of active time / interval)
	    if tot_active:
	    	util = 100.0 * tot_active / dt

	    if "t" in IostatOptions.xflag:
		print "%-24s %-12s %7.1f %7.1f %6.1f %6.1f %8.1f %8.1f %8.2f %8.2f %7.1f %7.1f %7.1f %5.1f" \
		% (timestamp, inst, rrqm, wrqm, r, w, rkb, wkb, avgrqsz, avgqsz, await, r_await, w_await, util)
	    else:
		print "%-12s %7.1f %7.1f %6.1f %6.1f %8.1f %8.1f %8.2f %8.2f %7.1f %7.1f %7.1f %5.1f" \
		% (inst, rrqm, wrqm, r, w, rkb, wkb, avgrqsz, avgqsz, await, r_await, w_await, util)


class IostatOptions(pmapi.pmOptions):
    # class attributes
    xflag = [] 

    def extraOptions(self, opt, optarg, index):
	if opt == "x":
	    IostatOptions.xflag = optarg.replace(',', ' ').split(' ')
	else:
	    print "Warning: option '", opt, "' not recognised"

    def __init__(self):
	pmapi.pmOptions.__init__(self, "A:a:D:h:O:S:s:T:t:VZ:z?x:")
	self.pmSetLongOptionHeader("General options:")
	self.pmSetLongOptionAlign()
	self.pmSetLongOptionArchive()
	self.pmSetLongOptionDebug()
	self.pmSetLongOptionHost()
	self.pmSetLongOptionOrigin()
	self.pmSetLongOptionStart()
	self.pmSetLongOptionSamples()
	self.pmSetLongOptionFinish()
	self.pmSetLongOptionInterval()
	self.pmSetLongOptionVersion()
	self.pmSetLongOptionTimeZone()
	self.pmSetLongOptionHostZone()
	self.pmSetLongOptionHelp()
	self.pmSetOptionCallback(self.extraOptions)
	self.pmSetLongOptionText("  -x comma separated extended options: [dm][,t][,h]")
	self.pmSetLongOptionText("     dm show device-mapper statistics (default is sd devices)")
	self.pmSetLongOptionText("     t precede every line with a timestamp in ctime format");
	self.pmSetLongOptionText("     h suppress headings");

if __name__ == '__main__':
    try:
	manager = pmcc.MetricGroupManager.builder(IostatOptions(), sys.argv)
	if "dm" in IostatOptions.xflag :
	    manager["iostat"] = IOSTAT_DM_METRICS
	else:
	    manager["iostat"] = IOSTAT_SD_METRICS
	manager.printer = IostatReport()
        sts = manager.run()
        sys.exit(sts)
    except pmapi.pmErr, error:
	print '%s: %s\n' % (error.progname(), error.message())
    except pmapi.pmUsageErr, usage:
	print usage.message()
        sys.exit(1)
    except KeyboardInterrupt:
	pass
