#!/usr/bin/perl
# -*- perl -*-

# Copyright (C) 2015-2023 Pirx Developers - https://pirx.dev/
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

=head1 NAME

hq_iostat - Munin plugin to monitor disk IO statistics.

=head1 APPLICABLE SYSTEMS

Linux systems with iostat command available.

=head1 CONFIGURATION

You may need to run this plugin as root to get all devices.
Just add following to your munin node configuration.

[hq_iostat]
  user root

=head1 VERSION

  20230309

=head1 MAGIC MARKERS

  #%# family=manual
  #%# capabilities=autoconf

=head1 BUGS

None.

=head1 AUTHOR

Pirx Developers - https://pirx.dev/

=head1 LICENSE

GPLv3

=cut

use strict;
use warnings;
use Munin::Plugin;

# Handle autoconf
if(defined($ARGV[0]) and $ARGV[0] eq 'autoconf') {
  print("yes\n");
  exit(0);
}

need_multigraph();

my %iodata;
my $iostat_fd;
# Read and parse df output
if(open($iostat_fd, "iostat -d -k -p ALL 2>/dev/null |")) {
  IOSTAT_READ_LOOP: while(defined(my $line=<$iostat_fd>)) {
    # Exclude headers
    if($line =~ m/^(Linux|Device|\s)/) {
      next(IOSTAT_READ_LOOP);
    }
    # Exclude ram, loop, cd/dvd and floppy
    if($line =~ m/^(fd|loop|ram|scd|sr)[0-9]/) {
      next(IOSTAT_READ_LOOP);
    }
    my @iostat_data = split(" ", $line);
    my $iostat_fields = $#iostat_data;
    my ($device, $tps, $kb_read_s, $kb_write_s, $kb_dscd_s, $kb_read, $kb_write, $kb_dscd);
    if($iostat_fields == 5) {
      ($device, $tps, $kb_read_s, $kb_write_s, $kb_read, $kb_write) = $line =~ m/^(.*)[\s]+([\d\.]+)[\s]+([\d\.]+)[\s]+([\d\.]+)[\s]+([\d\.]+)[\s]+([\d]+)$/;
    }
    elsif($iostat_fields == 7) {
      ($device, $tps, $kb_read_s, $kb_write_s, $kb_dscd_s, $kb_read, $kb_write, $kb_dscd) = $line =~ m/^(.*)[\s]+([\d\.]+)[\s]+([\d\.]+)[\s]+([\d\.]+)[\s]+([\d\.]+)[\s]+([\d]+)[\s]+([\d]+)[\s]+([\d]+)$/;
    }
    $device =~ s/^\s+|\s+$//g;
    my $device_name = "";
    # If device is hd* check its media type
    if($device =~ m/^hd[a-z]/) {
      my $device_type = `cat /sys/block/$device/device/media 2>/dev/null`;
      chomp($device_type);
      # Exclude cd/dvd and floppy
      if($device_type eq "cdrom" or $device_type eq "floppy") {
        next(IOSTAT_READ_LOOP);
      }
    }
    # If device is dm-* get friendly name from /sys
    if($device =~ m/^dm-[0-9]/) {
      $device_name = "/dev/mapper/" . `cat /sys/block/$device/dm/name 2>/dev/null`;
      chomp($device_name);
    }
    else {
      $device_name = "/dev/" . $device;
    }
    # Get device mountpoint from /proc
    my $mountpoint = `cat /proc/self/mounts 2>/dev/null | grep -E '^$device_name\\s' 2>/dev/null | cut -d' ' -f 2 2>/dev/null`;
    chomp($mountpoint);
    # if mountpoint is empty perhaps device was mounted using uuid, try to resolve
    if($mountpoint eq "") {
      my $uuid = `blkid -o value -s UUID $device_name`;
      chomp($uuid);
      if($uuid ne "") {
        $mountpoint = `cat /proc/self/mounts 2>/dev/null | grep -E '^/dev/disk/by-uuid/$uuid\\s' 2>/dev/null | cut -d' ' -f 2 2>/dev/null`;
        chomp($mountpoint);
      }
    }
    # If mountpoint is still empty check if this is swap device
    if($mountpoint eq "") {
      my $is_swap = `grep -E '^/dev/$device\\s' /proc/swaps 2>/dev/null`;
      if($is_swap ne "") {
        $mountpoint = "swap";
      }
    }
    if($mountpoint ne "") {
      $mountpoint = " (" . $mountpoint . ")";
    }
    $iodata{$device} = {
      'read' => $kb_read * 1024,
      'write' => $kb_write * 1024,
      'name' => $device_name,
      'mountpoint' => $mountpoint
    };
  }
  close($iostat_fd);
  if($? >> 8 != 0) {
    print("Error running iostat. Exiting.\n");
    exit(1);
  }
}
else {
  print("Error running iostat. Exiting.\n");
  exit(1);
}

# Handle config
if(defined($ARGV[0]) and $ARGV[0] eq 'config') {
  foreach my $device (sort keys %iodata) {
    my $graph_name = "disk_io_" . $device;
    $graph_name =~ s![/\-\.]!_!g;
    print <<EOF;
multigraph $graph_name
graph_title $iodata{$device}{'name'}$iodata{$device}{'mountpoint'} - read/write
graph_order read write
graph_args --base 1024 --lower-limit 0
graph_vlabel bytes
graph_category disk_io

read.label Read
read.type DERIVE
read.min 0
read.draw AREA
read.colour 00ff00
write.label Write
write.type DERIVE
write.min 0
write.draw LINE1
write.colour 0000ff

EOF
  }
  exit(0);
}

foreach my $device (sort keys %iodata) {
  my $graph_name = "disk_io_" . $device;
  $graph_name =~ s![/\-\.]!_!g;
  print("multigraph " . $graph_name . "\n");
  print("read.value " . $iodata{$device}{'read'} . "\n");
  print("write.value " . $iodata{$device}{'write'} . "\n");
}

exit(0);
