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