diff --git a/lib/puppet/pgraph.rb b/lib/puppet/pgraph.rb new file mode 100644 index 000000000..fba93d20c --- /dev/null +++ b/lib/puppet/pgraph.rb @@ -0,0 +1,170 @@ +#!/usr/bin/env ruby +# +# Created by Luke A. Kanies on 2006-11-24. +# Copyright (c) 2006. All rights reserved. + +require 'puppet/gratr/digraph' +require 'puppet/gratr/import' +require 'puppet/gratr/dot' +require 'puppet/relationship' + +# This class subclasses a graph class in order to handle relationships +# among resources. +class Puppet::PGraph < GRATR::Digraph + # Collect all of the targets for the list of events. Basically just iterates + # over the sources of the events and returns all of the targets of them. + def collect_targets(events) + events.collect do |event| + source = event.source + start = source + + # Get all of the edges that this vertex points at + adjacent(source, :direction => :out, :type => :edges).find_all do |edge| + edge.match?(event.event) + end.collect { |event| + target = event.target + if target.respond_to?(:ref) + source.info "Scheduling %s of %s" % + [event.callback, target.ref] + end + target + } + end.flatten + end + + # The dependencies for a given resource. + def dependencies(resource) + tree_from_vertex(resource, :dfs).keys + end + + # Override this method to use our class instead. + def edge_class() + Puppet::Relationship + end + + # Determine all of the leaf nodes below a given vertex. + def leaves(vertex, type = :dfs) + tree = tree_from_vertex(vertex, type) + leaves = tree.keys.find_all { |c| adjacent(c, :direction => :out).empty? } + return leaves + end + + # Take container information from another graph and use it + # to replace any container vertices with their respective leaves. + # This creates direct relationships where there were previously + # indirect relationships through the containers. + def splice!(other, type) + vertices.each do |vertex| + # Go through each vertex and replace the edges with edges + # to the leaves instead + next unless vertex.is_a?(type) + + leaves = other.leaves(vertex) + next if leaves.empty? + + # First create new edges for each of the :in edges + adjacent(vertex, :direction => :in).each do |up| + leaves.each do |leaf| + add_edge!(up, leaf) + if cyclic? + raise ArgumentError, + "%s => %s results in a loop" % + [up, leaf] + end + end + end + + # Then for each of the out edges + adjacent(vertex, :direction => :out).each do |down| + leaves.each do |leaf| + add_edge!(leaf, down) + if cyclic? + raise ArgumentError, + "%s => %s results in a loop" % + [leaf, down] + end + end + end + + # And finally, remove the vertex entirely. + remove_vertex!(vertex) + end + 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| + 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) + @resourcemetrics[:restarted] += 1 + rescue => detail + obj.err "Failed to call %s on %s: %s" % + [callback, obj, detail] + + @resourcemetrics[: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 to_jpg(name) + gv = vertices() + Dir.chdir("/Users/luke/Desktop/pics") do + induced_subgraph(gv).write_to_graphic_file('jpg', name) + end + end +end + +# $Id$ diff --git a/lib/puppet/relationship.rb b/lib/puppet/relationship.rb new file mode 100644 index 000000000..cbd15b2af --- /dev/null +++ b/lib/puppet/relationship.rb @@ -0,0 +1,51 @@ +#!/usr/bin/env ruby +# +# Created by Luke A. Kanies on 2006-11-24. +# Copyright (c) 2006. All rights reserved. + +require 'puppet/gratr' + +# subscriptions are permanent associations determining how different +# objects react to an event + +class Puppet::Relationship < GRATR::Edge + # Return the callback + def callback + label[:callback] + end + + # Return our event. + def event + label[:event] + end + + def initialize(source, target, label = nil) + if label + unless label.is_a?(Hash) + raise Puppet::DevError, "The label must be a hash" + end + + if label[:event] and label[:event] != :NONE and ! label[:callback] + raise Puppet::DevError, "You must pass a callback for non-NONE events" + end + else + label = {} + end + + super(source, target, label) + end + + # Does the passed event match our event? This is where the meaning + # of :NONE comes from. + def match?(event) + if event == :NONE or self.event == :NONE + return false + elsif self.event == :ALL_EVENTS or event == :ALL_EVENTS or event == self.event + return true + else + return false + end + end +end + +# $Id$ diff --git a/lib/puppet/transaction.rb b/lib/puppet/transaction.rb index a7ea48a19..1551acbb6 100644 --- a/lib/puppet/transaction.rb +++ b/lib/puppet/transaction.rb @@ -1,428 +1,432 @@ # 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 + if fails = failed?(dep) 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" @resourcemetrics[: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 @resourcemetrics[: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 @resourcemetrics[:applied] += 1 end events }.flatten.reject { |e| e.nil? } unless changes.empty? # Record when we last synced child.cache(:synced, Time.now) # Flush, if appropriate if child.respond_to?(:flush) child.flush end 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) prefetch() begin allevents = @objects.collect { |child| events = [] if (self.ignoretags or tags.nil? or child.tagged?(tags)) if self.ignoreschedules or child.scheduled? @resourcemetrics[: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 else child.debug "Not scheduled" end else child.debug "Not tagged with %s" % tags.join(", ") end # 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 + if @failures[obj] > 0 + return @failures[obj] + else + return false + end end # this should only be called by a Puppet::Container object now # and it should only receive an array def initialize(objects) @objects = objects @resourcemetrics = { :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 # Prefetch any providers that support it. We don't support prefetching # types, just providers. def prefetch @objects.collect { |obj| if pro = obj.provider pro.class else nil end }.reject { |o| o.nil? }.uniq.each do |klass| # XXX We need to do something special here in case of failure. if klass.respond_to?(:prefetch) klass.prefetch end end end # Generate a transaction report. def report @resourcemetrics[: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(:resources, @resourcemetrics) # 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| 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) @resourcemetrics[:restarted] += 1 rescue => detail obj.err "Failed to call %s on %s: %s" % [callback, obj, detail] @resourcemetrics[: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/lib/puppet/type/component.rb b/lib/puppet/type/component.rb index c159ccc83..3165d9e11 100644 --- a/lib/puppet/type/component.rb +++ b/lib/puppet/type/component.rb @@ -1,159 +1,178 @@ # the object allowing us to build complex structures # this thing contains everything else, including itself require 'puppet' require 'puppet/type' require 'puppet/transaction' +require 'puppet/pgraph' module Puppet newtype(:component) do include Enumerable newparam(:name) do desc "The name of the component. Generally optional." isnamevar end newparam(:type) do desc "The type that this component maps to. Generally some kind of class from the language." defaultto "component" end # topo sort functions def self.sort(objects) list = [] tmplist = {} objects.each { |obj| self.recurse(obj, tmplist, list) } return list.flatten end # FIXME this method assumes that dependencies themselves # are never components def self.recurse(obj, inlist, list) if inlist.include?(obj.object_id) return end inlist[obj.object_id] = true begin obj.eachdependency { |req| self.recurse(req, inlist, list) } rescue Puppet::Error => detail raise Puppet::Error, "%s: %s" % [obj.path, detail] end if obj.is_a? self obj.each { |child| self.recurse(child, inlist, list) } else list << obj end end # Remove a child from the component. def delete(child) if @children.include?(child) @children.delete(child) return true else return false end end # Return each child in turn. def each @children.each { |child| yield child } end # flatten all children, sort them, and evaluate them in order # this is only called on one component over the whole system # this also won't work with scheduling, but eh def evaluate self.finalize unless self.finalized? transaction = Puppet::Transaction.new(self.flatten) transaction.component = self return transaction end # Do all of the polishing off, mostly doing autorequires and making # dependencies. This will get run once on the top-level component, # and it will do everything necessary. def finalize started = {} finished = {} # First do all of the finish work, which mostly involves self.delve do |object| # Make sure we don't get into loops if started.has_key?(object) debug "Already finished %s" % object.title next else started[object] = true end unless finished.has_key?(object) object.finish object.builddepends finished[object] = true end end @finalized = true end def finalized? if defined? @finalized return @finalized else return false end end # Return a flattened array containing all of the children # and all child components' children, sorted in order of dependencies. def flatten self.class.sort(@children).flatten end # Initialize a new component def initialize(args) @children = [] super(args) end # We have a different way of setting the title def title unless defined? @title if self[:type] == self[:name] or self[:name] =~ /--\d+$/ @title = self[:type] else @title = "%s[%s]" % [self[:type],self[:name]] end end return @title end def refresh @children.collect { |child| if child.respond_to?(:refresh) child.refresh child.log "triggering %s" % :refresh end } end + + # Convert to a graph object with all of the container info. + def to_graph + graph = Puppet::PGraph.new + + delver = proc do |obj| + obj.each do |child| + if child.is_a?(Puppet::Type) + graph.add_edge!(obj, child) + delver.call(child) + end + end + end + + delver.call(self) + + return graph + end def to_s return "component(%s)" % self.title end end end # $Id$ diff --git a/lib/puppet/util/graph.rb b/lib/puppet/util/graph.rb index c32d15cb4..d9e122c7b 100644 --- a/lib/puppet/util/graph.rb +++ b/lib/puppet/util/graph.rb @@ -1,58 +1,39 @@ # Created by Luke Kanies on 2006-11-16. # Copyright (c) 2006. All rights reserved. require 'puppet' -require 'puppet/gratr/digraph' -require 'puppet/gratr/import' -require 'puppet/gratr/dot' - -class GRATR::Digraph - # Determine all of the leaf nodes below a given vertex. - def leaves(vertex, type = :dfs) - tree = tree_from_vertex(vertex, type) - leaves = tree.keys.find_all { |c| adjacent(c, :direction => :out).empty? } - return leaves - end -end +require 'puppet/pgraph' # A module that handles the small amount of graph stuff in Puppet. module Puppet::Util::Graph # Make a graph where each of our children gets converted to # the receiving end of an edge. Call the same thing on all # of our children, optionally using a block def to_graph(graph = nil, &block) # Allow our calling function to send in a graph, so that we # can call this recursively with one graph. - graph ||= GRATR::Digraph.new + graph ||= Puppet::PGraph.new self.each do |child| unless block_given? and ! yield(child) graph.add_edge!(self, child) if graph.cyclic? raise Puppet::Error, "%s created a cyclic graph" % self end if child.respond_to?(:to_graph) child.to_graph(graph, &block) end end end if graph.cyclic? raise Puppet::Error, "%s created a cyclic graph" % self end graph end - - def to_jpg(graph = nil) - graph ||= to_graph - gv = graph.vertices - Dir.chdir("/Users/luke/Desktop/pics") do - graph.induced_subgraph(gv).write_to_graphic_file('jpg', 'graph') - end - end end # $Id$ \ No newline at end of file diff --git a/test/lib/puppettest.rb b/test/lib/puppettest.rb index 05d59a3a7..6e3d8cd4c 100755 --- a/test/lib/puppettest.rb +++ b/test/lib/puppettest.rb @@ -1,239 +1,241 @@ require 'puppet' require 'test/unit' # Yay; hackish but it works if ARGV.include?("-d") Puppet.debug = true end module PuppetTest # Find the root of the Puppet tree; this is not the test directory, but # the parent of that dir. def basedir(*list) unless defined? @@basedir - case $0 - when /rake_test_loader/ + case + when $0 =~ /rake_test_loader/ @@basedir = File.dirname(Dir.getwd) + when ENV['BASEDIR'] + @@basedir = ENV['BASEDIR'] else dir = nil app = $0.sub /^\.\//, "" if app =~ /^#{File::SEPARATOR}.+\.rb/ dir = app else dir = File.join(Dir.getwd, app) end 3.times { dir = File.dirname(dir) } @@basedir = dir end end if list.empty? @@basedir else File.join(@@basedir, *list) end end def cleanup(&block) @@cleaners << block end def datadir(*list) File.join(basedir, "test", "data", *list) end def exampledir(*args) unless defined? @@exampledir @@exampledir = File.join(basedir, "examples") end if args.empty? return @@exampledir else return File.join(@@exampledir, *args) end end module_function :basedir, :datadir, :exampledir # Rails clobbers RUBYLIB, thanks def libsetup curlibs = ENV["RUBYLIB"].split(":") $:.reject do |dir| dir =~ /^\/usr/ end.each do |dir| unless curlibs.include?(dir) curlibs << dir end end ENV["RUBYLIB"] = curlibs.join(":") end def rake? $0 =~ /rake_test_loader/ end def setup @memoryatstart = Puppet::Util.memory if defined? @@testcount @@testcount += 1 else @@testcount = 0 end @configpath = File.join(tmpdir, self.class.to_s + "configdir" + @@testcount.to_s + "/" ) unless defined? $user and $group $user = nonrootuser().uid.to_s $group = nonrootgroup().gid.to_s end Puppet.config.clear Puppet[:user] = $user Puppet[:group] = $group Puppet[:confdir] = @configpath Puppet[:vardir] = @configpath unless File.exists?(@configpath) Dir.mkdir(@configpath) end @@tmpfiles = [@configpath, tmpdir()] @@tmppids = [] @@cleaners = [] # If we're running under rake, then disable debugging and such. if rake? and ! Puppet[:debug] Puppet::Log.close Puppet::Log.newdestination tempfile() Puppet[:httplog] = tempfile() else if textmate? Puppet[:color] = false end Puppet::Log.newdestination :console Puppet::Log.level = :debug #$VERBOSE = 1 Puppet.info @method_name Puppet[:trace] = true end Puppet[:ignoreschedules] = true end def tempfile if defined? @@tmpfilenum @@tmpfilenum += 1 else @@tmpfilenum = 1 end f = File.join(self.tmpdir(), self.class.to_s + "_" + @method_name.to_s + @@tmpfilenum.to_s) @@tmpfiles << f return f end def textmate? if ENV["TM_FILENAME"] return true else return false end end def tstdir dir = tempfile() Dir.mkdir(dir) return dir end def tmpdir unless defined? @tmpdir and @tmpdir @tmpdir = case Facter["operatingsystem"].value when "Darwin": "/private/tmp" when "SunOS": "/var/tmp" else "/tmp" end @tmpdir = File.join(@tmpdir, "puppettesting") unless File.exists?(@tmpdir) FileUtils.mkdir_p(@tmpdir) File.chmod(01777, @tmpdir) end end @tmpdir end def teardown stopservices @@cleaners.each { |cleaner| cleaner.call() } @@tmpfiles.each { |file| unless file =~ /tmp/ puts "Not deleting tmpfile %s" % file next end if FileTest.exists?(file) system("chmod -R 755 %s" % file) system("rm -rf %s" % file) end } @@tmpfiles.clear @@tmppids.each { |pid| %x{kill -INT #{pid} 2>/dev/null} } @@tmppids.clear Puppet::Type.allclear Puppet::Storage.clear if defined? Puppet::Rails Puppet::Rails.clear end Puppet.clear @memoryatend = Puppet::Util.memory diff = @memoryatend - @memoryatstart if diff > 1000 Puppet.info "%s#%s memory growth (%s to %s): %s" % [self.class, @method_name, @memoryatstart, @memoryatend, diff] end # reset all of the logs Puppet::Log.close # Just in case there are processes waiting to die... require 'timeout' begin Timeout::timeout(5) do Process.waitall end rescue Timeout::Error # just move on end if File.stat("/dev/null").mode & 007777 != 0666 File.open("/tmp/nullfailure", "w") { |f| f.puts self.class } exit(74) end end end require 'puppettest/support' require 'puppettest/filetesting' require 'puppettest/fakes' require 'puppettest/exetest' require 'puppettest/parsertesting' require 'puppettest/servertest' # $Id$ diff --git a/test/lib/puppettest/graph.rb b/test/lib/puppettest/graph.rb new file mode 100644 index 000000000..a6d0069cc --- /dev/null +++ b/test/lib/puppettest/graph.rb @@ -0,0 +1,40 @@ +#!/usr/bin/env ruby +# +# Created by Luke A. Kanies on 2006-11-24. +# Copyright (c) 2006. All rights reserved. + +require 'puppet/util/graph' + +class Container + include Puppet::Util::Graph + include Enumerable + attr_accessor :name + def each + @children.each do |c| yield c end + end + + def initialize(name, ary) + @name = name + @children = ary + end + + def push(*ary) + ary.each { |c| @children.push(c)} + end + + def to_s + @name + end +end + +module PuppetTest::Graph + def build_tree + one = Container.new("one", %w{a b}) + two = Container.new("two", ["c", "d"]) + middle = Container.new("middle", ["e", "f", two]) + top = Container.new("top", ["g", "h", middle, one]) + return one, two, middle, top + end +end + +# $Id$ \ No newline at end of file diff --git a/test/other/pgraph.rb b/test/other/pgraph.rb new file mode 100644 index 000000000..c3baa7722 --- /dev/null +++ b/test/other/pgraph.rb @@ -0,0 +1,77 @@ +#!/usr/bin/env ruby +# +# Created by Luke Kanies on 2006-11-16. +# Copyright (c) 2006. All rights reserved. + +$:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ + +require 'puppettest' +require 'puppettest/graph' + +class TestPGraph < Test::Unit::TestCase + include PuppetTest + include PuppetTest::Graph + + def test_collect_targets + graph = Puppet::PGraph.new + + event = Puppet::Event.new(:source => "a", :event => :yay) + none = Puppet::Event.new(:source => "a", :event => :NONE) + + graph.add_edge!("a", "b", :event => :yay) + + # Try it for the trivial case of one target and a matching event + assert_equal(["b"], graph.collect_targets([event])) + + # Make sure we get nothing with a different event + assert_equal([], graph.collect_targets([none])) + + # Set up multiple targets and make sure we get them all back + graph.add_edge!("a", "c", :event => :yay) + assert_equal(["b", "c"].sort, graph.collect_targets([event]).sort) + assert_equal([], graph.collect_targets([none])) + end + + def test_dependencies + graph = Puppet::PGraph.new + + graph.add_edge!("a", "b") + graph.add_edge!("a", "c") + graph.add_edge!("b", "d") + + assert_equal(%w{b c d}.sort, graph.dependencies("a").sort) + assert_equal(%w{d}.sort, graph.dependencies("b").sort) + assert_equal([].sort, graph.dependencies("c").sort) + end + + # Test that we can take a containment graph and rearrange it by dependencies + def test_splice + one, two, middle, top = build_tree + contgraph = top.to_graph + + # Now make a dependency graph + deps = Puppet::PGraph.new + + contgraph.vertices.each do |v| + deps.add_vertex(v) + end + + {one => two, "f" => "c", "h" => middle}.each do |source, target| + deps.add_edge!(source, target) + end + + deps.to_jpg("deps-before") + + deps.splice!(contgraph, Container) + + assert(! deps.cyclic?, "Created a cyclic graph") + + nons = deps.vertices.find_all { |v| ! v.is_a?(String) } + assert(nons.empty?, + "still contain non-strings %s" % nons.inspect) + + deps.to_jpg("deps-after") + end +end + +# $Id$ \ No newline at end of file diff --git a/test/other/relationship.rb b/test/other/relationship.rb new file mode 100644 index 000000000..c6f8eb763 --- /dev/null +++ b/test/other/relationship.rb @@ -0,0 +1,48 @@ +#!/usr/bin/env ruby +# +# Created by Luke A. Kanies on 2006-11-24. +# Copyright (c) 2006. All rights reserved. + +$:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ + +require 'puppettest' +require 'puppet/relationship' + +class TestRelationship < Test::Unit::TestCase + include PuppetTest + + def test_initialize + rel = Puppet::Relationship + + [["source", "target", "label"], + ["source", "target", {:event => :nothing}] + ].each do |ary| + # Make sure the label is required + assert_raise(Puppet::DevError) do + rel.new(*ary) + end + end + end + + def test_attributes + rel = Puppet::Relationship + + i = nil + assert_nothing_raised do + i = rel.new "source", "target", :event => :yay, :callback => :boo + end + + assert_equal(:yay, i.event, "event was not retrieved") + assert_equal(:boo, i.callback, "callback was not retrieved") + + # Now try it with nil values + assert_nothing_raised("failed to create with no event or callback") { + i = rel.new "source", "target" + } + + assert_nil(i.event, "event was not nil") + assert_nil(i.callback, "callback was not nil") + end +end + +# $Id$ \ No newline at end of file diff --git a/test/other/transactions.rb b/test/other/transactions.rb index ee4b901cb..985e9a0c5 100755 --- a/test/other/transactions.rb +++ b/test/other/transactions.rb @@ -1,463 +1,465 @@ #!/usr/bin/env ruby $:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ 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?("resources"), "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_prefetch # Create a type just for testing prefetch name = :prefetchtesting $prefetched = false type = Puppet::Type.newtype(name) do newparam(:name) {} end # Now create a provider type.provide(:prefetch) do def self.prefetch $prefetched = true end end # Now create an instance inst = type.create :name => "yay" # Create a transaction trans = Puppet::Transaction.new([inst]) # Make sure prefetch works assert_nothing_raised do trans.prefetch end assert_equal(true, $prefetched, "type prefetch was not called") # Now make sure it gets called from within evaluate() $prefetched = false assert_nothing_raised do trans.evaluate end assert_equal(true, $prefetched, "evaluate did not call prefetch") 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 file[:group] = @groups[0] assert_apply(file) @@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, :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, :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 file[:group] = @groups[0] assert_apply(file) 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, :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, :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, :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 + +# $Id$ \ No newline at end of file diff --git a/test/types/component.rb b/test/types/component.rb index 78f184eed..28d6748a0 100755 --- a/test/types/component.rb +++ b/test/types/component.rb @@ -1,295 +1,344 @@ #!/usr/bin/env ruby $:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'puppet' require 'puppettest' # $Id$ class TestComponent < Test::Unit::TestCase include PuppetTest def setup super @@used = {} + @type = Puppet::Type::Component + @file = Puppet::Type.type(:file) end def randnum(limit) num = nil looped = 0 loop do looped += 1 if looped > 2000 raise "Reached limit of looping" break end num = rand(limit) unless @@used.include?(num) @@used[num] = true break end end num end def mkfile(num = nil) unless num num = randnum(1000) end name = tempfile() + num.to_s file = Puppet.type(:file).create( :path => name, :checksum => "md5" ) @@tmpfiles << name file end def mkcomp Puppet.type(:component).create(:name => "component_" + randnum(1000).to_s) end def mkrandcomp(numfiles, numdivs) comp = mkcomp hash = {} found = 0 divs = {} numdivs.times { |i| num = i + 2 divs[num] = nil } while found < numfiles num = randnum(numfiles) found += 1 f = mkfile(num) hash[f.name] = f reqd = [] divs.each { |n,obj| if rand(50) % n == 0 if obj unless reqd.include?(obj.object_id) f[:require] = [[obj.class.name, obj.name]] reqd << obj.object_id end end end divs[n] = f } end hash.each { |name, obj| comp.push obj } comp.finalize comp end def test_ordering list = nil comp = mkrandcomp(30,5) assert_nothing_raised { list = comp.flatten } list.each_with_index { |obj, index| obj.eachdependency { |dep| assert(list.index(dep) < index) } } end + + def treefile(name) + @file.create :path => "/tmp/#{name}", :mode => 0755 + end + + def treecomp(name) + @type.create :name => name, :type => "yay" + end + + def treenode(name, *children) + comp = treecomp name + children.each do |c| + if c.is_a?(String) + comp.push treefile(c) + else + comp.push c + end + end + return comp + end + + def mktree + one = treenode("one", "a", "b") + two = treenode("two", "c", "d") + middle = treenode("middle", "e", "f", two) + top = treenode("top", "g", "h", middle, one) + + return one, two, middle, top + end + + def test_to_graph + one, two, middle, top = mktree + + graph = nil + assert_nothing_raised do + graph = top.to_graph + end + + assert(graph.is_a?(Puppet::PGraph), "result is not a pgraph") + + [one, two, middle, top].each do |comp| + comp.each do |child| + assert(graph.edge?(comp, child), + "Did not create edge from %s => %s" % [comp.name, child.name]) + end + end + end def test_correctsorting tmpfile = tempfile() @@tmpfiles.push tmpfile trans = nil cmd = 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_nothing_raised { cmd = Puppet.type(:exec).create( :command => "pwd", :path => "/usr/bin:/bin:/usr/sbin:/sbin", :subscribe => [[file.class.name,file.name]], :refreshonly => true ) } order = nil assert_nothing_raised { order = Puppet.type(:component).sort([file, cmd]) } [cmd, file].each { |obj| assert_equal(1, order.find_all { |o| o.name == obj.name }.length) } end def test_correctflattening tmpfile = tempfile() @@tmpfiles.push tmpfile trans = nil cmd = 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_nothing_raised { cmd = Puppet.type(:exec).create( :command => "pwd", :path => "/usr/bin:/bin:/usr/sbin:/sbin", :subscribe => [[file.class.name,file.name]], :refreshonly => true ) } comp = newcomp(cmd, file) comp.finalize objects = nil assert_nothing_raised { objects = comp.flatten } [cmd, file].each { |obj| assert_equal(1, objects.find_all { |o| o.name == obj.name }.length) } assert(objects[0] == file, "File was not first object") assert(objects[1] == cmd, "Exec was not second object") end def test_deepflatten tmpfile = tempfile() @@tmpfiles.push tmpfile trans = nil cmd = 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_nothing_raised { cmd = Puppet.type(:exec).create( :command => "pwd", :path => "/usr/bin:/bin:/usr/sbin:/sbin", :refreshonly => true ) } fcomp = newcomp("fflatten", file) ecomp = newcomp("eflatten", cmd) # this subscription can screw up the sorting ecomp[:subscribe] = [[fcomp.class.name,fcomp.name]] comp = newcomp("bflatten", ecomp, fcomp) comp.finalize objects = nil assert_nothing_raised { objects = comp.flatten } assert_equal(objects.length, 2, "Did not get two sorted objects") objects.each { |o| assert(o.is_a?(Puppet::Type), "Object %s is not a Type" % o.class) } assert(objects[0] == file, "File was not first object") assert(objects[1] == cmd, "Exec was not second object") end def test_deepflatten2 tmpfile = tempfile() @@tmpfiles.push tmpfile trans = nil cmd = 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_nothing_raised { cmd = Puppet.type(:exec).create( :command => "pwd", :path => "/usr/bin:/bin:/usr/sbin:/sbin", :refreshonly => true ) } ocmd = nil assert_nothing_raised { ocmd = Puppet.type(:exec).create( :command => "echo true", :path => "/usr/bin:/bin:/usr/sbin:/sbin", :refreshonly => true ) } fcomp = newcomp("fflatten", file) ecomp = newcomp("eflatten", cmd) ocomp = newcomp("oflatten", ocmd) # this subscription can screw up the sorting cmd[:subscribe] = [[fcomp.class.name,fcomp.name]] ocmd[:subscribe] = [[cmd.class.name,cmd.name]] comp = newcomp("bflatten", ocomp, ecomp, fcomp) comp.finalize objects = nil assert_nothing_raised { objects = comp.flatten } assert_equal(objects.length, 3, "Did not get three sorted objects") objects.each { |o| assert(o.is_a?(Puppet::Type), "Object %s is not a Type" % o.class) } assert(objects[0] == file, "File was not first object") assert(objects[1] == cmd, "Exec was not second object") assert(objects[2] == ocmd, "Other exec was not second object") end def test_moreordering dir = tempfile() comp = Puppet.type(:component).create( :name => "ordertesting" ) 10.times { |i| fileobj = Puppet.type(:file).create( :path => File.join(dir, "file%s" % i), :ensure => "file" ) comp.push(fileobj) } dirobj = Puppet.type(:file).create( :path => dir, :ensure => "directory" ) comp.push(dirobj) assert_apply(comp) end end diff --git a/test/util/graph.rb b/test/util/graph.rb index 0a331f0e9..1df294c77 100755 --- a/test/util/graph.rb +++ b/test/util/graph.rb @@ -1,135 +1,109 @@ #!/usr/bin/env ruby # # Created by Luke Kanies on 2006-11-16. # Copyright (c) 2006. All rights reserved. $:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'puppettest' +require 'puppettest/graph' require 'puppet/util/graph' class TestUtilGraph < Test::Unit::TestCase include PuppetTest - - class Container - include Puppet::Util::Graph - include Enumerable - attr_accessor :name - def each - @children.each do |c| yield c end - end - - def initialize(name, ary) - @name = name - @children = ary - end - - def push(*ary) - ary.each { |c| @children.push(c)} - end - - def to_s - @name - end - end + include PuppetTest::Graph def test_to_graph children = %w{a b c d} list = Container.new("yay", children) graph = nil assert_nothing_raised do graph = list.to_graph end assert(graph.vertices.include?(list), "wtf?") ([list] + children).each do |thing| assert(graph.vertex?(thing), "%s is not a vertex" % thing) end children.each do |child| assert(graph.edge?(list, child), "%s/%s was not added as an edge" % ["yay", child]) end end def test_recursive_to_graph - one = Container.new("one", %w{a b}) - - two = Container.new("two", ["c", "d"]) - - middle = Container.new("middle", ["e", "f", two]) - - top = Container.new("top", ["g", "h", middle, one]) + one, two, middle, top = build_tree graph = nil assert_nothing_raised do graph = top.to_graph end (%w{a b c d e f g h} + [one, two, middle, top]).each do |v| assert(graph.vertex?(v), "%s is not a vertex" % v) end [one, two, middle, top].each do |con| con.each do |child| assert(graph.edge?(con, child), "%s/%s is not an edge" % [con, child]) end end - top.to_jpg(graph) + graph.to_jpg("graph") # Now make sure we correctly retrieve the leaves from each container {top => %w{a b c d e f g h}, one => %w{a b}, two => %w{c d}, middle => %w{c d e f}}.each do |cont, list| leaves = nil assert_nothing_raised do leaves = graph.leaves(cont) end leaves = leaves.sort assert_equal(list.sort, leaves.sort, "Got incorrect leaf list for %s" % cont.name) %w{a b c d e f g h}.each do |letter| unless list.include?(letter) assert(!leaves.include?(letter), "incorrectly got %s as a leaf of %s" % [letter, cont.to_s]) end end end end def test_to_graph_with_block middle = Container.new "middle", ["c", "d", 3, 4] top = Container.new "top", ["a", "b", middle, 1, 2] graph = nil assert_nothing_raised() { graph = top.to_graph { |c| c.is_a?(String) or c.is_a?(Container) } } %w{a b c d}.each do |child| assert(graph.vertex?(child), "%s was not added as a vertex" % child) end [1, 2, 3, 4].each do |child| assert(! graph.vertex?(child), "%s is a vertex" % child) end end def test_cyclic_graphs one = Container.new "one", %w{a b} two = Container.new "two", %w{c d} one.push(two) two.push(one) assert_raise(Puppet::Error, "did not fail on cyclic graph") do one.to_graph end end end # $Id$ \ No newline at end of file