diff --git a/lib/puppet/transaction.rb b/lib/puppet/transaction.rb index e4f01a421..be18e56e1 100644 --- a/lib/puppet/transaction.rb +++ b/lib/puppet/transaction.rb @@ -1,363 +1,363 @@ require 'puppet' require 'puppet/util/tagging' require 'puppet/application' require 'digest/sha1' require 'set' # the class that actually walks our resource/property tree, collects the changes, # and performs them # # @api private class Puppet::Transaction require 'puppet/transaction/additional_resource_generator' require 'puppet/transaction/event' require 'puppet/transaction/event_manager' require 'puppet/transaction/resource_harness' require 'puppet/resource/status' attr_accessor :catalog, :ignoreschedules, :for_network_device # The report, once generated. attr_reader :report # Routes and stores any events and subscriptions. attr_reader :event_manager # Handles most of the actual interacting with resources attr_reader :resource_harness attr_reader :prefetched_providers include Puppet::Util include Puppet::Util::Tagging def initialize(catalog, report, prioritizer) @catalog = catalog @report = report || Puppet::Transaction::Report.new("apply", catalog.version, catalog.environment) @prioritizer = prioritizer @report.add_times(:config_retrieval, @catalog.retrieval_duration || 0) @event_manager = Puppet::Transaction::EventManager.new(self) @resource_harness = Puppet::Transaction::ResourceHarness.new(self) @prefetched_providers = Hash.new { |h,k| h[k] = {} } end # Invoke the pre_run_check hook in every resource in the catalog. # This should (only) be called by Transaction#evaluate before applying # the catalog. # # @see Puppet::Transaction#evaluate # @see Puppet::Type#pre_run_check # @raise [Puppet::Error] If any pre-run checks failed. # @return [void] def perform_pre_run_checks prerun_errors = {} @catalog.vertices.each do |res| begin res.pre_run_check rescue Puppet::Error => detail prerun_errors[res] = detail end end unless prerun_errors.empty? prerun_errors.each do |res, detail| res.log_exception(detail) end raise Puppet::Error, "Some pre-run checks failed" 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(&block) block ||= method(:eval_resource) generator = AdditionalResourceGenerator.new(@catalog, relationship_graph, @prioritizer) @catalog.vertices.each { |resource| generator.generate_additional_resources(resource) } perform_pre_run_checks Puppet.info "Applying configuration version '#{catalog.version}'" if catalog.version continue_while = lambda { !stop_processing? } post_evalable_providers = Set.new pre_process = lambda do |resource| prov_class = resource.provider.class post_evalable_providers << prov_class if prov_class.respond_to?(:post_resource_eval) prefetch_if_necessary(resource) # If we generated resources, we don't know what they are now # blocking, so we opt to recompute it, rather than try to track every # change that would affect the number. relationship_graph.clear_blockers if generator.eval_generate(resource) end providerless_types = [] overly_deferred_resource_handler = lambda do |resource| # We don't automatically assign unsuitable providers, so if there # is one, it must have been selected by the user. return if missing_tags?(resource) if resource.provider resource.err "Provider #{resource.provider.class.name} is not functional on this host" else providerless_types << resource.type end resource_status(resource).failed = true end canceled_resource_handler = lambda do |resource| resource_status(resource).skipped = true resource.debug "Transaction canceled, skipping" end teardown = lambda do # Just once per type. No need to punish the user. providerless_types.uniq.each do |type| Puppet.err "Could not find a suitable provider for #{type}" end post_evalable_providers.each do |provider| begin provider.post_resource_eval rescue => detail Puppet.log_exception(detail, "post_resource_eval failed for provider #{provider}") end end end relationship_graph.traverse(:while => continue_while, :pre_process => pre_process, :overly_deferred_resource_handler => overly_deferred_resource_handler, :canceled_resource_handler => canceled_resource_handler, :teardown => teardown) do |resource| if resource.is_a?(Puppet::Type::Component) Puppet.warning "Somehow left a component in the relationship graph" else resource.info "Starting to evaluate the resource" if Puppet[:evaltrace] and @catalog.host_config? seconds = thinmark { block.call(resource) } resource.info "Evaluated in %0.2f seconds" % seconds if Puppet[:evaltrace] and @catalog.host_config? end end Puppet.debug "Finishing transaction #{object_id}" end # Wraps application run state check to flag need to interrupt processing def stop_processing? Puppet::Application.stop_requested? && catalog.host_config? end # Are there any failed resources in this transaction? def any_failed? report.resource_statuses.values.detect { |status| status.failed? } 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 def relationship_graph catalog.relationship_graph(@prioritizer) end def resource_status(resource) report.resource_statuses[resource.to_s] || add_resource_status(Puppet::Resource::Status.new(resource)) end # The tags we should be checking. def tags self.tags = Puppet[:tags] unless defined?(@tags) super end def prefetch_if_necessary(resource) provider_class = resource.provider.class return unless provider_class.respond_to?(:prefetch) and !prefetched_providers[resource.type][provider_class.name] resources = resources_by_provider(resource.type, provider_class.name) if provider_class == resource.class.defaultprovider providerless_resources = resources_by_provider(resource.type, nil) providerless_resources.values.each {|res| res.provider = provider_class.name} resources.merge! providerless_resources end prefetch(provider_class, resources) end private # 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) unless status.failed? rescue => detail resource.err "Could not evaluate: #{detail}" end # Evaluate a single resource. def eval_resource(resource, ancestor = nil) if skip?(resource) resource_status(resource).skipped = true - resource.info("Resource is being skipped, unscheduling all events") - event_manager.dequeue_all_events_for_resource(resource) else resource_status(resource).scheduled = true apply(resource, ancestor) - event_manager.process_events(resource) end + + # Check to see if there are any events queued for this resource + event_manager.process_events(resource) 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 # When we introduced the :whit into the graph, to reduce the combinatorial # explosion of edges, we also ended up reporting failures for containers # like class and stage. This is undesirable; while just skipping the # output isn't perfect, it is RC-safe. --daniel 2011-06-07 suppress_report = (resource.class == Puppet::Type.type(:whit)) relationship_graph.dependencies(resource).each do |dep| next unless failed?(dep) found_failed = true # See above. --daniel 2011-06-06 unless suppress_report then resource.notice "Dependency #{dep} has failures: #{resource_status(dep).failed}" end end found_failed end # A general method for recursively generating new resources from a # resource. def generate_additional_resources(resource) return unless resource.respond_to?(:generate) begin made = resource.generate rescue => detail resource.log_exception(detail, "Failed to generate additional resources using 'generate': #{detail}") end return unless made made = [made] unless made.is_a?(Array) made.uniq.each do |res| begin res.tag(*resource.tags) @catalog.add_resource(res) res.finish add_conditional_directed_dependency(resource, res) generate_additional_resources(res) rescue Puppet::Resource::Catalog::DuplicateResourceError res.info "Duplicate generated resource; skipping" end end end # Should we ignore tags? def ignore_tags? ! @catalog.host_config? end def resources_by_provider(type_name, provider_name) unless @resources_by_provider @resources_by_provider = Hash.new { |h, k| h[k] = Hash.new { |h, k| h[k] = {} } } @catalog.vertices.each do |resource| if resource.class.attrclass(:provider) prov = resource.provider && resource.provider.class.name @resources_by_provider[resource.type][prov][resource.name] = resource end end end @resources_by_provider[type_name][provider_name] || {} end # Prefetch any providers that support it, yo. We don't support prefetching # types, just providers. def prefetch(provider_class, resources) type_name = provider_class.resource_type.name return if @prefetched_providers[type_name][provider_class.name] Puppet.debug "Prefetching #{provider_class.name} resources for #{type_name}" begin provider_class.prefetch(resources) rescue => detail Puppet.log_exception(detail, "Could not prefetch #{type_name} provider '#{provider_class.name}': #{detail}") end @prefetched_providers[type_name][provider_class.name] = true end def add_resource_status(status) report.add_resource_status(status) end # Is the resource currently scheduled? def scheduled?(resource) self.ignoreschedules or resource_harness.scheduled?(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) # When we introduced the :whit into the graph, to reduce the combinatorial # explosion of edges, we also ended up reporting failures for containers # like class and stage. This is undesirable; while just skipping the # output isn't perfect, it is RC-safe. --daniel 2011-06-07 unless resource.class == Puppet::Type.type(:whit) then resource.warning "Skipping because of failed dependencies" end elsif resource.virtual? resource.debug "Skipping because virtual" elsif !host_and_device_resource?(resource) && resource.appliable_to_host? && for_network_device resource.debug "Skipping host resources because running on a device" elsif !host_and_device_resource?(resource) && resource.appliable_to_device? && !for_network_device resource.debug "Skipping device resources because running on a posix host" else return false end true end def host_and_device_resource?(resource) resource.appliable_to_host? && resource.appliable_to_device? 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/lib/puppet/transaction/event_manager.rb b/lib/puppet/transaction/event_manager.rb index 4bfbb1319..4a630baae 100644 --- a/lib/puppet/transaction/event_manager.rb +++ b/lib/puppet/transaction/event_manager.rb @@ -1,167 +1,159 @@ require 'puppet/transaction' # This class stores, routes, and responds to events generated while evaluating # a transaction. # # @api private class Puppet::Transaction::EventManager # @!attribute [r] transaction # @return [Puppet::Transaction] The transaction associated with this event manager. attr_reader :transaction # @!attribute [r] events # @todo Determine if this instance variable is used for anything aside from testing. # @return [Array] A list of events that can be # handled by the target resouce. Events that cannot be handled by the # target resource will be discarded. attr_reader :events def initialize(transaction) @transaction = transaction @event_queues = {} @events = [] end def relationship_graph transaction.relationship_graph end # Respond to any queued events for this resource. def process_events(resource) restarted = false queued_events(resource) do |callback, events| r = process_callback(resource, callback, events) restarted ||= r end if restarted queue_events(resource, [resource.event(:name => :restarted, :status => "success")]) transaction.resource_status(resource).restarted = true end end # Queues events for other resources to respond to. All of these events have # to be from the same resource. # # @param resource [Puppet::Type] The resource generating the given events # @param events [Array] All events generated by this resource # @return [void] def queue_events(resource, events) #@events += events # Do some basic normalization so we're not doing so many # graph queries for large sets of events. events.inject({}) do |collection, event| collection[event.name] ||= [] collection[event.name] << event collection end.collect do |name, list| # It doesn't matter which event we use - they all have the same source # and name here. event = list[0] # Collect the targets of any subscriptions to those events. We pass # the parent resource in so it will override the source in the events, # since eval_generated children can't have direct relationships. received = (event.name != :restarted) relationship_graph.matching_edges(event, resource).each do |edge| received ||= true unless edge.target.is_a?(Puppet::Type.type(:whit)) next unless method = edge.callback next unless edge.target.respond_to?(method) queue_events_for_resource(resource, edge.target, method, list) end @events << event if received queue_events_for_resource(resource, resource, :refresh, [event]) if resource.self_refresh? and ! resource.deleting? end dequeue_events_for_resource(resource, :refresh) if events.detect { |e| e.invalidate_refreshes } end - def dequeue_all_events_for_resource(target) - callbacks = @event_queues[target] - if callbacks && !callbacks.empty? - target.info "Unscheduling all events on #{target}" - @event_queues[target] = {} - end - end - def dequeue_events_for_resource(target, callback) target.info "Unscheduling #{callback} on #{target}" - @event_queues[target][callback] = [] if @event_queues[target] + @event_queues[target][callback] = {} if @event_queues[target] end def queue_events_for_resource(source, target, callback, events) whit = Puppet::Type.type(:whit) # The message that a resource is refreshing the completed-whit for its own class # is extremely counter-intuitive. Basically everything else is easy to understand, # if you suppress the whit-lookingness of the whit resources refreshing_c_whit = target.is_a?(whit) && target.name =~ /^completed_/ if refreshing_c_whit source.debug "The container #{target} will propagate my #{callback} event" else source.info "Scheduling #{callback} of #{target}" end @event_queues[target] ||= {} @event_queues[target][callback] ||= [] @event_queues[target][callback].concat(events) end def queued_events(resource) return unless callbacks = @event_queues[resource] callbacks.each do |callback, events| yield callback, events unless events.empty? end end private # Should the callback for this resource be invoked? # @param resource [Puppet::Type] The resource to be refreshed # @param events [Array] A list of events # associated with this callback and resource. # @return [true, false] Whether the callback should be run. def process_callback?(resource, events) !(events.all? { |e| e.status == "noop" } || resource.noop?) end # Processes callbacks for a given resource. # # @param resource [Puppet::Type] The resource receiving the callback. # @param callback [Symbol] The name of the callback method that will be invoked. # @param events [Array] A list of events # associated with this callback and resource. # @return [true, false] Whether the callback was successfully run. def process_callback(resource, callback, events) if !process_callback?(resource, events) process_noop_events(resource, callback, events) return false end resource.send(callback) if not resource.is_a?(Puppet::Type.type(:whit)) resource.notice "Triggered '#{callback}' from #{events.length} events" end return true rescue => detail resource.err "Failed to call #{callback}: #{detail}" transaction.resource_status(resource).failed_to_restart = true resource.log_exception(detail) return false end def process_noop_events(resource, callback, events) resource.notice "Would have triggered '#{callback}' from #{events.length} events" # And then add an event for it. queue_events(resource, [resource.event(:status => "noop", :name => :noop_restart)]) end end diff --git a/spec/integration/transaction_spec.rb b/spec/integration/transaction_spec.rb index 7ae28411a..4106efc76 100755 --- a/spec/integration/transaction_spec.rb +++ b/spec/integration/transaction_spec.rb @@ -1,398 +1,382 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/transaction' 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 - def touch_path - Puppet.features.microsoft_windows? ? "#{ENV['windir']}/system32" : "/usr/bin:/bin" - end - def usr_bin_touch(path) Puppet.features.microsoft_windows? ? "#{ENV['windir']}/system32/cmd.exe /c \"type NUL >> \"#{path}\"\"" : "/usr/bin/touch #{path}" end def touch(path) Puppet.features.microsoft_windows? ? "cmd.exe /c \"type NUL >> \"#{path}\"\"" : "touch #{path}" 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 => make_absolute("/foo/bar"), :backup => false catalog.add_resource resource child_resource = Puppet::Type.type(:file).new :path => make_absolute("/foo/bar/baz"), :backup => false resource.expects(:eval_generate).returns([child_resource]) transaction = Puppet::Transaction.new(catalog, nil, Puppet::Graph::RandomPrioritizer.new) 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 => make_absolute("/foo/bar"), :backup => false resource.virtual = true catalog.add_resource resource transaction = Puppet::Transaction.new(catalog, nil, Puppet::Graph::RandomPrioritizer.new) 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 Puppet::FileSystem.exist?(path).should be_true end it "should not apply virtual exported resources" do catalog = Puppet::Resource::Catalog.new resource = Puppet::Type.type(:file).new :path => make_absolute("/foo/bar"), :backup => false resource.exported = true resource.virtual = true catalog.add_resource resource transaction = Puppet::Transaction.new(catalog, nil, Puppet::Graph::RandomPrioritizer.new) resource.expects(:evaluate).never transaction.evaluate end it "should not apply device resources on normal host" do catalog = Puppet::Resource::Catalog.new resource = Puppet::Type.type(:interface).new :name => "FastEthernet 0/1" catalog.add_resource resource transaction = Puppet::Transaction.new(catalog, nil, Puppet::Graph::RandomPrioritizer.new) transaction.for_network_device = false transaction.expects(:apply).never.with(resource, nil) transaction.evaluate transaction.resource_status(resource).should be_skipped end it "should not apply host resources on device" do catalog = Puppet::Resource::Catalog.new resource = Puppet::Type.type(:file).new :path => make_absolute("/foo/bar"), :backup => false catalog.add_resource resource transaction = Puppet::Transaction.new(catalog, nil, Puppet::Graph::RandomPrioritizer.new) transaction.for_network_device = true transaction.expects(:apply).never.with(resource, nil) transaction.evaluate transaction.resource_status(resource).should be_skipped end it "should apply device resources on device" do catalog = Puppet::Resource::Catalog.new resource = Puppet::Type.type(:interface).new :name => "FastEthernet 0/1" catalog.add_resource resource transaction = Puppet::Transaction.new(catalog, nil, Puppet::Graph::RandomPrioritizer.new) transaction.for_network_device = true transaction.expects(:apply).with(resource, nil) transaction.evaluate transaction.resource_status(resource).should_not be_skipped end it "should apply resources appliable on host and device on a device" do catalog = Puppet::Resource::Catalog.new resource = Puppet::Type.type(:schedule).new :name => "test" catalog.add_resource resource transaction = Puppet::Transaction.new(catalog, nil, Puppet::Graph::RandomPrioritizer.new) transaction.for_network_device = true transaction.expects(:apply).with(resource, nil) transaction.evaluate transaction.resource_status(resource).should_not be_skipped 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 Puppet::FileSystem.exist?(file1).should be_true Puppet::FileSystem.exist?(file2).should be_true end it "does not refresh resources that have 'noop => true'" do path = tmpfile("path") notify = Puppet::Type.type(:notify).new( :name => "trigger", :notify => Puppet::Resource.new(:exec, "noop exec") ) noop_exec = Puppet::Type.type(:exec).new( :name => "noop exec", :path => ENV["PATH"], :command => touch(path), :noop => true ) catalog = mk_catalog(notify, noop_exec) catalog.apply Puppet::FileSystem.exist?(path).should be_false end it "should apply no resources whatsoever if a pre_run_check fails" do path = tmpfile("path") file = Puppet::Type.type(:file).new( :path => path, :ensure => "file" ) notify = Puppet::Type.type(:notify).new( :title => "foo" ) notify.expects(:pre_run_check).raises(Puppet::Error, "fail for testing") catalog = mk_catalog(file, notify) catalog.apply Puppet::FileSystem.exist?(path).should_not be_true 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( :path => path, :ensure => "file" ) exec1 = Puppet::Type.type(:exec).new( :path => ENV["PATH"], :command => touch(File.expand_path("/this/cannot/possibly/exist")), :logoutput => true, :refreshonly => true, :subscribe => file, :title => "one" ) 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 Puppet::FileSystem.exist?(newfile).should be_true end - describe "skipping resources" do - let(:fname) { tmpfile("exec") } - - let(:file) do - Puppet::Type.type(:file).new( - :name => tmpfile("file"), - :ensure => "file", - :backup => false - ) - end + it "should still trigger skipped resources" do + catalog = mk_catalog + catalog.add_resource(*Puppet::Type.type(:schedule).mkdefaultschedules) - let(:exec) do - Puppet::Type.type(:exec).new( - :name => touch(fname), - :path => touch_path, - :subscribe => Puppet::Resource.new("file", file.name) - ) - end + Puppet[:ignoreschedules] = false - it "does not trigger unscheduled resources" do - catalog = mk_catalog - catalog.add_resource(*Puppet::Type.type(:schedule).mkdefaultschedules) + file = Puppet::Type.type(:file).new( + :name => tmpfile("file"), + :ensure => "file", + :backup => false + ) - Puppet[:ignoreschedules] = false + fname = tmpfile("exec") - exec[:schedule] = "monthly" + exec = Puppet::Type.type(:exec).new( + :name => touch(fname), + :path => Puppet.features.microsoft_windows? ? "#{ENV['windir']}/system32" : "/usr/bin:/bin", + :schedule => "monthly", + :subscribe => Puppet::Resource.new("file", file.name) + ) - catalog.add_resource(file, exec) + catalog.add_resource(file, exec) - # Run it once so further runs don't schedule the resource - catalog.apply - expect(Puppet::FileSystem.exist?(fname)).to be_true + # Run it once + catalog.apply + Puppet::FileSystem.exist?(fname).should be_true - # Now remove it, so it can get created again - Puppet::FileSystem.unlink(fname) + # Now remove it, so it can get created again + Puppet::FileSystem.unlink(fname) - file[:content] = "some content" + file[:content] = "some content" - catalog.apply - expect(Puppet::FileSystem.exist?(fname)).to be_false - end + catalog.apply + Puppet::FileSystem.exist?(fname).should be_true - it "does not trigger untagged resources" do - catalog = mk_catalog + # Now remove it, so it can get created again + Puppet::FileSystem.unlink(fname) - Puppet[:tags] = "runonly" - file.tag("runonly") + # And tag our exec + exec.tag("testrun") - catalog.add_resource(file, exec) - catalog.apply - expect(Puppet::FileSystem.exist?(fname)).to be_false - end + # And our file, so it runs + file.tag("norun") - it "does not trigger resources with failed dependencies" do - catalog = mk_catalog - file[:path] = make_absolute("/foo/bar/baz") + Puppet[:tags] = "norun" - catalog.add_resource(file, exec) - catalog.apply + file[:content] = "totally different content" - expect(Puppet::FileSystem.exist?(fname)).to be_false - end + catalog.apply + Puppet::FileSystem.exist?(fname).should be_true end it "should not attempt to evaluate resources with failed dependencies" do exec = Puppet::Type.type(:exec).new( :command => "#{File.expand_path('/bin/mkdir')} /this/path/cannot/possibly/exist", :title => "mkdir" ) file1 = Puppet::Type.type(:file).new( :title => "file1", :path => tmpfile("file1"), :require => exec, :ensure => :file ) file2 = Puppet::Type.type(:file).new( :title => "file2", :path => tmpfile("file2"), :require => file1, :ensure => :file ) catalog = mk_catalog(exec, file1, file2) catalog.apply Puppet::FileSystem.exist?(file1[:path]).should be_false Puppet::FileSystem.exist?(file2[:path]).should be_false 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 => "#{File.expand_path('/bin/mkdir')} /this/path/cannot/possibly/exist", :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 Puppet::FileSystem.exist?(file1).should be_false Puppet::FileSystem.exist?(file2).should be_false 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(resource) end end diff --git a/spec/unit/transaction_spec.rb b/spec/unit/transaction_spec.rb index e608a5d6b..bf7820227 100755 --- a/spec/unit/transaction_spec.rb +++ b/spec/unit/transaction_spec.rb @@ -1,736 +1,722 @@ #! /usr/bin/env ruby require 'spec_helper' require 'matchers/include_in_order' require 'puppet_spec/compiler' require 'puppet/transaction' require 'fileutils' describe Puppet::Transaction do include PuppetSpec::Files include PuppetSpec::Compiler def catalog_with_resource(resource) catalog = Puppet::Resource::Catalog.new catalog.add_resource(resource) catalog end def transaction_with_resource(resource) transaction = Puppet::Transaction.new(catalog_with_resource(resource), nil, Puppet::Graph::RandomPrioritizer.new) transaction end before do @basepath = make_absolute("/what/ever") @transaction = Puppet::Transaction.new(Puppet::Resource::Catalog.new, nil, Puppet::Graph::RandomPrioritizer.new) end it "should be able to look resource status up by resource reference" do resource = Puppet::Type.type(:notify).new :title => "foobar" transaction = transaction_with_resource(resource) transaction.evaluate transaction.resource_status(resource.to_s).should be_changed end # This will basically only ever be used during testing. it "should automatically create resource statuses if asked for a non-existent status" do resource = Puppet::Type.type(:notify).new :title => "foobar" transaction = transaction_with_resource(resource) transaction.resource_status(resource).should be_instance_of(Puppet::Resource::Status) end it "should add provided resource statuses to its report" do resource = Puppet::Type.type(:notify).new :title => "foobar" transaction = transaction_with_resource(resource) transaction.evaluate status = transaction.resource_status(resource) transaction.report.resource_statuses[resource.to_s].should equal(status) end it "should not consider there to be failed resources if no statuses are marked failed" do resource = Puppet::Type.type(:notify).new :title => "foobar" transaction = transaction_with_resource(resource) transaction.evaluate transaction.should_not be_any_failed end it "should use the provided report object" do report = Puppet::Transaction::Report.new("apply") transaction = Puppet::Transaction.new(Puppet::Resource::Catalog.new, report, nil) transaction.report.should == report end it "should create a report if none is provided" do transaction = Puppet::Transaction.new(Puppet::Resource::Catalog.new, nil, nil) transaction.report.should be_kind_of Puppet::Transaction::Report end describe "when initializing" do it "should create an event manager" do transaction = Puppet::Transaction.new(Puppet::Resource::Catalog.new, nil, nil) transaction.event_manager.should be_instance_of(Puppet::Transaction::EventManager) transaction.event_manager.transaction.should equal(transaction) end it "should create a resource harness" do transaction = Puppet::Transaction.new(Puppet::Resource::Catalog.new, nil, nil) transaction.resource_harness.should be_instance_of(Puppet::Transaction::ResourceHarness) transaction.resource_harness.transaction.should equal(transaction) end it "should set retrieval time on the report" do catalog = Puppet::Resource::Catalog.new report = Puppet::Transaction::Report.new("apply") catalog.retrieval_duration = 5 report.expects(:add_times).with(:config_retrieval, 5) transaction = Puppet::Transaction.new(catalog, report, nil) end end describe "when evaluating a resource" do let(:resource) { Puppet::Type.type(:file).new :path => @basepath } it "should process events" do transaction = transaction_with_resource(resource) transaction.expects(:skip?).with(resource).returns false transaction.event_manager.expects(:process_events).with(resource) transaction.evaluate end describe "and the resource should be skipped" do it "should mark the resource's status as skipped" do transaction = transaction_with_resource(resource) transaction.expects(:skip?).with(resource).returns true transaction.evaluate transaction.resource_status(resource).should be_skipped end - - it "does not process any scheduled events" do - transaction = transaction_with_resource(resource) - transaction.expects(:skip?).with(resource).returns true - transaction.event_manager.expects(:process_events).with(resource).never - transaction.evaluate - end - - it "dequeues all events scheduled on that resource" do - transaction = transaction_with_resource(resource) - transaction.expects(:skip?).with(resource).returns true - transaction.event_manager.expects(:dequeue_all_events_for_resource).with(resource) - transaction.evaluate - end end end describe "when applying a resource" do before do @catalog = Puppet::Resource::Catalog.new @resource = Puppet::Type.type(:file).new :path => @basepath @catalog.add_resource(@resource) @status = Puppet::Resource::Status.new(@resource) @transaction = Puppet::Transaction.new(@catalog, nil, Puppet::Graph::RandomPrioritizer.new) @transaction.event_manager.stubs(:queue_events) end it "should use its resource harness to apply the resource" do @transaction.resource_harness.expects(:evaluate).with(@resource) @transaction.evaluate end it "should add the resulting resource status to its status list" do @transaction.resource_harness.stubs(:evaluate).returns(@status) @transaction.evaluate @transaction.resource_status(@resource).should be_instance_of(Puppet::Resource::Status) end it "should queue any events added to the resource status" do @transaction.resource_harness.stubs(:evaluate).returns(@status) @status.expects(:events).returns %w{a b} @transaction.event_manager.expects(:queue_events).with(@resource, ["a", "b"]) @transaction.evaluate end it "should log and skip any resources that cannot be applied" do @resource.expects(:properties).raises ArgumentError @transaction.evaluate @transaction.report.resource_statuses[@resource.to_s].should be_failed end it "should report any_failed if any resources failed" do @resource.expects(:properties).raises ArgumentError @transaction.evaluate expect(@transaction).to be_any_failed end end describe "#unblock" do let(:graph) { @transaction.relationship_graph } let(:resource) { Puppet::Type.type(:notify).new(:name => 'foo') } it "should calculate the number of blockers if it's not known" do graph.add_vertex(resource) 3.times do |i| other = Puppet::Type.type(:notify).new(:name => i.to_s) graph.add_vertex(other) graph.add_edge(other, resource) end graph.unblock(resource) graph.blockers[resource].should == 2 end it "should decrement the number of blockers if there are any" do graph.blockers[resource] = 40 graph.unblock(resource) graph.blockers[resource].should == 39 end it "should warn if there are no blockers" do vertex = stub('vertex') vertex.expects(:warning).with "appears to have a negative number of dependencies" graph.blockers[vertex] = 0 graph.unblock(vertex) end it "should return true if the resource is now unblocked" do graph.blockers[resource] = 1 graph.unblock(resource).should == true end it "should return false if the resource is still blocked" do graph.blockers[resource] = 2 graph.unblock(resource).should == false end end describe "when traversing" do let(:path) { tmpdir('eval_generate') } let(:resource) { Puppet::Type.type(:file).new(:path => path, :recurse => true) } before :each do @transaction.catalog.add_resource(resource) end it "should yield the resource even if eval_generate is called" do Puppet::Transaction::AdditionalResourceGenerator.any_instance.expects(:eval_generate).with(resource).returns true yielded = false @transaction.evaluate do |res| yielded = true if res == resource end yielded.should == true end it "should prefetch the provider if necessary" do @transaction.expects(:prefetch_if_necessary).with(resource) @transaction.evaluate {} end it "traverses independent resources before dependent resources" do dependent = Puppet::Type.type(:notify).new(:name => "hello", :require => resource) @transaction.catalog.add_resource(dependent) seen = [] @transaction.evaluate do |res| seen << res end expect(seen).to include_in_order(resource, dependent) end it "traverses completely independent resources in the order they appear in the catalog" do independent = Puppet::Type.type(:notify).new(:name => "hello", :require => resource) @transaction.catalog.add_resource(independent) seen = [] @transaction.evaluate do |res| seen << res end expect(seen).to include_in_order(resource, independent) end it "should fail unsuitable resources and go on if it gets blocked" do dependent = Puppet::Type.type(:notify).new(:name => "hello", :require => resource) @transaction.catalog.add_resource(dependent) resource.stubs(:suitable?).returns false evaluated = [] @transaction.evaluate do |res| evaluated << res end # We should have gone on to evaluate the children evaluated.should == [dependent] @transaction.resource_status(resource).should be_failed end end describe "when generating resources before traversal" do let(:catalog) { Puppet::Resource::Catalog.new } let(:transaction) { Puppet::Transaction.new(catalog, nil, Puppet::Graph::RandomPrioritizer.new) } let(:generator) { Puppet::Type.type(:notify).new :title => "generator" } let(:generated) do %w[a b c].map { |name| Puppet::Type.type(:notify).new(:name => name) } end before :each do catalog.add_resource generator generator.stubs(:generate).returns generated # avoid crude failures because of nil resources that result # from implicit containment and lacking containers catalog.stubs(:container_of).returns generator end it "should call 'generate' on all created resources" do generated.each { |res| res.expects(:generate) } transaction.evaluate end it "should finish all resources" do generated.each { |res| res.expects(:finish) } transaction.evaluate end it "should copy all tags to the newly generated resources" do generator.tag('one', 'two') transaction.evaluate generated.each do |res| res.must be_tagged(*generator.tags) end end end describe "when performing pre-run checks" do let(:resource) { Puppet::Type.type(:notify).new(:title => "spec") } let(:transaction) { transaction_with_resource(resource) } let(:spec_exception) { 'spec-exception' } it "should invoke each resource's hook and apply the catalog after no failures" do resource.expects(:pre_run_check) transaction.evaluate end it "should abort the transaction on failure" do resource.expects(:pre_run_check).raises(Puppet::Error, spec_exception) expect { transaction.evaluate }.to raise_error(Puppet::Error, /Some pre-run checks failed/) end it "should log the resource-specific exception" do resource.expects(:pre_run_check).raises(Puppet::Error, spec_exception) resource.expects(:log_exception).with(responds_with(:message, spec_exception)) expect { transaction.evaluate }.to raise_error(Puppet::Error) end end describe "when skipping a resource" do before :each do @resource = Puppet::Type.type(:notify).new :name => "foo" @catalog = Puppet::Resource::Catalog.new @resource.catalog = @catalog @transaction = Puppet::Transaction.new(@catalog, nil, nil) end it "should skip resource with missing tags" do @transaction.stubs(:missing_tags?).returns(true) @transaction.should be_skip(@resource) end it "should skip unscheduled resources" do @transaction.stubs(:scheduled?).returns(false) @transaction.should be_skip(@resource) end it "should skip resources with failed dependencies" do @transaction.stubs(:failed_dependencies?).returns(true) @transaction.should be_skip(@resource) end it "should skip virtual resource" do @resource.stubs(:virtual?).returns true @transaction.should be_skip(@resource) end it "should skip device only resouce on normal host" do @resource.stubs(:appliable_to_host?).returns false @resource.stubs(:appliable_to_device?).returns true @transaction.for_network_device = false @transaction.should be_skip(@resource) end it "should not skip device only resouce on remote device" do @resource.stubs(:appliable_to_host?).returns false @resource.stubs(:appliable_to_device?).returns true @transaction.for_network_device = true @transaction.should_not be_skip(@resource) end it "should skip host resouce on device" do @resource.stubs(:appliable_to_host?).returns true @resource.stubs(:appliable_to_device?).returns false @transaction.for_network_device = true @transaction.should be_skip(@resource) end it "should not skip resouce available on both device and host when on device" do @resource.stubs(:appliable_to_host?).returns true @resource.stubs(:appliable_to_device?).returns true @transaction.for_network_device = true @transaction.should_not be_skip(@resource) end it "should not skip resouce available on both device and host when on host" do @resource.stubs(:appliable_to_host?).returns true @resource.stubs(:appliable_to_device?).returns true @transaction.for_network_device = false @transaction.should_not be_skip(@resource) end end describe "when determining if tags are missing" do before :each do @resource = Puppet::Type.type(:notify).new :name => "foo" @catalog = Puppet::Resource::Catalog.new @resource.catalog = @catalog @transaction = Puppet::Transaction.new(@catalog, nil, nil) @transaction.stubs(:ignore_tags?).returns false end it "should not be missing tags if tags are being ignored" do @transaction.expects(:ignore_tags?).returns true @resource.expects(:tagged?).never @transaction.should_not be_missing_tags(@resource) end it "should not be missing tags if the transaction tags are empty" do @transaction.tags = [] @resource.expects(:tagged?).never @transaction.should_not be_missing_tags(@resource) end it "should otherwise let the resource determine if it is missing tags" do tags = ['one', 'two'] @transaction.tags = tags @transaction.should be_missing_tags(@resource) end end describe "when determining if a resource should be scheduled" do before :each do @resource = Puppet::Type.type(:notify).new :name => "foo" @catalog = Puppet::Resource::Catalog.new @catalog.add_resource(@resource) @transaction = Puppet::Transaction.new(@catalog, nil, Puppet::Graph::RandomPrioritizer.new) end it "should always schedule resources if 'ignoreschedules' is set" do @transaction.ignoreschedules = true @transaction.resource_harness.expects(:scheduled?).never @transaction.evaluate @transaction.resource_status(@resource).should be_changed end it "should let the resource harness determine whether the resource should be scheduled" do @transaction.resource_harness.expects(:scheduled?).with(@resource).returns "feh" @transaction.evaluate end end describe "when prefetching" do let(:catalog) { Puppet::Resource::Catalog.new } let(:transaction) { Puppet::Transaction.new(catalog, nil, nil) } let(:resource) { Puppet::Type.type(:sshkey).new :title => "foo", :name => "bar", :type => :dsa, :key => "eh", :provider => :parsed } let(:resource2) { Puppet::Type.type(:package).new :title => "blah", :provider => "apt" } before :each do catalog.add_resource resource catalog.add_resource resource2 end it "should match resources by name, not title" do resource.provider.class.expects(:prefetch).with("bar" => resource) transaction.prefetch_if_necessary(resource) end it "should not prefetch a provider which has already been prefetched" do transaction.prefetched_providers[:sshkey][:parsed] = true resource.provider.class.expects(:prefetch).never transaction.prefetch_if_necessary(resource) end it "should mark the provider prefetched" do resource.provider.class.stubs(:prefetch) transaction.prefetch_if_necessary(resource) transaction.prefetched_providers[:sshkey][:parsed].should be_true end it "should prefetch resources without a provider if prefetching the default provider" do other = Puppet::Type.type(:sshkey).new :name => "other" other.instance_variable_set(:@provider, nil) catalog.add_resource other resource.provider.class.expects(:prefetch).with('bar' => resource, 'other' => other) transaction.prefetch_if_necessary(resource) end end describe "during teardown" do let(:catalog) { Puppet::Resource::Catalog.new } let(:transaction) do Puppet::Transaction.new(catalog, nil, Puppet::Graph::RandomPrioritizer.new) end let(:teardown_type) do Puppet::Type.newtype(:teardown_test) do newparam(:name) {} end end before :each do teardown_type.provide(:teardown_provider) do class << self attr_reader :result def post_resource_eval @result = 'passed' end end end end it "should call ::post_resource_eval on provider classes that support it" do resource = teardown_type.new(:title => "foo", :provider => :teardown_provider) transaction = transaction_with_resource(resource) transaction.evaluate expect(resource.provider.class.result).to eq('passed') end it "should call ::post_resource_eval even if other providers' ::post_resource_eval fails" do teardown_type.provide(:always_fails) do class << self attr_reader :result def post_resource_eval @result = 'failed' raise Puppet::Error, "This provider always fails" end end end good_resource = teardown_type.new(:title => "bloo", :provider => :teardown_provider) bad_resource = teardown_type.new(:title => "blob", :provider => :always_fails) catalog.add_resource(bad_resource) catalog.add_resource(good_resource) transaction.evaluate expect(good_resource.provider.class.result).to eq('passed') expect(bad_resource.provider.class.result).to eq('failed') end it "should call ::post_resource_eval even if one of the resources fails" do resource = teardown_type.new(:title => "foo", :provider => :teardown_provider) resource.stubs(:retrieve_resource).raises catalog.add_resource resource resource.provider.class.expects(:post_resource_eval) transaction.evaluate end end describe 'when checking application run state' do before do @catalog = Puppet::Resource::Catalog.new @transaction = Puppet::Transaction.new(@catalog, nil, Puppet::Graph::RandomPrioritizer.new) end context "when stop is requested" do before :each do Puppet::Application.stubs(:stop_requested?).returns(true) end it 'should return true for :stop_processing?' do @transaction.should be_stop_processing end it 'always evaluates non-host_config catalogs' do @catalog.host_config = false @transaction.should_not be_stop_processing end end it 'should return false for :stop_processing? if Puppet::Application.stop_requested? is false' do Puppet::Application.stubs(:stop_requested?).returns(false) @transaction.stop_processing?.should be_false end describe 'within an evaluate call' do before do @resource = Puppet::Type.type(:notify).new :title => "foobar" @catalog.add_resource @resource @transaction.stubs(:add_dynamically_generated_resources) end it 'should stop processing if :stop_processing? is true' do @transaction.stubs(:stop_processing?).returns(true) @transaction.expects(:eval_resource).never @transaction.evaluate end it 'should continue processing if :stop_processing? is false' do @transaction.stubs(:stop_processing?).returns(false) @transaction.expects(:eval_resource).returns(nil) @transaction.evaluate end end end it "errors with a dependency cycle for a resource that requires itself" do expect do apply_compiled_manifest(<<-MANIFEST) notify { cycle: require => Notify[cycle] } MANIFEST end.to raise_error(Puppet::Error, /Found 1 dependency cycle:.*\(Notify\[cycle\] => Notify\[cycle\]\)/m) end it "errors with a dependency cycle for a self-requiring resource also required by another resource" do expect do apply_compiled_manifest(<<-MANIFEST) notify { cycle: require => Notify[cycle] } notify { other: require => Notify[cycle] } MANIFEST end.to raise_error(Puppet::Error, /Found 1 dependency cycle:.*\(Notify\[cycle\] => Notify\[cycle\]\)/m) end it "errors with a dependency cycle for a resource that requires itself and another resource" do expect do apply_compiled_manifest(<<-MANIFEST) notify { cycle: require => [Notify[other], Notify[cycle]] } notify { other: } MANIFEST end.to raise_error(Puppet::Error, /Found 1 dependency cycle:.*\(Notify\[cycle\] => Notify\[cycle\]\)/m) end it "errors with a dependency cycle for a resource that is later modified to require itself" do expect do apply_compiled_manifest(<<-MANIFEST) notify { cycle: } Notify <| title == 'cycle' |> { require => Notify[cycle] } MANIFEST end.to raise_error(Puppet::Error, /Found 1 dependency cycle:.*\(Notify\[cycle\] => Notify\[cycle\]\)/m) end it "reports a changed resource with a successful run" do transaction = apply_compiled_manifest("notify { one: }") transaction.report.status.should == 'changed' transaction.report.resource_statuses['Notify[one]'].should be_changed end describe "when interrupted" do it "marks unprocessed resources as skipped" do Puppet::Application.stop! transaction = apply_compiled_manifest(<<-MANIFEST) notify { a: } -> notify { b: } MANIFEST transaction.report.resource_statuses['Notify[a]'].should be_skipped transaction.report.resource_statuses['Notify[b]'].should be_skipped end end end describe Puppet::Transaction, " when determining tags" do before do @config = Puppet::Resource::Catalog.new @transaction = Puppet::Transaction.new(@config, nil, nil) end it "should default to the tags specified in the :tags setting" do Puppet[:tags] = "one" @transaction.should be_tagged("one") end it "should split tags based on ','" do Puppet[:tags] = "one,two" @transaction.should be_tagged("one") @transaction.should be_tagged("two") end it "should use any tags set after creation" do Puppet[:tags] = "" @transaction.tags = %w{one two} @transaction.should be_tagged("one") @transaction.should be_tagged("two") end it "should always convert assigned tags to an array" do @transaction.tags = "one::two" @transaction.should be_tagged("one::two") end it "should accept a comma-delimited string" do @transaction.tags = "one, two" @transaction.should be_tagged("one") @transaction.should be_tagged("two") end it "should accept an empty string" do @transaction.tags = "one, two" @transaction.should be_tagged("one") @transaction.tags = "" @transaction.should_not be_tagged("one") end end