diff --git a/lib/puppet/transaction.rb b/lib/puppet/transaction.rb index aa650eea1..48154ad6f 100644 --- a/lib/puppet/transaction.rb +++ b/lib/puppet/transaction.rb @@ -1,334 +1,334 @@ # the class that actually walks our resource/property tree, collects the changes, # and performs them require 'puppet' require 'puppet/util/tagging' require 'puppet/application' class Puppet::Transaction require 'puppet/transaction/event' require 'puppet/transaction/event_manager' require 'puppet/transaction/resource_harness' require 'puppet/resource/status' attr_accessor :component, :catalog, :ignoreschedules attr_accessor :sorted_resources, :configurator # The report, once generated. attr_accessor :report # Routes and stores any events and subscriptions. attr_reader :event_manager # Handles most of the actual interacting with resources attr_reader :resource_harness include Puppet::Util include Puppet::Util::Tagging # Wraps application run state check to flag need to interrupt processing def stop_processing? Puppet::Application.stop_requested? end # Add some additional times for reporting def add_times(hash) hash.each do |name, num| report.add_times(name, num) end end # Are there any failed resources in this transaction? def any_failed? report.resource_statuses.values.detect { |status| status.failed? } end # Apply all changes for a resource def apply(resource, ancestor = nil) status = resource_harness.evaluate(resource) add_resource_status(status) - event_manager.queue_events(ancestor || resource, status.events) + event_manager.queue_events(ancestor || resource, status.events) unless status.failed? rescue => detail resource.err "Could not evaluate: #{detail}" end # Find all of the changed resources. def changed? report.resource_statuses.values.find_all { |status| status.changed }.collect { |status| catalog.resource(status.resource) } end # Copy an important relationships from the parent to the newly-generated # child resource. def make_parent_child_relationship(resource, children) depthfirst = resource.depthfirst? children.each do |gen_child| if depthfirst edge = [gen_child, resource] else edge = [resource, gen_child] end relationship_graph.add_vertex(gen_child) unless relationship_graph.edge?(edge[1], edge[0]) relationship_graph.add_edge(*edge) else resource.debug "Skipping automatic relationship to #{gen_child}" end end end # See if the resource generates new resources at evaluation time. def eval_generate(resource) generate_additional_resources(resource, :eval_generate) end # Evaluate a single resource. def eval_resource(resource, ancestor = nil) if skip?(resource) resource_status(resource).skipped = true else eval_children_and_apply_resource(resource, ancestor) end # Check to see if there are any events queued for this resource event_manager.process_events(resource) end def eval_children_and_apply_resource(resource, ancestor = nil) resource_status(resource).scheduled = true # We need to generate first regardless, because the recursive # actions sometimes change how the top resource is applied. children = eval_generate(resource) if ! children.empty? and resource.depthfirst? children.each do |child| # The child will never be skipped when the parent isn't eval_resource(child, ancestor || resource) end end # Perform the actual changes apply(resource, ancestor) if ! children.empty? and ! resource.depthfirst? children.each do |child| eval_resource(child, ancestor || resource) end end end # This method does all the actual work of running a transaction. It # collects all of the changes, executes them, and responds to any # necessary events. def evaluate # Start logging. Puppet::Util::Log.newdestination(@report) prepare Puppet.info "Applying configuration version '#{catalog.version}'" if catalog.version begin @sorted_resources.each do |resource| next if stop_processing? if resource.is_a?(Puppet::Type::Component) Puppet.warning "Somehow left a component in the relationship graph" next end ret = nil seconds = thinmark do ret = eval_resource(resource) end resource.info "Evaluated in %0.2f seconds" % seconds if Puppet[:evaltrace] and @catalog.host_config? ret end ensure # And then close the transaction log. Puppet::Util::Log.close(@report) end Puppet.debug "Finishing transaction #{object_id}" end def events event_manager.events end def failed?(resource) s = resource_status(resource) and s.failed? end # Does this resource have any failed dependencies? def failed_dependencies?(resource) # First make sure there are no failed dependencies. To do this, # we check for failures in any of the vertexes above us. It's not # enough to check the immediate dependencies, which is why we use # a tree from the reversed graph. found_failed = false relationship_graph.dependencies(resource).each do |dep| next unless failed?(dep) resource.notice "Dependency #{dep} has failures: #{resource_status(dep).failed}" found_failed = true end found_failed end # A general method for recursively generating new resources from a # resource. def generate_additional_resources(resource, method) return [] unless resource.respond_to?(method) begin made = resource.send(method) rescue => detail puts detail.backtrace if Puppet[:trace] resource.err "Failed to generate additional resources using '#{method}': #{detail}" end return [] unless made made = [made] unless made.is_a?(Array) made.uniq.find_all do |res| begin res.tag(*resource.tags) @catalog.add_resource(res) do |r| r.finish make_parent_child_relationship(resource, [r]) # Call 'generate' recursively generate_additional_resources(r, method) end true rescue Puppet::Resource::Catalog::DuplicateResourceError res.info "Duplicate generated resource; skipping" false end end end # Collect any dynamically generated resources. This method is called # before the transaction starts. def generate list = @catalog.vertices newlist = [] while ! list.empty? list.each do |resource| newlist += generate_additional_resources(resource, :generate) end list = newlist newlist = [] end end # Should we ignore tags? def ignore_tags? ! (@catalog.host_config? or Puppet[:name] == "puppet") end # this should only be called by a Puppet::Type::Component resource now # and it should only receive an array def initialize(catalog) @catalog = catalog @report = Report.new("apply", catalog.version) @event_manager = Puppet::Transaction::EventManager.new(self) @resource_harness = Puppet::Transaction::ResourceHarness.new(self) end # Prefetch any providers that support it. We don't support prefetching # types, just providers. def prefetch prefetchers = {} @catalog.vertices.each do |resource| if provider = resource.provider and provider.class.respond_to?(:prefetch) prefetchers[provider.class] ||= {} prefetchers[provider.class][resource.name] = resource end end # Now call prefetch, passing in the resources so that the provider instances can be replaced. prefetchers.each do |provider, resources| Puppet.debug "Prefetching #{provider.name} resources for #{provider.resource_type.name}" begin provider.prefetch(resources) rescue => detail puts detail.backtrace if Puppet[:trace] Puppet.err "Could not prefetch #{provider.resource_type.name} provider '#{provider.name}': #{detail}" end end end # Prepare to evaluate the resources in a transaction. def prepare # Now add any dynamically generated resources generate # Then prefetch. It's important that we generate and then prefetch, # so that any generated resources also get prefetched. prefetch # This will throw an error if there are cycles in the graph. @sorted_resources = relationship_graph.topsort end def relationship_graph catalog.relationship_graph end def add_resource_status(status) report.add_resource_status status end def resource_status(resource) report.resource_statuses[resource.to_s] || add_resource_status(Puppet::Resource::Status.new(resource)) end # Is the resource currently scheduled? def scheduled?(resource) self.ignoreschedules or resource_harness.scheduled?(resource_status(resource), resource) end # Should this resource be skipped? def skip?(resource) if missing_tags?(resource) resource.debug "Not tagged with #{tags.join(", ")}" elsif ! scheduled?(resource) resource.debug "Not scheduled" elsif failed_dependencies?(resource) resource.warning "Skipping because of failed dependencies" elsif resource.virtual? resource.debug "Skipping because virtual" else return false end true end # The tags we should be checking. def tags self.tags = Puppet[:tags] unless defined?(@tags) super end def handle_qualified_tags( qualified ) # The default behavior of Puppet::Util::Tagging is # to split qualified tags into parts. That would cause # qualified tags to match too broadly here. return end # Is this resource tagged appropriately? def missing_tags?(resource) return false if ignore_tags? return false if tags.empty? not resource.tagged?(*tags) end end require 'puppet/transaction/report' diff --git a/spec/integration/transaction_spec.rb b/spec/integration/transaction_spec.rb index 2c12b3d5f..66a049efe 100755 --- a/spec/integration/transaction_spec.rb +++ b/spec/integration/transaction_spec.rb @@ -1,278 +1,285 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../spec_helper' require 'puppet_spec/files' require 'puppet/transaction' require 'puppet_spec/files' describe Puppet::Transaction do include PuppetSpec::Files before do Puppet::Util::Storage.stubs(:store) end def mk_catalog(*resources) catalog = Puppet::Resource::Catalog.new(Puppet::Node.new("mynode")) resources.each { |res| catalog.add_resource res } catalog end it "should not apply generated resources if the parent resource fails" do catalog = Puppet::Resource::Catalog.new resource = Puppet::Type.type(:file).new :path => "/foo/bar", :backup => false catalog.add_resource resource child_resource = Puppet::Type.type(:file).new :path => "/foo/bar/baz", :backup => false resource.expects(:eval_generate).returns([child_resource]) transaction = Puppet::Transaction.new(catalog) resource.expects(:retrieve).raises "this is a failure" resource.stubs(:err) child_resource.expects(:retrieve).never transaction.evaluate end it "should not apply virtual resources" do catalog = Puppet::Resource::Catalog.new resource = Puppet::Type.type(:file).new :path => "/foo/bar", :backup => false resource.virtual = true catalog.add_resource resource transaction = Puppet::Transaction.new(catalog) resource.expects(:evaluate).never transaction.evaluate end it "should apply exported resources" do catalog = Puppet::Resource::Catalog.new path = tmpfile("exported_files") resource = Puppet::Type.type(:file).new :path => path, :backup => false, :ensure => :file resource.exported = true catalog.add_resource resource catalog.apply FileTest.should be_exist(path) end it "should not apply virtual exported resources" do catalog = Puppet::Resource::Catalog.new resource = Puppet::Type.type(:file).new :path => "/foo/bar", :backup => false resource.exported = true resource.virtual = true catalog.add_resource resource transaction = Puppet::Transaction.new(catalog) resource.expects(:evaluate).never transaction.evaluate end # Verify that one component requiring another causes the contained # resources in the requiring component to get refreshed. it "should propagate events from a contained resource through its container to its dependent container's contained resources" do transaction = nil file = Puppet::Type.type(:file).new :path => tmpfile("event_propagation"), :ensure => :present execfile = File.join(tmpdir("exec_event"), "exectestingness2") exec = Puppet::Type.type(:exec).new :command => "touch #{execfile}", :path => ENV['PATH'] catalog = mk_catalog(file) fcomp = Puppet::Type.type(:component).new(:name => "Foo[file]") catalog.add_resource fcomp catalog.add_edge(fcomp, file) ecomp = Puppet::Type.type(:component).new(:name => "Foo[exec]") catalog.add_resource ecomp catalog.add_resource exec catalog.add_edge(ecomp, exec) ecomp[:subscribe] = Puppet::Resource.new(:foo, "file") exec[:refreshonly] = true exec.expects(:refresh) catalog.apply end # Make sure that multiple subscriptions get triggered. it "should propagate events to all dependent resources" do path = tmpfile("path") file1 = tmpfile("file1") file2 = tmpfile("file2") file = Puppet::Type.type(:file).new( :path => path, :ensure => "file" ) exec1 = Puppet::Type.type(:exec).new( :path => ENV["PATH"], :command => "touch #{file1}", :refreshonly => true, :subscribe => Puppet::Resource.new(:file, path) ) exec2 = Puppet::Type.type(:exec).new( :path => ENV["PATH"], :command => "touch #{file2}", :refreshonly => true, :subscribe => Puppet::Resource.new(:file, path) ) catalog = mk_catalog(file, exec1, exec2) catalog.apply FileTest.should be_exist(file1) FileTest.should be_exist(file2) end it "should not let one failed refresh result in other refreshes failing" do path = tmpfile("path") newfile = tmpfile("file") - - file = Puppet::Type.type(:file).new( - + file = Puppet::Type.type(:file).new( :path => path, - :ensure => "file" ) - exec1 = Puppet::Type.type(:exec).new( - + exec1 = Puppet::Type.type(:exec).new( :path => ENV["PATH"], :command => "touch /this/cannot/possibly/exist", :logoutput => true, :refreshonly => true, :subscribe => file, - :title => "one" ) - exec2 = Puppet::Type.type(:exec).new( - + exec2 = Puppet::Type.type(:exec).new( :path => ENV["PATH"], :command => "touch #{newfile}", :logoutput => true, :refreshonly => true, :subscribe => [file, exec1], - :title => "two" ) exec1.stubs(:err) catalog = mk_catalog(file, exec1, exec2) catalog.apply FileTest.should be_exists(newfile) end it "should still trigger skipped resources" do catalog = mk_catalog catalog.add_resource(*Puppet::Type.type(:schedule).mkdefaultschedules) Puppet[:ignoreschedules] = false - file = Puppet::Type.type(:file).new( - + file = Puppet::Type.type(:file).new( :name => tmpfile("file"), - :ensure => "file", :backup => false ) fname = tmpfile("exec") - exec = Puppet::Type.type(:exec).new( - + exec = Puppet::Type.type(:exec).new( :name => "touch #{fname}", :path => "/usr/bin:/bin", :schedule => "monthly", - :subscribe => Puppet::Resource.new("file", file.name) ) catalog.add_resource(file, exec) # Run it once catalog.apply FileTest.should be_exists(fname) # Now remove it, so it can get created again File.unlink(fname) file[:content] = "some content" catalog.apply FileTest.should be_exists(fname) # Now remove it, so it can get created again File.unlink(fname) # And tag our exec exec.tag("testrun") # And our file, so it runs file.tag("norun") Puppet[:tags] = "norun" file[:content] = "totally different content" catalog.apply FileTest.should be_exists(fname) end it "should not attempt to evaluate resources with failed dependencies" do - exec = Puppet::Type.type(:exec).new( - + exec = Puppet::Type.type(:exec).new( :command => "/bin/mkdir /this/path/cannot/possibly/exit", - :title => "mkdir" ) - - file1 = Puppet::Type.type(:file).new( - + file1 = Puppet::Type.type(:file).new( :title => "file1", :path => tmpfile("file1"), - :require => exec, :ensure => :file ) - - file2 = Puppet::Type.type(:file).new( - + file2 = Puppet::Type.type(:file).new( :title => "file2", :path => tmpfile("file2"), - :require => file1, :ensure => :file ) catalog = mk_catalog(exec, file1, file2) catalog.apply FileTest.should_not be_exists(file1[:path]) FileTest.should_not be_exists(file2[:path]) end + it "should not trigger subscribing resources on failure" do + file1 = tmpfile("file1") + file2 = tmpfile("file2") + + create_file1 = Puppet::Type.type(:exec).new( + :command => "/usr/bin/touch #{file1}" + ) + + exec = Puppet::Type.type(:exec).new( + :command => "/bin/mkdir /this/path/cannot/possibly/exit", + :title => "mkdir", + :notify => create_file1 + ) + + create_file2 = Puppet::Type.type(:exec).new( + :command => "/usr/bin/touch #{file2}", + :subscribe => exec + ) + + catalog = mk_catalog(exec, create_file1, create_file2) + catalog.apply + + FileTest.should_not be_exists(file1) + FileTest.should_not be_exists(file2) + end + # #801 -- resources only checked in noop should be rescheduled immediately. it "should immediately reschedule noop resources" do Puppet::Type.type(:schedule).mkdefaultschedules resource = Puppet::Type.type(:notify).new(:name => "mymessage", :noop => true) catalog = Puppet::Resource::Catalog.new catalog.add_resource resource trans = catalog.apply trans.resource_harness.should be_scheduled(trans.resource_status(resource), resource) end end