inari 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGES +0 -0
- data/InstalledFiles +1 -0
- data/LICENSE +21 -0
- data/README +64 -0
- data/Rakefile +62 -0
- data/THANKS +5 -0
- data/bin/inari +5 -0
- data/lib/inari/commands/defaults.rb +33 -0
- data/lib/inari/commands/http.rb +42 -0
- data/lib/inari/commands/reporting.rb +17 -0
- data/lib/inari/commands/smtp.rb +16 -0
- data/lib/inari/commands/snmp.rb +30 -0
- data/lib/inari/commands/tcp_simple.rb +24 -0
- data/lib/inari/commands/test.rb +9 -0
- data/lib/inari/commands.rb +158 -0
- data/lib/inari/configuration.rb +128 -0
- data/lib/inari/daemon.rb +98 -0
- data/lib/inari/response.rb +9 -0
- data/lib/inari/task_manager.rb +78 -0
- data/lib/inari/task_process.rb +46 -0
- data/lib/inari/version.rb +9 -0
- data/lib/inari.rb +44 -0
- data/test/abstract_test.rb +4 -0
- data/test/commands_test.rb +41 -0
- data/test/configuration_test.rb +19 -0
- data/test/fixtures/test_config.rb +25 -0
- data/test/task_manager_test.rb +28 -0
- metadata +85 -0
data/CHANGES
ADDED
File without changes
|
data/InstalledFiles
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
/usr/local/lib/ruby/site_ruby/1.8/inari.rb
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
Copyright (c) 2006-2007 Alex Young, Helicoid Limited
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person
|
4
|
+
obtaining a copy of this software and associated documentation
|
5
|
+
files (the "Software"), to deal in the Software without
|
6
|
+
restriction, including without limitation the rights to use, copy,
|
7
|
+
modify, merge, publish, distribute, sublicense, and/or sell copies
|
8
|
+
of the Software, and to permit persons to whom the Software is
|
9
|
+
furnished to do so, subject to the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
18
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
19
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
21
|
+
DEALINGS IN THE SOFTWARE.
|
data/README
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
= +inari+
|
2
|
+
|
3
|
+
+inari+ is a simple network monitoring tool, intended for use with web services. It uses a user-friendly configuration file in a similar manner to Rake and Capistrano.
|
4
|
+
|
5
|
+
+inari+ is written in Ruby, can be extended through Ruby modules, and currently requires little external dependencies.
|
6
|
+
|
7
|
+
== Usage
|
8
|
+
|
9
|
+
+inari+ can be Run with:
|
10
|
+
|
11
|
+
+inari+.rb start/stop configuration_file
|
12
|
+
|
13
|
+
The configuration file is optional. If it isn't supplied, $prefix/etc/inari.conf is assumed (this is likely to be /usr/local).
|
14
|
+
|
15
|
+
== Configuration
|
16
|
+
|
17
|
+
+inari+'s configuration files are created using a simple domain-specific language, allowing you to clearly express how you want your services monitored.
|
18
|
+
|
19
|
+
=== Server definition
|
20
|
+
|
21
|
+
A set of servers must be defined. Each server can have several hosts. This is intended to make monitoring a web server cluster or set of similar services simple.
|
22
|
+
|
23
|
+
Servers can be defined as follows:
|
24
|
+
|
25
|
+
server :local, 'localhost'
|
26
|
+
server :blogs, 'blog.helicoid.net', 'mmm.helicoid.net'
|
27
|
+
|
28
|
+
=== Task definitions
|
29
|
+
|
30
|
+
+inari+ currently runs tasks as threaded processes, allowing them to run independently regardless of issues such as timeouts and software errors.
|
31
|
+
|
32
|
+
Tasks are a set of commands that allow you to define:
|
33
|
+
|
34
|
+
* What you want monitored
|
35
|
+
* How you want it monitored
|
36
|
+
* What you want to happen on an event
|
37
|
+
|
38
|
+
You can also supply a description of the task to explain it more clearly.
|
39
|
+
|
40
|
+
For example, this task fetches a web page every 60 seconds and sends an email whenever the page is unreachable:
|
41
|
+
|
42
|
+
desc "Defaults example: email"
|
43
|
+
task :example, :servers => [:local] do
|
44
|
+
get_web_page '/'
|
45
|
+
frequency '60 seconds'
|
46
|
+
email_on_events
|
47
|
+
end
|
48
|
+
|
49
|
+
The required servers have been specified using the task header definition:
|
50
|
+
|
51
|
+
task :example, :servers => [:local] do
|
52
|
+
|
53
|
+
Then a set of commands have been provided. get_web_page will fetch the required page. email_on_events is a standard command built into +inari+ that will send an email whenever the state of the service changes.
|
54
|
+
|
55
|
+
=== Additional events
|
56
|
+
|
57
|
+
If you don't want your inbox filling up with emails, you can use log_on_events. This writes event notifications to a log file, which you can monitor with tail -f in the command line.
|
58
|
+
|
59
|
+
=== Configuration settings
|
60
|
+
|
61
|
+
To configure where you want the emails to be sent, add this line to the top of the file:
|
62
|
+
|
63
|
+
config :email_to, 'alex@example.com'
|
64
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
$:.unshift 'lib' if File.directory? 'lib'
|
2
|
+
|
3
|
+
require 'rake/testtask'
|
4
|
+
require 'rake/rdoctask'
|
5
|
+
require 'rake/gempackagetask'
|
6
|
+
require 'inari/version'
|
7
|
+
|
8
|
+
desc 'Run the functional and unit tests.'
|
9
|
+
Rake::TestTask.new(:test) do |t|
|
10
|
+
t.test_files = FileList['test/*_test.rb']
|
11
|
+
t.verbose = true
|
12
|
+
end
|
13
|
+
|
14
|
+
desc 'Generate rdoc documentation'
|
15
|
+
Rake::RDocTask.new('rdoc') do |rdoc|
|
16
|
+
rdoc.rdoc_dir = 'doc'
|
17
|
+
rdoc.title = 'inari'
|
18
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
19
|
+
rdoc.rdoc_files.include('README')
|
20
|
+
rdoc.rdoc_files.include('lib/*.rb')
|
21
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
22
|
+
end
|
23
|
+
|
24
|
+
task :default => :test
|
25
|
+
|
26
|
+
Spec = Gem::Specification.new do |s|
|
27
|
+
s.name = 'inari'
|
28
|
+
s.version = Inari::INARI_VERSION
|
29
|
+
s.date = Inari::INARI_RELEASE_DATE
|
30
|
+
s.summary = 'Inari is a ruby system monitoring program.'
|
31
|
+
s.email = 'alex@helicoid.net'
|
32
|
+
s.homepage = Inari::UPSTREAM_URL
|
33
|
+
s.rubyforge_project = 'inari'
|
34
|
+
s.autorequire = 'inari'
|
35
|
+
s.bindir = 'bin'
|
36
|
+
s.executables = ['inari']
|
37
|
+
s.has_rdoc = true
|
38
|
+
s.files = FileList['lib/**/*.rb', 'bin/*', '[A-Z]*', 'test/**/*'].to_a
|
39
|
+
s.test_files = Dir['test/test_*.rb']
|
40
|
+
s.requirements = ['logger']
|
41
|
+
s.post_install_message = <<EOF
|
42
|
+
|
43
|
+
Welcome to Inari
|
44
|
+
================
|
45
|
+
|
46
|
+
Inari is a small and extenable system monitor, intended for us with
|
47
|
+
web applications. To start using it, tailor #{Config::CONFIG['prefix'] + '/etc/inari.conf'}
|
48
|
+
to your requirements.
|
49
|
+
|
50
|
+
Take a look at README for help on writing configuration files.
|
51
|
+
|
52
|
+
http://inari.rubyforge.org
|
53
|
+
|
54
|
+
EOF
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
task :gem => [:test]
|
59
|
+
Rake::GemPackageTask.new(Spec) do |p|
|
60
|
+
p.need_tar = true
|
61
|
+
end
|
62
|
+
|
data/THANKS
ADDED
data/bin/inari
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
module Defaults
|
2
|
+
def sig
|
3
|
+
return <<SIGNATURE
|
4
|
+
|
5
|
+
This message was reported by your system monitoring software.
|
6
|
+
|
7
|
+
--
|
8
|
+
#{Inari::APP_NAME}
|
9
|
+
|
10
|
+
SIGNATURE
|
11
|
+
end
|
12
|
+
|
13
|
+
def log_on_events
|
14
|
+
on_down do
|
15
|
+
Inari::logger.error("#{current_host} is down.")
|
16
|
+
end
|
17
|
+
|
18
|
+
on_up do
|
19
|
+
Inari::logger.info("#{current_host} came back after #{down_for} seconds")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def email_on_events
|
24
|
+
on_down do
|
25
|
+
email(:subject => "#{current_host} is down", :body => "#{current_host} is down." + sig)
|
26
|
+
end
|
27
|
+
|
28
|
+
on_up do
|
29
|
+
email(:subject => "#{current_host} is up (down for #{down_for} seconds)",
|
30
|
+
:body => "#{current_host} is up (down for #{down_for} seconds)" + sig)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
|
3
|
+
module HTTP
|
4
|
+
# HTTP commands
|
5
|
+
def page_size(response = nil)
|
6
|
+
response = @last_response if response.nil?
|
7
|
+
response.body.size rescue 0
|
8
|
+
end
|
9
|
+
|
10
|
+
def page_content; @last_response.body; end
|
11
|
+
|
12
|
+
def get_web_page(page)
|
13
|
+
Timeout::timeout(@timeout) do
|
14
|
+
response = Net::HTTP.get_response(@host, page, @port || 80)
|
15
|
+
add_response(page, response)
|
16
|
+
|
17
|
+
# Return true when a 200 is returned, in case people use get_web_page as a condition
|
18
|
+
response.code == '200'
|
19
|
+
end
|
20
|
+
rescue
|
21
|
+
add_response(page, Response.new(:code => $!), true)
|
22
|
+
false
|
23
|
+
end
|
24
|
+
|
25
|
+
def unexpected_page_change
|
26
|
+
@first_response = @last_response if @first_response.nil? and @last_response
|
27
|
+
|
28
|
+
# Fetch a fresh copy of the page and retain the last one
|
29
|
+
page_size(@first_response) == page_size
|
30
|
+
end
|
31
|
+
|
32
|
+
#def when_http_status_is(status, invert = false, &block)
|
33
|
+
# response = Net::HTTP.get_response(@host, @path)
|
34
|
+
# if status.to_i == response.code.to_i and not invert
|
35
|
+
# instance_eval(&block) if block
|
36
|
+
# end
|
37
|
+
#end
|
38
|
+
#
|
39
|
+
#def when_http_status_is_not(status, &block)
|
40
|
+
# when_http_status_is(status, true, &block)
|
41
|
+
#end
|
42
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Reporting
|
2
|
+
def email(options={})
|
3
|
+
from = "#{Etc.getlogin}@#{Socket.gethostname}"
|
4
|
+
to = Inari::configuration.options[:email_to]
|
5
|
+
message =<<MESSAGE
|
6
|
+
From: #{from}
|
7
|
+
To: #{to}
|
8
|
+
Subject: #{options[:subject]}
|
9
|
+
|
10
|
+
#{options[:body]}
|
11
|
+
MESSAGE
|
12
|
+
|
13
|
+
Net::SMTP.start(Inari::configuration.options[:smtp_host]) do |smtp|
|
14
|
+
smtp.send_message message, from, to
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'net/smtp'
|
2
|
+
|
3
|
+
module SMTP
|
4
|
+
# SMTP commands
|
5
|
+
def get_smtp_helo
|
6
|
+
response = Response.new
|
7
|
+
begin
|
8
|
+
Net::SMTP.start(@host, port=@port, helo=@localhost, user=nil, secret=nil, authtype=nil)
|
9
|
+
add_response('helo', Response.new(:code => 'Connection accepted'))
|
10
|
+
true
|
11
|
+
rescue
|
12
|
+
add_response('helo', Response.new(:code => $!), true)
|
13
|
+
false
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'snmp'
|
2
|
+
|
3
|
+
module SNMP
|
4
|
+
# SNMP commands
|
5
|
+
def get_snmp_out(interface)
|
6
|
+
snmp_walk().each do |snmp_data|
|
7
|
+
return snmp_data[2].to_i if (snmp_data[1] == interface.to_s)
|
8
|
+
end
|
9
|
+
return 0
|
10
|
+
end
|
11
|
+
|
12
|
+
def get_snmp_in(interface)
|
13
|
+
snmp_walk().each do |snmp_data|
|
14
|
+
return snmp_data[3].to_i if (snmp_data[1] == interface.to_s)
|
15
|
+
end
|
16
|
+
return 0
|
17
|
+
end
|
18
|
+
|
19
|
+
def snmp_walk()
|
20
|
+
columns, values, snmp_data = ['ifIndex', 'ifDescr', 'ifInOctets', 'ifOutOctets'], [], []
|
21
|
+
SNMP::Manager.open(:Host => 'mon') do |manager|
|
22
|
+
manager.walk(columns) do |row|
|
23
|
+
row.each { |vb| values << vb.value }
|
24
|
+
end
|
25
|
+
|
26
|
+
values.each_slice(4) { |slice| snmp_data << slice }
|
27
|
+
end
|
28
|
+
snmp_data
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# The simplest commands in the toolset: These check a por is available
|
2
|
+
module TCPSimple
|
3
|
+
def up?
|
4
|
+
state = false
|
5
|
+
name = @host + ' TCP check'
|
6
|
+
|
7
|
+
begin
|
8
|
+
Timeout::timeout(@timeout) do
|
9
|
+
t = TCPSocket.new(@host, @port)
|
10
|
+
state = true
|
11
|
+
add_response(name, Response.new(:code => 'Up'))
|
12
|
+
end
|
13
|
+
rescue
|
14
|
+
Inari::logger.error "TCP error: #{$!} for host: #{@host}, port: #{@port}"
|
15
|
+
add_response(name, Response.new(:code => $!), true)
|
16
|
+
ensure
|
17
|
+
t.close if defined? t
|
18
|
+
end
|
19
|
+
|
20
|
+
return state
|
21
|
+
end
|
22
|
+
|
23
|
+
def down? ; !up? ; end
|
24
|
+
end
|
@@ -0,0 +1,158 @@
|
|
1
|
+
# The response object
|
2
|
+
require 'inari/response'
|
3
|
+
|
4
|
+
# The command drivers
|
5
|
+
require 'inari/commands/http'
|
6
|
+
require 'inari/commands/snmp'
|
7
|
+
require 'inari/commands/smtp'
|
8
|
+
require 'inari/commands/reporting'
|
9
|
+
require 'inari/commands/tcp_simple'
|
10
|
+
require 'inari/commands/defaults'
|
11
|
+
require 'inari/commands/test'
|
12
|
+
|
13
|
+
module Inari
|
14
|
+
# This is the configuration API. These methods can be used from configuration files.
|
15
|
+
#
|
16
|
+
# +Commands+ is currently a singleton because mixins are used to extend the functionality
|
17
|
+
# with different types of protocols. I thought this would be more efficient than instantiating
|
18
|
+
# a +Commands+ object for each task. A host is set, then methods can be called. Results for
|
19
|
+
# the currently set host will be stored internally as hashes or arrays.
|
20
|
+
#
|
21
|
+
# This is a good area for improvement, perhaps by adding a simple plugin system:
|
22
|
+
#
|
23
|
+
# http://eigenclass.org/hiki.rb?cmd=view&p=ruby+plugins&key=plugin
|
24
|
+
#
|
25
|
+
class Commands
|
26
|
+
include Singleton
|
27
|
+
|
28
|
+
# The taskmanager created for this command instance.
|
29
|
+
attr_reader :last_response
|
30
|
+
attr_accessor :host, :port, :path, :sleep, :timeout
|
31
|
+
|
32
|
+
include HTTP
|
33
|
+
include SNMP
|
34
|
+
include SMTP
|
35
|
+
include Reporting
|
36
|
+
include TCPSimple
|
37
|
+
include Defaults
|
38
|
+
include TestCommands
|
39
|
+
|
40
|
+
def initialize
|
41
|
+
@host = ''
|
42
|
+
@port = nil
|
43
|
+
@path = ''
|
44
|
+
|
45
|
+
@sleep = 60
|
46
|
+
@timeout = 5
|
47
|
+
|
48
|
+
@responses = {}
|
49
|
+
@downtimers = {}
|
50
|
+
@last_downtime = {}
|
51
|
+
@responses_limit = 10
|
52
|
+
@last_response = ''
|
53
|
+
|
54
|
+
@localhost = 'localhost'
|
55
|
+
end
|
56
|
+
|
57
|
+
# Returns the current time in the same format
|
58
|
+
#
|
59
|
+
def time ; Time.now.utc ; end
|
60
|
+
|
61
|
+
# Used to define the current host for this command.
|
62
|
+
def current_host ; @host ; end
|
63
|
+
|
64
|
+
# The status of the last task against the service.
|
65
|
+
def status ; @last_response.code; end
|
66
|
+
|
67
|
+
# Set the port the service you're monitoring runs on.
|
68
|
+
def port(port) ; @port = port ; end
|
69
|
+
|
70
|
+
# Define the timeout and frequency for a task.
|
71
|
+
def timeout(timeout) ; @timeout = to_seconds timeout ; end
|
72
|
+
def frequency(sleep)
|
73
|
+
@sleep = to_seconds sleep
|
74
|
+
@timeout = @sleep - 1 if !@timeout or @sleep > @timeout
|
75
|
+
end
|
76
|
+
|
77
|
+
##
|
78
|
+
|
79
|
+
# Call on_up or on_down with a block to define an event:
|
80
|
+
#
|
81
|
+
# Example:
|
82
|
+
#
|
83
|
+
# on_up do
|
84
|
+
# logger.info("#{current_host} came back!")
|
85
|
+
# end
|
86
|
+
#
|
87
|
+
#
|
88
|
+
def on_down(&block)
|
89
|
+
@on_down = block
|
90
|
+
end
|
91
|
+
|
92
|
+
def on_up(&block)
|
93
|
+
@on_up = block
|
94
|
+
end
|
95
|
+
|
96
|
+
# Get the downtime for this task.
|
97
|
+
def down_for
|
98
|
+
if @downtimers[@host].nil?
|
99
|
+
return @last_downtime[@host] || 0
|
100
|
+
else
|
101
|
+
return time.to_i - @downtimers[@host].to_i
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# Called when a task has reported a service offline.
|
106
|
+
def down
|
107
|
+
if @downtimers[@host].nil?
|
108
|
+
@downtimers[@host] = time
|
109
|
+
@last_downtime[@host] = 0
|
110
|
+
@on_down.call(self) if @on_down
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# Called when a task has seen the service come back.
|
115
|
+
def up
|
116
|
+
if @downtimers[@host]
|
117
|
+
@last_downtime[@host] = time.to_i - @downtimers[@host].to_i
|
118
|
+
@downtimers[@host] = nil
|
119
|
+
@on_up.call(self) if @on_up
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def respond_to?(sym) #:nodoc:
|
124
|
+
self.methods.include?(sym) || super
|
125
|
+
end
|
126
|
+
|
127
|
+
private
|
128
|
+
|
129
|
+
# Store the remote responses in queue.
|
130
|
+
def add_response(name, response, down = false)
|
131
|
+
@last_response = response
|
132
|
+
|
133
|
+
down ? self.down : up
|
134
|
+
|
135
|
+
@responses[@host] ||= []
|
136
|
+
@responses[@host].insert 0, {:name => name, :response => response, :time => time, :down => down }
|
137
|
+
@responses[@host].pop if @responses[@host].size > @responses_limit
|
138
|
+
end
|
139
|
+
|
140
|
+
def to_seconds(argument)
|
141
|
+
return argument if argument.class < Integer
|
142
|
+
|
143
|
+
if argument.class == String
|
144
|
+
case argument
|
145
|
+
when /^(.*?)[+,](.*)$/ then to_sec($1) + to_sec($2)
|
146
|
+
when /^\s*([0-9_]+)\s*\*(.+)$/ then $1.to_i * to_sec($2)
|
147
|
+
when /^\s*[0-9_]+\s*(s(ec(ond)?s?)?)?\s*$/ then argument.to_i
|
148
|
+
when /^\s*([0-9_]+)\s*m(in(ute)?s?)?\s*$/ then $1.to_i * 60
|
149
|
+
when /^\s*([0-9_]+)\s*h(ours?)?\s*$/ then $1.to_i * 3600
|
150
|
+
when /^\s*([0-9_]+)\s*d(ays?)?\s*$/ then $1.to_i * 86400
|
151
|
+
when /^\s*([0-9_]+)\s*w(eeks?)?\s*$/ then $1.to_i * 604800
|
152
|
+
when /^\s*([0-9_]+)\s*months?\s*$/ then $1.to_i * 2419200
|
153
|
+
else 0
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
module Inari
|
2
|
+
# Loads the tasks file and defines task manager to carry out the tasks
|
3
|
+
class Configuration
|
4
|
+
include Singleton
|
5
|
+
|
6
|
+
Server = Struct.new(:host, :options)
|
7
|
+
|
8
|
+
# The TaskManager created for this configuration instance.
|
9
|
+
attr_reader :taskmanager
|
10
|
+
|
11
|
+
# The load paths used for locating recipe files.
|
12
|
+
attr_reader :load_paths, :default_load_path
|
13
|
+
|
14
|
+
# The hash of servers.
|
15
|
+
attr_reader :servers
|
16
|
+
|
17
|
+
# The hash of global options.
|
18
|
+
attr_reader :options
|
19
|
+
|
20
|
+
def initialize() #:nodoc:
|
21
|
+
@servers = Hash.new { |h, k| h[k] = [] }
|
22
|
+
@options = Hash.new { |h, k| h[k] = [] }
|
23
|
+
@default_load_path = File.join(Config::CONFIG['prefix'], 'etc')
|
24
|
+
@load_paths = ['.', @default_load_path]
|
25
|
+
@now = Time.now.utc
|
26
|
+
@taskmanager = TaskManager.instance
|
27
|
+
end
|
28
|
+
|
29
|
+
# Load a configuration file or string into this configuration.
|
30
|
+
#
|
31
|
+
# Usage:
|
32
|
+
#
|
33
|
+
# load(:file => "settings.conf"):
|
34
|
+
# Load a configuration file.
|
35
|
+
#
|
36
|
+
# load(:string => "set :scm, :subversion"):
|
37
|
+
# Load the given string as a configuration specification.
|
38
|
+
#
|
39
|
+
# load { ... }
|
40
|
+
# Load the block in the context of the configuration.
|
41
|
+
def load(*args, &block)
|
42
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
43
|
+
args.each { |arg| load options.merge(:file => arg) }
|
44
|
+
return unless args.empty?
|
45
|
+
|
46
|
+
if block
|
47
|
+
raise 'Loading a block requires 2 parameters' unless args.empty?
|
48
|
+
load(options.merge(:proc => block))
|
49
|
+
|
50
|
+
elsif options[:file]
|
51
|
+
file = options[:file]
|
52
|
+
unless file[0] == ?/
|
53
|
+
load_paths.each do |path|
|
54
|
+
if File.file?(File.join(path, file))
|
55
|
+
file = File.join(path, file)
|
56
|
+
break
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
raise ArgumentError, "Configuration file #{file} not found." if file.nil?
|
61
|
+
end
|
62
|
+
|
63
|
+
load :string => File.read(file), :name => options[:name] || file
|
64
|
+
|
65
|
+
elsif options[:string]
|
66
|
+
instance_eval(options[:string], options[:name] || "<eval>")
|
67
|
+
|
68
|
+
elsif options[:proc]
|
69
|
+
instance_eval(&options[:proc])
|
70
|
+
|
71
|
+
else
|
72
|
+
raise ArgumentError, "Don't know how to load #{options.inspect}"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Define a new server and its associated servers. You must specify at least
|
77
|
+
# one host for each server.
|
78
|
+
#
|
79
|
+
# Usage:
|
80
|
+
#
|
81
|
+
# server :db, 'db1.example.com', 'db2.example.com'
|
82
|
+
# server :mail, 'mail.example.com'
|
83
|
+
def server(which, *args)
|
84
|
+
raise ArgumentError, 'must give at least one host' if args.empty?
|
85
|
+
args.each { |host| servers[which] << Server.new(host) }
|
86
|
+
end
|
87
|
+
|
88
|
+
def config(field, *args)
|
89
|
+
@options[field] = args
|
90
|
+
end
|
91
|
+
|
92
|
+
# Describe the next task to be defined.
|
93
|
+
def desc(text)
|
94
|
+
@next_description = text
|
95
|
+
end
|
96
|
+
|
97
|
+
# Returns the logging object
|
98
|
+
def logger
|
99
|
+
Inari::logger
|
100
|
+
end
|
101
|
+
|
102
|
+
# Define a new task. If a description is active (see #desc), it is added to
|
103
|
+
# the options under the <tt>:desc</tt> key. This method ultimately
|
104
|
+
# delegates to TaskManager#define_task.
|
105
|
+
def task(name, options={}, &block)
|
106
|
+
raise ArgumentError, 'expected a block' unless block
|
107
|
+
|
108
|
+
if @next_description
|
109
|
+
options = options.merge(:desc => @next_description)
|
110
|
+
@next_description = nil
|
111
|
+
end
|
112
|
+
|
113
|
+
taskmanager.define_task(name, options, &block)
|
114
|
+
end
|
115
|
+
|
116
|
+
def run_tasks
|
117
|
+
taskmanager.run_tasks
|
118
|
+
end
|
119
|
+
|
120
|
+
def method_missing(sym, *args, &block) #:nodoc:
|
121
|
+
if args.length == 0 && block.nil? && @variables.has_key?(sym)
|
122
|
+
self[sym]
|
123
|
+
else
|
124
|
+
super
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
data/lib/inari/daemon.rb
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
# http://www.bigbold.com/snippets/posts/show/2265
|
2
|
+
#
|
3
|
+
|
4
|
+
require 'fileutils'
|
5
|
+
|
6
|
+
module Daemon
|
7
|
+
def self.exit?
|
8
|
+
!ARGV.empty? and ARGV[0] == 'stop'
|
9
|
+
end
|
10
|
+
|
11
|
+
class Base
|
12
|
+
def self.pid_file_name
|
13
|
+
File.join(Dir.tmpdir, 'inari.pid')
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.daemonize
|
17
|
+
Controller.daemonize(self)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
module PidFile
|
22
|
+
def self.store(daemon, pid)
|
23
|
+
File.open(daemon.pid_file_name, 'a+') {|f| f << pid.to_s + "\n"}
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.recall(daemon)
|
27
|
+
IO.read(daemon.pid_file_name).split("\n").collect {|pid| pid.to_i } rescue nil
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
module Controller
|
32
|
+
def self.daemonize(daemon)
|
33
|
+
case !ARGV.empty? && ARGV[0]
|
34
|
+
when 'start'
|
35
|
+
start(daemon)
|
36
|
+
when 'stop'
|
37
|
+
puts 'Cleaning up all processes.'
|
38
|
+
stop(daemon)
|
39
|
+
puts 'Stopped.'
|
40
|
+
exit
|
41
|
+
when 'restart'
|
42
|
+
stop(daemon)
|
43
|
+
start(daemon)
|
44
|
+
else
|
45
|
+
puts 'Invalid command. Please specify start, stop or restart.'
|
46
|
+
exit
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Create a process for this task.
|
51
|
+
def self.start(daemon)
|
52
|
+
fork do
|
53
|
+
# Become session leader
|
54
|
+
Process.setsid
|
55
|
+
# Zap session leader
|
56
|
+
exit if fork
|
57
|
+
# Store the pid
|
58
|
+
PidFile.store(daemon, Process.pid)
|
59
|
+
# Change directory
|
60
|
+
Dir.chdir Dir.tmpdir
|
61
|
+
|
62
|
+
redirect_logs if ARGV.empty? and ARGV[1] != 'debug'
|
63
|
+
|
64
|
+
trap('TERM') {daemon.stop; exit}
|
65
|
+
daemon.start
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Stop all tasks.
|
70
|
+
def self.stop(daemon)
|
71
|
+
puts 'Stopping all tasks...'
|
72
|
+
|
73
|
+
if !File.file?(daemon.pid_file_name)
|
74
|
+
puts "Pid file not found (#{daemon.pid_file_name}). Is the daemon running?"
|
75
|
+
exit
|
76
|
+
end
|
77
|
+
(PidFile.recall(daemon) or []).each do |pid|
|
78
|
+
pid && Process.kill('TERM', pid) rescue Errno::ESRCH
|
79
|
+
end
|
80
|
+
remove_pid_file(daemon)
|
81
|
+
|
82
|
+
puts 'Tasks stopped.'
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.remove_pid_file(daemon)
|
86
|
+
FileUtils.rm(daemon.pid_file_name)
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
def self.redirect_logs
|
92
|
+
# Free file descriptors and point them somewhere sensible.
|
93
|
+
STDIN.reopen '/dev/null'
|
94
|
+
STDOUT.reopen '/dev/null', 'a'
|
95
|
+
STDERR.reopen STDOUT
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module Inari
|
2
|
+
class TaskManager
|
3
|
+
include Singleton
|
4
|
+
|
5
|
+
# The command library associated with this TaskManager.
|
6
|
+
attr_reader :commands
|
7
|
+
|
8
|
+
# A hash of the tasks known to this TaskManager, keyed by name. The values are
|
9
|
+
# instances of TaskManager::Task.
|
10
|
+
attr_reader :tasks
|
11
|
+
|
12
|
+
# Represents the definition of a single task.
|
13
|
+
class Task #:nodoc:
|
14
|
+
attr_reader :name, :taskmanager, :options, :hosts
|
15
|
+
|
16
|
+
def initialize(name, taskmanager, options)
|
17
|
+
@name, @taskmanager, @options = name, taskmanager, options
|
18
|
+
end
|
19
|
+
|
20
|
+
# Returns the list of servers that are the target of this task.
|
21
|
+
def hosts
|
22
|
+
unless @hosts
|
23
|
+
servers = [*(@options[:servers] || Inari::configuration.servers.keys)].map do |name|
|
24
|
+
if Inari::configuration.servers[name].size == 0
|
25
|
+
raise ArgumentError, "Task: #{self.name.inspect} references non-existent or invalid server: #{name.inspect}."
|
26
|
+
else
|
27
|
+
Inari::configuration.servers[name]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
@hosts = servers.flatten.map { |server| server.host }.uniq
|
31
|
+
end
|
32
|
+
@hosts
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def initialize #:nodoc:
|
37
|
+
@commands = Commands.instance
|
38
|
+
@tasks = {}
|
39
|
+
end
|
40
|
+
|
41
|
+
# Define a new task for this TaskManager. The block will be invoked when this task is called.
|
42
|
+
def define_task(name, options={}, &block)
|
43
|
+
@tasks[name] = (options[:task_class] || Task).new(name, self, options)
|
44
|
+
define_method(name) do
|
45
|
+
send "before_#{name}" if respond_to? "before_#{name}"
|
46
|
+
result = instance_eval(&block)
|
47
|
+
send "after_#{name}" if respond_to? "after_#{name}"
|
48
|
+
result
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def run_tasks
|
53
|
+
tasks.each do |task|
|
54
|
+
TaskProcess.daemonize(self, task)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def metaclass
|
59
|
+
class << self; self; end
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def method_missing(sym, *args, &block)
|
65
|
+
if Inari::configuration.respond_to?(sym)
|
66
|
+
Inari::configuration.send(sym, *args, &block)
|
67
|
+
elsif @commands.respond_to?(sym)
|
68
|
+
@commands.send(sym, *args, &block)
|
69
|
+
else
|
70
|
+
super
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def define_method(name, &block)
|
75
|
+
metaclass.send(:define_method, name, &block)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Inari
|
2
|
+
# Starts a process that runs a task.
|
3
|
+
#
|
4
|
+
class TaskProcess < Daemon::Base
|
5
|
+
@@taskmanager, @@task = ''
|
6
|
+
|
7
|
+
def self.start
|
8
|
+
Inari::logger.info "Monitoring started for task #{@@task[0]}"
|
9
|
+
semaphore = Mutex.new
|
10
|
+
|
11
|
+
# Run threads for each host
|
12
|
+
threads = []
|
13
|
+
@@task[1].hosts.each do |host|
|
14
|
+
threads << Thread.new do
|
15
|
+
loop do
|
16
|
+
semaphore.synchronize do
|
17
|
+
begin
|
18
|
+
@@taskmanager.commands.host = host
|
19
|
+
@@taskmanager.send(@@task[0])
|
20
|
+
rescue Timeout::Error
|
21
|
+
# Potential network timeout issue
|
22
|
+
@@taskmanager.commands.down
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
sleep @@taskmanager.commands.sleep
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
threads.each do |thread|
|
32
|
+
thread.join
|
33
|
+
end
|
34
|
+
rescue ArgumentError
|
35
|
+
Daemon::Controller.remove_pid_file(self)
|
36
|
+
raise
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.daemonize(taskmanager, task)
|
40
|
+
@@taskmanager, @@task = taskmanager, task
|
41
|
+
Daemon::Controller.daemonize(self)
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.stop ; end
|
45
|
+
end
|
46
|
+
end
|
data/lib/inari.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'rbconfig'
|
2
|
+
require 'rubygems'
|
3
|
+
require 'enumerator'
|
4
|
+
require 'socket'
|
5
|
+
require 'net/smtp'
|
6
|
+
require 'timeout'
|
7
|
+
require 'singleton'
|
8
|
+
require 'logger'
|
9
|
+
require 'tmpdir'
|
10
|
+
|
11
|
+
# Inari's classes and modules
|
12
|
+
require 'inari/daemon'
|
13
|
+
require 'inari/commands'
|
14
|
+
require 'inari/task_manager'
|
15
|
+
require 'inari/configuration'
|
16
|
+
require 'inari/task_process'
|
17
|
+
|
18
|
+
module Inari
|
19
|
+
def self.logger #:nodoc:
|
20
|
+
unless defined? @@logger
|
21
|
+
Dir.chdir Dir.tmpdir do
|
22
|
+
@@logger = Logger.new('inari.log')
|
23
|
+
@@logger.datetime_format = '%Y-%m-%d %H:%M:%S'
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
@@logger
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.configuration #:nodoc:
|
31
|
+
Inari::Configuration.instance
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.go
|
35
|
+
unless Daemon::exit?
|
36
|
+
logger.info '*** Inari started'
|
37
|
+
configuration.load(ARGV[1] || 'inari.conf')
|
38
|
+
configuration.run_tasks
|
39
|
+
else
|
40
|
+
Daemon::Controller.stop(Daemon::Base)
|
41
|
+
logger.info '*** Inari stopped'
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'test/abstract_test'
|
2
|
+
|
3
|
+
class CommandsTest < Test::Unit::TestCase
|
4
|
+
def setup
|
5
|
+
@commands = Inari::Commands.instance
|
6
|
+
end
|
7
|
+
|
8
|
+
def test_down_time
|
9
|
+
@commands.host = 'test'
|
10
|
+
@commands.down
|
11
|
+
sleep 1
|
12
|
+
@commands.up
|
13
|
+
|
14
|
+
assert_equal 1, @commands.down_for
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_multiple_down_time
|
18
|
+
['localhost', 'test.localhost', 'test2.localhost'].each do |host|
|
19
|
+
@commands.host = host
|
20
|
+
@commands.down
|
21
|
+
end
|
22
|
+
|
23
|
+
@commands.host = 'test3.localhost'
|
24
|
+
@commands.down
|
25
|
+
|
26
|
+
sleep 1
|
27
|
+
|
28
|
+
|
29
|
+
['localhost', 'test.localhost', 'test2.localhost'].each do |host|
|
30
|
+
@commands.host = host
|
31
|
+
@commands.up
|
32
|
+
|
33
|
+
assert_equal 1, @commands.down_for
|
34
|
+
end
|
35
|
+
|
36
|
+
sleep 1
|
37
|
+
|
38
|
+
@commands.host = 'test3.localhost'
|
39
|
+
assert_equal 2, @commands.down_for
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'test/abstract_test'
|
2
|
+
|
3
|
+
class ConfigurationTest < Test::Unit::TestCase
|
4
|
+
def setup
|
5
|
+
@configuration = Inari::Configuration.instance
|
6
|
+
end
|
7
|
+
|
8
|
+
# Check if any of the load paths make sense
|
9
|
+
def test_default_load_path_exists
|
10
|
+
assert File.directory?(@configuration.default_load_path)
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_loads
|
14
|
+
@configuration.load_paths.push 'test/fixtures/'
|
15
|
+
assert_nil @configuration.load('test_config.rb')
|
16
|
+
assert_equal 3, @configuration.servers.size
|
17
|
+
assert_kind_of Inari::Configuration::Server, @configuration.servers[:local].first
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# Configuration
|
2
|
+
config :email_to, 'alex@example.com'
|
3
|
+
config :email_from, 'alex@example.com'
|
4
|
+
|
5
|
+
# Server definitions
|
6
|
+
#
|
7
|
+
# Define a name for each server so your tasks can refer to them, and
|
8
|
+
# provide the server's hostname. You can supply more than one
|
9
|
+
# address for each name.
|
10
|
+
|
11
|
+
server :local, 'localhost'
|
12
|
+
server :web_misc, 'helicoid.net', 'alexyoung.org'
|
13
|
+
server :blogs, 'blog.helicoid.net', 'work.alexyoung.org'
|
14
|
+
|
15
|
+
# Task definitions
|
16
|
+
#
|
17
|
+
# Tasks are units of tests to run against each server.
|
18
|
+
# Tasks may run against more than one server by providing a list of definitions.
|
19
|
+
|
20
|
+
desc 'Defaults example: logger'
|
21
|
+
task :example, :servers => [:local, :web_misc, :blogs] do
|
22
|
+
get_web_page '/'
|
23
|
+
frequency '30 seconds'
|
24
|
+
log_on_events
|
25
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'test/abstract_test'
|
2
|
+
|
3
|
+
class TaskManagerTest < Test::Unit::TestCase
|
4
|
+
def setup
|
5
|
+
@configuration = Inari::Configuration.instance
|
6
|
+
@configuration.load_paths.push 'test/fixtures/'
|
7
|
+
@configuration.load('test_config.rb')
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_task_definitions
|
11
|
+
@configuration.taskmanager.define_task(:example2, {}, &Proc.new { port 80 ; up? })
|
12
|
+
|
13
|
+
# Define the method
|
14
|
+
assert_nil @configuration.taskmanager.tasks[:example2].options[:desc]
|
15
|
+
|
16
|
+
# Run it
|
17
|
+
@configuration.taskmanager.send(:example2)
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_loaded_tasks
|
21
|
+
# The loaded configuration should result in one task
|
22
|
+
assert_equal 1, @configuration.taskmanager.tasks.size
|
23
|
+
|
24
|
+
# Check fields are set correctly and where expected
|
25
|
+
assert_equal 'Defaults example: logger', @configuration.taskmanager.tasks[:example].options[:desc]
|
26
|
+
assert_kind_of Array, @configuration.taskmanager.tasks[:example].options[:servers]
|
27
|
+
end
|
28
|
+
end
|
metadata
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
rubygems_version: 0.9.0
|
3
|
+
specification_version: 1
|
4
|
+
name: inari
|
5
|
+
version: !ruby/object:Gem::Version
|
6
|
+
version: 0.1.0
|
7
|
+
date: 2007-02-06 00:00:00 +00:00
|
8
|
+
summary: Inari is a ruby system monitoring program.
|
9
|
+
require_paths:
|
10
|
+
- lib
|
11
|
+
email: alex@helicoid.net
|
12
|
+
homepage: http://inari.rubyforge.org
|
13
|
+
rubyforge_project: inari
|
14
|
+
description:
|
15
|
+
autorequire: inari
|
16
|
+
default_executable:
|
17
|
+
bindir: bin
|
18
|
+
has_rdoc: true
|
19
|
+
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">"
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 0.0.0
|
24
|
+
version:
|
25
|
+
platform: ruby
|
26
|
+
signing_key:
|
27
|
+
cert_chain:
|
28
|
+
post_install_message: |+
|
29
|
+
|
30
|
+
Welcome to Inari
|
31
|
+
================
|
32
|
+
|
33
|
+
Inari is a small and extenable system monitor, intended for us with
|
34
|
+
web applications. To start using it, tailor /usr/local/etc/inari.conf
|
35
|
+
to your requirements.
|
36
|
+
|
37
|
+
Take a look at README for help on writing configuration files.
|
38
|
+
|
39
|
+
http://inari.rubyforge.org
|
40
|
+
|
41
|
+
authors: []
|
42
|
+
|
43
|
+
files:
|
44
|
+
- lib/inari.rb
|
45
|
+
- lib/inari/commands.rb
|
46
|
+
- lib/inari/configuration.rb
|
47
|
+
- lib/inari/daemon.rb
|
48
|
+
- lib/inari/response.rb
|
49
|
+
- lib/inari/task_manager.rb
|
50
|
+
- lib/inari/task_process.rb
|
51
|
+
- lib/inari/version.rb
|
52
|
+
- lib/inari/commands/defaults.rb
|
53
|
+
- lib/inari/commands/http.rb
|
54
|
+
- lib/inari/commands/reporting.rb
|
55
|
+
- lib/inari/commands/smtp.rb
|
56
|
+
- lib/inari/commands/snmp.rb
|
57
|
+
- lib/inari/commands/tcp_simple.rb
|
58
|
+
- lib/inari/commands/test.rb
|
59
|
+
- bin/inari
|
60
|
+
- CHANGES
|
61
|
+
- InstalledFiles
|
62
|
+
- LICENSE
|
63
|
+
- Rakefile
|
64
|
+
- README
|
65
|
+
- THANKS
|
66
|
+
- test/abstract_test.rb
|
67
|
+
- test/commands_test.rb
|
68
|
+
- test/configuration_test.rb
|
69
|
+
- test/fixtures
|
70
|
+
- test/task_manager_test.rb
|
71
|
+
- test/fixtures/test_config.rb
|
72
|
+
test_files: []
|
73
|
+
|
74
|
+
rdoc_options: []
|
75
|
+
|
76
|
+
extra_rdoc_files: []
|
77
|
+
|
78
|
+
executables:
|
79
|
+
- inari
|
80
|
+
extensions: []
|
81
|
+
|
82
|
+
requirements:
|
83
|
+
- logger
|
84
|
+
dependencies: []
|
85
|
+
|