silw 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
});
|