#!perl

package main;

# check_diskio is a Nagios plugin to monitor the amount of disk
# I/O in sectors on Linux
#
# See  the INSTALL file for installation instructions
#
# Copyright (c) 2007-2015 Matteo Corti
#
# This module is free software; you can redistribute it and/or modify it
# under the terms of GNU general public license (gpl) version 3.
# See the LICENSE file for details.
#
# RCS information
# enable substitution with:
#   $ svn propset svn:keywords "Id Revision HeadURL Source Date"
#
#   $Id: check_diskio 1446 2015-06-03 06:35:17Z corti $
#   $Revision: 1446 $
#   $HeadURL: https://svn.id.ethz.ch/nagios_plugins/check_diskio/check_diskio $
#   $Date: 2015-06-03 08:35:17 +0200 (Wed, 03 Jun 2015) $

use 5.00800;

use strict;
use warnings;

our $VERSION = '3.2.8';

use Array::Unique;
use English qw(-no_match_vars);
use Fcntl;
use File::Slurp;
use List::MoreUtils qw(any);
use Number::Format qw(format_number);
use POSIX qw(uname);
use Readonly;

Readonly my $OVERFLOW => 2**32;

Readonly my $BITS_PER_BYTE    => 8;
Readonly my $BYTES_PER_SECTOR => 512;

Readonly my $KILO => 1_000;
Readonly my $KIBI => 1_024;

sub load_module {

    my @names = @_;
    my $loaded_module;

    for my $name (@names) {

        my $file = $name;

        # requires need either a bare word or a file name
        $file =~ s{::}{/}gsxm;
        $file .= '.pm';

        eval {    ## no critic (ErrorHandling::RequireCheckingReturnValueOfEval)
            require $file;
            $name->import();
        };
        if ( !$EVAL_ERROR ) {
            $loaded_module = $name;
            last;
        }
    }

    if ( !$loaded_module ) {
        #<<<
        print 'CHECK_DISKIO: plugin not found: ' . join( ', ', @names ) . "\n";  ## no critic (RequireCheckedSyscall)
        #>>>

        exit 2;
    }

    return $loaded_module;

}

my $plugin_module = load_module( 'Monitoring::Plugin', 'Nagios::Plugin' );
my $plugin_threshold_module =
  load_module( 'Monitoring::Plugin::Threshold', 'Nagios::Plugin::Threshold' );
my $plugin_getopt_module =
  load_module( 'Monitoring::Plugin::Getopt', 'Nagios::Plugin::Getopt' );

# IMPORTANT: Nagios plugins could be executed using embedded perl in this case
#            the main routine would be executed as a subroutine and all the
#            declared subroutines would therefore be inner subroutines
#            This will cause all the global lexical variables not to stay shared
#            in the subroutines!
#
# All variables are therefore declared as package variables...
#
## no critic (ProhibitPackageVars)
use vars qw(
  $factor
  $format
  $plugin
  $threshold
  $options
  %tmp_files
  %diskio
  %diskio_w
  %diskio_r
  @stat_file
);
## use critic (ProhibitPackageVars)

# the script is declared as a package so that it can be unit tested
# but it should not be used as a module
if ( !caller ) {
    run();
}

## no critic (InputOutput::RequireCheckedSyscalls)

##############################################################################
# subroutines

##############################################################################
# Usage     : verbose("some message string", $optional_verbosity_level);
# Purpose   : write a message if the verbosity level is high enough
# Returns   : n/a
# Arguments : message : message string
#             level   : options verbosity level
# Throws    : n/a
# Comments  : n/a
# See also  : n/a
sub verbose {

    # arguments
    my $message = shift;
    my $level   = shift;

    if ( !defined $message ) {
        $plugin->nagios_exit( $plugin->UNKNOWN,
            q{Internal error: not enough parameters for 'verbose'} );
    }

    if ( !defined $level ) {
        $level = 0;
    }

    if ( $options->debug() ) {
        print '[DEBUG] ';
    }

    if ( $level < $options->verbose() || $options->debug() ) {
        print $message;
    }

    return;

}

##############################################################################
# Usage     : @output = exec_command( $command );
# Purpose   : executes the command and returns the output
# Returns   : array with the standard ouput
# Arguments : $command : the command to execute
# Throws    : n/a
# Comments  : n/a
# See also  : n/a
sub exec_command {

    my ($command) = @_;

    my @result;
    my $output;

    verbose "Executing '$command'\n", 1;

    ## no critic (InputOutput::RequireBriefOpen)
    my $pid = open $output, q{-|},
      $command
      or $plugin->nagios_exit( $plugin->UNKNOWN,
        "Cannot execute $command: $OS_ERROR" );

    while (<$output>) {
        chomp;
        push @result, $_;
    }

    if (  !( close $output )
        && ( $OS_ERROR != 0 ) )
    {

        # close to a piped open return false if the command with non-zero
        # status. In this case $! is set to 0
        $plugin->nagios_exit( $plugin->UNKNOWN,
            "Error while closing pipe to whoami: $OS_ERROR" );

    }
    ## use critic (InputOutput::RequireBriefOpen)

    return @result;

}

##############################################################################
# Usage     : write_timer($tmp, $in, $out)
# Purpose   : writes the time and I/O data to the temporary file
# Returns   : n/a
# Arguments : $tmp    : temporary file name
#             $in     : input
#             $output : output
# Throws    : n/a
# Comments  : n/a
# See also  : n/a
sub write_timer {

    my $tmp  = shift;
    my $in   = shift;
    my $out  = shift;
    my $time = time;

    my $TMP_FH;

    verbose 'saving data:  read='
      . format_number( $in, 0, 0, )
      . ' write='
      . format_number( $out, 0, 0, )
      . ' timestamp='
      . $time
      . "\n", 1;

# use sysopen to avoid a race condition (http://en.wikipedia.org/wiki/Symlink_race)

    sysopen $TMP_FH, $tmp,
      O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW
      or $plugin->nagios_exit( $plugin->UNKNOWN,
        "Cannot initialize timer ($tmp): $OS_ERROR" );

    print {$TMP_FH} $time
      . " $in $out\n"
      or $plugin->nagios_exit( $plugin->UNKNOWN,
        "Cannot write timer ($tmp): $OS_ERROR" );

    if ( $options->debug() ) {
        print "[DEBUG] writing $tmp: |$time $in $out|\n";
    }

    close $TMP_FH
      or
      $plugin->nagios_exit( $plugin->UNKNOWN, "Cannot close timer: $OS_ERROR" );

    return;

}

##############################################################################
# Usage     : ($time, $in, $out) = read_timer( $temporary_file )
# Purpose   : reads the time and I/O data from the temporary file
# Returns   : ($time, $in, $out) time difference, input and output data
# Arguments : $temporary_file : name of the temporary file holding the timer
#             data
# Throws    : n/a
# Comments  : n/a
# See also  : n/a
sub read_timer {

    my $tmp = shift;

    if ( !defined $tmp ) {
        $plugin->nagios_exit( $plugin->UNKNOWN,
            'Internal error: read_timer called without parameters' );
    }

    my $TMP_FH;
    my $time;
    my $in;
    my $out;
    my $diff;

    ## no critic (InputOutput::RequireBriefOpen)
    open $TMP_FH, q{<},
      $tmp
      or $plugin->nagios_exit( $plugin->UNKNOWN,
        "Cannot open timer ($tmp): $OS_ERROR" );

    while (<$TMP_FH>) {

        chomp;

        if ( $options->debug() ) {
            print "[DEBUG] reading $tmp: |$_|\n";
        }

        ( $time, $in, $out ) = split;
        $diff = time - $time;

    }

    close $TMP_FH
      or
      $plugin->nagios_exit( $plugin->UNKNOWN, "Cannot close timer: $OS_ERROR" );
    ## use critic (InputOutput::RequireBriefOpen)

    verbose 'reading data: read='
      . format_number( $in, 0, 0, )
      . ' write='
      . format_number( $out, 0, 0, )
      . ' timestamp='
      . $time
      . ' (step '
      . format_number( $diff, 0, 0, )
      . "s)\n", 1;

    return ( $diff, $in, $out );

}

##############################################################################
# Usage     : $boolean = use_diskstats();
# Purpose   : detects the Linux kernel version and determines if we should
#             use diskstats
# Returns   : true if we should use diskstats
# Arguments : n/a
# Throws    : n/a
# Comments  : n/a
# See also  : n/a
sub use_diskstats {

    my ( $sysname, $nodename, $release, $version, $machine ) = uname;

    if ( $options->debug() ) {
        print "[DEBUG] kernel $release\n";
    }

    return ( $release =~ /^2.6/mxs ) || ( $release =~ /^[3-9]/mxs );

}

##############################################################################
# Usage     : read_26($device)
# Purpose   : reads kernel 2.6 disk stats
# Returns   : n/a
# Arguments : n/a
# Throws    : n/a
# Comments  : n/a
# See also  : n/a
sub read_26 {

    my $device = shift;

    foreach my $line (@stat_file) {

        # kernel version 2.6

     # /proc/diskstats format
     #
     # Disks
     # =====
     #
     # Field  1 -- # of reads issued
     #     This is the total number of reads completed successfully.
     # Field  2 -- # of reads merged, field 6 -- # of writes merged
     #     Reads and writes which are adjacent to each other may be merged for
     #     efficiency.  Thus two 4K reads may become one 8K read before it is
     #     ultimately handed to the disk, and so it will be counted (and queued)
     #     as only one I/O.  This field lets you know how often this was done.
     # Field  3 -- # of sectors read
     #     This is the total number of sectors read successfully.
     # Field  4 -- # of milliseconds spent reading
     #     This is the total number of milliseconds spent by all reads (as
     #     measured from __make_request() to end_that_request_last()).
     # Field  5 -- # of writes completed
     #     This is the total number of writes completed successfully.
     # Field  7 -- # of sectors written
     #     This is the total number of sectors written successfully.
     # Field  8 -- # of milliseconds spent writing
     #     This is the total number of milliseconds spent by all writes (as
     #     measured from __make_request() to end_that_request_last()).
     # Field  9 -- # of I/Os currently in progress
     #     The only field that should go to zero. Incremented as requests are
     #     given to appropriate request_queue_t and decremented as they finish.
     # Field 10 -- # of milliseconds spent doing I/Os
     #     This field is increases so long as field 9 is nonzero.
     # Field 11 -- weighted # of milliseconds spent doing I/Os
     #     This field is incremented at each I/O start, I/O completion, I/O
     #     merge, or read of these stats by the number of I/Os in progress
     #     (field 9) times the number of milliseconds spent doing I/O since the
     #     last update of this field.  This can provide an easy measure of both
     #     I/O completion time and the backlog that may be accumulating.
     #
     # Partitions
     # ==========
     #
     # Field  1 -- # of reads issued
     #     This is the total number of reads issued to this partition.
     # Field  2 -- # of sectors read
     #     This is the total number of sectors requested to be read from this
     #     partition.
     # Field  3 -- # of writes issued
     #     This is the total number of writes issued to this partition.
     # Field  4 -- # of sectors written
     #     This is the total number of sectors requested to be written to
     #     this partition.

    # set $_ to line to be able to use the default split on whitespace *skipping
    # any leading whitespace*
        $_ = $line;

        my (
            $major, $minor, $name, $f01, $f02, $f03, $f04,
            $f05,   $f06,   $f07,  $f08, $f09, $f10, $f11
        ) = split;

        if ( $name eq $device ) {

            if ( $options->debug() ) {
                print "[DEBUG] Device info found: |$_|\n";
            }

            # In theory with 2.6 kernels we should get only 4 fields but
            # on some distributions we get 11 of them

            if ( defined $f05 && $f05 =~ m/\d/mxs ) {

                # 11 fields
                if ( $options->debug() ) {
                    print
"[DEBUG] 11 fields diskstats entry (reading the 3rd and the 7th)\n";
                }
                return ( $f03, $f07 );
            }
            else {

                # 4 fields
                if ( $options->debug() ) {
                    print
"[DEBUG] 4 fields diskstats entry: reading the 2nd ($f02) and the 4th ($f04)\n";
                }
                return ( $f02, $f04 );
            }

        }

    }

    $plugin->nagios_exit( $plugin->UNKNOWN, "Device '$device' not found" );

    return

}

##############################################################################
# Usage     : read_24($device)
# Purpose   : reads kernel 2.4 disk stats
# Returns   : n/a
# Arguments : n/a
# Throws    : n/a
# Comments  : n/a
# See also  : n/a
sub read_24 {

    my $device = shift;

    foreach my $line (@stat_file) {

        # kernel version 2.4

        # /proc/partitions format
        #
        # major         Major number
        # minor         Minor number
        # name          Name
        # rio           Number of read IO requests completed
        # rmerge        Number of submitted read requests that were merged
        #               into existing requests.
        # rsect         Number of read IO sectors submitted
        # ruse          Total length of time all completed read requests have
        #               taken to date, in milliseconds
        # wio           Number of write IO requests completed
        # wmerge        Number of submitted write requests that were merged
        #               into existing requests.
        # wsect         Number of write IO sectors submitted
        # wuse          Total length of time all completed write requests have
        #               taken to date, in milliseconds
        # running       Instantaneous count of IOs currently in flight
        # use           How many milliseconds there has been at least one
        #               IO in flight
        # aveq          The sum of how long all requests have spent in flight,
        #               in milliseconds

        $_ = $line;
        my (
            $major,  $minor, $blocks,  $name, $rio,
            $rmerge, $rsect, $ruse,    $wio,  $wmerge,
            $wsect,  $wuse,  $running, $use,  $aveq,
        ) = split;

        if ( defined $name && $name ne 'name' ) {
            verbose "Information found for '$name'\n", 1;
        }

        if ( $name =~ /^ide.*bus(\d).*target(\d).*lun(\d)/mxs ) {

            # we try to get the corresponding device

            my $bus    = $1;
            my $target = $2;
            my $lun    = $3;

            verbose "Mapping $name";

            verbose " to \n";
        }

        if ( $name eq $device ) {
            return ( $rsect, $wsect );
        }

    }

    return;

}

##############################################################################
# Usage     : whoami()
# Purpose   : retrieve the user runnging the process
# Returns   : username
# Arguments : n/a
# Throws    : n/a
# Comments  : n/a
# See also  : n/a
sub whoami {
    my $output;

    my $user;

    my $pid = open $output, q{-|},
      'whoami'
      or $plugin->nagios_exit( $plugin->UNKNOWN,
        "Cannot determine the user: $OS_ERROR" );
    while (<$output>) {
        chomp;
        $user = $_;
        last;
    }
    if (  !( close $output )
        && ( $OS_ERROR != 0 ) )
    {

        # close to a piped open return false if the command with non-zero
        # status. In this case $! is set to 0
        $plugin->nagios_exit( $plugin->UNKNOWN,
            "Error while closing pipe to whoami: $OS_ERROR" );

    }

    if ( !$user ) {
        $plugin->nagios_exit( $plugin->UNKNOWN, 'Cannot determine the user' );
    }

    return $user;

}

##############################################################################
# Usage     : shortenrate()
# Purpose   : shortens a number by a multiple of binary prefixes
# Returns   : number, binary prefix
# Arguments : number, factor (1024 or 1000)
# Throws    : n/a
# Comments  : n/a
# See also  : n/a
sub shortenrate {
    my $rate   = shift;
    my $factor = shift;
    my $unit   = q{};

    if ( $rate > $factor ) {
        $rate /= $factor;
        $unit = 'K';
        if ( $rate > $factor ) {
            $rate /= $factor;
            $unit = 'M';
        }
        if ( $rate > $factor ) {
            $rate /= $factor;
            $unit = 'G';
        }
    }

    return ( $rate, $unit );
}

##############################################################################
# Usage     : run();
# Purpose   : main method
# Returns   : n/a
# Arguments : n/a
# Throws    : n/a
# Comments  : n/a
# See also  : n/a
## no critic (Subroutines::ProhibitExcessComplexity)
sub run {

    ################
    # initialization
    $factor = 1;
    $format = q{s};
    $plugin = $plugin_module->new( shortname => 'CHECK_DISKIO' );

    ########################
    # Command line arguments

    my $usage = <<'EOT';
check_diskio --device=devicename --critical=critical --warning=warning
             [--help] [--reset] [--silent] [--ssize=size]
             [--verbose] [--version] [--uom=unit] [--factor=unit]
             [--strip-partition-number]
EOT

    my $extra = <<'EOT';

Units of measurement:

unit                       description
---------------------------------------------
  
sector,sectors,s           sectors/s
bit,bits,b        bps      bits per second
byte,bytes,B      Bps      bytes per second

Units of thresholds:

unit                       description
---------------------------------------------
 
Ki,Kibi                    kilo binary
Mi,Mebi                    mega binary
Gi,Gibi                    giga binary
K,kilo                     kilo
M,mega                     mega
G,giga                     giga

EOT

    $options = $plugin_getopt_module->new(
        usage   => $usage,
        extra   => $extra,
        version => $VERSION,
        url     => 'https://trac.id.ethz.ch/projects/nagios_plugins',
        blurb   => 'Monitor disk I/O',
    );

    $options->arg(
        spec     => 'device|d=s@',
        help     => 'device name(s) (or mount point(s))',
        required => 1,
    );

    $options->arg(
        spec     => 'critical|c=i',
        help     => 'critical number of sectors/s',
        required => 1,
    );

    $options->arg(
        spec     => 'warning|w=i',
        help     => 'number of sectors/s which generates a warning',
        required => 1,
    );

    $options->arg(
        spec     => 'reset|r',
        help     => 'reset the counter(s)',
        required => 0,
    );

    $options->arg(
        spec     => 'silent|s',
        help     => 'no warnings or critials are issued',
        required => 0,
    );

    $options->arg(
        spec     => 'ssize=i',
        help     => 'specify the sector size in bytes (default 512)',
        required => 0,
        default  => $BYTES_PER_SECTOR,
    );

    $options->arg(
        spec     => 'uom=s',
        help     => 'unit of measurement',
        required => 0,
    );

    $options->arg(
        spec     => 'factor=s',
        help     => 'unit of threshold',
        required => 0,
    );

    $options->arg(
        spec     => 'debug',
        help     => 'debugging output',
        required => 0,
    );

    $options->arg(
        spec => 'testfile=s',
        help =>
          'tests the plugin with the given disk statistics file (debugging)',
        required => 0,
    );

    $options->arg(
        spec => 'strip-partition-number',
        help =>
'strips p[0-9]+ from the device number (needed for HP Smart Array cards)',
        required => 0,
    );

    $options->getopts();

    ################################################################################
    # Sanity checks

    ################
    # kernel version

    my $proc_diskstats_available = ( -r '/proc/diskstats' );

    #########################
    # Disk stats (read files)

    if ( use_diskstats() ) {
        if ( !-r '/proc/diskstats' && !-r '/proc/partitions' ) {
            $plugin->nagios_exit( $plugin->UNKNOWN,
                '/proc/diskstats or /proc/partitions not readable' );
        }
    }
    else {
        if ( !-r '/proc/partitions' ) {
            $plugin->nagios_exit( $plugin->UNKNOWN,
                '/proc/partitions not readable' );
        }
    }

    if ( !$options->testfile() ) {

        if ( -r '/proc/diskstats' ) {
            @stat_file = File::Slurp::read_file('/proc/diskstats');
        }
        else {
            @stat_file = File::Slurp::read_file('/proc/partitions');
        }

    }
    else {

        if ( !-r $options->testfile() ) {
            $plugin->nagios_exit( $plugin->UNKNOWN,
                $options->testfile() . ' not readable' );
        }

        verbose 'Reading ' . $options->testfile() . "\n";

        @stat_file = File::Slurp::read_file( $options->testfile() );

    }

    if ( $options->debug() ) {
        for my $line (@stat_file) {
            chomp $line;
            my $filename;
            if ( -r '/proc/diskstats' ) {
                $filename = '/proc/diskstats';
            }
            else {
                $filename = '/proc/partitions';
            }
            print "[DEBUG] $filename |$line|\n";
        }
    }

    #########
    # Devices

    my @unique_devices;
    ## no critic (Miscellanea::ProhibitTies)
    tie @unique_devices, 'Array::Unique';
    ## use critic (Miscellanea::ProhibitTies)

    for my $device ( @{ $options->device() } ) {

        # Detect device from mount point
        if ( !( $device =~ /^\/dev\//mxs ) ) {

            if ( $options->debug() ) {
                print "[DEBUG] $device is a mount point\n";
            }

            if ( $device ne q{/} && $device =~ /\/$/mxs ) {

                # remove trailing /
                $device =~ s/\/$//mxs;
            }

            my $MTAB_FH;
            my $mount_point = $device;

            undef $device;

            ## no critic (InputOutput::RequireBriefOpen)
            open $MTAB_FH, q{<},
              '/etc/mtab'
              or $plugin->nagios_exit( $plugin->UNKNOWN,
                "Cannot open /etc/mtab: $OS_ERROR" );
            while (<$MTAB_FH>) {
                my $line = $_;
                chomp $line;
                if ( $options->debug() ) {
                    print "[DEBUG] /etc/mtab |$line|\n";
                }
                if ( $line =~ /(\/dev.*)[ ]+$mount_point[ ]+.*/mxs ) {
                    $device = $1;
                    if ( $options->debug() ) {
                        print "[DEBUG] device found: $device\n";
                    }
                    last;
                }
            }
            close $MTAB_FH
              or $plugin->nagios_exit( $plugin->UNKNOWN,
                "Error while closing /etc/mtab: $OS_ERROR" );
            ## use critic (InputOutput::RequireBriefOpen)

            if ( !$device ) {
                $plugin->nagios_exit( $plugin->UNKNOWN,
                    "Could not find a device for $mount_point" );
            }

            # device found: strip the partition number
            if ( $options->get('strip-partition-number') ) {
                $device =~ s/p?\d+$//mxs;
            }

            verbose "Mount point $mount_point corresponds to device $device\n";

            my $dev_name = $device;
            $dev_name =~ s/^\/dev\///mxs;

            if ( $options->debug ) {
                print "[DEBUG] device name: $dev_name\n";
            }

            ## no critic (ControlStructures::ProhibitDeepNests)
            if ( !any { /\s$dev_name\s/msx } @stat_file ) {

                # LVM?
                if ( whoami() eq 'root' ) {

                    my @output;

                    @output = exec_command("lvdisplay $device 2>&1 ");

                    if ( any { /not[ ]found/mxs } @output ) {
                        verbose "LVM volume not found\n";
                    }
                    else {

                        my @volume_groups = grep { /VG[ ]Name/mxs } @output;
                        if ( @volume_groups == 1 ) {

                            $volume_groups[0] =~ s{.*[ ]}{}mxs;

                            @output = exec_command(
                                "vgdisplay -v $volume_groups[0] 2>&1 ");

                            if ( any { /not[ ]found/mxs } @output ) {
                                verbose
                                  "Cannot get info on $volume_groups[0]\n";
                            }
                            else {

                                my @pvs = grep { /PV[ ]Name/mxs } @output;

                                if ( @pvs < 1 ) {
                                    verbose "No physical volumes found\n";
                                }
                                else {

                                    # strip everything but the device name fro
                                    # the output
                                    for (@pvs) {
s{.*\s+/dev/([[:lower:]]+).*}{/dev/$1}mxs;
                                    }

                                    push @unique_devices, @pvs;

                                }

                            }

                        }

                    }

                }
                else {

                    $plugin->nagios_exit( $plugin->UNKNOWN,
                        "Cannot check LVM volume $device if not root" );

                }

            }
            ## use critic (ControlStructures::ProhibitDeepNests)

        }
        else {

            push @unique_devices, $device;

        }

    }

    if ( $options->debug() ) {
        #<<<
        print "[DEBUG] devices: @unique_devices\n"; ## no critic (RequireCheckedSyscalls)
        #>>>
    }

    # check if the devices exist

    for my $device (@unique_devices) {
        if ( !-e $device && !$options->testfile() ) {
            $plugin->nagios_exit( $plugin->UNKNOWN,
                "Device $device not found" );
        }
    }

    verbose "Checking: @unique_devices\n";

    #####
    # UOM

    my $multiplier;
    my $UOM;

    if ( $options->uom() ) {
        $format = $options->uom();
    }

    # according to the guidelines only [TGMK]B (Bytes) are valid UOM
    #   http://nagiosplug.sourceforge.net/developer-guidelines.html#AEN201
    my $perfdata_uom = 'B';
    if ( $format eq 's' || $format =~ /^sectors?$/mxs ) {

        #sectors
        $multiplier   = 1;
        $UOM          = 'sectors';
        $perfdata_uom = q{};

    }
    elsif ( $format eq 'b' || $format =~ /^bits?$/mxs ) {

        # bits
        $multiplier   = $options->ssize() * $BITS_PER_BYTE;
        $UOM          = 'b';
        $perfdata_uom = q{};

    }
    elsif ( $format eq 'B' || $format =~ /^bytes?$/mxs ) {

        # bytes
        $multiplier = $options->ssize();
        $UOM        = 'B';

    }
    else {
        $plugin->nagios_exit( $plugin->UNKNOWN,
            "Uknown unit of measurement: $format" );
    }

    my $unit = $KIBI;
    ## no critic (ControlStructures::ProhibitCascadingIfElse)
    if ( $options->factor() ) {
        $format = $options->factor();

        if ( $format =~ m/^ki$/imxs || $format =~ m/^Kibi$/imxs ) {
            $factor = $KIBI;
        }
        elsif ( $format =~ m/^mi$/imxs || $format =~ m/^mebi$/imxs ) {
            $factor = $KIBI * $KIBI;
        }
        elsif ( $format =~ m/^gi$/imxs || $format =~ m/^gibi$/imxs ) {
            $factor = $KIBI * $KIBI * $KIBI;
        }
        elsif ( $format =~ m/^k$/imxs || $format =~ m/^Kilo$/imxs ) {
            $factor = $KILO;
            $unit   = $KILO;
        }
        elsif ( $format =~ m/^m$/imxs || $format =~ m/^mega$/imxs ) {
            $factor = $KILO * $KILO;
            $unit   = $KILO;
        }
        elsif ( $format =~ m/^g$/imxs || $format =~ m/^giga$/imxs ) {
            $factor = $KILO * $KILO * $KILO;
            $unit   = $KILO;
        }
        else {
            $plugin->nagios_exit( $plugin->UNKNOWN,
                "Uknown unit of threshold: $format" );
        }
    }
    ## use critic (ControlStructures::ProhibitCascadingIfElse)

    ################
    # Initialization

    $threshold = $plugin_threshold_module->set_thresholds(
        warning  => $options->warning() * $factor,
        critical => $options->critical() * $factor,
    );

    my $tmp_file_prefix = '/tmp/check_diskio_status-' . whoami();

    # strip /dev/
    for (@unique_devices) {
        s/^\/dev\///mxs;
    }

    for my $device (@unique_devices) {

        # we need one temporary file per device
        my ( $controller, $disk ) = split /\//mxs, $device;
        if ( !$disk ) {
            $tmp_files{$device} = "$tmp_file_prefix-$controller";
        }
        else {
            $tmp_files{$device} = "$tmp_file_prefix--$disk";
        }

    }

    ########################
    # Check the proc entry

    my $diff;
    my $found = 0;
    my $in;
    my $out;
    my $time;

    my $s_read;
    my $s_write;

    my $TMP_FH;
    my $status = $plugin->OK;
    my @status_lines;

    for my $device (@unique_devices) {

        if ( use_diskstats() && $proc_diskstats_available ) {
            verbose "Kernel version >= 2.6\n", 1;
            ( $s_read, $s_write ) = read_26($device);
        }
        else {
            verbose "Kernel version < 2.6\n",;
            ( $s_read, $s_write ) = read_24($device);
        }

        verbose 'current data: read='
          . format_number( $s_read, 0, 0 )
          . ' write='
          . format_number( $s_write, 0, 0 )
          . "\n", 1;

        # check if the temporary file exists and has some content
        if ( !-s $tmp_files{$device} ) {
            verbose "temporary file not available resetting and waiting\n";
            write_timer( $tmp_files{$device}, $s_write, $s_read );
            sleep 1;
            if ( use_diskstats() && $proc_diskstats_available ) {
                ( $s_read, $s_write ) = read_26($device);
            }
            else {
                ( $s_read, $s_write ) = read_24($device);
            }
        }

        ( $diff, $in, $out ) = read_timer( $tmp_files{$device} );

        if ( $diff < 1 ) {

            # wait a little bit
            sleep 1;
            ( $diff, $in, $out ) = read_timer( $tmp_files{$device} );

        }

        write_timer( $tmp_files{$device}, $s_write, $s_read );

        verbose 'time difference: ' . $diff . "s\n", 1;

        if ( $diff == 0 ) {

            # round up
            $diff = 1;

        }

        # check for overflows (2^32 sectors)
        if ( $s_write < $in ) {

            # overflow
            $diskio_w{$device} = $OVERFLOW - $in + $s_write;

        }
        else {
            $diskio_w{$device} = $s_write - $in;
        }

        if ( $s_read < $out ) {

            # overflow
            $diskio_r{$device} = $OVERFLOW - $out + $s_read;
        }
        else {
            $diskio_r{$device} = $s_read - $out;
        }

        $diskio_w{$device} = int( $diskio_w{$device} * $multiplier / $diff );
        $diskio_r{$device} = int( $diskio_r{$device} * $multiplier / $diff );
        $diskio{$device}   = $diskio_w{$device} + $diskio_r{$device};

        my $suffix = q{};

        # we append the device name to the labels only if there is more
        # than one device to maintain backwards compatibility
        if ( @unique_devices > 1 ) {
            $suffix = "_\U$device";
        }

        # use unscaled values for performance data - will be scaled by rrdtool

        $plugin->add_perfdata(
            label     => "WRITE$suffix",
            value     => sprintf( '%.0f', $diskio_w{$device} ),
            uom       => $perfdata_uom,
            threshold => $threshold,
        );

        $plugin->add_perfdata(
            label     => "READ$suffix",
            value     => sprintf( '%.0f', $diskio_r{$device} ),
            uom       => $perfdata_uom,
            threshold => $threshold,
        );

        $plugin->add_perfdata(
            label     => "TOTAL$suffix",
            value     => sprintf( '%.0f', $diskio{$device} ),
            uom       => $perfdata_uom,
            threshold => $threshold,
        );

        my $device_status = $threshold->get_status( $diskio{$device} );
        if ( $device_status == $plugin->CRITICAL ) {
            $status = $plugin->CRITICAL;
        }
        elsif ($device_status == $plugin->WARNING
            && $status != $plugin->CRITICAL )
        {
            $status = $plugin->WARNING;
        }

        my ( $val, $prefix ) = shortenrate( $diskio{$device}, $unit );
        push @status_lines,
          "$device " . sprintf( '%.2f', $val ) . " $prefix$UOM/s";

    }

    if ( $options->silent() ) {
        $plugin->nagios_exit( $plugin->OK,
            ( join ', ', @unique_devices ) . ' OK' );
    }
    else {
        $plugin->nagios_exit( $status, join ', ', @status_lines, );
    }

    return;

}
## use critic (Subroutines::ProhibitExcessComplexity)

1;
