diff --git a/lib/puppet/transaction.rb b/lib/puppet/transaction.rb index e6c5ac429..5302e8630 100644 --- a/lib/puppet/transaction.rb +++ b/lib/puppet/transaction.rb @@ -1,490 +1,487 @@ # the class that actually walks our resource/property tree, collects the changes, # and performs them require 'puppet' require 'puppet/util/tagging' require 'puppet/application' require 'digest/sha1' 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, :for_network_device attr_accessor :configurator # 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 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) 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 # Find all of the applied resources (including failed attempts). def applied_resources report.resource_statuses.values.collect { |status| catalog.resource(status.resource) } end # Copy an important relationships from the parent to the newly-generated # child resource. def add_conditional_directed_dependency(parent, child, label=nil) relationship_graph.add_vertex(child) edge = parent.depthfirst? ? [child, parent] : [parent, child] if relationship_graph.edge?(*edge.reverse) parent.debug "Skipping automatic relationship to #{child}" else relationship_graph.add_edge(edge[0],edge[1],label) end end # Evaluate a single resource. def eval_resource(resource, ancestor = nil) if skip?(resource) resource_status(resource).skipped = true else resource_status(resource).scheduled = true apply(resource, ancestor) end # Check to see if there are any events queued for this resource event_manager.process_events(resource) 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 - # Add any dynamically generated resources - xgenerate + add_dynamically_generated_resources Puppet.info "Applying configuration version '#{catalog.version}'" if catalog.version relationship_graph.traverse do |resource| if resource.is_a?(Puppet::Type::Component) Puppet.warning "Somehow left a component in the relationship graph" else seconds = thinmark { eval_resource(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 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 # 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 def eval_generate(resource) return false unless resource.respond_to?(:eval_generate) raise Puppet::DevError,"Depthfirst resources are not supported by eval_generate" if resource.depthfirst? begin made = resource.eval_generate.uniq return false if made.empty? made = Hash[made.map(&:name).zip(made)] rescue => detail puts detail.backtrace if Puppet[:trace] resource.err "Failed to generate additional resources using 'eval_generate: #{detail}" return false end made.values.each do |res| begin res.tag(*resource.tags) @catalog.add_resource(res) res.finish rescue Puppet::Resource::Catalog::DuplicateResourceError res.info "Duplicate generated resource; skipping" end end sentinel = Puppet::Type.type(:whit).new(:name => "completed_#{resource.title}", :catalog => resource.catalog) # The completed whit is now the thing that represents the resource is done relationship_graph.adjacent(resource,:direction => :out,:type => :edges).each { |e| # But children run as part of the resource, not after it next if made[e.target.name] add_conditional_directed_dependency(sentinel, e.target, e.label) relationship_graph.remove_edge! e } default_label = Puppet::Resource::Catalog::Default_label made.values.each do |res| # Depend on the nearest ancestor we generated, falling back to the # resource if we have none parent_name = res.ancestors.find { |a| made[a] and made[a] != res } parent = made[parent_name] || resource add_conditional_directed_dependency(parent, res) # This resource isn't 'completed' until each child has run add_conditional_directed_dependency(res, sentinel, default_label) end # This edge allows the resource's events to propagate, though it isn't # strictly necessary for ordering purposes add_conditional_directed_dependency(resource, sentinel, default_label) true 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 puts detail.backtrace if Puppet[:trace] resource.err "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 - # Collect any dynamically generated resources. This method is called - # before the transaction starts. - def xgenerate + def add_dynamically_generated_resources @catalog.vertices.each { |resource| generate_additional_resources(resource) } 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, report = nil) @catalog = catalog @report = report || Puppet::Transaction::Report.new("apply", catalog.version) @event_manager = Puppet::Transaction::EventManager.new(self) @resource_harness = Puppet::Transaction::ResourceHarness.new(self) @prefetched_providers = Hash.new { |h,k| h[k] = {} } 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 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 attr_reader :prefetched_providers # 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 puts detail.backtrace if Puppet[:trace] Puppet.err "Could not prefetch #{type_name} provider '#{provider_class.name}': #{detail}" end @prefetched_providers[type_name][provider_class.name] = true end # We want to monitor changes in the relationship graph of our # catalog but this is complicated by the fact that the catalog # both is_a graph and has_a graph, by the fact that changes to # the structure of the object can have adverse serialization # effects, by threading issues, by order-of-initialization issues, # etc. # # Since the proper lifetime/scope of the monitoring is a transaction # and the transaction is already commiting a mild law-of-demeter # transgression, we cut the Gordian knot here by simply wrapping the # transaction's view of the resource graph to capture and maintain # the information we need. Nothing outside the transaction needs # this information, and nothing outside the transaction can see it # except via the Transaction#relationship_graph class Relationship_graph_wrapper require 'puppet/rb_tree_map' attr_reader :real_graph,:transaction,:ready,:generated,:done,:blockers,:unguessable_deterministic_key def initialize(real_graph,transaction) @real_graph = real_graph @transaction = transaction @ready = Puppet::RbTreeMap.new @generated = {} @done = {} @blockers = {} @unguessable_deterministic_key = Hash.new { |h,k| h[k] = Digest::SHA1.hexdigest("NaCl, MgSO4 (salts) and then #{k.ref}") } @providerless_types = [] vertices.each do |v| blockers[v] = direct_dependencies_of(v).length enqueue(v) if blockers[v] == 0 end end def method_missing(*args,&block) real_graph.send(*args,&block) end def add_vertex(v) real_graph.add_vertex(v) end def add_edge(f,t,label=nil) key = unguessable_deterministic_key[t] ready.delete(key) real_graph.add_edge(f,t,label) end # Decrement the blocker count for resource r by 1. If the number of # blockers is unknown, count them and THEN decrement by 1. def unblock(r) blockers[r] ||= direct_dependencies_of(r).select { |r2| !done[r2] }.length if blockers[r] > 0 blockers[r] -= 1 else r.warning "appears to have a negative number of dependencies" end blockers[r] <= 0 end def enqueue(*resources) resources.each do |r| key = unguessable_deterministic_key[r] ready[key] = r end end def finish(r) direct_dependents_of(r).each do |v| enqueue(v) if unblock(v) end done[r] = true end def next_resource ready.delete_min end def traverse(&block) real_graph.report_cycles_in_graph deferred = [] while (r = next_resource) && !transaction.stop_processing? if r.suitable? made_progress = true transaction.prefetch_if_necessary(r) # 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. blockers.clear if transaction.eval_generate(r) yield r finish(r) else deferred << r end if ready.empty? and deferred.any? if made_progress enqueue(*deferred) else fail_unsuitable_resources(deferred) end made_progress = false deferred = [] end end # 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 end def fail_unsuitable_resources(resources) resources.each do |resource| # We don't automatically assign unsuitable providers, so if there # is one, it must have been selected by the user. if resource.provider resource.err "Provider #{resource.provider.class.name} is not functional on this host" else @providerless_types << resource.type end transaction.resource_status(resource).failed = true finish(resource) end end end def relationship_graph @relationship_graph ||= Relationship_graph_wrapper.new(catalog.relationship_graph,self) 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) # 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 resource.appliable_to_device? ^ for_network_device resource.debug "Skipping #{resource.appliable_to_device? ? 'device' : 'host'} resources because running on a #{for_network_device ? 'device' : 'host'}" 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/unit/transaction_spec.rb b/spec/unit/transaction_spec.rb index 836039f29..9e85390eb 100755 --- a/spec/unit/transaction_spec.rb +++ b/spec/unit/transaction_spec.rb @@ -1,847 +1,847 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/transaction' require 'fileutils' def without_warnings flag = $VERBOSE $VERBOSE = nil yield $VERBOSE = flag end describe Puppet::Transaction do include PuppetSpec::Files before do @basepath = make_absolute("/what/ever") @transaction = Puppet::Transaction.new(Puppet::Resource::Catalog.new) end it "should delegate its event list to the event manager" do @transaction = Puppet::Transaction.new(Puppet::Resource::Catalog.new) @transaction.event_manager.expects(:events).returns %w{my events} @transaction.events.should == %w{my events} end it "should delegate adding times to its report" do @transaction = Puppet::Transaction.new(Puppet::Resource::Catalog.new) @transaction.report.expects(:add_times).with(:foo, 10) @transaction.report.expects(:add_times).with(:bar, 20) @transaction.add_times :foo => 10, :bar => 20 end it "should be able to accept resource status instances" do resource = Puppet::Type.type(:notify).new :title => "foobar" status = Puppet::Resource::Status.new(resource) @transaction.add_resource_status(status) @transaction.resource_status(resource).should equal(status) end it "should be able to look resource status up by resource reference" do resource = Puppet::Type.type(:notify).new :title => "foobar" status = Puppet::Resource::Status.new(resource) @transaction.add_resource_status(status) @transaction.resource_status(resource.to_s).should equal(status) 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.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" status = Puppet::Resource::Status.new(resource) @transaction.add_resource_status(status) @transaction.report.resource_statuses[resource.to_s].should equal(status) end it "should consider a resource to be failed if a status instance exists for that resource and indicates it is failed" do resource = Puppet::Type.type(:notify).new :name => "yayness" status = Puppet::Resource::Status.new(resource) status.failed = "some message" @transaction.add_resource_status(status) @transaction.should be_failed(resource) end it "should not consider a resource to be failed if a status instance exists for that resource but indicates it is not failed" do resource = Puppet::Type.type(:notify).new :name => "yayness" status = Puppet::Resource::Status.new(resource) @transaction.add_resource_status(status) @transaction.should_not be_failed(resource) end it "should consider there to be failed resources if any statuses are marked failed" do resource = Puppet::Type.type(:notify).new :name => "yayness" status = Puppet::Resource::Status.new(resource) status.failed = "some message" @transaction.add_resource_status(status) @transaction.should be_any_failed end it "should not consider there to be failed resources if no statuses are marked failed" do resource = Puppet::Type.type(:notify).new :name => "yayness" status = Puppet::Resource::Status.new(resource) @transaction.add_resource_status(status) @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) @transaction.report.should == report end it "should create a report if none is provided" do @transaction = Puppet::Transaction.new(Puppet::Resource::Catalog.new) @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) @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) @transaction.resource_harness.should be_instance_of(Puppet::Transaction::ResourceHarness) @transaction.resource_harness.transaction.should equal(@transaction) end end describe "when evaluating a resource" do before do @transaction = Puppet::Transaction.new(Puppet::Resource::Catalog.new) @transaction.stubs(:skip?).returns false @resource = Puppet::Type.type(:file).new :path => @basepath end it "should check whether the resource should be skipped" do @transaction.expects(:skip?).with(@resource).returns false @transaction.eval_resource(@resource) end it "should process events" do @transaction.event_manager.expects(:process_events).with(@resource) @transaction.eval_resource(@resource) end describe "and the resource should be skipped" do before do @transaction.expects(:skip?).with(@resource).returns true end it "should mark the resource's status as skipped" do @transaction.eval_resource(@resource) @transaction.resource_status(@resource).should be_skipped end end end describe "when applying a resource" do before do @resource = Puppet::Type.type(:file).new :path => @basepath @status = Puppet::Resource::Status.new(@resource) @transaction = Puppet::Transaction.new(Puppet::Resource::Catalog.new) @transaction.event_manager.stubs(:queue_events) @transaction.resource_harness.stubs(:evaluate).returns(@status) end it "should use its resource harness to apply the resource" do @transaction.resource_harness.expects(:evaluate).with(@resource) @transaction.apply(@resource) end it "should add the resulting resource status to its status list" do @transaction.apply(@resource) @transaction.resource_status(@resource).should be_instance_of(Puppet::Resource::Status) end it "should queue any events added to the resource status" do @status.expects(:events).returns %w{a b} @transaction.event_manager.expects(:queue_events).with(@resource, ["a", "b"]) @transaction.apply(@resource) end it "should log and skip any resources that cannot be applied" do @transaction.resource_harness.expects(:evaluate).raises ArgumentError @resource.expects(:err) @transaction.apply(@resource) @transaction.report.resource_statuses[@resource.to_s].should be_nil end end describe "#eval_generate" do let(:path) { tmpdir('eval_generate') } let(:resource) { Puppet::Type.type(:file).new(:path => path, :recurse => true) } let(:graph) { @transaction.relationship_graph } def find_vertex(type, title) graph.vertices.find {|v| v.type == type and v.title == title} end before :each do @filenames = [] 'a'.upto('c') do |x| @filenames << File.join(path,x) 'a'.upto('c') do |y| @filenames << File.join(path,x,y) FileUtils.mkdir_p(File.join(path,x,y)) 'a'.upto('c') do |z| @filenames << File.join(path,x,y,z) FileUtils.touch(File.join(path,x,y,z)) end end end @transaction.catalog.add_resource(resource) end it "should add the generated resources to the catalog" do @transaction.eval_generate(resource) @filenames.each do |file| @transaction.catalog.resource(:file, file).should be_a(Puppet::Type.type(:file)) end end it "should add a sentinel whit for the resource" do @transaction.eval_generate(resource) find_vertex(:whit, "completed_#{path}").should be_a(Puppet::Type.type(:whit)) end it "should replace dependencies on the resource with dependencies on the sentinel" do dependent = Puppet::Type.type(:notify).new(:name => "hello", :require => resource) @transaction.catalog.add_resource(dependent) res = find_vertex(resource.type, resource.title) generated = find_vertex(dependent.type, dependent.title) graph.should be_edge(res, generated) @transaction.eval_generate(resource) sentinel = find_vertex(:whit, "completed_#{path}") graph.should be_edge(sentinel, generated) graph.should_not be_edge(res, generated) end it "should add an edge from the nearest ancestor to the generated resource" do @transaction.eval_generate(resource) @filenames.each do |file| v = find_vertex(:file, file) p = find_vertex(:file, File.dirname(file)) graph.should be_edge(p, v) end end it "should add an edge from each generated resource to the sentinel" do @transaction.eval_generate(resource) sentinel = find_vertex(:whit, "completed_#{path}") @filenames.each do |file| v = find_vertex(:file, file) graph.should be_edge(v, sentinel) end end it "should add an edge from the resource to the sentinel" do @transaction.eval_generate(resource) res = find_vertex(:file, path) sentinel = find_vertex(:whit, "completed_#{path}") graph.should be_edge(res, sentinel) end it "should return false if an error occured when generating resources" do resource.stubs(:eval_generate).raises(Puppet::Error) @transaction.eval_generate(resource).should == false end it "should return true if resources were generated" do @transaction.eval_generate(resource).should == true end it "should not add a sentinel if no resources are generated" do path2 = tmpfile('empty') other_file = Puppet::Type.type(:file).new(:path => path2) @transaction.catalog.add_resource(other_file) @transaction.eval_generate(other_file).should == false find_vertex(:whit, "completed_#{path2}").should be_nil 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 "#finish" do let(:graph) { @transaction.relationship_graph } 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 unblock the resource's dependents and queue them if ready" do dependent = Puppet::Type.type(:file).new(:path => tmpfile('dependent'), :require => resource) more_dependent = Puppet::Type.type(:file).new(:path => tmpfile('more_dependent'), :require => [resource, dependent]) @transaction.catalog.add_resource(dependent, more_dependent) graph.finish(resource) graph.blockers[dependent].should == 0 graph.blockers[more_dependent].should == 1 key = graph.unguessable_deterministic_key[dependent] graph.ready[key].must == dependent graph.ready.should_not be_has_key(graph.unguessable_deterministic_key[more_dependent]) end it "should mark the resource as done" do graph.finish(resource) graph.done[resource].should == true end end describe "when traversing" do let(:graph) { @transaction.relationship_graph } 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 clear blockers if resources are added" do graph.blockers['foo'] = 3 graph.blockers['bar'] = 4 graph.ready[graph.unguessable_deterministic_key[resource]] = resource @transaction.expects(:eval_generate).with(resource).returns true graph.traverse {} graph.blockers.should be_empty end it "should yield the resource even if eval_generate is called" do graph.ready[graph.unguessable_deterministic_key[resource]] = resource @transaction.expects(:eval_generate).with(resource).returns true yielded = false graph.traverse 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) graph.traverse {} end it "should not clear blockers if resources aren't added" do graph.blockers['foo'] = 3 graph.blockers['bar'] = 4 graph.ready[graph.unguessable_deterministic_key[resource]] = resource @transaction.expects(:eval_generate).with(resource).returns false graph.traverse {} graph.blockers.should == {'foo' => 3, 'bar' => 4, resource => 0} end it "should unblock all dependents of the resource" do dependent = Puppet::Type.type(:notify).new(:name => "hello", :require => resource) dependent2 = Puppet::Type.type(:notify).new(:name => "goodbye", :require => resource) @transaction.catalog.add_resource(dependent, dependent2) graph.blockers[dependent].should == 1 graph.blockers[dependent2].should == 1 graph.ready[graph.unguessable_deterministic_key[resource]] = resource graph.traverse {} graph.blockers[dependent].should == 0 graph.blockers[dependent2].should == 0 end it "should enqueue any unblocked dependents" do dependent = Puppet::Type.type(:notify).new(:name => "hello", :require => resource) dependent2 = Puppet::Type.type(:notify).new(:name => "goodbye", :require => resource) @transaction.catalog.add_resource(dependent, dependent2) graph.blockers[dependent].should == 1 graph.blockers[dependent2].should == 1 graph.ready[graph.unguessable_deterministic_key[resource]] = resource seen = [] graph.traverse do |res| seen << res end seen.should =~ [resource, dependent, dependent2] end it "should mark the resource done" do graph.ready[graph.unguessable_deterministic_key[resource]] = resource graph.traverse {} graph.done[resource].should == true end it "should not evaluate the resource if it's not suitable" do resource.stubs(:suitable?).returns false graph.traverse do |resource| raise "evaluated a resource" end end it "should defer an unsuitable resource unless it can't go on" do other = Puppet::Type.type(:notify).new(:name => "hello") @transaction.catalog.add_resource(other) # Show that we check once, then get the resource back and check again resource.expects(:suitable?).twice.returns(false).then.returns(true) graph.traverse {} 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 = [] graph.traverse 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" do it "should call 'generate' on all created resources" do first = Puppet::Type.type(:notify).new(:name => "first") second = Puppet::Type.type(:notify).new(:name => "second") third = Puppet::Type.type(:notify).new(:name => "third") @catalog = Puppet::Resource::Catalog.new @transaction = Puppet::Transaction.new(@catalog) first.expects(:generate).returns [second] second.expects(:generate).returns [third] third.expects(:generate) @transaction.generate_additional_resources(first) end it "should finish all resources" do generator = stub 'generator', :depthfirst? => true, :tags => [], :ref => "Some[resource]" resource = stub 'resource', :tag => nil @catalog = Puppet::Resource::Catalog.new @transaction = Puppet::Transaction.new(@catalog) generator.expects(:generate).returns [resource] @catalog.expects(:add_resource).yields(resource) resource.expects(:finish) @transaction.generate_additional_resources(generator) end it "should skip generated resources that conflict with existing resources" do generator = mock 'generator', :tags => [] resource = stub 'resource', :tag => nil @catalog = Puppet::Resource::Catalog.new @transaction = Puppet::Transaction.new(@catalog) generator.expects(:generate).returns [resource] @catalog.expects(:add_resource).raises(Puppet::Resource::Catalog::DuplicateResourceError.new("foo")) resource.expects(:finish).never resource.expects(:info) # log that it's skipped @transaction.generate_additional_resources(generator) end it "should copy all tags to the newly generated resources" do child = stub 'child', :ref => "Some[child_resource]" generator = stub 'resource', :tags => ["one", "two"], :ref => "Some[resource]" @catalog = Puppet::Resource::Catalog.new @transaction = Puppet::Transaction.new(@catalog) generator.stubs(:generate).returns [child] @catalog.stubs(:add_resource) child.expects(:tag).with("one", "two") child.expects(:finish) generator.expects(:depthfirst?) @transaction.generate_additional_resources(generator) 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) 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_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_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_device?).returns false @transaction.for_network_device = true @transaction.should 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) @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 @resource.expects(:tagged?).with(*tags).returns(false) @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 @resource.catalog = @catalog @transaction = Puppet::Transaction.new(@catalog) end it "should always schedule resources if 'ignoreschedules' is set" do @transaction.ignoreschedules = true @transaction.resource_harness.expects(:scheduled?).never @transaction.should be_scheduled(@resource) end it "should let the resource harness determine whether the resource should be scheduled" do @transaction.resource_harness.expects(:scheduled?).with(@transaction.resource_status(@resource), @resource).returns "feh" @transaction.scheduled?(@resource).should == "feh" end end describe "when prefetching" do let(:catalog) { Puppet::Resource::Catalog.new } let(:transaction) { Puppet::Transaction.new(catalog) } let(:resource) { Puppet::Type.type(:sshkey).create :title => "foo", :name => "bar", :type => :dsa, :key => "eh", :provider => :parsed } let(:resource2) { Puppet::Type.type(:package).create :title => "blah", :provider => "apt" } before :each do catalog.add_resource resource catalog.add_resource resource2 end describe "#resources_by_provider" do it "should fetch resources by their type and provider" do transaction.resources_by_provider(:sshkey, :parsed).should == { resource.name => resource, } transaction.resources_by_provider(:package, :apt).should == { resource2.name => resource2, } end it "should omit resources whose types don't use providers" do # faking the sshkey type not to have a provider resource.class.stubs(:attrclass).returns nil transaction.resources_by_provider(:sshkey, :parsed).should == {} end it "should return empty hash for providers with no resources" do transaction.resources_by_provider(:package, :yum).should == {} end 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).create :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 it "should return all resources for which the resource status indicates the resource has changed when determinig changed resources" do @catalog = Puppet::Resource::Catalog.new @transaction = Puppet::Transaction.new(@catalog) names = [] 2.times do |i| name = File.join(@basepath, "file#{i}") resource = Puppet::Type.type(:file).new :path => name names << resource.to_s @catalog.add_resource resource @transaction.add_resource_status Puppet::Resource::Status.new(resource) end @transaction.resource_status(names[0]).changed = true @transaction.changed?.should == [@catalog.resource(names[0])] end describe 'when checking application run state' do before do without_warnings { Puppet::Application = Class.new(Puppet::Application) } @catalog = Puppet::Resource::Catalog.new @transaction = Puppet::Transaction.new(@catalog) end after do without_warnings { Puppet::Application = Puppet::Application.superclass } end it 'should return true for :stop_processing? if Puppet::Application.stop_requested? is true' do Puppet::Application.stubs(:stop_requested?).returns(true) @transaction.stop_processing?.should be_true 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(:xgenerate) + @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 end describe Puppet::Transaction, " when determining tags" do before do @config = Puppet::Resource::Catalog.new @transaction = Puppet::Transaction.new(@config) end it "should default to the tags specified in the :tags setting" do Puppet.expects(:[]).with(:tags).returns("one") @transaction.tags.should == %w{one} end it "should split tags based on ','" do Puppet.expects(:[]).with(:tags).returns("one,two") @transaction.tags.should == %w{one two} end it "should use any tags set after creation" do Puppet.expects(:[]).with(:tags).never @transaction.tags = %w{one two} @transaction.tags.should == %w{one two} end it "should always convert assigned tags to an array" do @transaction.tags = "one::two" @transaction.tags.should == %w{one::two} end it "should accept a comma-delimited string" do @transaction.tags = "one, two" @transaction.tags.should == %w{one two} end it "should accept an empty string" do @transaction.tags = "" @transaction.tags.should == [] end end diff --git a/test/other/transactions.rb b/test/other/transactions.rb index 68bdcbbd1..d77fd1538 100755 --- a/test/other/transactions.rb +++ b/test/other/transactions.rb @@ -1,395 +1,395 @@ #!/usr/bin/env ruby require File.expand_path(File.dirname(__FILE__) + '/../lib/puppettest') require 'mocha' require 'puppet' require 'puppettest' require 'puppettest/support/resources' require 'puppettest/support/utils' class TestTransactions < Test::Unit::TestCase include PuppetTest::FileTesting include PuppetTest::Support::Resources include PuppetTest::Support::Utils class Fakeprop true) def finish $finished << self.name end end type.class_eval(&block) if block cleanup do Puppet::Type.rmtype(:generator) end type end # Create a new type that generates instances with shorter names. def mkreducer(&block) type = mkgenerator do def eval_generate ret = [] if title.length > 1 ret << self.class.new(:title => title[0..-2]) else return nil end ret end end type.class_eval(&block) if block type end def test_prefetch # Create a type just for testing prefetch name = :prefetchtesting $prefetched = false type = Puppet::Type.newtype(name) do newparam(:name) {} end cleanup do Puppet::Type.rmtype(name) end # Now create a provider type.provide(:prefetch) do def self.prefetch(resources) $prefetched = resources end end # Now create an instance inst = type.new :name => "yay" # Create a transaction trans = Puppet::Transaction.new(mk_catalog(inst)) # Make sure it gets called from within evaluate $prefetched = false assert_nothing_raised do trans.evaluate end assert_equal({inst.title => inst}, $prefetched, "evaluate did not call prefetch") end def test_ignore_tags? config = Puppet::Resource::Catalog.new config.host_config = true transaction = Puppet::Transaction.new(config) assert(! transaction.ignore_tags?, "Ignoring tags when applying a host catalog") config.host_config = false transaction = Puppet::Transaction.new(config) assert(transaction.ignore_tags?, "Not ignoring tags when applying a non-host catalog") end def test_missing_tags? resource = Puppet::Type.type(:notify).new :title => "foo" resource.stubs(:tagged?).returns true config = Puppet::Resource::Catalog.new # Mark it as a host config so we don't care which test is first config.host_config = true transaction = Puppet::Transaction.new(config) assert(! transaction.missing_tags?(resource), "Considered a resource to be missing tags when none are set") # host catalogs pay attention to tags, no one else does. Puppet[:tags] = "three,four" config.host_config = false transaction = Puppet::Transaction.new(config) assert(! transaction.missing_tags?(resource), "Considered a resource to be missing tags when not running a host catalog") # config.host_config = true transaction = Puppet::Transaction.new(config) assert(! transaction.missing_tags?(resource), "Considered a resource to be missing tags when running a host catalog and all tags are present") transaction = Puppet::Transaction.new(config) resource.stubs :tagged? => false assert(transaction.missing_tags?(resource), "Considered a resource not to be missing tags when running a host catalog and tags are missing") end # Make sure changes in contained files still generate callback events. def test_generated_callbacks dir = tempfile maker = tempfile Dir.mkdir(dir) file = File.join(dir, "file") File.open(file, "w") { |f| f.puts "" } File.chmod(0644, file) File.chmod(0755, dir) # So only the child file causes a change dirobj = Puppet::Type.type(:file).new :mode => "755", :recurse => true, :path => dir exec = Puppet::Type.type(:exec).new :title => "make", :command => "touch #{maker}", :path => ENV['PATH'], :refreshonly => true, :subscribe => dirobj assert_apply(dirobj, exec) assert(FileTest.exists?(maker), "Did not make callback file") end # Testing #401 -- transactions are calling refresh on classes that don't support it. def test_callback_availability $called = [] klass = Puppet::Type.newtype(:norefresh) do newparam(:name, :namevar => true) {} def method_missing(method, *args) $called << method end end cleanup do $called = nil Puppet::Type.rmtype(:norefresh) end file = Puppet::Type.type(:file).new :path => tempfile, :content => "yay" one = klass.new :name => "one", :subscribe => file assert_apply(file, one) assert(! $called.include?(:refresh), "Called refresh when it wasn't set as a method") end # Testing #437 - cyclic graphs should throw failures. def test_fail_on_cycle one = Puppet::Type.type(:exec).new(:name => "/bin/echo one") two = Puppet::Type.type(:exec).new(:name => "/bin/echo two") one[:require] = two two[:require] = one config = mk_catalog(one, two) trans = Puppet::Transaction.new(config) assert_raise(Puppet::Error) do trans.evaluate end end def test_errors_during_generation type = Puppet::Type.newtype(:failer) do newparam(:name) {} def eval_generate raise ArgumentError, "Invalid value" end def generate raise ArgumentError, "Invalid value" end end cleanup { Puppet::Type.rmtype(:failer) } obj = type.new(:name => "testing") assert_apply(obj) end def test_self_refresh_causes_triggering type = Puppet::Type.newtype(:refresher, :self_refresh => true) do attr_accessor :refreshed, :testing newparam(:name) {} newproperty(:testing) do def retrieve :eh end def sync # noop :ran_testing end end def refresh @refreshed = true end end cleanup { Puppet::Type.rmtype(:refresher)} obj = type.new(:name => "yay", :testing => "cool") assert(! obj.insync?(obj.retrieve), "fake object is already in sync") # Now make sure it gets refreshed when the change happens assert_apply(obj) assert(obj.refreshed, "object was not refreshed during transaction") end # Testing #433 def test_explicit_dependencies_beat_automatic # Create a couple of different resource sets that have automatic relationships and make sure the manual relationships win rels = {} # Now add the explicit relationship # Now files d = tempfile f = File.join(d, "file") file = Puppet::Type.type(:file).new(:path => f, :content => "yay") dir = Puppet::Type.type(:file).new(:path => d, :ensure => :directory, :require => file) rels[dir] = file rels.each do |after, before| config = mk_catalog(before, after) trans = Puppet::Transaction.new(config) str = "from #{before} to #{after}" assert_nothing_raised("Failed to create graph #{str}") do - trans.xgenerate + trans.add_dynamically_generated_resources end graph = trans.relationship_graph assert(graph.edge?(before, after), "did not create manual relationship #{str}") assert(! graph.edge?(after, before), "created automatic relationship #{str}") end end # #542 - make sure resources in noop mode still notify their resources, # so that users know if a service will get restarted. def test_noop_with_notify path = tempfile epath = tempfile spath = tempfile file = Puppet::Type.type(:file).new( :path => path, :ensure => :file, :title => "file") exec = Puppet::Type.type(:exec).new( :command => "touch #{epath}", :path => ENV["PATH"], :subscribe => file, :refreshonly => true, :title => 'exec1') exec2 = Puppet::Type.type(:exec).new( :command => "touch #{spath}", :path => ENV["PATH"], :subscribe => exec, :refreshonly => true, :title => 'exec2') Puppet[:noop] = true assert(file.noop, "file not in noop") assert(exec.noop, "exec not in noop") @logs.clear assert_apply(file, exec, exec2) assert(! FileTest.exists?(path), "Created file in noop") assert(! FileTest.exists?(epath), "Executed exec in noop") assert(! FileTest.exists?(spath), "Executed second exec in noop") assert(@logs.detect { |l| l.message =~ /should be/ and l.source == file.property(:ensure).path}, "did not log file change") assert( @logs.detect { |l| l.message =~ /Would have/ and l.source == exec.path }, "did not log first exec trigger") assert( @logs.detect { |l| l.message =~ /Would have/ and l.source == exec2.path }, "did not log second exec trigger") end def test_only_stop_purging_with_relations files = [] paths = [] 3.times do |i| path = tempfile paths << path file = Puppet::Type.type(:file).new( :path => path, :ensure => :absent, :backup => false, :title => "file#{i}") File.open(path, "w") { |f| f.puts "" } files << file end files[0][:ensure] = :file files[0][:require] = files[1..2] # Mark the second as purging files[1].purging assert_apply(*files) assert(FileTest.exists?(paths[1]), "Deleted required purging file") assert(! FileTest.exists?(paths[2]), "Did not delete non-purged file") end def test_flush $state = :absent $flushed = 0 type = Puppet::Type.newtype(:flushtest) do newparam(:name) newproperty(:ensure) do newvalues :absent, :present, :other def retrieve $state end def set(value) $state = value :thing_changed end end def flush $flushed += 1 end end cleanup { Puppet::Type.rmtype(:flushtest) } obj = type.new(:name => "test", :ensure => :present) # first make sure it runs through and flushes assert_apply(obj) assert_equal(:present, $state, "Object did not make a change") assert_equal(1, $flushed, "object was not flushed") # Now run a noop and make sure we don't flush obj[:ensure] = "other" obj[:noop] = true assert_apply(obj) assert_equal(:present, $state, "Object made a change in noop") assert_equal(1, $flushed, "object was flushed in noop") end end