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

# Copyright (C) 2015 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_storage - Munin plugin to monitor storage usage.

=head1 APPLICABLE SYSTEMS

Linux systems with df command available. Optional LVM
storage statistics require lvm2 vgs command.

=head1 CONFIGURATION

It may be required to run this plugin as root to access
all device informations. You may also configure warning
and critical thresholds in your munin node configuration:

[hq_storage]
  user root
  env.warning 90
  env.critical 95

Default thresholds are 90% for warning and 95% for critical.

=head1 VERSION

  20151218

=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;

# Mountpoints starting with these strings will be excluded
my @excludes = ("/dev", "/proc", "/run", "/sys");

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

need_multigraph();

my %storages;
my %vgs;

# Read and parse df output
my $exec_fd;
if(open($exec_fd, "df -P -l 2>/dev/null |")) {
  DF_READ_LOOP: while(defined(my $line=<$exec_fd>)) {
    # Exclude header
    if($line =~ m/^Filesystem/) {
      next(DF_READ_LOOP);
    }
    my ($device, $size, $used, $free, $percent, $mountpoint) = $line =~ m/^(.*)[ \t]+(\d+)[ \t]+(\d+)[ \t]+(\d+)[ \t]+(\d+)%[ \t]+(.*)$/;
    EXCLUDES_LOOP: foreach my $exclude (@excludes) {
      if($mountpoint =~ m/^\Q$exclude\E/) {
        next(DF_READ_LOOP);
      }
    }
    $device =~ s/^\s+|\s+$//g;
    $storages{$device} = {
      'size' => $size * 1024,
      'used' => $used * 1024,
      'free' => $free * 1024,
      'reserved' => ($size - $used - $free) * 1024,
      'mountpoint' => $mountpoint,
    };
  }
  close($exec_fd);
}
else {
  print("Error running df command. Exiting.\n");
  exit(1);
}

if(open($exec_fd, "vgs --noheadings --nosuffix --separator=: --units=b --options=vg_name,vg_size,vg_free 2>/dev/null |")) {
  VGS_READ_LOOP: while(defined(my $line=<$exec_fd>)) {
    $line =~ s/\s//g;
    my ($vgname, $vgsize, $vgfree) = $line =~ m/^(.*):(\d+):(\d+)$/;
    $vgs{$vgname} = {
      'size' => $vgsize,
      'used' => $vgsize - $vgfree,
      'free' => $vgfree,
    }
  }
  close($exec_fd);
  my $status = $? >> 8;
  if($status != 0 and $status != 127) {
    print("Error running vgs command. Exiting.\n");
    exit(1);
  }
}

# get warning and critical thresholds
my $warning_level = 90;
my $critical_level = 95;
if(defined($ENV{'warning'})) {
  $warning_level = $ENV{'warning'};
}
if(defined($ENV{'critical'})) {
  $critical_level = $ENV{'critical'};
}

# Handle config
if(defined($ARGV[0]) and $ARGV[0] eq 'config') {
  foreach my $storage (sort keys %storages) {
    # set warning and critical thresholds
    my $warning = ($storages{$storage}{'size'} - $storages{$storage}{'reserved'}) * $warning_level / 100;
    my $critical = ($storages{$storage}{'size'} - $storages{$storage}{'reserved'}) * $critical_level / 100;
    # Choose color for graph depending on usage warning levels
    my $used_color = "0000ff";
    if($storages{$storage}{'used'} > $warning) {
      $used_color = "ff9500";
    }
    if($storages{$storage}{'used'} > $critical) {
      $used_color = "ff0000";
    }
    my $graph_name = $storage;
    $graph_name =~ s![/\-\.]!_!g;
    $graph_name =~ s!^_+!!g;
    print <<EOF;
multigraph storage_$graph_name
graph_title $storage ($storages{$storage}{'mountpoint'})
graph_order reserved used free size
graph_args --base 1024 --lower-limit 0
graph_vlabel bytes
graph_category storage

reserved.label Reserved
reserved.type GAUGE
reserved.draw AREASTACK
reserved.colour ff00ff
used.label Used
used.type GAUGE
used.draw AREASTACK
used.colour $used_color
used.line $warning:ffff00:Warning threshold
used.warning $warning
used.critical $critical
free.label Free
free.type GAUGE
free.draw AREASTACK
free.colour 00ff00
size.label Size
size.type GAUGE
size.draw LINE1
size.colour 000000

EOF
  }
  foreach my $vg (sort keys %vgs) {
    # set warning and critical thresholds
    my $warning = $vgs{$vg}{'size'} * $warning_level / 100;
    my $critical = $vgs{$vg}{'size'} * $critical_level / 100;
    # Choose color for graph depending on usage warning levels
    my $used_color = "0000ff";
    if($vgs{$vg}{'used'} > $warning) {
      $used_color = "ff9500";
    }
    if($vgs{$vg}{'used'} > $critical) {
      $used_color = "ff0000";
    }
    my $graph_name = $vg;
    $graph_name =~ s![/\-\.]!_!g;
    $graph_name =~ s!^_+!!g;
    print <<EOF;
multigraph vg_$graph_name
graph_title LVM VG "$vg"
graph_order used free size
graph_args --base 1024 --lower-limit 0
graph_vlabel bytes
graph_category lvm

used.label Used
used.type GAUGE
used.draw AREASTACK
used.colour $used_color
free.label Free
free.type GAUGE
free.draw AREASTACK
free.colour 00ff00
size.label Size
size.type GAUGE
size.draw LINE1
size.colour 000000

EOF
  }
  exit(0);
}

foreach my $storage (sort keys %storages) {
  my $graph_name = $storage;
  $graph_name =~ s![/\-\.]!_!g;
  $graph_name =~ s!^_+!!g;
  print("multigraph storage_" . $graph_name . "\n");
  print("reserved.value " . $storages{$storage}{'reserved'} . "\n");
  print("used.value " . $storages{$storage}{'used'} . "\n");
  print("free.value " . $storages{$storage}{'free'} . "\n");
  print("size.value " . $storages{$storage}{'size'} . "\n");
}

foreach my $vg (sort keys %vgs) {
  my $graph_name = $vg;
  $graph_name =~ s![/\-\.]!_!g;
  $graph_name =~ s!^_+!!g;
  print("multigraph vg_" . $graph_name . "\n");
  print("used.value " . $vgs{$vg}{'used'} . "\n");
  print("free.value " . $vgs{$vg}{'free'} . "\n");
  print("size.value " . $vgs{$vg}{'size'} . "\n");
}

exit(0);
