diff --git a/lib/puppet/metatype/container.rb b/lib/puppet/metatype/container.rb index 3f970b7d9..9ed587a4c 100644 --- a/lib/puppet/metatype/container.rb +++ b/lib/puppet/metatype/container.rb @@ -1,116 +1,96 @@ class Puppet::Type attr_accessor :children # this is a retarded hack method to get around the difference between # component children and file children def self.depthfirst? if defined? @depthfirst return @depthfirst else return false end end def parent=(parent) if self.parentof?(parent) devfail "%s[%s] is already the parent of %s[%s]" % [self.class.name, self.title, parent.class.name, parent.title] end @parent = parent end # Add a hook for testing for recursion. def parentof?(child) if (self == child) debug "parent is equal to child" return true elsif defined? @parent and @parent.parentof?(child) debug "My parent is parent of child" return true elsif @children.include?(child) debug "child is already in children array" return true else return false end end def push(*childs) unless defined? @children @children = [] end childs.each { |child| # Make sure we don't have any loops here. if parentof?(child) devfail "Already the parent of %s[%s]" % [child.class.name, child.title] end unless child.is_a?(Puppet::Element) self.debug "Got object of type %s" % child.class self.devfail( "Containers can only contain Puppet::Elements, not %s" % child.class ) end @children.push(child) child.parent = self } end # Remove an object. The argument determines whether the object's # subscriptions get eliminated, too. def remove(rmdeps = true) # Our children remove themselves from our @children array (else the object # we called this on at the top would not be removed), so we duplicate the # array and iterate over that. If we don't do this, only half of the # objects get removed. @children.dup.each { |child| child.remove(rmdeps) } @children.clear # This is hackish (mmm, cut and paste), but it works for now, and it's # better than warnings. [@states, @parameters, @metaparams].each do |hash| hash.each do |name, obj| obj.remove end hash.clear end - - if rmdeps - Puppet::Event::Subscription.dependencies(self).each { |dep| - #info "Deleting dependency %s" % dep - #begin - # self.unsubscribe(dep) - #rescue - # # ignore failed unsubscribes - #end - dep.delete - } - Puppet::Event::Subscription.subscribers(self).each { |dep| - #info "Unsubscribing from %s" % dep - begin - dep.unsubscribe(self) - rescue - # ignore failed unsubscribes - end - } - end self.class.delete(self) if defined? @parent and @parent @parent.delete(self) @parent = nil end # Remove the reference to the provider. if self.provider @provider.clear @provider = nil end end end # $Id$ diff --git a/lib/puppet/metatype/manager.rb b/lib/puppet/metatype/manager.rb index 996d9a3cc..d2749b87d 100644 --- a/lib/puppet/metatype/manager.rb +++ b/lib/puppet/metatype/manager.rb @@ -1,145 +1,153 @@ require 'puppet' require 'puppet/util/classgen' # Methods dealing with Type management. This module gets included into the # Puppet::Type class, it's just split out here for clarity. module Puppet::MetaType module Manager include Puppet::Util::ClassGen # remove all type instances; this is mostly only useful for testing def allclear Puppet::Event::Subscription.clear @types.each { |name, type| type.clear } end # iterate across all of the subclasses of Type def eachtype @types.each do |name, type| # Only consider types that have names #if ! type.parameters.empty? or ! type.validstates.empty? yield type #end end end # Load all types. Only currently used for documentation. def loadall typeloader.loadall end # Do an on-demand plugin load def loadplugin(name) paths = Puppet[:pluginpath].split(":") unless paths.include?(Puppet[:plugindest]) Puppet.notice "Adding plugin destination %s to plugin search path" % Puppet[:plugindest] Puppet[:pluginpath] += ":" + Puppet[:plugindest] end paths.each do |dir| file = ::File.join(dir, name.to_s + ".rb") if FileTest.exists?(file) begin load file Puppet.info "loaded %s" % file return true rescue LoadError => detail Puppet.info "Could not load plugin %s: %s" % [file, detail] return false end end end end # Define a new type. def newtype(name, parent = nil, &block) # First make sure we don't have a method sitting around name = symbolize(name) newmethod = "new#{name.to_s}" # Used for method manipulation. selfobj = metaclass() @types ||= {} if @types.include?(name) if self.respond_to?(newmethod) # Remove the old newmethod selfobj.send(:remove_method,newmethod) end end # Then create the class. klass = genclass(name, :parent => (parent || Puppet::Type), :overwrite => true, :hash => @types, &block ) # Now define a "new" method for convenience. if self.respond_to? newmethod # Refuse to overwrite existing methods like 'newparam' or 'newtype'. Puppet.warning "'new#{name.to_s}' method already exists; skipping" else selfobj.send(:define_method, newmethod) do |*args| klass.create(*args) end end # If they've got all the necessary methods defined and they haven't # already added the state, then do so now. if klass.ensurable? and ! klass.validstate?(:ensure) klass.ensurable end # Now set up autoload any providers that might exist for this type. klass.providerloader = Puppet::Autoload.new(klass, "puppet/provider/#{klass.name.to_s}" ) # We have to load everything so that we can figure out the default type. klass.providerloader.loadall() klass end + + # Remove an existing defined type. Largely used for testing. + def rmtype(name) + # Then create the class. + klass = rmclass(name, + :hash => @types + ) + end # Return a Type instance by name. def type(name) @types ||= {} name = symbolize(name) if t = @types[name] return t else if typeloader.load(name) unless @types.include? name Puppet.warning "Loaded puppet/type/#{name} but no class was created" end else # If we can't load it from there, try loading it as a plugin. loadplugin(name) end return @types[name] end end # Create a loader for Puppet types. def typeloader unless defined? @typeloader @typeloader = Puppet::Autoload.new(self, "puppet/type", :wrap => false ) end @typeloader end end end # $Id$ diff --git a/lib/puppet/metatype/metaparams.rb b/lib/puppet/metatype/metaparams.rb index e09bbc6d5..f28103a87 100644 --- a/lib/puppet/metatype/metaparams.rb +++ b/lib/puppet/metatype/metaparams.rb @@ -1,352 +1,355 @@ require 'puppet' require 'puppet/type' class Puppet::Type # Add all of the meta parameters. #newmetaparam(:onerror) do # desc "How to handle errors -- roll back innermost # transaction, roll back entire transaction, ignore, etc. Currently # non-functional." #end newmetaparam(:noop) do desc "Boolean flag indicating whether work should actually be done. *true*/**false**" munge do |noop| if noop == "true" or noop == true return true elsif noop == "false" or noop == false return false else self.fail("Invalid noop value '%s'" % noop) end end end newmetaparam(:schedule) do desc "On what schedule the object should be managed. You must create a schedule object, and then reference the name of that object to use that for your schedule: schedule { daily: period => daily, range => \"2-4\" } exec { \"/usr/bin/apt-get update\": schedule => daily } The creation of the schedule object does not need to appear in the configuration before objects that use it." munge do |name| if schedule = Puppet.type(:schedule)[name] return schedule else return name end end end newmetaparam(:check) do desc "States which should have their values retrieved but which should not actually be modified. This is currently used internally, but will eventually be used for querying, so that you could specify that you wanted to check the install state of all packages, and then query the Puppet client daemon to get reports on all packages." munge do |args| # If they've specified all, collect all known states if args == :all args = @parent.class.states.collect do |state| state.name end end unless args.is_a?(Array) args = [args] end unless defined? @parent self.devfail "No parent for %s, %s?" % [self.class, self.name] end args.each { |state| unless state.is_a?(Symbol) state = state.intern end next if @parent.statedefined?(state) stateklass = @parent.class.validstate?(state) unless stateklass raise Puppet::Error, "%s is not a valid attribute for %s" % [state, self.class.name] end next unless stateklass.checkable? @parent.newstate(state) } end end + + # We've got four relationship metaparameters, so this method is used + # to reduce code duplication between them. + def store_relationship(param, values) + # We need to support values passed in as an array or as a + # resource reference. + result = [] + + # 'values' could be an array or a reference. If it's an array, + # it could be an array of references or an array of arrays. + if values.is_a?(Puppet::Type) + result << [values.class.name, values.title] + else + unless values.is_a?(Array) + devfail "Relationships must be resource references" + end + if values[0].is_a?(String) or values[0].is_a?(Symbol) + # we're a type/title array reference + values[0] = symbolize(values[0]) + result << values + else + # we're an array of stuff + values.each do |value| + if value.is_a?(Puppet::Type) + result << [value.class.name, value.title] + elsif value.is_a?(Array) + value[0] = symbolize(value[0]) + result << value + else + devfail "Invalid relationship %s" % value.inspect + end + end + end + end + + if existing = self[param] + result = existing + result + end + + result + end # For each object we require, subscribe to all events that it generates. We # might reduce the level of subscription eventually, but for now... newmetaparam(:require) do desc "One or more objects that this object depends on. This is used purely for guaranteeing that changes to required objects happen before the dependent object. For instance: # Create the destination directory before you copy things down file { \"/usr/local/scripts\": ensure => directory } file { \"/usr/local/scripts/myscript\": source => \"puppet://server/module/myscript\", mode => 755, require => file[\"/usr/local/scripts\"] } Note that Puppet will autorequire everything that it can, and there are hooks in place so that it's easy for elements to add new ways to autorequire objects, so if you think Puppet could be smarter here, let us know. In fact, the above code was redundant -- Puppet will autorequire any parent directories that are being managed; it will automatically realize that the parent directory should be created before the script is pulled down. Currently, exec elements will autorequire their CWD (if it is specified) plus any fully qualified paths that appear in the command. For instance, if you had an ``exec`` command that ran the ``myscript`` mentioned above, the above code that pulls the file down would be automatically listed as a requirement to the ``exec`` code, so that you would always be running againts the most recent version. " # Take whatever dependencies currently exist and add these. # Note that this probably doesn't behave correctly with unsubscribe. munge do |requires| - # We need to be two arrays deep... - unless requires.is_a?(Array) - requires = [requires] - end - unless requires[0].is_a?(Array) - requires = [requires] - end - if values = @parent[:require] - requires = values + requires - end - requires + @parent.store_relationship(:require, requires) end end # For each object we require, subscribe to all events that it generates. # We might reduce the level of subscription eventually, but for now... newmetaparam(:subscribe) do desc "One or more objects that this object depends on. Changes in the subscribed to objects result in the dependent objects being refreshed (e.g., a service will get restarted). For instance: class nagios { file { \"/etc/nagios/nagios.conf\": source => \"puppet://server/module/nagios.conf\", alias => nagconf # just to make things easier for me } service { nagios: running => true, subscribe => file[nagconf] } } " munge do |requires| - if values = @parent[:subscribe] - requires = values + requires - end - requires - # @parent.handledepends(requires, :ALL_EVENTS, :refresh) + @parent.store_relationship(:subscribe, requires) end end newmetaparam(:loglevel) do desc "Sets the level that information will be logged. The log levels have the biggest impact when logs are sent to syslog (which is currently the default)." defaultto :notice newvalues(*Puppet::Log.levels) newvalues(:verbose) munge do |loglevel| val = super(loglevel) if val == :verbose val = :info end val end end newmetaparam(:alias) do desc "Creates an alias for the object. Puppet uses this internally when you provide a symbolic name: file { sshdconfig: path => $operatingsystem ? { solaris => \"/usr/local/etc/ssh/sshd_config\", default => \"/etc/ssh/sshd_config\" }, source => \"...\" } service { sshd: subscribe => file[sshdconfig] } When you use this feature, the parser sets ``sshdconfig`` as the name, and the library sets that as an alias for the file so the dependency lookup for ``sshd`` works. You can use this parameter yourself, but note that only the library can use these aliases; for instance, the following code will not work: file { \"/etc/ssh/sshd_config\": owner => root, group => root, alias => sshdconfig } file { sshdconfig: mode => 644 } There's no way here for the Puppet parser to know that these two stanzas should be affecting the same file. See the [language tutorial][] for more information. [language tutorial]: languagetutorial.html " munge do |aliases| unless aliases.is_a?(Array) aliases = [aliases] end @parent.info "Adding aliases %s" % aliases.collect { |a| a.inspect }.join(", ") aliases.each do |other| if obj = @parent.class[other] unless obj == @parent self.fail( "%s can not create alias %s: object already exists" % [@parent.title, other] ) end next end @parent.class.alias(other, @parent) end end end newmetaparam(:tag) do desc "Add the specified tags to the associated element. While all elements are automatically tagged with as much information as possible (e.g., each class and component containing the element), it can be useful to add your own tags to a given element. Tags are currently useful for things like applying a subset of a host's configuration: puppetd --test --tag mytag This way, when you're testing a configuration you can run just the portion you're testing." munge do |tags| tags = [tags] unless tags.is_a? Array tags.each do |tag| @parent.tag(tag) end end end newmetaparam(:notify) do desc %{This parameter is the opposite of **subscribe** -- it sends events to the specified object: file { "/etc/sshd_config": source => "....", notify => service[sshd] } service { sshd: ensure => running } This will restart the sshd service if the sshd config file changes.} - # Take whatever dependencies currently exist and add these. munge do |notifies| - # We need to be two arrays deep... - unless notifies.is_a?(Array) - notifies = [notifies] - end - unless notifies[0].is_a?(Array) - notifies = [notifies] - end - if values = @parent[:notify] - notifies = values + notifies - end - notifies + @parent.store_relationship(:notify, notifies) end - end newmetaparam(:before) do desc %{This parameter is the opposite of **require** -- it guarantees that the specified object is applied later than the specifying object: file { "/var/nagios/configuration": source => "...", recurse => true, before => exec["nagios-rebuid"] } exec { "nagios-rebuild": command => "/usr/bin/make", cwd => "/var/nagios/configuration" } This will make sure all of the files are up to date before the make command is run.} - # Take whatever dependencies currently exist and add these. munge do |notifies| - # We need to be two arrays deep... - unless notifies.is_a?(Array) - notifies = [notifies] - end - unless notifies[0].is_a?(Array) - notifies = [notifies] - end - if values = @parent[:notify] - notifies = values + notifies - end - notifies + @parent.store_relationship(:before, notifies) end - end end # Puppet::Type # $Id$ diff --git a/lib/puppet/metatype/relationships.rb b/lib/puppet/metatype/relationships.rb index 714ed7690..5f2471460 100644 --- a/lib/puppet/metatype/relationships.rb +++ b/lib/puppet/metatype/relationships.rb @@ -1,215 +1,159 @@ class Puppet::Type # Specify a block for generating a list of objects to autorequire. This # makes it so that you don't have to manually specify things that you clearly # require. def self.autorequire(name, &block) @autorequires ||= {} @autorequires[name] = block end # Yield each of those autorequires in turn, yo. def self.eachautorequire @autorequires ||= {} @autorequires.each { |type, block| yield(type, block) } end # Figure out of there are any objects we can automatically add as # dependencies. def autorequire + reqs = [] self.class.eachautorequire { |type, block| # Ignore any types we can't find, although that would be a bit odd. next unless typeobj = Puppet.type(type) # Retrieve the list of names from the block. next unless list = self.instance_eval(&block) unless list.is_a?(Array) list = [list] end # Collect the current prereqs list.each { |dep| obj = nil # Support them passing objects directly, to save some effort. - if dep.is_a? Puppet::Type - type = dep.class.name - obj = dep - - # Now change our dependency to just the string, instead of - # the object itself. - dep = dep.title - else + unless dep.is_a? Puppet::Type # Skip autorequires that we aren't managing - unless obj = typeobj[dep] + unless dep = typeobj[dep] next end end - - # Skip autorequires that we already require - next if self.requires?(obj) - - debug "Autorequiring %s %s" % [obj.class.name, obj.title] - self[:require] = [type, dep] + + debug "Autorequiring %s" % [dep.ref] + reqs << Puppet::Relationship[dep, self] } - - #self.info reqs.inspect - #self[:require] = reqs } + + return reqs end - # Build the dependencies associated with an individual object. + # Build the dependencies associated with an individual object. :in + # relationships are specified by the event-receivers, and :out + # relationships are specified by the event generator. This + # way 'source' and 'target' are consistent terms in both edges + # and events -- that is, an event targets edges whose source matches + # the event's source. Note that the direction of the relationship + # doesn't actually mean anything until you start using events -- + # the same information is present regardless. def builddepends # Handle the requires - if self[:require] - self.handledepends(self[:require], :NONE, nil, true) - end - - # And the subscriptions - if self[:subscribe] - self.handledepends(self[:subscribe], :ALL_EVENTS, :refresh, true) - end - - if self[:notify] - self.handledepends(self[:notify], :ALL_EVENTS, :refresh, false) - end - - if self[:before] - self.handledepends(self[:before], :NONE, nil, false) - end - end - - # return all objects that we depend on - def eachdependency - Puppet::Event::Subscription.dependencies(self).each { |dep| - yield dep.source - } - end - - # return all objects subscribed to the current object - def eachsubscriber - Puppet::Event::Subscription.subscribers(self).each { |sub| - yield sub.target - } + {:require => [:NONE, nil, :in], + :subscribe => [:ALL_EVENTS, :refresh, :in], + :notify => [:ALL_EVENTS, :refresh, :out], + :before => [:NONE, nil, :out]}.collect do |type, args| + if self[type] + handledepends(self[type], *args) + end + end.flatten.reject { |r| r.nil? } end - def handledepends(requires, event, method, up) + def handledepends(requires, event, method, direction) # Requires are specified in the form of [type, name], so they're always # an array. But we want them to be an array of arrays. unless requires[0].is_a?(Array) requires = [requires] end - requires.each { |rname| + requires.collect { |rname| # we just have a name and a type, and we need to convert it # to an object... type = nil object = nil tname = rname[0] unless type = Puppet::Type.type(tname) self.fail "Could not find type %s" % tname.inspect end name = rname[1] unless object = type[name] self.fail "Could not retrieve object '%s' of type '%s'" % [name,type] end self.debug("subscribes to %s" % [object]) - # Are we requiring them, or vice versa? - source = target = nil - if up + # Are we requiring them, or vice versa? See the builddepends + # method for further docs on this. + if direction == :in source = object target = self else source = self target = object end # ok, both sides of the connection store some information # we store the method to call when a given subscription is # triggered, but the source object decides whether subargs = { - :event => event, - :source => source, - :target => target + :event => event } - if method and target.respond_to?(method) + if method subargs[:callback] = method end - Puppet::Event::Subscription.new(subargs) + rel = Puppet::Relationship.new(source, target, subargs) } end - def requires?(object) - req = false - self.eachdependency { |dep| - if dep == object - req = true - break - end - } - - return req - end - - def subscribe(hash) - hash[:source] = self - Puppet::Event::Subscription.new(hash) - - # add to the correct area - #@subscriptions.push sub - end - - def subscribesto?(object) - sub = false - self.eachsubscriber { |o| - if o == object - sub = true - break - end - } - - return sub - end - # Unsubscribe from a given object, possibly with a specific event. def unsubscribe(object, event = nil) - Puppet::Event::Subscription.dependencies(self).find_all { |sub| - if event - sub.match?(event) - else - sub.source == object + # First look through our own relationship params + [:require, :subscribe].each do |param| + if values = self[param] + newvals = values.reject { |d| + d == [object.class.name, object.title] + } + if newvals.length != values.length + self.delete(param) + self[param] = newvals + end end - }.each { |sub| - sub.delete - } + end end # we've received an event # we only support local events right now, so we can pass actual # objects around, including the transaction object # the assumption here is that container objects will pass received # methods on to contained objects # i.e., we don't trigger our children, our refresh() method calls # refresh() on our children def trigger(event, source) trans = event.transaction if @callbacks.include?(source) [:ALL_EVENTS, event.event].each { |eventname| if method = @callbacks[source][eventname] if trans.triggered?(self, method) > 0 next end if self.respond_to?(method) self.send(method) end trans.triggered(self, method) end } end end end # $Id$ diff --git a/lib/puppet/pgraph.rb b/lib/puppet/pgraph.rb index fba93d20c..292e25073 100644 --- a/lib/puppet/pgraph.rb +++ b/lib/puppet/pgraph.rb @@ -1,170 +1,109 @@ #!/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 + # Collect all of the edges that the passed events match. Returns + # an array of edges. + def matching_edges(events) + events.collect do |event| + source = event.source + + unless vertex?(source) + Puppet.warning "Got an event from invalid vertex %s" % source.ref + next + end + + # Get all of the edges that this vertex should forward events + # to, which is the same thing as saying all edges directly below + # This vertex in the graph. + adjacent(source, :direction => :out, :type => :edges).find_all do |edge| + edge.match?(event.event) + end.each { |edge| + target = edge.target + if target.respond_to?(:ref) + source.info "Scheduling %s of %s" % + [edge.callback, target.ref] + end + } + end.flatten + 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| + adjacent(vertex, :direction => :in, :type => :edges).each do |edge| leaves.each do |leaf| - add_edge!(up, leaf) + add_edge!(edge.source, leaf, edge.label) 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| + adjacent(vertex, :direction => :out, :type => :edges).each do |edge| leaves.each do |leaf| - add_edge!(leaf, down) + add_edge!(leaf, edge.target, edge.label) 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 + # For some reason, unconnected vertices do not show up in + # this graph. 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 index cbd15b2af..879beeab7 100644 --- a/lib/puppet/relationship.rb +++ b/lib/puppet/relationship.rb @@ -1,51 +1,63 @@ #!/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] + if label + label[:callback] + else + nil + end end # Return our event. def event - label[:event] + if label + label[:event] + else + nil + end end - def initialize(source, target, label = nil) + def initialize(source, target, label = {}) 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 + + def ref + "%s => %s" % [source.ref, target.ref] + end end # $Id$ diff --git a/lib/puppet/transaction.rb b/lib/puppet/transaction.rb index 1551acbb6..6835b1b19 100644 --- a/lib/puppet/transaction.rb +++ b/lib/puppet/transaction.rb @@ -1,432 +1,479 @@ -# the class that actually walks our object/state tree, collects the changes, +# the class that actually walks our resource/state tree, collects the changes, # and performs them require 'puppet' require 'puppet/statechange' module Puppet class Transaction - attr_accessor :component, :objects, :tags, :ignoreschedules, :ignoretags + attr_accessor :component, :resources, :tags, :ignoreschedules, :ignoretags + attr_accessor :relgraph 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. + :tags => ["", "Tags to use to find resources. If this is set, then + only resources 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| + child.info "applying" + # 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. + p @relgraph.vertices.collect { |v| v.ref } + @relgraph.reversal.tree_from_vertex(child, :dfs).keys.each do |dep| skip = false 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] + if Puppet[:trace] 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. + # Find all of the changed resources. 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() + + # Now add any dynamically generated resources + generate() + + # Create a relationship graph from our resource graph + @relgraph = relationship_graph + + @relgraph.to_jpg("relations") begin - allevents = @objects.collect { |child| + allevents = @relgraph.topsort.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 + # Keep track of how long we spend in each type of resource @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) + @relgraph.matching_edges(events).each do |edge| + @targets[edge.target] << edge + end # 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. + # Determine whether a given resource has failed. def failed?(obj) if @failures[obj] > 0 return @failures[obj] else return false end end + + # Collect any dynamically generated resources. + def generate + list = @resources.vertices + newlist = [] + while ! list.empty? + list.each do |resource| + if resource.respond_to?(:generate) + made = resource.generate + unless made.is_a?(Array) + made = [made] + end + made.each do |res| + @resources.add_vertex!(res) + newlist << res + end + end + end + list.clear + list = newlist + newlist = [] + end + end - # this should only be called by a Puppet::Container object now + # this should only be called by a Puppet::Type::Component resource now # and it should only receive an array - def initialize(objects) - @objects = objects + def initialize(resources) + @resources = resources.to_graph @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 + :total => @resources.vertices.length, + :out_of_sync => 0, # The number of resources that had changes + :applied => 0, # The number of resources fixed + :skipped => 0, # The number of resources skipped + :restarted => 0, # The number of resources triggered + :failed_restarts => 0, # The number of resources that fail a trigger + :scheduled => 0 # The number of resources scheduled } # Metrics for distributing times across the different types. @timemetrics = Hash.new(0) - # The number of objects that were triggered in this run + # The number of resources 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] = {} + 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. + # The resources that have failed and the number of failures each. This + # is used for skipping resources 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| + @resources.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 + + # Create a graph of all of the relationships in our resource graph. + def relationship_graph + graph = Puppet::PGraph.new + + # First create the dependency graph + @resources.vertices.each do |vertex| + graph.add_vertex!(vertex) + vertex.builddepends.each do |edge| + graph.add_edge!(edge) + end + end + + # Then splice in the container information + graph.splice!(@resources, Puppet::Type::Component) + + # Lastly, add in any autorequires + graph.vertices.each do |vertex| + vertex.autorequire.each do |edge| + unless graph.edge?(edge) + graph.add_edge!(edge) + end + end + end + + return graph + 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 + # Add all of the metrics related to resource count and status @report.newmetric(:resources, @resourcemetrics) - # Record the relative time spent in each object. + # Record the relative time spent in each resource. @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] + if Puppet[:trace] 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 + # so just record that a sync failed for a given resource #@@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 + + @relgraph.matching_edges(events).each do |edge| + @targets[edge.target] << edge + end # 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 + # search -- it triggers the passed resource, but also the resource'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| + @targets[obj].each do |edge| + # Some edges don't have callbacks + next unless edge.callback + # Collect all of the subs for each callback - callbacks[sub.callback] << sub + callbacks[edge.callback] << edge # And collect the sources for logging - sources[event.source] << sub.callback + sources[edge.source] << edge.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] + if Puppet[:trace] 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 + def triggered(resource, method) + @triggered[resource][method] += 1 end - def triggered?(object, method) - @triggered[object][method] + def triggered?(resource, method) + @triggered[resource][method] end end end require 'puppet/transaction/report' # $Id$ diff --git a/lib/puppet/type.rb b/lib/puppet/type.rb index 1adeb1366..19de99b7c 100644 --- a/lib/puppet/type.rb +++ b/lib/puppet/type.rb @@ -1,415 +1,413 @@ require 'puppet' require 'puppet/log' require 'puppet/element' require 'puppet/event' require 'puppet/metric' require 'puppet/type/state' require 'puppet/parameter' require 'puppet/util' require 'puppet/autoload' require 'puppet/metatype/manager' # see the bottom of the file for the rest of the inclusions module Puppet class Type < Puppet::Element # Nearly all of the code in this class is stored in files in the # metatype/ directory. This is a temporary measure until I get a chance # to refactor this class entirely. There's still more simplification to # do, but this works for now. require 'puppet/metatype/attributes' require 'puppet/metatype/closure' require 'puppet/metatype/container' require 'puppet/metatype/evaluation' require 'puppet/metatype/instances' require 'puppet/metatype/metaparams' require 'puppet/metatype/providers' require 'puppet/metatype/relationships' require 'puppet/metatype/schedules' require 'puppet/metatype/tags' # Types (which map to elements in the languages) are entirely composed of # attribute value pairs. Generally, Puppet calls any of these things an # 'attribute', but these attributes always take one of three specific # forms: parameters, metaparams, or states. # In naming methods, I have tried to consistently name the method so # that it is clear whether it operates on all attributes (thus has 'attr' in # the method name, or whether it operates on a specific type of attributes. attr_accessor :file, :line attr_reader :parent attr_writer :title include Enumerable # class methods dealing with Type management public # the Type class attribute accessors class << self attr_reader :name include Enumerable, Puppet::Util::ClassGen include Puppet::MetaType::Manager end # all of the variables that must be initialized for each subclass def self.initvars # all of the instances of this class @objects = Hash.new @aliases = Hash.new @providers = Hash.new @defaults = {} unless defined? @parameters @parameters = [] end @validstates = {} @states = [] @parameters = [] @paramhash = {} @paramdoc = Hash.new { |hash,key| if key.is_a?(String) key = key.intern end if hash.include?(key) hash[key] else "Param Documentation for %s not found" % key end } unless defined? @doc @doc = "" end unless defined? @states @states = [] end end def self.to_s if defined? @name "Puppet::Type::" + @name.to_s.capitalize else super end end # Create a block to validate that our object is set up entirely. This will # be run before the object is operated on. def self.validate(&block) define_method(:validate, &block) #@validate = block end # iterate across all children, and then iterate across states # we do children first so we're sure that all dependent objects # are checked first # we ignore parameters here, because they only modify how work gets # done, they don't ever actually result in work specifically def each # we want to return the states in the order that each type # specifies it, because it may (as in the case of File#create) # be important if self.class.depthfirst? @children.each { |child| yield child } end self.eachstate { |state| yield state } unless self.class.depthfirst? @children.each { |child| yield child } end end # Recurse deeply through the tree, but only yield types, not states. def delve(&block) self.each do |obj| if obj.is_a? Puppet::Type obj.delve(&block) end end block.call(self) end # create a log at specified level def log(msg) Puppet::Log.create( :level => @metaparams[:loglevel].value, :message => msg, :source => self ) end # instance methods related to instance intrinsics # e.g., initialize() and name() public def initvars @children = [] @evalcount = 0 @tags = [] # callbacks are per object and event @callbacks = Hash.new { |chash, key| chash[key] = {} } # states and parameters are treated equivalently from the outside: # as name-value pairs (using [] and []=) # internally, however, parameters are merely a hash, while states # point to State objects # further, the lists of valid states and parameters are defined # at the class level unless defined? @states @states = Hash.new(false) end unless defined? @parameters @parameters = Hash.new(false) end unless defined? @metaparams @metaparams = Hash.new(false) end # set defalts @noop = false # keeping stats for the total number of changes, and how many were # completely sync'ed # this isn't really sufficient either, because it adds lots of special # cases such as failed changes # it also doesn't distinguish between changes from the current transaction # vs. changes over the process lifetime @totalchanges = 0 @syncedchanges = 0 @failedchanges = 0 @inited = true end # initialize the type instance def initialize(hash) unless defined? @inited self.initvars end namevar = self.class.namevar orighash = hash # If we got passed a transportable object, we just pull a bunch of info # directly from it. This is the main object instantiation mechanism. if hash.is_a?(Puppet::TransObject) #self[:name] = hash[:name] [:file, :line, :tags].each { |getter| if hash.respond_to?(getter) setter = getter.to_s + "=" if val = hash.send(getter) self.send(setter, val) end end } # XXX This will need to change when transobjects change to titles. @title = hash.name hash = hash.to_hash elsif hash[:title] # XXX This should never happen @title = hash[:title] hash.delete(:title) end # Before anything else, set our parent if it was included if hash.include?(:parent) @parent = hash[:parent] hash.delete(:parent) end # Munge up the namevar stuff so we only have one value. hash = self.argclean(hash) # Let's do the name first, because some things need to happen once # we have the name but before anything else attrs = self.class.allattrs if hash.include?(namevar) #self.send(namevar.to_s + "=", hash[namevar]) self[namevar] = hash[namevar] hash.delete(namevar) if attrs.include?(namevar) attrs.delete(namevar) else self.devfail "My namevar isn\'t a valid attribute...?" end else self.devfail "I was not passed a namevar" end # If the name and title differ, set up an alias if self.name != self.title if obj = self.class[self.name] if self.class.isomorphic? raise Puppet::Error, "%s already exists with name %s" % [obj.title, self.name] end else self.class.alias(self.name, self) end end # This is all of our attributes except the namevar. attrs.each { |attr| if hash.include?(attr) begin self[attr] = hash[attr] rescue ArgumentError, Puppet::Error, TypeError raise rescue => detail self.devfail( "Could not set %s on %s: %s" % [attr, self.class.name, detail] ) end hash.delete attr end } # Set all default values. self.setdefaults if hash.length > 0 self.debug hash.inspect self.fail("Class %s does not accept argument(s) %s" % [self.class.name, hash.keys.join(" ")]) end if self.respond_to?(:validate) self.validate end end # Set up all of our autorequires. def finish - self.autorequire - # Scheduling has to be done when the whole config is instantiated, so # that file order doesn't matter in finding them. self.schedule end # Return a cached value def cached(name) Puppet::Storage.cache(self)[name] #@cache[name] ||= nil end # Cache a value def cache(name, value) Puppet::Storage.cache(self)[name] = value #@cache[name] = value end # def set(name, value) # send(name.to_s + "=", value) # end # # def get(name) # send(name) # end # For now, leave the 'name' method functioning like it used to. Once 'title' # works everywhere, I'll switch it. def name return self[:name] end # Return the "type[name]" style reference. def ref "%s[%s]" % [self.class.name, self.title] end # Retrieve the title of an object. If no title was set separately, # then use the object's name. def title unless defined? @title and @title namevar = self.class.namevar if self.class.validparameter?(namevar) @title = self[:name] elsif self.class.validstate?(namevar) @title = self.should(namevar) else self.devfail "Could not find namevar %s for %s" % [namevar, self.class.name] end end return @title end # convert to a string def to_s self.title end # Convert to a transportable object def to_trans(ret = true) # Retrieve the object, if they tell use to. if ret retrieve() end trans = TransObject.new(self.title, self.class.name) states().each do |state| trans[state.name] = state.is end @parameters.each do |name, param| # Avoid adding each instance name as both the name and the namevar next if param.class.isnamevar? and param.value == self.title trans[name] = param.value end @metaparams.each do |name, param| trans[name] = param.value end trans.tags = self.tags # FIXME I'm currently ignoring 'parent' and 'path' return trans end end # Puppet::Type end require 'puppet/statechange' require 'puppet/provider' require 'puppet/type/component' require 'puppet/type/cron' require 'puppet/type/exec' require 'puppet/type/group' require 'puppet/type/package' require 'puppet/type/pfile' require 'puppet/type/pfilebucket' require 'puppet/type/schedule' require 'puppet/type/service' require 'puppet/type/symlink' require 'puppet/type/user' require 'puppet/type/tidy' require 'puppet/type/parsedtype' require 'puppet/type/notify' # $Id$ diff --git a/lib/puppet/type/component.rb b/lib/puppet/type/component.rb index 3165d9e11..cadd586c8 100644 --- a/lib/puppet/type/component.rb +++ b/lib/puppet/type/component.rb @@ -1,178 +1,177 @@ # 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 = Puppet::Transaction.new(self) 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/test/lib/puppettest/support/resources.rb b/test/lib/puppettest/support/resources.rb new file mode 100755 index 000000000..45d89c5fb --- /dev/null +++ b/test/lib/puppettest/support/resources.rb @@ -0,0 +1,37 @@ +#!/usr/bin/env ruby +# +# Created by Luke A. Kanies on 2006-11-29. +# Copyright (c) 2006. All rights reserved. + +module PuppetTest::Support::Resources + def treefile(name) + Puppet::Type.type(:file).create :path => "/tmp/#{name}", :mode => 0755 + end + + def treecomp(name) + Puppet::Type::Component.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 +end + +# $Id$ \ No newline at end of file diff --git a/test/other/overrides.rb b/test/other/overrides.rb index e25670291..2bc443980 100755 --- a/test/other/overrides.rb +++ b/test/other/overrides.rb @@ -1,112 +1,110 @@ #!/usr/bin/env ruby $:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'puppet' require 'puppettest' class TestOverrides < Test::Unit::TestCase include PuppetTest def mksubdirs(basedir, level) @@tmpfiles << basedir dir = basedir.dup (level + 1).times { |index| Dir.mkdir(dir) path = File.join(dir, "file") File.open(path, "w") { |f| f.puts "yayness" } dir = File.join(dir, index.to_s) } end def test_simpleoverride basedir = File.join(tmpdir(), "overridetesting") mksubdirs(basedir, 1) baseobj = nil basefile = File.join(basedir, "file") assert_nothing_raised("Could not create base obj") { baseobj = Puppet.type(:file).create( + :title => "base", :path => basedir, :recurse => true, :mode => "755" ) } subobj = nil subdir = File.join(basedir, "0") subfile = File.join(subdir, "file") assert_nothing_raised("Could not create sub obj") { subobj = Puppet.type(:file).create( + :title => "sub", :path => subdir, :recurse => true, :mode => "644" ) } - comp = newcomp("overrides", baseobj, subobj) - assert_nothing_raised("Could not eval component") { - trans = comp.evaluate - trans.evaluate - } + assert_apply(baseobj, subobj) assert(File.stat(basefile).mode & 007777 == 0755) assert(File.stat(subfile).mode & 007777 == 0644) end - def test_zdeepoverride + def test_deepoverride basedir = File.join(tmpdir(), "deepoverridetesting") mksubdirs(basedir, 10) baseobj = nil assert_nothing_raised("Could not create base obj") { baseobj = Puppet.type(:file).create( :path => basedir, :recurse => true, :mode => "755" ) } children = [] files = {} subdir = basedir.dup mode = nil 10.times { |index| next unless index % 3 subdir = File.join(subdir, index.to_s) path = File.join(subdir, "file") if index % 2 mode = "644" files[path] = 0644 else mode = "750" files[path] = 0750 end assert_nothing_raised("Could not create sub obj") { children << Puppet.type(:file).create( :path => subdir, :recurse => true, :mode => mode ) } } comp = newcomp("overrides", baseobj) children.each { |child| comp.push child } assert_nothing_raised("Could not eval component") { trans = comp.evaluate trans.evaluate } files.each { |path, mode| assert(FileTest.exists?(path), "File %s does not exist" % path) curmode = File.stat(path).mode & 007777 assert(curmode == mode, "File %s was incorrect mode %o instead of %o" % [path, curmode, mode]) } end end # $Id$ diff --git a/test/other/pgraph.rb b/test/other/pgraph.rb index c3baa7722..adf290b34 100644 --- a/test/other/pgraph.rb +++ b/test/other/pgraph.rb @@ -1,77 +1,102 @@ #!/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 + Edge = Puppet::Relationship + + def test_matching_edges 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) + edges = {} + + edges["a/b"] = Edge["a", "b", {:event => :yay, :callback => :refresh}] + edges["a/c"] = Edge["a", "c", {:event => :yay, :callback => :refresh}] + + graph.add_edge!(edges["a/b"]) # Try it for the trivial case of one target and a matching event - assert_equal(["b"], graph.collect_targets([event])) + assert_equal([edges["a/b"]], graph.matching_edges([event])) # Make sure we get nothing with a different event - assert_equal([], graph.collect_targets([none])) + assert_equal([], graph.matching_edges([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])) + graph.add_edge!(edges["a/c"]) + assert_equal([edges["a/b"], edges["a/c"]].sort, graph.matching_edges([event]).sort) + assert_equal([], graph.matching_edges([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 add a couple of child files, so that we can test whether all containers + # get spliced, rather than just components. + # 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) + deps.add_edge!(source, target, :callback => :refresh) end deps.to_jpg("deps-before") deps.splice!(contgraph, Container) assert(! deps.cyclic?, "Created a cyclic graph") + # Now make sure the containers got spliced correctly. + contgraph.leaves(middle).each do |leaf| + assert(deps.edge?("h", leaf), "no edge for h => %s" % leaf) + end + one.each do |oobj| + two.each do |tobj| + assert(deps.edge?(oobj, tobj), "no %s => %s edge" % [oobj, tobj]) + end + end + 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") + + deps.edges.each do |edge| + assert_equal({:callback => :refresh}, edge.label, + "Label was not copied on splice") + end end end # $Id$ \ No newline at end of file diff --git a/test/other/relationships.rb b/test/other/relationships.rb index 893ef1ff4..e9c1d1c9c 100755 --- a/test/other/relationships.rb +++ b/test/other/relationships.rb @@ -1,140 +1,242 @@ #!/usr/bin/env ruby $:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'puppet' require 'puppettest' class TestRelationships < Test::Unit::TestCase include PuppetTest def newfile assert_nothing_raised() { return Puppet.type(:file).create( :path => tempfile, :check => [:mode, :owner, :group] ) } end + + def check_relationship(sources, targets, out, refresher) + if out + deps = sources.builddepends + sources = [sources] + else + deps = targets.builddepends + targets = [targets] + end + assert_instance_of(Array, deps) + assert(! deps.empty?, "Did not receive any relationships") + + deps.each do |edge| + assert_instance_of(Puppet::Relationship, edge) + end + + sources.each do |source| + targets.each do |target| + edge = deps.find { |e| e.source == source and e.target == target } + assert(edge, "Could not find edge for %s => %s" % + [source.ref, target.ref]) + + if refresher + assert_equal(:ALL_EVENTS, edge.event) + assert_equal(:refresh, edge.callback) + else + assert_equal(:NONE, edge.event) + assert_nil(edge.callback, "Got a callback with no events") + end + end + end + end - def test_simplerel - file1 = newfile() - file2 = newfile() - assert_nothing_raised { - file1[:require] = [file2.class.name, file2.name] - } - - comp = newcomp(file1, file2) - comp.finalize - deps = [] - assert_nothing_raised { - file1.eachdependency { |obj| - deps << obj - } - } - - assert_equal(1, deps.length, "Did not get dependency") - - assert_nothing_raised { - file1.unsubscribe(file2) - } - - deps = [] - assert_nothing_raised { - file1.eachdependency { |obj| - deps << obj - } - } - - assert_equal(0, deps.length, "Still have dependency") + # Make sure our various metaparams work correctly. We're just checking + # here whether they correctly set up the callbacks and the direction of + # the relationship. + def test_relationship_metaparams + out = {:require => false, :subscribe => false, + :notify => true, :before => true} + refreshers = [:subscribe, :notify] + [:require, :subscribe, :notify, :before].each do |param| + # Create three files to generate our events and three + # execs to receive them + files = [] + execs = [] + 3.times do |i| + files << Puppet::Type.newfile( + :title => "file#{i}", + :path => tempfile(), + :ensure => :file + ) + + path = tempfile() + execs << Puppet::Type.newexec( + :title => "notifytest#{i}", + :path => "/usr/bin:/bin", + :command => "touch #{path}", + :refreshonly => true + ) + end + + # Add our first relationship + if out[param] + files[0][param] = execs[0] + sources = files[0] + targets = [execs[0]] + else + execs[0][param] = files[0] + sources = [files[0]] + targets = execs[0] + end + check_relationship(sources, targets, out[param], refreshers.include?(param)) + + # Now add another relationship + if out[param] + files[0][param] = execs[1] + targets << execs[1] + assert_equal(targets.collect { |t| [t.class.name, t.title]}, + files[0][param], "Incorrect target list") + else + execs[0][param] = files[1] + sources << files[1] + assert_equal(sources.collect { |t| [t.class.name, t.title]}, + execs[0][param], "Incorrect source list") + end + check_relationship(sources, targets, out[param], refreshers.include?(param)) + + Puppet::Type.allclear + end + end + + def test_store_relationship + file = Puppet::Type.newfile :path => tempfile(), :mode => 0755 + execs = [] + 3.times do |i| + execs << Puppet::Type.newexec(:title => "yay#{i}", :command => "/bin/echo yay") + end + + # First try it with one object, specified as a reference and an array + result = nil + [execs[0], [:exec, "yay0"], ["exec", "yay0"]].each do |target| + assert_nothing_raised do + result = file.send(:store_relationship, :require, target) + end + + assert_equal([[:exec, "yay0"]], result) + end + + # Now try it with multiple objects + symbols = execs.collect { |e| [e.class.name, e.title] } + strings = execs.collect { |e| [e.class.name.to_s, e.title] } + [execs, symbols, strings].each do |target| + assert_nothing_raised do + result = file.send(:store_relationship, :require, target) + end + + assert_equal(symbols, result) + end + + # Make sure we can mix it up, even though this shouldn't happen + assert_nothing_raised do + result = file.send(:store_relationship, :require, [execs[0], [execs[1].class.name, execs[1].title]]) + end + + assert_equal([[:exec, "yay0"], [:exec, "yay1"]], result) + + # Finally, make sure that new results get added to old. The only way + # to get rid of relationships is to delete the parameter. + file[:require] = execs[0] + + assert_nothing_raised do + result = file.send(:store_relationship, :require, [execs[1], execs[2]]) + end + + assert_equal(symbols, result) end def test_newsub file1 = newfile() file2 = newfile() sub = nil assert_nothing_raised("Could not create subscription") { sub = Puppet::Event::Subscription.new( :source => file1, :target => file2, :event => :ALL_EVENTS, :callback => :refresh ) } subs = nil assert_nothing_raised { subs = Puppet::Event::Subscription.subscribers(file1) } assert_equal(1, subs.length, "Got incorrect number of subs") assert_equal(sub.target, subs[0], "Got incorrect sub") deps = nil assert_nothing_raised { deps = Puppet::Event::Subscription.dependencies(file2) } assert_equal(1, deps.length, "Got incorrect number of deps") assert_equal(sub, deps[0], "Got incorrect dep") end def test_eventmatch file1 = newfile() file2 = newfile() sub = nil assert_nothing_raised("Could not create subscription") { sub = Puppet::Event::Subscription.new( :source => file1, :target => file2, :event => :ALL_EVENTS, :callback => :refresh ) } assert(sub.match?(:anything), "ALL_EVENTS did not match") assert(! sub.match?(:NONE), "ALL_EVENTS matched :NONE") sub.event = :file_created assert(sub.match?(:file_created), "event did not match") assert(sub.match?(:ALL_EVENTS), "ALL_EVENTS did not match") assert(! sub.match?(:NONE), "ALL_EVENTS matched :NONE") sub.event = :NONE assert(! sub.match?(:file_created), "Invalid match") assert(! sub.match?(:ALL_EVENTS), "ALL_EVENTS matched") assert(! sub.match?(:NONE), "matched :NONE") end - - def test_deletingsubs - file1 = newfile() - file2 = newfile() - - file1[:subscribe] = [:file, file2.name] - - comp = newcomp(file1, file2) - comp.finalize - - assert(file1.requires?(file2)) - - assert_nothing_raised { - file1.unsubscribe(file2) - } - assert(!file1.requires?(file2)) - - # Now readd it, so we can use 'remove' - file1[:subscribe] = [:file, file2.name] - comp.finalize - - assert_nothing_raised { - file1.remove - } - - assert(!comp.include?(file1)) - assert(!file2.subscribesto?(file1)) + + def test_autorequire + # We know that execs autorequire their cwd, so we'll use that + path = tempfile() + + file = Puppet::Type.newfile(:title => "myfile", :path => path, + :ensure => :directory) + exec = Puppet::Type.newexec(:title => "myexec", :cwd => path, + :command => "/bin/echo") + + reqs = nil + assert_nothing_raised do + reqs = exec.autorequire + end + assert_equal([Puppet::Relationship[file, exec]], reqs) + + # Now make sure that these relationships are added to the transaction's + # relgraph + trans = Puppet::Transaction.new(newcomp(file, exec)) + assert_nothing_raised do + trans.evaluate + end + + graph = trans.relgraph + assert(graph.edge?(file, exec), "autorequire edge was not created") end end # $Id$ diff --git a/test/other/transactions.rb b/test/other/transactions.rb index 985e9a0c5..c143b3a0c 100755 --- a/test/other/transactions.rb +++ b/test/other/transactions.rb @@ -1,465 +1,577 @@ #!/usr/bin/env ruby $:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'puppet' require 'puppettest' +require 'puppettest/support/resources' # $Id$ class TestTransactions < Test::Unit::TestCase include PuppetTest::FileTesting + include PuppetTest::Support::Resources 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 + + cleanup do + Puppet::Type.rmtype(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]) + trans = Puppet::Transaction.new(newcomp(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") + file = Puppet::Type.newfile(:title => "file", :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 + # Verify that one component requiring another causes the contained + # resources in the requiring component to get refreshed. + def test_refresh_across_two_components 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]] + ecomp[:subscribe] = fcomp 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 + def test_unscheduled_and_untagged_response 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( + file1 = Puppet::Type.type(:file).create( + :title => "file1", :path => tempfile(), - :require => ["exec", "mkdir"], + :require => exec, :ensure => :file ) - comp = newcomp(exec, file) + file2 = Puppet::Type.type(:file).create( + :title => "file2", + :path => tempfile(), + :require => file1, + :ensure => :file + ) + + comp = newcomp(exec, file1, file2) comp.finalize assert_apply(comp) - assert(! FileTest.exists?(file[:path]), + assert(! FileTest.exists?(file1[:path]), "File got created even tho its dependency failed") + assert(! FileTest.exists?(file2[:path]), + "File got created even tho its deep dependency failed") + end + end + + def f(n) + Puppet::Type.type(:file)["/tmp/#{n.to_s}"] + end + + def test_relationship_graph + one, two, middle, top = mktree + + {one => two, "f" => "c", "h" => middle}.each do |source, target| + if source.is_a?(String) + source = f(source) + end + if target.is_a?(String) + target = f(target) + end + target[:require] = source + end + + trans = Puppet::Transaction.new(top) + + graph = nil + assert_nothing_raised do + graph = trans.relationship_graph + end + + assert_instance_of(Puppet::PGraph, graph, + "Did not get relationship graph") + + # Make sure all of the components are gone + comps = graph.vertices.find_all { |v| v.is_a?(Puppet::Type::Component)} + assert(comps.empty?, "Deps graph still contains components") + + # It must be reversed because of how topsort works + sorted = graph.topsort.reverse + + # Now make sure the appropriate edges are there and are in the right order + assert(graph.dependencies(f(:f)).include?(f(:c)), + "c not marked a dep of f") + assert(sorted.index(f(:c)) < sorted.index(f(:f)), + "c is not before f") + + one.each do |o| + two.each do |t| + assert(graph.dependencies(o).include?(t), + "%s not marked a dep of %s" % [t.ref, o.ref]) + assert(sorted.index(t) < sorted.index(o), + "%s is not before %s" % [t.ref, o.ref]) + end + end + + trans.resources.leaves(middle).each do |child| + assert(graph.dependencies(f(:h)).include?(child), + "%s not marked a dep of h" % [child.ref]) + assert(sorted.index(child) < sorted.index(f(:h)), + "%s is not before h" % child.ref) + end + + # Lastly, make sure our 'g' vertex made it into the relationship + # graph, since it's not involved in any relationships. + assert(graph.vertex?(f(:g)), + "Lost vertexes with no relations") + + graph.to_jpg("normal_relations") end + + def test_generate + # Create a bogus type that generates new instances with shorter + Puppet::Type.newtype(:generator) do + newparam(:name, :namevar => true) + + def generate + ret = [] + if title.length > 1 + ret << self.class.create(:title => title[0..-2]) + end + ret + end + end + cleanup do + Puppet::Type.rmtype(:generator) + end + + yay = Puppet::Type.newgenerator :title => "yay" + rah = Puppet::Type.newgenerator :title => "rah" + comp = newcomp(yay, rah) + trans = comp.evaluate + + assert_nothing_raised do + trans.generate + end + + %w{ya ra y r}.each do |name| + assert(trans.resources.vertex?(Puppet::Type.type(:generator)[name]), + "Generated %s was not a vertex" % name) + end end end # $Id$ \ No newline at end of file diff --git a/test/types/component.rb b/test/types/component.rb index 28d6748a0..3fdc7bfe2 100755 --- a/test/types/component.rb +++ b/test/types/component.rb @@ -1,344 +1,316 @@ #!/usr/bin/env ruby $:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'puppet' require 'puppettest' +require 'puppettest/support/resources' # $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/types/manager.rb b/test/types/manager.rb new file mode 100755 index 000000000..f4353c128 --- /dev/null +++ b/test/types/manager.rb @@ -0,0 +1,44 @@ +#!/usr/bin/env ruby +# +# Created by Luke A. Kanies on 2006-11-29. +# Copyright (c) 2006. All rights reserved. + +$:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ + +require 'puppet' +require 'puppettest' + +class TestTypeManager < Test::Unit::TestCase + include PuppetTest + + class FakeManager + extend Puppet::MetaType::Manager + def self.clear + @types = {} + end + end + + def teardown + super + FakeManager.clear + end + + # Make sure we can remove defined types + def test_rmtype + assert_nothing_raised { + FakeManager.newtype :testing do + newparam(:name, :namevar => true) + end + } + assert(FakeManager.type(:testing), "Did not get fake type") + + assert_nothing_raised do + FakeManager.rmtype(:testing) + end + + assert_nil(FakeManager.type(:testing), "Type was not removed") + assert(! defined?(FakeManager::Testing), "Constant was not removed") + end +end + +# $Id$ \ No newline at end of file diff --git a/test/types/type.rb b/test/types/type.rb index 4be929ae9..821a9524b 100755 --- a/test/types/type.rb +++ b/test/types/type.rb @@ -1,812 +1,763 @@ #!/usr/bin/env ruby $:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'puppet/type' require 'puppettest' class TestType < Test::Unit::TestCase include PuppetTest def test_typemethods Puppet::Type.eachtype { |type| name = nil assert_nothing_raised("Searching for name for %s caused failure" % type.to_s) { name = type.name } assert(name, "Could not find name for %s" % type.to_s) assert_equal( type, Puppet::Type.type(name), "Failed to retrieve %s by name" % name ) # Skip types with no parameters or valid states #unless ! type.parameters.empty? or ! type.validstates.empty? # next #end assert_nothing_raised { assert( type.namevar, "Failed to retrieve namevar for %s" % name ) assert_not_nil( type.states, "States for %s are nil" % name ) assert_not_nil( type.validstates, "Valid states for %s are nil" % name ) } } end def test_stringvssymbols file = nil path = tempfile() assert_nothing_raised() { system("rm -f %s" % path) file = Puppet.type(:file).create( :path => path, :ensure => "file", :recurse => true, :checksum => "md5" ) } assert_nothing_raised() { file.retrieve } assert_nothing_raised() { file.evaluate } Puppet.type(:file).clear assert_nothing_raised() { system("rm -f %s" % path) file = Puppet.type(:file).create( "path" => path, "ensure" => "file", "recurse" => true, "checksum" => "md5" ) } assert_nothing_raised() { file.retrieve } assert_nothing_raised() { file[:path] } assert_nothing_raised() { file["path"] } assert_nothing_raised() { file[:recurse] } assert_nothing_raised() { file["recurse"] } assert_nothing_raised() { file.evaluate } end # This was supposed to test objects whose name was a state, but that # fundamentally doesn't make much sense, and we now don't have any such # types. def disabled_test_nameasstate # currently groups are the only objects with the namevar as a state group = nil assert_nothing_raised { group = Puppet.type(:group).create( :name => "testing" ) } assert_equal("testing", group.name, "Could not retrieve name") end # Verify that values get merged correctly def test_mergestatevalues file = tempfile() # Create the first version assert_nothing_raised { Puppet.type(:file).create( :path => file, :owner => ["root", "bin"] ) } # Make sure no other statements are allowed assert_raise(Puppet::Error) { Puppet.type(:file).create( :path => file, :group => "root" ) } end # Verify that aliasing works def test_aliasing file = tempfile() baseobj = nil assert_nothing_raised { baseobj = Puppet.type(:file).create( :name => file, :ensure => "file", :alias => ["funtest"] ) } # Verify our adding ourselves as an alias isn't an error. assert_nothing_raised { baseobj[:alias] = file } assert_instance_of(Puppet.type(:file), Puppet.type(:file)["funtest"], "Could not retrieve alias") end # Verify that requirements don't depend on file order def test_prereqorder one = tempfile() two = tempfile() twoobj = nil oneobj = nil assert_nothing_raised("Could not create prereq that doesn't exist yet") { twoobj = Puppet.type(:file).create( :name => two, :require => [:file, one] ) } assert_nothing_raised { oneobj = Puppet.type(:file).create( :name => one ) } comp = newcomp(twoobj, oneobj) assert_nothing_raised { comp.finalize } assert(twoobj.requires?(oneobj), "Requirement was not created") end # Verify that names are aliases, not equivalents def test_nameasalias file = nil # Create the parent dir, so we make sure autorequiring the parent dir works parentdir = tempfile() dir = Puppet.type(:file).create( :name => parentdir, :ensure => "directory" ) assert_apply(dir) path = File.join(parentdir, "subdir") name = "a test file" transport = Puppet::TransObject.new(name, "file") transport[:path] = path transport[:ensure] = "file" assert_nothing_raised { file = transport.to_type } assert_equal(path, file[:path]) assert_equal(name, file.title) assert_nothing_raised { file.retrieve } assert_apply(file) assert(Puppet.type(:file)[name], "Could not look up object by name") end def test_ensuredefault user = nil assert_nothing_raised { user = Puppet.type(:user).create( :name => "pptestAA", :check => [:uid] ) } # make sure we don't get :ensure for unmanaged files assert(! user.state(:ensure), "User got an ensure state") assert_nothing_raised { user = Puppet.type(:user).create( :name => "pptestAA", :comment => "Testingness" ) } # but make sure it gets added once we manage them assert(user.state(:ensure), "User did not add ensure state") assert_nothing_raised { user = Puppet.type(:user).create( :name => "pptestBB", :comment => "A fake user" ) } # and make sure managed objects start with them assert(user.state(:ensure), "User did not get an ensure state") end # Make sure removal works def test_remove objects = {} top = Puppet.type(:component).create(:name => "top") objects[top.class] = top base = tempfile() # now make a two-tier, 5 piece tree %w{a b}.each do |letter| name = "comp%s" % letter comp = Puppet.type(:component).create(:name => name) top.push comp objects[comp.class] = comp 5.times do |i| file = base + letter + i.to_s obj = Puppet.type(:file).create(:name => file, :ensure => "file") comp.push obj objects[obj.class] = obj end end assert_nothing_raised do top.remove end objects.each do |klass, obj| assert_nil(klass[obj.name], "object %s was not removed" % obj.name) end end # Verify that objects can't be their own children. def test_object_recursion comp = Puppet.type(:component).create(:name => "top") file = Puppet.type(:file).create(:path => tempfile, :ensure => :file) assert_raise(Puppet::DevError) do comp.push(comp) end assert_raise(Puppet::DevError) do file.push(file) end assert_raise(Puppet::DevError) do comp.parent = comp end assert_raise(Puppet::DevError) do file.parent = file end assert_nothing_raised { comp.push(file) } assert_raise(Puppet::DevError) do file.push(comp) end assert_raise(Puppet::DevError) do comp.parent = file end end def test_loadplugins names = %w{loadedplugin1 loadplugin2 loadplugin3} dirs = [] 3.times { dirs << tempfile() } # Set plugindest to something random Puppet[:plugindest] = tempfile() Puppet[:pluginpath] = dirs.join(":") names.each do |name| dir = dirs.shift Dir.mkdir(dir) # Create an extra file for later [name, name + "2ness"].each do |n| file = File.join(dir, n + ".rb") File.open(file, "w") do |f| f.puts %{Puppet::Type.newtype('#{n}') do newparam(:argument) do isnamevar end end } end end assert(Puppet::Type.type(name), "Did not get loaded plugin") assert_nothing_raised { Puppet::Type.type(name).create( :name => "myname" ) } end # Now make sure the plugindest got added to our pluginpath assert(Puppet[:pluginpath].split(":").include?(Puppet[:plugindest]), "Plugin dest did not get added to plugin path") # Now make sure it works with just a single path, using the extra files # created above. Puppet[:pluginpath] = Puppet[:pluginpath].split(":")[0] assert(Puppet::Type.type("loadedplugin12ness"), "Did not get loaded plugin") end def test_newtype_methods assert_nothing_raised { Puppet::Type.newtype(:mytype) do newparam(:wow) do isnamevar end end } assert(Puppet::Type.respond_to?(:newmytype), "new method did not get created") obj = nil assert_nothing_raised { obj = Puppet::Type.newmytype(:wow => "yay") } assert(obj.is_a?(Puppet::Type.type(:mytype)), "Obj is not the correct type") # Now make the type again, just to make sure it works on refreshing. assert_nothing_raised { Puppet::Type.newtype(:mytype) do newparam(:yay) do isnamevar end end } obj = nil # Make sure the old class was thrown away and only the new one is sitting # around. assert_raise(Puppet::Error) { obj = Puppet::Type.newmytype(:wow => "yay") } assert_nothing_raised { obj = Puppet::Type.newmytype(:yay => "yay") } # Now make sure that we don't replace existing, non-type methods parammethod = Puppet::Type.method(:newparam) assert_nothing_raised { Puppet::Type.newtype(:param) do newparam(:rah) do isnamevar end end } assert_equal(parammethod, Puppet::Type.method(:newparam), "newparam method got replaced by newtype") end - def test_notify_metaparam - file = Puppet::Type.newfile( - :path => tempfile(), - :notify => ["exec", "notifytest"], - :ensure => :file - ) - - path = tempfile() - exec = Puppet::Type.newexec( - :title => "notifytest", - :path => "/usr/bin:/bin", - :command => "touch #{path}", - :refreshonly => true - ) - - assert_apply(file, exec) - - assert(exec.requires?(file), - "Notify did not correctly set up the requirement chain.") - - assert(FileTest.exists?(path), - "Exec path did not get created.") - end - - def test_before_metaparam - file = Puppet::Type.newfile( - :path => tempfile(), - :before => ["exec", "beforetest"], - :content => "yaytest" - ) - - path = tempfile() - exec = Puppet::Type.newexec( - :title => "beforetest", - :command => "/bin/cp #{file[:path]} #{path}" - ) - - assert_apply(file, exec) - - assert(exec.requires?(file), - "Before did not correctly set up the requirement chain.") - - assert(FileTest.exists?(path), - "Exec path did not get created.") - - assert_equal("yaytest", File.read(path), - "Exec did not correctly copy file.") - end - def test_newstate_options # Create a type with a fake provider providerclass = Class.new do def method_missing(method, *args) return method end end self.class.const_set("ProviderClass", providerclass) type = Puppet::Type.newtype(:mytype) do newparam(:name) do isnamevar end def provider @provider ||= ProviderClass.new @provider end end # Now make a state with no options. state = nil assert_nothing_raised do state = type.newstate(:noopts) do end end # Now create an instance obj = type.create(:name => :myobj) inst = state.new(:parent => obj) # And make sure it's correctly setting @is ret = nil assert_nothing_raised { ret = inst.retrieve } assert_equal(:noopts, inst.is) # Now create a state with a different way of doing it state = nil assert_nothing_raised do state = type.newstate(:setretrieve, :retrieve => :yayness) end inst = state.new(:parent => obj) # And make sure it's correctly setting @is ret = nil assert_nothing_raised { ret = inst.retrieve } assert_equal(:yayness, inst.is) end def test_name_vs_title path = tempfile() trans = nil assert_nothing_raised { trans = Puppet::TransObject.new(path, :file) } file = nil assert_nothing_raised { file = Puppet::Type.newfile(trans) } assert(file.respond_to?(:title), "No 'title' method") assert(file.respond_to?(:name), "No 'name' method") assert_equal(file.title, file.name, "Name and title were not marked equal") assert_nothing_raised { file.title = "My file" } assert_equal("My file", file.title) assert_equal(path, file.name) end # Make sure the title is sufficiently differentiated from the namevar. def test_title_at_creation_with_hash file = nil fileclass = Puppet::Type.type(:file) path = tempfile() assert_nothing_raised do file = fileclass.create( :title => "Myfile", :path => path ) end assert_equal("Myfile", file.title, "Did not get correct title") assert_equal(path, file[:name], "Did not get correct name") file = nil Puppet::Type.type(:file).clear # Now make sure we can specify both and still get the right answers assert_nothing_raised do file = fileclass.create( :title => "Myfile", :name => path ) end assert_instance_of(fileclass, file) assert_equal("Myfile", file.title, "Did not get correct title") assert_equal(path, file[:name], "Did not get correct name") end # Make sure the "create" class method behaves appropriately. def test_class_create title = "Myfile" validate = proc do |element| assert(element, "Did not create file") assert_instance_of(Puppet::Type.type(:file), element) assert_equal(title, element.title, "Title is not correct") end type = :file args = {:path => tempfile(), :owner => "root"} trans = Puppet::TransObject.new(title, type) args.each do |name, val| trans[name] = val end # First call it on the appropriate typeclass obj = nil assert_nothing_raised do obj = Puppet::Type.type(:file).create(trans) end validate.call(obj) # Now try it using the class method on Type oldid = obj.object_id obj = nil Puppet::Type.type(:file).clear assert_nothing_raised { obj = Puppet::Type.create(trans) } validate.call(obj) assert(oldid != obj.object_id, "Got same object back") # Now try the same things with hashes instead of a transobject oldid = obj.object_id obj = nil Puppet::Type.type(:file).clear hash = { :type => :file, :title => "Myfile", :path => tempfile(), :owner => "root" } # First call it on the appropriate typeclass obj = nil assert_nothing_raised do obj = Puppet::Type.type(:file).create(hash) end validate.call(obj) assert_equal(:file, obj.should(:type), "Type param did not pass through") assert(oldid != obj.object_id, "Got same object back") # Now try it using the class method on Type oldid = obj.object_id obj = nil Puppet::Type.type(:file).clear assert_nothing_raised { obj = Puppet::Type.create(hash) } validate.call(obj) assert(oldid != obj.object_id, "Got same object back") assert_nil(obj.should(:type), "Type param passed through") end def test_multiplenames obj = nil path = tempfile() assert_raise ArgumentError do obj = Puppet::Type.type(:file).create( :name => path, :path => path ) end end def test_title_and_name obj = nil path = tempfile() fileobj = Puppet::Type.type(:file) assert_nothing_raised do obj = fileobj.create( :title => "myfile", :path => path ) end assert_equal(obj, fileobj["myfile"], "Could not retrieve obj by title") assert_equal(obj, fileobj[path], "Could not retrieve obj by name") end # Make sure default providers behave correctly def test_defaultproviders # Make a fake type type = Puppet::Type.newtype(:defaultprovidertest) do newparam(:name) do end end basic = type.provide(:basic) do defaultfor :operatingsystem => :somethingelse, :operatingsystemrelease => :yayness end assert_equal(basic, type.defaultprovider) type.defaultprovider = nil greater = type.provide(:greater) do defaultfor :operatingsystem => Facter.value("operatingsystem") end assert_equal(greater, type.defaultprovider) end # Make sure that we can have multiple isomorphic objects with the same name, # but not with non-isomorphic objects. def test_isomorphic_names # First do execs, since they're not isomorphic. echo = Puppet::Util.binary "echo" exec1 = exec2 = nil assert_nothing_raised do exec1 = Puppet::Type.type(:exec).create( :title => "exec1", :command => "#{echo} funtest" ) end assert_nothing_raised do exec2 = Puppet::Type.type(:exec).create( :title => "exec2", :command => "#{echo} funtest" ) end assert_apply(exec1, exec2) # Now do files, since they are. This should fail. file1 = file2 = nil path = tempfile() assert_nothing_raised do file1 = Puppet::Type.type(:file).create( :title => "file1", :path => path, :content => "yayness" ) end # This will fail, but earlier systems will catch it. assert_raise(Puppet::Error) do file2 = Puppet::Type.type(:file).create( :title => "file2", :path => path, :content => "rahness" ) end assert(file1, "Did not create first file") assert_nil(file2, "Incorrectly created second file") end def test_tags obj = Puppet::Type.type(:file).create(:path => tempfile()) tags = [:some, :test, :tags] obj.tags = tags assert_equal(tags + [:file], obj.tags) end def disabled_test_list Puppet::Type.loadall Puppet::Type.eachtype do |type| next if type.name == :symlink next if type.name == :component next if type.name == :tidy assert(type.respond_to?(:list), "%s does not respond to list" % type.name) end end def test_to_hash file = Puppet::Type.newfile :path => tempfile(), :owner => "luke", :recurse => true, :loglevel => "warning" hash = nil assert_nothing_raised do hash = file.to_hash end [:path, :owner, :recurse, :loglevel].each do |param| assert(hash[param], "Hash did not include %s" % param) end end # Make sure that classes behave like hashes. def test_class_hash_behaviour path = tempfile() filetype = Puppet::Type.type(:file) one = Puppet::Type.newfile :path => path assert_equal(one, filetype[path], "Did not get file back") assert_raise(Puppet::Error) do filetype[path] = one end end def test_ref path = tempfile() file = Puppet::Type.newfile(:path => path) assert_equal("file[#{path}]", file.ref) exec = Puppet::Type.newexec(:title => "yay", :command => "/bin/echo yay") assert_equal("exec[yay]", exec.ref) end end # $Id$