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
This commit is contained in:
luke 2006-01-23 22:38:39 +00:00
Родитель 258114d48b
Коммит 18e8e74a2e
21 изменённых файлов: 1158 добавлений и 282 удалений

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

@ -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
def schedule
unless self[:schedule]
return
end
# Return a cached value
def cached(name)
Puppet::Storage.cache(self)[name]
#@cache[name] ||= nil
end
if sched = Puppet.type(:schedule)[self[:schedule]]
self[:schedule] = sched
# 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
# 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

Просмотреть файл

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

Просмотреть файл

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

341
lib/puppet/type/schedule.rb Executable file
Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

336
test/types/schedule.rb Executable file
Просмотреть файл

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