silw 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +44 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +119 -0
- data/Rakefile +8 -0
- data/SILW_server_page_example.png +0 -0
- data/bin/silw +21 -0
- data/lib/silw.rb +35 -0
- data/lib/silw/agent.rb +17 -0
- data/lib/silw/cli.rb +165 -0
- data/lib/silw/command.rb +25 -0
- data/lib/silw/helpers.rb +54 -0
- data/lib/silw/plugin.rb +19 -0
- data/lib/silw/plugins/cpu.rb +55 -0
- data/lib/silw/plugins/diskio.rb +70 -0
- data/lib/silw/plugins/mem.rb +43 -0
- data/lib/silw/server.rb +46 -0
- data/lib/silw/version.rb +3 -0
- data/lib/silw/views/index.haml +30 -0
- data/lib/silw/views/layout.haml +50 -0
- data/lib/silw/views/partials/_cpu.haml +9 -0
- data/lib/silw/views/partials/_diskio.haml +12 -0
- data/lib/silw/views/partials/_mem.haml +12 -0
- data/silw.gemspec +46 -0
- data/spec/fixtures/cpu_t0.txt +11 -0
- data/spec/fixtures/cpu_t1.txt +11 -0
- data/spec/fixtures/diskstats.txt +48 -0
- data/spec/fixtures/mem.txt +25 -0
- data/spec/fixtures/silw.yaml +17 -0
- data/spec/fixtures/silw_with_logstash.yaml +22 -0
- data/spec/fixtures/top.txt +0 -0
- data/spec/silw_spec.rb +59 -0
- data/spec/spec_helper.rb +15 -0
- metadata +368 -0
data/lib/silw/command.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
module Silw
|
2
|
+
class Command
|
3
|
+
def initialize(auth_options={})
|
4
|
+
@username = auth_options[:authentication][:username]
|
5
|
+
@password = auth_options[:authentication][:password]
|
6
|
+
pub_key = File.expand_path(@password)
|
7
|
+
if File.exists?(pub_key)
|
8
|
+
@pub_key = pub_key
|
9
|
+
end
|
10
|
+
raise RuntimeError unless @password or @pub_key
|
11
|
+
end
|
12
|
+
|
13
|
+
# see: http://ruby-metaprogramming.rubylearning.com/html/ruby_metaprogramming_3.html
|
14
|
+
def run(command, remotes={})
|
15
|
+
plugin = Kernel.const_get("Silw::Plugins::#{command.capitalize}").new
|
16
|
+
plugin.instance_variable_set(:@username, @username)
|
17
|
+
if @pub_key
|
18
|
+
plugin.instance_variable_set(:@pub_key, @pub_key)
|
19
|
+
else
|
20
|
+
plugin.instance_variable_set(:@password, @password)
|
21
|
+
end
|
22
|
+
plugin.run(remotes)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/silw/helpers.rb
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
module Silw
|
2
|
+
module Helpers
|
3
|
+
def h(*args)
|
4
|
+
escape_html(*args)
|
5
|
+
end
|
6
|
+
|
7
|
+
def find_template(views, name, engine, &block)
|
8
|
+
Array(views).each { |v| super(v, name, engine, &block) }
|
9
|
+
end
|
10
|
+
|
11
|
+
def base_url
|
12
|
+
"#{env['rack.url_scheme']}://#{env['HTTP_HOST']}/"
|
13
|
+
end
|
14
|
+
|
15
|
+
def partial(thing, locals = {})
|
16
|
+
name = case thing
|
17
|
+
when String then thing
|
18
|
+
else thing.class.to_s.demodulize.underscore
|
19
|
+
end
|
20
|
+
haml :"partials/_#{name}", :locals => { name.to_sym => thing }.merge(locals)
|
21
|
+
end
|
22
|
+
|
23
|
+
def url_for(thing, options = {})
|
24
|
+
url = thing.respond_to?(:to_url) ? thing.to_url : thing.to_s
|
25
|
+
url = "#{base_url.sub(/\/$/, '')}#{url}" if options[:absolute]
|
26
|
+
url
|
27
|
+
end
|
28
|
+
|
29
|
+
def production?
|
30
|
+
settings.environment.to_sym == :production
|
31
|
+
end
|
32
|
+
|
33
|
+
def user_logged_in?
|
34
|
+
session[:auth].present?
|
35
|
+
end
|
36
|
+
|
37
|
+
def link_to(title, target = "", options = {})
|
38
|
+
options[:href] = target.respond_to?(:to_url) ? target.to_url : target
|
39
|
+
options[:data] ||= {}
|
40
|
+
[:method, :confirm].each { |a| options[:data][a] = options.delete(a) }
|
41
|
+
haml "%a#{options} #{title}"
|
42
|
+
end
|
43
|
+
|
44
|
+
def like_filesize(nr)
|
45
|
+
{
|
46
|
+
'B' => 1024,
|
47
|
+
'KB' => 1024 * 1024,
|
48
|
+
'MB' => 1024 * 1024 * 1024,
|
49
|
+
'GB' => 1024 * 1024 * 1024 * 1024,
|
50
|
+
'TB' => 1024 * 1024 * 1024 * 1024 * 1024
|
51
|
+
}.each_pair{|e, s| return "#{(nr.to_f / (s / 1024)).round(2)}#{e}" if nr < s }
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
data/lib/silw/plugin.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require "silw/command"
|
2
|
+
|
3
|
+
module Silw
|
4
|
+
class Plugin < Command
|
5
|
+
attr :username, :password, :pub_key
|
6
|
+
|
7
|
+
def run
|
8
|
+
puts "implement me"
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.inherited(base)
|
12
|
+
subclasses << base
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.subclasses
|
16
|
+
@subclasses ||= []
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'net/sftp'
|
2
|
+
|
3
|
+
# CPU info - report the total CPU usage for a remote system.
|
4
|
+
# For more details about 'proc/stat' see:
|
5
|
+
# - http://www.linuxhowtos.org/System/procstat.htm
|
6
|
+
#
|
7
|
+
# @return - a JSON containing the total CPU usage as a percentage
|
8
|
+
module Silw
|
9
|
+
module Plugins
|
10
|
+
class Cpu
|
11
|
+
attr_accessor :cpu_stats
|
12
|
+
CpuStats = Struct.new :user, :nice, :system, :idle, :iowait,
|
13
|
+
:irq, :softirq, :steal, :guest, :guest_nice,
|
14
|
+
:total
|
15
|
+
|
16
|
+
def run(args)
|
17
|
+
host = args[:at]
|
18
|
+
|
19
|
+
if fixture_cpu_at_t0 = args[:fixture_cpu_at_t0] and fixture_cpu_at_t1 = args[:fixture_cpu_at_t1]
|
20
|
+
cpu_stats = parse_stats File.read(fixture_cpu_at_t0), File.read(fixture_cpu_at_t1)
|
21
|
+
else
|
22
|
+
cpu_at_t0 = get_stat(host)
|
23
|
+
sleep 0.5
|
24
|
+
cpu_at_t1 = get_stat(host)
|
25
|
+
cpu_stats = parse_stats cpu_at_t0, cpu_at_t1
|
26
|
+
end
|
27
|
+
|
28
|
+
{:host => host, :cpu => cpu_stats}
|
29
|
+
end
|
30
|
+
|
31
|
+
private #methods
|
32
|
+
def proc_stat_struct(cpu_times)
|
33
|
+
parts = cpu_times.split(/\n/).first.split
|
34
|
+
times = parts[1..-1].map(&:to_i)
|
35
|
+
CpuStats[*times].tap {|r| r[:total] = times.reduce(:+)}
|
36
|
+
end
|
37
|
+
|
38
|
+
def parse_stats(cpu_at_t0, cpu_at_t1)
|
39
|
+
cpu0 = proc_stat_struct cpu_at_t0
|
40
|
+
cpu1 = proc_stat_struct cpu_at_t1
|
41
|
+
idle = cpu1.idle - cpu0.idle
|
42
|
+
total = cpu1.total - cpu0.total
|
43
|
+
usage = total - idle
|
44
|
+
# {:cpu_usage => ("%.1f%%" % (100.0 * usage / total))}
|
45
|
+
{:usage => (100.0 * usage / total).round}
|
46
|
+
end
|
47
|
+
|
48
|
+
def get_stat(remote, opts={})
|
49
|
+
Net::SFTP.start(remote, @username, :keys => @pub_key ) do |scp|
|
50
|
+
return scp.download!('/proc/stat').split('\n').first
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'net/sftp'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module Silw
|
5
|
+
module Plugins
|
6
|
+
|
7
|
+
# verify the disk usage on a remote host using the contents of
|
8
|
+
# /proc/diskstats file.
|
9
|
+
# See: https://www.kernel.org/doc/Documentation/ABI/testing/procfs-diskstats
|
10
|
+
#
|
11
|
+
# return a JSON containing the disk [read, write] stats
|
12
|
+
class Diskio
|
13
|
+
attr_accessor :diskstats
|
14
|
+
|
15
|
+
def run(args)
|
16
|
+
host = args[:at]
|
17
|
+
|
18
|
+
if fixture_name = args[:fixture]
|
19
|
+
diskstats = parse_diskstats File.read(fixture_name)
|
20
|
+
else
|
21
|
+
diskio_then = parse_diskstats get_diskstats(host)
|
22
|
+
sleep 1
|
23
|
+
diskio_now = parse_diskstats get_diskstats(host)
|
24
|
+
|
25
|
+
# disk [read, write] stats
|
26
|
+
diskstats = [diskio_now[0]-diskio_then[0], diskio_now[1]-diskio_then[1]]
|
27
|
+
end
|
28
|
+
|
29
|
+
{:host => host, :diskio => diskstats}
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
def parse_diskstats(txt)
|
34
|
+
diskstats = txt.split(/\n/).collect { |x| x.strip }
|
35
|
+
rowcount = diskstats.count
|
36
|
+
|
37
|
+
rowcount.times do |i|
|
38
|
+
diskstats[i] = diskstats[i].gsub(/\s+/m, ' ').split(' ')
|
39
|
+
end
|
40
|
+
|
41
|
+
columns_array = []
|
42
|
+
rowcount.times do |i|
|
43
|
+
columns_array << [diskstats[i][3],diskstats[i][7]]
|
44
|
+
end
|
45
|
+
|
46
|
+
columncount = columns_array[0].count
|
47
|
+
|
48
|
+
total_read_writes = []
|
49
|
+
columncount.times do |i|
|
50
|
+
total_read_writes[i] = 0
|
51
|
+
end
|
52
|
+
|
53
|
+
columncount.times do |j|
|
54
|
+
rowcount.times do |k|
|
55
|
+
total_read_writes[j] = columns_array[k][j].to_i + total_read_writes[j]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
total_read_writes
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
def get_diskstats(remote)
|
64
|
+
Net::SFTP.start(remote, @username, :keys => @pub_key) do |scp|
|
65
|
+
return scp.download!('/proc/diskstats')
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'net/sftp'
|
2
|
+
|
3
|
+
module Silw
|
4
|
+
module Plugins
|
5
|
+
|
6
|
+
# Meminfo - report the memory stats collected from a remote system.
|
7
|
+
# For more details about meminfo see this featured article: /proc/meminfo Explained, at:
|
8
|
+
# - http://www.redhat.com/advice/tips/meminfo.html
|
9
|
+
#
|
10
|
+
# @return - a JSON containing the main memory stats
|
11
|
+
class Mem
|
12
|
+
attr_accessor :mem
|
13
|
+
|
14
|
+
def run(args)
|
15
|
+
host = args[:at]
|
16
|
+
|
17
|
+
if fixture_name = args[:fixture]
|
18
|
+
mem = parse_meminfo File.read(fixture_name)
|
19
|
+
else
|
20
|
+
mem = parse_meminfo get_meminfo(host)
|
21
|
+
end
|
22
|
+
|
23
|
+
{:host => host, :mem => mem}
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
def parse_meminfo(txt)
|
28
|
+
meminfo = txt.split(/\n/).collect{|x| x.strip}
|
29
|
+
memtotal = meminfo[0].gsub(/[^0-9]/, "").to_f
|
30
|
+
memfree = meminfo[1].gsub(/[^0-9]/, "").to_f
|
31
|
+
memactive = meminfo[5].gsub(/[^0-9]/, "").to_f
|
32
|
+
{:total => memtotal.round, :active => memactive.round, :free => memfree.round,
|
33
|
+
:usagepercentage => ((memactive * 100) / memtotal).round}
|
34
|
+
end
|
35
|
+
|
36
|
+
def get_meminfo(remote, opts={})
|
37
|
+
Net::SFTP.start(remote, @username, :keys => @pub_key ) do |scp|
|
38
|
+
return scp.download!('/proc/meminfo')
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
data/lib/silw/server.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
require "sinatra/content_for"
|
2
|
+
|
3
|
+
module Silw
|
4
|
+
class Server < Sinatra::Base
|
5
|
+
STATIC_PATHS = ["/favicon.ico", "/img"]
|
6
|
+
|
7
|
+
app_dir = File.dirname(File.expand_path(__FILE__))
|
8
|
+
|
9
|
+
set :views, "#{app_dir}/views"
|
10
|
+
set :public_folder, "#{app_dir}/public"
|
11
|
+
|
12
|
+
use Rack::ShowExceptions
|
13
|
+
use Rack::MethodOverride
|
14
|
+
|
15
|
+
use Rack::StaticCache,
|
16
|
+
urls: STATIC_PATHS,
|
17
|
+
root: File.expand_path('./public/', __FILE__)
|
18
|
+
|
19
|
+
use Rack::Session::Cookie,
|
20
|
+
key: '<#>&wksjuIuwo!',
|
21
|
+
domain: "localhost",
|
22
|
+
path: '/',
|
23
|
+
expire_after: 14400,
|
24
|
+
secret: '*987^514('
|
25
|
+
|
26
|
+
helpers Sinatra::ContentFor
|
27
|
+
helpers Silw::Helpers
|
28
|
+
|
29
|
+
include Rack::Utils
|
30
|
+
|
31
|
+
configure do
|
32
|
+
disable :protection
|
33
|
+
enable :logging
|
34
|
+
end
|
35
|
+
|
36
|
+
get '/' do
|
37
|
+
# "Hello World! :#{settings.agents}:"
|
38
|
+
haml :index
|
39
|
+
end
|
40
|
+
|
41
|
+
# 404
|
42
|
+
not_found do
|
43
|
+
haml :"404"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/lib/silw/version.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
.row
|
2
|
+
.span7
|
3
|
+
-unless settings.agents.empty?
|
4
|
+
.well
|
5
|
+
- settings.agents.each_value do |agent|
|
6
|
+
%table.table-striped
|
7
|
+
%thead
|
8
|
+
%tr
|
9
|
+
%th(width="20%" align="right" nowrap)
|
10
|
+
.span2
|
11
|
+
%span.label.label-important= agent.host
|
12
|
+
%th
|
13
|
+
-unless agent.plugins.empty?
|
14
|
+
-agent.plugins.each_key do |k|
|
15
|
+
%th(align="left")
|
16
|
+
%span.label.label= k
|
17
|
+
%tbody
|
18
|
+
-unless agent.plugins.empty?
|
19
|
+
%tr
|
20
|
+
%td(valign="top")
|
21
|
+
%small.label.label-inverse= agent.time
|
22
|
+
%td
|
23
|
+
%span
|
24
|
+
|
25
|
+
-agent.plugins.each do |k, v|
|
26
|
+
%td(valign="top")
|
27
|
+
= partial k.to_s, :plugin => v
|
28
|
+
-else
|
29
|
+
pending ...
|
30
|
+
|
@@ -0,0 +1,50 @@
|
|
1
|
+
!!! 5
|
2
|
+
%html
|
3
|
+
%head
|
4
|
+
%title= "SILW"
|
5
|
+
%meta{ :"http-equiv" => "content-type", :content => "text/html; charset=UTF-8" }
|
6
|
+
%meta{ :name => "viewport", :content => "width=device-width, initial-scale=1.0" }
|
7
|
+
%link{ :href => "//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.2/css/bootstrap-combined.min.css",
|
8
|
+
:media => "screen", :rel => "stylesheet", :type => "text/css" }
|
9
|
+
%body
|
10
|
+
.container-fluid
|
11
|
+
%header
|
12
|
+
%h2
|
13
|
+
%a{:href => '/'} SILW
|
14
|
+
%p.muted
|
15
|
+
Various system metrics frequently aggregated from multiple remote instances.
|
16
|
+
%p
|
17
|
+
autorefresh on/off
|
18
|
+
%button#button.btn.btn-mini
|
19
|
+
%a#refresh-switch(href="#")
|
20
|
+
%i.icon-stop
|
21
|
+
= yield
|
22
|
+
|
23
|
+
%footer
|
24
|
+
%p
|
25
|
+
|
26
|
+
%script{ :type => 'text/javascript', :src => "//ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js" }
|
27
|
+
%script{ :type => 'text/javascript', :src => "//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.2/js/bootstrap.min.js" }
|
28
|
+
:javascript
|
29
|
+
$(document).ready(function () {
|
30
|
+
var interval = 10; //number of seconds before page refresh
|
31
|
+
var refresh = function() {
|
32
|
+
setTimeout(function() {
|
33
|
+
if( $("#refresh-switch > i").attr("class").match(/^icon-(.*)$/)[1] === "stop"){
|
34
|
+
location.reload(true);
|
35
|
+
$("#refresh-switch > i").attr("class", "icon-stop");
|
36
|
+
}
|
37
|
+
}, interval * 1000);
|
38
|
+
};
|
39
|
+
|
40
|
+
$("#button").click(function(e) {
|
41
|
+
if( $("#refresh-switch > i").attr("class").match(/^icon-(.*)$/)[1] === "stop"){
|
42
|
+
$("#refresh-switch > i").attr("class", "icon-play");
|
43
|
+
clearInterval(refresh);
|
44
|
+
}else{
|
45
|
+
location.reload(true);
|
46
|
+
}
|
47
|
+
});
|
48
|
+
|
49
|
+
refresh();
|
50
|
+
});
|