#!/usr/bin/env ruby
#
# Munin plugin for the D-link DIR-655 router
#
# This plugin can graph # of wifi clients, # of DHCP clients, collisions & errors for all network interfaces, dropped packets for all interfaces, and both transmit and receive rates for all interfaces of the router.
#
# Author:  David Reitz
#
# 1.  Copy script to a Linux server on your network.
# 2.  Create symlink. (ln -s /path/to/dlink_dir655 /etc/munin/plugins/dlink_dir655)
# 3.  Edit plugin configuration for this script (vi /etc/munin/plugin-conf.d/munin-node) and add:
#     [dlink_dir655]
#     host_name dir655
#     env.router_password password
#     env.router_ip_address 10.0.0.1
# 4.  Edit munin configuration to point to itself to gather the data from your router (vi /etc/munin/munin-conf.d/munin) and add:
#     [dir655]
#     address 127.0.0.1
#     use_node_name no
# 5.  Modify the 'password' and 'router_path' variables below to reflect the correct password for the 'User' user on your D-link DIR-655 router and to reflect the correct IP address of your router (this *MUST* be on your local subnet/LAN).
#
# NOTICE 1:  This is the first Ruby script I've written (as well as being my first Munin plugin), so I consider this a bit of a hack.  Just letting you know ahead of time.  :)
#
# NOTICE 2:  I provide this script *as-is*.  Use at your own risk!  I thought other people may find this script useful/interesting but have no plans to support it for anyone else.
#
# Many thanks go to Clarke Brunsdon for his code listed at http://clarkebrunsdon.com/2010/12/the-dlink-dir-655-hash-changes/!!!  This really helped me out!
#
# Original Implementation:  13 Jan 2011
gem 'mechanize'
require 'mechanize'
require 'digest/md5'
require 'nokogiri'


def output
  nics = Hash.new
  nics["LAN"] = Hash.new
  nics["WAN"] = Hash.new
  nics["WLAN"] = Hash.new
  password = ENV['router_password'] || ""
  router_path = ENV['router_ip_address'] || "10.0.0.1"
  router_path = "http://" + router_path
  agent = Mechanize.new
  x = agent.get(router_path)
  salt = x.body.match(/salt = "(.*)"/)[1]

  # pad the pasword to length 16
  pad_size = (16 - password.length)
  padded_password = password + "\x01" * pad_size

  # pad it the rest of the way, length 64 for user
  salted_password = salt + padded_password + ("\x01" * (63 - salt.length - padded_password.length)) + "U"
  login_hash = salt + Digest::MD5.hexdigest(salted_password)

  # authenticate against the router using the hash that we just built
  login_path = "#{router_path}/post_login.xml?hash=#{login_hash}"
  x = agent.get(login_path)

  # grab the statistics for all interfaces and parse it into a usable form
  clients_xml = agent.get("#{router_path}/interface_stats.xml").body
  doc = Nokogiri::XML(clients_xml.to_s)
  doc.xpath('//interface').each do |interface|
    children = interface.children
    name = children.search('name')[0].text
    nics[name]["packets_sent"] = children.search('packets_sent')[0].text
    nics[name]["packets_received"] = children.search('packets_received')[0].text
    nics[name]["tx_dropped"] = children.search('tx_dropped')[0].text
    begin
      nics[name]["tx_collisions"] = children.search('tx_collisions')[0].text
    rescue Exception
      nics[name]["tx_collisions"] = "0"
    end
    nics[name]["rx_dropped"] = children.search('rx_dropped')[0].text
    nics[name]["rx_errors"] = children.search('rx_errors')[0].text
  end

  # get wifi associations and print out info for munin graph
  puts "multigraph clients"
  clients_xml = agent.get("#{router_path}/wifi_assoc.xml").body
  j = 0
  doc = Nokogiri::XML(clients_xml.to_s)
  doc.xpath('//assoc').each do |assoc|
    j+=1
  end
  puts "wifi_assoc.value " + j.to_s

  # get dhcp clients and print out info for munin graph
  clients_xml = agent.get("#{router_path}/dhcp_clients.xml").body
  j = 0
  doc = Nokogiri::XML(clients_xml.to_s)
  doc.xpath('//client').each do |client|
    j+=1
  end
  puts "dhcp_clients.value " + j.to_s

  puts "multigraph uptime"
  # get uptime of connection
  clients_xml = agent.get("#{router_path}/wan_connection_status.xml").body
  doc = Nokogiri::XML(clients_xml.to_s)
  uptime = doc.children.search('wan_interface_up_time_0')[0].text
  puts "uptime.value " + sprintf( "%.2f", (Float(uptime)/86400) )

  # graph overall interface packets transferred per interval
  puts "multigraph if_packets"
  for i in [ "LAN", "WAN", "WLAN" ] do
    puts "#{i}_recv.value " + nics[i]["packets_received"]
    puts "#{i}_send.value " + nics[i]["packets_sent"]
  end

  # graph overall interface dropped packets per interval
  puts "multigraph if_drop"
  for i in [ "LAN", "WAN", "WLAN" ] do
    puts "#{i}_recv.value " + nics[i]["rx_dropped"]
    puts "#{i}_send.value " + nics[i]["tx_dropped"]
  end

  # graph overall interface collisions & errors per interval
  puts "multigraph if_collerr"
  for i in [ "LAN", "WAN", "WLAN" ] do
    puts "#{i}_coll.value " + nics[i]["tx_collisions"]
    puts "#{i}_err.value " + nics[i]["rx_errors"]
  end

  # graph stats for each interface
  for i in [ "LAN", "WAN", "WLAN" ] do
    puts "multigraph if_packets.#{i}"
    puts "send.value " + nics[i]["packets_sent"]
    puts "recv.value " + nics[i]["packets_received"]
    puts "multigraph if_drop.#{i}"
    puts "send.value " + nics[i]["tx_dropped"]
    puts "recv.value " + nics[i]["rx_dropped"]
    puts "multigraph if_collerr.#{i}"
    puts "coll.value " + nics[i]["tx_collisions"]
    puts "err.value " + nics[i]["rx_errors"]
  end
end


def config
  # build the configuration for graphs
  puts "multigraph if_packets"
  puts 'graph_title D-Link DIR-655 interface traffic'
  puts 'graph_category network'
  puts 'graph_order LAN_recv LAN_send WAN_recv WAN_send WLAN_recv WLAN_send'
  puts 'graph_vlabel packets in (-) / out (+) per ${graph_period}'
  for i in [ "LAN", "WAN", "WLAN" ] do
    puts "#{i}_recv.type DERIVE"
    puts "#{i}_recv.graph no"
    puts "#{i}_recv.min 0"
    puts "#{i}_send.label #{i}"
    puts "#{i}_send.type DERIVE"
    puts "#{i}_send.negative #{i}_recv"
    puts "#{i}_send.min 0"
  end

  puts "multigraph if_drop"
  puts 'graph_title D-Link DIR-655 interface drops'
  puts 'graph_category network'
  puts 'graph_order LAN_recv LAN_send WAN_recv WAN_send WLAN_recv WLAN_send'
  puts 'graph_vlabel packets / ${graph_period}'
  for i in [ "LAN", "WAN", "WLAN" ] do
    puts "#{i}_recv.type DERIVE"
    puts "#{i}_recv.graph no"
    puts "#{i}_recv.min 0"
    puts "#{i}_send.label #{i}"
    puts "#{i}_send.type DERIVE"
    puts "#{i}_send.negative #{i}_recv"
    puts "#{i}_send.min 0"
  end

  puts "multigraph if_collerr"
  puts 'graph_title D-Link DIR-655 interface collisions & errors'
  puts 'graph_category network'
  puts 'graph_order LAN_coll LAN_err WAN_coll WAN_err WLAN_coll WLAN_coll'
  puts 'graph_vlabel packets / ${graph_period}'
  for i in [ "LAN", "WAN", "WLAN" ] do
    puts "#{i}_coll.label #{i} collisions"
    puts "#{i}_coll.type DERIVE"
    puts "#{i}_coll.min 0"
    puts "#{i}_err.label #{i} errors"
    puts "#{i}_err.type DERIVE"
    puts "#{i}_err.min 0"
  end

  puts "multigraph clients"
  puts "graph_title D-Link DIR-655 client information"
  puts "graph_category system"
  puts "graph_order dhcp_clients wifi_assoc"
  puts "graph_vlabel number of clients"
  puts "dhcp_clients.label DHCP clients"
  puts "dhcp_clients.type GAUGE"
  puts "dhcp_clients.min 0"
  puts "wifi_assoc.label wifi clients"
  puts "wifi_assoc.type GAUGE"
  puts "wifi_assoc.min 0"

  puts "multigraph uptime"
  puts "graph_title Uptime"
  puts 'graph_vlabel uptime in days'
  puts 'graph_category system'
  puts 'uptime.label uptime'
  puts 'uptime.draw AREA'

  for i in [ "LAN", "WAN", "WLAN" ] do
    puts "multigraph if_packets.#{i}"
    puts "graph_title D-Link DIR-655 #{i} traffic"
    puts 'graph_category network'
    puts 'graph_order recv send'
    puts 'graph_vlabel packets in (-) / out (+) per ${graph_period}'
    puts 'recv.label received'
    puts 'recv.type DERIVE'
    puts 'recv.graph no'
    puts 'recv.min 0'
    puts 'send.label packets/sec'
    puts 'send.type DERIVE'
    puts 'send.negative recv'
    puts 'send.min 0'

    puts "multigraph if_drop.#{i}"
    puts "graph_title D-Link DIR-655 #{i} drops"
    puts 'graph_category network'
    puts 'graph_order recv send'
    puts 'graph_vlabel packets / ${graph_period}'
    puts 'recv.label RX packets dropped'
    puts 'recv.type DERIVE'
    puts 'recv.graph no'
    puts 'recv.min 0'
    puts 'send.label TX packets dropped'
    puts 'send.type DERIVE'
    puts 'send.negative recv'
    puts 'send.min 0'

    puts "multigraph if_collerr.#{i}"
    puts "graph_title D-Link DIR-655 #{i} collisions & errors"
    puts 'graph_category network'
    puts 'graph_order coll err'
    puts 'graph_vlabel packets / ${graph_period}'
    puts 'coll.label collisions'
    puts 'coll.type DERIVE'
    puts 'coll.min 0'
    puts 'err.label errors'
    puts 'err.type DERIVE'
    puts 'err.min 0'
  end
end


# main
if ARGV.length == 1 and ARGV[0] == 'config'
  config()
else
  output()
end
