Permalink
Browse files

Initial commit of apocalypse-client

  • Loading branch information...
0 parents commit 5c0d917ca090e44427b8a5b6d9933936f2bcd196 @ariejan committed Jul 5, 2011
Showing with 347 additions and 0 deletions.
  1. +4 −0 .gitignore
  2. +1 −0 .rspec
  3. +4 −0 Gemfile
  4. +9 −0 Rakefile
  5. +25 −0 apocalypse-client.gemspec
  6. +37 −0 bin/apocalypse-client
  7. +133 −0 lib/apocalypse-client.rb
  8. +5 −0 lib/apocalypse-client/version.rb
  9. +125 −0 spec/client_spec.rb
  10. +4 −0 spec/spec_helper.rb
@@ -0,0 +1,4 @@
+*.gem
+.bundle
+Gemfile.lock
+pkg/*
1 .rspec
@@ -0,0 +1 @@
+--colour -f progress
@@ -0,0 +1,4 @@
+source "http://rubygems.org"
+
+# Specify your gem's dependencies in apocalypse-reporter.gemspec
+gemspec
@@ -0,0 +1,9 @@
+require 'bundler/gem_tasks'
+
+task :default => :spec
+
+desc "Run all specs"
+task "spec" do
+ exec "bundle exec rspec spec"
+end
+
@@ -0,0 +1,25 @@
+# -*- encoding: utf-8 -*-
+$:.push File.expand_path("../lib", __FILE__)
+require "apocalypse-client/version"
+
+Gem::Specification.new do |s|
+ s.name = "apocalypse-client"
+ s.version = Apocalypse::Client::VERSION
+ s.authors = ["Ariejan de Vroom"]
+ s.email = ["ariejan@ariejan.net"]
+ s.homepage = ""
+ s.summary = %q{Watch out for the apocalypse}
+ s.description = %q{Server monitoring made easy}
+
+ s.rubyforge_project = "apocalypse-client"
+
+ s.files = `git ls-files`.split("\n")
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
+ s.require_paths = ["lib"]
+
+ s.add_dependency("json", ["~> 1.5.3"])
+ s.add_dependency("trollop", ["~> 1.16.2"])
+
+ s.add_development_dependency("rspec", ["~> 2.6.0"])
+end
@@ -0,0 +1,37 @@
+#!/usr/bin/env ruby
+
+lib = File.expand_path(File.dirname(__FILE__) + '/../lib')
+$LOAD_PATH.unshift(lib) if File.directory?(lib) && !$LOAD_PATH.include?(lib)
+
+require "apocalypse-reporter"
+require 'trollop'
+
+SUB_COMMANDS = %w(report install check)
+
+global_opts = Trollop::options do
+ banner "Apocalypse Reporter"
+ stop_on SUB_COMMANDS
+end
+
+cmd = ARGV.shift
+cmd_opts = case cmd
+ when "report"
+ Trollop::options do
+ opt :server, "Hostname of the Apocalypse Server", :type => :string, :short => "-s"
+ opt :port, "Port the Apocalypse Server is listening on. Default: 80", :type => :string, :default => '80', :short => "-p"
+ opt :hostid, "Identifier for this server", :type => :string, :short => "-i"
+ end
+ when "install"
+ Trollop::options do
+ # TODO
+ end
+ when "check"
+ Trollop::options do
+ # TODO
+ end
+ else
+ Trollop::die "unknown command #{cmd.inspect}"
+ end
+
+reporter = Apocalypse::Client.new
+reporter.send(cmd, cmd_opts)
@@ -0,0 +1,133 @@
+require 'rubygems'
+require 'net/http'
+require 'json'
+
+class Hash
+ #
+ # Create a hash from an array of keys and corresponding values.
+ def self.zip(keys, values, default=nil, &block)
+ hsh = block_given? ? Hash.new(&block) : Hash.new(default)
+ keys.zip(values) { |k,v| hsh[k]=v }
+ hsh
+ end
+end
+
+module Apocalypse
+ class Client
+ ## Commands
+
+ # Report metrics
+ def report(options)
+ puts options.inspect
+ request = Net::HTTP::Post.new("/api/metrics/#{options[:hostid]}", initheader = {'Content-Type' =>'application/json'})
+ request.body = gather_metrics.to_json
+ response = Net::HTTP.new(options[:server], options[:port]).start {|http| http.request(request) }
+ puts "Response #{response.code} #{response.message}: #{response.body}"
+ end
+
+ # Check if all local deps are available
+ def check(options)
+ errors = []
+ [
+ ["/usr/bin/iostat", "sysstat"]
+ ].each do |filename, package|
+ errors << "Cannot find `#{filename}`. Please run `apt-get install #{package}` to resolve this." if !File.exists?(filename)
+ end
+
+ if errors.empty?
+ puts "Everything seems to be in place. You're good to go."
+ else
+ errors.each { |error| puts errors }
+ end
+ end
+
+ def install(options)
+ puts <<-EOF
+Sorry, no automated installation yet. Installation is, however, quite easy.
+
+Simply create the file `/etc/cron.d/apocalypse` and put the following line in it:
+
+* * * * * root PATH=$PATH:/sbin:/usr/sbin /usr/bin/env apocalypse-reporter report --server APOCALYPSE_SERVER --hostid SERVER_ID > /dev/null
+
+Make sure to replace the server and hostid placeholders with something more useful.
+ EOF
+ end
+
+ # Gather metrics
+ def gather_metrics
+ {
+ 'cpu' => {
+ 'cores' => cpu_cores.strip,
+ 'loadavg' => cpu_loadavg
+ },
+ 'memory' => memory_metrics,
+ 'swap' => swap_metrics,
+ 'blockdevices' => blockdevice_metrics,
+ 'network' => network_metrics
+ }
+ end
+
+ # Returns the number of CPU Cores for this system
+ def cpu_cores
+ `cat /proc/cpuinfo | grep bogomips | wc -l`.strip
+ end
+
+ # Gather load average data
+ def cpu_loadavg
+ `cat /proc/loadavg`.split
+ end
+
+ def blockdevice_metrics
+ columns = ["tps", "rps", "wps", "size", "used", "available", "usage", "mount"]
+
+ io_data = `/usr/bin/env iostat -dk | tail -n+4`.split("\n").map { |line| line.split[0...-2] }
+ usage_data = `df -l --block-size=M | grep -i ^/dev/[sh]`.split("\n").map { |line| line.split }
+
+ io_data.collect do |device_data|
+ device_name = device_data.shift
+ device_usage = usage_data.select { |x| x[0].include? device_name }.flatten
+
+ unless device_usage.empty?
+ device_data += device_usage[1..-1]
+ end
+
+ { device_name => Hash.zip(columns, device_data) }
+ end
+ end
+
+ def memory_metrics
+ {
+ 'free' => `cat /proc/meminfo | grep MemFree | awk '{print $2}'`.strip,
+ 'total' => `cat /proc/meminfo | grep MemTotal | awk '{print $2}'`.strip
+ }
+ end
+
+ def swap_metrics
+ {
+ 'free' => `cat /proc/meminfo | grep SwapFree | awk '{print $2}'`.strip,
+ 'total' => `cat /proc/meminfo | grep SwapTotal | awk '{print $2}'`.strip
+ }
+ end
+
+ def network_metrics
+ devices = `/usr/bin/env ifconfig | egrep 'Link encap' | grep -v 'lo' | awk '{print $1}'`.split
+
+ devices.collect do |device_name|
+ { device_name => {
+ 'hwaddr' => `/usr/bin/env ifconfig #{device_name} | egrep -o 'HWaddr \([0-9a-fA-F]\{2\}\:*\){6}' | awk '{print $2}'`.strip,
+ 'mtu' => `/usr/bin/env ifconfig #{device_name} | egrep -o MTU\:[0-9]+ | tr -s ':' ' ' | awk '{print $2}'`.strip,
+ 'metric' => `/usr/bin/env ifconfig #{device_name} | egrep -o Metric\:[0-9]+ | tr -s ':' ' ' | awk '{print $2}'`.strip,
+ 'encapsulation' => `/usr/bin/env ifconfig #{device_name} | egrep -o 'Link encap\:[a-zA-Z]+' | cut -d":" -f2`.strip,
+ 'rxbytes' => `/usr/bin/env ifconfig #{device_name} | grep bytes | awk '/RX/ {print $2}' | tr -s ':' ' ' | awk '{print $2}'`.strip,
+ 'txbytes' => `/usr/bin/env ifconfig #{device_name} | grep bytes | awk '/TX/ {print $6}' | tr -s ':' ' ' | awk '{print $2}'`.strip,
+ 'ipv4address' => `/usr/bin/env ifconfig #{device_name} | egrep -o 'inet addr\:[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}' | tr -s ':' ' ' | awk '{print $3}'`.strip,
+ 'broadcast' => `/usr/bin/env ifconfig #{device_name} | egrep -o Bcast\:[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\} | tr -s ':' ' ' | awk '{print $2}'`.strip,
+ 'netmask' => `/usr/bin/env ifconfig #{device_name} | egrep -o Mask\:[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\} | tr -s ':' ' ' | awk '{print $2}'`.strip,
+ 'gateway' => `netstat -rn | grep ^0.0.0.0 | grep UG | grep #{device_name} | awk '{print $2}' | head -n1`.strip,
+ 'ipv6addr' => `/usr/bin/env ifconfig #{device_name} | egrep -o 'inet6 addr\:\ \([a-fA-F0-9]\{1,4}\:\{1,2\}[a-fA-F0-9]\{1,4}\:\{1,2\}\)+[A-Fa-f0-9\/^\ ]+' | awk '{print $3}'`.strip,
+ 'ipv6scope' => `/usr/bin/env ifconfig #{device_name} | egrep -o Scope\:[a-zA-Z]+ | cut -d":" -f2`.strip
+ }}
+ end
+ end
+ end
+end
@@ -0,0 +1,5 @@
+module Apocalypse
+ class Client
+ VERSION = "0.0.1"
+ end
+end
@@ -0,0 +1,125 @@
+require 'spec_helper'
+
+describe Apocalypse::Client do
+ before(:each) do
+ @reporter = Apocalypse::Client.new
+ end
+
+ describe "cpu cores" do
+ it "should return the correct number of cores" do
+ @reporter.should_receive(:`).with("cat /proc/cpuinfo | grep bogomips | wc -l").and_return("4")
+ @reporter.cpu_cores.should eql("4")
+ end
+ end
+
+ describe "cpu loadavg" do
+ it "should return the correct loadavg values" do
+ @reporter.should_receive(:`).with("cat /proc/loadavg").and_return("0.89 0.92 1.12 1/124 28786")
+ @reporter.cpu_loadavg.should eql(["0.89", "0.92", "1.12", "1/124", "28786"])
+ end
+ end
+
+ describe "memory" do
+ it "should gather free and used memory" do
+ @reporter.should_receive(:`).with("cat /proc/meminfo | grep MemFree | awk '{print $2}'").and_return("998452")
+ @reporter.should_receive(:`).with("cat /proc/meminfo | grep MemTotal | awk '{print $2}'").and_return("7864548")
+
+ memory_metrics = @reporter.memory_metrics
+ memory_metrics["free"].should eql("998452")
+ memory_metrics["total"].should eql("7864548")
+ end
+ end
+
+ describe "swap" do
+ it "should gather free and used swap" do
+ @reporter.should_receive(:`).with("cat /proc/meminfo | grep SwapFree | awk '{print $2}'").and_return("934452")
+ @reporter.should_receive(:`).with("cat /proc/meminfo | grep SwapTotal | awk '{print $2}'").and_return("15728632")
+
+ swap_metrics = @reporter.swap_metrics
+ swap_metrics["free"].should eql("934452")
+ swap_metrics["total"].should eql("15728632")
+ end
+ end
+
+ describe "networking" do
+ it "should get network metrics" do
+ {
+ "/usr/bin/env ifconfig | egrep 'Link encap' | grep -v 'lo' | awk '{print $1}'" => "eth0",
+ "/usr/bin/env ifconfig eth0 | egrep -o 'HWaddr \([0-9a-fA-F]\{2\}\:*\){6}' | awk '{print $2}'" => "bc:de:12:ba:38:b3",
+ "/usr/bin/env ifconfig eth0 | egrep -o MTU\:[0-9]+ | tr -s ':' ' ' | awk '{print $2}'" => "1500",
+ "/usr/bin/env ifconfig eth0 | egrep -o Metric\:[0-9]+ | tr -s ':' ' ' | awk '{print $2}'" => "1",
+ "/usr/bin/env ifconfig eth0 | egrep -o 'Link encap\:[a-zA-Z]+' | cut -d\":\" -f2" => "Ethernet",
+ "/usr/bin/env ifconfig eth0 | grep bytes | awk '/RX/ {print $2}' | tr -s ':' ' ' | awk '{print $2}'" => "384735745",
+ "/usr/bin/env ifconfig eth0 | grep bytes | awk '/TX/ {print $6}' | tr -s ':' ' ' | awk '{print $2}'" => "3575372573",
+ "/usr/bin/env ifconfig eth0 | egrep -o 'inet addr\:[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}' | tr -s ':' ' ' | awk '{print $3}'" => "10.187.234.18",
+ "/usr/bin/env ifconfig eth0 | egrep -o Bcast\:[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\} | tr -s ':' ' ' | awk '{print $2}'" => "10.187.234.254",
+ "/usr/bin/env ifconfig eth0 | egrep -o Mask\:[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\} | tr -s ':' ' ' | awk '{print $2}'" => "255.0.0.0",
+ "netstat -rn | grep ^0.0.0.0 | grep UG | grep eth0 | awk '{print $2}' | head -n1" => "10.187.234.1",
+ "/usr/bin/env ifconfig eth0 | egrep -o 'inet6 addr\:\ \([a-fA-F0-9]\{1,4}\:\{1,2\}[a-fA-F0-9]\{1,4}\:\{1,2\}\)+[A-Fa-f0-9\/^\ ]+' | awk '{print $3}'" => "fe80::1031:3cff:fe00:bde1/64",
+ "/usr/bin/env ifconfig eth0 | egrep -o Scope\:[a-zA-Z]+ | cut -d\":\" -f2" => "Link"
+ }.each do |command, result|
+ @reporter.should_receive(:`).with(command).and_return(result)
+ end
+
+ network_metrics = @reporter.network_metrics.first["eth0"]
+
+ network_metrics["hwaddr"].should eql("bc:de:12:ba:38:b3")
+ network_metrics["mtu"].should eql("1500")
+ network_metrics["metric"].should eql("1")
+ network_metrics["encapsulation"].should eql("Ethernet")
+ network_metrics["rxbytes"].should eql("384735745")
+ network_metrics["txbytes"].should eql("3575372573")
+ network_metrics["ipv4address"].should eql("10.187.234.18")
+ network_metrics["broadcast"].should eql("10.187.234.254")
+ network_metrics["netmask"].should eql("255.0.0.0")
+ network_metrics["gateway"].should eql("10.187.234.1")
+ network_metrics["ipv6addr"].should eql("fe80::1031:3cff:fe00:bde1/64")
+ network_metrics["ipv6scope"].should eql("Link")
+ end
+ end
+
+ describe "block devices" do
+ it "should gather the correct block device info" do
+ iostat = <<-EOF
+sda1 0.21 1.95 2.35 157872 189884
+sda2 5.74 18.53 22.08 1496597 1783516
+
+EOF
+
+ df = <<-EOF
+/dev/sda2 29529M 7506M 20524M 27% /
+EOF
+
+ @reporter.should_receive(:`).with("/usr/bin/env iostat -dk | tail -n+4").and_return(iostat)
+ @reporter.should_receive(:`).with("df -l --block-size=M | grep -i ^/dev/[sh]").and_return(df)
+
+ result = @reporter.blockdevice_metrics
+
+ result.size.should eql(2)
+
+ # sda1
+ sda1 = result.select{ |a| a.keys.include?("sda1") }.first["sda1"]
+
+ sda1["tps"].should eql("0.21")
+ sda1["rps"].should eql("1.95")
+ sda1["wps"].should eql("2.35")
+ sda1["size"].should be_nil
+ sda1["usage"].should be_nil
+ sda1["used"].should be_nil
+ sda1["available"].should be_nil
+ sda1["mount"].should be_nil
+
+ # sda2
+ sda2 = result.select{ |a| a.keys.include?("sda2") }.first["sda2"]
+
+ sda2["tps"].should eql("5.74")
+ sda2["rps"].should eql("18.53")
+ sda2["wps"].should eql("22.08")
+ sda2["size"].should eql("29529M")
+ sda2["usage"].should eql("27%")
+ sda2["used"].should eql("7506M")
+ sda2["available"].should eql("20524M")
+ sda2["mount"].should eql("/")
+ end
+ end
+end
@@ -0,0 +1,4 @@
+require 'apocalypse-client'
+
+require 'rspec'
+require 'json'

0 comments on commit 5c0d917

Please sign in to comment.