diff options
-rwxr-xr-x | lib/puppet/type/cron.rb | 343 | ||||
-rwxr-xr-x | test/types/cron.rb | 105 |
2 files changed, 294 insertions, 154 deletions
diff --git a/lib/puppet/type/cron.rb b/lib/puppet/type/cron.rb index 88ea83b4d..e3bac17fc 100755 --- a/lib/puppet/type/cron.rb +++ b/lib/puppet/type/cron.rb @@ -116,21 +116,25 @@ module Puppet # out which event to return. Finally, call @parent.sync to write the # cron tab. def sync - @parent.store - event = nil if @is == :notfound #@is = @should event = :cron_created + # We're the first state, so if we're creating the job + # then sync all of the other states + @parent.eachstate { |state| + next if state == self + state.sync(true) + } + + @is = self.should elsif self.should == :notfound @parent.remove(true) event = :cron_deleted - elsif self.should == @is - self.err "Uh, they're both %s" % self.should + elsif self.insync? return nil else - #@is = @should - self.err "@is is %s" % @is + @is = self.should event = :cron_changed end @@ -139,6 +143,170 @@ module Puppet return event end end + + # A base class for all of the Cron parameters, since they all have + # similar argument checking going on. + class CronParam < Puppet::State + class << self + attr_reader :checking + end + + def weekdays + %w{sunday monday tuesday wednesday thursday friday saturday} + end + + def months + %w{january february march april may june july + august september october november december} + end + + # Normally this would retrieve the current value, but our state is not + # actually capable of doing so. The Cron class does the actual tab + # retrieval, so all this method does is default to :notfound for @is. + def retrieve + unless defined? @is and ! @is.nil? + @is = :notfound + end + end + + # Determine whether the cron job should be destroyed, and figure + # out which event to return. Finally, call @parent.sync to write the + # cron tab. + def sync(nostore = false) + event = nil + if @is == :notfound + @is = self.should + event = :cron_created + elsif self.should == :notfound + @parent.remove(true) + event = :cron_deleted + elsif self.insync? + return nil + else + @is = self.should + event = :cron_changed + end + + unless nostore + @parent.store + end + + return event + end + + # A method used to do parameter input handling. Converts integers + # in string form to actual integers, and returns the value if it's + # an integer or false if it's just a normal string. + def numfix(num) + if num =~ /^\d+$/ + return num.to_i + elsif num.is_a?(Integer) + return num + else + return false + end + end + + # Verify that a number is within the specified limits. Return the + # number if it is, or false if it is not. + def limitcheck(num, lower, upper) + if num >= lower and num <= upper + return num + else + return false + end + end + + # Verify that a value falls within the specified array. Does case + # insensitive matching, and supports matching either the entire word + # or the first three letters of the word. + def alphacheck(value, ary) + tmp = value.downcase + if tmp.length == 3 + ary.each_with_index { |name, index| + if name =~ /#{tmp}/i + return index + end + } + else + if ary.include?(tmp) + return ary.index(tmp) + end + end + + return false + end + + def should_to_s + if @should.empty? + return "*" + else + return @should.join(",") + end + end + + # The method that does all of the actual parameter value + # checking; called by all of the +param<name>=+ methods. + # Requires the value, type, and bounds, and optionally supports + # a boolean of whether to do alpha checking, and if so requires + # the ary against which to do the checking. + def validate(value, lower, upper, arymethod = nil) + retval = nil + if num = numfix(value) + retval = limitcheck(num, lower, upper) + elsif arymethod + retval = alphacheck(value, send(arymethod)) + end + + if retval + return retval + else + raise Puppet::Error, "%s is not a valid %s" % + [value, self.class.name] + end + end + + def shouldprocess(value) + val = validate(value, *self.class.checking) + return val.to_s + end + end + + class CronMinute < CronParam + @name = :minute + @checking = [0, 59] + @doc = "The minute at which to run the cron job. + Optional; if specified, must be between 0 and 59, inclusive." + end + + class CronHour < CronParam + @name = :hour + @checking = [0, 23] + @doc = "The hour at which to run the cron job. Optional; + if specified, must be between 0 and 23, inclusive." + end + + class CronWeekday < CronParam + @name = :weekday + @checking = [0, 6, :weekdays] + @doc = "The weekday on which to run the command. + Optional; if specified, must be between 0 and 6, inclusive, with + 0 being Sunday, or must be the name of the day (e.g., Tuesday)." + end + + class CronMonth < CronParam + @name = :month + @checking = [1, 12, :months] + @doc = "The month of the year. Optional; if specified + must be between 1 and 12 or the month name (e.g., December)." + end + + class CronMonthDay < CronParam + @name = :monthday + @checking = [1, 31] + @doc = "The day of the month on which to run the + command. Optional; if specified, must be between 1 and 31." + end end class Type @@ -148,17 +316,16 @@ module Puppet # and is used to manage the job. class Cron < Type @states = [ - Puppet::State::CronCommand + Puppet::State::CronCommand, + Puppet::State::CronMinute, + Puppet::State::CronHour, + Puppet::State::CronWeekday, + Puppet::State::CronMonth, + Puppet::State::CronMonthDay ] - @parameters = [ - :name, :user, - :minute, - :hour, - :weekday, - :month, - :monthday + :name ] @paramdoc[:name] = "The symbolic name of the cron job. This name @@ -166,17 +333,6 @@ module Puppet @paramdoc[:user] = "The user to run the command as. This user must be allowed to run cron jobs, which is not currently checked by Puppet." - @paramdoc[:minute] = "The minute at which to run the cron job. - Optional; if specified, must be between 0 and 59, inclusive." - @paramdoc[:hour] = "The hour at which to run the cron job. Optional; - if specified, must be between 0 and 23, inclusive." - @paramdoc[:weekday] = "The weekday on which to run the command. - Optional; if specified, must be between 0 and 6, inclusive, with - 0 being Sunday, or must be the name of the day (e.g., Tuesday)." - @paramdoc[:month] = "The month of the year. Optional; if specified - must be between 1 and 12 or the month name (e.g., December)." - @paramdoc[:monthday] = "The day of the month on which to run the - command. Optional; if specified, must be between 1 and 31." @doc = "Installs and manages cron jobs. All fields except the command and the user are optional, although specifying no periodic @@ -196,11 +352,6 @@ module Puppet @instances = {} - @@weekdays = %w{sunday monday tuesday wednesday thursday friday saturday} - - @@months = %w{january february march april may june july - august september october november december} - case Facter["operatingsystem"].value when "SunOS": @crontype = Puppet::CronType::SunOS @@ -289,17 +440,21 @@ module Puppet # add other comments to the list as they are @instances[user] << line else - ary = line.split(" ") - fields().each { |param| - value = ary.shift - unless value == "*" - hash[param] = value - end - } - - if ary.length > 0 - hash[:command] += " " + ary.join(" ") + if match = /^(\S+) (\S+) (\S+) (\S+) (\S+) (.+)$/.match(line) + fields().zip(match.captures).each { |param, value| + unless value == "*" + unless param == :command + if value =~ /,/ + value = value.split(",") + end + end + hash[param] = value + end + } + else + raise Puppet::Error, "Could not match '%s'" % line end + cron = nil unless name Puppet.info "Autogenerating name for %s" % hash[:command] @@ -348,7 +503,6 @@ module Puppet # method. Returns nil if there was no cron job; else, returns the # number of cron instances found. def self.retrieve(user) - #%x{crontab -u #{user} -l 2>/dev/null}.split("\n").each { |line| text = @crontype.read(user) if $? != 0 # there is no cron file @@ -398,99 +552,6 @@ module Puppet end end - # A method used to do parameter input handling. Converts integers - # in string form to actual integers, and returns the value if it's - # an integer or false if it's just a normal string. - def numfix(num) - if num =~ /^\d+$/ - return num.to_i - elsif num.is_a?(Integer) - return num - else - return false - end - end - - # Verify that a number is within the specified limits. Return the - # number if it is, or false if it is not. - def limitcheck(num, lower, upper) - if num >= lower and num <= upper - return num - else - return false - end - end - - # Verify that a value falls within the specified array. Does case - # insensitive matching, and supports matching either the entire word - # or the first three letters of the word. - def alphacheck(value, ary) - tmp = value.downcase - if tmp.length == 3 - ary.each_with_index { |name, index| - if name =~ /#{tmp}/i - return index - end - } - else - if ary.include?(tmp) - return ary.index(tmp) - end - end - - return false - end - - # The method that does all of the actual parameter value checking; called - # by all of the +param<name>=+ methods. Requires the value, type, and - # bounds, and optionally supports a boolean of whether to do alpha - # checking, and if so requires the ary against which to do the checking. - def parameter(values, type, lower, upper, alpha = nil, ary = nil) - unless values.is_a?(Array) - if values =~ /,/ - values = values.split(/,/) - else - values = [values] - end - end - - @parameters[type] = values.collect { |value| - retval = nil - if num = numfix(value) - retval = limitcheck(num, lower, upper) - elsif alpha - retval = alphacheck(value, ary) - end - - if retval - @parameters[type] = retval - else - raise Puppet::Error, "%s is not a valid %s" % - [value, type] - end - } - end - - def paramminute=(value) - parameter(value, :minute, 0, 59) - end - - def paramhour=(value) - parameter(value, :hour, 0, 23) - end - - def paramweekday=(value) - parameter(value, :weekday, 0, 6, true, @@weekdays) - end - - def parammonth=(value) - parameter(value, :month, 1, 12, true, @@months) - end - - def parammonthday=(value) - parameter(value, :monthday, 1, 31) - end - def paramuser=(user) require 'etc' @@ -511,7 +572,7 @@ module Puppet end self.class.retrieve(@parameters[:user]) - @states[:command].retrieve + self.eachstate { |st| st.retrieve } end # Write the entire user's cron tab out. @@ -523,17 +584,17 @@ module Puppet # as a comment above the cron job, in the form '# Puppet Name: <name>'. def to_cron hash = {:command => @states[:command].should || @states[:command].is } + + # Collect all of the values that we have self.class.fields().reject { |f| f == :command }.each { |param| - hash[param] = @parameters[param] || "*" + if @states.include?(param) + hash[param] = @states[param].should_to_s + end } return "# Puppet Name: %s\n" % self.name + self.class.fields.collect { |f| - if hash[f].is_a?(Array) - hash[f].join(",") - else - hash[f] - end + hash[f] || "*" }.join(" ") end end diff --git a/test/types/cron.rb b/test/types/cron.rb index bcd5be93c..1a60eecd3 100755 --- a/test/types/cron.rb +++ b/test/types/cron.rb @@ -12,7 +12,15 @@ require 'puppet/type/cron' require 'test/unit' require 'facter' -class TestExec < Test::Unit::TestCase + +# Here we just want to unit-test our cron type, to verify that +class TestCronType < Test::Unit::TestCase + include TestPuppet + + +end + +class TestCron < Test::Unit::TestCase include TestPuppet def setup super @@ -28,6 +36,38 @@ class TestExec < Test::Unit::TestCase end # god i'm lazy @crontype = Puppet::Type::Cron + + # Here we just create a fake cron type that answers to all of the methods + # but does not modify our actual system. + unless defined? @fakecrontype + @fakecrontype = Class.new { + @tabs = Hash.new("") + def self.clear + @tabs = Hash.new("") + end + + def self.read(user) + @tabs[user] + end + + def self.write(user, text) + @tabs[user] = text + end + + def self.remove(user) + @tabs.delete(user) + end + } + + @oldcrontype = @crontype.crontype + @crontype.crontype = @fakecrontype + end + end + + def teardown + @crontype.crontype = @oldcrontype + @fakecrontype.clear + super end # Back up the user's existing cron tab if they have one. @@ -107,7 +147,7 @@ class TestExec < Test::Unit::TestCase assert_nothing_raised { cron = @crontype.create( :name => name, - :command => "date", + :command => "date > /dev/null", :user => @me ) } @@ -116,9 +156,54 @@ class TestExec < Test::Unit::TestCase assert_nothing_raised { str = cron.to_cron } - assert_equal(str, "# Puppet Name: #{name}\n* * * * * date", + + assert_equal(str, "# Puppet Name: #{name}\n* * * * * date > /dev/null", "Cron did not generate correctly") end + + # Test that changing any field results in the cron tab being rewritten. + # it directly + def test_any_field_changes + cron = nil + # make the cron + name = "yaytest" + assert_nothing_raised { + cron = @crontype.create( + :name => name, + :command => "date > /dev/null", + :month => "May", + :user => @me + ) + } + assert_nothing_raised { + cron.sync + } + assert_nothing_raised { + cron[:month] = "June" + } + comp = newcomp(cron) + + assert_events(comp, [:cron_changed], "did not change cron job") + end + + # Test that a cron job with spaces at the end doesn't get rewritten + def test_trailingspaces + cron = nil + # make the cron + name = "yaytest" + assert_nothing_raised { + cron = @crontype.create( + :name => name, + :command => "date > /dev/null ", + :month => "May", + :user => @me + ) + } + comp = newcomp(cron) + + assert_events(comp, [:cron_created], "did not create cron job") + assert_events(comp, [], "cron job got rewritten") + end # Test that comments are correctly retained def test_retain_comments @@ -162,16 +247,13 @@ class TestExec < Test::Unit::TestCase # Test adding a cron when there is currently no file. def test_mkcronwithnotab - cronback Puppet::Type::Cron.crontype.remove(@me) cron = mkcron("testwithnotab") cyclecron(cron) - cronrestore end def test_mkcronwithtab - cronback Puppet::Type::Cron.crontype.remove(@me) Puppet::Type::Cron.crontype.write(@me, "1 1 1 1 * date > %s/crontesting\n" % testdir() @@ -179,11 +261,9 @@ class TestExec < Test::Unit::TestCase cron = mkcron("testwithtab") cyclecron(cron) - cronrestore end def test_makeandretrievecron - cronback Puppet::Type::Cron.crontype.remove(@me) name = "storeandretrieve" @@ -198,20 +278,19 @@ class TestExec < Test::Unit::TestCase assert(cron = Puppet::Type::Cron[name], "Could not retrieve named cron") assert_instance_of(Puppet::Type::Cron, cron) - cronrestore end # Do input validation testing on all of the parameters. def test_arguments values = { :monthday => { - :valid => [ 1, 13, "1,30" ], + :valid => [ 1, 13, "1" ], :invalid => [ -1, 0, 32 ] }, :weekday => { - :valid => [ 0, 3, 6, "1,2", "tue", "wed", + :valid => [ 0, 3, 6, "1", "tue", "wed", "Wed", "MOnday", "SaTurday" ], - :invalid => [ -1, 7, "1, 3", "tues", "teusday", "thurs" ] + :invalid => [ -1, 7, "13", "tues", "teusday", "thurs" ] }, :hour => { :valid => [ 0, 21, 23 ], @@ -238,7 +317,7 @@ class TestExec < Test::Unit::TestCase } if value.is_a?(Integer) - assert_equal([value], cron[param], + assert_equal(value.to_s, cron.should(param), "Cron value was not set correctly") end when :invalid: |