#!/usr/bin/perl
# -*- perl -*-
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; version 2 dated June,
# 1991.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Magic markers (used by munin-node-configure and some installation scripts):
#%# family=auto
#%# capabilities=autoconf

use strict;
use warnings;
use WWW::Mechanize;

=pod

=encoding UTF-8

=head1 NAME

tl_sg - Plugin to monitor packets per second and link speed for TP-Link SG108E/SG1016E switches

=head1 APPLICABLE SYSTEMS

TP-Link SG108E/SG1016E switches with web management (http). Tested with software version 1.0.2 Build 20160526 Rel.34615 on TL SG108E

=head1 CONFIGURATION

Add this to the relevant munin-node config file. You can specify switch address, username, password and description for each port
(the switch management doesn't allow port descriptions). You should also create a fake host for the switch and attach the graphs to it.
Details here: https://cweiske.de/tagebuch/munin-multiple-hosts.htm

In /etc/munin/munin.conf add a new host called tl-sg108e (or whatever you want):
  [tl-sg108e]
    address 127.0.0.1
    use_node_name no

In /etc/munin/plugin-conf.d/munin-node add the following entry:

  [tl_sg]
  host_name tl-sg108e
  env.host 192.168.1.12
  env.port 80
  env.numberOfPorts 8
  env.username admin
  env.password mySecretPassword
  env.p1 'Link to PC1'
  env.p2 'Link to server1'
  env.p3 'Not used'
  env.p4 'Link to AP'
  env.p5 'Link to PC2'
  env.p6 'Link to PC3'
  env.p7 'Not used'
  env.p8 'Uplink'

The name in host_name must match the name defined in munin.conf, and the tl_sg name must match the plugin instance name (symlink).

If you're monitoring multiple switches, create different symlinks in /etc/munin/plugins pointing to this plugin and use the symlink
name as a configuration section as described above.

Requires WWW:Mechanize module:
  sudo apt-get install libwww-mechanize-perl

=head1 BUGS/GOTCHAS

The link speed is represented as a number:
  0 - down
  5 - 100Mbps full
  6 - 1Gbps

=head1 AUTHOR

Adrian Popa (https://github.com/mad-ady)

=head1 COPYRIGHT

Copyright (c) 2018, Adrian Popa

All rights reserved. This program is free software; you can
redistribute it and/or modify it under the terms of the GNU General
Public License as published by the Free Software Foundation; version 2
dated June, 1991.

=head1 VERSION

 1.1

=cut

# read parameters from munin
my $host     = ( $ENV{host}   || '192.168.1.1' );
my $tcpport  = ( $ENV{port}   || '80' );
my $username = ( $ENV{username} || 'admin' );
my $password = ( $ENV{password} || 'admin' );
my $numberOfPorts = ( $ENV{numberOfPorts} || '8' );

my %speedMapping = (
    0 => "down",
    1 => "auto",
    2 => "10M half-duplex",
    3 => "10M full-duplex",
    4 => "100M half-duplex",
    5 => "100M full-duplex",
    6 => "1G full-duplex",
);


#populate the ports and descriptions based on ENV
my %ports = ();
for ( 1 .. $numberOfPorts ) {
    my $i = $_;
    if ( defined $ENV{"p$i"} ) {
        $ports{$i} = $ENV{"p$i"};
    }
    else {
        #no description
        $ports{$i} = "Port $i";
    }
}

if ( $ARGV[0] and $ARGV[0] eq 'autoconf' ) {
    print "no (manual configuration needed)\n";
    exit 0;
}

if ( $ARGV[0] and $ARGV[0] eq "config" ) {
    foreach my $graphType (qw/Packets Speed/) {
        foreach my $port ( sort keys %ports ) {
            print "multigraph ${graphType}_if_$port\n",
              "graph_title $graphType for $host port $port $ports{$port}\n",
              "graph_info $graphType graph for port $port $ports{$port}\n",
              "graph_args --base 1000 -l 0\n",
              "graph_scale yes\n",
              "graph_category network\n";
            if ( $graphType eq 'Speed' ) {
                print "graph_vlabel speed\n";
                foreach my $value (sort keys %speedMapping){
                    print "p${port}_$value.label $speedMapping{$value}\n";
                    print "p${port}_$value.type GAUGE\n";
                    print "p${port}_$value.draw AREA\n";
                }
            }
            else {
                print "graph_vlabel packets\n";
                foreach my $gb (qw/good bad/) {
                    foreach my $direction (qw/tx rx/) {
                        print "p${port}${direction}${gb}.label Port $port $direction ($gb)\n";
                        print "p${port}${direction}${gb}.type COUNTER\n";
                    }
                }
            }
        }
    }
    exit 0;
}

# extract data from the switch - uses web scraping (tested with 1.0.2 Build 20160526 Rel.34615)

# print STDERR "Connecting to $host with user $username";
my $mech = WWW::Mechanize->new(
    autocheck             => 0,
    requests_redirectable => [ 'GET', 'HEAD', 'POST' ]
);
my $result = $mech->post(
    "http://$host:$tcpport/logon.cgi",
    [ username => $username, password => $password, logon => 'Login' ],
    "Referer" => "http://$host:$tcpport/"
);
my $response = $mech->response();

# navigate to the page with the network stats
$result   = $mech->get("http://$host:$tcpport/PortStatisticsRpm.htm");
$response = $mech->response();

#print STDERR $response->code()."\n";

# get the data
my $data = $mech->content( raw => 1 );

#print STDERR "$data\n";

# The page stores the data in a table, but internally it is stored in 3 javascript arrays:
# state:[1,1,1,1,1,1,1,1,0,0],
# link_status:[0,5,0,0,5,5,5,6,0,0],
# pkts:[0,0,0,0,14141090,0,10461386,0,14226,0,12252,0,0,0,0,0,2872063,0,1402200,0,59764503,0,34619246,0,4913873,0,4393574,0,44170456,0,68499653,0,0,0]

# state: 1 - Enabled, 0 - Disabled (administratively)
# link_status: 0 - down, 1 - auto, 2 - 10Mbps half, 3 - 10Mbps full, 4 - 100Mbps half, 5 - 100Mbps full, 6 - 1Gbps full
# pkts: every group of 4 values represent txGoodPkt, txBadPkt, rxGoodPkt, rxBadPkt

# parse good/bad packets
if ( $data =~ /pkts:\[([0-9,]+)\]/ ) {

    my $packetString = $1;
    my @packets = split( /,/, $packetString );

    for ( 1 .. $numberOfPorts ) {
        my $currentPort = $_;
        my $txGoodPkt   = $packets[ ( $currentPort - 1 ) * 4 ];
        my $txBadPkt    = $packets[ ( $currentPort - 1 ) * 4 + 1 ];
        my $rxGoodPkt   = $packets[ ( $currentPort - 1 ) * 4 + 2 ];
        my $rxBadPkt    = $packets[ ( $currentPort - 1 ) * 4 + 3 ];
        print "multigraph Packets_if_$currentPort\n";

        print "p${currentPort}txgood.value $txGoodPkt\n";
        print "p${currentPort}rxgood.value $rxGoodPkt\n";
        print "p${currentPort}txbad.value $txBadPkt\n";
        print "p${currentPort}rxbad.value $rxBadPkt\n";
    }
}

# parse link speed
if ( $data =~ /link_status:\[([0-9,]+)\]/ ) {

    my $linkString = $1;
    my @links = split( /,/, $linkString );
    for ( 1 .. $numberOfPorts ) {
        my $currentPort = $_;
        my $link        = $links[ $currentPort - 1 ];
        print "multigraph Speed_if_$currentPort\n";
        foreach my $value (sort keys %speedMapping){
            print "p${currentPort}_$value.value ".(($value eq $link)?1:0)."\n";
        }
    }

}

# vim: ft=perl : ts=4 : expandtab
