#!/usr/bin/env pmpython # # Copyright (C) 2014-2016,2020 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 # Free 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=bad-whitespace,protected-access,redefined-outer-name # pylint: disable=too-many-nested-blocks,too-many-boolean-expressions """ Display tape I/O statistics """ import re import sys import signal from pcp import pmapi, pmcc from cpmapi import PM_CONTEXT_ARCHIVE, PM_MODE_FORW # use default SIGPIPE handler to avoid broken pipe exceptions signal.signal(signal.SIGPIPE, signal.SIG_DFL) # Tape: r/s w/s kB_read/s kB_wrtn/s %Rd %Wr %Oa Rs/s Ot/s # These are the metrics tapestat (sysstat) shows and we will do the same # for starters # [root@nkshirsa sysstat]# ls /sys/class/scsi_tape/st0/stats/ -l # total 0 # in_flight # io_ns # other_cnt # read_byte_cnt # read_cnt # read_ns # resid_cnt # write_byte_cnt # write_cnt # write_ns TAPESTAT_METRICS = [ 'tape.dev.in_flight', 'tape.dev.io_ns', 'tape.dev.other_cnt', 'tape.dev.read_byte_cnt', 'tape.dev.read_cnt', 'tape.dev.read_ns', 'tape.dev.resid_cnt', 'tape.dev.write_byte_cnt', 'tape.dev.write_cnt', 'tape.dev.write_ns'] def aggregate(method, aggr_value, value): if method == 'sum': aggr_value += value elif method == 'avg': aggr_value += value elif method == 'min': aggr_value = min(aggr_value, value) elif method == 'max': aggr_value = max(aggr_value, value) else: raise pmapi.pmUsageErr return aggr_value class TapestatReport(pmcc.MetricGroupPrinter): Hcount = 0 def timeStampDelta(self, group): s = group.timestamp.tv_sec - group.prevTimestamp.tv_sec u = group.timestamp.tv_usec - group.prevTimestamp.tv_usec # u may be negative here, calculation is still correct. return s + u / 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): regex = TapestatOptions.Rflag if regex == '': regex = '.*' aggr = TapestatOptions.Gflag if aggr and aggr not in ('sum', 'avg', 'min', 'max'): print("Error, -G aggregation method must be one of 'sum', 'avg', 'min' or 'max'") raise pmapi.pmUsageErr precision = TapestatOptions.Pflag if precision < 0 or precision > 10 : print("Precision value must be between 0 and 10") raise pmapi.pmUsageErr subtree = 'tape.dev' group = manager["tapestat"] if group[subtree + '.read_cnt'].netPrevValues is None: # need two fetches to report rate converted counter metrics #print "found none, returning" return instlist = self.instlist(group, subtree + '.read_cnt') dt = self.timeStampDelta(group) timestamp = group.contextCache.pmCtime(int(group.timestamp)).rstrip() c_r = self.curVals(group, subtree + '.read_cnt') p_r = self.prevVals(group, subtree + '.read_cnt') c_w = self.curVals(group, subtree + '.write_cnt') p_w = self.prevVals(group, subtree + '.write_cnt') c_rkb = self.curVals(group, subtree + '.read_byte_cnt') p_rkb = self.prevVals(group, subtree + '.read_byte_cnt') c_wkb = self.curVals(group, subtree + '.write_byte_cnt') p_wkb = self.prevVals(group, subtree + '.write_byte_cnt') # calculate the percentage waits c_rpw = self.curVals(group, subtree + '.read_ns') p_rpw = self.prevVals(group, subtree + '.read_ns') c_wpw = self.curVals(group, subtree + '.write_ns') p_wpw = self.prevVals(group, subtree + '.write_ns') c_opw = self.curVals(group, subtree + '.io_ns') p_opw = self.prevVals(group, subtree + '.io_ns') p_resid = self.prevVals(group, subtree + '.resid_cnt') c_resid = self.curVals(group, subtree + '.resid_cnt') p_other = self.prevVals(group, subtree + '.other_cnt') c_other = self.curVals(group, subtree + '.other_cnt') if precision == 1: utilspace=precision+5 awaitspace=precision+6 rrqmspace=precision+5 wrqmspace=precision+5 headfmtavgspace=precision+7 headfmtquspace=precision+7 elif precision == 0: utilspace=precision+5 awaitspace=precision+7 rrqmspace=precision+6 wrqmspace=precision+6 headfmtavgspace=precision+8 headfmtquspace=precision+8 else: utilspace=precision+5 awaitspace=precision+5 rrqmspace=precision+5 wrqmspace=precision+5 headfmtavgspace=precision+6 headfmtquspace=precision+6 tmpspace = precision+5 if precision == 0 : tmpspace = tmpspace +1 if "h" not in TapestatOptions.xflag: self.Hcount += 1 if self.Hcount == 24: self.Hcount = 1 if "t" in TapestatOptions.xflag: valfmt = "%-24s %-12s %*.*f %*.*f %*d %*d %*.*f %*.*f %*.*f %*.*f %*.*f" headfmt = "%-24s %-12s %*s %*s %*s %*s %*s %*s %*s %*s %*s" if self.Hcount == 1: print(headfmt % ('# Timestamp', 'Device', tmpspace - 1, 'r/s', tmpspace, 'w/s', tmpspace, 'kb_r/s', tmpspace, 'kb_w/s', tmpspace, 'r_pct', tmpspace, 'w_pct', tmpspace, 'o_pct', tmpspace, 'Rs/s', tmpspace, 'o_cnt')) else: valfmt = "%-12s %*.*f %*.*f %*d %*d %*.*f %*.*f %*.*f %*.*f %*.*f" headfmt = "%-12s %*s %*s %*s %*s %*s %*s %*s %*s %*s" if self.Hcount == 1: print(headfmt % ('# Device', tmpspace, 'r/s', tmpspace, 'w/s', tmpspace, 'kb_r/s', tmpspace, 'kb_w/s', tmpspace, 'r_pct', tmpspace, 'w_pct', tmpspace, 'o_pct', tmpspace, 'Rs/s', tmpspace, 'o_cnt')) if p_r == {} or p_w == {} or p_rkb == {} or p_wkb == {} or \ p_rpw == {} or p_wpw == {} or p_opw == {} or \ p_resid == {} or p_other == {}: # no values for some metric (e.g. near start of archive) if "t" in TapestatOptions.xflag: headfmt = "%-24s %-12s %*s %*s %*s %*s %*s %*s %*s %*s %*s" print(headfmt % (timestamp, 'NODATA', rrqmspace - 1, '?', wrqmspace, '?', precision + 5, '?', precision + 5, '?', precision + 5, '?', precision + 5, '?', headfmtavgspace - 1, '?', headfmtquspace - 1, '?', precision + 5, '?')) return try: if TapestatOptions.Gflag: aggr_r = aggr_w = aggr_rkb = aggr_wkb = aggr_actual_rpw = aggr_actual_wpw = 0.0 aggr_actual_opw = aggr_resid_cnt = aggr_o_cnt = 0.0 aggr_count = 0 for inst in sorted(instlist): # basic stats r = (c_r[inst] - p_r[inst]) / dt w = (c_w[inst] - p_w[inst]) / dt rkb = (c_rkb[inst] - p_rkb[inst]) / dt / 1000.0 # bytes to kb wkb = (c_wkb[inst] - p_wkb[inst]) / dt / 1000.0 #calculate and convert from nano seconds rpw = 100 * (c_rpw[inst] - p_rpw[inst])/ 10.0**9 / dt wpw = 100 * (c_wpw[inst] - p_wpw[inst]) / 10.0**9 / dt actual_rpw = rpw actual_wpw = wpw opw = 100 * (c_opw[inst] - p_opw[inst]) / 10.0**9 / dt actual_opw = opw resid_cnt = (c_resid[inst] - p_resid[inst]) / dt / 1000.0 #The number of I/Os, expressed as the number per second averaged #over the interval, that were included as "other" is o_cnt o_cnt = (c_other[inst] - p_other[inst]) / dt / 1000.0 device = inst # prepare name for printing badcounters = (r < 0 or w < 0 or rkb < 0 or wkb < 0 or rpw < 0 or wpw < 0 or opw < 0 or resid_cnt < 0 or o_cnt < 0) if "t" in TapestatOptions.xflag: if badcounters: print(headfmt % (timestamp, device, rrqmspace, '?', wrqmspace, '?', precision + 5, '?', precision + 4, '?', precision + 6, '?', precision + 6, '?', headfmtavgspace, '?', headfmtquspace, '?', precision+5, '?')) else: if TapestatOptions.Rflag and re.search(regex,device) is None: continue if TapestatOptions.Gflag: aggr_count += 1 if "noidle" in TapestatOptions.xflag: if rkb == 0 and wkb == 0 and r == 0 and w == 0 : continue if not TapestatOptions.Gflag: print(valfmt % (timestamp, device, tmpspace, precision, r, tmpspace, precision, w, tmpspace, rkb, tmpspace, wkb, tmpspace, precision, actual_rpw, tmpspace, precision, actual_wpw, tmpspace, precision, actual_opw, tmpspace, precision, resid_cnt, tmpspace, precision, o_cnt)) else: if badcounters: print(headfmt % (device, rrqmspace, '?', wrqmspace, '?', precision + 5, '?', precision + 4, '?', precision + 6, '?', precision + 6, '?', headfmtavgspace, '?', headfmtquspace, '?', precision + 5, '?', awaitspace, '?', awaitspace, '?', utilspace, '?')) else: if TapestatOptions.Rflag and re.search(regex,device) is None: continue if TapestatOptions.Gflag: aggr_count += 1 if "noidle" in TapestatOptions.xflag: if rkb == 0 and wkb == 0 and r == 0 and w == 0 : continue if not TapestatOptions.Gflag: print(valfmt % (device, tmpspace, precision, r, tmpspace, precision, w, tmpspace, rkb, tmpspace, wkb, tmpspace, precision, actual_rpw, tmpspace, precision, actual_wpw, tmpspace, precision, actual_opw, tmpspace, precision, resid_cnt, tmpspace, precision, o_cnt)) if TapestatOptions.Gflag and not badcounters: aggr_r = aggregate(aggr, aggr_r, r) aggr_w = aggregate(aggr, aggr_w, w) aggr_rkb = aggregate(aggr, aggr_rkb, rkb) aggr_wkb = aggregate(aggr, aggr_wkb, wkb) aggr_actual_rpw = aggregate(aggr, aggr_actual_rpw, actual_rpw) aggr_actual_wpw = aggregate(aggr, aggr_actual_wpw, actual_wpw) aggr_actual_opw = aggregate(aggr, aggr_actual_opw, actual_opw) aggr_resid_cnt = aggregate(aggr, aggr_resid_cnt, resid_cnt) aggr_o_cnt = aggregate(aggr, aggr_o_cnt, o_cnt) # end of loop if TapestatOptions.Gflag: if TapestatOptions.Gflag == 'avg' and aggr_count > 0: aggr_r /= aggr_count aggr_w /= aggr_count aggr_rkb /= aggr_count aggr_wkb /= aggr_count aggr_actual_rpw /= aggr_count aggr_actual_wpw /= aggr_count aggr_actual_opw /= aggr_count aggr_resid_cnt /= aggr_count aggr_o_cnt /= aggr_count # report aggregate values - the 'device' here is reported as the regex used for the aggregation device = '%s(%s)' % (aggr, regex) if "t" in TapestatOptions.xflag: print(valfmt % (timestamp, device, tmpspace, precision, aggr_r, tmpspace, precision, aggr_w, tmpspace, aggr_rkb, tmpspace, aggr_wkb, tmpspace, precision, aggr_actual_rpw, tmpspace, precision, aggr_actual_wpw, tmpspace, precision, aggr_actual_opw, tmpspace, precision, aggr_resid_cnt, tmpspace, precision, aggr_o_cnt)) else: print(valfmt % (device, tmpspace, precision, aggr_r, tmpspace, precision, aggr_w, tmpspace, aggr_rkb, tmpspace, aggr_wkb, tmpspace, precision, aggr_actual_rpw, tmpspace, precision, aggr_actual_wpw, tmpspace, precision, aggr_actual_opw, tmpspace, precision, aggr_resid_cnt, tmpspace, precision, aggr_o_cnt)) except KeyError: # instance missing from previous sample pass class TapestatOptions(pmapi.pmOptions): # class attributes xflag = [] uflag = None Pflag = 2 Rflag = "" Gflag = "" def checkOptions(self, manager): if TapestatOptions.uflag: if manager._options.pmGetOptionInterval(): print("Error: -t incompatible with -u") return False if manager.type != PM_CONTEXT_ARCHIVE: print("Error: -u can only be specified with -a archive") return False return True def extraOptions(self, opt, optarg, index): if opt == "x": TapestatOptions.xflag += optarg.replace(',', ' ').split(' ') elif opt == "u": TapestatOptions.uflag = True elif opt == "P": TapestatOptions.Pflag = int(optarg) elif opt == "R": TapestatOptions.Rflag = optarg elif opt == "G": TapestatOptions.Gflag = optarg # all this stuff needs changes but leaving it as it is for now, anyway we will first get # it working without any arguments def __init__(self): pmapi.pmOptions.__init__(self, "A:a:D:G:h:O:P:R:S:s:T:t:uVZ:z?x:") self.pmSetOptionCallback(self.extraOptions) self.pmSetLongOptionHeader("General options") self.pmSetLongOptionAlign() self.pmSetLongOptionArchive() self.pmSetLongOptionDebug() self.pmSetLongOption("aggregate", 1, "G", "method", "aggregate values for devices matching -R regex" + " using 'method' (sum, avg, min or max)") self.pmSetLongOptionHost() self.pmSetLongOptionOrigin() self.pmSetLongOption("precision", 1, "P", "N", "N digits after the decimal separator") self.pmSetLongOption("regex", 1, "R", "pattern", "only report for devices names matching pattern," " e.g. 'sd[a-zA-Z]+'. See also -G.") self.pmSetLongOptionStart() self.pmSetLongOptionSamples() self.pmSetLongOptionFinish() self.pmSetLongOptionInterval() self.pmSetLongOption("no-interpolation", 0, "u", "", "disable interpolation mode with archives") self.pmSetLongOptionVersion() self.pmSetLongOptionTimeZone() self.pmSetLongOptionHostZone() self.pmSetLongOptionHelp() self.pmSetLongOptionHeader("Extended options") self.pmSetLongOption("", 1, 'x', "LIST", "comma separated extended options: [[t],[h],[noidle]]") self.pmSetLongOptionText("\t\tt\tprecede every line with a timestamp in ctime format") self.pmSetLongOptionText("\t\th\tsuppress headings") self.pmSetLongOptionText("\t\tnoidle\tdo not display idle devices") if __name__ == '__main__': try: opts = TapestatOptions() manager = pmcc.MetricGroupManager.builder(opts, sys.argv) if not opts.checkOptions(manager): raise pmapi.pmUsageErr if TapestatOptions.uflag: # -u turns off interpolation manager.pmSetMode(PM_MODE_FORW, manager._options.pmGetOptionOrigin(), 0) missing = manager.checkMissingMetrics(TAPESTAT_METRICS) if missing is not None: sys.stderr.write('Error: not all required metrics are available\nMissing: %s\n' % (missing)) sys.exit(1) manager["tapestat"] = TAPESTAT_METRICS #print "calling TapestatReport()" manager.printer = TapestatReport() sts = manager.run() sys.exit(sts) except pmapi.pmErr as error: sys.stderr.write('%s: %s\n' % (error.progname(), error.message())) except pmapi.pmUsageErr as usage: usage.message() sys.exit(1) except KeyboardInterrupt: pass