reckoner 0.4.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/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
|