diff --git a/lib/puppet/transaction.rb b/lib/puppet/transaction.rb index 823ad8c63..344c571ef 100644 --- a/lib/puppet/transaction.rb +++ b/lib/puppet/transaction.rb @@ -1,398 +1,400 @@ # 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 prepare 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) 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 + 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| 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 @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) end # Prefetch any providers that support it. We don't support prefetching # types, just providers. def prefetch prefetchers = {} @catalog.vertices.each do |resource| if provider = resource.provider and provider.class.respond_to?(:prefetch) prefetchers[provider.class] ||= {} prefetchers[provider.class][resource.name] = resource end end # Now call prefetch, passing in the resources so that the provider instances can be replaced. prefetchers.each do |provider, resources| Puppet.debug "Prefetching #{provider.name} resources for #{provider.resource_type.name}" begin provider.prefetch(resources) rescue => detail puts detail.backtrace if Puppet[:trace] Puppet.err "Could not prefetch #{provider.resource_type.name} provider '#{provider.name}': #{detail}" end end end # Prepare to evaluate the resources in a transaction. def prepare # Now add any dynamically generated resources xgenerate # Then prefetch. It's important that we generate and then prefetch, # so that any generated resources also get prefetched. prefetch 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 attr_reader :real_graph,:transaction,:ready,:generated,:done,:unguessable_deterministic_key def initialize(real_graph,transaction) @real_graph = real_graph @transaction = transaction @ready = {} @generated = {} @done = {} @unguessable_deterministic_key = Hash.new { |h,k| h[k] = Digest::SHA1.hexdigest("NaCl, MgSO4 (salts) and then #{k.title}") } vertices.each { |v| check_if_now_ready(v) } end def method_missing(*args,&block) real_graph.send(*args,&block) end def add_vertex(v) real_graph.add_vertex(v) check_if_now_ready(v) # ????????????????????????????????????????? end def add_edge(f,t,label=nil) ready.delete(t) real_graph.add_edge(f,t,label) end def check_if_now_ready(r) ready[r] = true if direct_dependencies_of(r).all? { |r2| done[r2] } end def next_resource ready.keys.sort_by { |r0| unguessable_deterministic_key[r0] }.first end def traverse(&block) real_graph.report_cycles_in_graph while (r = next_resource) && !transaction.stop_processing? if !generated[r] && r.respond_to?(:eval_generate) transaction.eval_generate(r) generated[r] = true else ready.delete(r) yield r done[r] = true direct_dependents_of(r).each { |v| check_if_now_ready(v) } end 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 b70577dc1..90025daeb 100755 --- a/spec/unit/transaction_spec.rb +++ b/spec/unit/transaction_spec.rb @@ -1,556 +1,577 @@ #!/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| 'a'.upto('c') do |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 "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 => [] 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' generator = stub 'resource', :tags => ["one", "two"] @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 it "should match resources by name, not title" do @catalog = Puppet::Resource::Catalog.new @transaction = Puppet::Transaction.new(@catalog) # Have both a title and name resource = Puppet::Type.type(:sshkey).create :title => "foo", :name => "bar", :type => :dsa, :key => "eh" @catalog.add_resource resource resource.provider.class.expects(:prefetch).with("bar" => resource) @transaction.prefetch 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(:prepare) 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/ral/type/exec.rb b/test/ral/type/exec.rb index 0831d466d..587c831b0 100755 --- a/test/ral/type/exec.rb +++ b/test/ral/type/exec.rb @@ -1,717 +1,717 @@ #!/usr/bin/env ruby require File.expand_path(File.dirname(__FILE__) + '/../../lib/puppettest') require 'puppettest' class TestExec < Test::Unit::TestCase include PuppetTest def test_numvsstring [0, "0"].each { |val| command = nil output = nil assert_nothing_raised { command = Puppet::Type.type(:exec).new( :command => "/bin/echo", :returns => val ) } assert_events([:executed_command], command) } end def test_path_or_qualified command = nil output = nil assert_raise(Puppet::Error) { command = Puppet::Type.type(:exec).new( :command => "echo" ) } assert_nothing_raised { command = Puppet::Type.type(:exec).new( :command => "echo", :path => "/usr/bin:/bin:/usr/sbin:/sbin" ) } assert_nothing_raised { command = Puppet::Type.type(:exec).new( :command => "/bin/echo" ) } assert_nothing_raised { command = Puppet::Type.type(:exec).new( :command => "/bin/echo", :path => "/usr/bin:/bin:/usr/sbin:/sbin" ) } end def test_nonzero_returns assert_nothing_raised { command = Puppet::Type.type(:exec).new( :command => "mkdir /this/directory/does/not/exist", :path => "/usr/bin:/bin:/usr/sbin:/sbin", :returns => 1 ) } assert_nothing_raised { command = Puppet::Type.type(:exec).new( :command => "touch /etc", :path => "/usr/bin:/bin:/usr/sbin:/sbin", :returns => 1 ) } assert_nothing_raised { command = Puppet::Type.type(:exec).new( :command => "thiscommanddoesnotexist", :path => "/usr/bin:/bin:/usr/sbin:/sbin", :returns => 127 ) } end def test_cwdsettings command = nil dir = "/tmp" wd = Dir.chdir(dir) { Dir.getwd } assert_nothing_raised { command = Puppet::Type.type(:exec).new( :command => "pwd", :cwd => dir, :path => "/usr/bin:/bin:/usr/sbin:/sbin", :returns => 0 ) } assert_events([:executed_command], command) assert_equal(wd,command.output.chomp) end def test_refreshonly_functional file = nil cmd = nil tmpfile = tempfile @@tmpfiles.push tmpfile trans = nil file = Puppet::Type.type(:file).new( :path => tmpfile, :content => "yay" ) # Get the file in sync assert_apply(file) # Now make an exec maker = tempfile assert_nothing_raised { cmd = Puppet::Type.type(:exec).new( :command => "touch #{maker}", :path => "/usr/bin:/bin:/usr/sbin:/sbin", :subscribe => file, :refreshonly => true ) } assert(cmd, "did not make exec") assert_nothing_raised do assert(! cmd.check_all_attributes, "Check passed when refreshonly is set") end assert_events([], file, cmd) assert(! FileTest.exists?(maker), "made file without refreshing") # Now change our content, so we throw a refresh file[:content] = "yayness" - assert_events([:content_changed, :restarted], file, cmd) + assert_events([:content_changed], file, cmd) assert(FileTest.exists?(maker), "file was not made in refresh") end def test_refreshonly cmd = true assert_nothing_raised { cmd = Puppet::Type.type(:exec).new( :command => "pwd", :path => "/usr/bin:/bin:/usr/sbin:/sbin", :refreshonly => true ) } # Checks should always fail when refreshonly is enabled assert(!cmd.check_all_attributes, "Check passed with refreshonly true") # Now make sure it passes if we pass in "true" assert(cmd.check_all_attributes(true), "Check failed with refreshonly true while refreshing") # Now set it to false cmd[:refreshonly] = false assert(cmd.check_all_attributes, "Check failed with refreshonly false") end def test_creates file = tempfile exec = nil assert(! FileTest.exists?(file), "File already exists") assert_nothing_raised { exec = Puppet::Type.type(:exec).new( :command => "touch #{file}", :path => "/usr/bin:/bin:/usr/sbin:/sbin", :creates => file ) } comp = mk_catalog("createstest", exec) assert_events([:executed_command], comp, "creates") assert_events([], comp, "creates") end # Verify that we can download the file that we're going to execute. def test_retrievethenmkexe exe = tempfile oexe = tempfile sh = %x{which sh} File.open(exe, "w") { |f| f.puts "#!#{sh}\necho yup" } file = Puppet::Type.type(:file).new( :path => oexe, :source => exe, :mode => 0755 ) exec = Puppet::Type.type(:exec).new( :command => oexe, :require => Puppet::Resource.new(:file, oexe) ) comp = mk_catalog("Testing", file, exec) assert_events([:file_created, :executed_command], comp) end # Verify that we auto-require any managed scripts. def test_autorequire_files exe = tempfile oexe = tempfile sh = %x{which sh} File.open(exe, "w") { |f| f.puts "#!#{sh}\necho yup" } file = Puppet::Type.type(:file).new( :path => oexe, :source => exe, :mode => 755 ) basedir = File.dirname(oexe) baseobj = Puppet::Type.type(:file).new( :path => basedir, :source => exe, :mode => 755 ) ofile = Puppet::Type.type(:file).new( :path => exe, :mode => 755 ) exec = Puppet::Type.type(:exec).new( :command => oexe, :path => ENV["PATH"], :cwd => basedir ) cat = Puppet::Type.type(:exec).new( :command => "cat #{exe} #{oexe}", :path => ENV["PATH"] ) catalog = mk_catalog(file, baseobj, ofile, exec, cat) rels = nil assert_nothing_raised do rels = exec.autorequire end # Verify we get the script itself assert(rels.detect { |r| r.source == file }, "Exec did not autorequire its command") # Verify we catch the cwd assert(rels.detect { |r| r.source == baseobj }, "Exec did not autorequire its cwd") # Verify we don't require ourselves assert(! rels.detect { |r| r.source == ofile }, "Exec incorrectly required mentioned file") # We not longer autorequire inline files assert_nothing_raised do rels = cat.autorequire end assert(! rels.detect { |r| r.source == ofile }, "Exec required second inline file") assert(! rels.detect { |r| r.source == file }, "Exec required inline file") end def test_ifonly afile = tempfile bfile = tempfile exec = nil assert_nothing_raised { exec = Puppet::Type.type(:exec).new( :command => "touch #{bfile}", :onlyif => "test -f #{afile}", :path => ENV['PATH'] ) } assert_events([], exec) system("touch #{afile}") assert_events([:executed_command], exec) assert_events([:executed_command], exec) system("rm #{afile}") assert_events([], exec) end def test_unless afile = tempfile bfile = tempfile exec = nil assert_nothing_raised { exec = Puppet::Type.type(:exec).new( :command => "touch #{bfile}", :unless => "test -f #{afile}", :path => ENV['PATH'] ) } comp = mk_catalog(exec) assert_events([:executed_command], comp) assert_events([:executed_command], comp) system("touch #{afile}") assert_events([], comp) assert_events([], comp) system("rm #{afile}") assert_events([:executed_command], comp) assert_events([:executed_command], comp) end if Puppet.features.root? # Verify that we can execute commands as a special user def mknverify(file, user, group = nil, id = true) File.umask(0022) args = { :command => "touch #{file}", :path => "/usr/bin:/bin:/usr/sbin:/sbin", } if user #Puppet.warning "Using user #{user.name}" if id # convert to a string, because that's what the object expects args[:user] = user.uid.to_s else args[:user] = user.name end end if group #Puppet.warning "Using group #{group.name}" if id args[:group] = group.gid.to_s else args[:group] = group.name end end exec = nil assert_nothing_raised { exec = Puppet::Type.type(:exec).new(args) } comp = mk_catalog("usertest", exec) assert_events([:executed_command], comp, "usertest") assert(FileTest.exists?(file), "File does not exist") assert_equal(user.uid, File.stat(file).uid, "File UIDs do not match") if user # We can't actually test group ownership, unfortunately, because # behaviour changes wildlly based on platform. Puppet::Type.allclear end def test_userngroup file = tempfile [ [nonrootuser], # just user, by name [nonrootuser, nil, true], # user, by uid [nil, nonrootgroup], # just group [nil, nonrootgroup, true], # just group, by id [nonrootuser, nonrootgroup], # user and group, by name [nonrootuser, nonrootgroup, true], # user and group, by id ].each { |ary| mknverify(file, *ary) { } } end end def test_logoutput exec = nil assert_nothing_raised { exec = Puppet::Type.type(:exec).new( :title => "logoutputesting", :path => "/usr/bin:/bin", :command => "echo logoutput is false", :logoutput => false ) } assert_apply(exec) assert_nothing_raised { exec[:command] = "echo logoutput is true" exec[:logoutput] = true } assert_apply(exec) assert_nothing_raised { exec[:command] = "echo logoutput is on_failure" exec[:logoutput] = "on_failure" } assert_apply(exec) end def test_execthenfile exec = nil file = nil basedir = tempfile path = File.join(basedir, "subfile") assert_nothing_raised { exec = Puppet::Type.type(:exec).new( :title => "mkdir", :path => "/usr/bin:/bin", :creates => basedir, :command => "mkdir #{basedir}; touch #{path}" ) } assert_nothing_raised { file = Puppet::Type.type(:file).new( :path => basedir, :recurse => true, :mode => "755", :require => Puppet::Resource.new("exec", "mkdir") ) } comp = mk_catalog(file, exec) comp.finalize assert_events([:executed_command, :mode_changed], comp) assert(FileTest.exists?(path), "Exec ran first") assert(File.stat(path).mode & 007777 == 0755) end # Make sure all checks need to be fully qualified. def test_falsevals exec = nil assert_nothing_raised do exec = Puppet::Type.type(:exec).new( :command => "/bin/touch yayness" ) end Puppet::Type.type(:exec).checks.each do |check| klass = Puppet::Type.type(:exec).paramclass(check) next if klass.value_collection.values.include? :false assert_raise(Puppet::Error, "Check '#{check}' did not fail on false") do exec[check] = false end end end def test_createcwdandexe exec1 = exec2 = nil dir = tempfile file = tempfile assert_nothing_raised { exec1 = Puppet::Type.type(:exec).new( :title => "one", :path => ENV["PATH"], :command => "mkdir #{dir}" ) } assert_nothing_raised("Could not create exec w/out existing cwd") { exec2 = Puppet::Type.type(:exec).new( :title => "two", :path => ENV["PATH"], :command => "touch #{file}", :cwd => dir ) } # Throw a check in there with our cwd and make sure it works assert_nothing_raised("Could not check with a missing cwd") do exec2[:unless] = "test -f /this/file/does/not/exist" exec2.retrieve end assert_raise(Puppet::Error) do exec2.property(:returns).sync end assert_nothing_raised do exec2[:require] = exec1 end assert_apply(exec1, exec2) assert(FileTest.exists?(file)) end def test_checkarrays exec = nil file = tempfile test = "test -f #{file}" assert_nothing_raised { exec = Puppet::Type.type(:exec).new( :path => ENV["PATH"], :command => "touch #{file}" ) } assert_nothing_raised { exec[:unless] = test } assert_nothing_raised { assert(exec.check_all_attributes, "Check did not pass") } assert_nothing_raised { exec[:unless] = [test, test] } assert_nothing_raised { exec.finish } assert_nothing_raised { assert(exec.check_all_attributes, "Check did not pass") } assert_apply(exec) assert_nothing_raised { assert(! exec.check_all_attributes, "Check passed") } end def test_missing_checks_cause_failures # Solaris's sh exits with 1 here instead of 127 return if Facter.value(:operatingsystem) == "Solaris" exec = Puppet::Type.type(:exec).new( :command => "echo true", :path => ENV["PATH"], :onlyif => "/bin/nosuchthingexists" ) assert_raise(ArgumentError, "Missing command did not raise error") { exec.provider.run("/bin/nosuchthingexists") } end def test_environmentparam exec = Puppet::Type.newexec( :command => "echo $environmenttest", :path => ENV["PATH"], :environment => "environmenttest=yayness" ) assert(exec, "Could not make exec") output = status = nil assert_nothing_raised { output, status = exec.provider.run("echo $environmenttest") } assert_equal("yayness\n", output) # Now check whether we can do multiline settings assert_nothing_raised do exec[:environment] = "environmenttest=a list of things and stuff" end output = status = nil assert_nothing_raised { output, status = exec.provider.run('echo "$environmenttest"') } assert_equal("a list of things\nand stuff\n", output) # Now test arrays assert_nothing_raised do exec[:environment] = ["funtest=A", "yaytest=B"] end output = status = nil assert_nothing_raised { output, status = exec.provider.run('echo "$funtest" "$yaytest"') } assert_equal("A B\n", output) end # Testing #470 def test_run_as_created_user exec = nil if Process.uid == 0 user = "nosuchuser" assert_nothing_raised("Could not create exec with non-existent user") do exec = Puppet::Type.type(:exec).new( :command => "/bin/echo yay", :user => user ) end end # Now try the group group = "nosuchgroup" assert_nothing_raised("Could not create exec with non-existent user") do exec = Puppet::Type.type(:exec).new( :command => "/bin/echo yay", :group => group ) end end # make sure paths work both as arrays and strings def test_paths_as_arrays path = %w{/usr/bin /usr/sbin /sbin} exec = nil assert_nothing_raised("Could not use an array for the path") do exec = Puppet::Type.type(:exec).new(:command => "echo yay", :path => path) end assert_equal(path, exec[:path], "array-based path did not match") assert_nothing_raised("Could not use a string for the path") do exec = Puppet::Type.type(:exec).new(:command => "echo yay", :path => path.join(":")) end assert_equal(path, exec[:path], "string-based path did not match") assert_nothing_raised("Could not use a colon-separated strings in an array for the path") do exec = Puppet::Type.type(:exec).new(:command => "echo yay", :path => ["/usr/bin", "/usr/sbin:/sbin"]) end assert_equal(path, exec[:path], "colon-separated array path did not match") end def test_checks_apply_to_refresh file = tempfile maker = tempfile exec = Puppet::Type.type(:exec).new( :title => "maker", :command => "touch #{maker}", :path => ENV["PATH"] ) # Make sure it runs normally assert_apply(exec) assert(FileTest.exists?(maker), "exec did not run") File.unlink(maker) # Now make sure it refreshes assert_nothing_raised("Failed to refresh exec") do exec.refresh end assert(FileTest.exists?(maker), "exec did not run refresh") File.unlink(maker) # Now add the checks exec[:creates] = file # Make sure it runs when the file doesn't exist assert_nothing_raised("Failed to refresh exec") do exec.refresh end assert(FileTest.exists?(maker), "exec did not refresh when checks passed") File.unlink(maker) # Now create the file and make sure it doesn't refresh File.open(file, "w") { |f| f.puts "" } assert_nothing_raised("Failed to refresh exec") do exec.refresh end assert(! FileTest.exists?(maker), "exec refreshed with failing checks") end def test_explicit_refresh refresher = tempfile maker = tempfile exec = Puppet::Type.type(:exec).new( :title => "maker", :command => "touch #{maker}", :path => ENV["PATH"] ) # Call refresh normally assert_nothing_raised do exec.refresh end # Make sure it created the normal file assert(FileTest.exists?(maker), "normal refresh did not work") File.unlink(maker) # Now reset refresh, and make sure it wins assert_nothing_raised("Could not set refresh parameter") do exec[:refresh] = "touch #{refresher}" end assert_nothing_raised do exec.refresh end # Make sure it created the normal file assert(FileTest.exists?(refresher), "refresh param was ignored") assert(! FileTest.exists?(maker), "refresh param also ran command") end if Puppet.features.root? def test_autorequire_user user = Puppet::Type.type(:user).new(:name => "yay") exec = Puppet::Type.type(:exec).new(:command => "/bin/echo fun", :user => "yay") rels = nil assert_nothing_raised("Could not evaluate autorequire") do rels = exec.autorequire end assert(rels.find { |r| r.source == user and r.target == exec }, "Exec did not autorequire user") end end end