munin-contrib/plugins/disk/scsi_queue

247 lines
8.2 KiB
Python
Executable File

#!/usr/bin/env python
"""
Munin plugin which reports queue busy-values per online SCSI
device on Linux, as seen in /proc/scsi/sg/devices
If the busy-values often reach the queue depth of the device,
one might consider increasing the queue depth. Hence, this
plugin.
Wildcard use:
If your system has many SCSI-like devices, filtering may be needed
to make the resulting graphs readable.
If you symlink the plugin, so that it's executed as
scsi_queue_X_through_Y
then the plugin will only look at devices
/dev/sdX .. /dev/sdY
X and Y may only be one-character values.
X and Y are translated into a regular expression like:
sd[X-Y]
"""
# Author: Troels Arvin <tra@sst.dk>
# See http://troels.arvin.dk/code/munin/ for latest version.
# Only tested with Red Hat Enterprise Linux 5 / CentOS 5, currently.
# Released according to the "New BSD License" AKA the 3-clause
# BSD License:
# ====================================================================
# Copyright (c) 2010, Danish National Board of Health.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the the Danish National Board of Health nor the
# names of its contributors may be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY the Danish National Board of Health ''AS IS'' AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL the Danish National Board of Health BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
# ====================================================================
# $Id: scsi_queue 13630 2010-08-31 15:29:14Z tra $
# Note to self:
# The fields in /proc/scsi/sg/devices are:
# host chan id lun type opens qdepth busy online
# TODO:
# - Make it possible to group by multipath group. Might be
# hard, though, because determining path groups seems
# to require root privileges.
# - Support autoconf
# - How to support filtering on installations which have
# many SCSI devices, beyond /dev/sdz?
import os, sys, re
procfile = '/proc/scsi/sg/devices'
sysfs_base = '/sys/bus/scsi/devices'
my_canonical_name = 'scsi_queue' # If called as - e.g. - scsi_queue_foo, then
# foo will be interpreted as a device filter.
# For this, we need a base name.
def bailout(msg):
sys.stderr.write(msg+"\n")
sys.exit(1)
def print_config(devices,filter_from,filter_through):
title_qualification = ''
if filter_from and filter_through:
title_qualification = ' for devices sd%s through sd%s' % (filter_from,filter_through)
print 'graph_title SCSI queue busy values' + title_qualification
print 'graph_vlabel busy count'
print 'graph_args --base 1000 -l 0'
print 'graph_category disk'
print 'graph_info This graph shows the queue busy values, as seen in /prod/scsi/sg/devices'
keys = devices.keys()
keys.sort()
for key in keys:
qdepth = devices[key]['qdepth']
print '%s.min 0' % key
print '%s.type GAUGE' % key
print '%s.label %s (%s %s); qdepth=%s' % (
key,
key,
devices[key]['vendor'],
devices[key]['model'],
qdepth
)
print '%s.max %s' % (key,qdepth)
# Return a list of lists representing interesting parts from procfile
def parse_procfile():
retval = []
try:
fh = open(procfile)
for line in fh:
retval.append(line.split())
except IOError, e:
bailout('IO error: '+str(e))
return retval
# Try to read a file's content. If any I/O problem: return empty string
def readfile(path):
try:
f = open(path)
retval = f.read().rstrip()
f.close()
except IOError, e:
return ''
return retval
# Return dict of dicts, indexed by device name
def map_procentries_to_devices(list_of_dicts,devfilter_regex):
device_dict={}
if devfilter_regex:
regex_compiled = re.compile(devfilter_regex)
for elem in list_of_dicts:
# In /sys/bus/scsi/devices we see a number of directory
# entries, such as:
# 0:0:0:0
# 2:0:0:0
# 3:0:0:0
#
# The colon-separated values map to the first four parts
# of /proc/scsi/sg/devices
# And the directory entries are symlinks which point to directories
# in /sys/devices. By following a symlink, we may end up in
# a directory which contains directory entries like:
# - block:sdb
# ...
# - model
# ...
# - vendor
sys_pathname = sysfs_base + '/' + ':'.join(elem[:4]) # isolate stuff like 2:0:0:0
# Should actually not happen, but nonetheless:
if not os.path.islink(sys_pathname):
continue
# Search for dirent called block:SOMETHING
# Put SOMETHING into blockdev_name
# Couldn't make glob.glob() work: The length of the result
# of glob() returned TypeError: len() of unsized object on
# RHEL 5's python...
dirents = os.listdir(sys_pathname)
num_blocklines=0
for dirent in dirents:
if dirent.startswith('block:'):
block_line = dirent
num_blocklines += 1
if num_blocklines == 0:
continue
if num_blocklines > 1:
bailout("Got more than one result when globbing for '%s'" % glob_for)
blockdev_name = block_line.split(':')[1]
# If device filtering is active, filter now
if devfilter_regex:
if not regex_compiled.match(blockdev_name):
continue
# Merge info from the /proc and /sys sources
device_dict[blockdev_name] = {
'model' : readfile(sys_pathname+'/model'),
'vendor': readfile(sys_pathname+'/vendor'),
'qdepth': elem[6],
'busy' : elem[7]
}
return device_dict
def print_values(devices):
devnames = devices.keys()
devnames.sort()
retval = ''
for devname in devnames:
print "%s.value %s" % (
devname,
devices[devname]['busy']
)
# Initial sanity check
n_args=len(sys.argv)
if n_args > 2:
# At most one arg expected
print '%d arguments given - expecting only one' % n_args
sys.exit(1)
# See if we were called with a Munin wildcard-style 'arg0-argument'
# E.g., if called as scsi_queue_a_through_c, then consider only
# devices sda, sdb, sdc.
devfilter_regex = None
called_as = os.path.basename(sys.argv[0])
match = re.match(my_canonical_name+'_([^_])_through_([^_])', called_as)
filter_from = None
filter_through = None
if match:
filter_from = match.group(1)
filter_through = match.group(2)
devfilter_regex = 'sd['+filter_from+'-'+filter_through+']'
# Perform main piece of work
devices = map_procentries_to_devices(
parse_procfile(),
devfilter_regex
)
# See how we were called
if n_args == 2:
# An argument was given, so let's not simply print
# values.
arg = sys.argv[1]
if arg == 'config':
print_config(devices,filter_from,filter_through)
sys.exit(0)
else:
print "Unknown argument '%s'" % arg
sys.exit(1)
# No arguments given; print values
print_values(devices)