diff --git a/lib/puppet/transaction.rb b/lib/puppet/transaction.rb index 66d691d97..e95b3fdd9 100644 --- a/lib/puppet/transaction.rb +++ b/lib/puppet/transaction.rb @@ -1,386 +1,404 @@ # the class that actually walks our object/state tree, collects the changes, # and performs them require 'puppet' require 'puppet/statechange' module Puppet class Transaction attr_accessor :component, :objects, :tags, :ignoreschedules, :ignoretags include Puppet::Util Puppet.config.setdefaults(:transaction, :tags => ["", "Tags to use to find objects. If this is set, then only objects tagged with the specified tags will be applied. Values must be comma-separated."] ) # Add some additional times for reporting def addtimes(hash) hash.each do |name, num| @timemetrics[name] = num end end # Apply all changes for a child, returning a list of the events # generated. def apply(child) # First make sure there are no failed dependencies child.eachdependency do |dep| skip = false if @failures[dep] > 0 child.notice "Dependency %s[%s] has %s failures" % [dep.class.name, dep.name, @failures[dep]] skip = true end if skip child.warning "Skipping because of failed dependencies" @objectmetrics[:skipped] += 1 return [] end end begin changes = child.evaluate rescue => detail if Puppet[:trace] puts detail.backtrace end child.err "Failed to retrieve current state: %s" % detail # Mark that it failed @failures[child] += 1 # And then return return [] end unless changes.is_a? Array changes = [changes] end if changes.length > 0 @objectmetrics[:out_of_sync] += 1 end childevents = changes.collect { |change| @changes << change @count += 1 change.transaction = self events = nil begin # use an array, so that changes can return more than one # event if they want events = [change.forward].flatten.reject { |e| e.nil? } rescue => detail if Puppet[:debug] puts detail.backtrace end change.state.err "change from %s to %s failed: %s" % [change.state.is_to_s, change.state.should_to_s, detail] @failures[child] += 1 next # FIXME this should support using onerror to determine # behaviour; or more likely, the client calling us # should do so end # Mark that our change happened, so it can be reversed # if we ever get to that point unless events.nil? or (events.is_a?(Array) and events.empty?) change.changed = true @objectmetrics[:applied] += 1 end events }.flatten.reject { |e| e.nil? } unless changes.empty? # Record when we last synced child.cache(:synced, Time.now) end childevents end # Find all of the changed objects. def changed? @changes.find_all { |change| change.changed }.collect { |change| change.state.parent }.uniq end # Collect all of the targets for the list of events. This is an unintuitive # method because it recurses up through the source the event. def collecttargets(events) events.each do |event| source = event.source start = source while source Puppet::Event::Subscription.targets_of(event, source) do |sub| if target = sub.target start.info "Scheduling %s of %s[%s]" % [sub.callback, sub.target.class.name, sub.target.title] @targets[sub.target][event] = sub else raise Puppet::DevError, "Failed to find a target for %s" % sub.inspect end end source = source.parent end end end # This method does all the actual work of running a transaction. It # collects all of the changes, executes them, and responds to any # necessary events. def evaluate @count = 0 # Allow the tags to be overriden tags = self.tags || Puppet[:tags] if tags.nil? or tags == "" tags = nil else tags = [tags] unless tags.is_a? Array tags = tags.collect do |tag| tag.split(/\s*,\s*/) end.flatten end # Start logging. Puppet::Log.newdestination(@report) begin allevents = @objects.collect { |child| - events = nil + events = [] if (self.ignoretags or tags.nil? or child.tagged?(tags)) if self.ignoreschedules or child.scheduled? @objectmetrics[:scheduled] += 1 # Perform the actual changes seconds = thinmark do events = apply(child) end # Keep track of how long we spend in each type of object @timemetrics[child.class.name] += seconds - - # Collect the targets of any subscriptions to those events - collecttargets(events) else child.debug "Not scheduled" end else child.debug "Not tagged with %s" % tags.join(", ") end - # Now check to see if there are any events for this child - trigger(child) + # Check to see if there are any events for this child + if triggedevents = trigger(child) + events += triggedevents + end + + # Collect the targets of any subscriptions to those events + collecttargets(events) # And return the events for collection events }.flatten.reject { |e| e.nil? } ensure # And then close the transaction log. Puppet::Log.close(@report) end Puppet.debug "Finishing transaction %s with %s changes" % [self.object_id, @count] allevents end # Determine whether a given object has failed. def failed?(obj) @failures[obj] > 0 end # this should only be called by a Puppet::Container object now # and it should only receive an array def initialize(objects) @objects = objects @objectmetrics = { :total => @objects.length, :out_of_sync => 0, # The number of objects that had changes :applied => 0, # The number of objects fixed :skipped => 0, # The number of objects skipped :restarted => 0, # The number of objects triggered :failed_restarts => 0, # The number of objects that fail a trigger :scheduled => 0 # The number of objects scheduled } # Metrics for distributing times across the different types. @timemetrics = Hash.new(0) # The number of objects that were triggered in this run @triggered = Hash.new { |hash, key| hash[key] = Hash.new(0) } # Targets of being triggered. @targets = Hash.new do |hash, key| hash[key] = {} end # The changes we're performing @changes = [] # The objects that have failed and the number of failures each. This # is used for skipping objects because of failed dependencies. @failures = Hash.new do |h, key| h[key] = 0 end @report = Report.new end # Generate a transaction report. def report @objectmetrics[:failed] = @failures.find_all do |name, num| num > 0 end.length # Get the total time spent @timemetrics[:total] = @timemetrics.inject(0) do |total, vals| total += vals[1] total end # Unfortunately, RRD does not deal well with changing lists of values, # so we have to pick a list of values and stick with it. In this case, # that means we record the total time, the config time, and that's about # it. We should probably send each type's time as a separate metric. @timemetrics.dup.each do |name, value| if Puppet::Type.type(name) @timemetrics.delete(name) end end # Add all of the metrics related to object count and status @report.newmetric(:objects, @objectmetrics) # Record the relative time spent in each object. @report.newmetric(:time, @timemetrics) # Then all of the change-related metrics @report.newmetric(:changes, :total => @changes.length ) @report.time = Time.now return @report end # Roll all completed changes back. def rollback @targets.clear @triggered.clear allevents = @changes.reverse.collect { |change| # skip changes that were never actually run unless change.changed Puppet.debug "%s was not changed" % change.to_s next end begin events = change.backward rescue => detail Puppet.err("%s rollback failed: %s" % [change,detail]) if Puppet[:debug] puts detail.backtrace end next # at this point, we would normally do error handling # but i haven't decided what to do for that yet # so just record that a sync failed for a given object #@@failures[change.state.parent] += 1 # this still could get hairy; what if file contents changed, # but a chmod failed? how would i handle that error? dern end collecttargets(events) if events # Now check to see if there are any events for this child. # Kind of hackish, since going backwards goes a change at a # time, not a child at a time. trigger(change.state.parent) # And return the events for collection events }.flatten.reject { |e| e.nil? } end # Trigger any subscriptions to a child. This does an upwardly recursive # search -- it triggers the passed object, but also the object's parent # and so on up the tree. def trigger(child) obj = child callbacks = Hash.new { |hash, key| hash[key] = [] } sources = Hash.new { |hash, key| hash[key] = [] } + trigged = [] while obj if @targets.include?(obj) callbacks.clear sources.clear @targets[obj].each do |event, sub| # Collect all of the subs for each callback callbacks[sub.callback] << sub # And collect the sources for logging sources[event.source] << sub.callback end sources.each do |source, callbacklist| obj.debug "%s[%s] results in triggering %s" % [source.class.name, source.name, callbacklist.join(", ")] end callbacks.each do |callback, subs| - obj.notice "Triggering '%s' from %s dependencies" % + message = "Triggering '%s' from %s dependencies" % [callback, subs.length] + obj.notice message # At this point, just log failures, don't try to react # to them in any way. begin obj.send(callback) @objectmetrics[:restarted] += 1 rescue => detail obj.err "Failed to call %s on %s: %s" % [callback, obj, detail] @objectmetrics[:failed_restarts] += 1 if Puppet[:debug] puts detail.backtrace end end + # And then add an event for it. + trigged << Puppet::Event.new( + :event => :triggered, + :transaction => self, + :source => obj, + :message => message + ) + triggered(obj, callback) end end obj = obj.parent end + + if trigged.empty? + return nil + else + return trigged + end end def triggered(object, method) @triggered[object][method] += 1 end def triggered?(object, method) @triggered[object][method] end end end require 'puppet/transaction/report' # $Id$ diff --git a/test/other/events.rb b/test/other/events.rb index ab6270b2b..a8f062b81 100755 --- a/test/other/events.rb +++ b/test/other/events.rb @@ -1,152 +1,152 @@ require 'puppet' require 'puppettest' # $Id$ class TestEvents < Test::Unit::TestCase include PuppetTest def teardown super Puppet::Event::Subscription.clear end def test_simplesubscribe name = tempfile() file = Puppet.type(:file).create( :name => name, :ensure => "file" ) exec = Puppet.type(:exec).create( :name => "echo true", :path => "/usr/bin:/bin", :refreshonly => true, :subscribe => [[file.class.name, file.name]] ) comp = newcomp("eventtesting", file, exec) - trans = assert_events([:file_created], comp) + trans = assert_events([:file_created, :triggered], comp) assert_equal(1, trans.triggered?(exec, :refresh)) end def test_simplerequire name = tempfile() file = Puppet.type(:file).create( :name => name, :ensure => "file" ) exec = Puppet.type(:exec).create( :name => "echo true", :path => "/usr/bin:/bin", :refreshonly => true, :require => [[file.class.name, file.name]] ) comp = Puppet.type(:component).create( :name => "eventtesting" ) comp.push exec trans = comp.evaluate events = nil assert_nothing_raised { events = trans.evaluate } assert_equal(1, events.length) assert_equal(0, trans.triggered?(exec, :refresh)) end # Verify that one component can subscribe to another component and the "right" # thing happens def test_ladderrequire comps = {} objects = {} fname = tempfile() file = Puppet.type(:file).create( :name => tempfile(), :ensure => "file" ) exec = Puppet.type(:exec).create( :name => "touch %s" % fname, :path => "/usr/bin:/bin", :refreshonly => true ) fcomp = newcomp(file) ecomp = newcomp(exec) comp = newcomp("laddercomp", fcomp, ecomp) ecomp[:subscribe] = [[fcomp.class.name, fcomp.name]] comp.finalize trans = comp.evaluate events = nil assert_nothing_raised { events = trans.evaluate } assert(FileTest.exists?(fname), "#{fname} does not exist") #assert_equal(events.length, trans.triggered?(objects[:b], :refresh)) end def test_multiplerefreshes files = [] 4.times { |i| files << Puppet.type(:file).create( :name => tempfile(), :ensure => "file" ) } fname = tempfile() exec = Puppet.type(:exec).create( :name => "touch %s" % fname, :path => "/usr/bin:/bin", :refreshonly => true ) exec[:subscribe] = files.collect { |f| ["file", f.name] } comp = newcomp(exec, *files) assert_apply(comp) assert(FileTest.exists?(fname), "Exec file did not get created") end def test_refreshordering file = tempfile() exec1 = Puppet.type(:exec).create( :name => "echo one >> %s" % file, :path => "/usr/bin:/bin" ) exec2 = Puppet.type(:exec).create( :name => "echo two >> %s" % file, :path => "/usr/bin:/bin", :refreshonly => true, :subscribe => ["exec", exec1.name] ) exec3 = Puppet.type(:exec).create( :name => "echo three >> %s" % file, :path => "/usr/bin:/bin" ) comp = newcomp(exec1,exec2,exec3) assert_apply(comp) assert(FileTest.exists?(file), "File does not exist") assert_equal("one\ntwo\nthree\n", File.read(file)) end end diff --git a/test/other/transactions.rb b/test/other/transactions.rb index e66151979..381a434c7 100644 --- a/test/other/transactions.rb +++ b/test/other/transactions.rb @@ -1,395 +1,416 @@ require 'puppet' require 'puppettest' # $Id$ class TestTransactions < Test::Unit::TestCase include PuppetTest::FileTesting def test_reports path1 = tempfile() path2 = tempfile() objects = [] objects << Puppet::Type.newfile( :path => path1, :content => "yayness" ) objects << Puppet::Type.newfile( :path => path2, :content => "booness" ) trans = assert_events([:file_created, :file_created], *objects) report = nil assert_nothing_raised { report = trans.report } # First test the report logs assert(report.logs.length > 0, "Did not get any report logs") report.logs.each do |obj| assert_instance_of(Puppet::Log, obj) end # Then test the metrics metrics = report.metrics assert(metrics, "Did not get any metrics") assert(metrics.length > 0, "Did not get any metrics") assert(metrics.has_key?("objects"), "Did not get object metrics") assert(metrics.has_key?("changes"), "Did not get change metrics") metrics.each do |name, metric| assert_instance_of(Puppet::Metric, metric) end end + def test_refreshes_generate_events + path = tempfile() + firstpath = tempfile() + secondpath = tempfile() + file = Puppet::Type.newfile(:path => path, :content => "yayness") + first = Puppet::Type.newexec(:title => "first", + :command => "/bin/echo first > #{firstpath}", + :subscribe => [:file, path], + :refreshonly => true + ) + second = Puppet::Type.newexec(:title => "second", + :command => "/bin/echo second > #{secondpath}", + :subscribe => [:exec, "first"], + :refreshonly => true + ) + + assert_apply(file, first, second) + + assert(FileTest.exists?(secondpath), "Refresh did not generate an event") + end + unless %x{groups}.chomp.split(/ /).length > 1 $stderr.puts "You must be a member of more than one group to test transactions" else def ingroup(gid) require 'etc' begin group = Etc.getgrgid(gid) rescue => detail puts "Could not retrieve info for group %s: %s" % [gid, detail] return nil end return @groups.include?(group.name) end def setup super @groups = %x{groups}.chomp.split(/ /) unless @groups.length > 1 p @groups raise "You must be a member of more than one group to test this" end end def newfile(hash = {}) tmpfile = tempfile() File.open(tmpfile, "w") { |f| f.puts rand(100) } # XXX now, because os x apparently somehow allows me to make a file # owned by a group i'm not a member of, i have to verify that # the file i just created is owned by one of my groups # grrr unless ingroup(File.stat(tmpfile).gid) Puppet.info "Somehow created file in non-member group %s; fixing" % File.stat(tmpfile).gid require 'etc' firstgr = @groups[0] unless firstgr.is_a?(Integer) str = Etc.getgrnam(firstgr) firstgr = str.gid end File.chown(nil, firstgr, tmpfile) end hash[:name] = tmpfile assert_nothing_raised() { return Puppet.type(:file).create(hash) } end def newservice assert_nothing_raised() { return Puppet.type(:service).create( :name => "sleeper", :type => "init", :path => exampledir("root/etc/init.d"), :hasstatus => true, :check => [:ensure] ) } end def newexec(file) assert_nothing_raised() { return Puppet.type(:exec).create( :name => "touch %s" % file, :path => "/bin:/usr/bin:/sbin:/usr/sbin", :returns => 0 ) } end # modify a file and then roll the modifications back def test_filerollback transaction = nil file = newfile() states = {} check = [:group,:mode] file[:check] = check assert_nothing_raised() { file.retrieve } assert_nothing_raised() { check.each { |state| assert(file[state]) states[state] = file[state] } } component = newcomp("file",file) require 'etc' groupname = Etc.getgrgid(File.stat(file.name).gid).name assert_nothing_raised() { # Find a group that it's not set to group = @groups.find { |group| group != groupname } unless group raise "Could not find suitable group" end file[:group] = group file[:mode] = "755" } trans = assert_events([:file_changed, :file_changed], component) file.retrieve assert_rollback_events(trans, [:file_changed, :file_changed], "file") assert_nothing_raised() { file.retrieve } states.each { |state,value| assert_equal( value,file.is(state), "File %s remained %s" % [state, file.is(state)] ) } end # start a service, and then roll the modification back # Disabled, because it wasn't really worth the effort. def disabled_test_servicetrans transaction = nil service = newservice() component = newcomp("service",service) assert_nothing_raised() { service[:ensure] = 1 } service.retrieve assert(service.insync?, "Service did not start") system("ps -ef | grep ruby") trans = assert_events([:service_started], component) service.retrieve assert_rollback_events(trans, [:service_stopped], "service") end # test that services are correctly restarted and that work is done # in the right order def test_refreshing transaction = nil file = newfile() execfile = File.join(tmpdir(), "exectestingness") exec = newexec(execfile) states = {} check = [:group,:mode] file[:check] = check @@tmpfiles << execfile component = newcomp("both",file,exec) # 'subscribe' expects an array of arrays exec[:subscribe] = [[file.class.name,file.name]] exec[:refreshonly] = true assert_nothing_raised() { file.retrieve exec.retrieve } check.each { |state| states[state] = file[state] } assert_nothing_raised() { file[:mode] = "755" } - trans = assert_events([:file_changed], component) + trans = assert_events([:file_changed, :triggered], component) assert(FileTest.exists?(execfile), "Execfile does not exist") File.unlink(execfile) assert_nothing_raised() { file[:group] = @groups[1] } - trans = assert_events([:file_changed], component) + trans = assert_events([:file_changed, :triggered], component) assert(FileTest.exists?(execfile), "Execfile does not exist") end def test_refreshAcrossTwoComponents transaction = nil file = newfile() execfile = File.join(tmpdir(), "exectestingness2") @@tmpfiles << execfile exec = newexec(execfile) states = {} check = [:group,:mode] file[:check] = check fcomp = newcomp("file",file) ecomp = newcomp("exec",exec) component = newcomp("both",fcomp,ecomp) # 'subscribe' expects an array of arrays #component[:require] = [[file.class.name,file.name]] ecomp[:subscribe] = [[fcomp.class.name,fcomp.name]] exec[:refreshonly] = true trans = assert_events([], component) assert_nothing_raised() { file[:group] = @groups[1] file[:mode] = "755" } - trans = assert_events([:file_changed, :file_changed], component) + trans = assert_events([:file_changed, :file_changed, :triggered], component) end # Make sure that multiple subscriptions get triggered. def test_multisubs path = tempfile() file1 = tempfile() file2 = tempfile() file = Puppet.type(:file).create( :path => path, :ensure => "file" ) exec1 = Puppet.type(:exec).create( :path => ENV["PATH"], :command => "touch %s" % file1, :refreshonly => true, :subscribe => [:file, path] ) exec2 = Puppet.type(:exec).create( :path => ENV["PATH"], :command => "touch %s" % file2, :refreshonly => true, :subscribe => [:file, path] ) assert_apply(file, exec1, exec2) assert(FileTest.exists?(file1), "File 1 did not get created") assert(FileTest.exists?(file2), "File 2 did not get created") end # Make sure that a failed trigger doesn't result in other events not # getting triggered. def test_failedrefreshes path = tempfile() newfile = tempfile() file = Puppet.type(:file).create( :path => path, :ensure => "file" ) svc = Puppet.type(:service).create( :name => "thisservicedoesnotexist", :subscribe => [:file, path] ) exec = Puppet.type(:exec).create( :path => ENV["PATH"], :command => "touch %s" % newfile, :logoutput => true, :refreshonly => true, :subscribe => [:file, path] ) assert_apply(file, svc, exec) assert(FileTest.exists?(path), "File did not get created") assert(FileTest.exists?(newfile), "Refresh file did not get created") end # Make sure that unscheduled and untagged objects still respond to events def test_unscheduledanduntaggedresponse Puppet::Type.type(:schedule).mkdefaultschedules Puppet[:ignoreschedules] = false file = Puppet.type(:file).create( :name => tempfile(), :ensure => "file" ) fname = tempfile() exec = Puppet.type(:exec).create( :name => "touch %s" % fname, :path => "/usr/bin:/bin", :schedule => "monthly", :subscribe => ["file", file.name] ) comp = newcomp(file,exec) comp.finalize # Run it once assert_apply(comp) assert(FileTest.exists?(fname), "File did not get created") assert(!exec.scheduled?, "Exec is somehow scheduled") # Now remove it, so it can get created again File.unlink(fname) file[:content] = "some content" - assert_events([:file_changed], comp) + assert_events([:file_changed, :triggered], comp) assert(FileTest.exists?(fname), "File did not get recreated") # Now remove it, so it can get created again File.unlink(fname) # And tag our exec exec.tag("testrun") # And our file, so it runs file.tag("norun") Puppet[:tags] = "norun" file[:content] = "totally different content" assert(! file.insync?, "Uh, file is in sync?") - assert_events([:file_changed], comp) + assert_events([:file_changed, :triggered], comp) assert(FileTest.exists?(fname), "File did not get recreated") end def test_failed_reqs_mean_no_run exec = Puppet::Type.type(:exec).create( :command => "/bin/mkdir /this/path/cannot/possibly/exit", :title => "mkdir" ) file = Puppet::Type.type(:file).create( :path => tempfile(), :require => ["exec", "mkdir"], :ensure => :file ) comp = newcomp(exec, file) comp.finalize assert_apply(comp) assert(! FileTest.exists?(file[:path]), "File got created even tho its dependency failed") end end end diff --git a/test/types/exec.rb b/test/types/exec.rb index d51af1736..615dd86cd 100755 --- a/test/types/exec.rb +++ b/test/types/exec.rb @@ -1,581 +1,581 @@ require 'puppet' require 'puppettest' require 'facter' class TestExec < Test::Unit::TestCase include PuppetTest def test_execution command = nil output = nil assert_nothing_raised { command = Puppet.type(:exec).create( :command => "/bin/echo" ) } assert_nothing_raised { command.evaluate } assert_events([:executed_command], command) end def test_numvsstring [0, "0"].each { |val| Puppet.type(:exec).clear Puppet.type(:component).clear command = nil output = nil assert_nothing_raised { command = Puppet.type(:exec).create( :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(:exec).create( :command => "echo" ) } assert_nothing_raised { command = Puppet.type(:exec).create( :command => "echo", :path => "/usr/bin:/bin:/usr/sbin:/sbin" ) } Puppet.type(:exec).clear assert_nothing_raised { command = Puppet.type(:exec).create( :command => "/bin/echo" ) } Puppet.type(:exec).clear assert_nothing_raised { command = Puppet.type(:exec).create( :command => "/bin/echo", :path => "/usr/bin:/bin:/usr/sbin:/sbin" ) } end def test_nonzero_returns assert_nothing_raised { command = Puppet.type(:exec).create( :command => "mkdir /this/directory/does/not/exist", :path => "/usr/bin:/bin:/usr/sbin:/sbin", :returns => 1 ) } assert_nothing_raised { command = Puppet.type(:exec).create( :command => "touch /etc", :path => "/usr/bin:/bin:/usr/sbin:/sbin", :returns => 1 ) } assert_nothing_raised { command = Puppet.type(:exec).create( :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(:exec).create( :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 file = nil cmd = nil tmpfile = tempfile() @@tmpfiles.push tmpfile trans = nil File.open(tmpfile, File::WRONLY|File::CREAT|File::TRUNC) { |of| of.puts rand(100) } file = Puppet.type(:file).create( :path => tmpfile, :checksum => "md5" ) assert_instance_of(Puppet.type(:file), file) assert_nothing_raised { cmd = Puppet.type(:exec).create( :command => "pwd", :path => "/usr/bin:/bin:/usr/sbin:/sbin", :subscribe => [[file.class.name,file.name]], :refreshonly => true ) } assert_instance_of(Puppet.type(:exec), cmd) comp = Puppet.type(:component).create(:name => "RefreshTest") [file,cmd].each { |obj| comp.push obj } events = nil assert_nothing_raised { trans = comp.evaluate file.retrieve sum = file.state(:checksum) assert_equal(sum.is, sum.should) events = trans.evaluate.collect { |event| event.event } } # the first checksum shouldn't result in a changed file assert_equal([],events) File.open(tmpfile, File::WRONLY|File::CREAT|File::TRUNC) { |of| of.puts rand(100) of.puts rand(100) of.puts rand(100) } assert_nothing_raised { trans = comp.evaluate sum = file.state(:checksum) events = trans.evaluate.collect { |event| event.event } } # verify that only the file_changed event was kicked off, not the # command_executed assert_equal( - [:file_changed], + [:file_changed, :triggered], events ) end def test_creates file = tempfile() exec = nil assert(! FileTest.exists?(file), "File already exists") assert_nothing_raised { exec = Puppet.type(:exec).create( :command => "touch %s" % file, :path => "/usr/bin:/bin:/usr/sbin:/sbin", :creates => file ) } comp = newcomp("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(:file).create( :path => oexe, :source => exe, :mode => 0755 ) exec = Puppet.type(:exec).create( :command => oexe, :require => [:file, oexe] ) comp = newcomp("Testing", file, exec) assert_events([:file_created, :executed_command], comp) end # Verify that we auto-require any managed scripts. def test_autorequire exe = tempfile() oexe = tempfile() sh = %x{which sh} File.open(exe, "w") { |f| f.puts "#!#{sh}\necho yup" } file = Puppet.type(:file).create( :path => oexe, :source => exe, :mode => 755 ) basedir = File.dirname(oexe) baseobj = Puppet.type(:file).create( :path => basedir, :source => exe, :mode => 755 ) ofile = Puppet.type(:file).create( :path => exe, :mode => 755 ) exec = Puppet.type(:exec).create( :command => oexe, :path => ENV["PATH"], :cwd => basedir ) cat = Puppet.type(:exec).create( :command => "cat %s %s" % [exe, oexe], :path => ENV["PATH"] ) comp = newcomp(ofile, exec, cat, file, baseobj) comp.finalize # Verify we get the script itself assert(exec.requires?(file), "Exec did not autorequire %s" % file) # Verify we catch the cwd assert(exec.requires?(baseobj), "Exec did not autorequire cwd") # Verify we don't require ourselves assert(!exec.requires?(ofile), "Exec incorrectly required file") # Verify that we catch inline files # We not longer autorequire inline files assert(! cat.requires?(ofile), "Exec required second inline file") assert(! cat.requires?(file), "Exec required inline file") end def test_ifonly afile = tempfile() bfile = tempfile() exec = nil assert_nothing_raised { exec = Puppet.type(:exec).create( :command => "touch %s" % bfile, :onlyif => "test -f %s" % afile, :path => ENV['PATH'] ) } assert_events([], exec) system("touch %s" % afile) assert_events([:executed_command], exec) assert_events([:executed_command], exec) system("rm %s" % afile) assert_events([], exec) end def test_unless afile = tempfile() bfile = tempfile() exec = nil assert_nothing_raised { exec = Puppet.type(:exec).create( :command => "touch %s" % bfile, :unless => "test -f %s" % afile, :path => ENV['PATH'] ) } comp = newcomp(exec) assert_events([:executed_command], comp) assert_events([:executed_command], comp) system("touch %s" % afile) assert_events([], comp) assert_events([], comp) system("rm %s" % afile) assert_events([:executed_command], comp) assert_events([:executed_command], comp) end if Process.uid == 0 # Verify that we can execute commands as a special user def mknverify(file, user, group = nil, id = true) args = { :command => "touch %s" % file, :path => "/usr/bin:/bin:/usr/sbin:/sbin", } if user #Puppet.warning "Using user %s" % 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 %s" % 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(:exec).create(args) } comp = newcomp("usertest", exec) assert_events([:executed_command], comp, "usertest") assert(FileTest.exists?(file), "File does not exist") if user assert_equal(user.uid, File.stat(file).uid, "File UIDs do not match") end # 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(:exec).create( :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 warning" exec[:logoutput] = "warning" } assert_apply(exec) end def test_execthenfile exec = nil file = nil basedir = tempfile() path = File.join(basedir, "subfile") assert_nothing_raised { exec = Puppet.type(:exec).create( :title => "mkdir", :path => "/usr/bin:/bin", :creates => basedir, :command => "mkdir %s; touch %s" % [basedir, path] ) } assert_nothing_raised { file = Puppet.type(:file).create( :path => basedir, :recurse => true, :mode => "755", :require => ["exec", "mkdir"] ) } comp = newcomp(file, exec) comp.finalize assert_events([:executed_command, :file_changed], comp) assert(FileTest.exists?(path), "Exec ran first") assert(File.stat(path).mode & 007777 == 0755) end def test_falsevals exec = nil assert_nothing_raised do exec = Puppet.type(:exec).create( :command => "/bin/touch yayness" ) end Puppet.type(:exec).checks.each do |check| klass = Puppet.type(:exec).paramclass(check) next if klass.values.include? :false assert_raise(Puppet::Error, "Check %s did not fail on false" % check) do exec[check] = false end end end def test_createcwdandexe exec1 = exec2 = nil dir = tempfile() file = tempfile() assert_nothing_raised { exec1 = Puppet.type(:exec).create( :path => ENV["PATH"], :command => "mkdir #{dir}" ) } assert_nothing_raised("Could not create exec w/out existing cwd") { exec2 = Puppet.type(:exec).create( :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.state(:returns).sync end assert_nothing_raised do exec2[:require] = ["exec", exec1.name] exec2.finish 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(:exec).create( :path => ENV["PATH"], :command => "touch #{file}" ) } assert_nothing_raised { exec[:unless] = test } assert_nothing_raised { assert(exec.check, "Check did not pass") } assert_nothing_raised { exec[:unless] = [test, test] } assert_nothing_raised { exec.finish } assert_nothing_raised { assert(exec.check, "Check did not pass") } assert_apply(exec) assert_nothing_raised { assert(! exec.check, "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.newexec( :command => "echo true", :path => ENV["PATH"], :onlyif => "/bin/nosuchthingexists" ) assert_raise(ArgumentError, "Missing command did not raise error") { exec.run("/bin/nosuchthingexists") } end def test_envparam exec = Puppet::Type.newexec( :command => "echo $envtest", :path => ENV["PATH"], :env => "envtest=yayness" ) assert(exec, "Could not make exec") output = status = nil assert_nothing_raised { output, status = exec.run("echo $envtest") } assert_equal("yayness\n", output) # Now check whether we can do multiline settings assert_nothing_raised do exec[:env] = "envtest=a list of things and stuff" end output = status = nil assert_nothing_raised { output, status = exec.run('echo "$envtest"') } assert_equal("a list of things\nand stuff\n", output) # Now test arrays assert_nothing_raised do exec[:env] = ["funtest=A", "yaytest=B"] end output = status = nil assert_nothing_raised { output, status = exec.run('echo "$funtest" "$yaytest"') } assert_equal("A B\n", output) end end # $Id$