reckoner 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +9 -0
- data/Manifest.txt +22 -0
- data/README.txt +67 -0
- data/Rakefile +11 -0
- data/bin/reckoner +8 -0
- data/lib/abstract_check.rb +86 -0
- data/lib/main.rb +141 -0
- data/lib/reckoner.rb +99 -0
- data/lib/rfile.rb +91 -0
- data/lib/sample.rb +55 -0
- data/lib/sendmail.rb +28 -0
- data/lib/standard_checks.rb +39 -0
- data/test/files/additional_checks.rb +10 -0
- data/test/files/one-k.bin +0 -0
- data/test/support.rb +41 -0
- data/test/test_abstract_check.rb +36 -0
- data/test/test_exist.rb +36 -0
- data/test/test_freshness.rb +49 -0
- data/test/test_main.rb +60 -0
- data/test/test_minimum_size.rb +33 -0
- data/test/test_reckoner.rb +38 -0
- data/test/test_rfile.rb +37 -0
- data/test/test_support.rb +28 -0
- metadata +102 -0
data/History.txt
ADDED
data/Manifest.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
History.txt
|
2
|
+
Manifest.txt
|
3
|
+
README.txt
|
4
|
+
Rakefile
|
5
|
+
bin/reckoner
|
6
|
+
lib/main.rb
|
7
|
+
lib/reckoner.rb
|
8
|
+
lib/abstract_check.rb
|
9
|
+
lib/rfile.rb
|
10
|
+
lib/sendmail.rb
|
11
|
+
lib/sample.rb
|
12
|
+
lib/standard_checks.rb
|
13
|
+
test/support.rb
|
14
|
+
test/test_abstract_check.rb
|
15
|
+
test/test_exist.rb
|
16
|
+
test/test_freshness.rb
|
17
|
+
test/test_main.rb
|
18
|
+
test/test_minimum_size.rb
|
19
|
+
test/test_rfile.rb
|
20
|
+
test/test_support.rb
|
21
|
+
test/files/one-k.bin
|
22
|
+
test/files/additional_checks.rb
|
data/README.txt
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
= reckoner
|
2
|
+
|
3
|
+
* http://reckoner.rubyforge.com
|
4
|
+
|
5
|
+
== DESCRIPTION:
|
6
|
+
|
7
|
+
Ruby Reckoner is an easily-configurable program that monitors
|
8
|
+
files and can send notifications when it finds problems.
|
9
|
+
|
10
|
+
Currently it can only check that files exist, have been updated
|
11
|
+
recently and that they have a minimum size, however it is easy to add
|
12
|
+
your own checks written in Ruby.
|
13
|
+
|
14
|
+
== FEATURES/PROBLEMS:
|
15
|
+
|
16
|
+
* Easy YAML Configuration
|
17
|
+
* Check for file existence, freshness and size
|
18
|
+
* Easy to add your own checks
|
19
|
+
|
20
|
+
== USAGE:
|
21
|
+
|
22
|
+
reckoner [OPTIONS] [CONFIG FILE]
|
23
|
+
|
24
|
+
Reckoner will read its configuration information from either
|
25
|
+
the CONFIG FILE argument or from stdin.
|
26
|
+
|
27
|
+
*Options*
|
28
|
+
|
29
|
+
-d --debug Output debugging information
|
30
|
+
|
31
|
+
-q --quiet Suppress the check output
|
32
|
+
|
33
|
+
-c --checks=FILE Specifies a Ruby file that contains
|
34
|
+
additional checks that can be performed.
|
35
|
+
|
36
|
+
== REQUIREMENTS:
|
37
|
+
|
38
|
+
* Ruby
|
39
|
+
|
40
|
+
== INSTALL:
|
41
|
+
|
42
|
+
* sudo gem install reckoner
|
43
|
+
|
44
|
+
== LICENSE:
|
45
|
+
|
46
|
+
(The MIT License)
|
47
|
+
|
48
|
+
Copyright (c) 2009 Geoff Kloess
|
49
|
+
|
50
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
51
|
+
a copy of this software and associated documentation files (the
|
52
|
+
'Software'), to deal in the Software without restriction, including
|
53
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
54
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
55
|
+
permit persons to whom the Software is furnished to do so, subject to
|
56
|
+
the following conditions:
|
57
|
+
|
58
|
+
The above copyright notice and this permission notice shall be
|
59
|
+
included in all copies or substantial portions of the Software.
|
60
|
+
|
61
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
62
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
63
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
64
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
65
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
66
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
67
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
data/bin/reckoner
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'find'
|
2
|
+
|
3
|
+
# The parent class of all checks. It provides all necessary
|
4
|
+
# logic except for the check itself.
|
5
|
+
class AbstractCheck
|
6
|
+
attr_reader :errors
|
7
|
+
attr_reader :path
|
8
|
+
@@classes = []
|
9
|
+
|
10
|
+
def self.inherited(c)
|
11
|
+
@@classes << c
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.children
|
15
|
+
return @@classes
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.check_name(name)
|
19
|
+
self.class_eval("def self.get_check_name; '#{name}'; end")
|
20
|
+
end
|
21
|
+
|
22
|
+
# Returns the name of the check. By default it un-camel-cases the class
|
23
|
+
# name. (Code taken from the underscore method of Rails' Inflector Module.)
|
24
|
+
def self.get_check_name
|
25
|
+
self.name.gsub(/::/, '/').
|
26
|
+
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
|
27
|
+
gsub(/([a-z\d])([A-Z])/,'\1_\2').
|
28
|
+
tr("-", "_").
|
29
|
+
downcase
|
30
|
+
end
|
31
|
+
|
32
|
+
# Performs simple unit parsing of strings of the form "[number] [units]".
|
33
|
+
#
|
34
|
+
# The unit hash contains a hash where the keys are regular expressions
|
35
|
+
# that match a unit. Each value is a factor that will be multiplied against
|
36
|
+
# the parsed number.
|
37
|
+
#
|
38
|
+
# Example converting to inches (12 inches = 1 foot):
|
39
|
+
# uhash = { /^inch/ => 1, /^feet/ => 12, /^foot/ => 12, 'default' => 1}
|
40
|
+
#
|
41
|
+
# * AbstractCheck.unit_parse("1",uhash) #=> 1
|
42
|
+
# * AbstractCheck.unit_parse("1 inch",uhash) #=> 1
|
43
|
+
# * AbstractCheck.unit_parse("1 foot",uhash) #=> 12
|
44
|
+
# * AbstractCheck.unit_parse("2 feet",uhash) #=> 24 inches
|
45
|
+
#
|
46
|
+
def unit_parse(content,unit_hash)
|
47
|
+
m = /(\d*\.?\d+)\s*(.*)/.match content
|
48
|
+
raise "Could not parse '#{content}'" unless m
|
49
|
+
if m.length == 2
|
50
|
+
factor = unit_hash['default'] || 1
|
51
|
+
elsif m.length == 3
|
52
|
+
key,factor = unit_hash.detect{|key,value| key.match(m[2])}
|
53
|
+
raise "Invalid unit in '#{content}'" unless factor
|
54
|
+
end
|
55
|
+
if /\./.match m[1]
|
56
|
+
m[1].to_f * factor
|
57
|
+
else
|
58
|
+
m[1].to_i * factor
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Creates the check object, requires a path to check as the argument.
|
63
|
+
def initialize(file_path)
|
64
|
+
@errors = []
|
65
|
+
@path = RFile.new(file_path)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Called by the Recokoner class to run the check
|
69
|
+
def run_check(options)
|
70
|
+
check(@path,options)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Adds an error message for this check. Use this if your check finds that
|
74
|
+
# the check is invalid.
|
75
|
+
def add_error(msg)
|
76
|
+
@errors << "#{self.class.get_check_name.capitalize} found a problem with '#{@path.path}': #{msg}"
|
77
|
+
end
|
78
|
+
|
79
|
+
# Override this method to perform your custom check. Its first argument is
|
80
|
+
# a RFile path object, the second argument is any options passed in along with the
|
81
|
+
# check in the YAML file.
|
82
|
+
def check(path_obj,options)
|
83
|
+
raise "Called abstract methd: check"
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
data/lib/main.rb
ADDED
@@ -0,0 +1,141 @@
|
|
1
|
+
require 'net/smtp'
|
2
|
+
require 'rubygems'
|
3
|
+
require 'yaml'
|
4
|
+
require 'optparse'
|
5
|
+
require 'ftools'
|
6
|
+
|
7
|
+
require 'rfile.rb'
|
8
|
+
require 'abstract_check.rb'
|
9
|
+
require 'reckoner'
|
10
|
+
require 'sendmail'
|
11
|
+
require 'sample'
|
12
|
+
|
13
|
+
=begin rdoc
|
14
|
+
The Main class ties together the core Reckoner functionality
|
15
|
+
with the command line arguments, configuration file and email.
|
16
|
+
=end
|
17
|
+
|
18
|
+
class Main
|
19
|
+
include SampleFile
|
20
|
+
|
21
|
+
DEFAULT_ARGUMENTS = {
|
22
|
+
'debug' => false,
|
23
|
+
'quiet' => false,
|
24
|
+
'extra_checks' => [],
|
25
|
+
'sample' => nil,
|
26
|
+
'email' => true
|
27
|
+
}
|
28
|
+
|
29
|
+
def self.error_out(message)
|
30
|
+
STDERR.puts "Aborting Reckoner operations!"
|
31
|
+
STDERR.puts message
|
32
|
+
exit 1
|
33
|
+
end
|
34
|
+
|
35
|
+
def initialize(argv)
|
36
|
+
@argv = argv
|
37
|
+
@arguments = DEFAULT_ARGUMENTS.dup
|
38
|
+
@opt_parser = OptionParser.new do |o|
|
39
|
+
o.banner = "Usage: #{$0} [options] [config_file]"
|
40
|
+
o.on('-d', '--debug', 'Output debugging information') do |d|
|
41
|
+
@arguments['debug'] = d
|
42
|
+
end
|
43
|
+
o.on('-q', '--quiet', 'Do not print final report') do |q|
|
44
|
+
@arguments['quiet'] = q
|
45
|
+
end
|
46
|
+
o.on('-e', '--email-off', 'Do not send email, even if configured') do |q|
|
47
|
+
@arguments['email'] = !q
|
48
|
+
end
|
49
|
+
o.on('-c','--checks=FILE', 'Additional checks file') do |c|
|
50
|
+
@arguments['extra_checks'] << c
|
51
|
+
end
|
52
|
+
o.on('-s FILE','--sample-config FILE', 'Create a sample config named FILE') do |sample|
|
53
|
+
@arguments['sample'] = sample
|
54
|
+
end
|
55
|
+
end
|
56
|
+
@opt_parser.parse!(@argv)
|
57
|
+
end
|
58
|
+
|
59
|
+
def run_reckoner
|
60
|
+
if @arguments['sample']
|
61
|
+
return(make_sample_file(@arguments['sample']))
|
62
|
+
end
|
63
|
+
|
64
|
+
if @argv.length > 1
|
65
|
+
Main.error_out "Aborting Reckoner: Too many arguments\n" + @opt_parser.help
|
66
|
+
end
|
67
|
+
|
68
|
+
if @argv.empty?
|
69
|
+
config_file = STDIN.read
|
70
|
+
else
|
71
|
+
config_file = File.read(@argv[0])
|
72
|
+
end
|
73
|
+
|
74
|
+
run_config(config_file)
|
75
|
+
end
|
76
|
+
|
77
|
+
def run_config(config_file)
|
78
|
+
begin
|
79
|
+
yaml_tree = YAML::load(config_file)
|
80
|
+
rescue ArgumentError => e
|
81
|
+
Main.error_out "There was an error parsing the YAML configuration file."
|
82
|
+
end
|
83
|
+
|
84
|
+
cm = Reckoner.new(@arguments['extra_checks'],@arguments['debug'])
|
85
|
+
cm.check(yaml_tree['check'])
|
86
|
+
|
87
|
+
if yaml_tree['mail'] && @arguments['email']
|
88
|
+
send_email( cm, yaml_tree['mail'])
|
89
|
+
end
|
90
|
+
|
91
|
+
if @arguments['quiet']
|
92
|
+
return nil
|
93
|
+
else
|
94
|
+
return output_results(cm)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def send_email(cm,mail_config)
|
99
|
+
raise "Mail section must have a 'to' parameter" unless mail_config['to']
|
100
|
+
mailobj = Sendmail.new(mail_config['sendmail_path'],
|
101
|
+
mail_config['sendmail_options'])
|
102
|
+
|
103
|
+
from = mail_config['from'] || ENV['USER']
|
104
|
+
always_mail = mail_config['always_mail'] || true
|
105
|
+
subject = mail_config['subject_prefix'] || 'Reckoner:'
|
106
|
+
subject.strip!
|
107
|
+
subject << ' '
|
108
|
+
|
109
|
+
if !cm.errors.empty?
|
110
|
+
subject << "Found #{cm.errors.length} problem(s)"
|
111
|
+
body = cm.errors.join("\n")
|
112
|
+
mailobj.send(mail_config['to'],from,subject,body)
|
113
|
+
elsif always_mail
|
114
|
+
subject << "No problems found"
|
115
|
+
body = "No problems found"
|
116
|
+
mailobj.send(mail_config['to'],from,subject,body)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def output_results(cm)
|
121
|
+
s = String.new
|
122
|
+
if cm.errors.empty?
|
123
|
+
s = "No failed checks"
|
124
|
+
else
|
125
|
+
s = "#{cm.errors.length} problem(s) found!\n"
|
126
|
+
cm.errors.each do |err|
|
127
|
+
s << err + "\n"
|
128
|
+
end
|
129
|
+
end
|
130
|
+
return s
|
131
|
+
end
|
132
|
+
|
133
|
+
def make_sample_file(file_name)
|
134
|
+
out = File.open(file_name,'w')
|
135
|
+
out.write SAMPLE_CONFIGURATION_FILE
|
136
|
+
out.close
|
137
|
+
"Generated file #{file_name}"
|
138
|
+
end
|
139
|
+
|
140
|
+
|
141
|
+
end
|
data/lib/reckoner.rb
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
|
2
|
+
=begin rdoc
|
3
|
+
The Reckoner class is intialized with the configuration information,
|
4
|
+
loads the checks and performs them.
|
5
|
+
=end
|
6
|
+
class Reckoner
|
7
|
+
VERSION = '0.4.0'
|
8
|
+
|
9
|
+
attr_accessor :debug
|
10
|
+
attr_reader :errors
|
11
|
+
|
12
|
+
# Creates the class. The first argument is an array of custom check files,
|
13
|
+
# if any. The debug arguments determines whether or not debugging information
|
14
|
+
# will be printed out during execution.
|
15
|
+
def initialize(custom_check_files = nil,debug = false)
|
16
|
+
@debug = debug
|
17
|
+
@errors = []
|
18
|
+
@registered_checks = {}
|
19
|
+
load_checks(custom_check_files)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Loads the checks from the standard_checks file and the
|
23
|
+
def load_checks(custom_check_files)
|
24
|
+
#load the standard checks
|
25
|
+
require 'standard_checks.rb'
|
26
|
+
|
27
|
+
#load the custom check files from the arguments
|
28
|
+
custom_check_files.each{|f| require f } if custom_check_files
|
29
|
+
|
30
|
+
AbstractCheck.children.each do |chk|
|
31
|
+
@registered_checks[chk.get_check_name] = chk
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Goes through the passed in check hash and begins the checking
|
36
|
+
# process. Ensures that the files exist before the other checks
|
37
|
+
# are called.
|
38
|
+
def check(check_hash)
|
39
|
+
unless check_hash
|
40
|
+
raise "No checks to perform - check that your configuration " +
|
41
|
+
"file is formatted correctly"
|
42
|
+
return
|
43
|
+
end
|
44
|
+
|
45
|
+
default_checks = check_hash.delete('default_check') || {}
|
46
|
+
puts "Using defaults: #{default_checks.inspect}" if @debug && !default_checks.empty?
|
47
|
+
|
48
|
+
check_hash.each do |block,checks|
|
49
|
+
puts "\n'#{block}'" if @debug
|
50
|
+
|
51
|
+
checks = default_checks.merge(checks)
|
52
|
+
puts " checks: #{checks.inspect}" if @debug
|
53
|
+
|
54
|
+
files = checks.delete('files') || checks.delete('file')
|
55
|
+
unless files
|
56
|
+
raise "No file entries found in block '#{block}'"
|
57
|
+
end
|
58
|
+
|
59
|
+
base_path = checks.delete('base_path')
|
60
|
+
|
61
|
+
files.each do |f|
|
62
|
+
if base_path
|
63
|
+
file_path = File.join(base_path,f.strip)
|
64
|
+
else
|
65
|
+
file_path = f.strip
|
66
|
+
end
|
67
|
+
if File.exists?(file_path) && File.readable?(file_path)
|
68
|
+
puts " Checking file '#{file_path}'" if @debug
|
69
|
+
run_checks(block,file_path,checks)
|
70
|
+
else
|
71
|
+
@errors << "Reckoner found a problem with '#{file_path}': file " +
|
72
|
+
"does not exist or is not readable by this user"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
# Runs a set of checks on a file path
|
81
|
+
def run_checks(block,file_path,checks)
|
82
|
+
checks.each do |chk|
|
83
|
+
puts " Running check #{chk.inspect} on #{file_path}" if @debug
|
84
|
+
|
85
|
+
check_name = chk[0]
|
86
|
+
check_options = chk[1]
|
87
|
+
|
88
|
+
if @registered_checks[check_name]
|
89
|
+
check_obj = @registered_checks[check_name].new(file_path)
|
90
|
+
check_obj.run_check(check_options.to_s)
|
91
|
+
@errors = @errors + check_obj.errors
|
92
|
+
else
|
93
|
+
raise "Invalid check name: #{check_name}, " +
|
94
|
+
"Known checks: #{@registered_checks.keys.join(', ')}"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
data/lib/rfile.rb
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
|
2
|
+
# RFile provides a unified and easy way to check various
|
3
|
+
# attributes of a specific file or path. It provides simple
|
4
|
+
# access to various methods from the core File class.
|
5
|
+
#
|
6
|
+
# It also offers several methods that provide simplified
|
7
|
+
# Find functionality.
|
8
|
+
class RFile
|
9
|
+
attr_reader :path
|
10
|
+
|
11
|
+
%w(<=> atime blksize blockdev? blocks chardev? ctime
|
12
|
+
dev dev_major dev_minor directory? executable? executable_real?
|
13
|
+
file? ftype gid grpowned? ino inspect mode mtime new nlink owned? pipe?
|
14
|
+
pretty_print rdev rdev_major rdev_minor readable? readable_real?
|
15
|
+
setgid? setuid? size size? socket? sticky? symlink? uid writable?
|
16
|
+
writable_real? zero?).each do |m|
|
17
|
+
define_method m do |*args|
|
18
|
+
stat.send m, *args
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
%w(basename dirname expand_path extname lstat ).each do |m|
|
23
|
+
define_method m do
|
24
|
+
File.send m, @path
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Lazy loading of the stat object.
|
29
|
+
def stat
|
30
|
+
unless instance_variables.include?(:stat)
|
31
|
+
@stat = File.stat(@path)
|
32
|
+
end
|
33
|
+
@stat
|
34
|
+
end
|
35
|
+
|
36
|
+
def initialize(path)
|
37
|
+
@path = path
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_s
|
41
|
+
@path
|
42
|
+
end
|
43
|
+
|
44
|
+
def sub_nodes(options = {})
|
45
|
+
opts = { :directories => true,
|
46
|
+
:files => true }.merge(options)
|
47
|
+
Find.find(@path) do |path|
|
48
|
+
if ((File.file?(path) && opts[:files]) ||
|
49
|
+
(File.directory?(path) && opts[:directories]))
|
50
|
+
yield RFile.new(path)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Loops through all sub-directories and yields to a block
|
56
|
+
# for each node, passing in an RFile object for the current path.
|
57
|
+
# Returns true if any yield returns true, otherwise returns false.
|
58
|
+
#
|
59
|
+
# Useful for efficiently determining if any
|
60
|
+
# sub-directories or their files meet a user-defined criteria.
|
61
|
+
def any_sub_node?(options={})
|
62
|
+
sub_nodes(options) do |rf|
|
63
|
+
return true if yield(rf)
|
64
|
+
end
|
65
|
+
return false
|
66
|
+
end
|
67
|
+
|
68
|
+
# Loops through all sub-directories and yields to a block
|
69
|
+
# for each node, passing in an RFile object for the current path.
|
70
|
+
# Returns true if all yields returns true, otherwise returns false.
|
71
|
+
#
|
72
|
+
# Useful for efficiently determining if any
|
73
|
+
# sub-directories or their files meet a user-defined criteria.
|
74
|
+
def all_sub_nodes?(options={})
|
75
|
+
sub_nodes(options) do |rf|
|
76
|
+
return false unless yield(rf)
|
77
|
+
end
|
78
|
+
return true
|
79
|
+
end
|
80
|
+
|
81
|
+
# Returns an array with an RFile object for every sub-directory
|
82
|
+
# and file below the current path.
|
83
|
+
def sub_node_array(options={})
|
84
|
+
a = Array.new
|
85
|
+
sub_nodes(options) do |rf|
|
86
|
+
a << rf
|
87
|
+
end
|
88
|
+
return a
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
data/lib/sample.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
module SampleFile
|
2
|
+
SAMPLE_CONFIGURATION_FILE = <<SAMPLE
|
3
|
+
# SAMPLE RECKONER CONFIGURATION FILES
|
4
|
+
# Reckoner uses a YAML formatted file to define the checks
|
5
|
+
# that it should preform. The file has two sections,
|
6
|
+
# the 'check' section and the 'mail' section.
|
7
|
+
|
8
|
+
# The check section contains a list of check blocks, each
|
9
|
+
# of which defines one set of checks.
|
10
|
+
check:
|
11
|
+
|
12
|
+
# Define default settings for all of your check blocks.
|
13
|
+
# This default check sets my home directory as a default
|
14
|
+
# base path for my checks.
|
15
|
+
#
|
16
|
+
# The default_check block is not required.
|
17
|
+
default_check:
|
18
|
+
base_path: /home/geoffk
|
19
|
+
|
20
|
+
# This is the simplest possible check. It simply ensures
|
21
|
+
# that there exists a file named '.bash_history'. Since this
|
22
|
+
# checks doesn't overwrite the default 'base_path' it will
|
23
|
+
# check for this file in '/home/geoffk'
|
24
|
+
geoff_check:
|
25
|
+
files: .bash_history
|
26
|
+
|
27
|
+
# This check block, named 'etc-files', sets it's own base
|
28
|
+
# path of '/etc' and then checks that two files,
|
29
|
+
# 'redhat-release' and 'inittab', exist there. Note the
|
30
|
+
# use of square brackets and the comma to make a list of files.
|
31
|
+
etc-files:
|
32
|
+
base_path: /etc
|
33
|
+
files: [fake-file, inittab]
|
34
|
+
|
35
|
+
# This check ensures that two files exist and that they be
|
36
|
+
# at least 1kb in size and have been updated in the last
|
37
|
+
# three days.
|
38
|
+
desktop-files:
|
39
|
+
base_path: /home/geoffk/Desktop
|
40
|
+
files: [bookmarks.html, songs.txt]
|
41
|
+
freshness: 3 days
|
42
|
+
minimum_size: 1 kb
|
43
|
+
|
44
|
+
# The mail section is required for email notifications. The only
|
45
|
+
# required setting inside the mail section is 'to'.
|
46
|
+
mail:
|
47
|
+
to: DESTINATION EMAIL ADDRESS
|
48
|
+
#from: defaults to current user
|
49
|
+
#subject_prefix: "RECKONER:"
|
50
|
+
#sendmail_path: sendmail
|
51
|
+
#sendmail_options: -i -t
|
52
|
+
#always_email: true
|
53
|
+
|
54
|
+
SAMPLE
|
55
|
+
end
|
data/lib/sendmail.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
|
2
|
+
# Provides sendmail functionality for the reckoner script.
|
3
|
+
class Sendmail
|
4
|
+
|
5
|
+
# Creates the class with a path to the binary (if any is
|
6
|
+
# given) and options.
|
7
|
+
def initialize(path,options)
|
8
|
+
@path = path
|
9
|
+
@options = options
|
10
|
+
end
|
11
|
+
|
12
|
+
#Sends the email.
|
13
|
+
def send(to,from,subject,message)
|
14
|
+
path = @path || 'sendmail'
|
15
|
+
options = @options || '-i -t'
|
16
|
+
|
17
|
+
cmd = %|#{path} #{options}|
|
18
|
+
|
19
|
+
body = "To: #{to}\n"
|
20
|
+
body << "From: #{from}\n"
|
21
|
+
body << "Subject: #{subject}\n"
|
22
|
+
body << "\n#{message}"
|
23
|
+
|
24
|
+
io = IO.popen(cmd,'w')
|
25
|
+
io.puts body
|
26
|
+
io.close
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
# Ensures that a file is not too old.
|
4
|
+
class Freshness < AbstractCheck
|
5
|
+
SECONDS_IN_HOUR = 60 * 60
|
6
|
+
HOUR_HASH = {'default' => 24,
|
7
|
+
/^hour/i => 1,
|
8
|
+
/^day/i => 24,
|
9
|
+
/^week/i => 7*24,
|
10
|
+
/^month/i => 30*24,
|
11
|
+
/^year/i => 365*24}
|
12
|
+
|
13
|
+
def check(path_obj,options)
|
14
|
+
hours = unit_parse(options,HOUR_HASH)
|
15
|
+
|
16
|
+
old_time = Time.now - (hours * SECONDS_IN_HOUR)
|
17
|
+
|
18
|
+
unless path_obj.any_sub_node?{|f| f.mtime > old_time}
|
19
|
+
add_error('file is too old')
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Ensures that the size of the file is not
|
25
|
+
# too small.
|
26
|
+
class MinimumSize < AbstractCheck
|
27
|
+
BYTES_HASH = {'default' => 1,
|
28
|
+
/^b/i => 1,
|
29
|
+
/^kb/i => 1024,
|
30
|
+
/^mb/i => 1024**2,
|
31
|
+
/^gb/i => 1024**3}
|
32
|
+
|
33
|
+
def check(path_obj,options)
|
34
|
+
bytes = unit_parse(options,BYTES_HASH)
|
35
|
+
unless path_obj.size >= bytes
|
36
|
+
add_error("file is too small")
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
Binary file
|
data/test/support.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
####
|
2
|
+
# Offers support for running backup check tests
|
3
|
+
|
4
|
+
require 'fileutils'
|
5
|
+
|
6
|
+
module BackupCheckSupport
|
7
|
+
include FileUtils
|
8
|
+
|
9
|
+
ROOT = '/tmp/file_verifier_tests'
|
10
|
+
|
11
|
+
#Makes a set of file that meet the specified options
|
12
|
+
def makef(root,name,options = {})
|
13
|
+
mkdir root unless File.exists?(root.to_s)
|
14
|
+
if name.is_a? Hash
|
15
|
+
name.each do |k,n|
|
16
|
+
makef File.join(root,k.to_s),n,options
|
17
|
+
end
|
18
|
+
elsif name.is_a? Array
|
19
|
+
name.each do |n|
|
20
|
+
makef(root,n,options)
|
21
|
+
end
|
22
|
+
elsif name.is_a?(String) || name.is_a?(Symbol)
|
23
|
+
path = File.join(root,name.to_s)
|
24
|
+
touch path
|
25
|
+
File.chmod(options[:chmod],path) if options[:chmod]
|
26
|
+
File.utime(options[:atime],options[:atime],path) if options[:atime]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
#Converts days to seconds
|
31
|
+
def d2s(days)
|
32
|
+
days * 24 * 60 * 60
|
33
|
+
end
|
34
|
+
|
35
|
+
#Determines if the object has any errors matching the regular
|
36
|
+
#expression.
|
37
|
+
def has_error(cmain,reg)
|
38
|
+
cmain.errors.any?{|msg| reg.match msg}
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'test/support'
|
2
|
+
require 'lib/main'
|
3
|
+
require 'test/unit'
|
4
|
+
|
5
|
+
class TestCheck < AbstractCheck
|
6
|
+
check_name 'test_check'
|
7
|
+
end
|
8
|
+
|
9
|
+
class TestCheckTwo < AbstractCheck
|
10
|
+
end
|
11
|
+
|
12
|
+
class AbstractTest < Test::Unit::TestCase
|
13
|
+
include BackupCheckSupport
|
14
|
+
|
15
|
+
#Test that the name of a test is set correctly and
|
16
|
+
#that it can be over-ridden.
|
17
|
+
def test_abstract_check
|
18
|
+
assert_equal 'test_check', TestCheck.get_check_name
|
19
|
+
assert_equal 'test_check_two', TestCheckTwo.get_check_name
|
20
|
+
|
21
|
+
tc2 = TestCheckTwo.new('.')
|
22
|
+
assert tc2.is_a?(AbstractCheck)
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_unit_parse
|
26
|
+
uhash = { /^inch/ => 1, /^feet/ => 12, /^foot/ => 12, 'default' => 1 }
|
27
|
+
ac = AbstractCheck.new(__FILE__)
|
28
|
+
assert_equal 1, ac.unit_parse("1", uhash)
|
29
|
+
assert_equal 1, ac.unit_parse("1 inch", uhash)
|
30
|
+
assert_equal 1, ac.unit_parse("1inch", uhash)
|
31
|
+
assert_equal 1.5, ac.unit_parse("1.5inch", uhash)
|
32
|
+
assert_equal 12, ac.unit_parse("1 foot", uhash)
|
33
|
+
assert_equal 24, ac.unit_parse("2 feet", uhash)
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
data/test/test_exist.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'test/support'
|
2
|
+
require 'lib/main'
|
3
|
+
require 'test/unit'
|
4
|
+
|
5
|
+
class ExistsTest < Test::Unit::TestCase
|
6
|
+
include BackupCheckSupport
|
7
|
+
|
8
|
+
def setup
|
9
|
+
@root = '/tmp/test'
|
10
|
+
makef(@root,['one','two','three'])
|
11
|
+
end
|
12
|
+
|
13
|
+
def teardown
|
14
|
+
rm_rf @root
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_files_exist
|
18
|
+
cm = Reckoner.new()
|
19
|
+
cm.check('test'=> {'files'=>'/tmp/test/one'})
|
20
|
+
assert cm.errors.empty?
|
21
|
+
|
22
|
+
cm.check('test'=> {'files'=>['/tmp/test/one','/tmp/test/two']})
|
23
|
+
assert cm.errors.empty?
|
24
|
+
|
25
|
+
cm.check('test'=> {'base_path'=>'/tmp/test','files'=>['one','two']})
|
26
|
+
assert cm.errors.empty?
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_errors
|
30
|
+
cm = Reckoner.new(nil)
|
31
|
+
cm.check('test'=>{'files'=>'nofile'})
|
32
|
+
assert_equal 1,cm.errors.length
|
33
|
+
assert has_error(cm,/does not exist/)
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'date'
|
3
|
+
require 'test/support'
|
4
|
+
require 'lib/main'
|
5
|
+
|
6
|
+
class FreshnessTest < Test::Unit::TestCase
|
7
|
+
include BackupCheckSupport
|
8
|
+
|
9
|
+
def setup
|
10
|
+
makef(ROOT,['one','two','three',
|
11
|
+
{'dir' => ['four','five']}], :atime => Time.now - d2s(10))
|
12
|
+
makef(ROOT,'six', :atime => Time.now - d2s(1))
|
13
|
+
@cm = Reckoner.new()
|
14
|
+
end
|
15
|
+
|
16
|
+
def teardown
|
17
|
+
rm_rf ROOT
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_single_file_success
|
21
|
+
@cm.check('test'=> {'files'=>File.join(ROOT,'one'), 'freshness' => '20'})
|
22
|
+
assert @cm.errors.empty?
|
23
|
+
|
24
|
+
@cm.check('test'=> {'files'=>File.join(ROOT,'six'), 'freshness' => '30 hours'})
|
25
|
+
assert @cm.errors.empty?
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_single_file_failure
|
29
|
+
@cm.check('test'=> {'files'=>File.join(ROOT,'one'), 'freshness' => '5'})
|
30
|
+
assert_equal 1, @cm.errors.length
|
31
|
+
assert has_error(@cm,/file is too old/)
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_hour_failure
|
35
|
+
@cm.check('test'=> {'files'=>File.join(ROOT,'six'), 'freshness' => '22 hours'})
|
36
|
+
assert_equal 1, @cm.errors.length
|
37
|
+
assert has_error(@cm,/file is too old/)
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_recursive_success
|
41
|
+
hundred_days_ago = Time.now - d2s(100)
|
42
|
+
File.utime(hundred_days_ago,hundred_days_ago,ROOT)
|
43
|
+
assert_equal File.mtime(ROOT).to_s,hundred_days_ago.to_s
|
44
|
+
|
45
|
+
@cm.check('test'=> {'files'=>ROOT, 'freshness' => '20'})
|
46
|
+
assert @cm.errors.empty?
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
data/test/test_main.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'date'
|
3
|
+
require 'test/support'
|
4
|
+
require 'lib/main'
|
5
|
+
|
6
|
+
class MainTest < Test::Unit::TestCase
|
7
|
+
include BackupCheckSupport
|
8
|
+
|
9
|
+
def setup
|
10
|
+
makef(ROOT,[{'d1' => ['d1f1', 'd1f2', 'd1f3']},
|
11
|
+
'f4','h1.txt',
|
12
|
+
['f5', 'f6', 'f7'],
|
13
|
+
{'d2' => ['d2f1', 'd2f2']}])
|
14
|
+
@good_config = <<-CONFIG
|
15
|
+
check:
|
16
|
+
default_check:
|
17
|
+
base_path: #{ROOT}
|
18
|
+
f4:
|
19
|
+
files: f4
|
20
|
+
d1:
|
21
|
+
files: [d1, d1/d1f1]
|
22
|
+
CONFIG
|
23
|
+
end
|
24
|
+
|
25
|
+
def teardown
|
26
|
+
rm_rf ROOT
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_simple
|
30
|
+
m = Main.new([])
|
31
|
+
out = m.run_config(@good_config)
|
32
|
+
assert(/No failed checks/.match(out))
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_sample_config
|
36
|
+
fname = ROOT + '/sample.yaml'
|
37
|
+
m1 = Main.new(['-s',fname])
|
38
|
+
m1.run_reckoner
|
39
|
+
assert File.exists?(fname)
|
40
|
+
|
41
|
+
m2 = Main.new(['-e',fname])
|
42
|
+
out = m2.run_reckoner
|
43
|
+
assert(/Reckoner found a problem with '\/etc\/fake-file'/.match(out))
|
44
|
+
assert out.split("\n").length > 4
|
45
|
+
end
|
46
|
+
|
47
|
+
def test_additional_checks
|
48
|
+
m1 = Main.new(['-c','test/files/additional_checks.rb'])
|
49
|
+
out = m1.run_config <<-CONFIG
|
50
|
+
check:
|
51
|
+
default_check:
|
52
|
+
base_path: #{ROOT}
|
53
|
+
txt:
|
54
|
+
files: [d1, h1.txt]
|
55
|
+
extension: .txt
|
56
|
+
CONFIG
|
57
|
+
assert_equal 2,out.split("\n").length
|
58
|
+
assert(/Extension found a problem with '\/tmp\/file_verifier_tests\/d1'/.match(out))
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'date'
|
3
|
+
require 'test/support'
|
4
|
+
require 'lib/main'
|
5
|
+
|
6
|
+
class MinimumSizeTest < Test::Unit::TestCase
|
7
|
+
include BackupCheckSupport
|
8
|
+
|
9
|
+
def setup
|
10
|
+
@cm = Reckoner.new()
|
11
|
+
@bin = File.join('test','files','one-k.bin')
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_file_right_size
|
15
|
+
@cm.check('test'=> {'files'=>@bin, 'minimum_size' => '1'})
|
16
|
+
assert @cm.errors.empty?
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_file_too_small
|
20
|
+
@cm.check('test'=> {'files'=>@bin, 'minimum_size' => '2048'})
|
21
|
+
assert_equal 1, @cm.errors.length
|
22
|
+
assert has_error(@cm,/file is too small/)
|
23
|
+
end
|
24
|
+
|
25
|
+
def kilobyte_parsing
|
26
|
+
@cm.check('test'=> {'files'=>@bin, 'minimum_size' => '1kb'})
|
27
|
+
assert @cm.errors.empty?
|
28
|
+
|
29
|
+
@cm.check('test'=> {'files'=>@bin, 'minimum_size' => '2kb'})
|
30
|
+
assert_equal 1, @cm.errors.length
|
31
|
+
assert has_error(@cm,/file is too small/)
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'date'
|
3
|
+
require 'test/support'
|
4
|
+
require 'lib/main'
|
5
|
+
|
6
|
+
class ReckonerTest < Test::Unit::TestCase
|
7
|
+
include BackupCheckSupport
|
8
|
+
|
9
|
+
def setup
|
10
|
+
makef(ROOT,['one','two','three'], :atime => Time.now - d2s(10))
|
11
|
+
@cm = Reckoner.new()
|
12
|
+
end
|
13
|
+
|
14
|
+
def teardown
|
15
|
+
rm_rf ROOT
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_non_existant_check
|
19
|
+
assert_raise RuntimeError do
|
20
|
+
@cm.check('test'=> {'files'=> File.join(ROOT,'one'), 'nocheck' => '20'})
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_multiple_files
|
25
|
+
@cm.check('test'=> {'files'=> [File.join(ROOT,'one'), File.join(ROOT,'two')]})
|
26
|
+
assert @cm.errors.empty?
|
27
|
+
|
28
|
+
@cm.check('test'=> {'files'=> [File.join(ROOT,'one'),
|
29
|
+
File.join(ROOT,'blah'),
|
30
|
+
File.join(ROOT,'two'),
|
31
|
+
File.join(ROOT,'other'),
|
32
|
+
]})
|
33
|
+
assert_equal 2, @cm.errors.length
|
34
|
+
assert(/file does not exist/.match(@cm.errors[0]))
|
35
|
+
assert(/file does not exist/.match(@cm.errors[1]))
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
data/test/test_rfile.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'date'
|
3
|
+
require 'test/support'
|
4
|
+
require 'lib/main'
|
5
|
+
|
6
|
+
class RFileTest < Test::Unit::TestCase
|
7
|
+
include BackupCheckSupport
|
8
|
+
|
9
|
+
def setup
|
10
|
+
makef(ROOT,[{:d1 => [:f1, :f2, :f3]},
|
11
|
+
{:d2 => [{ :d3 => :f3 }, :f4]}])
|
12
|
+
@cfile = RFile.new(ROOT)
|
13
|
+
end
|
14
|
+
|
15
|
+
def teardown
|
16
|
+
rm_rf ROOT
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_methods
|
20
|
+
assert_equal @cfile.path, ROOT
|
21
|
+
assert_equal @cfile.atime, File.atime(ROOT)
|
22
|
+
assert_equal @cfile.mtime, File.mtime(ROOT)
|
23
|
+
assert_equal @cfile.stat, File.stat(ROOT)
|
24
|
+
assert_equal @cfile.basename, File.basename(ROOT)
|
25
|
+
assert_equal @cfile.expand_path, File.expand_path(ROOT)
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_recurse_functions
|
29
|
+
assert_equal 9, @cfile.sub_node_array.length
|
30
|
+
assert_equal 5, @cfile.sub_node_array(:directories => false).length
|
31
|
+
assert_equal 4, @cfile.sub_node_array(:files => false).length
|
32
|
+
|
33
|
+
assert @cfile.all_sub_nodes?{|f| f.basename.length == 2 || f.directory? }
|
34
|
+
assert @cfile.any_sub_node?{|f| f.basename == 'f3' }
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'date'
|
3
|
+
require 'test/support'
|
4
|
+
|
5
|
+
class SupportTest < Test::Unit::TestCase
|
6
|
+
include BackupCheckSupport
|
7
|
+
|
8
|
+
def test_makef
|
9
|
+
makef(ROOT,[{'d1' => ['d1f1', 'd1f2', 'd1f3']},
|
10
|
+
'f4',
|
11
|
+
['f5', 'f6', 'f7'],
|
12
|
+
{'d2' => ['d2f1', 'd2f2']}])
|
13
|
+
|
14
|
+
assert File.exists?(File.join(ROOT,'d1'))
|
15
|
+
assert File.exists?(File.join(ROOT,'f4'))
|
16
|
+
assert File.exists?(File.join(ROOT,'f5'))
|
17
|
+
assert File.exists?(File.join(ROOT,'f6'))
|
18
|
+
assert File.exists?(File.join(ROOT,'f7'))
|
19
|
+
assert File.exists?(File.join(ROOT,'d1','d1f1'))
|
20
|
+
assert File.exists?(File.join(ROOT,'d1','d1f2'))
|
21
|
+
assert File.exists?(File.join(ROOT,'d1','d1f3'))
|
22
|
+
assert File.exists?(File.join(ROOT,'d2'))
|
23
|
+
assert File.exists?(File.join(ROOT,'d2','d2f1'))
|
24
|
+
assert File.exists?(File.join(ROOT,'d2','d2f2'))
|
25
|
+
rm_rf ROOT
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
metadata
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: reckoner
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.4.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Geoff Kloess
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-07-02 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: hoe
|
17
|
+
type: :development
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 2.2.0
|
24
|
+
version:
|
25
|
+
description: |-
|
26
|
+
Ruby Reckoner is an easily-configurable program that monitors
|
27
|
+
files and can send notifications when it finds problems.
|
28
|
+
|
29
|
+
Currently it can only check that files exist, have been updated
|
30
|
+
recently and that they have a minimum size, however it is easy to add
|
31
|
+
your own checks written in Ruby.
|
32
|
+
email:
|
33
|
+
- geoff.kloess@gmail.com
|
34
|
+
executables:
|
35
|
+
- reckoner
|
36
|
+
extensions: []
|
37
|
+
|
38
|
+
extra_rdoc_files:
|
39
|
+
- History.txt
|
40
|
+
- Manifest.txt
|
41
|
+
- README.txt
|
42
|
+
files:
|
43
|
+
- History.txt
|
44
|
+
- Manifest.txt
|
45
|
+
- README.txt
|
46
|
+
- Rakefile
|
47
|
+
- bin/reckoner
|
48
|
+
- lib/main.rb
|
49
|
+
- lib/reckoner.rb
|
50
|
+
- lib/abstract_check.rb
|
51
|
+
- lib/rfile.rb
|
52
|
+
- lib/sendmail.rb
|
53
|
+
- lib/sample.rb
|
54
|
+
- lib/standard_checks.rb
|
55
|
+
- test/support.rb
|
56
|
+
- test/test_abstract_check.rb
|
57
|
+
- test/test_exist.rb
|
58
|
+
- test/test_freshness.rb
|
59
|
+
- test/test_main.rb
|
60
|
+
- test/test_minimum_size.rb
|
61
|
+
- test/test_rfile.rb
|
62
|
+
- test/test_support.rb
|
63
|
+
- test/files/one-k.bin
|
64
|
+
- test/files/additional_checks.rb
|
65
|
+
has_rdoc: true
|
66
|
+
homepage: http://reckoner.rubyforge.com
|
67
|
+
licenses: []
|
68
|
+
|
69
|
+
post_install_message:
|
70
|
+
rdoc_options:
|
71
|
+
- --main
|
72
|
+
- README.txt
|
73
|
+
require_paths:
|
74
|
+
- lib
|
75
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
76
|
+
requirements:
|
77
|
+
- - ">="
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: "0"
|
80
|
+
version:
|
81
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
82
|
+
requirements:
|
83
|
+
- - ">="
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: "0"
|
86
|
+
version:
|
87
|
+
requirements: []
|
88
|
+
|
89
|
+
rubyforge_project: reckoner
|
90
|
+
rubygems_version: 1.3.4
|
91
|
+
signing_key:
|
92
|
+
specification_version: 3
|
93
|
+
summary: Ruby Reckoner is an easily-configurable program that monitors files and can send notifications when it finds problems
|
94
|
+
test_files:
|
95
|
+
- test/test_abstract_check.rb
|
96
|
+
- test/test_exist.rb
|
97
|
+
- test/test_freshness.rb
|
98
|
+
- test/test_main.rb
|
99
|
+
- test/test_minimum_size.rb
|
100
|
+
- test/test_reckoner.rb
|
101
|
+
- test/test_rfile.rb
|
102
|
+
- test/test_support.rb
|