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 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
+