inari 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,5 @@
1
+ Gabriel Gironda
2
+ http://gabriel.gironda.org/
3
+
4
+ Minero Aoki
5
+ http://i.loveruby.net/en/projects/setup/
data/bin/inari ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/local/bin/ruby
2
+ require 'inari'
3
+
4
+ # Run Inari
5
+ Inari.go
@@ -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,9 @@
1
+ module TestCommands
2
+ def run_test(probability)
3
+ if rand(100) > probability
4
+ add_response('Test', Response.new(:code => '200'))
5
+ else
6
+ add_response('Test', Response.new(:code => '404'), true)
7
+ end
8
+ end
9
+ 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
@@ -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,9 @@
1
+ class Response
2
+ attr_accessor :code
3
+ attr_accessor :size
4
+ attr_accessor :logged
5
+
6
+ def initialize(*args)
7
+ args.each { |field, value| self.send(field, value) } if args.is_a? Hash
8
+ end
9
+ 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
@@ -0,0 +1,9 @@
1
+
2
+ module Inari
3
+ APP_NAME = 'Inari'
4
+
5
+ INARI_VERSION = '0.1.0'
6
+ INARI_RELEASE_DATE = '2007/02/06'
7
+
8
+ UPSTREAM_URL = 'http://inari.rubyforge.org'
9
+ 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,4 @@
1
+ # Generic test requirements
2
+
3
+ require 'test/unit'
4
+ require 'inari'
@@ -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
+