reckoner 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,9 @@
1
+ === 0.2.0 / 2009-06-19
2
+
3
+ * Still more to do:
4
+ * Break out the stuff from bin/reckoner into class so that it
5
+ be tested more easily.
6
+
7
+ * Add some more checks
8
+
9
+
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
@@ -0,0 +1,11 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'rubygems'
4
+ require 'hoe'
5
+
6
+ Hoe.spec 'reckoner' do
7
+ # self.rubyforge_name = 'reckonerx' # if different than 'reckoner'
8
+ self.developer('Geoff Kloess', 'geoff.kloess@gmail.com')
9
+ end
10
+
11
+ # vim: syntax=ruby
data/bin/reckoner ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.unshift File.join(File.dirname(__FILE__), '../lib')
4
+ require 'main'
5
+
6
+ main = Main.new(ARGV)
7
+ output = main.run_reckoner
8
+ puts output if output
@@ -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
@@ -0,0 +1,10 @@
1
+
2
+ class Extension < AbstractCheck
3
+
4
+ def check(path_obj,options)
5
+ unless path_obj.extname == options.strip
6
+ add_error 'is not a txt file'
7
+ end
8
+ end
9
+ end
10
+
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
@@ -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
@@ -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