This file is indexed.

/usr/share/munin/plugins/diskstats is in munin-node 1.4.6-3ubuntu3.

This file is owned by root:root, with mode 0o755.

The actual contents of the file can be viewed below.

   1
   2
   3
   4
   5
   6
   7
   8
   9
  10
  11
  12
  13
  14
  15
  16
  17
  18
  19
  20
  21
  22
  23
  24
  25
  26
  27
  28
  29
  30
  31
  32
  33
  34
  35
  36
  37
  38
  39
  40
  41
  42
  43
  44
  45
  46
  47
  48
  49
  50
  51
  52
  53
  54
  55
  56
  57
  58
  59
  60
  61
  62
  63
  64
  65
  66
  67
  68
  69
  70
  71
  72
  73
  74
  75
  76
  77
  78
  79
  80
  81
  82
  83
  84
  85
  86
  87
  88
  89
  90
  91
  92
  93
  94
  95
  96
  97
  98
  99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
 113
 114
 115
 116
 117
 118
 119
 120
 121
 122
 123
 124
 125
 126
 127
 128
 129
 130
 131
 132
 133
 134
 135
 136
 137
 138
 139
 140
 141
 142
 143
 144
 145
 146
 147
 148
 149
 150
 151
 152
 153
 154
 155
 156
 157
 158
 159
 160
 161
 162
 163
 164
 165
 166
 167
 168
 169
 170
 171
 172
 173
 174
 175
 176
 177
 178
 179
 180
 181
 182
 183
 184
 185
 186
 187
 188
 189
 190
 191
 192
 193
 194
 195
 196
 197
 198
 199
 200
 201
 202
 203
 204
 205
 206
 207
 208
 209
 210
 211
 212
 213
 214
 215
 216
 217
 218
 219
 220
 221
 222
 223
 224
 225
 226
 227
 228
 229
 230
 231
 232
 233
 234
 235
 236
 237
 238
 239
 240
 241
 242
 243
 244
 245
 246
 247
 248
 249
 250
 251
 252
 253
 254
 255
 256
 257
 258
 259
 260
 261
 262
 263
 264
 265
 266
 267
 268
 269
 270
 271
 272
 273
 274
 275
 276
 277
 278
 279
 280
 281
 282
 283
 284
 285
 286
 287
 288
 289
 290
 291
 292
 293
 294
 295
 296
 297
 298
 299
 300
 301
 302
 303
 304
 305
 306
 307
 308
 309
 310
 311
 312
 313
 314
 315
 316
 317
 318
 319
 320
 321
 322
 323
 324
 325
 326
 327
 328
 329
 330
 331
 332
 333
 334
 335
 336
 337
 338
 339
 340
 341
 342
 343
 344
 345
 346
 347
 348
 349
 350
 351
 352
 353
 354
 355
 356
 357
 358
 359
 360
 361
 362
 363
 364
 365
 366
 367
 368
 369
 370
 371
 372
 373
 374
 375
 376
 377
 378
 379
 380
 381
 382
 383
 384
 385
 386
 387
 388
 389
 390
 391
 392
 393
 394
 395
 396
 397
 398
 399
 400
 401
 402
 403
 404
 405
 406
 407
 408
 409
 410
 411
 412
 413
 414
 415
 416
 417
 418
 419
 420
 421
 422
 423
 424
 425
 426
 427
 428
 429
 430
 431
 432
 433
 434
 435
 436
 437
 438
 439
 440
 441
 442
 443
 444
 445
 446
 447
 448
 449
 450
 451
 452
 453
 454
 455
 456
 457
 458
 459
 460
 461
 462
 463
 464
 465
 466
 467
 468
 469
 470
 471
 472
 473
 474
 475
 476
 477
 478
 479
 480
 481
 482
 483
 484
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
#!/usr/bin/perl
# -*- perl -*-
# vim: sts=4 sw=4 ts=8

# Docs at the bottom

use strict;
use warnings;

use File::Basename;
use Carp;
use POSIX;
use Munin::Plugin;
use MIME::Base64;
use Storable qw(nfreeze thaw);

# Hardcoded pollinterval of 300 seconds
my $poll_interval = 300;

# Graph width defaults to 400 in Munin 1.4
my $graph_width = $ENV{'graph_width'} ? $ENV{'graph_width'} : 400;

# Should lables be trimmed to fit graph width?
my $trim_labels = $ENV{'trim_labels'};

my $plugin_name = $Munin::Plugin::me;

# Check for multigraph capabilities
need_multigraph();

# Handle munin 'autoconf' command
do_autoconf() if ( $ARGV[0] && $ARGV[0] eq 'autoconf' );

# Fetch current counter values
my %cur_diskstats = fetch_device_counters();

# Weed out unwanted devices
filter_device_list( \%cur_diskstats );

# Handle munin 'config' command
# This can only be done after getting the device data
if ( defined $ARGV[0] && $ARGV[0] eq 'config' ) {
    do_config();
}

# Restore data from previous run
my ( $prev_time, %prev_diskstats ) = choose_old_state();

# Persist state from current run
add_new_state( time(), %cur_diskstats );

# Probably the first run for the given device, we need state to do our job,
# so let's wait for the next run.
exit if ( not defined $prev_time or not %prev_diskstats );

# Here happens the magic
generate_multigraph_data( $prev_time, \%prev_diskstats, \%cur_diskstats );

exit 0;

########
# SUBS #
########

# generate_multigraph_data
#
# Creates the data which is needed by munin's fetch command

sub generate_multigraph_data {

    my ( $prev_time, $prev_diskstats, $cur_diskstats ) = @_;

    my %results;

    for my $device ( keys %{$cur_diskstats} ) {
        $results{$device} =
          calculate_values( $prev_time, $prev_diskstats{$device},
            $cur_diskstats{$device} );
    }

    print_values_root( \%results );

    for my $device ( keys %results ) {
        print_values_device( $device, $results{$device} );
    }
    return;
}

# choose_old_state
#
# Look through the list of old states and choose the one which is closest
# to the poll interval

sub choose_old_state {

    my (%states) = restore_state();

    return unless ( keys %states );

    my $now = time();

    my $old_delta;
    my $return_timestamp;

    for my $timestamp ( sort keys %states ) {

        # Calculate deviation from ideal interval
        my $delta = abs( $now - $timestamp - $poll_interval );

        # Safe initial delta
        $old_delta = $delta + 1 unless defined $old_delta;

        # Bail out and use previous result if it was closer to the interval
        last if ( $delta > $old_delta );

        $old_delta        = $delta;
        $return_timestamp = $timestamp;
    }
    return $return_timestamp,
      %{ thaw decode_base64 $states{$return_timestamp} };
}

# add_new_state
#
# Add the current state to the list of states
# Discard any state that is noticeable older than the poll interval

sub add_new_state {
    my ( $cur_time, %cur_diskstats ) = @_;

    my (%states) = restore_state();
    my $now = time();

    for my $timestamp ( sort keys %states ) {
        last if ( ( $now - $timestamp ) <= $poll_interval * 1.5 );
        delete $states{$timestamp};
    }

    # FIXME: There ought to be a better way to do this.
    $states{$cur_time} = encode_base64 nfreeze \%cur_diskstats;

    save_state(%states);
    return;
}

# calculate_values
#
# Calculates all data that get's graphed

sub calculate_values {
    my ( $prev_time, $prev_stats, $cur_stats ) = @_;

    my $bytes_per_sector = 512;

    my $interval = time() - $prev_time;

    my $read_ios  = $cur_stats->{'rd_ios'} - $prev_stats->{'rd_ios'};
    my $write_ios = $cur_stats->{'wr_ios'} - $prev_stats->{'wr_ios'};

    my $rd_ticks = $cur_stats->{'rd_ticks'} - $prev_stats->{'rd_ticks'};
    my $wr_ticks = $cur_stats->{'wr_ticks'} - $prev_stats->{'wr_ticks'};

    my $rd_sectors = $cur_stats->{'rd_sectors'} - $prev_stats->{'rd_sectors'};
    my $wr_sectors = $cur_stats->{'wr_sectors'} - $prev_stats->{'wr_sectors'};

    my $tot_ticks = $cur_stats->{'tot_ticks'} - $prev_stats->{'tot_ticks'};

    my $read_io_per_sec  = $read_ios / $interval;
    my $write_io_per_sec = $write_ios / $interval;

    my $read_bytes_per_sec  = $rd_sectors / $interval * $bytes_per_sector;
    my $write_bytes_per_sec = $wr_sectors / $interval * $bytes_per_sector;

    my $total_ios         = $read_ios + $write_ios;
    my $total_ios_per_sec = $total_ios / $interval;

    # Utilization - or "how busy is the device"?
    # If the time spent for I/O was close to 1000msec for
    # a given second, the device is nearly 100% saturated.
    my $utilization = $tot_ticks / $interval;

    # Average time an I/O takes on the block device
    my $servicetime_in_sec =
      $total_ios_per_sec ? $utilization / $total_ios_per_sec / 1000 : 0;

    # Average wait time for an I/O from start to finish
    # (includes queue times et al)
    my $average_wait_in_sec =
      $total_ios ? ( $rd_ticks + $wr_ticks ) / $total_ios / 1000 : 0;
    my $average_rd_wait_in_sec = $read_ios  ? $rd_ticks / $read_ios / 1000  : 0;
    my $average_wr_wait_in_sec = $write_ios ? $wr_ticks / $write_ios / 1000 : 0;

    my $average_rd_rq_size_in_kb =
      $read_ios ? $rd_sectors * $bytes_per_sector / 1024 / $read_ios : 0;
    my $average_wr_rq_size_in_kb =
        $write_ios
      ? $wr_sectors * $bytes_per_sector / 1024 / $write_ios
      : 0;

    my $util_print = $utilization / 10;

    return {
        utilization              => $util_print,
        servicetime              => $servicetime_in_sec,
        average_wait             => $average_wait_in_sec,
        average_rd_wait          => $average_rd_wait_in_sec,
        average_wr_wait          => $average_wr_wait_in_sec,
        read_bytes_per_sec       => $read_bytes_per_sec,
        write_bytes_per_sec      => $write_bytes_per_sec,
        read_io_per_sec          => $read_io_per_sec,
        write_io_per_sec         => $write_io_per_sec,
        average_rd_rq_size_in_kb => $average_rd_rq_size_in_kb,
        average_wr_rq_size_in_kb => $average_wr_rq_size_in_kb,
    };

}

# print_values_root
#
# Return multigraph values for root graphs

sub print_values_root {

    my ($result) = @_;

    print "multigraph ${plugin_name}_latency\n";

    for my $device ( keys %{$result} ) {

        next unless ( $cur_diskstats{$device}->{'does_latency'} );
        my $graph_id = $cur_diskstats{$device}->{'graph_id'};

        print "${graph_id}_avgwait.value "
          . $result->{$device}->{'average_wait'} . "\n";
    }

    print "\nmultigraph ${plugin_name}_utilization\n";

    for my $device ( keys %{$result} ) {

        next unless ( $cur_diskstats{$device}->{'does_latency'} );
        my $graph_id = $cur_diskstats{$device}->{'graph_id'};

        print "${graph_id}_util.value "
          . $result->{$device}->{'utilization'} . "\n";
    }

    print "\nmultigraph ${plugin_name}_throughput\n";

    for my $device ( keys %{$result} ) {
        my $graph_id = $cur_diskstats{$device}->{'graph_id'};
        print "${graph_id}_rdbytes.value "
          . $result->{$device}->{'read_bytes_per_sec'} . "\n";
        print "${graph_id}_wrbytes.value "
          . $result->{$device}->{'write_bytes_per_sec'} . "\n";
    }

    print "\nmultigraph ${plugin_name}_iops\n";

    for my $device ( keys %{$result} ) {
        my $graph_id = $cur_diskstats{$device}->{'graph_id'};
        print "${graph_id}_rdio.value "
          . $result->{$device}->{'read_io_per_sec'} . "\n";
        print "${graph_id}_wrio.value "
          . $result->{$device}->{'write_io_per_sec'} . "\n";
    }
    return;
}

# print_values_device
#
# Return multigraph values for device graphs

sub print_values_device {

    my ( $device, $result ) = @_;
    my $graph_id = $cur_diskstats{$device}->{'graph_id'};

    if ( $cur_diskstats{$device}->{'does_latency'} ) {
        print <<"EOF";

multigraph ${plugin_name}_latency.$graph_id
svctm.value $result->{'servicetime'}
avgwait.value $result->{'average_wait'}
avgrdwait.value $result->{'average_rd_wait'}
avgwrwait.value $result->{'average_wr_wait'}
EOF

        print <<"EOF";

multigraph ${plugin_name}_utilization.$graph_id
util.value $result->{'utilization'}
EOF

    }

    print <<"EOF";

multigraph ${plugin_name}_throughput.$graph_id
rdbytes.value $result->{'read_bytes_per_sec'}
wrbytes.value $result->{'write_bytes_per_sec'}
EOF

    print <<"EOF";

multigraph ${plugin_name}_iops.$graph_id
rdio.value $result->{'read_io_per_sec'}
wrio.value $result->{'write_io_per_sec'}
avgrdrqsz.value $result->{'average_rd_rq_size_in_kb'}
avgwrrqsz.value $result->{'average_wr_rq_size_in_kb'}
EOF

    return;
}

# read_procfs
#
# Pull diskstat information from procfs

sub read_procfs {

    my $statfh;

    open $statfh, '<', '/proc/diskstats'
      or croak "Failed to open '/proc/diskstats': $!\n";

    my @lines;

    while ( my $line = <$statfh> ) {

        # Strip trailing newline and leading whitespace
        chomp $line;
        $line =~ s/^\s+//;

        my @elems = split /\s+/, $line;

        # We explicitly don't support old-style diskstats
        # There are situations where only _some_ lines (e.g.
        # partitions on older 2.6 kernels) have fewer stats
        # numbers, therefore we'll skip them silently
        if ( @elems != 14 ) {
            next;
        }
        push @lines, \@elems;
    }

    close $statfh or croak "Failed to close '/proc/diskstats': $!";
    return @lines;
}

# read_sysfs
#
# Pull diskstat information from sysfs

sub read_sysfs {

    my @devices;
    my @lines;

    @devices = glob "/sys/block/*/stat";
    @devices = map { m!/sys/block/([^/]+)/stat! } @devices;

    for my $cur_device (@devices) {
        my $stats_file = "/sys/block/$cur_device/stat";

        my $statfh;

        open $statfh, '<', $stats_file
          or croak "Failed to open '$stats_file': $!\n";

        my $line = <$statfh>;

        close $statfh or croak "Failed to close '$stats_file': $!\n";

        # Trimming whitespace
        $line =~ s/^\s+//;
        chomp $line;

        my @elems = split /\s+/, $line;

        croak "'$stats_file' doesn't contain exactly 11 values. Aborting"
          if ( @elems != 11 );

        # Translate the devicename back before storing the information
        $cur_device =~ tr#!#/#;

        # Faking missing diskstats values
        unshift @elems, ( '', '', $cur_device );

        push @lines, \@elems;
    }

    return @lines;
}

# parse_diskstats
#
# Pulls diskstat information eitehr from procfs or sysfs, parses them and provides
# helper information.

sub parse_diskstats {

    my @stats;

    if ( glob "/sys/block/*/stat" ) {

        @stats = read_sysfs();
    }
    else {
        @stats = read_procfs();
    }

    my %diskstats;

    for my $entry (@stats) {

        my %devstat;

        # Hash-Slicing for fun and profit
        @devstat{
            qw(major minor devname
              rd_ios rd_merges rd_sectors rd_ticks
              wr_ios wr_merges wr_sectors wr_ticks
              ios_in_prog tot_ticks rq_ticks)
          }
          = @{$entry};

        # Resolve devicemapper names to their LVM counterparts
        my $device = $devstat{'devname'};
        my $pretty_device;

        if ( $device =~ /^dm-\d+$/ ) {
            $pretty_device = translate_devicemapper_name($device);
        }

        $pretty_device ||= $device;

        $devstat{'pretty_device_name'} = $pretty_device;

        # Short device name only containing the stuff after the last '/'
        # for graph labels et al.

        ( $devstat{'short_pretty_device_name'} ) =
          $pretty_device =~ m#/?([^/]+)$#;

        if ($trim_labels) {

            $devstat{'pretty_device_name'} =
              trim_label( 'pos', $devstat{'pretty_device_name'} );
            $devstat{'short_pretty_device_name'} =
              trim_label( 'posneg', $devstat{'short_pretty_device_name'} );
        }

        # The graph identifier needs to be cleaned up because munin will
        # complain about strange characters in the name otherwise
        #
        # The LVM <-> device mapper id mapping isn't stable across reboots,
        # use the LVM volume name instead

        $devstat{'graph_id'} = clean_fieldname($pretty_device);

        # Does the device provide latency information?
        $devstat{'does_latency'} =
          $devstat{'rd_ticks'} + $devstat{'wr_ticks'} ? 1 : 0;

        $diskstats{ $devstat{'devname'} } = \%devstat;
    }

    return %diskstats;
}

# fetch_device_counters
#
# Filters partitions and devices without IOs from diskstats
# and returns them

sub fetch_device_counters {

    my %diskstats = parse_diskstats();

    my @valid_devices;
  DEVICE:

# We need to see the devices before the partitions to make the partition filter work
#
# Sorting by the length of the device name gives us this certainty

    for my $devname ( sort { length($a) <=> length($b) } keys %diskstats ) {

        # Remove devices without traffic
        if (   $diskstats{$devname}->{'rd_ios'} == 0
            && $diskstats{$devname}->{'wr_ios'} == 0 )
        {
            delete $diskstats{$devname};
            next DEVICE;
        }

# Filter out partitions, since we only want to track the data of the parent devices
#
# We skip:
# - sda1 -> sda
# - c0d0p1 -> c0d0
# - md1p1 -> md1
# - etherd/e1.1p1 -> etherd/e1.1
#
# But we don't want to filter:
# - dm-100 -> dm-1
# - etherd/e1.10 -> etherd/e1.1
#
# To achieve this we skip a device if
# - it looks like a device we use with a "p\d" suffix
# - it looks like a device we use with a "\d" suffix, and the device didn't
#   have a numeric suffix in the first place

        for my $valid_device (@valid_devices) {

            if (
                $devname =~ m/^${valid_device}p\d+$/
                || (   $valid_device !~ /\d$/
                    && $devname =~ m/^$valid_device\d+$/ )
              )
            {
                delete $diskstats{$devname};
                next DEVICE;
            }
        }

        push @valid_devices, $devname;
    }

    return %diskstats;
}

# translate_devicemapper_name
#
# Tries to find a devicemapper name based on a minor number
# Returns either a resolved LVM path or the original devicename

sub translate_devicemapper_name {
    my ($device) = @_;

    my ($want_minor) = $device =~ m/^dm-(\d+)$/;

    croak "Failed to extract devicemapper id" unless defined($want_minor);

    my $dm_major = find_devicemapper_major();
    croak "Failed to get device-mapper major number\n"
      unless defined $dm_major;

    for my $entry ( glob "/dev/mapper/\*" ) {

        my $rdev  = ( stat($entry) )[6];
        my $major = floor( $rdev / 256 );
        my $minor = $rdev % 256;

        if ( $major == $dm_major && $minor == $want_minor ) {

            my $pretty_name = translate_lvm_name($entry);

            $entry =~ s|/dev/||;

            return defined $pretty_name ? $pretty_name : $entry;
        }
    }

    # Return original string if the device can't be found.
    return $device;
}

# translate_lvm_name
#
# Translates devicemapper names to their nicer LVM counterparts
# e.g. /dev/mapper/VGfoo-LVbar -> /dev/VGfoo/LVbar

sub translate_lvm_name {

    my ($entry) = @_;

    my $device_name = basename($entry);

# Check for single-dash-occurence to see if this could be a lvm devicemapper device.
    if ( $device_name =~ m/(?<!-)-(?!-)/ ) {

        # split device name into vg and lv parts
        my ( $vg, $lv ) = split /(?<!-)-(?!-)/, $device_name, 2;
        return unless ( defined($vg) && defined($lv) );

        # remove extraneous dashes from vg and lv names
        $vg =~ s/--/-/g;
        $lv =~ s/--/-/g;

        $device_name = "$vg/$lv";

        # Sanity check - does the constructed device name exist?
        # Breaks unless we are root.
        if ( stat("/dev/$device_name") ) {
            return "$device_name";
        }

    }
    return;
}

# find_devicemapper_major
#
# Searches for the major number of the devicemapper device

sub find_devicemapper_major {

    my $devicefh;

    open( $devicefh, '<', '/proc/devices' )
      or croak "Failed to open '/proc/devices': $!";

    my $dm_major;

    while ( my $line = <$devicefh> ) {
        chomp $line;

        my ( $major, $name ) = split /\s+/, $line, 2;

        next unless defined $name;

        if ( $name eq 'device-mapper' ) {
            $dm_major = $major;
            last;
        }
    }
    close($devicefh);

    return $dm_major;
}

sub do_autoconf {

    my %stats;

    # Capture any croaks on the way
    if ( eval { %stats = parse_diskstats() } && keys %stats ) {

        print "yes\n";
        exit 0;
    }
    else {
        print "no\n";
        exit 1;
    }
}

sub do_config {

    do_config_root();
    do_config_device();

    exit 0;
}

# do_config_root
#
# Print the configuration for the root graphs

sub do_config_root {

    my $config_graph_width = $graph_width;

    my @sorted_devices = sort_by_dm_last( keys %cur_diskstats );

    # If we don't trim labels, we have to find the correct graph width to
    # accomodate all labels
    unless ($trim_labels) {

        my @short_labels =
          map { $cur_diskstats{$_}->{'short_pretty_device_name'} }
          keys %cur_diskstats;
        my @long_labels =
          map { $cur_diskstats{$_}->{'pretty_device_name'} }
          keys %cur_diskstats;

        my $graph_width_short =
          find_required_graph_width( 'posneg', @short_labels );
        my $graph_width_long = find_required_graph_width( 'pos', @long_labels );

        $config_graph_width =
            $graph_width_short > $graph_width_long
          ? $graph_width_short
          : $graph_width_long;
    }

    # Print config for latency

    print <<"EOF";
multigraph ${plugin_name}_latency
graph_title Disk latency per device
graph_args --base 1000
graph_vlabel Average IO Wait (seconds)
graph_category disk
graph_width $config_graph_width

EOF

    for my $device (@sorted_devices) {
        next unless $cur_diskstats{$device}->{'does_latency'};
        my $graph_id = $cur_diskstats{$device}->{'graph_id'};

        print <<"EOF";
${graph_id}_avgwait.label $cur_diskstats{$device}->{'pretty_device_name'}
${graph_id}_avgwait.type GAUGE
${graph_id}_avgwait.info Average wait time for an I/O request
${graph_id}_avgwait.min 0
${graph_id}_avgwait.draw LINE1
EOF
    }

    # Print config for utilization

    print <<"EOF";

multigraph ${plugin_name}_utilization
graph_title Disk utilization per device
graph_args --base 1000 --lower-limit 0 --upper-limit 100 --rigid
graph_vlabel Percent
graph_category disk
graph_width $config_graph_width

EOF

    for my $device (@sorted_devices) {
        next unless $cur_diskstats{$device}->{'does_latency'};
        my $graph_id = $cur_diskstats{$device}->{'graph_id'};

        print <<"EOF";
${graph_id}_util.label $cur_diskstats{$device}->{'pretty_device_name'}
${graph_id}_util.type GAUGE
${graph_id}_util.info Utilization of the device
${graph_id}_util.min 0
${graph_id}_util.draw LINE1
EOF
    }

    # Print config for throughput

    print <<"EOF";

multigraph ${plugin_name}_throughput
graph_title Disk throughput per device
graph_args --base 1024
graph_vlabel Bytes/\${graph_period} read (-) / write (+)
graph_category disk
graph_width $config_graph_width

EOF

    for my $device (@sorted_devices) {
        my $graph_id = $cur_diskstats{$device}->{'graph_id'};

        print <<"EOF";
${graph_id}_rdbytes.label $cur_diskstats{$device}->{'short_pretty_device_name'}
${graph_id}_rdbytes.type GAUGE
${graph_id}_rdbytes.min 0
${graph_id}_rdbytes.draw LINE1
${graph_id}_rdbytes.graph no
${graph_id}_wrbytes.label $cur_diskstats{$device}->{'short_pretty_device_name'}
${graph_id}_wrbytes.type GAUGE
${graph_id}_wrbytes.min 0
${graph_id}_wrbytes.draw LINE1
${graph_id}_wrbytes.negative ${graph_id}_rdbytes
EOF
    }

    # Print config for iops

    print <<"EOF";

multigraph ${plugin_name}_iops
graph_title Disk IOs per device
graph_args --base 1000
graph_vlabel IOs/\${graph_period} read (-) / write (+) 
graph_category disk
graph_width $config_graph_width

EOF

    for my $device (@sorted_devices) {
        my $graph_id = $cur_diskstats{$device}->{'graph_id'};

        print <<"EOF";
${graph_id}_rdio.label $cur_diskstats{$device}->{'short_pretty_device_name'}
${graph_id}_rdio.type GAUGE
${graph_id}_rdio.min 0
${graph_id}_rdio.draw LINE1
${graph_id}_rdio.graph no
${graph_id}_wrio.label $cur_diskstats{$device}->{'short_pretty_device_name'}
${graph_id}_wrio.type GAUGE
${graph_id}_wrio.min 0
${graph_id}_wrio.draw LINE1
${graph_id}_wrio.negative ${graph_id}_rdio
EOF
    }
    print "\n";
    return;
}

# do_config_device
#
# Print the configuration for all device graphs

sub do_config_device {

    for my $device ( sort keys %cur_diskstats ) {

        # Nice name for graph
        my $pretty_device = $cur_diskstats{$device}->{'pretty_device_name'};
        my $graph_id      = $cur_diskstats{$device}->{'graph_id'};

        if ( $cur_diskstats{$device}->{'does_latency'} ) {

            print <<"EOF";
multigraph ${plugin_name}_latency.$graph_id
graph_title Disk latency for /dev/$pretty_device
graph_args --base 1000
graph_vlabel seconds
graph_category disk

svctm.label Average device IO time
svctm.type GAUGE
svctm.info Average time an I/O takes on the block device
svctm.min 0
svctm.draw LINE1
avgwait.label Average IO Wait time
avgwait.type GAUGE
avgwait.info Average wait time for an I/O from request start to finish (includes queue times et al)
avgwait.min 0
avgwait.draw LINE1
avgrdwait.label Average Read IO Wait time
avgrdwait.type GAUGE
avgrdwait.info Average wait time for a read I/O from request start to finish (includes queue times et al)
avgrdwait.min 0
avgrdwait.draw LINE1
avgwrwait.label Average Write IO Wait time
avgwrwait.type GAUGE
avgwrwait.info Average wait time for a write I/O from request start to finish (includes queue times et al)
avgwrwait.min 0
avgwrwait.draw LINE1

EOF

            print <<"EOF";
multigraph ${plugin_name}_utilization.$graph_id
graph_title Disk utilization for /dev/$pretty_device
graph_args --base 1000 --lower-limit 0 --upper-limit 100 --rigid
graph_vlabel Percent
graph_category disk

util.label Device utilization
util.type GAUGE
util.info Utilization of the device. If the time spent for I/O is close to 1000msec for a given second, the device is nearly 100% saturated.
util.min 0
util.draw LINE1

EOF

        }

        print <<"EOF";
multigraph ${plugin_name}_throughput.$graph_id
graph_title Disk throughput for /dev/$pretty_device
graph_args --base 1024
graph_vlabel Bytes/\${graph_period} read (-) / write (+)
graph_category disk

rdbytes.label Read Bytes
rdbytes.type GAUGE
rdbytes.min 0
rdbytes.draw LINE1
rdbytes.graph no
wrbytes.label Write Bytes
wrbytes.type GAUGE
wrbytes.min 0
wrbytes.draw LINE1
wrbytes.negative rdbytes

EOF

        print <<"EOF";
multigraph ${plugin_name}_iops.$graph_id
graph_title Disk IOs for /dev/$pretty_device
graph_args --base 1000
graph_vlabel Units read (-) / write (+)
graph_category disk

rdio.label dummy
rdio.type GAUGE
rdio.min 0
rdio.draw LINE1
rdio.graph no
wrio.label IO/sec
wrio.type GAUGE
wrio.min 0
wrio.draw LINE1
wrio.negative rdio
avgrdrqsz.label dummy
avgrdrqsz.type GAUGE
avgrdrqsz.min 0
avgrdrqsz.draw LINE1
avgrdrqsz.graph no
avgwrrqsz.label Avg Req Size (KiB)
avgwrrqsz.info Average Request Size in Kibibyte
avgwrrqsz.type GAUGE
avgwrrqsz.min 0
avgwrrqsz.draw LINE1
avgwrrqsz.negative avgrdrqsz

EOF

    }
    return;
}

# sort_by_dm_last
#
# Sort a given list, move devicemapper devices (dm-xx) to the end of the list

sub sort_by_dm_last {

    my @devices = @_;
    my $re      = qr/^dm-\d+/;
    my ( @dm, @non_dm );

    for my $device (@devices) {
        if ( $device =~ m/$re/ ) {
            push @dm, $device;
        }
        else {
            push @non_dm, $device;
        }
    }

    return ( sort @non_dm ), ( sort @dm );
}

# filter_device_list
#
# Filter unwanted devices from given hash

sub filter_device_list {

    my ($devices) = @_;

    my $include = $ENV{'include_only'};
    my $exclude = $ENV{'exclude'};

    croak
      "include_only and exclude are mutually exclusive. Please specify only one"
      if ( $include && $exclude );
    return unless ( $include || $exclude );

    my $mode = $include ? 0 : 1;

    # Pull data from environment variable
    my @filter_list =
      map { my $dev = $_; $dev =~ s!^/dev/!!; $dev; } split /\s*,\s*/,
      $include ? $include : $exclude;

    for my $device ( keys %{$devices} ) {

# Check if one of the user-provided names matches the current device-name or "pretty" LVM name
        my $match = map {
                 $device =~ m!\Q$_\E!
              || $devices->{$device}->{'pretty_device_name'} =~ m!\Q$_\E!;
        } @filter_list;

# Delete the device when it matches and mode is exclude or when it doesn't match and mode is include(_only)
        delete $devices->{$device} unless ( $match xor $mode );
    }
    return;
}

# calculate_pixels
#
# Calculates either
# the amount of available label-characters for $graph_width
# or
# the necessary $graph_width for a given label length
#
# type refers to the graph being positive only or positive/negative

sub calculate_pixels {

    my ( $mode, $type, $data ) = @_;

    # These values are probably wrong, but a good approximation
    # $graph_width + $graph_border_width == real image width
    my $graph_border_width = 97;

    # ($data_characters + $max_label_length + $padding_characters)
    # * $pixels_per_character == real image width
    my $padding_characters   = 10;
    my $pixels_per_character = 6;

    my $data_characters;

    if ( $type eq 'posneg' ) {

        # nnn.nnU/nnn.nnU_ times 4
        $data_characters = 64;
    }
    elsif ( $type eq 'pos' ) {

        # nnn.nnU_ times 4
        $data_characters = 32;
    }
    else {
        croak "Wrong $type in calculate_pixels";
    }

    my $return_data;

    if ( $mode eq 'required_width' ) {
        $return_data =
          $pixels_per_character *
          ( $padding_characters + $data_characters + $data ) -
          $graph_border_width;
    }
    elsif ( $mode eq 'available_characters' ) {
        $return_data =
          abs( ( $graph_width + $graph_border_width ) / $pixels_per_character )
          - $padding_characters - $data_characters;

    }
    else {
        croak "Wrong $mode in calculate_pixels";
    }
    return $return_data;
}

# find_required_graph_width
#
# Returns the necessary graph width for a list of labels and a type of graph

sub find_required_graph_width {

    my ( $type, @labels ) = @_;

    my $longest_label_length = 0;

    for my $label (@labels) {
        if ( length $label > $longest_label_length ) {
            $longest_label_length = length $label;
        }
    }

    my $required_graph_width =
      calculate_pixels( 'required_width', $type, $longest_label_length );
    return $graph_width if ( $required_graph_width <= $graph_width );

    # Return sufficient graph_width in 50 pixel increments
    return $graph_width +
      ( ceil( ( $required_graph_width - $graph_width ) / 50 ) * 50 );
}

# trim_label
#
# Trims a given label to it's non-wrapping size

sub trim_label {

    my ( $type, $label ) = @_;

    my $available_characters =
      calculate_pixels( 'available_characters', $type );

    if ( $available_characters < length $label ) {
        $label = '..' . substr $label, ( $available_characters - 2 ) * -1;
    }

    return $label;
}

__END__

=head1 NAME

diskstats - Munin multigraph plugin to monitor various values provided
via C</proc/diskstats> or C</sys/block/*/stat>

=head1 APPLICABLE SYSTEMS

Linux 2.6 systems with extended block device statistics enabled.

=head1 CONFIGURATION

None needed.

=head2 device-mapper names

This plugin displays nicer device-mapper device names if it is run as
root, but it functions as needed without root privilege.  To configure
for running as root enter this in a plugin configuration file:

  [diskstats]
    user root

=head2 Monitor specific devices

You can specify which devices should get monitored by the plugin via
environment variables. The variables are mutually exclusive and should
contain a comma-separated list of device names. Partial names
(e.g. 'sd' or 'dm-') are okay.

  [diskstats]
    env.include_only sda,sdb,cciss/c0d0

or

  [diskstats]
    env.exclude sdc,VGroot/LVswap

LVM volumes can be filtered either by their canonical names or their
internal device-mapper based names (e.g. 'dm-3', see dmsetup(8) for
further information).

=head2 Graph width and labels

This plugin will increase the graph_width dynamically to accomodate
longer-than-normal device names. You can disable this behavior by
setting the B<trim_labels> environment variable. Additionally, you can
specify a fixed graph_width for the graphs.

  [diskstats]
    # Set graph_width to 450, device names which are longer get trimmed
    env.trim_labels yes
    env.graph_width 450


=head1 INTERPRETATION

Among the more self-describing or well-known values like C<throughput>
(Bytes per second) there are a few which might need further
introduction.


=head2 Device Utilization

Linux provides a counter which increments in a millisecond-interval
for as long as there are outstanding I/O requests. If this counter is
close to 1000msec in a given 1 second timeframe the device is nearly
100% saturated. This plugin provides values averaged over a 5 minute
time frame per default, so it can't catch short-lived saturations, but
it'll give a nice trend for semi-uniform load patterns as they're
expected in most server or multi-user environments.


=head2 Device IO Time

The C<Device IO Time> takes the counter described under C<Device
Utilization> and divides it by the number of I/Os that happened in the
given time frame, resulting in an average time per I/O on the
block-device level.

This value can give you a good comparison base amongst different
controllers, storage subsystems and disks for similiar workloads.


=head2 Syscall Wait Time

These values describe the average time it takes between an application
issuing a syscall resulting in a hit to a blockdevice to the syscall
returning to the application.

The values are bound to be higher (at least for read requests) than
the time it takes the device itself to fulfill the requests, since
calling overhead, queuing times and probably a dozen other things are
included in those times.

These are the values to watch out for when an user complains that
C<the disks are too slow!>.


=head3 What causes a block device hit?

A non-exhaustive list:

=over

=item * Reads from files when the given range is not in the page cache or the O_DIRECT
flag is set.

=item * Writes to files if O_DIRECT or O_SYNC is set or sys.vm.dirty_(background_)ratio
is exceeded.

=item * Filesystem metadata operations (stat(2), getdents(2), file creation,
modification of any of the values returned by stat(2), etc.)

=item * The pdflush daemon writing out dirtied pages

=item * (f)sync

=item * Swapping

=item * raw device I/O (mkfs, dd, etc.)

=back

=head1 ACKNOWLEDGEMENTS

The core logic of this script is based on the B<iostat> tool of the
B<sysstat> package written and maintained by Sebastien Godard.

=head1 SEE ALSO

See C<Documentation/iostats.txt> in your Linux source tree for further
information about the C<numbers> involved in this module.

L<http://www.westnet.com/~gsmith/content/linux-pdflush.htm> has a nice
writeup about the pdflush daemon.

=head1 VERSION

  $Id$

=head1 MAGIC MARKERS

  #%# family=auto
  #%# capabilities=autoconf

=head1 AUTHOR

Michael Renner <michael.renner@amd.co.at>

=head1 LICENSE

GPLv2


=cut