diff --git a/lib/puppet/transaction/change.rb b/lib/puppet/transaction/change.rb index ecc3b5a5f..d57ac1917 100644 --- a/lib/puppet/transaction/change.rb +++ b/lib/puppet/transaction/change.rb @@ -1,87 +1,75 @@ require 'puppet/transaction' require 'puppet/transaction/event' # Handle all of the work around performing an actual change, # including calling 'sync' on the properties and producing events. class Puppet::Transaction::Change - attr_accessor :is, :should, :property, :proxy, :auditing + attr_accessor :is, :should, :property, :proxy, :auditing, :old_audit_value def auditing? auditing end - # Create our event object. - def event - result = property.event - result.previous_value = is - result.desired_value = should - result - end - def initialize(property, currentvalue) @property = property @is = currentvalue @should = property.should @changed = false end def apply - return audit_event if auditing? - return noop_event if noop? - - property.sync - - result = event - result.message = property.change_to_s(is, should) - result.status = "success" - result.send_log - result + event = property.event + event.previous_value = is + event.desired_value = should + event.historical_value = old_audit_value + + if auditing? and old_audit_value != is + event.message = "audit change: previously recorded value #{property.is_to_s(old_audit_value)} has been changed to #{property.is_to_s(is)}" + event.status = "audit" + event.audited = true + brief_audit_message = " (previously recorded value was #{property.is_to_s(old_audit_value)})" + else + brief_audit_message = "" + end + + if property.insync?(is) + # nothing happens + elsif noop? + event.message = "is #{property.is_to_s(is)}, should be #{property.should_to_s(should)} (noop)#{brief_audit_message}" + event.status = "noop" + else + property.sync + event.message = [ property.change_to_s(is, should), brief_audit_message ].join + event.status = "success" + end + event rescue => detail puts detail.backtrace if Puppet[:trace] - result = event - result.status = "failure" + event.status = "failure" - result.message = "change from #{property.is_to_s(is)} to #{property.should_to_s(should)} failed: #{detail}" - result.send_log - result + event.message = "change from #{property.is_to_s(is)} to #{property.should_to_s(should)} failed: #{detail}" + event + ensure + event.send_log end # Is our property noop? This is used for generating special events. def noop? @property.noop end # The resource that generated this change. This is used for handling events, # and the proxy resource is used for generated resources, since we can't # send an event to a resource we don't have a direct relationship with. If we # have a proxy resource, then the events will be considered to be from # that resource, rather than us, so the graph resolution will still work. def resource self.proxy || @property.resource end def to_s "change #{@property.change_to_s(@is, @should)}" end - - private - - def audit_event - # This needs to store the appropriate value, and then produce a new event - result = event - result.message = "audit change: previously recorded value #{property.should_to_s(should)} has been changed to #{property.is_to_s(is)}" - result.status = "audit" - result.send_log - result - end - - def noop_event - result = event - result.message = "is #{property.is_to_s(is)}, should be #{property.should_to_s(should)} (noop)" - result.status = "noop" - result.send_log - result - end end diff --git a/lib/puppet/transaction/event.rb b/lib/puppet/transaction/event.rb index e5e5793da..da5b14727 100644 --- a/lib/puppet/transaction/event.rb +++ b/lib/puppet/transaction/event.rb @@ -1,61 +1,61 @@ require 'puppet/transaction' require 'puppet/util/tagging' require 'puppet/util/logging' # A simple struct for storing what happens on the system. class Puppet::Transaction::Event include Puppet::Util::Tagging include Puppet::Util::Logging - ATTRIBUTES = [:name, :resource, :property, :previous_value, :desired_value, :status, :message, :node, :version, :file, :line, :source_description] + ATTRIBUTES = [:name, :resource, :property, :previous_value, :desired_value, :historical_value, :status, :message, :node, :version, :file, :line, :source_description, :audited] attr_accessor *ATTRIBUTES attr_writer :tags attr_accessor :time attr_reader :default_log_level EVENT_STATUSES = %w{noop success failure audit} def initialize(*args) options = args.last.is_a?(Hash) ? args.pop : ATTRIBUTES.inject({}) { |hash, attr| hash[attr] = args.pop; hash } options.each { |attr, value| send(attr.to_s + "=", value) unless value.nil? } @time = Time.now end def property=(prop) @property = prop.to_s end def resource=(res) if res.respond_to?(:[]) and level = res[:loglevel] @default_log_level = level end @resource = res.to_s end def send_log super(log_level, message) end def status=(value) raise ArgumentError, "Event status can only be #{EVENT_STATUSES.join(', ')}" unless EVENT_STATUSES.include?(value) @status = value end def to_s message end private # If it's a failure, use 'err', else use either the resource's log level (if available) # or 'notice'. def log_level status == "failure" ? :err : (@default_log_level || :notice) end # Used by the Logging module def log_source source_description || property || resource end end diff --git a/lib/puppet/transaction/resource_harness.rb b/lib/puppet/transaction/resource_harness.rb index 29ec9a539..c978e5545 100644 --- a/lib/puppet/transaction/resource_harness.rb +++ b/lib/puppet/transaction/resource_harness.rb @@ -1,150 +1,152 @@ require 'puppet/resource/status' class Puppet::Transaction::ResourceHarness extend Forwardable def_delegators :@transaction, :relationship_graph attr_reader :transaction def allow_changes?(resource) return true unless resource.purging? and resource.deleting? return true unless deps = relationship_graph.dependents(resource) and ! deps.empty? and deps.detect { |d| ! d.deleting? } deplabel = deps.collect { |r| r.ref }.join(",") plurality = deps.length > 1 ? "":"s" resource.warning "#{deplabel} still depend#{plurality} on me -- not purging" false end def apply_changes(status, changes) changes.each do |change| status << change.apply cache(change.property.resource, change.property.name, change.is) if change.auditing? end status.changed = true end - # Used mostly for scheduling at this point. + # Used mostly for scheduling and auditing at this point. def cached(resource, name) Puppet::Util::Storage.cache(resource)[name] end - # Used mostly for scheduling at this point. + # Used mostly for scheduling and auditing at this point. def cache(resource, name, value) Puppet::Util::Storage.cache(resource)[name] = value end def changes_to_perform(status, resource) current = resource.retrieve_resource cache resource, :checked, Time.now return [] if ! allow_changes?(resource) audited = copy_audited_parameters(resource, current) if param = resource.parameter(:ensure) return [] if absent_and_not_being_created?(current, param) - return [Puppet::Transaction::Change.new(param, current[:ensure])] unless ensure_is_insync?(current, param) + unless ensure_is_insync?(current, param) + audited.keys.reject{|name| name == :ensure}.each do |name| + resource.parameter(name).notice "audit change: previously recorded value #{audited[name]} has been changed to #{current[param]}" + cache(resource, name, current[param]) + end + return [Puppet::Transaction::Change.new(param, current[:ensure])] + end return [] if ensure_should_be_absent?(current, param) end - resource.properties.reject { |p| p.name == :ensure }.reject do |param| - param.should.nil? - end.reject do |param| - param_is_insync?(current, param) + resource.properties.reject { |param| param.name == :ensure }.select do |param| + (audited.include?(param.name) && audited[param.name] != current[param.name]) || (param.should != nil && !param_is_insync?(current, param)) end.collect do |param| change = Puppet::Transaction::Change.new(param, current[param.name]) change.auditing = true if audited.include?(param.name) + change.old_audit_value = audited[param.name] change end end def copy_audited_parameters(resource, current) - return [] unless audit = resource[:audit] + return {} unless audit = resource[:audit] audit = Array(audit).collect { |p| p.to_sym } - audited = [] + audited = {} audit.find_all do |param| - next if resource[param] - if value = cached(resource, param) - resource[param] = value - audited << param + audited[param] = value else - resource.debug "Storing newly-audited value #{current[param]} for #{param}" + resource.property(param).notice "audit change: newly-recorded recorded value #{current[param]}" cache(resource, param, current[param]) end end audited end def evaluate(resource) start = Time.now status = Puppet::Resource::Status.new(resource) if changes = changes_to_perform(status, resource) and ! changes.empty? status.out_of_sync = true status.change_count = changes.length apply_changes(status, changes) if ! resource.noop? cache(resource, :synced, Time.now) resource.flush if resource.respond_to?(:flush) end end return status rescue => detail resource.fail "Could not create resource status: #{detail}" unless status puts detail.backtrace if Puppet[:trace] resource.err "Could not evaluate: #{detail}" status.failed = true return status ensure (status.evaluation_time = Time.now - start) if status end def initialize(transaction) @transaction = transaction end def scheduled?(status, resource) return true if Puppet[:ignoreschedules] return true unless schedule = schedule(resource) # We use 'checked' here instead of 'synced' because otherwise we'll # end up checking most resources 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). schedule.match?(cached(resource, :checked).to_i) end def schedule(resource) unless resource.catalog resource.warning "Cannot schedule without a schedule-containing catalog" return nil end return nil unless name = resource[:schedule] resource.catalog.resource(:schedule, name) || resource.fail("Could not find schedule #{name}") end private def absent_and_not_being_created?(current, param) current[:ensure] == :absent and param.should.nil? end def ensure_is_insync?(current, param) param.insync?(current[:ensure]) end def ensure_should_be_absent?(current, param) param.should == :absent end def param_is_insync?(current, param) param.insync?(current[param.name]) end end diff --git a/lib/puppet/util/log.rb b/lib/puppet/util/log.rb index 36a765c61..7764dc1d1 100644 --- a/lib/puppet/util/log.rb +++ b/lib/puppet/util/log.rb @@ -1,257 +1,258 @@ require 'puppet/util/tagging' require 'puppet/util/classgen' # Pass feedback to the user. Log levels are modeled after syslog's, and it is # expected that that will be the most common log destination. Supports # multiple destinations, one of which is a remote server. class Puppet::Util::Log include Puppet::Util extend Puppet::Util::ClassGen include Puppet::Util::Tagging @levels = [:debug,:info,:notice,:warning,:err,:alert,:emerg,:crit] @loglevel = 2 @desttypes = {} # Create a new destination type. def self.newdesttype(name, options = {}, &block) - dest = genclass( - name, :parent => Puppet::Util::Log::Destination, :prefix => "Dest", - :block => block, - :hash => @desttypes, - + dest = genclass( + name, + :parent => Puppet::Util::Log::Destination, + :prefix => "Dest", + :block => block, + :hash => @desttypes, :attributes => options ) dest.match(dest.name) dest end require 'puppet/util/log/destination' require 'puppet/util/log/destinations' @destinations = {} @queued = [] class << self include Puppet::Util include Puppet::Util::ClassGen attr_reader :desttypes end # Reset log to basics. Basically just flushes and closes files and # undefs other objects. def Log.close(destination) if @destinations.include?(destination) @destinations[destination].flush if @destinations[destination].respond_to?(:flush) @destinations[destination].close if @destinations[destination].respond_to?(:close) @destinations.delete(destination) end end def self.close_all destinations.keys.each { |dest| close(dest) } end # Flush any log destinations that support such operations. def Log.flush @destinations.each { |type, dest| dest.flush if dest.respond_to?(:flush) } end # Create a new log message. The primary role of this method is to # avoid creating log messages below the loglevel. def Log.create(hash) raise Puppet::DevError, "Logs require a level" unless hash.include?(:level) raise Puppet::DevError, "Invalid log level #{hash[:level]}" unless @levels.index(hash[:level]) @levels.index(hash[:level]) >= @loglevel ? Puppet::Util::Log.new(hash) : nil end def Log.destinations @destinations end # Yield each valid level in turn def Log.eachlevel @levels.each { |level| yield level } end # Return the current log level. def Log.level @levels[@loglevel] end # Set the current log level. def Log.level=(level) level = level.intern unless level.is_a?(Symbol) raise Puppet::DevError, "Invalid loglevel #{level}" unless @levels.include?(level) @loglevel = @levels.index(level) end def Log.levels @levels.dup end # Create a new log destination. def Log.newdestination(dest) # Each destination can only occur once. if @destinations.find { |name, obj| obj.name == dest } return end name, type = @desttypes.find do |name, klass| klass.match?(dest) end raise Puppet::DevError, "Unknown destination type #{dest}" unless type begin if type.instance_method(:initialize).arity == 1 @destinations[dest] = type.new(dest) else @destinations[dest] = type.new end flushqueue @destinations[dest] rescue => detail puts detail.backtrace if Puppet[:debug] # If this was our only destination, then add the console back in. newdestination(:console) if @destinations.empty? and (dest != :console and dest != "console") end end # Route the actual message. FIXME There are lots of things this method # should do, like caching and a bit more. It's worth noting that there's # a potential for a loop here, if the machine somehow gets the destination set as # itself. def Log.newmessage(msg) return if @levels.index(msg.level) < @loglevel queuemessage(msg) if @destinations.length == 0 @destinations.each do |name, dest| threadlock(dest) do dest.handle(msg) end end end def Log.queuemessage(msg) @queued.push(msg) end def Log.flushqueue return unless @destinations.size >= 1 @queued.each do |msg| Log.newmessage(msg) end @queued.clear end def Log.sendlevel?(level) @levels.index(level) >= @loglevel end # Reopen all of our logs. def Log.reopen Puppet.notice "Reopening log files" types = @destinations.keys @destinations.each { |type, dest| dest.close if dest.respond_to?(:close) } @destinations.clear # We need to make sure we always end up with some kind of destination begin types.each { |type| Log.newdestination(type) } rescue => detail if @destinations.empty? Log.newdestination(:syslog) Puppet.err detail.to_s end end end # Is the passed level a valid log level? def self.validlevel?(level) @levels.include?(level) end attr_accessor :time, :remote, :file, :line, :version, :source attr_reader :level, :message def initialize(args) self.level = args[:level] self.message = args[:message] self.source = args[:source] || "Puppet" @time = Time.now if tags = args[:tags] tags.each { |t| self.tag(t) } end [:file, :line, :version].each do |attr| next unless value = args[attr] send(attr.to_s + "=", value) end Log.newmessage(self) end def message=(msg) raise ArgumentError, "Puppet::Util::Log requires a message" unless msg @message = msg.to_s end def level=(level) raise ArgumentError, "Puppet::Util::Log requires a log level" unless level @level = level.to_sym raise ArgumentError, "Invalid log level #{@level}" unless self.class.validlevel?(@level) # Tag myself with my log level tag(level) end # If they pass a source in to us, we make sure it is a string, and # we retrieve any tags we can. def source=(source) if source.respond_to?(:source_descriptors) descriptors = source.source_descriptors @source = descriptors[:path] descriptors[:tags].each { |t| tag(t) } [:file, :line, :version].each do |param| next unless descriptors[param] send(param.to_s + "=", descriptors[param]) end else @source = source.to_s end end def to_report "#{time} #{source} (#{level}): #{to_s}" end def to_s message end end # This is for backward compatibility from when we changed the constant to Puppet::Util::Log # because the reports include the constant name. Apparently the alias was created in # March 2007, should could probably be removed soon. Puppet::Log = Puppet::Util::Log diff --git a/spec/unit/transaction/change_spec.rb b/spec/unit/transaction/change_spec.rb index e443e3baa..fbc662df0 100755 --- a/spec/unit/transaction/change_spec.rb +++ b/spec/unit/transaction/change_spec.rb @@ -1,193 +1,206 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet/transaction/change' describe Puppet::Transaction::Change do Change = Puppet::Transaction::Change describe "when initializing" do before do @property = stub 'property', :path => "/property/path", :should => "shouldval" end it "should require the property and current value" do lambda { Change.new }.should raise_error end it "should set its property to the provided property" do Change.new(@property, "value").property.should == :property end it "should set its 'is' value to the provided value" do Change.new(@property, "value").is.should == "value" end it "should retrieve the 'should' value from the property" do # Yay rspec :) Change.new(@property, "value").should.should == @property.should end end describe "when an instance" do before do - @property = stub 'property', :path => "/property/path", :should => "shouldval" + @property = stub 'property', :path => "/property/path", :should => "shouldval", :is_to_s => 'formatted_property' @change = Change.new(@property, "value") end it "should be noop if the property is noop" do @property.expects(:noop).returns true @change.noop?.should be_true end it "should be auditing if set so" do @change.auditing = true @change.must be_auditing end it "should set its resource to the proxy if it has one" do @change.proxy = :myresource @change.resource.should == :myresource end it "should set its resource to the property's resource if no proxy is set" do @property.expects(:resource).returns :myresource @change.resource.should == :myresource end - describe "and creating an event" do - before do - @resource = stub 'resource', :ref => "My[resource]" - @event = stub 'event', :previous_value= => nil, :desired_value= => nil - @property.stubs(:event).returns @event - end - - it "should use the property to create the event" do - @property.expects(:event).returns @event - @change.event.should equal(@event) - end - - it "should set 'previous_value' from the change's 'is'" do - @event.expects(:previous_value=).with(@change.is) - @change.event - end - - it "should set 'desired_value' from the change's 'should'" do - @event.expects(:desired_value=).with(@change.should) - @change.event - end - end - describe "and executing" do before do @event = Puppet::Transaction::Event.new(:myevent) @event.stubs(:send_log) @change.stubs(:noop?).returns false @property.stubs(:event).returns @event @property.stub_everything @property.stubs(:resource).returns "myresource" @property.stubs(:name).returns :myprop end describe "in noop mode" do before { @change.stubs(:noop?).returns true } it "should log that it is in noop" do @property.expects(:is_to_s) @property.expects(:should_to_s) @event.expects(:message=).with { |msg| msg.include?("should be") } @change.apply end it "should produce a :noop event and return" do @property.stub_everything + @property.expects(:sync).never.never.never.never.never # VERY IMPORTANT @event.expects(:status=).with("noop") @change.apply.should == @event end end describe "in audit mode" do - before { @change.auditing = true } + before do + @change.auditing = true + @change.old_audit_value = "old_value" + @property.stubs(:insync?).returns(true) + end it "should log that it is in audit mode" do - @property.expects(:is_to_s) - @property.expects(:should_to_s) - - @event.expects(:message=).with { |msg| msg.include?("audit") } + message = nil + @event.expects(:message=).with { |msg| message = msg } @change.apply + message.should == "audit change: previously recorded value formatted_property has been changed to formatted_property" end it "should produce a :audit event and return" do @property.stub_everything @event.expects(:status=).with("audit") @change.apply.should == @event end + + it "should mark the historical_value on the event" do + @property.stub_everything + + @change.apply.historical_value.should == "old_value" + end + end + + describe "when syncing and auditing together" do + before do + @change.auditing = true + @change.old_audit_value = "old_value" + @property.stubs(:insync?).returns(false) + end + + it "should sync the property" do + @property.expects(:sync) + + @change.apply + end + + it "should produce a success event" do + @property.stub_everything + + @change.apply.status.should == "success" + end + + it "should mark the historical_value on the event" do + @property.stub_everything + + @change.apply.historical_value.should == "old_value" + end end it "should sync the property" do @property.expects(:sync) @change.apply end it "should return the default event if syncing the property returns nil" do @property.stubs(:sync).returns nil - @change.expects(:event).with(nil).returns @event + @property.expects(:event).with(nil).returns @event @change.apply.should == @event end it "should return the default event if syncing the property returns an empty array" do @property.stubs(:sync).returns [] - @change.expects(:event).with(nil).returns @event + @property.expects(:event).with(nil).returns @event @change.apply.should == @event end it "should log the change" do @property.expects(:sync).returns [:one] @event.expects(:send_log) @change.apply end it "should set the event's message to the change log" do @property.expects(:change_to_s).returns "my change" @change.apply.message.should == "my change" end it "should set the event's status to 'success'" do @change.apply.status.should == "success" end describe "and the change fails" do before { @property.expects(:sync).raises "an exception" } it "should catch the exception and log the err" do @event.expects(:send_log) lambda { @change.apply }.should_not raise_error end it "should mark the event status as 'failure'" do @change.apply.status.should == "failure" end it "should set the event log to a failure log" do @change.apply.message.should be_include("failed") end end end end end diff --git a/spec/unit/transaction/resource_harness_spec.rb b/spec/unit/transaction/resource_harness_spec.rb index 4611409d5..b143c21ed 100755 --- a/spec/unit/transaction/resource_harness_spec.rb +++ b/spec/unit/transaction/resource_harness_spec.rb @@ -1,401 +1,514 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' +require 'puppet_spec/files' require 'puppet/transaction/resource_harness' describe Puppet::Transaction::ResourceHarness do + include PuppetSpec::Files + before do @transaction = Puppet::Transaction.new(Puppet::Resource::Catalog.new) @resource = Puppet::Type.type(:file).new :path => "/my/file" @harness = Puppet::Transaction::ResourceHarness.new(@transaction) @current_state = Puppet::Resource.new(:file, "/my/file") @resource.stubs(:retrieve).returns @current_state @status = Puppet::Resource::Status.new(@resource) Puppet::Resource::Status.stubs(:new).returns @status end it "should accept a transaction at initialization" do harness = Puppet::Transaction::ResourceHarness.new(@transaction) harness.transaction.should equal(@transaction) end it "should delegate to the transaction for its relationship graph" do @transaction.expects(:relationship_graph).returns "relgraph" Puppet::Transaction::ResourceHarness.new(@transaction).relationship_graph.should == "relgraph" end - describe "when copying audited parameters" do - before do - @resource = Puppet::Type.type(:file).new :path => "/foo/bar", :audit => :mode - end - - it "should do nothing if no parameters are being audited" do - @resource[:audit] = [] - @harness.expects(:cached).never - @harness.copy_audited_parameters(@resource, {}).should == [] - end - - it "should do nothing if an audited parameter already has a desired value set" do - @resource[:mode] = "755" - @harness.expects(:cached).never - @harness.copy_audited_parameters(@resource, {}).should == [] - end - - it "should copy any cached values to the 'should' values" do - @harness.cache(@resource, :mode, "755") - @harness.copy_audited_parameters(@resource, {}).should == [:mode] - - @resource[:mode].should == "755" - end - - it "should cache and log the current value if no cached values are present" do - @resource.expects(:debug) - @harness.copy_audited_parameters(@resource, {:mode => "755"}).should == [] - - @harness.cached(@resource, :mode).should == "755" - end - end - describe "when evaluating a resource" do it "should create and return a resource status instance for the resource" do @harness.evaluate(@resource).should be_instance_of(Puppet::Resource::Status) end it "should fail if no status can be created" do Puppet::Resource::Status.expects(:new).raises ArgumentError lambda { @harness.evaluate(@resource) }.should raise_error end it "should retrieve the current state of the resource" do @resource.expects(:retrieve).returns @current_state @harness.evaluate(@resource) end it "should mark the resource as failed and return if the current state cannot be retrieved" do @resource.expects(:retrieve).raises ArgumentError @harness.evaluate(@resource).should be_failed end it "should use the status and retrieved state to determine which changes need to be made" do @harness.expects(:changes_to_perform).with(@status, @resource).returns [] @harness.evaluate(@resource) end it "should mark the status as out of sync and apply the created changes if there are any" do changes = %w{mychanges} @harness.expects(:changes_to_perform).returns changes @harness.expects(:apply_changes).with(@status, changes) @harness.evaluate(@resource).should be_out_of_sync end it "should cache the last-synced time" do changes = %w{mychanges} @harness.stubs(:changes_to_perform).returns changes @harness.stubs(:apply_changes) @harness.expects(:cache).with { |resource, name, time| name == :synced and time.is_a?(Time) } @harness.evaluate(@resource) end it "should flush the resource when applying changes if appropriate" do changes = %w{mychanges} @harness.stubs(:changes_to_perform).returns changes @harness.stubs(:apply_changes) @resource.expects(:flush) @harness.evaluate(@resource) end it "should use the status and retrieved state to determine which changes need to be made" do @harness.expects(:changes_to_perform).with(@status, @resource).returns [] @harness.evaluate(@resource) end it "should not attempt to apply changes if none need to be made" do @harness.expects(:changes_to_perform).returns [] @harness.expects(:apply_changes).never @harness.evaluate(@resource).should_not be_out_of_sync end it "should store the resource's evaluation time in the resource status" do @harness.evaluate(@resource).evaluation_time.should be_instance_of(Float) end it "should set the change count to the total number of changes" do changes = %w{a b c d} @harness.expects(:changes_to_perform).returns changes @harness.expects(:apply_changes).with(@status, changes) @harness.evaluate(@resource).change_count.should == 4 end end describe "when creating changes" do before do @current_state = Puppet::Resource.new(:file, "/my/file") @resource.stubs(:retrieve).returns @current_state Puppet.features.stubs(:root?).returns true end it "should retrieve the current values from the resource" do @resource.expects(:retrieve).returns @current_state @harness.changes_to_perform(@status, @resource) end it "should cache that the resource was checked" do @harness.expects(:cache).with { |resource, name, time| name == :checked and time.is_a?(Time) } @harness.changes_to_perform(@status, @resource) end it "should create changes with the appropriate property and current value" do @resource[:ensure] = :present @current_state[:ensure] = :absent change = stub 'change' Puppet::Transaction::Change.expects(:new).with(@resource.parameter(:ensure), :absent).returns change @harness.changes_to_perform(@status, @resource)[0].should equal(change) end it "should not attempt to manage properties that do not have desired values set" do mode = @resource.newattr(:mode) @current_state[:mode] = :absent mode.expects(:insync?).never @harness.changes_to_perform(@status, @resource) end - it "should copy audited parameters" do - @resource[:audit] = :mode - @harness.cache(@resource, :mode, "755") - @harness.changes_to_perform(@status, @resource) - @resource[:mode].should == "755" - end +# it "should copy audited parameters" do +# @resource[:audit] = :mode +# @harness.cache(@resource, :mode, "755") +# @harness.changes_to_perform(@status, @resource) +# @resource[:mode].should == "755" +# end it "should mark changes created as a result of auditing as auditing changes" do @current_state[:mode] = 0644 @resource[:audit] = :mode @harness.cache(@resource, :mode, "755") @harness.changes_to_perform(@status, @resource)[0].must be_auditing end describe "and the 'ensure' parameter is present but not in sync" do it "should return a single change for the 'ensure' parameter" do @resource[:ensure] = :present @resource[:mode] = "755" @current_state[:ensure] = :absent @current_state[:mode] = :absent @resource.stubs(:retrieve).returns @current_state changes = @harness.changes_to_perform(@status, @resource) changes.length.should == 1 changes[0].property.name.should == :ensure end end describe "and the 'ensure' parameter should be set to 'absent', and is correctly set to 'absent'" do it "should return no changes" do @resource[:ensure] = :absent @resource[:mode] = "755" @current_state[:ensure] = :absent @current_state[:mode] = :absent @harness.changes_to_perform(@status, @resource).should == [] end end describe "and the 'ensure' parameter is 'absent' and there is no 'desired value'" do it "should return no changes" do @resource.newattr(:ensure) @resource[:mode] = "755" @current_state[:ensure] = :absent @current_state[:mode] = :absent @harness.changes_to_perform(@status, @resource).should == [] end end describe "and non-'ensure' parameters are not in sync" do it "should return a change for each parameter that is not in sync" do @resource[:ensure] = :present @resource[:mode] = "755" @resource[:owner] = 0 @current_state[:ensure] = :present @current_state[:mode] = 0444 @current_state[:owner] = 50 - mode = stub 'mode_change' - owner = stub 'owner_change' + mode = stub_everything 'mode_change' + owner = stub_everything 'owner_change' Puppet::Transaction::Change.expects(:new).with(@resource.parameter(:mode), 0444).returns mode Puppet::Transaction::Change.expects(:new).with(@resource.parameter(:owner), 50).returns owner changes = @harness.changes_to_perform(@status, @resource) changes.length.should == 2 changes.should be_include(mode) changes.should be_include(owner) end end describe "and all parameters are in sync" do it "should return an empty array" do @resource[:ensure] = :present @resource[:mode] = "755" @current_state[:ensure] = :present @current_state[:mode] = "755" @harness.changes_to_perform(@status, @resource).should == [] end end end describe "when applying changes" do before do @change1 = stub 'change1', :apply => stub("event", :status => "success"), :auditing? => false @change2 = stub 'change2', :apply => stub("event", :status => "success"), :auditing? => false @changes = [@change1, @change2] end it "should apply the change" do @change1.expects(:apply).returns( stub("event", :status => "success") ) @change2.expects(:apply).returns( stub("event", :status => "success") ) @harness.apply_changes(@status, @changes) end it "should mark the resource as changed" do @harness.apply_changes(@status, @changes) @status.should be_changed end it "should queue the resulting event" do @harness.apply_changes(@status, @changes) @status.events.should be_include(@change1.apply) @status.events.should be_include(@change2.apply) end it "should cache the new value if it is an auditing change" do @change1.expects(:auditing?).returns true property = stub 'property', :name => "foo", :resource => "myres" @change1.stubs(:property).returns property @change1.stubs(:is).returns "myval" @harness.apply_changes(@status, @changes) @harness.cached("myres", "foo").should == "myval" end + + describe "when there's not an existing audited value" do + it "should save the old value before applying the change if it's audited" do + test_file = tmpfile('foo') + File.open(test_file, "w", 0750).close + + resource = Puppet::Type.type(:file).new :path => test_file, :mode => '755', :audit => :mode + + @harness.evaluate(resource) + @harness.cached(resource, :mode).should == "750" + + (File.stat(test_file).mode & 0777).should == 0755 + @logs.map {|l| "#{l.level}: #{l.source}: #{l.message}"}.should =~ [ + "notice: /#{resource}/mode: mode changed '750' to '755'", + "notice: /#{resource}/mode: audit change: newly-recorded recorded value 750" + ] + end + + it "should audit the value if there's no change" do + test_file = tmpfile('foo') + File.open(test_file, "w", 0755).close + + resource = Puppet::Type.type(:file).new :path => test_file, :mode => '755', :audit => :mode + + @harness.evaluate(resource) + @harness.cached(resource, :mode).should == "755" + + (File.stat(test_file).mode & 0777).should == 0755 + + @logs.map {|l| "#{l.level}: #{l.source}: #{l.message}"}.should =~ [ + "notice: /#{resource}/mode: audit change: newly-recorded recorded value 755" + ] + end + + it "should have :absent for audited value if the file doesn't exist" do + test_file = tmpfile('foo') + + resource = Puppet::Type.type(:file).new :ensure => 'present', :path => test_file, :mode => '755', :audit => :mode + + @harness.evaluate(resource) + @harness.cached(resource, :mode).should == :absent + + (File.stat(test_file).mode & 0777).should == 0755 + @logs.map {|l| "#{l.level}: #{l.source}: #{l.message}"}.should =~ [ + "notice: /#{resource}/ensure: created", + "notice: /#{resource}/mode: audit change: newly-recorded recorded value absent" + ] + end + + it "should do nothing if there are no changes to make and the stored value is correct" do + test_file = tmpfile('foo') + + resource = Puppet::Type.type(:file).new :path => test_file, :mode => '755', :audit => :mode, :ensure => 'absent' + @harness.cache(resource, :mode, :absent) + + @harness.evaluate(resource) + @harness.cached(resource, :mode).should == :absent + + File.exists?(test_file).should == false + @logs.map {|l| "#{l.level}: #{l.source}: #{l.message}"}.should =~ [] + end + end + + describe "when there's an existing audited value" do + it "should save the old value before applying the change" do + test_file = tmpfile('foo') + File.open(test_file, "w", 0750).close + + resource = Puppet::Type.type(:file).new :path => test_file, :audit => :mode + @harness.cache(resource, :mode, '555') + + @harness.evaluate(resource) + @harness.cached(resource, :mode).should == "750" + + (File.stat(test_file).mode & 0777).should == 0750 + @logs.map {|l| "#{l.level}: #{l.source}: #{l.message}"}.should =~ [ + "notice: /#{resource}/mode: audit change: previously recorded value 555 has been changed to 750" + ] + end + + it "should save the old value before applying the change" do + test_file = tmpfile('foo') + File.open(test_file, "w", 0750).close + + resource = Puppet::Type.type(:file).new :path => test_file, :mode => '755', :audit => :mode + @harness.cache(resource, :mode, '555') + + @harness.evaluate(resource) + @harness.cached(resource, :mode).should == "750" + + (File.stat(test_file).mode & 0777).should == 0755 + @logs.map {|l| "#{l.level}: #{l.source}: #{l.message}"}.should =~ [ + "notice: /#{resource}/mode: mode changed '750' to '755' (previously recorded value was 555)" + ] + end + + it "should audit the value if there's no change" do + test_file = tmpfile('foo') + File.open(test_file, "w", 0755).close + + resource = Puppet::Type.type(:file).new :path => test_file, :mode => '755', :audit => :mode + @harness.cache(resource, :mode, '555') + + @harness.evaluate(resource) + @harness.cached(resource, :mode).should == "755" + + (File.stat(test_file).mode & 0777).should == 0755 + @logs.map {|l| "#{l.level}: #{l.source}: #{l.message}"}.should =~ [ + "notice: /#{resource}/mode: audit change: previously recorded value 555 has been changed to 755" + ] + end + + it "should have :absent for audited value if the file doesn't exist" do + test_file = tmpfile('foo') + + resource = Puppet::Type.type(:file).new :ensure => 'present', :path => test_file, :mode => '755', :audit => :mode + @harness.cache(resource, :mode, '555') + + @harness.evaluate(resource) + @harness.cached(resource, :mode).should == :absent + + (File.stat(test_file).mode & 0777).should == 0755 + + @logs.map {|l| "#{l.level}: #{l.source}: #{l.message}"}.should =~ [ + "notice: /#{resource}/ensure: created", "notice: /#{resource}/mode: audit change: previously recorded value 555 has been changed to absent" + ] + end + + it "should do nothing if there are no changes to make and the stored value is correct" do + test_file = tmpfile('foo') + File.open(test_file, "w", 0755).close + + resource = Puppet::Type.type(:file).new :path => test_file, :mode => '755', :audit => :mode + @harness.cache(resource, :mode, '755') + + @harness.evaluate(resource) + @harness.cached(resource, :mode).should == "755" + + (File.stat(test_file).mode & 0777).should == 0755 + @logs.map {|l| "#{l.level}: #{l.source}: #{l.message}"}.should =~ [] + end + end end describe "when determining whether the resource can be changed" do before do @resource.stubs(:purging?).returns true @resource.stubs(:deleting?).returns true end it "should be true if the resource is not being purged" do @resource.expects(:purging?).returns false @harness.should be_allow_changes(@resource) end it "should be true if the resource is not being deleted" do @resource.expects(:deleting?).returns false @harness.should be_allow_changes(@resource) end it "should be true if the resource has no dependents" do @harness.relationship_graph.expects(:dependents).with(@resource).returns [] @harness.should be_allow_changes(@resource) end it "should be true if all dependents are being deleted" do dep = stub 'dependent', :deleting? => true @harness.relationship_graph.expects(:dependents).with(@resource).returns [dep] @resource.expects(:purging?).returns true @harness.should be_allow_changes(@resource) end it "should be false if the resource's dependents are not being deleted" do dep = stub 'dependent', :deleting? => false, :ref => "myres" @resource.expects(:warning) @harness.relationship_graph.expects(:dependents).with(@resource).returns [dep] @harness.should_not be_allow_changes(@resource) end end describe "when finding the schedule" do before do @catalog = Puppet::Resource::Catalog.new @resource.catalog = @catalog end it "should warn and return nil if the resource has no catalog" do @resource.catalog = nil @resource.expects(:warning) @harness.schedule(@resource).should be_nil end it "should return nil if the resource specifies no schedule" do @harness.schedule(@resource).should be_nil end it "should fail if the named schedule cannot be found" do @resource[:schedule] = "whatever" @resource.expects(:fail) @harness.schedule(@resource) end it "should return the named schedule if it exists" do sched = Puppet::Type.type(:schedule).new(:name => "sched") @catalog.add_resource(sched) @resource[:schedule] = "sched" @harness.schedule(@resource).to_s.should == sched.to_s end end describe "when determining if a resource is scheduled" do before do @catalog = Puppet::Resource::Catalog.new @resource.catalog = @catalog @status = Puppet::Resource::Status.new(@resource) end it "should return true if 'ignoreschedules' is set" do Puppet[:ignoreschedules] = true @resource[:schedule] = "meh" @harness.should be_scheduled(@status, @resource) end it "should return true if the resource has no schedule set" do @harness.should be_scheduled(@status, @resource) end it "should return the result of matching the schedule with the cached 'checked' time if a schedule is set" do t = Time.now @harness.expects(:cached).with(@resource, :checked).returns(t) sched = Puppet::Type.type(:schedule).new(:name => "sched") @catalog.add_resource(sched) @resource[:schedule] = "sched" sched.expects(:match?).with(t.to_i).returns "feh" @harness.scheduled?(@status, @resource).should == "feh" end end it "should be able to cache data in the Storage module" do data = {} Puppet::Util::Storage.expects(:cache).with(@resource).returns data @harness.cache(@resource, :foo, "something") data[:foo].should == "something" end it "should be able to retrieve data from the cache" do data = {:foo => "other"} Puppet::Util::Storage.expects(:cache).with(@resource).returns data @harness.cached(@resource, :foo).should == "other" end end diff --git a/test/lib/puppettest/support/utils.rb b/test/lib/puppettest/support/utils.rb index e022f123c..bca5d9634 100644 --- a/test/lib/puppettest/support/utils.rb +++ b/test/lib/puppettest/support/utils.rb @@ -1,160 +1,160 @@ module PuppetTest::Support end module PuppetTest::Support::Utils def gcdebug(type) Puppet.warning "#{type}: #{ObjectSpace.each_object(type) { |o| }}" end def basedir(*list) unless defined? @@basedir Dir.chdir(File.dirname(__FILE__)) do @@basedir = File.dirname(File.dirname(File.dirname(File.dirname(Dir.getwd)))) end end if list.empty? @@basedir else File.join(@@basedir, *list) end end def fakedata(dir,pat='*') glob = "#{basedir}/test/#{dir}/#{pat}" files = Dir.glob(glob,File::FNM_PATHNAME) raise Puppet::DevError, "No fakedata matching #{glob}" if files.empty? files end def datadir(*list) File.join(basedir, "test", "data", *list) end # # TODO: I think this method needs to be renamed to something a little more # explanatory. # def newobj(type, name, hash) transport = Puppet::TransObject.new(name, "file") transport[:path] = path transport[:ensure] = "file" assert_nothing_raised { file = transport.to_ral } end # Turn a list of resources, or possibly a catalog and some resources, # into a catalog object. def resources2catalog(*resources) if resources[0].is_a?(Puppet::Resource::Catalog) config = resources.shift resources.each { |r| config.add_resource r } unless resources.empty? elsif resources[0].is_a?(Puppet::Type.type(:component)) raise ArgumentError, "resource2config() no longer accpts components" comp = resources.shift comp.delve else config = Puppet::Resource::Catalog.new resources.each { |res| config.add_resource res } end config end # TODO: rewrite this to use the 'etc' module. # Define a variable that contains the name of my user. def setme # retrieve the user name id = %x{id}.chomp if id =~ /uid=\d+\(([^\)]+)\)/ @me = $1 else puts id end raise "Could not retrieve user name; 'id' did not work" unless defined?(@me) end # Define a variable that contains a group I'm in. def set_mygroup # retrieve the user name group = %x{groups}.chomp.split(/ /)[0] raise "Could not find group to set in @mygroup" unless group @mygroup = group end def run_events(type, trans, events, msg) case type when :evaluate, :rollback # things are hunky-dory else raise Puppet::DevError, "Incorrect run_events type" end method = type trans.send(method) - newevents = trans.events.reject { |e| e.status == 'failure' }.collect { |e| + newevents = trans.events.reject { |e| ['failure', 'audit'].include? e.status }.collect { |e| e.name } assert_equal(events, newevents, "Incorrect #{type} #{msg} events") trans end def fakefile(name) ary = [basedir, "test"] ary += name.split("/") file = File.join(ary) raise Puppet::DevError, "No fakedata file #{file}" unless FileTest.exists?(file) file end # wrap how to retrieve the masked mode def filemode(file) File.stat(file).mode & 007777 end def memory Puppet::Util.memory end # a list of files that we can parse for testing def textfiles textdir = datadir "snippets" Dir.entries(textdir).reject { |f| f =~ /^\./ or f =~ /fail/ }.each { |f| yield File.join(textdir, f) } end def failers textdir = datadir "failers" # only parse this one file now files = Dir.entries(textdir).reject { |file| file =~ %r{\.swp} }.reject { |file| file =~ %r{\.disabled} }.collect { |file| File.join(textdir,file) }.find_all { |file| FileTest.file?(file) }.sort.each { |file| Puppet.debug "Processing #{file}" yield file } end def mk_catalog(*resources) if resources[0].is_a?(String) name = resources.shift else name = :testing end config = Puppet::Resource::Catalog.new :testing do |conf| resources.each { |resource| conf.add_resource resource } end config end end diff --git a/test/ral/type/filesources.rb b/test/ral/type/filesources.rb index dd73cea27..242a82e83 100755 --- a/test/ral/type/filesources.rb +++ b/test/ral/type/filesources.rb @@ -1,510 +1,507 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../lib/puppettest' require 'puppettest' require 'puppettest/support/utils' require 'cgi' require 'fileutils' require 'mocha' class TestFileSources < Test::Unit::TestCase include PuppetTest::Support::Utils include PuppetTest::FileTesting def setup super if defined?(@port) @port += 1 else @port = 12345 end @file = Puppet::Type.type(:file) Puppet[:filetimeout] = -1 Puppet::Util::SUIDManager.stubs(:asuser).yields Facter.stubs(:to_hash).returns({}) end def teardown super Puppet::Network::HttpPool.clear_http_instances end def use_storage initstorage rescue system("rm -rf #{Puppet[:statefile]}") end def initstorage Puppet::Util::Storage.init Puppet::Util::Storage.load end # Make a simple recursive tree. def mk_sourcetree source = tempfile sourcefile = File.join(source, "file") Dir.mkdir source File.open(sourcefile, "w") { |f| f.puts "yay" } dest = tempfile destfile = File.join(dest, "file") return source, dest, sourcefile, destfile end def recursive_source_test(fromdir, todir) initstorage tofile = nil trans = nil tofile = Puppet::Type.type(:file).new( :path => todir, :recurse => true, :backup => false, :source => fromdir ) catalog = mk_catalog(tofile) catalog.apply assert(FileTest.exists?(todir), "Created dir #{todir} does not exist") end def run_complex_sources(networked = false) path = tempfile # first create the source directory FileUtils.mkdir_p path # okay, let's create a directory structure fromdir = File.join(path,"fromdir") Dir.mkdir(fromdir) FileUtils.cd(fromdir) { File.open("one", "w") { |f| f.puts "onefile"} File.open("two", "w") { |f| f.puts "twofile"} } todir = File.join(path, "todir") source = fromdir source = "puppet://localhost/#{networked}#{fromdir}" if networked recursive_source_test(source, todir) [fromdir,todir, File.join(todir, "one"), File.join(todir, "two")] end def test_complex_sources_twice fromdir, todir, one, two = run_complex_sources assert_trees_equal(fromdir,todir) recursive_source_test(fromdir, todir) assert_trees_equal(fromdir,todir) # Now remove the whole tree and try it again. [one, two].each do |f| File.unlink(f) end Dir.rmdir(todir) recursive_source_test(fromdir, todir) assert_trees_equal(fromdir,todir) end def test_sources_with_deleted_destfiles fromdir, todir, one, two = run_complex_sources assert(FileTest.exists?(todir)) # then delete a file File.unlink(two) # and run recursive_source_test(fromdir, todir) assert(FileTest.exists?(two), "Deleted file was not recopied") # and make sure they're still equal assert_trees_equal(fromdir,todir) end def test_sources_with_readonly_destfiles fromdir, todir, one, two = run_complex_sources assert(FileTest.exists?(todir)) File.chmod(0600, one) recursive_source_test(fromdir, todir) # and make sure they're still equal assert_trees_equal(fromdir,todir) # Now try it with the directory being read-only File.chmod(0111, todir) recursive_source_test(fromdir, todir) # and make sure they're still equal assert_trees_equal(fromdir,todir) end def test_sources_with_modified_dest_files fromdir, todir, one, two = run_complex_sources assert(FileTest.exists?(todir)) # Modify a dest file File.open(two, "w") { |f| f.puts "something else" } recursive_source_test(fromdir, todir) # and make sure they're still equal assert_trees_equal(fromdir,todir) end def test_sources_with_added_destfiles fromdir, todir = run_complex_sources assert(FileTest.exists?(todir)) # and finally, add some new files add_random_files(todir) recursive_source_test(fromdir, todir) fromtree = file_list(fromdir) totree = file_list(todir) assert(fromtree != totree, "Trees are incorrectly equal") # then remove our new files FileUtils.cd(todir) { %x{find . 2>/dev/null}.chomp.split(/\n/).each { |file| if file =~ /file[0-9]+/ FileUtils.rm_rf(file) end } } # and make sure they're still equal assert_trees_equal(fromdir,todir) end # Make sure added files get correctly caught during recursion def test_RecursionWithAddedFiles basedir = tempfile Dir.mkdir(basedir) @@tmpfiles << basedir file1 = File.join(basedir, "file1") file2 = File.join(basedir, "file2") subdir1 = File.join(basedir, "subdir1") file3 = File.join(subdir1, "file") File.open(file1, "w") { |f| f.puts "yay" } rootobj = nil assert_nothing_raised { rootobj = Puppet::Type.type(:file).new( :name => basedir, :recurse => true, :check => %w{type owner}, :mode => 0755 ) } assert_apply(rootobj) assert_equal(0755, filemode(file1)) File.open(file2, "w") { |f| f.puts "rah" } assert_apply(rootobj) assert_equal(0755, filemode(file2)) Dir.mkdir(subdir1) File.open(file3, "w") { |f| f.puts "foo" } assert_apply(rootobj) assert_equal(0755, filemode(file3)) end def mkfileserverconf(mounts) file = tempfile File.open(file, "w") { |f| mounts.each { |path, name| f.puts "[#{name}]\n\tpath #{path}\n\tallow *\n" } } @@tmpfiles << file file end def test_unmountedNetworkSources server = nil mounts = { "/" => "root", "/noexistokay" => "noexist" } fileserverconf = mkfileserverconf(mounts) Puppet[:autosign] = true Puppet[:masterport] = @port Puppet[:certdnsnames] = "localhost" serverpid = nil assert_nothing_raised("Could not start on port #{@port}") { server = Puppet::Network::HTTPServer::WEBrick.new( :Port => @port, :Handlers => { :CA => {}, # so that certs autogenerate :FileServer => { :Config => fileserverconf } } ) } serverpid = fork { assert_nothing_raised { #trap(:INT) { server.shutdown; Kernel.exit! } trap(:INT) { server.shutdown } server.start } } @@tmppids << serverpid sleep(1) name = File.join(tmpdir, "nosourcefile") file = Puppet::Type.type(:file).new( :source => "puppet://localhost/noexist/file", :name => name ) assert_raise Puppet::Error do file.retrieve end comp = mk_catalog(file) comp.apply assert(!FileTest.exists?(name), "File with no source exists anyway") end def test_sourcepaths files = [] 3.times { files << tempfile } to = tempfile File.open(files[-1], "w") { |f| f.puts "yee-haw" } file = nil assert_nothing_raised { file = Puppet::Type.type(:file).new( :name => to, :source => files ) } comp = mk_catalog(file) assert_events([:file_created], comp) assert(File.exists?(to), "File does not exist") txt = nil File.open(to) { |f| txt = f.read.chomp } assert_equal("yee-haw", txt, "Contents do not match") end # Make sure that source-copying updates the checksum on the same run def test_sourcebeatsensure source = tempfile dest = tempfile File.open(source, "w") { |f| f.puts "yay" } file = nil assert_nothing_raised { - - file = Puppet::Type.type(:file).new( - + file = Puppet::Type.type(:file).new( :name => dest, :ensure => "file", - :source => source ) } file.retrieve assert_events([:file_created], file) file.retrieve assert_events([], file) assert_events([], file) end def test_sourcewithlinks source = tempfile link = tempfile dest = tempfile File.open(source, "w") { |f| f.puts "yay" } File.symlink(source, link) file = Puppet::Type.type(:file).new(:name => dest, :source => link) catalog = mk_catalog(file) # Default to managing links catalog.apply assert(FileTest.symlink?(dest), "Did not create link") # Now follow the links file[:links] = :follow catalog.apply assert(FileTest.file?(dest), "Destination is not a file") end # Make sure files aren't replaced when replace is false, but otherwise # are. def test_replace dest = tempfile file = Puppet::Type.newfile( :path => dest, :content => "foobar", :recurse => true ) assert_apply(file) File.open(dest, "w") { |f| f.puts "yayness" } file[:replace] = false assert_apply(file) # Make sure it doesn't change. assert_equal("yayness\n", File.read(dest), "File got replaced when :replace was false") file[:replace] = true assert_apply(file) # Make sure it changes. assert_equal("foobar", File.read(dest), "File was not replaced when :replace was true") end def test_sourceselect dest = tempfile sources = [] 2.times { |i| i = i + 1 source = tempfile sources << source file = File.join(source, "file#{i}") Dir.mkdir(source) File.open(file, "w") { |f| f.print "yay" } } file1 = File.join(dest, "file1") file2 = File.join(dest, "file2") file3 = File.join(dest, "file3") # Now make different files with the same name in each source dir sources.each_with_index do |source, i| File.open(File.join(source, "file3"), "w") { |f| f.print i.to_s } end obj = Puppet::Type.newfile( :path => dest, :recurse => true, :source => sources) assert_equal(:first, obj[:sourceselect], "sourceselect has the wrong default") # First, make sure we default to just copying file1 assert_apply(obj) assert(FileTest.exists?(file1), "File from source 1 was not copied") assert(! FileTest.exists?(file2), "File from source 2 was copied") assert(FileTest.exists?(file3), "File from source 1 was not copied") assert_equal("0", File.read(file3), "file3 got wrong contents") # Now reset sourceselect assert_nothing_raised do obj[:sourceselect] = :all end File.unlink(file1) File.unlink(file3) Puppet.err :yay assert_apply(obj) assert(FileTest.exists?(file1), "File from source 1 was not copied") assert(FileTest.exists?(file2), "File from source 2 was copied") assert(FileTest.exists?(file3), "File from source 1 was not copied") assert_equal("0", File.read(file3), "file3 got wrong contents") end def test_recursive_sourceselect dest = tempfile source1 = tempfile source2 = tempfile files = [] [source1, source2, File.join(source1, "subdir"), File.join(source2, "subdir")].each_with_index do |dir, i| Dir.mkdir(dir) # Make a single file in each directory file = File.join(dir, "file#{i}") File.open(file, "w") { |f| f.puts "yay#{i}"} # Now make a second one in each directory file = File.join(dir, "second-file#{i}") File.open(file, "w") { |f| f.puts "yaysecond-#{i}"} files << file end obj = Puppet::Type.newfile(:path => dest, :source => [source1, source2], :sourceselect => :all, :recurse => true) assert_apply(obj) ["file0", "file1", "second-file0", "second-file1", "subdir/file2", "subdir/second-file2", "subdir/file3", "subdir/second-file3"].each do |file| path = File.join(dest, file) assert(FileTest.exists?(path), "did not create #{file}") assert_equal("yay#{File.basename(file).sub("file", '')}\n", File.read(path), "file was not copied correctly") end end # #594 def test_purging_missing_remote_files source = tempfile dest = tempfile s1 = File.join(source, "file1") s2 = File.join(source, "file2") d1 = File.join(dest, "file1") d2 = File.join(dest, "file2") Dir.mkdir(source) [s1, s2].each { |name| File.open(name, "w") { |file| file.puts "something" } } # We have to add a second parameter, because that's the only way to expose the "bug". file = Puppet::Type.newfile(:path => dest, :source => source, :recurse => true, :purge => true, :mode => "755") assert_apply(file) assert(FileTest.exists?(d1), "File1 was not copied") assert(FileTest.exists?(d2), "File2 was not copied") File.unlink(s2) assert_apply(file) assert(FileTest.exists?(d1), "File1 was not kept") assert(! FileTest.exists?(d2), "File2 was not purged") end end