#!/usr/bin/ruby

require 'yaml'

## Simple wrapper around NIC detection and speed.
class NetworkInterface
  #
  # does the given NIC exist?
  #
  def self.exists?(interface)
    File.exist?("/sys/class/net/#{interface}")
  end

  #
  # What is the speed?
  #
  def self.speed(interface)
    speed = nil
    file = nil
    file = '/sbin/ethtool' if File.exist?('/sbin/ethtool')
    file = '/usr/sbin/ethtool' if File.exist?('/usr/sbin/ethtool')

    if file.nil?
      puts 'ethtool not found!!'
      exit 1
    end

    `#{file} #{interface}`.split("\n").each do |line|
      next unless line =~ /Speed: ([0-9]+)/
      speed = Regexp.last_match(1).dup
      speed.strip!
      speed = speed.to_i
    end
    speed
  end
end

##  Simpler wrapper for making queries against a bonded interface
class Bonding
  #
  # Does bonding exist?
  #
  def exists?
    return false unless File.directory?('/proc/net/bonding')
    return false unless NetworkInterface.exists?('bond0')

    res = true

    f = File.open('/proc/net/bonding/bond0', 'r')
    while (line = f.gets)
      res = false if line =~ /MII Status: down/
    end

    res
  end

  #
  # Get the name of bonded interfaces
  #
  def interfaces
    bonds = []

    Dir.foreach('/proc/net/bonding/') do |item|
      bonds.push(item) if item =~ /bond/
    end

    bonds
  end

  #
  #  Get the bonding mode
  #
  def mode(name)
    mode = 'unknown'

    File.readlines("/proc/net/bonding/#{name}").each do |line|
      mode = Regexp.last_match(1).dup if line =~ /Bonding Mode: (.*)/
    end

    verbose("Bonding for #{name} is in mode: #{mode}")

    mode
  end

  #
  # which slave is active for the named bonded interface?
  #
  def active_slave(name)
    active = nil

    File.readlines("/proc/net/bonding/#{name}").each do |line|
      if line =~ /Currently Active Slave:(.*)/
        active = Regexp.last_match(1).dup
        active.strip!
      end
    end
    active
  end

  #
  # what are the slave names?
  #
  def slaves(name)
    active = []

    File.readlines("/proc/net/bonding/#{name}").each do |line|
      next unless line =~ /Slave Interface:(.*)/
      nic = Regexp.last_match(1).dup
      nic.strip!
      active.push(nic)
    end
    active
  end
end

#
#  Entry point to our code.
#
if __FILE__ == $PROGRAM_NAME

  def verbose(str)
    STDERR.puts(str)
  end

  if !File.exist?('/usr/sbin/ethtool') &&
     !File.exist?('/sbin/ethtool')

    h = {}
    h[:id] = "bonding-#{bond}-normal"
    h[:summary] = 'missing tool(s)'
    h[:detail] = "<p>'ethtool' is not present upon this host.</p>"
    to_raise.push(h)

    puts YAML.dump(to_raise)
    exit 0
  end

  #
  #  The alerts we'll raise
  #
  to_raise = []

  #
  #  Create the helper, and exit if we're not running with bonding.
  #
  bonding = Bonding.new
  unless bonding.exists?
    verbose('Bonding is not in effect')
    puts YAML.dump(to_raise)
    exit 0
  end

  #
  #  Now we want to iterate over each bonded interface
  #
  bonding.interfaces.each do |bond|
    verbose("Processing: #{bond}")

    mode = bonding.mode(bond)

    if mode =~ /backup/i
      #
      #  The best interface
      #
      best = bonding.slaves(bond)[0]

      #
      #  For each slave get the speed - if the speed is better than what
      # we've found thus far update the best interface.
      #
      bonding.slaves(bond).each do |nic|
        # Get the speed of the proposed bonded member.
        speed = NetworkInterface.speed(nic)
        next if speed.nil?

        # get the current best speed, and update if we found better.
        current = NetworkInterface.speed(best)
        current = 0 if current.nil?

        best = nic if current < speed
      end

      #
      # OK so we've found the best interface
      #
      best_speed   = NetworkInterface.speed(best)
      active_speed = NetworkInterface.speed(bonding.active_slave(bond))

      verbose("Best speed is #{best_speed}")
      verbose("Currently active speed is #{active_speed}")

      #
      #  OK by this point we've found the best NIC, does that match what
      # is the currently active bonding slave?  If not we should alert.
      #
      if File.exist?('/etc/bonding.sane') ||
         (best_speed == active_speed) ||
         bonding.slaves(bond).empty?
        verbose("#{bond} is OK.  Not raising alert")
      else
        verbose("Raising alert for #{bond}")

        h = {}
        h[:id] = "bonding-#{bond}-normal"
        h[:summary] = "Bonding for #{bond} is not setup correctly."
        h[:detail] = "<p>Bonded interface #{bond} should have #{best} as the master, but it doesn't.</p>"
        to_raise.push(h)
      end

    elsif mode == 'IEEE 802.3ad Dynamic link aggregation'

      bond_info = File.read("/proc/net/bonding/#{bond}")
      slaves = bond_info.scan(/Slave Interface: (.*?)\nMII Status: (.*)\n/)
      slaves_down = slaves.map { |slave| slave[0] if slave[1] == 'down' }.compact.uniq

      if slaves_down.count > 0
        h = {}
        h[:id] = "bonding-slave-down-#{bond}"
        h[:summary] = "The interface #{bond} has slave(s) down."
        h[:detail] =  "<p>The bonded interface #{bond} has one or more slaves down: #{slaves_down.join(', ')}.</p>"
        to_raise.push(h)
      end

    else
      h = {}
      h[:id] = "bonding-mode-#{bond}-normal"
      h[:summary] = "The interface #{bond} has the wrong mode."
      h[:detail] =  "<p>The bonded interface #{bond} has the wrong mode '#{mode}', it should be set to 'backup'.</p>"
      to_raise.push(h)
    end
  end

  puts YAML.dump(to_raise)
  exit 0

end
