diff options
| author | luke <luke@980ebf18-57e1-0310-9a29-db15c13687c0> | 2006-01-23 22:38:39 +0000 |
|---|---|---|
| committer | luke <luke@980ebf18-57e1-0310-9a29-db15c13687c0> | 2006-01-23 22:38:39 +0000 |
| commit | 18e8e74a2e3b4c5d092fc0aae38bbc5455d4db48 (patch) | |
| tree | a655a05e449fb0f3218d87555b33c7fb57968fdc | |
| parent | 258114d48b4853cbaa79b53776d5279f727451ab (diff) | |
| download | puppet-18e8e74a2e3b4c5d092fc0aae38bbc5455d4db48.tar.gz puppet-18e8e74a2e3b4c5d092fc0aae38bbc5455d4db48.tar.xz puppet-18e8e74a2e3b4c5d092fc0aae38bbc5455d4db48.zip | |
Committing most of the scheduling stuff. There is still a bit of work to do in terms of how puppetd interacts with scheduling, but the bulk of the work is done.
git-svn-id: https://reductivelabs.com/svn/puppet/trunk@847 980ebf18-57e1-0310-9a29-db15c13687c0
| -rwxr-xr-x | bin/puppetd | 18 | ||||
| -rw-r--r-- | lib/puppet.rb | 5 | ||||
| -rw-r--r-- | lib/puppet/client.rb | 9 | ||||
| -rw-r--r-- | lib/puppet/parameter.rb | 113 | ||||
| -rw-r--r-- | lib/puppet/storage.rb | 64 | ||||
| -rw-r--r-- | lib/puppet/transaction.rb | 18 | ||||
| -rw-r--r-- | lib/puppet/type.rb | 149 | ||||
| -rwxr-xr-x | lib/puppet/type/parsedtype.rb | 2 | ||||
| -rwxr-xr-x | lib/puppet/type/pfile/checksum.rb | 87 | ||||
| -rwxr-xr-x | lib/puppet/type/schedule.rb | 341 | ||||
| -rw-r--r-- | lib/puppet/type/state.rb | 123 | ||||
| -rw-r--r-- | test/client/client.rb | 4 | ||||
| -rw-r--r-- | test/other/state.rb | 85 | ||||
| -rwxr-xr-x | test/other/storage.rb | 31 | ||||
| -rwxr-xr-x | test/puppet/defaults.rb | 2 | ||||
| -rw-r--r-- | test/puppettest.rb | 6 | ||||
| -rw-r--r-- | test/types/file.rb | 39 | ||||
| -rwxr-xr-x | test/types/filebucket.rb | 2 | ||||
| -rw-r--r-- | test/types/fileignoresource.rb | 2 | ||||
| -rwxr-xr-x | test/types/filesources.rb | 2 | ||||
| -rwxr-xr-x | test/types/schedule.rb | 336 |
21 files changed, 1157 insertions, 281 deletions
diff --git a/bin/puppetd b/bin/puppetd index 9ba6f89f1..6d2b804b0 100755 --- a/bin/puppetd +++ b/bin/puppetd @@ -11,7 +11,7 @@ # puppetd [-h|--help] [-V|--version] [-d|--debug] [-v|--verbose] # [--ssldir <cert directory>] [-l|--logdest <syslog|<file>|console>] # [--fqdn <host name>] [-p|--port <port>] [-o|--onetime] -# [-s|--server <server>] +# [-s|--server <server>] [-i|--ignoreschedules] # [-w|--waitforcert <seconds>] [-c|--confdir <configuration directory>] # [--vardir <var directory>] [--centrallogging] # @@ -58,7 +58,15 @@ # The port to which to connect on the remote server. Currently defaults to 8139. # # onetime:: -# Run the configuration once, rather than as a long-running daemon. +# Run the configuration once, rather than as a long-running daemon. This is +# useful for interactively running puppetd. +# +# schedule:: +# What schedule Puppet itself should run on. This dictates how often the +# entire configuration is retrieved and run. The default is named 'puppet', +# and runs every half hour or so. The schedules themselves are defined in the +# configuration, which means that on startup puppetd will always retrieve +# the configuration and then check to see if it's scheduled to run. # # server:: # The remote server from whom to receive the local configuration. Currently @@ -117,6 +125,7 @@ result = GetoptLong.new( [ "--noop", "-n", GetoptLong::NO_ARGUMENT ], [ "--onetime", "-o", GetoptLong::NO_ARGUMENT ], [ "--port", "-p", GetoptLong::REQUIRED_ARGUMENT ], + [ "--schedule", "-S", GetoptLong::REQUIRED_ARGUMENT ], [ "--server", "-s", GetoptLong::REQUIRED_ARGUMENT ], [ "--ssldir", GetoptLong::REQUIRED_ARGUMENT ], [ "--verbose", "-v", GetoptLong::NO_ARGUMENT ], @@ -134,6 +143,7 @@ waitforcert = false onetime = false centrallogs = false + begin result.each { |opt,arg| case opt @@ -159,6 +169,10 @@ begin Puppet[:logdest] = :console when "--noop" Puppet[:noop] = true + when "--schedule" + # This is late-binding -- it'll only look up the schedule name + # when it needs to run + Puppet[:schedule] = arg when "--ssldir" Puppet[:ssldir] = arg when "--fqdn" diff --git a/lib/puppet.rb b/lib/puppet.rb index 5dacb15bf..b092dd7e2 100644 --- a/lib/puppet.rb +++ b/lib/puppet.rb @@ -94,13 +94,16 @@ PUPPETVERSION = '0.11.2' :httplogfile => [:logdir, "http.log"], :masterlog => [:logdir, "puppetmaster.log"], :masterhttplog => [:logdir, "masterhttp.log"], - :checksumfile => [:statedir, "checksums"], + :statefile => [:statedir, "state.yaml"], + :checksumfile => [:statedir, "state.yaml"], :ssldir => [:puppetconf, "ssl"], # and finally the simple answers, :server => "puppet", :user => "puppet", :group => "puppet", + :schedule => "puppet", + :ignoreschedules => false, :rrdgraph => false, :noop => false, :parseonly => false, diff --git a/lib/puppet/client.rb b/lib/puppet/client.rb index 9023a2529..27c7cef72 100644 --- a/lib/puppet/client.rb +++ b/lib/puppet/client.rb @@ -264,6 +264,11 @@ module Puppet detail rescue => detail Puppet.err "Found a bug: %s" % detail + if Puppet[:debug] + puts detail.backtrace + end + ensure + Puppet::Storage.store end Puppet::Metric.gather Puppet::Metric.tally @@ -271,7 +276,6 @@ module Puppet Metric.store Metric.graph end - Puppet::Storage.store return transaction end @@ -416,6 +420,9 @@ module Puppet end @objects = nil + # First create the default scheduling objects + Puppet.type(:schedule).mkdefaultschedules + # Now convert the objects to real Puppet objects @objects = objects.to_type diff --git a/lib/puppet/parameter.rb b/lib/puppet/parameter.rb index a4fe00b7b..dfb259909 100644 --- a/lib/puppet/parameter.rb +++ b/lib/puppet/parameter.rb @@ -4,7 +4,7 @@ module Puppet attr_reader :validater, :munger, :name, :default attr_accessor :ismetaparameter, :element - # Define the default value for a given state or parameter. This + # Define the default value for a given parameter or parameter. This # means that 'nil' is an invalid default value. This defines # the 'default' instance method. def defaultto(value = nil, &block) @@ -29,7 +29,7 @@ module Puppet # This is how we munge the value. Basically, this is our # opportunity to convert the value from one form into another. def munge(&block) - # I need to wrap the unsafe version in begin/rescue statements, + # I need to wrap the unsafe version in begin/rescue parameterments, # but if I directly call the block then it gets bound to the # class's context, not the instance's, thus the two methods, # instead of just one. @@ -107,6 +107,41 @@ module Puppet end end end + + # Define a new value for our parameter. + def newvalues(*names) + @parametervalues ||= [] + + names.each { |name| + if @parametervalues.include?(name) + Puppet.warning "%s already has a value for %s" % [name, name] + end + @parametervalues << name + } + end + + def aliasvalue(name, other) + @parametervalues ||= [] + unless @parametervalues.include?(other) + raise Puppet::DevError, "Cannot alias nonexistent value %s" % other + end + + @aliasvalues ||= {} + @aliasvalues[name] = other + end + + def alias(name) + @aliasvalues[name] + end + + # Return the list of valid values. + def values + @parametervalues ||= [] + @aliasvalues ||= {} + + #[@aliasvalues.keys, @parametervalues.keys].flatten + @parametervalues.dup + end end # Just a simple method to proxy instance methods to class methods @@ -177,6 +212,78 @@ module Puppet raise error end + # Log a message using the parent's log level. + def log(msg) + unless @parent[:loglevel] + p @parent + self.devfail "Parent %s has no loglevel" % + @parent.name + end + Puppet::Log.create( + :level => @parent[:loglevel], + :message => msg, + :source => self + ) + end + + # each parameter class must define the name() method, and parameter instances + # do not change that name + # this implicitly means that a given object can only have one parameter + # instance of a given parameter class + def name + return self.class.name + end + + # for testing whether we should actually do anything + def noop + unless defined? @noop + @noop = false + end + tmp = @noop || self.parent.noop || Puppet[:noop] || false + #debug "noop is %s" % tmp + return tmp + end + + # return the full path to us, for logging and rollback; not currently + # used + def path + return [@parent.path, self.name].join("/") + end + + # If the specified value is allowed, then munge appropriately. + munge do |value| + if self.class.values.empty? + # This parameter isn't using defined values to do its work. + return value + end + intern = value.to_s.intern + # If it's a valid value, always return it as a symbol. + if self.class.values.include?(intern) + retval = intern + elsif other = self.class.alias(intern) + self.info "returning alias %s for %s" % [other, intern] + retval = other + else + retval = value + end + retval + end + + # Verify that the passed value is valid. + validate do |value| + if self.class.values.empty? + # This parameter isn't using defined values to do its work. + return + end + unless value.is_a?(Symbol) + value = value.to_s.intern + end + unless self.class.values.include?(value) or self.class.alias(value) + self.fail "Invalid '%s' value '%s'. Valid values are '%s'" % + [self.class.name, value, self.class.values.join(", ")] + end + end + # This should only be called for parameters, but go ahead and make # it possible to call for states, too. def value @@ -195,7 +302,7 @@ module Puppet # late-binding (e.g., users might not exist when the value is assigned # but might when it is asked for). def value=(value) - # If we're a state, just hand the processing off to the should method. + # If we're a parameter, just hand the processing off to the should method. if self.is_a?(Puppet::State) return self.should = value end diff --git a/lib/puppet/storage.rb b/lib/puppet/storage.rb index b174e0183..a12a94178 100644 --- a/lib/puppet/storage.rb +++ b/lib/puppet/storage.rb @@ -9,6 +9,18 @@ module Puppet self.class.load end + # Return a hash that will be stored to disk. It's worth noting + # here that we use the object's full path, not just the name/type + # combination. At the least, this is useful for those non-isomorphic + # types like exec, but it also means that if an object changes locations + # in the configuration it will lose its cache. + def self.cache(object) + unless object.is_a? Puppet::Type + raise Puppet::DevFail, "Must pass a Type instance to Storage.cache" + end + return @@state[object.path] ||= {} + end + def self.clear @@state.clear Storage.init @@ -23,32 +35,32 @@ module Puppet self.init def self.load - if Puppet[:checksumfile].nil? + if Puppet[:statefile].nil? raise Puppet::DevError, "Somehow the statefile is nil" end - unless File.exists?(Puppet[:checksumfile]) - Puppet.info "Statefile %s does not exist" % Puppet[:checksumfile] + unless File.exists?(Puppet[:statefile]) + Puppet.info "Statefile %s does not exist" % Puppet[:statefile] unless defined? @@state and ! @@state.nil? self.init end return end - #Puppet.debug "Loading statefile %s" % Puppet[:checksumfile] - Puppet::Util.lock(Puppet[:checksumfile]) { |file| + #Puppet.debug "Loading statefile %s" % Puppet[:statefile] + Puppet::Util.lock(Puppet[:statefile]) { |file| #@@state = Marshal.load(file) begin @@state = YAML.load(file) rescue => detail Puppet.err "Checksumfile %s is corrupt; replacing" % - Puppet[:checksumfile] + Puppet[:statefile] begin - File.rename(Puppet[:checksumfile], - Puppet[:checksumfile] + ".bad") + File.rename(Puppet[:statefile], + Puppet[:statefile] + ".bad") rescue raise Puppet::Error, "Could not rename corrupt %s; remove manually" % - Puppet[:checksumfile] + Puppet[:statefile] end end } @@ -60,44 +72,26 @@ module Puppet @@state.inspect end - def self.state(myclass) - unless myclass.is_a? Class - myclass = myclass.class - end - - @@state[myclass.to_s] ||= {} - return @@state[myclass.to_s] - end - def self.store - unless FileTest.directory?(File.dirname(Puppet[:checksumfile])) + unless FileTest.directory?(File.dirname(Puppet[:statefile])) begin - Puppet.recmkdir(File.dirname(Puppet[:checksumfile])) + Puppet.recmkdir(File.dirname(Puppet[:statefile])) Puppet.info "Creating state directory %s" % - File.dirname(Puppet[:checksumfile]) + File.dirname(Puppet[:statefile]) rescue => detail Puppet.err "Could not create state file: %s" % detail return end end - unless FileTest.exist?(Puppet[:checksumfile]) - Puppet.info "Creating state file %s" % Puppet[:checksumfile] + unless FileTest.exist?(Puppet[:statefile]) + Puppet.info "Creating state file %s" % Puppet[:statefile] end - Puppet::Util.lock(Puppet[:checksumfile], File::CREAT|File::WRONLY, 0600) { |file| + Puppet::Util.lock( + Puppet[:statefile], File::CREAT|File::WRONLY, 0600 + ) { |file| file.print YAML.dump(@@state) - #file.puts(Marshal::dump(@@state)) - #File.open(Puppet[:checksumfile], File::CREAT|File::WRONLY, 0600) { |file| - # @@state.each { |klass, thash| - # thash.each { |key,value| - # Puppet.warning "Storing: %s %s %s" % - # [klass, key.inspect, value.inspect] - # mvalue = Marshal::dump(value) - # file.puts([klass,key,mvalue].join(@@splitchar)) - # } - # } - #} } end end diff --git a/lib/puppet/transaction.rb b/lib/puppet/transaction.rb index 612cee87e..39aa27858 100644 --- a/lib/puppet/transaction.rb +++ b/lib/puppet/transaction.rb @@ -44,6 +44,7 @@ class Transaction Puppet.debug "Beginning transaction %s with %s changes" % [self.object_id, @changes.length] + now = Time.now.to_i events = @changes.collect { |change| if change.is_a?(Puppet::StateChange) change.transaction = self @@ -66,13 +67,19 @@ class Transaction # should do so end + # This is kinda lame, because it can result in the same + # object being modified multiple times, but that's difficult + # to avoid as long as we're syncing each state individually. + change.state.parent.cache(:synced, now) + unless events.nil? or (events.is_a?(Array) and events.empty?) change.changed = true end events else + puts caller raise Puppet::DevError, - "Transactions cannot handle objects of type %s" % child.class + "Transactions cannot handle objects of type %s" % change.class end }.flatten.reject { |event| event.nil? @@ -107,11 +114,16 @@ class Transaction end # change collection is in-band, and message generation is out-of-band # of course, exception raising is also out-of-band - @changes = @objects.collect { |child| + now = Time.now.to_i + @changes = @objects.find_all { |child| + child.scheduled? + }.collect { |child| # these children are all Puppet::Type instances # not all of the children will return a change, and Containers # return transactions - child.evaluate + ary = child.evaluate + child.cache(:checked, now) + ary }.flatten.reject { |child| child.nil? # remove empties } diff --git a/lib/puppet/type.rb b/lib/puppet/type.rb index 6a7dcacc1..5c9bcf027 100644 --- a/lib/puppet/type.rb +++ b/lib/puppet/type.rb @@ -54,23 +54,6 @@ class Type < Puppet::Element public - # these objects are used for mapping type names (e.g., 'file') - # to actual object classes; because Type.inherited is - # called before the <subclass>.name method is defined, we need - # to store each class in an array, and then later actually iterate - # across that array and make a map - #@@typeary = [self] # so that the allowedmethods stuff works - #@@typehash = Hash.new { |hash,key| - # if key.is_a?(String) - # key = key.intern - # end - # if hash.include?(key) - # hash[key] - # else - # raise TypeError.new("Object type %s not found" % key) - # end - #} - # the Type class attribute accessors class << self attr_reader :name, :states @@ -463,6 +446,7 @@ class Type < Puppet::Element end param.ismetaparameter param.class_eval(&block) + const_set("MetaParam" + name.to_s.capitalize,param) @@metaparams ||= [] @@metaparams << param @@ -483,6 +467,7 @@ class Type < Puppet::Element end param.element = self param.class_eval(&block) + const_set("Parameter" + name.to_s.capitalize,param) @parameters ||= [] @parameters << param @@ -756,8 +741,12 @@ class Type < Puppet::Element else if @states.has_key?(attr) @states.delete(attr) + elsif @parameters.has_key?(attr) + @parameters.delete(attr) + elsif @metaparams.has_key?(attr) + @metaparams.delete(attr) else - raise Puppet::DevError.new("Undefined state '#{attr}' in #{self}") + raise Puppet::DevError.new("Undefined attribute '#{attr}' in #{self}") end end end @@ -809,11 +798,11 @@ class Type < Puppet::Element error = type.new(args.join(" ")) - if @line + if defined? @line and @line error.line = @line end - if @file + if defined? @file and @file error.file = @file end @@ -1142,8 +1131,8 @@ class Type < Puppet::Element @noop = false # keeping stats for the total number of changes, and how many were # completely sync'ed - # this isn't really sufficient either, because it adds lots of special cases - # such as failed changes + # this isn't really sufficient either, because it adds lots of special + # cases such as failed changes # it also doesn't distinguish between changes from the current transaction # vs. changes over the process lifetime @totalchanges = 0 @@ -1180,15 +1169,42 @@ class Type < Puppet::Element hash.delete(:parent) end + # Convert all args to symbols hash = self.argclean(hash) - self.class.allattrs.each { |name| + # Let's do the name first, because some things need to happen once + # we have the name but before anything else + + attrs = self.class.allattrs + namevar = self.class.namevar + + if hash.include?(namevar) + self[namevar] = hash[namevar] + hash.delete(namevar) + if attrs.include?(namevar) + attrs.delete(namevar) + else + self.devfail "My namevar isn't a valid attribute...?" + end + else + self.devfail "I was not passed a namevar" + end + + # The information to cache to disk. We have to do this after + # the name is set because it uses the name and/or path, but before + # everything else is set because the states need to be able to + # retrieve their stored info. + #@cache = Puppet::Storage.cache(self) + + # This is all of our attributes except the namevar. + attrs.each { |name| if hash.include?(name) begin self[name] = hash[name] rescue => detail self.devfail( - "Could not set %s on %s: %s" % [name, self.class.name, detail] + "Could not set %s on %s: %s" % + [name, self.class.name, detail] ) end hash.delete name @@ -1253,18 +1269,66 @@ class Type < Puppet::Element self.schedule end + # Return a cached value + def cached(name) + Puppet::Storage.cache(self)[name] + #@cache[name] ||= nil + end + + # Cache a value + def cache(name, value) + Puppet::Storage.cache(self)[name] = value + #@cache[name] = value + end + + # Look up the schedule and set it appropriately. This is done after + # the instantiation phase, so that the schedule can be anywhere in the + # file. def schedule - unless self[:schedule] - return - end - if sched = Puppet.type(:schedule)[self[:schedule]] - self[:schedule] = sched + # If we've already set the schedule, then just move on + return if self[:schedule].is_a?(Puppet.type(:schedule)) + + # Schedules don't need to be scheduled + return if self.is_a?(Puppet.type(:schedule)) + + # Nor do components + return if self.is_a?(Puppet.type(:component)) + + if self[:schedule] + if sched = Puppet.type(:schedule)[self[:schedule]] + self[:schedule] = sched + else + self.fail "Could not find schedule %s" % self[:schedule] + end + elsif Puppet[:schedule] and ! Puppet[:ignoreschedules] + # We handle schedule defaults here mostly because otherwise things + # will behave very very erratically during testing. + if sched = Puppet.type(:schedule)[Puppet[:schedule]] + self[:schedule] = sched + else + self.fail "Could not find default schedule %s" % Puppet[:schedule] + end else - self.fail "Could not find schedule %s" % self[:schedule] + # While it's unlikely we won't have any schedule (since there's a + # default), it's at least possible during testing + return true end end + # Check whether we are scheduled to run right now or not. + def scheduled? + return true if Puppet[:ignoreschedules] + return true unless schedule = self[:schedule] + + # We use 'checked' here instead of 'synced' because otherwise we'll + # end up checking most elements most times, because they will generally + # have been synced a long time ago (e.g., a file only gets updated + # once a month on the server and its schedule is daily; the last sync time + # will have been a month ago, so we'd end up checking every run). + return schedule.match?(self.cached(:checked)) + end + # Is the specified parameter set? def attrset?(type, attr) case type @@ -1776,8 +1840,29 @@ class Type < Puppet::Element end newmetaparam(:schedule) do - desc "On what schedule the object should be managed. - Currently non-functional." + desc "On what schedule the object should be managed. You must create a + schedule_ object, and then reference the name of that object to use + that for your schedule: + + schedule { daily: + period => daily, + range => \"2-4\" + } + + exec { \"/usr/bin/apt-get update\": + schedule => daily + } + + The creation of the schedule object does not need to appear in the + configuration before objects that use it." + + munge do |name| + if schedule = Puppet.type(:schedule)[name] + return schedule + else + return name + end + end end newmetaparam(:check) do diff --git a/lib/puppet/type/parsedtype.rb b/lib/puppet/type/parsedtype.rb index 21d0299f1..268b7ad56 100755 --- a/lib/puppet/type/parsedtype.rb +++ b/lib/puppet/type/parsedtype.rb @@ -238,7 +238,7 @@ module Puppet return str else - Puppet.notice "No host instances for %s" % user + Puppet.notice "No host instances" return "" end end diff --git a/lib/puppet/type/pfile/checksum.rb b/lib/puppet/type/pfile/checksum.rb index 61d8fea80..5d936f91e 100755 --- a/lib/puppet/type/pfile/checksum.rb +++ b/lib/puppet/type/pfile/checksum.rb @@ -20,6 +20,27 @@ module Puppet @checktypes[0] end + # Checksums need to invert how changes are printed. + def change_to_s + begin + if @is == :absent + return "defined '%s' as '%s'" % + [self.name, self.should_to_s] + elsif self.should == :absent + return "undefined %s from '%s'" % + [self.name, self.is_to_s] + else + return "%s changed '%s' to '%s'" % + [self.name, self.should_to_s, self.is_to_s] + end + rescue Puppet::Error, Puppet::DevError + raise + rescue => detail + raise Puppet::DevError, "Could not convert change %s to string: %s" % + [self.name, detail] + end + end + def getsum(checktype) sum = "" case checktype @@ -85,27 +106,45 @@ module Puppet end @checktypes << value - state = Puppet::Storage.state(self) - unless state - self.devfail "Did not get state back from Storage" + hash = nil + unless hash = @parent.cached(:checksums) + hash = {} + @parent.cache(:checksums, hash) end - if hash = state[@parent[:path]] - if hash.include?(value) - #self.notice "Found checksum %s for %s" % - # [hash[value] ,@parent[:path]] - return hash[value] - else - #self.notice "Found checksum for %s but not of type %s" % - # [@parent[:path],@checktypes[0]] - return :nosum - end - else - # We can't use :absent here, because then it'll match on - # non-existent files + + #unless state + # self.devfail "Did not get state back from Storage" + #end + + if hash.include?(value) + #self.notice "Found checksum %s for %s" % + # [hash[value] ,@parent[:path]] + return hash[value] + elsif hash.empty? #self.notice "Could not find sum of type %s" % @checktypes[0] return :nosum + else + #self.notice "Found checksum for %s but not of type %s" % + # [@parent[:path],@checktypes[0]] + return :nosum end +# if hash = state[@parent[:path]] +# if hash.include?(value) +# #self.notice "Found checksum %s for %s" % +# # [hash[value] ,@parent[:path]] +# return hash[value] +# else +# #self.notice "Found checksum for %s but not of type %s" % +# # [@parent[:path],@checktypes[0]] +# return :nosum +# end +# else +# # We can't use :absent here, because then it'll match on +# # non-existent files +# #self.notice "Could not find sum of type %s" % @checktypes[0] +# return :nosum +# end end # Even though they can specify multiple checksums, the insync? @@ -188,11 +227,11 @@ module Puppet # Store the new sum to the state db. def updatesum result = false - state = Puppet::Storage.state(self) - unless state.include?(@parent.name) - self.debug "Initializing state hash for %s" % @parent.name - - state[@parent.name] = Hash.new + state = nil + unless state = @parent.cached(:checksums) + self.debug "Initializing checksum hash for %s" % @parent.name + state = {} + @parent.cache(:checksums, state) end if @is.is_a?(Symbol) @@ -206,7 +245,7 @@ module Puppet end # if we're replacing, vs. updating - if state[@parent.name].include?(@checktypes[0]) + if state.include?(@checktypes[0]) unless defined? @should raise Puppet::Error.new( ("@should is not initialized for %s, even though we " + @@ -214,7 +253,7 @@ module Puppet ) end self.debug "Replacing %s checksum %s with %s" % - [@parent.name, state[@parent.name][@checktypes[0]],@is] + [@parent.name, state[@checktypes[0]],@is] #@parent.debug "@is: %s; @should: %s" % [@is,@should] result = true else @@ -222,7 +261,7 @@ module Puppet [@is,@checktypes[0]] result = false end - state[@parent.name][@checktypes[0]] = @is + state[@checktypes[0]] = @is return result end end diff --git a/lib/puppet/type/schedule.rb b/lib/puppet/type/schedule.rb new file mode 100755 index 000000000..515ae0194 --- /dev/null +++ b/lib/puppet/type/schedule.rb @@ -0,0 +1,341 @@ +module Puppet + newtype(:schedule) do + @doc = "Defined schedules for Puppet. The important thing to understand + about how schedules are currently implemented in Puppet is that they + can only be used to stop an element from being applied, they never + guarantee that it is applied. + + Every time Puppet applies its configuration, it will collect the + list of elements whose schedule does not eliminate them from + running right then, but there is currently no system in place to + guarantee that a given element runs at a given time. If you + specify a very restrictive schedule and Puppet happens to run at a + time within that schedule, then the elements will get applied; + otherwise, that work may never get done. + + Thus, it behooves you to use wider scheduling (e.g., over a couple of + hours) combined with periods and repetitions. For instance, if you + wanted to restrict certain elements to only running once, between + the hours of two and 4 AM, then you would use this schedule:: + + schedule { maint: + range => \"2 - 4\", + period => daily, + repeat => 1 + } + + With this schedule, the first time that Puppet runs between 2 and 4 AM, + all elements with this schedule will get applied, but they won't + get applied again between 2 and 4 because they will have already + run once that day, and they won't get applied outside that schedule + because they will be outside the scheduled range. + + Puppet automatically creates a schedule for each valid period with the + same name as that period (e.g., hourly and daily). Additionally, + a schedule named *puppet* is created and used as the default, + with the following attributes: + + schedule { puppet: + period => hourly, + repeat => 2 + } + + This will cause elements to be applied every 30 minutes by default. + " + + @states = [] + + newparam(:name) do + desc "The name of the schedule. This name is used to retrieve the + schedule when assigning it to an object:: + + schedule { daily: + period => daily, + range => [2, 4] + } + + exec { \"/usr/bin/apt-get update\": + schedule => daily + } + + " + isnamevar + end + + newparam(:range) do + desc "The earliest and latest that an element can be applied. This + is always a range within a 24 hour period, and hours must be + specified in numbers between 0 and 23, inclusive. Minutes and + seconds can be provided, using the normal colon as a separator. + For instance:: + + schedule { maintenance: + range => \"1:30 - 4:30\" + } + + This is mostly useful for restricting certain elements to being + applied in maintenance windows or during off-peak hours." + + # This is lame; states all use arrays as values, but parameters don't. + # That's going to hurt eventually. + validate do |values| + values = [values] unless values.is_a?(Array) + values.each { |value| + unless value.is_a?(String) and + value =~ /\d+(:\d+){0,2}\s*-\s*\d+(:\d+){0,2}/ + self.fail "Invalid range value '%s'" % value + end + } + end + + munge do |values| + values = [values] unless values.is_a?(Array) + ret = [] + + values.each { |value| + range = [] + # Split each range value into a hour, minute, second triad + value.split(/\s*-\s*/).each { |val| + # Add the values as an array. + range << val.split(":").collect { |n| n.to_i } + } + + if range.length != 2 + self.fail "Invalid range %s" % value + end + + if range[0][0] > range[1][0] + self.fail(("Invalid range %s; " % value) + + "ranges cannot span days." + ) + end + ret << range + } + + # Now our array of arrays + ret + end + + def match?(previous, now) + # The lowest-level array is of the hour, minute, second triad + # then it's an array of two of those, to present the limits + # then it's array of those ranges + unless @value[0][0].is_a?(Array) + @value = [@value] + end + + @value.each do |value| + limits = value.collect do |range| + ary = [now.year, now.month, now.day, range[0]] + if range[1] + ary << range[1] + else + ary << now.min + end + + if range[2] + ary << range[2] + else + ary << now.sec + end + + time = Time.local(*ary) + + unless time.hour == range[0] + self.devfail( + "Incorrectly converted time: %s: %s vs %s" % + [time, time.hour, range[0]] + ) + end + + time + end + + unless limits[0] < limits[1] + self.info( + "Assuming upper limit should be that time the next day" + ) + + ary = limits[1].to_a + ary[3] += 1 + limits[1] = Time.local(*ary) + + #self.devfail("Lower limit is above higher limit: %s" % + # limits.inspect + #) + end + + #self.info limits.inspect + #self.notice now + return now.between?(*limits) + end + + # Else, return false, since our current time isn't between + # any valid times + return false + end + end + + newparam(:periodmatch) do + desc "Whether periods should be matched by number (e.g., the two times + are in the same hour) or by distance (e.g., the two times are + 60 minutes apart). *number*/**distance**" + + newvalues(:number, :distance) + + defaultto :distance + end + + newparam(:period) do + desc "The period of repetition for an element. Choose from among + a fixed list of *hourly*, *daily*, *weekly*, and *monthly*. + The default is for an element to get applied every time that + Puppet runs, whatever that period is. + + Note that the period defines how often a given element will get + applied but not when; if you would like to restrict the hours + that a given element can be applied (e.g., only at night during + a maintenance window) then use the ``range`` attribute. + + If the provided periods are not sufficient, you can provide a + value to the *repeat* attribute, which will cause Puppet to + schedule the affected elements evenly in the period the + specified number of times. Take this schedule:: + + schedule { veryoften: + period => hourly, + repeat => 6 + } + + This can cause Puppet to apply that element up to every 10 minutes. + + At the moment, Puppet cannot guarantee that level of + repetition; that is, it can run up to every 10 minutes, but + internal factors might prevent it from actually running that + often (e.g., long-running Puppet runs will squash conflictingly + scheduled runs). + + See the ``periodmatch`` attribute for tuning whether to match + times by their distance apart or by their specific value." + + newvalues(:hourly, :daily, :weekly, :monthly) + + @@scale = { + :hourly => 3600, + :daily => 86400, + :weekly => 604800, + :monthly => 2592000 + } + @@methods = { + :hourly => :hour, + :daily => :day, + :monthly => :month, + :weekly => proc do |prev, now| + prev.strftime("%U") == now.strftime("%U") + end + } + + def match?(previous, now) + value = self.value + case @parent[:periodmatch] + when :number + method = @@methods[value] + if method.is_a?(Proc) + return method.call(previous, now) + else + # We negate it, because if they're equal we don't run + val = now.send(method) != previous.send(method) + return val + end + when :distance + scale = @@scale[value] + + # If the number of seconds between the two times is greater + # than the unit of time, we match. We divide the scale + # by the repeat, so that we'll repeat that often within + # the scale. + return (now.to_i - previous.to_i) >= (scale / @parent[:repeat]) + end + end + end + + newparam(:repeat) do + desc "How often the application gets repeated in a given period. + Defaults to 1." + + defaultto 1 + + validate do |value| + unless value.is_a?(Integer) or value =~ /^\d+$/ + raise Puppet::Error, + "Repeat must be a number" + end + + # This implicitly assumes that 'periodmatch' is distance -- that + # is, if there's no value, we assume it's a valid value. + return unless @parent[:periodmatch] + + if value != 1 and @parent[:periodmatch] != :distance + raise Puppet::Error, + "Repeat must be 1 unless periodmatch is 'distance', not '%s'" % + @parent[:periodmatch] + end + end + + munge do |value| + unless value.is_a?(Integer) + value = Integer(value) + end + + value + end + + def match?(previous, now) + true + end + end + + def self.mkdefaultschedules + Puppet.info "Creating default schedules" + # Create our default schedule + self.create( + :name => "puppet", + :period => :hourly, + :repeat => "2" + ) + + # And then one for every period + @parameters.find { |p| p.name == :period }.values.each { |value| + self.create( + :name => value.to_s, + :period => value + ) + } + end + + def match?(previous = nil, now = nil) + + # If we've got a value, then convert it to a Time instance + if previous + previous = Time.at(previous) + end + + now ||= Time.now + + # Pull them in order + self.class.allattrs.each { |param| + if @parameters.include?(param) and + @parameters[param].respond_to?(:match?) + #self.notice "Trying to match %s" % param + return false unless @parameters[param].match?(previous, now) + end + } + + # If we haven't returned false, then return true; in other words, + # any provided schedules need to all match + return true + end + end +end + +# $Id$ diff --git a/lib/puppet/type/state.rb b/lib/puppet/type/state.rb index 6e1600083..2a5f18a89 100644 --- a/lib/puppet/type/state.rb +++ b/lib/puppet/type/state.rb @@ -28,32 +28,32 @@ class State < Puppet::Parameter end end - # Define a new value for our state. + # Parameters just use 'newvalues', since there's no work associated with them. def self.newvalue(name, &block) - @statevalues ||= {} + @parametervalues ||= {} - if @statevalues.include?(name) - Puppet.warning "%s already has a value for %s" % [self.name, name] + if @parametervalues.include?(name) + Puppet.warning "%s already has a value for %s" % [name, name] end - @statevalues[name] = block + @parametervalues[name] = block define_method("set_" + name.to_s, &block) end - - def self.aliasvalue(name, other) - @statevalues ||= {} - unless @statevalues.include?(other) - raise Puppet::DevError, "Cannot alias nonexistent value %s" % other - end - - @aliasvalues ||= {} - @aliasvalues[name] = other - end - - def self.alias(name) - @aliasvalues[name] - end - +# +# def self.aliasvalue(name, other) +# @statevalues ||= {} +# unless @statevalues.include?(other) +# raise Puppet::DevError, "Cannot alias nonexistent value %s" % other +# end +# +# @aliasvalues ||= {} +# @aliasvalues[name] = other +# end +# +# def self.alias(name) +# @aliasvalues[name] +# end +# def self.defaultvalues newvalue(:present) do @parent.create @@ -66,15 +66,15 @@ class State < Puppet::Parameter # This doc will probably get overridden @doc ||= "The basic state that the object should be in." end - - # Return the list of valid values. - def self.values - @statevalues ||= {} - @aliasvalues ||= {} - - #[@aliasvalues.keys, @statevalues.keys].flatten - @statevalues.keys - end +# +# # Return the list of valid values. +# def self.values +# @statevalues ||= {} +# @aliasvalues ||= {} +# +# #[@aliasvalues.keys, @statevalues.keys].flatten +# @statevalues.keys +# end # Call the method associated with a given value. def set @@ -278,38 +278,38 @@ class State < Puppet::Parameter self.set end - munge do |value| - if self.class.values.empty? - # This state isn't using defined values to do its work. - return value - end - intern = value.to_s.intern - # If it's a valid value, always return it as a symbol. - if self.class.values.include?(intern) - retval = intern - elsif other = self.class.alias(intern) - self.info "returning alias %s for %s" % [other, intern] - retval = other - else - retval = value - end - retval - end - - # Verify that the passed value is valid. - validate do |value| - if self.class.values.empty? - # This state isn't using defined values to do its work. - return - end - unless value.is_a?(Symbol) - value = value.to_s.intern - end - unless self.class.values.include?(value) or self.class.alias(value) - self.fail "Invalid '%s' value '%s'. Valid values are '%s'" % - [self.class.name, value, self.class.values.join(", ")] - end - end +# munge do |value| +# if self.class.values.empty? +# # This state isn't using defined values to do its work. +# return value +# end +# intern = value.to_s.intern +# # If it's a valid value, always return it as a symbol. +# if self.class.values.include?(intern) +# retval = intern +# elsif other = self.class.alias(intern) +# self.info "returning alias %s for %s" % [other, intern] +# retval = other +# else +# retval = value +# end +# retval +# end +# +# # Verify that the passed value is valid. +# validate do |value| +# if self.class.values.empty? +# # This state isn't using defined values to do its work. +# return +# end +# unless value.is_a?(Symbol) +# value = value.to_s.intern +# end +# unless self.class.values.include?(value) or self.class.alias(value) +# self.fail "Invalid '%s' value '%s'. Valid values are '%s'" % +# [self.class.name, value, self.class.values.join(", ")] +# end +# end # How should a state change be printed as a string? def change_to_s @@ -378,7 +378,6 @@ class State < Puppet::Parameter end def retrieve - self.warning "retrieving" if @parent.exists? @is = :present else diff --git a/test/client/client.rb b/test/client/client.rb index 608fa1dc6..3a462c097 100644 --- a/test/client/client.rb +++ b/test/client/client.rb @@ -114,11 +114,11 @@ class TestClient < Test::Unit::TestCase # now verify that our client cannot do non-cert operations # because its certs are signed by a different CA - assert_raise(Puppet::NetworkClientError, + assert_raise(Puppet::Error, "Client was allowed to call getconfig with no certs") { nonemaster.getconfig } - assert_raise(Puppet::NetworkClientError, + assert_raise(Puppet::Error, "Client was allowed to call getconfig with untrusted certs") { certmaster.getconfig } diff --git a/test/other/state.rb b/test/other/state.rb deleted file mode 100644 index 15512f6fe..000000000 --- a/test/other/state.rb +++ /dev/null @@ -1,85 +0,0 @@ -if __FILE__ == $0 - $:.unshift '..' - $:.unshift '../../lib' - $puppetbase = "../../../../language/trunk" -end - -require 'puppet' -require 'puppettest' -require 'puppet/storage' -require 'test/unit' - -# $Id$ - -class StorageTestingClass -end - -class TestStorage < Test::Unit::TestCase - include TestPuppet - def test_simple - state = nil - assert_nothing_raised { - Puppet::Storage.load - } - assert_nothing_raised { - state = Puppet::Storage.state(Puppet::Type) - } - assert(state) - state["/etc/passwd"] = ["md5","9ebebe0c02445c40b9dc6871b64ee416"] - assert_nothing_raised { - Puppet::Storage.store - } - - # clear the memory, so we're sure we're hitting the state file - assert_nothing_raised { - Puppet::Storage.clear - Puppet::Storage.init - } - assert_nothing_raised { - Puppet::Storage.load - } - assert_equal( - ["md5","9ebebe0c02445c40b9dc6871b64ee416"], - Puppet::Storage.state(Puppet::Type)["/etc/passwd"] - ) - end - - def test_instance - file = nil - state = nil - assert_nothing_raised { - file = Puppet.type(:file).create( - :path => "/etc/passwd" - ) - } - assert_nothing_raised { - Puppet::Storage.load - } - assert_nothing_raised { - state = Puppet::Storage.state(file) - } - assert(state) - end - - def test_update - state = Puppet::Storage.state(StorageTestingClass) - state["testing"] = "yayness" - Puppet::Storage.store - assert(FileTest.exists?(Puppet[:checksumfile])) - end - - def test_hashstorage - state = Puppet::Storage.state(StorageTestingClass) - hash = { - :yay => "boo", - :rah => "foo" - } - state["testing"] = hash - Puppet::Storage.store - Puppet::Storage.clear - Puppet::Storage.init - Puppet::Storage.load - state = Puppet::Storage.state(StorageTestingClass) - assert_equal(hash, state["testing"]) - end -end diff --git a/test/other/storage.rb b/test/other/storage.rb index 64f08cdc7..fc9b52cd6 100755 --- a/test/other/storage.rb +++ b/test/other/storage.rb @@ -11,12 +11,28 @@ require 'test/unit' class TestParsedFile < Test::Unit::TestCase include TestPuppet + def mkfile + path = tempfile() + File.open(path, "w") { |f| f.puts :yayness } + + f = Puppet.type(:file).create( + :name => path, + :check => %w{checksum type} + ) + + return f + end + def test_storeandretrieve + path = tempfile() + + f = mkfile() + hash = {:a => :b, :c => :d} state = nil assert_nothing_raised { - state = Puppet::Storage.state(hash) + state = Puppet::Storage.cache(f) } assert(!state.include?("name")) @@ -34,8 +50,11 @@ class TestParsedFile < Test::Unit::TestCase assert_nothing_raised { Puppet::Storage.load } + + # Reset it + state = nil assert_nothing_raised { - state = Puppet::Storage.state(hash) + state = Puppet::Storage.cache(f) } assert_equal(state["name"], hash) @@ -45,6 +64,8 @@ class TestParsedFile < Test::Unit::TestCase # are reading or writing the file at once # so we need to test that def test_multiwrite + f = mkfile() + value = {:a => :b} threads = [] 9.times { |a| @@ -52,7 +73,7 @@ class TestParsedFile < Test::Unit::TestCase 9.times { |b| assert_nothing_raised { Puppet::Storage.load - state = Puppet::Storage.state(value) + state = Puppet::Storage.cache(f) value.each { |k,v| state[k] = v } state[:e] = rand(100) Puppet::Storage.store @@ -68,7 +89,9 @@ class TestParsedFile < Test::Unit::TestCase Puppet::Storage.store Puppet::Storage.clear Puppet::Storage.load - state = Puppet::Storage.state('newstate') + + f = mkfile() + state = Puppet::Storage.cache(f) assert_same Hash, state.class assert_equal 0, state.size end diff --git a/test/puppet/defaults.rb b/test/puppet/defaults.rb index 9c16578e7..2fbd4fe46 100755 --- a/test/puppet/defaults.rb +++ b/test/puppet/defaults.rb @@ -13,7 +13,7 @@ require 'test/unit' class TestPuppetDefaults < Test::Unit::TestCase include TestPuppet @@dirs = %w{rrddir puppetconf puppetvar logdir statedir} - @@files = %w{logfile checksumfile manifest masterlog} + @@files = %w{logfile statefile manifest masterlog} @@normals = %w{puppetport masterport server} @@booleans = %w{rrdgraph noop} diff --git a/test/puppettest.rb b/test/puppettest.rb index 34d48ff52..ec7b1b104 100644 --- a/test/puppettest.rb +++ b/test/puppettest.rb @@ -53,6 +53,8 @@ module TestPuppet Puppet[:logdest] = "/dev/null" Puppet[:httplog] = "/dev/null" end + + Puppet[:ignoreschedules] = true end @@ -209,10 +211,12 @@ module TestPuppet trans = comp.evaluate } + events = nil assert_nothing_raised("Failed to evaluate transaction") { - trans.evaluate + events = trans.evaluate.collect { |e| e.event } } Puppet.type(:component).delete(comp) + events end def run_events(type, trans, events, msg) diff --git a/test/types/file.rb b/test/types/file.rb index b9e85839e..a54b78e12 100644 --- a/test/types/file.rb +++ b/test/types/file.rb @@ -36,14 +36,14 @@ class TestFile < Test::Unit::TestCase begin initstorage rescue - system("rm -rf %s" % Puppet[:checksumfile]) + system("rm -rf %s" % Puppet[:statefile]) end end def teardown clearstorage Puppet::Storage.clear - system("rm -rf %s" % Puppet[:checksumfile]) + system("rm -rf %s" % Puppet[:statefile]) super end @@ -306,9 +306,8 @@ class TestFile < Test::Unit::TestCase ) comp.push file trans = nil - assert_nothing_raised() { - trans = comp.evaluate - } + + file.retrieve if file.name !~ /nonexists/ sum = file.state(:checksum) @@ -316,14 +315,16 @@ class TestFile < Test::Unit::TestCase assert(sum.insync?) end - assert_nothing_raised() { - events = trans.evaluate.collect { |e| e.event } - } - # we don't want to kick off an event the first time we - # come across a file - assert( - ! events.include?(:file_changed) - ) + events = assert_apply(comp) + + assert(! events.include?(:file_changed), + "File incorrectly changed") + assert_events([], comp) + + # We have to sleep because the time resolution of the time-based + # mechanisms is greater than one second + sleep 1 + assert_nothing_raised() { File.open(path,File::CREAT|File::TRUNC|File::WRONLY) { |of| of.puts "some more text, yo" @@ -332,10 +333,6 @@ class TestFile < Test::Unit::TestCase Puppet.type(:file).clear Puppet.type(:component).clear - # We have to sleep because the time resolution of the time-based - # mechanisms is greater than one second - sleep 1.1 - # now recreate the file assert_nothing_raised() { file = Puppet.type(:file).create( @@ -351,11 +348,11 @@ class TestFile < Test::Unit::TestCase # If the file was missing, it should not generate an event # when it gets created. - if path =~ /nonexists/e - assert_events([], comp) - else + #if path =~ /nonexists/ + # assert_events([], comp) + #else assert_events([:file_changed], comp) - end + #end assert_nothing_raised() { File.unlink(path) File.open(path,File::CREAT|File::TRUNC|File::WRONLY) { |of| diff --git a/test/types/filebucket.rb b/test/types/filebucket.rb index 5220eea98..a788bd929 100755 --- a/test/types/filebucket.rb +++ b/test/types/filebucket.rb @@ -52,7 +52,7 @@ class TestFileBucket < Test::Unit::TestCase begin initstorage rescue - system("rm -rf %s" % Puppet[:checksumfile]) + system("rm -rf %s" % Puppet[:statefile]) end end diff --git a/test/types/fileignoresource.rb b/test/types/fileignoresource.rb index ce8352384..4189c50e3 100644 --- a/test/types/fileignoresource.rb +++ b/test/types/fileignoresource.rb @@ -20,7 +20,7 @@ class TestFileIgnoreSources < Test::Unit::TestCase begin initstorage rescue - system("rm -rf %s" % Puppet[:checksumfile]) + system("rm -rf %s" % Puppet[:statefile]) end end diff --git a/test/types/filesources.rb b/test/types/filesources.rb index ba401b446..e08fa268c 100755 --- a/test/types/filesources.rb +++ b/test/types/filesources.rb @@ -18,7 +18,7 @@ class TestFileSources < Test::Unit::TestCase begin initstorage rescue - system("rm -rf %s" % Puppet[:checksumfile]) + system("rm -rf %s" % Puppet[:statefile]) end if defined? @port @port += 1 diff --git a/test/types/schedule.rb b/test/types/schedule.rb new file mode 100755 index 000000000..04519b3ca --- /dev/null +++ b/test/types/schedule.rb @@ -0,0 +1,336 @@ +if __FILE__ == $0 + $:.unshift '..' + $:.unshift '../../lib' + $puppetbase = "../.." +end + +require 'puppet' +require 'test/unit' +require 'puppet/type/schedule' +require 'puppettest' + +class TestSchedule < Test::Unit::TestCase + include TestPuppet + + def setup + super + @stype = Puppet::Type::Schedule + + # This will probably get overridden by different tests + @now = Time.now + Puppet[:ignoreschedules] = false + end + + def mksched + s = nil + assert_nothing_raised { + s = @stype.create( + :name => "testsched" + ) + } + + s + end + + def diff(unit, incr, method, count) + diff = @now.to_i.send(method, incr * count) + t = Time.at(diff) + + #Puppet.notice "%s: %s %s %s = %s" % + # [unit, @now.send(unit), method, count, t] + #t.strftime("%H:%M:%S") + t + end + + def month(method, count) + diff(:hour, 3600 * 24 * 30, method, count) + end + + def week(method, count) + diff(:hour, 3600 * 24 * 7, method, count) + end + + def day(method, count) + diff(:hour, 3600 * 24, method, count) + end + + def hour(method, count) + diff(:hour, 3600, method, count) + end + + def min(method, count) + diff(:min, 60, method, count) + end + + def sec(method, count) + diff(:sec, 1, method, count) + end + + def settimes + unless defined? @@times + @@times = [Time.now] + + # Make one with an edge year on each side + ary = Time.now.to_a + [1999, 2000, 2001].each { |y| + ary[5] = y; @@times << Time.local(*ary) + } + + # And with edge hours + ary = Time.now.to_a + #[23, 0].each { |h| ary[2] = h; @@times << Time.local(*ary) } + # 23 hour + ary[2] = 23 + @@times << Time.local(*ary) + # 0 hour, next day + ary[2] = 0 + @@times << addday(Time.local(*ary)) + + # And with edge minutes + #[59, 0].each { |m| ary[1] = m; @@times << Time.local(*ary) } + ary = Time.now.to_a + ary[1] = 59; @@times << Time.local(*ary) + ary[1] = 0 + #if ary[2] == 23 + @@times << Time.local(*ary) + #else + # @@times << addday(Time.local(*ary)) + #end + end + + Puppet.err @@times.inspect + + @@times.each { |time| + @now = time + yield time + } + + @now = Time.now + end + + def test_range + s = mksched + + ary = @now.to_a + ary[2] = 12 + @now = Time.local(*ary) + data = { + true => [ + # An hour previous, an hour after + [hour("-", 1), hour("+", 1)], + + # an hour previous but a couple minutes later, and an hour plus + [min("-", 57), hour("+", 1)] + ], + false => [ + # five minutes from now, an hour from now + [min("+", 5), hour("+", 1)], + + # an hour ago, 20 minutes ago + [hour("-", 1), min("-", 20)] + ] + } + + data.each { |result, values| + values = values.collect { |value| + "%s - %s" % [value[0].strftime("%H:%M:%S"), + value[1].strftime("%H:%M:%S")] + } + values.each { |value| + assert_nothing_raised("Could not parse %s" % value) { + s[:range] = value + } + + assert_equal(result, s.match?(nil, @now), + "%s matched %s incorrectly" % [value.inspect, @now]) + } + + assert_nothing_raised("Could not parse %s" % values) { + s[:range] = values + } + + assert_equal(result, s.match?(nil, @now), + "%s matched %s incorrectly" % [values.inspect, @now]) + } + end + + def test_period_by_distance + previous = @now + + s = mksched + + assert_nothing_raised { + s[:period] = :daily + } + + assert(s.match?(day("-", 1)), "did not match minus a day") + assert(s.match?(day("-", 2)), "did not match two days") + assert(! s.match?(@now), "matched today") + assert(! s.match?(hour("-", 11)), "matched minus 11 hours") + + # Now test hourly + assert_nothing_raised { + s[:period] = :hourly + } + + assert(s.match?(hour("-", 1)), "did not match minus an hour") + assert(s.match?(hour("-", 2)), "did not match two hours") + assert(! s.match?(@now), "matched now") + assert(! s.match?(min("-", 59)), "matched minus 11 hours") + + # and weekly + assert_nothing_raised { + s[:period] = :weekly + } + + assert(s.match?(week("-", 1)), "did not match minus a week") + assert(s.match?(day("-", 7)), "did not match minus 7 days") + assert(s.match?(day("-", 8)), "did not match minus 8 days") + assert(s.match?(week("-", 2)), "did not match two weeks") + assert(! s.match?(@now), "matched now") + assert(! s.match?(day("-", 6)), "matched minus 6 days") + + # and monthly + assert_nothing_raised { + s[:period] = :monthly + } + + assert(s.match?(month("-", 1)), "did not match minus a month") + assert(s.match?(week("-", 5)), "did not match minus 5 weeks") + assert(s.match?(week("-", 7)), "did not match minus 7 weeks") + assert(s.match?(day("-", 50)), "did not match minus 50 days") + assert(s.match?(month("-", 2)), "did not match two months") + assert(! s.match?(@now), "matched now") + assert(! s.match?(week("-", 3)), "matched minus 3 weeks") + assert(! s.match?(day("-", 20)), "matched minus 20 days") + end + + # A shortened test... + def test_period_by_number + s = mksched + assert_nothing_raised { + s[:periodmatch] = :number + } + + assert_nothing_raised { + s[:period] = :daily + } + + assert(s.match?(day("+", 1)), "didn't match plus a day") + assert(s.match?(week("+", 1)), "didn't match plus a week") + assert(! s.match?(@now), "matched today") + assert(! s.match?(hour("-", 11)), "matched minus 11 hours") + assert(! s.match?(hour("-", 1)), "matched minus an hour") + assert(! s.match?(hour("-", 2)), "matched plus two hours") + + # Now test hourly + assert_nothing_raised { + s[:period] = :hourly + } + + assert(s.match?(hour("+", 1)), "did not match plus an hour") + assert(s.match?(hour("+", 2)), "did not match plus two hours") + assert(! s.match?(@now), "matched now") + assert(! s.match?(sec("+", 20)), "matched plus 20 seconds") + end + + def test_periodmatch_default + s = mksched + + match = s[:periodmatch] + assert(match, "Could not find periodmatch") + + assert_equal(:distance, match, "Periodmatch was %s" % match) + end + + def test_scheduled_objects + s = mksched + s[:period] = :hourly + + f = nil + path = tempfile() + assert_nothing_raised { + f = Puppet.type(:file).create( + :name => path, + :schedule => s.name, + :ensure => "file" + ) + } + + assert(f.scheduled?, "File is not scheduled to run") + + assert_apply(f) + + assert(! f.scheduled?, "File is scheduled to run already") + File.unlink(path) + + assert_apply(f) + + assert(! FileTest.exists?(path), "File was created when not scheduled") + end + + def test_latebinding_schedules + f = nil + path = tempfile() + assert_nothing_raised { + f = Puppet.type(:file).create( + :name => path, + :schedule => "testsched", + :ensure => "file" + ) + } + + s = mksched + s[:period] = :hourly + + assert_nothing_raised { + f.schedule + } + + assert(f.scheduled?, "File is not scheduled to run") + end + + # Verify that each of our default schedules exist + def test_defaultschedules + Puppet.type(:schedule).mkdefaultschedules + %w{puppet hourly daily weekly monthly}.each { |period| + assert(Puppet.type(:schedule)[period], "Could not find %s schedule" % + period) + } + end + + def test_period_with_repeat + previous = @now + + s = mksched + s[:period] = :hourly + + assert_nothing_raised("Was not able to set periodmatch") { + s[:periodmatch] = :number + } + assert_raise(Puppet::Error) { + s[:repeat] = 2 + } + assert_nothing_raised("Was not able to reset periodmatch") { + s[:periodmatch] = :distance + } + + assert(! s.match?(min("-", 40)), "matched minus 40 minutes") + + assert_nothing_raised("Was not able to set period") { + s[:repeat] = 2 + } + + assert(! s.match?(min("-", 20)), "matched minus 20 minutes with half-hourly") + assert(s.match?(min("-", 40)), "Did not match minus 40 with half-hourly") + + assert_nothing_raised("Was not able to set period") { + s[:repeat] = 3 + } + + assert(! s.match?(min("-", 15)), "matched minus 15 minutes with half-hourly") + assert(s.match?(min("-", 25)), "Did not match minus 25 with half-hourly") + end +end + +# $Id$ |
