inari 0.1.0
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.
- 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
|
+
|