diff --git a/lib/puppet/resource/catalog.rb b/lib/puppet/resource/catalog.rb
index a8668d844..98c29657e 100644
--- a/lib/puppet/resource/catalog.rb
+++ b/lib/puppet/resource/catalog.rb
@@ -1,595 +1,653 @@
require 'puppet/node'
require 'puppet/indirector'
require 'puppet/simple_graph'
require 'puppet/transaction'
require 'puppet/util/cacher'
require 'puppet/util/pson'
require 'puppet/util/tagging'
# This class models a node catalog. It is the thing
# meant to be passed from server to client, and it contains all
# of the information in the catalog, including the resources
# and the relationships between them.
class Puppet::Resource::Catalog < Puppet::SimpleGraph
class DuplicateResourceError < Puppet::Error; end
extend Puppet::Indirector
indirects :catalog, :terminus_setting => :catalog_terminus
include Puppet::Util::Tagging
extend Puppet::Util::Pson
include Puppet::Util::Cacher::Expirer
# The host name this is a catalog for.
attr_accessor :name
# The catalog version. Used for testing whether a catalog
# is up to date.
attr_accessor :version
# How long this catalog took to retrieve. Used for reporting stats.
attr_accessor :retrieval_duration
# Whether this is a host catalog, which behaves very differently.
# In particular, reports are sent, graphs are made, and state is
# stored in the state database. If this is set incorrectly, then you often
# end up in infinite loops, because catalogs are used to make things
# that the host catalog needs.
attr_accessor :host_config
# Whether this catalog was retrieved from the cache, which affects
# whether it is written back out again.
attr_accessor :from_cache
# Some metadata to help us compile and generally respond to the current state.
attr_accessor :client_version, :server_version
# Add classes to our class list.
def add_class(*classes)
classes.each do |klass|
@classes << klass
end
# Add the class names as tags, too.
tag(*classes)
end
def title_key_for_ref( ref )
ref =~ /^([-\w:]+)\[(.*)\]$/m
[$1, $2]
end
- # Add one or more resources to our graph and to our resource table.
+ # Add a resource to our graph and to our resource table.
# This is actually a relatively complicated method, because it handles multiple
# aspects of Catalog behaviour:
# * Add the resource to the resource table
# * Add the resource to the resource graph
# * Add the resource to the relationship graph
# * Add any aliases that make sense for the resource (e.g., name != title)
- def add_resource(*resources)
- resources.each do |resource|
- raise ArgumentError, "Can only add objects that respond to :ref, not instances of #{resource.class}" unless resource.respond_to?(:ref)
- end.each { |resource| fail_on_duplicate_type_and_title(resource) }.each do |resource|
- title_key = title_key_for_ref(resource.ref)
-
- @transient_resources << resource if applying?
- @resource_table[title_key] = resource
-
- # If the name and title differ, set up an alias
-
- if resource.respond_to?(:name) and resource.respond_to?(:title) and resource.respond_to?(:isomorphic?) and resource.name != resource.title
- self.alias(resource, resource.uniqueness_key) if resource.isomorphic?
- end
-
- resource.catalog = self if resource.respond_to?(:catalog=)
-
- add_vertex(resource)
-
- @relationship_graph.add_vertex(resource) if @relationship_graph
-
- yield(resource) if block_given?
+ def add_resource(*resource)
+ add_resource(*resource[0..-2]) if resource.length > 1
+ resource = resource.pop
+ raise ArgumentError, "Can only add objects that respond to :ref, not instances of #{resource.class}" unless resource.respond_to?(:ref)
+ fail_on_duplicate_type_and_title(resource)
+ title_key = title_key_for_ref(resource.ref)
+
+ @transient_resources << resource if applying?
+ @resource_table[title_key] = resource
+
+ # If the name and title differ, set up an alias
+
+ if resource.respond_to?(:name) and resource.respond_to?(:title) and resource.respond_to?(:isomorphic?) and resource.name != resource.title
+ self.alias(resource, resource.uniqueness_key) if resource.isomorphic?
end
+
+ resource.catalog = self if resource.respond_to?(:catalog=)
+ add_vertex(resource)
+ @relationship_graph.add_vertex(resource) if @relationship_graph
end
# Create an alias for a resource.
def alias(resource, key)
resource.ref =~ /^(.+)\[/
class_name = $1 || resource.class.name
newref = [class_name, key]
if key.is_a? String
ref_string = "#{class_name}[#{key}]"
return if ref_string == resource.ref
end
# LAK:NOTE It's important that we directly compare the references,
# because sometimes an alias is created before the resource is
# added to the catalog, so comparing inside the below if block
# isn't sufficient.
if existing = @resource_table[newref]
return if existing == resource
raise(ArgumentError, "Cannot alias #{resource.ref} to #{key.inspect}; resource #{newref.inspect} already exists")
end
@resource_table[newref] = resource
@aliases[resource.ref] ||= []
@aliases[resource.ref] << newref
end
# Apply our catalog to the local host. Valid options
# are:
# :tags - set the tags that restrict what resources run
# during the transaction
# :ignoreschedules - tell the transaction to ignore schedules
# when determining the resources to run
def apply(options = {})
@applying = true
# Expire all of the resource data -- this ensures that all
# data we're operating against is entirely current.
expire
Puppet::Util::Storage.load if host_config?
transaction = Puppet::Transaction.new(self)
transaction.report = options[:report] if options[:report]
transaction.tags = options[:tags] if options[:tags]
transaction.ignoreschedules = true if options[:ignoreschedules]
transaction.add_times :config_retrieval => self.retrieval_duration || 0
begin
transaction.evaluate
rescue Puppet::Error => detail
puts detail.backtrace if Puppet[:trace]
Puppet.err "Could not apply complete catalog: #{detail}"
rescue => detail
puts detail.backtrace if Puppet[:trace]
Puppet.err "Got an uncaught exception of type #{detail.class}: #{detail}"
ensure
# Don't try to store state unless we're a host config
# too recursive.
Puppet::Util::Storage.store if host_config?
end
yield transaction if block_given?
return transaction
ensure
@applying = false
cleanup
end
# Are we in the middle of applying the catalog?
def applying?
@applying
end
def clear(remove_resources = true)
super()
# We have to do this so that the resources clean themselves up.
@resource_table.values.each { |resource| resource.remove } if remove_resources
@resource_table.clear
if @relationship_graph
@relationship_graph.clear
@relationship_graph = nil
end
end
def classes
@classes.dup
end
# Create a new resource and register it in the catalog.
def create_resource(type, options)
unless klass = Puppet::Type.type(type)
raise ArgumentError, "Unknown resource type #{type}"
end
return unless resource = klass.new(options)
add_resource(resource)
resource
end
def dependent_data_expired?(ts)
if applying?
return super
else
return true
end
end
# Turn our catalog graph into an old-style tree of TransObjects and TransBuckets.
# LAK:NOTE(20081211): This is a pre-0.25 backward compatibility method.
# It can be removed as soon as xmlrpc is killed.
def extract
top = nil
current = nil
buckets = {}
unless main = resource(:stage, "main")
raise Puppet::DevError, "Could not find 'main' stage; cannot generate catalog"
end
if stages = vertices.find_all { |v| v.type == "Stage" and v.title != "main" } and ! stages.empty?
Puppet.warning "Stages are not supported by 0.24.x client; stage(s) #{stages.collect { |s| s.to_s }.join(', ') } will be ignored"
end
bucket = nil
walk(main, :out) do |source, target|
# The sources are always non-builtins.
unless tmp = buckets[source.to_s]
if tmp = buckets[source.to_s] = source.to_trans
bucket = tmp
else
# This is because virtual resources return nil. If a virtual
# container resource contains realized resources, we still need to get
# to them. So, we keep a reference to the last valid bucket
# we returned and use that if the container resource is virtual.
end
end
bucket = tmp || bucket
if child = target.to_trans
raise "No bucket created for #{source}" unless bucket
bucket.push child
# It's important that we keep a reference to any TransBuckets we've created, so
# we don't create multiple buckets for children.
buckets[target.to_s] = child unless target.builtin?
end
end
# Retrieve the bucket for the top-level scope and set the appropriate metadata.
unless result = buckets[main.to_s]
# This only happens when the catalog is entirely empty.
result = buckets[main.to_s] = main.to_trans
end
result.classes = classes
# Clear the cache to encourage the GC
buckets.clear
result
end
# Make sure all of our resources are "finished".
def finalize
make_default_resources
@resource_table.values.each { |resource| resource.finish }
write_graph(:resources)
end
def host_config?
host_config
end
def initialize(name = nil)
super()
@name = name if name
@classes = []
@resource_table = {}
@transient_resources = []
@applying = false
@relationship_graph = nil
@host_config = true
@aliases = {}
if block_given?
yield(self)
finalize
end
end
# Make the default objects necessary for function.
def make_default_resources
# We have to add the resources to the catalog, or else they won't get cleaned up after
# the transaction.
# First create the default scheduling objects
Puppet::Type.type(:schedule).mkdefaultschedules.each { |res| add_resource(res) unless resource(res.ref) }
# And filebuckets
if bucket = Puppet::Type.type(:filebucket).mkdefaultbucket
add_resource(bucket) unless resource(bucket.ref)
end
end
# Create a graph of all of the relationships in our catalog.
def relationship_graph
unless @relationship_graph
# It's important that we assign the graph immediately, because
# the debug messages below use the relationships in the
# relationship graph to determine the path to the resources
# spitting out the messages. If this is not set,
# then we get into an infinite loop.
@relationship_graph = Puppet::SimpleGraph.new
# First create the dependency graph
self.vertices.each do |vertex|
@relationship_graph.add_vertex vertex
vertex.builddepends.each do |edge|
@relationship_graph.add_edge(edge)
end
end
# Lastly, add in any autorequires
@relationship_graph.vertices.each do |vertex|
vertex.autorequire(self).each do |edge|
unless @relationship_graph.edge?(edge.source, edge.target) # don't let automatic relationships conflict with manual ones.
unless @relationship_graph.edge?(edge.target, edge.source)
vertex.debug "Autorequiring #{edge.source}"
@relationship_graph.add_edge(edge)
else
vertex.debug "Skipping automatic relationship with #{(edge.source == vertex ? edge.target : edge.source)}"
end
end
end
end
@relationship_graph.write_graph(:relationships) if host_config?
# Then splice in the container information
- @relationship_graph.splice!(self, Puppet::Type::Component)
+ splice!(@relationship_graph)
@relationship_graph.write_graph(:expanded_relationships) if host_config?
end
@relationship_graph
end
+ # Impose our container information on another graph by using it
+ # to replace any container vertices X with a pair of verticies
+ # { admissible_X and completed_X } such that that
+ #
+ # 0) completed_X depends on admissible_X
+ # 1) contents of X each depend on admissible_X
+ # 2) completed_X depends on each on the contents of X
+ # 3) everything which depended on X depens on completed_X
+ # 4) admissible_X depends on everything X depended on
+ # 5) the containers and their edges must be removed
+ #
+ # Note that this requires attention to the possible case of containers
+ # which contain or depend on other containers, but has the advantage
+ # that the number of new edges created scales linearly with the number
+ # of contained verticies regardless of how containers are related;
+ # alternatives such as replacing container-edges with content-edges
+ # scale as the product of the number of external dependences, which is
+ # to say geometrically in the case of nested / chained containers.
+ #
+ Default_label = { :callback => :refresh, :event => :ALL_EVENTS }
+ def splice!(other)
+ stage_class = Puppet::Type.type(:stage)
+ whit_class = Puppet::Type.type(:whit)
+ component_class = Puppet::Type.type(:component)
+ containers = vertices.find_all { |v| (v.is_a?(component_class) or v.is_a?(stage_class)) and vertex?(v) }
+ #
+ # These two hashes comprise the aforementioned attention to the possible
+ # case of containers that contain / depend on other containers; they map
+ # containers to their sentinals but pass other verticies through. Thus we
+ # can "do the right thing" for references to other verticies that may or
+ # may not be containers.
+ #
+ admissible = Hash.new { |h,k| k }
+ completed = Hash.new { |h,k| k }
+ containers.each { |x|
+ admissible[x] = whit_class.new(:name => "admissible_#{x.name}", :catalog => self)
+ completed[x] = whit_class.new(:name => "completed_#{x.name}", :catalog => self)
+ }
+ #
+ # Implement the six requierments listed above
+ #
+ containers.each { |x|
+ contents = adjacent(x, :direction => :out)
+ other.add_edge(admissible[x],completed[x]) if contents.empty? # (0)
+ contents.each { |v|
+ other.add_edge(admissible[x],admissible[v],Default_label) # (1)
+ other.add_edge(completed[v], completed[x], Default_label) # (2)
+ }
+ # (3) & (5)
+ other.adjacent(x,:direction => :in,:type => :edges).each { |e|
+ other.add_edge(completed[e.source],admissible[x],e.label)
+ other.remove_edge! e
+ }
+ # (4) & (5)
+ other.adjacent(x,:direction => :out,:type => :edges).each { |e|
+ other.add_edge(completed[x],admissible[e.target],e.label)
+ other.remove_edge! e
+ }
+ }
+ containers.each { |x| other.remove_vertex! x } # (5)
+ end
+
# Remove the resource from our catalog. Notice that we also call
# 'remove' on the resource, at least until resource classes no longer maintain
# references to the resource instances.
def remove_resource(*resources)
resources.each do |resource|
@resource_table.delete(resource.ref)
if aliases = @aliases[resource.ref]
aliases.each { |res_alias| @resource_table.delete(res_alias) }
@aliases.delete(resource.ref)
end
remove_vertex!(resource) if vertex?(resource)
@relationship_graph.remove_vertex!(resource) if @relationship_graph and @relationship_graph.vertex?(resource)
resource.remove
end
end
# Look a resource up by its reference (e.g., File[/etc/passwd]).
def resource(type, title = nil)
# Always create a resource reference, so that it always canonizes how we
# are referring to them.
if title
res = Puppet::Resource.new(type, title)
else
# If they didn't provide a title, then we expect the first
# argument to be of the form 'Class[name]', which our
# Reference class canonizes for us.
res = Puppet::Resource.new(nil, type)
end
title_key = [res.type, res.title.to_s]
uniqueness_key = [res.type, res.uniqueness_key]
@resource_table[title_key] || @resource_table[uniqueness_key]
end
def resource_refs
resource_keys.collect{ |type, name| name.is_a?( String ) ? "#{type}[#{name}]" : nil}.compact
end
def resource_keys
@resource_table.keys
end
def resources
@resource_table.values.uniq
end
def self.from_pson(data)
result = new(data['name'])
if tags = data['tags']
result.tag(*tags)
end
if version = data['version']
result.version = version
end
if resources = data['resources']
resources = PSON.parse(resources) if resources.is_a?(String)
resources.each do |res|
resource_from_pson(result, res)
end
end
if edges = data['edges']
edges = PSON.parse(edges) if edges.is_a?(String)
edges.each do |edge|
edge_from_pson(result, edge)
end
end
if classes = data['classes']
result.add_class(*classes)
end
result
end
def self.edge_from_pson(result, edge)
# If no type information was presented, we manually find
# the class.
edge = Puppet::Relationship.from_pson(edge) if edge.is_a?(Hash)
unless source = result.resource(edge.source)
raise ArgumentError, "Could not convert from pson: Could not find relationship source #{edge.source.inspect}"
end
edge.source = source
unless target = result.resource(edge.target)
raise ArgumentError, "Could not convert from pson: Could not find relationship target #{edge.target.inspect}"
end
edge.target = target
result.add_edge(edge)
end
def self.resource_from_pson(result, res)
res = Puppet::Resource.from_pson(res) if res.is_a? Hash
result.add_resource(res)
end
PSON.register_document_type('Catalog',self)
def to_pson_data_hash
{
'document_type' => 'Catalog',
'data' => {
'tags' => tags,
'name' => name,
'version' => version,
'resources' => vertices.collect { |v| v.to_pson_data_hash },
'edges' => edges. collect { |e| e.to_pson_data_hash },
'classes' => classes
},
'metadata' => {
'api_version' => 1
}
}
end
def to_pson(*args)
to_pson_data_hash.to_pson(*args)
end
# Convert our catalog into a RAL catalog.
def to_ral
to_catalog :to_ral
end
# Convert our catalog into a catalog of Puppet::Resource instances.
def to_resource
to_catalog :to_resource
end
# filter out the catalog, applying +block+ to each resource.
# If the block result is false, the resource will
# be kept otherwise it will be skipped
def filter(&block)
to_catalog :to_resource, &block
end
# Store the classes in the classfile.
def write_class_file
::File.open(Puppet[:classfile], "w") do |f|
f.puts classes.join("\n")
end
rescue => detail
Puppet.err "Could not create class file #{Puppet[:classfile]}: #{detail}"
end
# Produce the graph files if requested.
def write_graph(name)
# We only want to graph the main host catalog.
return unless host_config?
super
end
private
def cleanup
# Expire any cached data the resources are keeping.
expire
end
# Verify that the given resource isn't defined elsewhere.
def fail_on_duplicate_type_and_title(resource)
# Short-curcuit the common case,
return unless existing_resource = @resource_table[title_key_for_ref(resource.ref)]
# If we've gotten this far, it's a real conflict
msg = "Duplicate definition: #{resource.ref} is already defined"
msg << " in file #{existing_resource.file} at line #{existing_resource.line}" if existing_resource.file and existing_resource.line
msg << "; cannot redefine" if resource.line or resource.file
raise DuplicateResourceError.new(msg)
end
# An abstracted method for converting one catalog into another type of catalog.
# This pretty much just converts all of the resources from one class to another, using
# a conversion method.
def to_catalog(convert)
result = self.class.new(self.name)
result.version = self.version
map = {}
vertices.each do |resource|
next if virtual_not_exported?(resource)
next if block_given? and yield resource
#This is hackity hack for 1094
#Aliases aren't working in the ral catalog because the current instance of the resource
#has a reference to the catalog being converted. . . So, give it a reference to the new one
#problem solved. . .
if resource.class == Puppet::Resource
resource = resource.dup
resource.catalog = result
elsif resource.is_a?(Puppet::TransObject)
resource = resource.dup
resource.catalog = result
elsif resource.is_a?(Puppet::Parser::Resource)
resource = resource.to_resource
resource.catalog = result
end
if resource.is_a?(Puppet::Resource) and convert.to_s == "to_resource"
newres = resource
else
newres = resource.send(convert)
end
# We can't guarantee that resources don't munge their names
# (like files do with trailing slashes), so we have to keep track
# of what a resource got converted to.
map[resource.ref] = newres
result.add_resource newres
end
message = convert.to_s.gsub "_", " "
edges.each do |edge|
# Skip edges between virtual resources.
next if virtual_not_exported?(edge.source)
next if block_given? and yield edge.source
next if virtual_not_exported?(edge.target)
next if block_given? and yield edge.target
unless source = map[edge.source.ref]
raise Puppet::DevError, "Could not find resource #{edge.source.ref} when converting #{message} resources"
end
unless target = map[edge.target.ref]
raise Puppet::DevError, "Could not find resource #{edge.target.ref} when converting #{message} resources"
end
result.add_edge(source, target, edge.label)
end
map.clear
result.add_class(*self.classes)
result.tag(*self.tags)
result
end
def virtual_not_exported?(resource)
resource.respond_to?(:virtual?) and resource.virtual? and (resource.respond_to?(:exported?) and not resource.exported?)
end
end
diff --git a/lib/puppet/simple_graph.rb b/lib/puppet/simple_graph.rb
index 27e068e30..671eef150 100644
--- a/lib/puppet/simple_graph.rb
+++ b/lib/puppet/simple_graph.rb
@@ -1,594 +1,553 @@
require 'puppet/external/dot'
require 'puppet/relationship'
require 'set'
# A hopefully-faster graph class to replace the use of GRATR.
class Puppet::SimpleGraph
#
# All public methods of this class must maintain (assume ^ ensure) the following invariants, where "=~=" means
# equiv. up to order:
#
# @in_to.keys =~= @out_to.keys =~= all vertices
# @in_to.values.collect { |x| x.values }.flatten =~= @out_from.values.collect { |x| x.values }.flatten =~= all edges
# @in_to[v1][v2] =~= @out_from[v2][v1] =~= all edges from v1 to v2
# @in_to [v].keys =~= vertices with edges leading to v
# @out_from[v].keys =~= vertices with edges leading from v
# no operation may shed reference loops (for gc)
# recursive operation must scale with the depth of the spanning trees, or better (e.g. no recursion over the set
# of all vertices, etc.)
#
# This class is intended to be used with DAGs. However, if the
# graph has a cycle, it will not cause non-termination of any of the
- # algorithms. The topsort method detects and reports cycles.
+ # algorithms.
#
def initialize
@in_to = {}
@out_from = {}
@upstream_from = {}
@downstream_from = {}
end
# Clear our graph.
def clear
@in_to.clear
@out_from.clear
@upstream_from.clear
@downstream_from.clear
end
# Which resources depend upon the given resource.
def dependencies(resource)
vertex?(resource) ? upstream_from_vertex(resource).keys : []
end
def dependents(resource)
vertex?(resource) ? downstream_from_vertex(resource).keys : []
end
# Whether our graph is directed. Always true. Used to produce dot files.
def directed?
true
end
# Determine all of the leaf nodes below a given vertex.
def leaves(vertex, direction = :out)
tree_from_vertex(vertex, direction).keys.find_all { |c| adjacent(c, :direction => direction).empty? }
end
# Collect all of the edges that the passed events match. Returns
# an array of edges.
def matching_edges(event, base = nil)
source = base || event.resource
unless vertex?(source)
Puppet.warning "Got an event from invalid vertex #{source.ref}"
return []
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.
@out_from[source].values.flatten.find_all { |edge| edge.match?(event.name) }
end
# Return a reversed version of this graph.
def reversal
result = self.class.new
vertices.each { |vertex| result.add_vertex(vertex) }
edges.each do |edge|
result.add_edge edge.class.new(edge.target, edge.source, edge.label)
end
result
end
# Return the size of the graph.
def size
vertices.size
end
def to_a
vertices
end
# This is a simple implementation of Tarjan's algorithm to find strongly
# connected components in the graph; this is a fairly ugly implementation,
# because I can't just decorate the vertices themselves.
#
# This method has an unhealthy relationship with the find_cycles_in_graph
# method below, which contains the knowledge of how the state object is
# maintained.
def tarjan(root, s)
# initialize the recursion stack we use to work around the nasty lack of a
# decent Ruby stack.
recur = [{ :node => root }]
while not recur.empty? do
frame = recur.last
vertex = frame[:node]
case frame[:step]
when nil then
s[:index][vertex] = s[:number]
s[:lowlink][vertex] = s[:number]
s[:number] = s[:number] + 1
s[:stack].push(vertex)
s[:seen][vertex] = true
frame[:children] = adjacent(vertex)
frame[:step] = :children
when :children then
if frame[:children].length > 0 then
child = frame[:children].shift
if ! s[:index][child] then
# Never seen, need to recurse.
frame[:step] = :after_recursion
frame[:child] = child
recur.push({ :node => child })
elsif s[:seen][child] then
s[:lowlink][vertex] = [s[:lowlink][vertex], s[:index][child]].min
end
else
if s[:lowlink][vertex] == s[:index][vertex] then
this_scc = []
begin
top = s[:stack].pop
s[:seen][top] = false
this_scc << top
end until top == vertex
# NOTE: if we don't reverse we get the components in the opposite
# order to what a human being would expect; reverse should be an
# O(1) operation, without even copying, because we know the length
# of the source, but I worry that an implementation will get this
# wrong. Still, the worst case is O(n) for n vertices as we can't
# possibly put a vertex into two SCCs.
#
# Also, my feeling is that most implementations are going to do
# better with a reverse operation than a string of 'unshift'
# insertions at the head of the array; if they were going to mess
# up the performance of one, it would be unshift.
s[:scc] << this_scc.reverse
end
recur.pop # done with this node, finally.
end
when :after_recursion then
s[:lowlink][vertex] = [s[:lowlink][vertex], s[:lowlink][frame[:child]]].min
frame[:step] = :children
else
fail "#{frame[:step]} is an unknown step"
end
end
end
# Find all cycles in the graph by detecting all the strongly connected
# components, then eliminating everything with a size of one as
# uninteresting - which it is, because it can't be a cycle. :)
#
# This has an unhealthy relationship with the 'tarjan' method above, which
# it uses to implement the detection of strongly connected components.
def find_cycles_in_graph
state = {
:number => 0, :index => {}, :lowlink => {}, :scc => [],
:stack => [], :seen => {}
}
# we usually have a disconnected graph, must walk all possible roots
vertices.each do |vertex|
if ! state[:index][vertex] then
tarjan vertex, state
end
end
state[:scc].select { |c| c.length > 1 }
end
# Perform a BFS on the sub graph representing the cycle, with a view to
# generating a sufficient set of paths to report the cycle meaningfully, and
# ideally usefully, for the end user.
#
# BFS is preferred because it will generally report the shortest paths
# through the graph first, which are more likely to be interesting to the
# user. I think; it would be interesting to verify that. --daniel 2011-01-23
def paths_in_cycle(cycle, max_paths = 1)
raise ArgumentError, "negative or zero max_paths" if max_paths < 1
# Calculate our filtered outbound vertex lists...
adj = {}
cycle.each do |vertex|
adj[vertex] = adjacent(vertex).select{|s| cycle.member? s}
end
found = []
# frame struct is vertex, [path]
stack = [[cycle.first, []]]
while frame = stack.shift do
if frame[1].member?(frame[0]) then
found << frame[1] + [frame[0]]
break if found.length >= max_paths
else
adj[frame[0]].each do |to|
stack.push [to, frame[1] + [frame[0]]]
end
end
end
return found
end
def report_cycles_in_graph
cycles = find_cycles_in_graph
n = cycles.length # where is "pluralize"? --daniel 2011-01-22
+ return if n == 0
s = n == 1 ? '' : 's'
message = "Found #{n} dependency cycle#{s}:\n"
cycles.each do |cycle|
paths = paths_in_cycle(cycle)
message += paths.map{ |path| '(' + path.join(" => ") + ')'}.join("\n") + "\n"
end
if Puppet[:graph] then
filename = write_cycles_to_graph(cycles)
message += "Cycle graph written to #{filename}."
else
message += "Try the '--graph' option and opening the "
message += "resulting '.dot' file in OmniGraffle or GraphViz"
end
raise Puppet::Error, message
end
def write_cycles_to_graph(cycles)
# This does not use the DOT graph library, just writes the content
# directly. Given the complexity of this, there didn't seem much point
# using a heavy library to generate exactly the same content. --daniel 2011-01-27
Puppet.settings.use(:graphing)
graph = ["digraph Resource_Cycles {"]
graph << ' label = "Resource Cycles"'
cycles.each do |cycle|
paths_in_cycle(cycle, 10).each do |path|
graph << path.map { |v| '"' + v.to_s.gsub(/"/, '\\"') + '"' }.join(" -> ")
end
end
graph << '}'
filename = File.join(Puppet[:graphdir], "cycles.dot")
File.open(filename, "w") { |f| f.puts graph }
return filename
end
- # Provide a topological sort.
- def topsort
- degree = {}
- zeros = []
- result = []
-
- # Collect each of our vertices, with the number of in-edges each has.
- vertices.each do |v|
- edges = @in_to[v]
- zeros << v if edges.empty?
- degree[v] = edges.length
- end
-
- # Iterate over each 0-degree vertex, decrementing the degree of
- # each of its out-edges.
- while v = zeros.pop
- result << v
- @out_from[v].each { |v2,es|
- zeros << v2 if (degree[v2] -= 1) == 0
- }
- end
-
- # If we have any vertices left with non-zero in-degrees, then we've found a cycle.
- if cycles = degree.values.reject { |ns| ns == 0 } and cycles.length > 0
- report_cycles_in_graph
- end
-
- result
- end
-
# Add a new vertex to the graph.
def add_vertex(vertex)
@in_to[vertex] ||= {}
@out_from[vertex] ||= {}
end
# Remove a vertex from the graph.
def remove_vertex!(v)
return unless vertex?(v)
@upstream_from.clear
@downstream_from.clear
(@in_to[v].values+@out_from[v].values).flatten.each { |e| remove_edge!(e) }
@in_to.delete(v)
@out_from.delete(v)
end
# Test whether a given vertex is in the graph.
def vertex?(v)
@in_to.include?(v)
end
# Return a list of all vertices.
def vertices
@in_to.keys
end
# Add a new edge. The graph user has to create the edge instance,
# since they have to specify what kind of edge it is.
def add_edge(e,*a)
return add_relationship(e,*a) unless a.empty?
@upstream_from.clear
@downstream_from.clear
add_vertex(e.source)
add_vertex(e.target)
@in_to[ e.target][e.source] ||= []; @in_to[ e.target][e.source] |= [e]
@out_from[e.source][e.target] ||= []; @out_from[e.source][e.target] |= [e]
end
def add_relationship(source, target, label = nil)
add_edge Puppet::Relationship.new(source, target, label)
end
# Find all matching edges.
def edges_between(source, target)
(@out_from[source] || {})[target] || []
end
# Is there an edge between the two vertices?
def edge?(source, target)
vertex?(source) and vertex?(target) and @out_from[source][target]
end
def edges
@in_to.values.collect { |x| x.values }.flatten
end
def each_edge
@in_to.each { |t,ns| ns.each { |s,es| es.each { |e| yield e }}}
end
# Remove an edge from our graph.
def remove_edge!(e)
if edge?(e.source,e.target)
@upstream_from.clear
@downstream_from.clear
@in_to [e.target].delete e.source if (@in_to [e.target][e.source] -= [e]).empty?
@out_from[e.source].delete e.target if (@out_from[e.source][e.target] -= [e]).empty?
end
end
# Find adjacent edges.
def adjacent(v, options = {})
return [] unless ns = (options[:direction] == :in) ? @in_to[v] : @out_from[v]
(options[:type] == :edges) ? ns.values.flatten : ns.keys
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)
- # We have to get the container list via a topological sort on the
- # configuration graph, because otherwise containers that contain
- # other containers will add those containers back into the
- # graph. We could get a similar affect by only setting relationships
- # to container leaves, but that would result in many more
- # relationships.
- stage_class = Puppet::Type.type(:stage)
- whit_class = Puppet::Type.type(:whit)
- containers = other.topsort.find_all { |v| (v.is_a?(type) or v.is_a?(stage_class)) and vertex?(v) }
- containers.each do |container|
- # Get the list of children from the other graph.
- children = other.adjacent(container, :direction => :out)
-
- # MQR TODO: Luke suggests that it should be possible to refactor the system so that
- # container nodes are retained, thus obviating the need for the whit.
- children = [whit_class.new(:name => container.name, :catalog => other)] if children.empty?
-
- # First create new edges for each of the :in edges
- [:in, :out].each do |dir|
- edges = adjacent(container, :direction => dir, :type => :edges)
- edges.each do |edge|
- children.each do |child|
- if dir == :in
- s = edge.source
- t = child
- else
- s = child
- t = edge.target
- end
-
- add_edge(s, t, edge.label)
- end
-
- # Now get rid of the edge, so remove_vertex! works correctly.
- remove_edge!(edge)
- end
- end
- remove_vertex!(container)
- end
- end
-
# Just walk the tree and pass each edge.
def walk(source, direction)
# Use an iterative, breadth-first traversal of the graph. One could do
# this recursively, but Ruby's slow function calls and even slower
# recursion make the shorter, recursive algorithm cost-prohibitive.
stack = [source]
seen = Set.new
until stack.empty?
node = stack.shift
next if seen.member? node
connected = adjacent(node, :direction => direction)
connected.each do |target|
yield node, target
end
stack.concat(connected)
seen << node
end
end
# A different way of walking a tree, and a much faster way than the
# one that comes with GRATR.
def tree_from_vertex(start, direction = :out)
predecessor={}
walk(start, direction) do |parent, child|
predecessor[child] = parent
end
predecessor
end
def downstream_from_vertex(v)
return @downstream_from[v] if @downstream_from[v]
result = @downstream_from[v] = {}
@out_from[v].keys.each do |node|
result[node] = 1
result.update(downstream_from_vertex(node))
end
result
end
+ def direct_dependents_of(v)
+ (@out_from[v] || {}).keys
+ end
+
def upstream_from_vertex(v)
return @upstream_from[v] if @upstream_from[v]
result = @upstream_from[v] = {}
@in_to[v].keys.each do |node|
result[node] = 1
result.update(upstream_from_vertex(node))
end
result
end
+ def direct_dependencies_of(v)
+ (@in_to[v] || {}).keys
+ end
+
+ # Return an array of the edge-sets between a series of n+1 vertices (f=v0,v1,v2...t=vn)
+ # connecting the two given verticies. The ith edge set is an array containing all the
+ # edges between v(i) and v(i+1); these are (by definition) never empty.
+ #
+ # * if f == t, the list is empty
+ # * if they are adjacent the result is an array consisting of
+ # a single array (the edges from f to t)
+ # * and so on by induction on a vertex m between them
+ # * if there is no path from f to t, the result is nil
+ #
+ # This implementation is not particularly efficient; it's used in testing where clarity
+ # is more important than last-mile efficiency.
+ #
+ def path_between(f,t)
+ if f==t
+ []
+ elsif direct_dependents_of(f).include?(t)
+ [edges_between(f,t)]
+ elsif dependents(f).include?(t)
+ m = (dependents(f) & direct_dependencies_of(t)).first
+ path_between(f,m) + path_between(m,t)
+ else
+ nil
+ end
+ end
+
# LAK:FIXME This is just a paste of the GRATR code with slight modifications.
# Return a DOT::DOTDigraph for directed graphs or a DOT::DOTSubgraph for an
# undirected Graph. _params_ can contain any graph property specified in
# rdot.rb. If an edge or vertex label is a kind of Hash then the keys
# which match +dot+ properties will be used as well.
def to_dot_graph (params = {})
params['name'] ||= self.class.name.gsub(/:/,'_')
fontsize = params['fontsize'] ? params['fontsize'] : '8'
graph = (directed? ? DOT::DOTDigraph : DOT::DOTSubgraph).new(params)
edge_klass = directed? ? DOT::DOTDirectedEdge : DOT::DOTEdge
vertices.each do |v|
name = v.to_s
params = {'name' => '"'+name+'"',
'fontsize' => fontsize,
'label' => name}
v_label = v.to_s
params.merge!(v_label) if v_label and v_label.kind_of? Hash
graph << DOT::DOTNode.new(params)
end
edges.each do |e|
params = {'from' => '"'+ e.source.to_s + '"',
'to' => '"'+ e.target.to_s + '"',
'fontsize' => fontsize }
e_label = e.to_s
params.merge!(e_label) if e_label and e_label.kind_of? Hash
graph << edge_klass.new(params)
end
graph
end
# Output the dot format as a string
def to_dot (params={}) to_dot_graph(params).to_s; end
# Call +dotty+ for the graph which is written to the file 'graph.dot'
# in the # current directory.
def dotty (params = {}, dotfile = 'graph.dot')
File.open(dotfile, 'w') {|f| f << to_dot(params) }
system('dotty', dotfile)
end
# Produce the graph files if requested.
def write_graph(name)
return unless Puppet[:graph]
Puppet.settings.use(:graphing)
file = File.join(Puppet[:graphdir], "#{name}.dot")
File.open(file, "w") { |f|
f.puts to_dot("name" => name.to_s.capitalize)
}
end
# This flag may be set to true to use the new YAML serialzation
# format (where @vertices is a simple list of vertices rather than a
# list of VertexWrapper objects). Deserialization supports both
# formats regardless of the setting of this flag.
class << self
attr_accessor :use_new_yaml_format
end
self.use_new_yaml_format = false
# Stub class to allow graphs to be represented in YAML using the old
# (version 2.6) format.
class VertexWrapper
attr_reader :vertex, :adjacencies
def initialize(vertex, adjacencies)
@vertex = vertex
@adjacencies = adjacencies
end
def inspect
{ :@adjacencies => @adjacencies, :@vertex => @vertex.to_s }.inspect
end
end
# instance_variable_get is used by Object.to_zaml to get instance
# variables. Override it so that we can simulate the presence of
# instance variables @edges and @vertices for serialization.
def instance_variable_get(v)
case v.to_s
when '@edges' then
edges
when '@vertices' then
if self.class.use_new_yaml_format
vertices
else
result = {}
vertices.each do |vertex|
adjacencies = {}
[:in, :out].each do |direction|
adjacencies[direction] = {}
adjacent(vertex, :direction => direction, :type => :edges).each do |edge|
other_vertex = direction == :in ? edge.source : edge.target
(adjacencies[direction][other_vertex] ||= Set.new).add(edge)
end
end
result[vertex] = Puppet::SimpleGraph::VertexWrapper.new(vertex, adjacencies)
end
result
end
else
super(v)
end
end
def to_yaml_properties
other_vars = instance_variables.
map {|v| v.to_s}.
reject { |v| %w{@in_to @out_from @upstream_from @downstream_from}.include?(v) }
(other_vars + %w{@vertices @edges}).sort.uniq
end
def yaml_initialize(tag, var)
initialize()
vertices = var.delete('vertices')
edges = var.delete('edges')
if vertices.is_a?(Hash)
# Support old (2.6) format
vertices = vertices.keys
end
vertices.each { |v| add_vertex(v) }
edges.each { |e| add_edge(e) }
var.each do |varname, value|
instance_variable_set("@#{varname}", value)
end
end
end
diff --git a/lib/puppet/transaction.rb b/lib/puppet/transaction.rb
index eba601cfe..8118178c3 100644
--- a/lib/puppet/transaction.rb
+++ b/lib/puppet/transaction.rb
@@ -1,334 +1,372 @@
# the class that actually walks our resource/property tree, collects the changes,
# and performs them
require 'puppet'
require 'puppet/util/tagging'
require 'puppet/application'
+require 'sha1'
class Puppet::Transaction
require 'puppet/transaction/event'
require 'puppet/transaction/event_manager'
require 'puppet/transaction/resource_harness'
require 'puppet/resource/status'
attr_accessor :component, :catalog, :ignoreschedules
- attr_accessor :sorted_resources, :configurator
+ attr_accessor :configurator
# The report, once generated.
attr_accessor :report
# Routes and stores any events and subscriptions.
attr_reader :event_manager
# Handles most of the actual interacting with resources
attr_reader :resource_harness
include Puppet::Util
include Puppet::Util::Tagging
# Wraps application run state check to flag need to interrupt processing
def stop_processing?
Puppet::Application.stop_requested?
end
# Add some additional times for reporting
def add_times(hash)
hash.each do |name, num|
report.add_times(name, num)
end
end
# Are there any failed resources in this transaction?
def any_failed?
report.resource_statuses.values.detect { |status| status.failed? }
end
# Apply all changes for a resource
def apply(resource, ancestor = nil)
status = resource_harness.evaluate(resource)
add_resource_status(status)
event_manager.queue_events(ancestor || resource, status.events)
rescue => detail
resource.err "Could not evaluate: #{detail}"
end
# Find all of the changed resources.
def changed?
report.resource_statuses.values.find_all { |status| status.changed }.collect { |status| catalog.resource(status.resource) }
end
+ # Find all of the applied resources (including failed attempts).
+ def applied_resources
+ report.resource_statuses.values.collect { |status| catalog.resource(status.resource) }
+ end
+
# Copy an important relationships from the parent to the newly-generated
# child resource.
- def make_parent_child_relationship(resource, children)
- depthfirst = resource.depthfirst?
-
- children.each do |gen_child|
- if depthfirst
- edge = [gen_child, resource]
- else
- edge = [resource, gen_child]
- end
- relationship_graph.add_vertex(gen_child)
-
- unless relationship_graph.edge?(edge[1], edge[0])
- relationship_graph.add_edge(*edge)
- else
- resource.debug "Skipping automatic relationship to #{gen_child}"
- end
+ def add_conditional_directed_dependency(parent, child, label=nil)
+ relationship_graph.add_vertex(child)
+ edge = parent.depthfirst? ? [child, parent] : [parent, child]
+ if relationship_graph.edge?(*edge.reverse)
+ parent.debug "Skipping automatic relationship to #{child}"
+ else
+ relationship_graph.add_edge(edge[0],edge[1],label)
end
end
- # See if the resource generates new resources at evaluation time.
- def eval_generate(resource)
- generate_additional_resources(resource, :eval_generate)
- end
-
# Evaluate a single resource.
def eval_resource(resource, ancestor = nil)
if skip?(resource)
resource_status(resource).skipped = true
else
- eval_children_and_apply_resource(resource, ancestor)
+ resource_status(resource).scheduled = true
+ apply(resource, ancestor)
end
# Check to see if there are any events queued for this resource
event_manager.process_events(resource)
end
- def eval_children_and_apply_resource(resource, ancestor = nil)
- resource_status(resource).scheduled = true
-
- # We need to generate first regardless, because the recursive
- # actions sometimes change how the top resource is applied.
- children = eval_generate(resource)
-
- if ! children.empty? and resource.depthfirst?
- children.each do |child|
- # The child will never be skipped when the parent isn't
- eval_resource(child, ancestor || resource)
- end
- end
-
- # Perform the actual changes
- apply(resource, ancestor)
-
- if ! children.empty? and ! resource.depthfirst?
- children.each do |child|
- eval_resource(child, ancestor || resource)
- 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
# Start logging.
Puppet::Util::Log.newdestination(@report)
prepare
Puppet.info "Applying configuration version '#{catalog.version}'" if catalog.version
begin
- @sorted_resources.each do |resource|
- next if stop_processing?
+ relationship_graph.traverse do |resource|
if resource.is_a?(Puppet::Type::Component)
Puppet.warning "Somehow left a component in the relationship graph"
- next
+ else
+ seconds = thinmark { eval_resource(resource) }
+ resource.info "Evaluated in %0.2f seconds" % seconds if Puppet[:evaltrace] and @catalog.host_config?
end
- ret = nil
- seconds = thinmark do
- ret = eval_resource(resource)
- end
-
- resource.info "Evaluated in %0.2f seconds" % seconds if Puppet[:evaltrace] and @catalog.host_config?
- ret
end
ensure
# And then close the transaction log.
Puppet::Util::Log.close(@report)
end
Puppet.debug "Finishing transaction #{object_id}"
end
def events
event_manager.events
end
def failed?(resource)
s = resource_status(resource) and s.failed?
end
# Does this resource have any failed dependencies?
def failed_dependencies?(resource)
# First make sure there are no failed dependencies. To do this,
# we check for failures in any of the vertexes above us. It's not
# enough to check the immediate dependencies, which is why we use
# a tree from the reversed graph.
found_failed = false
relationship_graph.dependencies(resource).each do |dep|
next unless failed?(dep)
resource.notice "Dependency #{dep} has failures: #{resource_status(dep).failed}"
found_failed = true
end
found_failed
end
+ def eval_generate(resource)
+ raise Puppet::DevError,"Depthfirst resources are not supported by eval_generate" if resource.depthfirst?
+ begin
+ made = resource.eval_generate.uniq.reverse
+ rescue => detail
+ puts detail.backtrace if Puppet[:trace]
+ resource.err "Failed to generate additional resources using 'eval_generate: #{detail}"
+ return
+ end
+ made.each do |res|
+ begin
+ res.tag(*resource.tags)
+ @catalog.add_resource(res)
+ res.finish
+ rescue Puppet::Resource::Catalog::DuplicateResourceError
+ res.info "Duplicate generated resource; skipping"
+ end
+ end
+ sentinal = Puppet::Type::Whit.new(:name => "completed_#{resource.title}", :catalog => resource.catalog)
+ relationship_graph.adjacent(resource,:direction => :out,:type => :edges).each { |e|
+ add_conditional_directed_dependency(sentinal, e.target, e.label)
+ relationship_graph.remove_edge! e
+ }
+ default_label = Puppet::Resource::Catalog::Default_label
+ made.each do |res|
+ add_conditional_directed_dependency(made.find { |r| r != res && r.name == res.name[0,r.name.length]} || resource, res)
+ add_conditional_directed_dependency(res, sentinal, default_label)
+ end
+ add_conditional_directed_dependency(resource, sentinal, default_label)
+ end
+
# A general method for recursively generating new resources from a
# resource.
- def generate_additional_resources(resource, method)
- return [] unless resource.respond_to?(method)
+ def generate_additional_resources(resource)
+ return unless resource.respond_to?(:generate)
begin
- made = resource.send(method)
+ made = resource.generate
rescue => detail
puts detail.backtrace if Puppet[:trace]
- resource.err "Failed to generate additional resources using '#{method}': #{detail}"
+ resource.err "Failed to generate additional resources using 'generate': #{detail}"
end
- return [] unless made
+ return unless made
made = [made] unless made.is_a?(Array)
- made.uniq.find_all do |res|
+ made.uniq.each do |res|
begin
res.tag(*resource.tags)
- @catalog.add_resource(res) do |r|
- r.finish
- make_parent_child_relationship(resource, [r])
-
- # Call 'generate' recursively
- generate_additional_resources(r, method)
- end
- true
+ @catalog.add_resource(res)
+ res.finish
+ add_conditional_directed_dependency(resource, res)
+ generate_additional_resources(res)
rescue Puppet::Resource::Catalog::DuplicateResourceError
res.info "Duplicate generated resource; skipping"
- false
end
end
end
# Collect any dynamically generated resources. This method is called
# before the transaction starts.
- def generate
- list = @catalog.vertices
- newlist = []
- while ! list.empty?
- list.each do |resource|
- newlist += generate_additional_resources(resource, :generate)
- end
- list = newlist
- newlist = []
- end
+ def xgenerate
+ @catalog.vertices.each { |resource| generate_additional_resources(resource) }
end
# Should we ignore tags?
def ignore_tags?
! (@catalog.host_config? or Puppet[:name] == "puppet")
end
# this should only be called by a Puppet::Type::Component resource now
# and it should only receive an array
def initialize(catalog)
@catalog = catalog
@report = Puppet::Transaction::Report.new("apply")
@event_manager = Puppet::Transaction::EventManager.new(self)
@resource_harness = Puppet::Transaction::ResourceHarness.new(self)
end
# Prefetch any providers that support it. We don't support prefetching
# types, just providers.
def prefetch
prefetchers = {}
@catalog.vertices.each do |resource|
if provider = resource.provider and provider.class.respond_to?(:prefetch)
prefetchers[provider.class] ||= {}
prefetchers[provider.class][resource.name] = resource
end
end
# Now call prefetch, passing in the resources so that the provider instances can be replaced.
prefetchers.each do |provider, resources|
Puppet.debug "Prefetching #{provider.name} resources for #{provider.resource_type.name}"
begin
provider.prefetch(resources)
rescue => detail
puts detail.backtrace if Puppet[:trace]
Puppet.err "Could not prefetch #{provider.resource_type.name} provider '#{provider.name}': #{detail}"
end
end
end
# Prepare to evaluate the resources in a transaction.
def prepare
# Now add any dynamically generated resources
- generate
+ xgenerate
# Then prefetch. It's important that we generate and then prefetch,
# so that any generated resources also get prefetched.
prefetch
+ end
+
- # This will throw an error if there are cycles in the graph.
- @sorted_resources = relationship_graph.topsort
+ # We want to monitor changes in the relationship graph of our
+ # catalog but this is complicated by the fact that the catalog
+ # both is_a graph and has_a graph, by the fact that changes to
+ # the structure of the object can have adverse serialization
+ # effects, by threading issues, by order-of-initialization issues,
+ # etc.
+ #
+ # Since the proper lifetime/scope of the monitoring is a transaction
+ # and the transaction is already commiting a mild law-of-demeter
+ # transgression, we cut the Gordian knot here by simply wrapping the
+ # transaction's view of the resource graph to capture and maintain
+ # the information we need. Nothing outside the transaction needs
+ # this information, and nothing outside the transaction can see it
+ # except via the Transaction#relationship_graph
+
+ class Relationship_graph_wrapper
+ attr_reader :real_graph,:transaction,:ready,:generated,:done,:unguessable_deterministic_key
+ def initialize(real_graph,transaction)
+ @real_graph = real_graph
+ @transaction = transaction
+ @ready = {}
+ @generated = {}
+ @done = {}
+ @unguessable_deterministic_key = Hash.new { |h,k| h[k] = Digest::SHA1.hexdigest("NaCl, MgSO4 (salts) and then #{k.title}") }
+ vertices.each { |v| check_if_now_ready(v) }
+ end
+ def method_missing(*args,&block)
+ real_graph.send(*args,&block)
+ end
+ def add_vertex(v)
+ real_graph.add_vertex(v)
+ check_if_now_ready(v) # ?????????????????????????????????????????
+ end
+ def add_edge(f,t,label=nil)
+ ready.delete(t)
+ real_graph.add_edge(f,t,label)
+ end
+ def check_if_now_ready(r)
+ ready[r] = true if direct_dependencies_of(r).all? { |r2| done[r2] }
+ end
+ def next_resource
+ ready.keys.sort_by { |r0| unguessable_deterministic_key[r0] }.first
+ end
+ def traverse(&block)
+ real_graph.report_cycles_in_graph
+ while (r = next_resource) && !transaction.stop_processing?
+ if !generated[r] && r.respond_to?(:eval_generate)
+ transaction.eval_generate(r)
+ generated[r] = true
+ else
+ ready.delete(r)
+ yield r
+ done[r] = true
+ direct_dependents_of(r).each { |v| check_if_now_ready(v) }
+ end
+ end
+ end
end
def relationship_graph
- catalog.relationship_graph
+ @relationship_graph ||= Relationship_graph_wrapper.new(catalog.relationship_graph,self)
end
def add_resource_status(status)
report.add_resource_status status
end
def resource_status(resource)
report.resource_statuses[resource.to_s] || add_resource_status(Puppet::Resource::Status.new(resource))
end
# Is the resource currently scheduled?
def scheduled?(resource)
self.ignoreschedules or resource_harness.scheduled?(resource_status(resource), resource)
end
# Should this resource be skipped?
def skip?(resource)
if missing_tags?(resource)
resource.debug "Not tagged with #{tags.join(", ")}"
elsif ! scheduled?(resource)
resource.debug "Not scheduled"
elsif failed_dependencies?(resource)
resource.warning "Skipping because of failed dependencies"
elsif resource.virtual?
resource.debug "Skipping because virtual"
else
return false
end
true
end
# The tags we should be checking.
def tags
self.tags = Puppet[:tags] unless defined?(@tags)
super
end
def handle_qualified_tags( qualified )
# The default behavior of Puppet::Util::Tagging is
# to split qualified tags into parts. That would cause
# qualified tags to match too broadly here.
return
end
# Is this resource tagged appropriately?
def missing_tags?(resource)
return false if ignore_tags?
return false if tags.empty?
not resource.tagged?(*tags)
end
end
require 'puppet/transaction/report'
diff --git a/lib/puppet/transaction/event_manager.rb b/lib/puppet/transaction/event_manager.rb
index 3ebb0a9d3..a21bbf892 100644
--- a/lib/puppet/transaction/event_manager.rb
+++ b/lib/puppet/transaction/event_manager.rb
@@ -1,99 +1,102 @@
require 'puppet/transaction'
class Puppet::Transaction::EventManager
attr_reader :transaction, :events
def initialize(transaction)
@transaction = transaction
@event_queues = {}
@events = []
end
def relationship_graph
transaction.relationship_graph
end
# Respond to any queued events for this resource.
def process_events(resource)
restarted = false
queued_events(resource) do |callback, events|
r = process_callback(resource, callback, events)
restarted ||= r
end
if restarted
queue_events(resource, [resource.event(:name => :restarted, :status => "success")])
transaction.resource_status(resource).restarted = true
end
end
# Queue events for other resources to respond to. All of these events have
# to be from the same resource.
def queue_events(resource, events)
- @events += events
+ #@events += events
# Do some basic normalization so we're not doing so many
# graph queries for large sets of events.
events.inject({}) do |collection, event|
collection[event.name] ||= []
collection[event.name] << event
collection
end.collect do |name, list|
# It doesn't matter which event we use - they all have the same source
# and name here.
event = list[0]
# Collect the targets of any subscriptions to those events. We pass
# the parent resource in so it will override the source in the events,
# since eval_generated children can't have direct relationships.
+ received = (event.name != :restarted)
relationship_graph.matching_edges(event, resource).each do |edge|
+ received ||= true unless edge.target.is_a?(Puppet::Type::Whit)
next unless method = edge.callback
next unless edge.target.respond_to?(method)
queue_events_for_resource(resource, edge.target, method, list)
end
+ @events << event if received
queue_events_for_resource(resource, resource, :refresh, [event]) if resource.self_refresh? and ! resource.deleting?
end
end
def queue_events_for_resource(source, target, callback, events)
source.info "Scheduling #{callback} of #{target}"
@event_queues[target] ||= {}
@event_queues[target][callback] ||= []
@event_queues[target][callback] += events
end
def queued_events(resource)
return unless callbacks = @event_queues[resource]
callbacks.each do |callback, events|
yield callback, events
end
end
private
def process_callback(resource, callback, events)
process_noop_events(resource, callback, events) and return false unless events.detect { |e| e.status != "noop" }
resource.send(callback)
resource.notice "Triggered '#{callback}' from #{events.length} events"
return true
rescue => detail
resource.err "Failed to call #{callback}: #{detail}"
transaction.resource_status(resource).failed_to_restart = true
puts detail.backtrace if Puppet[:trace]
return false
end
def process_noop_events(resource, callback, events)
resource.notice "Would have triggered '#{callback}' from #{events.length} events"
# And then add an event for it.
queue_events(resource, [resource.event(:status => "noop", :name => :noop_restart)])
true # so the 'and if' works
end
end
diff --git a/lib/puppet/type.rb b/lib/puppet/type.rb
index d24cc8554..5ecc430d4 100644
--- a/lib/puppet/type.rb
+++ b/lib/puppet/type.rb
@@ -1,1910 +1,1904 @@
require 'puppet'
require 'puppet/util/log'
require 'puppet/util/metric'
require 'puppet/property'
require 'puppet/parameter'
require 'puppet/util'
require 'puppet/util/autoload'
require 'puppet/metatype/manager'
require 'puppet/util/errors'
require 'puppet/util/log_paths'
require 'puppet/util/logging'
require 'puppet/util/cacher'
require 'puppet/file_collection/lookup'
require 'puppet/util/tagging'
# see the bottom of the file for the rest of the inclusions
module Puppet
class Type
include Puppet::Util
include Puppet::Util::Errors
include Puppet::Util::LogPaths
include Puppet::Util::Logging
include Puppet::Util::Cacher
include Puppet::FileCollection::Lookup
include Puppet::Util::Tagging
###############################
# Code related to resource type attributes.
class << self
include Puppet::Util::ClassGen
include Puppet::Util::Warnings
attr_reader :properties
end
def self.states
warnonce "The states method is deprecated; use properties"
properties
end
# All parameters, in the appropriate order. The key_attributes come first, then
# the provider, then the properties, and finally the params and metaparams
# in the order they were specified in the files.
def self.allattrs
key_attributes | (parameters & [:provider]) | properties.collect { |property| property.name } | parameters | metaparams
end
# Retrieve an attribute alias, if there is one.
def self.attr_alias(param)
@attr_aliases[symbolize(param)]
end
# Create an alias to an existing attribute. This will cause the aliased
# attribute to be valid when setting and retrieving values on the instance.
def self.set_attr_alias(hash)
hash.each do |new, old|
@attr_aliases[symbolize(new)] = symbolize(old)
end
end
# Find the class associated with any given attribute.
def self.attrclass(name)
@attrclasses ||= {}
# We cache the value, since this method gets called such a huge number
# of times (as in, hundreds of thousands in a given run).
unless @attrclasses.include?(name)
@attrclasses[name] = case self.attrtype(name)
when :property; @validproperties[name]
when :meta; @@metaparamhash[name]
when :param; @paramhash[name]
end
end
@attrclasses[name]
end
# What type of parameter are we dealing with? Cache the results, because
# this method gets called so many times.
def self.attrtype(attr)
@attrtypes ||= {}
unless @attrtypes.include?(attr)
@attrtypes[attr] = case
when @validproperties.include?(attr); :property
when @paramhash.include?(attr); :param
when @@metaparamhash.include?(attr); :meta
end
end
@attrtypes[attr]
end
def self.eachmetaparam
@@metaparams.each { |p| yield p.name }
end
# Create the 'ensure' class. This is a separate method so other types
# can easily call it and create their own 'ensure' values.
def self.ensurable(&block)
if block_given?
self.newproperty(:ensure, :parent => Puppet::Property::Ensure, &block)
else
self.newproperty(:ensure, :parent => Puppet::Property::Ensure) do
self.defaultvalues
end
end
end
# Should we add the 'ensure' property to this class?
def self.ensurable?
# If the class has all three of these methods defined, then it's
# ensurable.
ens = [:exists?, :create, :destroy].inject { |set, method|
set &&= self.public_method_defined?(method)
}
ens
end
# Deal with any options passed into parameters.
def self.handle_param_options(name, options)
# If it's a boolean parameter, create a method to test the value easily
if options[:boolean]
define_method(name.to_s + "?") do
val = self[name]
if val == :true or val == true
return true
end
end
end
end
# Is the parameter in question a meta-parameter?
def self.metaparam?(param)
@@metaparamhash.include?(symbolize(param))
end
# Find the metaparameter class associated with a given metaparameter name.
def self.metaparamclass(name)
@@metaparamhash[symbolize(name)]
end
def self.metaparams
@@metaparams.collect { |param| param.name }
end
def self.metaparamdoc(metaparam)
@@metaparamhash[metaparam].doc
end
# Create a new metaparam. Requires a block and a name, stores it in the
# @parameters array, and does some basic checking on it.
def self.newmetaparam(name, options = {}, &block)
@@metaparams ||= []
@@metaparamhash ||= {}
name = symbolize(name)
param = genclass(
name,
:parent => options[:parent] || Puppet::Parameter,
:prefix => "MetaParam",
:hash => @@metaparamhash,
:array => @@metaparams,
:attributes => options[:attributes],
&block
)
# Grr.
param.required_features = options[:required_features] if options[:required_features]
handle_param_options(name, options)
param.metaparam = true
param
end
def self.key_attribute_parameters
@key_attribute_parameters ||= (
params = @parameters.find_all { |param|
param.isnamevar? or param.name == :name
}
)
end
def self.key_attributes
key_attribute_parameters.collect { |p| p.name }
end
def self.title_patterns
case key_attributes.length
when 0; []
when 1;
identity = lambda {|x| x}
[ [ /(.*)/m, [ [key_attributes.first, identity ] ] ] ]
else
raise Puppet::DevError,"you must specify title patterns when there are two or more key attributes"
end
end
def uniqueness_key
self.class.key_attributes.sort_by { |attribute_name| attribute_name.to_s }.map{ |attribute_name| self[attribute_name] }
end
# Create a new parameter. Requires a block and a name, stores it in the
# @parameters array, and does some basic checking on it.
def self.newparam(name, options = {}, &block)
options[:attributes] ||= {}
param = genclass(
name,
:parent => options[:parent] || Puppet::Parameter,
:attributes => options[:attributes],
:block => block,
:prefix => "Parameter",
:array => @parameters,
:hash => @paramhash
)
handle_param_options(name, options)
# Grr.
param.required_features = options[:required_features] if options[:required_features]
param.isnamevar if options[:namevar]
param
end
def self.newstate(name, options = {}, &block)
Puppet.warning "newstate() has been deprecrated; use newproperty(#{name})"
newproperty(name, options, &block)
end
# Create a new property. The first parameter must be the name of the property;
# this is how users will refer to the property when creating new instances.
# The second parameter is a hash of options; the options are:
# * :parent: The parent class for the property. Defaults to Puppet::Property.
# * :retrieve: The method to call on the provider or @parent object (if
# the provider is not set) to retrieve the current value.
def self.newproperty(name, options = {}, &block)
name = symbolize(name)
# This is here for types that might still have the old method of defining
# a parent class.
unless options.is_a? Hash
raise Puppet::DevError,
"Options must be a hash, not #{options.inspect}"
end
raise Puppet::DevError, "Class #{self.name} already has a property named #{name}" if @validproperties.include?(name)
if parent = options[:parent]
options.delete(:parent)
else
parent = Puppet::Property
end
# We have to create our own, new block here because we want to define
# an initial :retrieve method, if told to, and then eval the passed
# block if available.
prop = genclass(name, :parent => parent, :hash => @validproperties, :attributes => options) do
# If they've passed a retrieve method, then override the retrieve
# method on the class.
if options[:retrieve]
define_method(:retrieve) do
provider.send(options[:retrieve])
end
end
class_eval(&block) if block
end
# If it's the 'ensure' property, always put it first.
if name == :ensure
@properties.unshift prop
else
@properties << prop
end
prop
end
def self.paramdoc(param)
@paramhash[param].doc
end
# Return the parameter names
def self.parameters
return [] unless defined?(@parameters)
@parameters.collect { |klass| klass.name }
end
# Find the parameter class associated with a given parameter name.
def self.paramclass(name)
@paramhash[name]
end
# Return the property class associated with a name
def self.propertybyname(name)
@validproperties[name]
end
def self.validattr?(name)
name = symbolize(name)
return true if name == :name
@validattrs ||= {}
unless @validattrs.include?(name)
@validattrs[name] = !!(self.validproperty?(name) or self.validparameter?(name) or self.metaparam?(name))
end
@validattrs[name]
end
# does the name reflect a valid property?
def self.validproperty?(name)
name = symbolize(name)
@validproperties.include?(name) && @validproperties[name]
end
# Return the list of validproperties
def self.validproperties
return {} unless defined?(@parameters)
@validproperties.keys
end
# does the name reflect a valid parameter?
def self.validparameter?(name)
raise Puppet::DevError, "Class #{self} has not defined parameters" unless defined?(@parameters)
!!(@paramhash.include?(name) or @@metaparamhash.include?(name))
end
# This is a forward-compatibility method - it's the validity interface we'll use in Puppet::Resource.
def self.valid_parameter?(name)
validattr?(name)
end
# Return either the attribute alias or the attribute.
def attr_alias(name)
name = symbolize(name)
if synonym = self.class.attr_alias(name)
return synonym
else
return name
end
end
# Are we deleting this resource?
def deleting?
obj = @parameters[:ensure] and obj.should == :absent
end
# Create a new property if it is valid but doesn't exist
# Returns: true if a new parameter was added, false otherwise
def add_property_parameter(prop_name)
if self.class.validproperty?(prop_name) && !@parameters[prop_name]
self.newattr(prop_name)
return true
end
false
end
#
# The name_var is the key_attribute in the case that there is only one.
#
def name_var
key_attributes = self.class.key_attributes
(key_attributes.length == 1) && key_attributes.first
end
# abstract accessing parameters and properties, and normalize
# access to always be symbols, not strings
# This returns a value, not an object. It returns the 'is'
# value, but you can also specifically return 'is' and 'should'
# values using 'object.is(:property)' or 'object.should(:property)'.
def [](name)
name = attr_alias(name)
fail("Invalid parameter #{name}(#{name.inspect})") unless self.class.validattr?(name)
if name == :name && nv = name_var
name = nv
end
if obj = @parameters[name]
# Note that if this is a property, then the value is the "should" value,
# not the current value.
obj.value
else
return nil
end
end
# Abstract setting parameters and properties, and normalize
# access to always be symbols, not strings. This sets the 'should'
# value on properties, and otherwise just sets the appropriate parameter.
def []=(name,value)
name = attr_alias(name)
fail("Invalid parameter #{name}") unless self.class.validattr?(name)
if name == :name && nv = name_var
name = nv
end
raise Puppet::Error.new("Got nil value for #{name}") if value.nil?
property = self.newattr(name)
if property
begin
# make sure the parameter doesn't have any errors
property.value = value
rescue => detail
error = Puppet::Error.new("Parameter #{name} failed: #{detail}")
error.set_backtrace(detail.backtrace)
raise error
end
end
nil
end
# remove a property from the object; useful in testing or in cleanup
# when an error has been encountered
def delete(attr)
attr = symbolize(attr)
if @parameters.has_key?(attr)
@parameters.delete(attr)
else
raise Puppet::DevError.new("Undefined attribute '#{attr}' in #{self}")
end
end
# iterate across the existing properties
def eachproperty
# properties is a private method
properties.each { |property|
yield property
}
end
# Create a transaction event. Called by Transaction or by
# a property.
def event(options = {})
Puppet::Transaction::Event.new({:resource => self, :file => file, :line => line, :tags => tags}.merge(options))
end
# Let the catalog determine whether a given cached value is
# still valid or has expired.
def expirer
catalog
end
# retrieve the 'should' value for a specified property
def should(name)
name = attr_alias(name)
(prop = @parameters[name] and prop.is_a?(Puppet::Property)) ? prop.should : nil
end
# Create the actual attribute instance. Requires either the attribute
# name or class as the first argument, then an optional hash of
# attributes to set during initialization.
def newattr(name)
if name.is_a?(Class)
klass = name
name = klass.name
end
unless klass = self.class.attrclass(name)
raise Puppet::Error, "Resource type #{self.class.name} does not support parameter #{name}"
end
if provider and ! provider.class.supports_parameter?(klass)
missing = klass.required_features.find_all { |f| ! provider.class.feature?(f) }
info "Provider %s does not support features %s; not managing attribute %s" % [provider.class.name, missing.join(", "), name]
return nil
end
return @parameters[name] if @parameters.include?(name)
@parameters[name] = klass.new(:resource => self)
end
# return the value of a parameter
def parameter(name)
@parameters[name.to_sym]
end
def parameters
@parameters.dup
end
# Is the named property defined?
def propertydefined?(name)
name = name.intern unless name.is_a? Symbol
@parameters.include?(name)
end
# Return an actual property instance by name; to return the value, use 'resource[param]'
# LAK:NOTE(20081028) Since the 'parameter' method is now a superset of this method,
# this one should probably go away at some point.
def property(name)
(obj = @parameters[symbolize(name)] and obj.is_a?(Puppet::Property)) ? obj : nil
end
# For any parameters or properties that have defaults and have not yet been
# set, set them now. This method can be handed a list of attributes,
# and if so it will only set defaults for those attributes.
def set_default(attr)
return unless klass = self.class.attrclass(attr)
return unless klass.method_defined?(:default)
return if @parameters.include?(klass.name)
return unless parameter = newattr(klass.name)
if value = parameter.default and ! value.nil?
parameter.value = value
else
@parameters.delete(parameter.name)
end
end
# Convert our object to a hash. This just includes properties.
def to_hash
rethash = {}
@parameters.each do |name, obj|
rethash[name] = obj.value
end
rethash
end
def type
self.class.name
end
# Return a specific value for an attribute.
def value(name)
name = attr_alias(name)
(obj = @parameters[name] and obj.respond_to?(:value)) ? obj.value : nil
end
def version
return 0 unless catalog
catalog.version
end
# Return all of the property objects, in the order specified in the
# class.
def properties
self.class.properties.collect { |prop| @parameters[prop.name] }.compact
end
# Is this type's name isomorphic with the object? That is, if the
# name conflicts, does it necessarily mean that the objects conflict?
# Defaults to true.
def self.isomorphic?
if defined?(@isomorphic)
return @isomorphic
else
return true
end
end
def isomorphic?
self.class.isomorphic?
end
# is the instance a managed instance? A 'yes' here means that
# the instance was created from the language, vs. being created
# in order resolve other questions, such as finding a package
# in a list
def managed?
# Once an object is managed, it always stays managed; but an object
# that is listed as unmanaged might become managed later in the process,
# so we have to check that every time
if @managed
return @managed
else
@managed = false
properties.each { |property|
s = property.should
if s and ! property.class.unmanaged
@managed = true
break
end
}
return @managed
end
end
###############################
# Code related to the container behaviour.
- # this is a retarded hack method to get around the difference between
- # component children and file children
- def self.depthfirst?
- @depthfirst
- end
-
def depthfirst?
- self.class.depthfirst?
+ false
end
# Remove an object. The argument determines whether the object's
# subscriptions get eliminated, too.
def remove(rmdeps = true)
# This is hackish (mmm, cut and paste), but it works for now, and it's
# better than warnings.
@parameters.each do |name, obj|
obj.remove
end
@parameters.clear
@parent = nil
# Remove the reference to the provider.
if self.provider
@provider.clear
@provider = nil
end
end
###############################
# Code related to evaluating the resources.
# Flush the provider, if it supports it. This is called by the
# transaction.
def flush
self.provider.flush if self.provider and self.provider.respond_to?(:flush)
end
# if all contained objects are in sync, then we're in sync
# FIXME I don't think this is used on the type instances any more,
# it's really only used for testing
def insync?(is)
insync = true
if property = @parameters[:ensure]
unless is.include? property
raise Puppet::DevError,
"The is value is not in the is array for '#{property.name}'"
end
ensureis = is[property]
if property.safe_insync?(ensureis) and property.should == :absent
return true
end
end
properties.each { |property|
unless is.include? property
raise Puppet::DevError,
"The is value is not in the is array for '#{property.name}'"
end
propis = is[property]
unless property.safe_insync?(propis)
property.debug("Not in sync: #{propis.inspect} vs #{property.should.inspect}")
insync = false
#else
# property.debug("In sync")
end
}
#self.debug("#{self} sync status is #{insync}")
insync
end
# retrieve the current value of all contained properties
def retrieve
fail "Provider #{provider.class.name} is not functional on this host" if self.provider.is_a?(Puppet::Provider) and ! provider.class.suitable?
result = Puppet::Resource.new(type, title)
# Provide the name, so we know we'll always refer to a real thing
result[:name] = self[:name] unless self[:name] == title
if ensure_prop = property(:ensure) or (self.class.validattr?(:ensure) and ensure_prop = newattr(:ensure))
result[:ensure] = ensure_state = ensure_prop.retrieve
else
ensure_state = nil
end
properties.each do |property|
next if property.name == :ensure
if ensure_state == :absent
result[property] = :absent
else
result[property] = property.retrieve
end
end
result
end
def retrieve_resource
resource = retrieve
resource = Resource.new(type, title, :parameters => resource) if resource.is_a? Hash
resource
end
# Get a hash of the current properties. Returns a hash with
# the actual property instance as the key and the current value
# as the, um, value.
def currentpropvalues
# It's important to use the 'properties' method here, as it follows the order
# in which they're defined in the class. It also guarantees that 'ensure'
# is the first property, which is important for skipping 'retrieve' on
# all the properties if the resource is absent.
ensure_state = false
return properties.inject({}) do | prophash, property|
if property.name == :ensure
ensure_state = property.retrieve
prophash[property] = ensure_state
else
if ensure_state == :absent
prophash[property] = :absent
else
prophash[property] = property.retrieve
end
end
prophash
end
end
# Are we running in noop mode?
def noop?
# If we're not a host_config, we're almost certainly part of
# Settings, and we want to ignore 'noop'
return false if catalog and ! catalog.host_config?
if defined?(@noop)
@noop
else
Puppet[:noop]
end
end
def noop
noop?
end
###############################
# Code related to managing resource instances.
require 'puppet/transportable'
# retrieve a named instance of the current type
def self.[](name)
raise "Global resource access is deprecated"
@objects[name] || @aliases[name]
end
# add an instance by name to the class list of instances
def self.[]=(name,object)
raise "Global resource storage is deprecated"
newobj = nil
if object.is_a?(Puppet::Type)
newobj = object
else
raise Puppet::DevError, "must pass a Puppet::Type object"
end
if exobj = @objects[name] and self.isomorphic?
msg = "Object '#{newobj.class.name}[#{name}]' already exists"
msg += ("in file #{object.file} at line #{object.line}") if exobj.file and exobj.line
msg += ("and cannot be redefined in file #{object.file} at line #{object.line}") if object.file and object.line
error = Puppet::Error.new(msg)
raise error
else
#Puppet.info("adding %s of type %s to class list" %
# [name,object.class])
@objects[name] = newobj
end
end
# Create an alias. We keep these in a separate hash so that we don't encounter
# the objects multiple times when iterating over them.
def self.alias(name, obj)
raise "Global resource aliasing is deprecated"
if @objects.include?(name)
unless @objects[name] == obj
raise Puppet::Error.new(
"Cannot create alias #{name}: object already exists"
)
end
end
if @aliases.include?(name)
unless @aliases[name] == obj
raise Puppet::Error.new(
"Object #{@aliases[name].name} already has alias #{name}"
)
end
end
@aliases[name] = obj
end
# remove all of the instances of a single type
def self.clear
raise "Global resource removal is deprecated"
if defined?(@objects)
@objects.each do |name, obj|
obj.remove(true)
end
@objects.clear
end
@aliases.clear if defined?(@aliases)
end
# Force users to call this, so that we can merge objects if
# necessary.
def self.create(args)
# LAK:DEP Deprecation notice added 12/17/2008
Puppet.warning "Puppet::Type.create is deprecated; use Puppet::Type.new"
new(args)
end
# remove a specified object
def self.delete(resource)
raise "Global resource removal is deprecated"
return unless defined?(@objects)
@objects.delete(resource.title) if @objects.include?(resource.title)
@aliases.delete(resource.title) if @aliases.include?(resource.title)
if @aliases.has_value?(resource)
names = []
@aliases.each do |name, otherres|
if otherres == resource
names << name
end
end
names.each { |name| @aliases.delete(name) }
end
end
# iterate across each of the type's instances
def self.each
raise "Global resource iteration is deprecated"
return unless defined?(@objects)
@objects.each { |name,instance|
yield instance
}
end
# does the type have an object with the given name?
def self.has_key?(name)
raise "Global resource access is deprecated"
@objects.has_key?(name)
end
# Retrieve all known instances. Either requires providers or must be overridden.
def self.instances
raise Puppet::DevError, "#{self.name} has no providers and has not overridden 'instances'" if provider_hash.empty?
# Put the default provider first, then the rest of the suitable providers.
provider_instances = {}
providers_by_source.collect do |provider|
provider.instances.collect do |instance|
# We always want to use the "first" provider instance we find, unless the resource
# is already managed and has a different provider set
if other = provider_instances[instance.name]
Puppet.warning "%s %s found in both %s and %s; skipping the %s version" %
[self.name.to_s.capitalize, instance.name, other.class.name, instance.class.name, instance.class.name]
next
end
provider_instances[instance.name] = instance
new(:name => instance.name, :provider => instance, :audit => :all)
end
end.flatten.compact
end
# Return a list of one suitable provider per source, with the default provider first.
def self.providers_by_source
# Put the default provider first, then the rest of the suitable providers.
sources = []
[defaultprovider, suitableprovider].flatten.uniq.collect do |provider|
next if sources.include?(provider.source)
sources << provider.source
provider
end.compact
end
# Convert a simple hash into a Resource instance.
def self.hash2resource(hash)
hash = hash.inject({}) { |result, ary| result[ary[0].to_sym] = ary[1]; result }
title = hash.delete(:title)
title ||= hash[:name]
title ||= hash[key_attributes.first] if key_attributes.length == 1
raise Puppet::Error, "Title or name must be provided" unless title
# Now create our resource.
resource = Puppet::Resource.new(self.name, title)
[:catalog].each do |attribute|
if value = hash[attribute]
hash.delete(attribute)
resource.send(attribute.to_s + "=", value)
end
end
hash.each do |param, value|
resource[param] = value
end
resource
end
# Create the path for logging and such.
def pathbuilder
if p = parent
[p.pathbuilder, self.ref].flatten
else
[self.ref]
end
end
###############################
# Add all of the meta parameters.
newmetaparam(:noop) do
desc "Boolean flag indicating whether work should actually
be done."
newvalues(:true, :false)
munge do |value|
case value
when true, :true, "true"; @resource.noop = true
when false, :false, "false"; @resource.noop = false
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."
end
newmetaparam(:audit) do
desc "Marks a subset of this resource's unmanaged attributes for auditing. Accepts an
attribute name or a list of attribute names.
Auditing a resource attribute has two effects: First, whenever a catalog
is applied with puppet apply or puppet agent, Puppet will check whether
that attribute of the resource has been modified, comparing its current
value to the previous run; any change will be logged alongside any actions
performed by Puppet while applying the catalog.
Secondly, marking a resource attribute for auditing will include that
attribute in inspection reports generated by puppet inspect; see the
puppet inspect documentation for more details.
Managed attributes for a resource can also be audited, but note that
changes made by Puppet will be logged as additional modifications. (I.e.
if a user manually edits a file whose contents are audited and managed,
puppet agent's next two runs will both log an audit notice: the first run
will log the user's edit and then revert the file to the desired state,
and the second run will log the edit made by Puppet.)"
validate do |list|
list = Array(list).collect {|p| p.to_sym}
unless list == [:all]
list.each do |param|
next if @resource.class.validattr?(param)
fail "Cannot audit #{param}: not a valid attribute for #{resource}"
end
end
end
munge do |args|
properties_to_audit(args).each do |param|
next unless resource.class.validproperty?(param)
resource.newattr(param)
end
end
def all_properties
resource.class.properties.find_all do |property|
resource.provider.nil? or resource.provider.class.supports_parameter?(property)
end.collect do |property|
property.name
end
end
def properties_to_audit(list)
if !list.kind_of?(Array) && list.to_sym == :all
list = all_properties
else
list = Array(list).collect { |p| p.to_sym }
end
end
end
newmetaparam(:check) do
desc "Audit specified attributes of resources over time, and report if any have changed.
This parameter has been deprecated in favor of 'audit'."
munge do |args|
resource.warning "'check' attribute is deprecated; use 'audit' instead"
resource[:audit] = args
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::Util::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](http://docs.puppetlabs.com/guides/language_tutorial.html) for more information.
"
munge do |aliases|
aliases = [aliases] unless aliases.is_a?(Array)
raise(ArgumentError, "Cannot add aliases without a catalog") unless @resource.catalog
aliases.each do |other|
if obj = @resource.catalog.resource(@resource.class.name, other)
unless obj.object_id == @resource.object_id
self.fail("#{@resource.title} can not create alias #{other}: object already exists")
end
next
end
# Newschool, add it to the catalog.
@resource.catalog.alias(@resource, other)
end
end
end
newmetaparam(:tag) do
desc "Add the specified tags to the associated resource. While all resources
are automatically tagged with as much information as possible
(e.g., each class and definition containing the resource), it can
be useful to add your own tags to a given resource.
Tags are currently useful for things like applying a subset of a
host's configuration:
puppet agent --test --tags 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|
@resource.tag(tag)
end
end
end
class RelationshipMetaparam < Puppet::Parameter
class << self
attr_accessor :direction, :events, :callback, :subclasses
end
@subclasses = []
def self.inherited(sub)
@subclasses << sub
end
def munge(references)
references = [references] unless references.is_a?(Array)
references.collect do |ref|
if ref.is_a?(Puppet::Resource)
ref
else
Puppet::Resource.new(ref)
end
end
end
def validate_relationship
@value.each do |ref|
unless @resource.catalog.resource(ref.to_s)
description = self.class.direction == :in ? "dependency" : "dependent"
fail "Could not find #{description} #{ref} for #{resource.ref}"
end
end
end
# Create edges from each of our relationships. :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. The direction of the relationship determines
# which resource is applied first and which resource is considered
# to be the event generator.
def to_edges
@value.collect do |reference|
reference.catalog = resource.catalog
# Either of the two retrieval attempts could have returned
# nil.
unless related_resource = reference.resolve
self.fail "Could not retrieve dependency '#{reference}' of #{@resource.ref}"
end
# Are we requiring them, or vice versa? See the method docs
# for futher info on this.
if self.class.direction == :in
source = related_resource
target = @resource
else
source = @resource
target = related_resource
end
if method = self.class.callback
subargs = {
:event => self.class.events,
:callback => method
}
self.debug("subscribes to #{related_resource.ref}")
else
# If there's no callback, there's no point in even adding
# a label.
subargs = nil
self.debug("requires #{related_resource.ref}")
end
rel = Puppet::Relationship.new(source, target, subargs)
end
end
end
def self.relationship_params
RelationshipMetaparam.subclasses
end
# Note that the order in which the relationships params is defined
# matters. The labelled params (notify and subcribe) must be later,
# so that if both params are used, those ones win. It's a hackish
# solution, but it works.
newmetaparam(:require, :parent => RelationshipMetaparam, :attributes => {:direction => :in, :events => :NONE}) 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\"]
}
Multiple dependencies can be specified by providing a comma-seperated list
of resources, enclosed in square brackets:
require => [ File[\"/usr/local\"], 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 resources 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 resources 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.
"
end
newmetaparam(:subscribe, :parent => RelationshipMetaparam, :attributes => {:direction => :in, :events => :ALL_EVENTS, :callback => :refresh}) 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:
ensure => running,
subscribe => File[nagconf]
}
}
Currently the `exec`, `mount` and `service` type support
refreshing.
"
end
newmetaparam(:before, :parent => RelationshipMetaparam, :attributes => {:direction => :out, :events => :NONE}) 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.}
end
newmetaparam(:notify, :parent => RelationshipMetaparam, :attributes => {:direction => :out, :events => :ALL_EVENTS, :callback => :refresh}) 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.}
end
newmetaparam(:stage) do
desc %{Which run stage a given resource should reside in. This just creates
a dependency on or from the named milestone. For instance, saying that
this is in the 'bootstrap' stage creates a dependency on the 'bootstrap'
milestone.
By default, all classes get directly added to the
'main' stage. You can create new stages as resources:
stage { [pre, post]: }
To order stages, use standard relationships:
stage { pre: before => Stage[main] }
Or use the new relationship syntax:
Stage[pre] -> Stage[main] -> Stage[post]
Then use the new class parameters to specify a stage:
class { foo: stage => pre }
Stages can only be set on classes, not individual resources. This will
fail:
file { '/foo': stage => pre, ensure => file }
}
end
###############################
# All of the provider plumbing for the resource types.
require 'puppet/provider'
require 'puppet/util/provider_features'
# Add the feature handling module.
extend Puppet::Util::ProviderFeatures
attr_reader :provider
# the Type class attribute accessors
class << self
attr_accessor :providerloader
attr_writer :defaultprovider
end
# Find the default provider.
def self.defaultprovider
unless @defaultprovider
suitable = suitableprovider
# Find which providers are a default for this system.
defaults = suitable.find_all { |provider| provider.default? }
# If we don't have any default we use suitable providers
defaults = suitable if defaults.empty?
max = defaults.collect { |provider| provider.specificity }.max
defaults = defaults.find_all { |provider| provider.specificity == max }
retval = nil
if defaults.length > 1
Puppet.warning(
"Found multiple default providers for #{self.name}: #{defaults.collect { |i| i.name.to_s }.join(", ")}; using #{defaults[0].name}"
)
retval = defaults.shift
elsif defaults.length == 1
retval = defaults.shift
else
raise Puppet::DevError, "Could not find a default provider for #{self.name}"
end
@defaultprovider = retval
end
@defaultprovider
end
def self.provider_hash_by_type(type)
@provider_hashes ||= {}
@provider_hashes[type] ||= {}
end
def self.provider_hash
Puppet::Type.provider_hash_by_type(self.name)
end
# Retrieve a provider by name.
def self.provider(name)
name = Puppet::Util.symbolize(name)
# If we don't have it yet, try loading it.
@providerloader.load(name) unless provider_hash.has_key?(name)
provider_hash[name]
end
# Just list all of the providers.
def self.providers
provider_hash.keys
end
def self.validprovider?(name)
name = Puppet::Util.symbolize(name)
(provider_hash.has_key?(name) && provider_hash[name].suitable?)
end
# Create a new provider of a type. This method must be called
# directly on the type that it's implementing.
def self.provide(name, options = {}, &block)
name = Puppet::Util.symbolize(name)
if obj = provider_hash[name]
Puppet.debug "Reloading #{name} #{self.name} provider"
unprovide(name)
end
parent = if pname = options[:parent]
options.delete(:parent)
if pname.is_a? Class
pname
else
if provider = self.provider(pname)
provider
else
raise Puppet::DevError,
"Could not find parent provider #{pname} of #{name}"
end
end
else
Puppet::Provider
end
options[:resource_type] ||= self
self.providify
provider = genclass(
name,
:parent => parent,
:hash => provider_hash,
:prefix => "Provider",
:block => block,
:include => feature_module,
:extend => feature_module,
:attributes => options
)
provider
end
# Make sure we have a :provider parameter defined. Only gets called if there
# are providers.
def self.providify
return if @paramhash.has_key? :provider
newparam(:provider) do
desc "The specific backend for #{self.name.to_s} to use. You will
seldom need to specify this -- Puppet will usually discover the
appropriate provider for your platform."
# This is so we can refer back to the type to get a list of
# providers for documentation.
class << self
attr_accessor :parenttype
end
# We need to add documentation for each provider.
def self.doc
@doc + " Available providers are:\n\n" + parenttype.providers.sort { |a,b|
a.to_s <=> b.to_s
}.collect { |i|
"* **#{i}**: #{parenttype().provider(i).doc}"
}.join("\n")
end
defaultto {
@resource.class.defaultprovider.name
}
validate do |provider_class|
provider_class = provider_class[0] if provider_class.is_a? Array
provider_class = provider_class.class.name if provider_class.is_a?(Puppet::Provider)
unless provider = @resource.class.provider(provider_class)
raise ArgumentError, "Invalid #{@resource.class.name} provider '#{provider_class}'"
end
end
munge do |provider|
provider = provider[0] if provider.is_a? Array
provider = provider.intern if provider.is_a? String
@resource.provider = provider
if provider.is_a?(Puppet::Provider)
provider.class.name
else
provider
end
end
end.parenttype = self
end
def self.unprovide(name)
if provider_hash.has_key? name
rmclass(
name,
:hash => provider_hash,
:prefix => "Provider"
)
if @defaultprovider and @defaultprovider.name == name
@defaultprovider = nil
end
end
end
# Return an array of all of the suitable providers.
def self.suitableprovider
providerloader.loadall if provider_hash.empty?
provider_hash.find_all { |name, provider|
provider.suitable?
}.collect { |name, provider|
provider
}.reject { |p| p.name == :fake } # For testing
end
def provider=(name)
if name.is_a?(Puppet::Provider)
@provider = name
@provider.resource = self
elsif klass = self.class.provider(name)
@provider = klass.new(self)
else
raise ArgumentError, "Could not find #{name} provider of #{self.class.name}"
end
end
###############################
# All of the relationship code.
# 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(rel_catalog = nil)
rel_catalog ||= catalog
raise(Puppet::DevError, "You cannot add relationships without a catalog") unless rel_catalog
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(type)
# Retrieve the list of names from the block.
next unless list = self.instance_eval(&block)
list = [list] unless list.is_a?(Array)
# Collect the current prereqs
list.each { |dep|
obj = nil
# Support them passing objects directly, to save some effort.
unless dep.is_a? Puppet::Type
# Skip autorequires that we aren't managing
unless dep = rel_catalog.resource(type, dep)
next
end
end
reqs << Puppet::Relationship.new(dep, self)
}
}
reqs
end
# Build the dependencies associated with an individual object.
def builddepends
# Handle the requires
self.class.relationship_params.collect do |klass|
if param = @parameters[klass.name]
param.to_edges
end
end.flatten.reject { |r| r.nil? }
end
# Define the initial list of tags.
def tags=(list)
tag(self.class.name)
tag(*list)
end
# Types (which map to resources 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 properties.
# 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_writer :title
attr_writer :noop
include Enumerable
# class methods dealing with Type management
public
# the Type class attribute accessors
class << self
attr_reader :name
attr_accessor :self_refresh
include Enumerable, Puppet::Util::ClassGen
include Puppet::MetaType::Manager
include Puppet::Util
include Puppet::Util::Logging
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
@defaults = {}
@parameters ||= []
@validproperties = {}
@properties = []
@parameters = []
@paramhash = {}
@attr_aliases = {}
@paramdoc = Hash.new { |hash,key|
key = key.intern if key.is_a?(String)
if hash.include?(key)
hash[key]
else
"Param Documentation for #{key} not found"
end
}
@doc ||= ""
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
# The catalog that this resource is stored in.
attr_accessor :catalog
# is the resource exported
attr_accessor :exported
# is the resource virtual (it should not :-))
attr_accessor :virtual
# create a log at specified level
def log(msg)
Puppet::Util::Log.create(
:level => @parameters[:loglevel].value,
:message => msg,
:source => self
)
end
# instance methods related to instance intrinsics
# e.g., initialize and name
public
attr_reader :original_parameters
# initialize the type instance
def initialize(resource)
raise Puppet::DevError, "Got TransObject instead of Resource or hash" if resource.is_a?(Puppet::TransObject)
resource = self.class.hash2resource(resource) unless resource.is_a?(Puppet::Resource)
# The list of parameter/property instances.
@parameters = {}
# Set the title first, so any failures print correctly.
if resource.type.to_s.downcase.to_sym == self.class.name
self.title = resource.title
else
# This should only ever happen for components
self.title = resource.ref
end
[:file, :line, :catalog, :exported, :virtual].each do |getter|
setter = getter.to_s + "="
if val = resource.send(getter)
self.send(setter, val)
end
end
@tags = resource.tags
@original_parameters = resource.to_hash
set_name(@original_parameters)
set_default(:provider)
set_parameters(@original_parameters)
self.validate if self.respond_to?(:validate)
end
private
# Set our resource's name.
def set_name(hash)
self[name_var] = hash.delete(name_var) if name_var
end
# Set all of the parameters from a hash, in the appropriate order.
def set_parameters(hash)
# Use the order provided by allattrs, but add in any
# extra attributes from the resource so we get failures
# on invalid attributes.
no_values = []
(self.class.allattrs + hash.keys).uniq.each do |attr|
begin
# Set any defaults immediately. This is mostly done so
# that the default provider is available for any other
# property validation.
if hash.has_key?(attr)
self[attr] = hash[attr]
else
no_values << attr
end
rescue ArgumentError, Puppet::Error, TypeError
raise
rescue => detail
error = Puppet::DevError.new( "Could not set #{attr} on #{self.class.name}: #{detail}")
error.set_backtrace(detail.backtrace)
raise error
end
end
no_values.each do |attr|
set_default(attr)
end
end
public
# Set up all of our autorequires.
def finish
# Make sure all of our relationships are valid. Again, must be done
# when the entire catalog is instantiated.
self.class.relationship_params.collect do |klass|
if param = @parameters[klass.name]
param.validate_relationship
end
end.flatten.reject { |r| r.nil? }
end
# For now, leave the 'name' method functioning like it used to. Once 'title'
# works everywhere, I'll switch it.
def name
self[:name]
end
# Look up our parent in the catalog, if we have one.
def parent
return nil unless catalog
unless defined?(@parent)
if parents = catalog.adjacent(self, :direction => :in)
# We should never have more than one parent, so let's just ignore
# it if we happen to.
@parent = parents.shift
else
@parent = nil
end
end
@parent
end
# Return the "type[name]" style reference.
def ref
"#{self.class.name.to_s.capitalize}[#{self.title}]"
end
def self_refresh?
self.class.self_refresh
end
# Mark that we're purging.
def purging
@purging = true
end
# Is this resource being purged? Used by transactions to forbid
# deletion when there are dependencies.
def purging?
if defined?(@purging)
@purging
else
false
end
end
# Retrieve the title of an object. If no title was set separately,
# then use the object's name.
def title
unless @title
if self.class.validparameter?(name_var)
@title = self[:name]
elsif self.class.validproperty?(name_var)
@title = self.should(name_var)
else
self.devfail "Could not find namevar #{name_var} for #{self.class.name}"
end
end
@title
end
# convert to a string
def to_s
self.ref
end
# Convert to a transportable object
def to_trans(ret = true)
trans = TransObject.new(self.title, self.class.name)
values = retrieve_resource
values.each do |name, value|
name = name.name if name.respond_to? :name
trans[name] = value
end
@parameters.each do |name, param|
# Avoid adding each instance name twice
next if param.class.isnamevar? and param.value == self.title
# We've already got property values
next if param.is_a?(Puppet::Property)
trans[name] = param.value
end
trans.tags = self.tags
# FIXME I'm currently ignoring 'parent' and 'path'
trans
end
def to_resource
# this 'type instance' versus 'resource' distinction seems artificial
# I'd like to see it collapsed someday ~JW
self.to_trans.to_resource
end
def virtual?; !!@virtual; end
def exported?; !!@exported; end
end
end
require 'puppet/provider'
# Always load these types.
require 'puppet/type/component'
diff --git a/lib/puppet/type/file.rb b/lib/puppet/type/file.rb
index a73ada57e..5632d41f1 100644
--- a/lib/puppet/type/file.rb
+++ b/lib/puppet/type/file.rb
@@ -1,803 +1,793 @@
require 'digest/md5'
require 'cgi'
require 'etc'
require 'uri'
require 'fileutils'
require 'puppet/network/handler'
require 'puppet/util/diff'
require 'puppet/util/checksums'
require 'puppet/network/client'
require 'puppet/util/backups'
Puppet::Type.newtype(:file) do
include Puppet::Util::MethodHelper
include Puppet::Util::Checksums
include Puppet::Util::Backups
@doc = "Manages local files, including setting ownership and
permissions, creation of both files and directories, and
retrieving entire files from remote servers. As Puppet matures, it
expected that the `file` resource will be used less and less to
manage content, and instead native resources will be used to do so.
If you find that you are often copying files in from a central
location, rather than using native resources, please contact
Puppet Labs and we can hopefully work with you to develop a
native resource to support what you are doing.
**Autorequires:** If Puppet is managing the user or group that owns a file, the file resource will autorequire them. If Puppet is managing any parent directories of a file, the file resource will autorequire them."
def self.title_patterns
[ [ /^(.*?)\/*\Z/m, [ [ :path, lambda{|x| x} ] ] ] ]
end
newparam(:path) do
desc "The path to the file to manage. Must be fully qualified."
isnamevar
validate do |value|
# accept various path syntaxes: lone slash, posix, win32, unc
unless (Puppet.features.posix? and value =~ /^\//) or (Puppet.features.microsoft_windows? and (value =~ /^.:\// or value =~ /^\/\/[^\/]+\/[^\/]+/))
fail Puppet::Error, "File paths must be fully qualified, not '#{value}'"
end
end
# convert the current path in an index into the collection and the last
# path name. The aim is to use less storage for all common paths in a hierarchy
munge do |value|
path, name = ::File.split(value.gsub(/\/+/,'/'))
{ :index => Puppet::FileCollection.collection.index(path), :name => name }
end
# and the reverse
unmunge do |value|
basedir = Puppet::FileCollection.collection.path(value[:index])
# a lone slash as :name indicates a root dir on windows
if value[:name] == '/'
basedir
else
::File.join( basedir, value[:name] )
end
end
end
newparam(:backup) do
desc "Whether files should be backed up before
being replaced. The preferred method of backing files up is via
a `filebucket`, which stores files by their MD5 sums and allows
easy retrieval without littering directories with backups. You
can specify a local filebucket or a network-accessible
server-based filebucket by setting `backup => bucket-name`.
Alternatively, if you specify any value that begins with a `.`
(e.g., `.puppet-bak`), then Puppet will use copy the file in
the same directory with that value as the extension of the
backup. Setting `backup => false` disables all backups of the
file in question.
Puppet automatically creates a local filebucket named `puppet` and
defaults to backing up there. To use a server-based filebucket,
you must specify one in your configuration
filebucket { main:
server => puppet
}
The `puppet master` daemon creates a filebucket by default,
so you can usually back up to your main server with this
configuration. Once you've described the bucket in your
configuration, you can use it in any file
file { \"/my/file\":
source => \"/path/in/nfs/or/something\",
backup => main
}
This will back the file up to the central server.
At this point, the benefits of using a filebucket are that you do not
have backup files lying around on each of your machines, a given
version of a file is only backed up once, and you can restore
any given file manually, no matter how old. Eventually,
transactional support will be able to automatically restore
filebucketed files.
"
defaultto "puppet"
munge do |value|
# I don't really know how this is happening.
value = value.shift if value.is_a?(Array)
case value
when false, "false", :false
false
when true, "true", ".puppet-bak", :true
".puppet-bak"
when String
value
else
self.fail "Invalid backup type #{value.inspect}"
end
end
end
newparam(:recurse) do
desc "Whether and how deeply to do recursive
management. Options are:
inf,true => Regular style recursion on both remote and local
directory structure.
remote => Descends recursively into the remote directory
but not the local directory. Allows copying of
a few files into a directory containing many
unmanaged files without scanning all the local files.
false => Default of no-recursion.
[0-9]+ => Both, but limit recursion. Warning: this syntax
is deprecated and has moved to recurselimit.
"
newvalues(:true, :false, :inf, :remote, /^[0-9]+$/)
# Replace the validation so that we allow numbers in
# addition to string representations of them.
validate { |arg| }
munge do |value|
newval = super(value)
case newval
when :true, :inf; true
when :false; false
when :remote; :remote
when Integer, Fixnum, Bignum
self.warning "Setting recursion depth with the recurse parameter is now deprecated, please use recurselimit"
# recurse == 0 means no recursion
return false if value == 0
resource[:recurselimit] = value
true
when /^\d+$/
self.warning "Setting recursion depth with the recurse parameter is now deprecated, please use recurselimit"
value = Integer(value)
# recurse == 0 means no recursion
return false if value == 0
resource[:recurselimit] = value
true
else
self.fail "Invalid recurse value #{value.inspect}"
end
end
end
newparam(:recurselimit) do
desc "How deeply to do recursive management."
newvalues(/^[0-9]+$/)
munge do |value|
newval = super(value)
case newval
when Integer, Fixnum, Bignum; value
when /^\d+$/; Integer(value)
else
self.fail "Invalid recurselimit value #{value.inspect}"
end
end
end
newparam(:replace, :boolean => true) do
desc "Whether or not to replace a file that is
sourced but exists. This is useful for using file sources
purely for initialization."
newvalues(:true, :false)
aliasvalue(:yes, :true)
aliasvalue(:no, :false)
defaultto :true
end
newparam(:force, :boolean => true) do
desc "Force the file operation. Currently only used when replacing
directories with links."
newvalues(:true, :false)
defaultto false
end
newparam(:ignore) do
desc "A parameter which omits action on files matching
specified patterns during recursion. Uses Ruby's builtin globbing
engine, so shell metacharacters are fully supported, e.g. `[a-z]*`.
Matches that would descend into the directory structure are ignored,
e.g., `*/*`."
validate do |value|
unless value.is_a?(Array) or value.is_a?(String) or value == false
self.devfail "Ignore must be a string or an Array"
end
end
end
newparam(:links) do
desc "How to handle links during file actions. During file copying,
`follow` will copy the target file instead of the link, `manage`
will copy the link itself, and `ignore` will just pass it by.
When not copying, `manage` and `ignore` behave equivalently
(because you cannot really ignore links entirely during local recursion), and `follow` will manage the file to which the
link points."
newvalues(:follow, :manage)
defaultto :manage
end
newparam(:purge, :boolean => true) do
desc "Whether unmanaged files should be purged. If you have a filebucket
configured the purged files will be uploaded, but if you do not,
this will destroy data. Only use this option for generated
files unless you really know what you are doing. This option only
makes sense when recursively managing directories.
Note that when using `purge` with `source`, Puppet will purge any files
that are not on the remote system."
defaultto :false
newvalues(:true, :false)
end
newparam(:sourceselect) do
desc "Whether to copy all valid sources, or just the first one. This parameter
is only used in recursive copies; by default, the first valid source is the
only one used as a recursive source, but if this parameter is set to `all`,
then all valid sources will have all of their contents copied to the local host,
and for sources that have the same file, the source earlier in the list will
be used."
defaultto :first
newvalues(:first, :all)
end
# Autorequire any parent directories.
autorequire(:file) do
basedir = ::File.dirname(self[:path])
if basedir != self[:path]
basedir
else
nil
end
end
# Autorequire the owner and group of the file.
{:user => :owner, :group => :group}.each do |type, property|
autorequire(type) do
if @parameters.include?(property)
# The user/group property automatically converts to IDs
next unless should = @parameters[property].shouldorig
val = should[0]
if val.is_a?(Integer) or val =~ /^\d+$/
nil
else
val
end
end
end
end
CREATORS = [:content, :source, :target]
SOURCE_ONLY_CHECKSUMS = [:none, :ctime, :mtime]
validate do
creator_count = 0
CREATORS.each do |param|
creator_count += 1 if self.should(param)
end
creator_count += 1 if @parameters.include?(:source)
self.fail "You cannot specify more than one of #{CREATORS.collect { |p| p.to_s}.join(", ")}" if creator_count > 1
self.fail "You cannot specify a remote recursion without a source" if !self[:source] and self[:recurse] == :remote
self.fail "You cannot specify source when using checksum 'none'" if self[:checksum] == :none && !self[:source].nil?
SOURCE_ONLY_CHECKSUMS.each do |checksum_type|
self.fail "You cannot specify content when using checksum '#{checksum_type}'" if self[:checksum] == checksum_type && !self[:content].nil?
end
self.warning "Possible error: recurselimit is set but not recurse, no recursion will happen" if !self[:recurse] and self[:recurselimit]
end
def self.[](path)
return nil unless path
super(path.gsub(/\/+/, '/').sub(/\/$/, ''))
end
def self.instances(base = '/')
return self.new(:name => base, :recurse => true, :recurselimit => 1, :audit => :all).recurse_local.values
end
- @depthfirst = false
-
# Determine the user to write files as.
def asuser
if self.should(:owner) and ! self.should(:owner).is_a?(Symbol)
writeable = Puppet::Util::SUIDManager.asuser(self.should(:owner)) {
FileTest.writable?(::File.dirname(self[:path]))
}
# If the parent directory is writeable, then we execute
# as the user in question. Otherwise we'll rely on
# the 'owner' property to do things.
asuser = self.should(:owner) if writeable
end
asuser
end
def bucket
return @bucket if @bucket
backup = self[:backup]
return nil unless backup
return nil if backup =~ /^\./
unless catalog or backup == "puppet"
fail "Can not find filebucket for backups without a catalog"
end
unless catalog and filebucket = catalog.resource(:filebucket, backup) or backup == "puppet"
fail "Could not find filebucket #{backup} specified in backup"
end
return default_bucket unless filebucket
@bucket = filebucket.bucket
@bucket
end
def default_bucket
Puppet::Type.type(:filebucket).mkdefaultbucket.bucket
end
# Does the file currently exist? Just checks for whether
# we have a stat
def exist?
stat ? true : false
end
# We have to do some extra finishing, to retrieve our bucket if
# there is one.
def finish
# Look up our bucket, if there is one
bucket
super
end
# Create any children via recursion or whatever.
def eval_generate
return [] unless self.recurse?
recurse
#recurse.reject do |resource|
# catalog.resource(:file, resource[:path])
#end.each do |child|
# catalog.add_resource child
# catalog.relationship_graph.add_edge self, child
#end
end
def flush
# We want to make sure we retrieve metadata anew on each transaction.
@parameters.each do |name, param|
param.flush if param.respond_to?(:flush)
end
@stat = nil
end
def initialize(hash)
# Used for caching clients
@clients = {}
super
# If they've specified a source, we get our 'should' values
# from it.
unless self[:ensure]
if self[:target]
self[:ensure] = :symlink
elsif self[:content]
self[:ensure] = :file
end
end
@stat = nil
end
# Configure discovered resources to be purged.
def mark_children_for_purging(children)
children.each do |name, child|
next if child[:source]
child[:ensure] = :absent
end
end
# Create a new file or directory object as a child to the current
# object.
def newchild(path)
full_path = ::File.join(self[:path], path)
# Add some new values to our original arguments -- these are the ones
# set at initialization. We specifically want to exclude any param
# values set by the :source property or any default values.
# LAK:NOTE This is kind of silly, because the whole point here is that
# the values set at initialization should live as long as the resource
# but values set by default or by :source should only live for the transaction
# or so. Unfortunately, we don't have a straightforward way to manage
# the different lifetimes of this data, so we kludge it like this.
# The right-side hash wins in the merge.
options = @original_parameters.merge(:path => full_path).reject { |param, value| value.nil? }
# These should never be passed to our children.
[:parent, :ensure, :recurse, :recurselimit, :target, :alias, :source].each do |param|
options.delete(param) if options.include?(param)
end
self.class.new(options)
end
# Files handle paths specially, because they just lengthen their
# path names, rather than including the full parent's title each
# time.
def pathbuilder
# We specifically need to call the method here, so it looks
# up our parent in the catalog graph.
if parent = parent()
# We only need to behave specially when our parent is also
# a file
if parent.is_a?(self.class)
# Remove the parent file name
list = parent.pathbuilder
list.pop # remove the parent's path info
return list << self.ref
else
return super
end
else
return [self.ref]
end
end
# Should we be purging?
def purge?
@parameters.include?(:purge) and (self[:purge] == :true or self[:purge] == "true")
end
# Recursively generate a list of file resources, which will
# be used to copy remote files, manage local files, and/or make links
# to map to another directory.
def recurse
- children = {}
- children = recurse_local if self[:recurse] != :remote
+ children = (self[:recurse] == :remote) ? {} : recurse_local
if self[:target]
recurse_link(children)
elsif self[:source]
recurse_remote(children)
end
# If we're purging resources, then delete any resource that isn't on the
# remote system.
mark_children_for_purging(children) if self.purge?
result = children.values.sort { |a, b| a[:path] <=> b[:path] }
remove_less_specific_files(result)
end
# This is to fix bug #2296, where two files recurse over the same
# set of files. It's a rare case, and when it does happen you're
# not likely to have many actual conflicts, which is good, because
# this is a pretty inefficient implementation.
def remove_less_specific_files(files)
mypath = self[:path].split(::File::Separator)
other_paths = catalog.vertices.
select { |r| r.is_a?(self.class) and r[:path] != self[:path] }.
collect { |r| r[:path].split(::File::Separator) }.
select { |p| p[0,mypath.length] == mypath }
return files if other_paths.empty?
files.reject { |file|
path = file[:path].split(::File::Separator)
other_paths.any? { |p| path[0,p.length] == p }
}
end
# A simple method for determining whether we should be recursing.
def recurse?
- return false unless @parameters.include?(:recurse)
-
- val = @parameters[:recurse].value
-
- !!(val and (val == true or val == :remote))
+ self[:recurse] == true or self[:recurse] == :remote
end
# Recurse the target of the link.
def recurse_link(children)
perform_recursion(self[:target]).each do |meta|
if meta.relative_path == "."
self[:ensure] = :directory
next
end
children[meta.relative_path] ||= newchild(meta.relative_path)
if meta.ftype == "directory"
children[meta.relative_path][:ensure] = :directory
else
children[meta.relative_path][:ensure] = :link
children[meta.relative_path][:target] = meta.full_path
end
end
children
end
# Recurse the file itself, returning a Metadata instance for every found file.
def recurse_local
result = perform_recursion(self[:path])
return {} unless result
result.inject({}) do |hash, meta|
next hash if meta.relative_path == "."
hash[meta.relative_path] = newchild(meta.relative_path)
hash
end
end
# Recurse against our remote file.
def recurse_remote(children)
sourceselect = self[:sourceselect]
total = self[:source].collect do |source|
next unless result = perform_recursion(source)
return if top = result.find { |r| r.relative_path == "." } and top.ftype != "directory"
result.each { |data| data.source = "#{source}/#{data.relative_path}" }
break result if result and ! result.empty? and sourceselect == :first
result
end.flatten
# This only happens if we have sourceselect == :all
unless sourceselect == :first
found = []
total.reject! do |data|
result = found.include?(data.relative_path)
found << data.relative_path unless found.include?(data.relative_path)
result
end
end
total.each do |meta|
if meta.relative_path == "."
parameter(:source).metadata = meta
next
end
children[meta.relative_path] ||= newchild(meta.relative_path)
children[meta.relative_path][:source] = meta.source
children[meta.relative_path][:checksum] = :md5 if meta.ftype == "file"
children[meta.relative_path].parameter(:source).metadata = meta
end
children
end
def perform_recursion(path)
-
Puppet::FileServing::Metadata.indirection.search(
-
path,
:links => self[:links],
:recurse => (self[:recurse] == :remote ? true : self[:recurse]),
-
:recurselimit => self[:recurselimit],
:ignore => self[:ignore],
:checksum_type => (self[:source] || self[:content]) ? self[:checksum] : :none
)
end
# Remove any existing data. This is only used when dealing with
# links or directories.
def remove_existing(should)
return unless s = stat
self.fail "Could not back up; will not replace" unless perform_backup
unless should.to_s == "link"
return if s.ftype.to_s == should.to_s
end
case s.ftype
when "directory"
if self[:force] == :true
debug "Removing existing directory for replacement with #{should}"
FileUtils.rmtree(self[:path])
else
notice "Not removing directory; use 'force' to override"
end
when "link", "file"
debug "Removing existing #{s.ftype} for replacement with #{should}"
::File.unlink(self[:path])
else
self.fail "Could not back up files of type #{s.ftype}"
end
expire
end
def retrieve
if source = parameter(:source)
source.copy_source_values
end
super
end
# Set the checksum, from another property. There are multiple
# properties that modify the contents of a file, and they need the
# ability to make sure that the checksum value is in sync.
def setchecksum(sum = nil)
if @parameters.include? :checksum
if sum
@parameters[:checksum].checksum = sum
else
# If they didn't pass in a sum, then tell checksum to
# figure it out.
currentvalue = @parameters[:checksum].retrieve
@parameters[:checksum].checksum = currentvalue
end
end
end
# Should this thing be a normal file? This is a relatively complex
# way of determining whether we're trying to create a normal file,
# and it's here so that the logic isn't visible in the content property.
def should_be_file?
return true if self[:ensure] == :file
# I.e., it's set to something like "directory"
return false if e = self[:ensure] and e != :present
# The user doesn't really care, apparently
if self[:ensure] == :present
return true unless s = stat
return(s.ftype == "file" ? true : false)
end
# If we've gotten here, then :ensure isn't set
return true if self[:content]
return true if stat and stat.ftype == "file"
false
end
# Stat our file. Depending on the value of the 'links' attribute, we
# use either 'stat' or 'lstat', and we expect the properties to use the
# resulting stat object accordingly (mostly by testing the 'ftype'
# value).
cached_attr(:stat) do
method = :stat
# Files are the only types that support links
if (self.class.name == :file and self[:links] != :follow) or self.class.name == :tidy
method = :lstat
end
path = self[:path]
begin
::File.send(method, self[:path])
rescue Errno::ENOENT => error
return nil
rescue Errno::EACCES => error
warning "Could not stat; permission denied"
return nil
end
end
# We have to hack this just a little bit, because otherwise we'll get
# an error when the target and the contents are created as properties on
# the far side.
def to_trans(retrieve = true)
obj = super
obj.delete(:target) if obj[:target] == :notlink
obj
end
# Write out the file. Requires the property name for logging.
# Write will be done by the content property, along with checksum computation
def write(property)
remove_existing(:file)
use_temporary_file = write_temporary_file?
if use_temporary_file
path = "#{self[:path]}.puppettmp_#{rand(10000)}"
path = "#{self[:path]}.puppettmp_#{rand(10000)}" while ::File.exists?(path) or ::File.symlink?(path)
else
path = self[:path]
end
mode = self.should(:mode) # might be nil
umask = mode ? 000 : 022
mode_int = mode ? mode.to_i(8) : nil
content_checksum = Puppet::Util.withumask(umask) { ::File.open(path, 'w', mode_int ) { |f| write_content(f) } }
# And put our new file in place
if use_temporary_file # This is only not true when our file is empty.
begin
fail_if_checksum_is_wrong(path, content_checksum) if validate_checksum?
::File.rename(path, self[:path])
rescue => detail
fail "Could not rename temporary file #{path} to #{self[:path]}: #{detail}"
ensure
# Make sure the created file gets removed
::File.unlink(path) if FileTest.exists?(path)
end
end
# make sure all of the modes are actually correct
property_fix
end
private
# Should we validate the checksum of the file we're writing?
def validate_checksum?
self[:checksum] !~ /time/
end
# Make sure the file we wrote out is what we think it is.
def fail_if_checksum_is_wrong(path, content_checksum)
newsum = parameter(:checksum).sum_file(path)
return if [:absent, nil, content_checksum].include?(newsum)
self.fail "File written to disk did not match checksum; discarding changes (#{content_checksum} vs #{newsum})"
end
# write the current content. Note that if there is no content property
# simply opening the file with 'w' as done in write is enough to truncate
# or write an empty length file.
def write_content(file)
(content = property(:content)) && content.write(file)
end
private
def write_temporary_file?
# unfortunately we don't know the source file size before fetching it
# so let's assume the file won't be empty
(c = property(:content) and c.length) || (s = @parameters[:source] and 1)
end
# There are some cases where all of the work does not get done on
# file creation/modification, so we have to do some extra checking.
def property_fix
properties.each do |thing|
next unless [:mode, :owner, :group, :seluser, :selrole, :seltype, :selrange].include?(thing.name)
# Make sure we get a new stat objct
expire
currentvalue = thing.retrieve
thing.sync unless thing.safe_insync?(currentvalue)
end
end
end
# We put all of the properties in separate files, because there are so many
# of them. The order these are loaded is important, because it determines
# the order they are in the property lit.
require 'puppet/type/file/checksum'
require 'puppet/type/file/content' # can create the file
require 'puppet/type/file/source' # can create the file
require 'puppet/type/file/target' # creates a different type of file
require 'puppet/type/file/ensure' # can create the file
require 'puppet/type/file/owner'
require 'puppet/type/file/group'
require 'puppet/type/file/mode'
require 'puppet/type/file/type'
require 'puppet/type/file/selcontext' # SELinux file context
require 'puppet/type/file/ctime'
require 'puppet/type/file/mtime'
diff --git a/lib/puppet/type/tidy.rb b/lib/puppet/type/tidy.rb
index 146481fed..1a308e17d 100755
--- a/lib/puppet/type/tidy.rb
+++ b/lib/puppet/type/tidy.rb
@@ -1,333 +1,331 @@
Puppet::Type.newtype(:tidy) do
require 'puppet/file_serving/fileset'
require 'puppet/file_bucket/dipper'
@doc = "Remove unwanted files based on specific criteria. Multiple
criteria are OR'd together, so a file that is too large but is not
old enough will still get tidied.
If you don't specify either `age` or `size`, then all files will
be removed.
This resource type works by generating a file resource for every file
that should be deleted and then letting that resource perform the
actual deletion.
"
newparam(:path) do
desc "The path to the file or directory to manage. Must be fully
qualified."
isnamevar
end
newparam(:recurse) do
desc "If target is a directory, recursively descend
into the directory looking for files to tidy."
newvalues(:true, :false, :inf, /^[0-9]+$/)
# Replace the validation so that we allow numbers in
# addition to string representations of them.
validate { |arg| }
munge do |value|
newval = super(value)
case newval
when :true, :inf; true
when :false; false
when Integer, Fixnum, Bignum; value
when /^\d+$/; Integer(value)
else
raise ArgumentError, "Invalid recurse value #{value.inspect}"
end
end
end
newparam(:matches) do
desc "One or more (shell type) file glob patterns, which restrict
the list of files to be tidied to those whose basenames match
at least one of the patterns specified. Multiple patterns can
be specified using an array.
Example:
tidy { \"/tmp\":
age => \"1w\",
recurse => 1,
matches => [ \"[0-9]pub*.tmp\", \"*.temp\", \"tmpfile?\" ]
}
This removes files from `/tmp` if they are one week old or older,
are not in a subdirectory and match one of the shell globs given.
Note that the patterns are matched against the basename of each
file -- that is, your glob patterns should not have any '/'
characters in them, since you are only specifying against the last
bit of the file.
Finally, note that you must now specify a non-zero/non-false value
for recurse if matches is used, as matches only apply to files found
by recursion (there's no reason to use static patterns match against
a statically determined path). Requiering explicit recursion clears
up a common source of confusion."
# Make sure we convert to an array.
munge do |value|
fail "Tidy can't use matches with recurse 0, false, or undef" if "#{@resource[:recurse]}" =~ /^(0|false|)$/
[value].flatten
end
# Does a given path match our glob patterns, if any? Return true
# if no patterns have been provided.
def tidy?(path, stat)
basename = File.basename(path)
flags = File::FNM_DOTMATCH | File::FNM_PATHNAME
return(value.find {|pattern| File.fnmatch(pattern, basename, flags) } ? true : false)
end
end
newparam(:backup) do
desc "Whether tidied files should be backed up. Any values are passed
directly to the file resources used for actual file deletion, so use
its backup documentation to determine valid values."
end
newparam(:age) do
desc "Tidy files whose age is equal to or greater than
the specified time. You can choose seconds, minutes,
hours, days, or weeks by specifying the first letter of any
of those words (e.g., '1w').
Specifying 0 will remove all files."
@@ageconvertors = {
:s => 1,
:m => 60
}
@@ageconvertors[:h] = @@ageconvertors[:m] * 60
@@ageconvertors[:d] = @@ageconvertors[:h] * 24
@@ageconvertors[:w] = @@ageconvertors[:d] * 7
def convert(unit, multi)
if num = @@ageconvertors[unit]
return num * multi
else
self.fail "Invalid age unit '#{unit}'"
end
end
def tidy?(path, stat)
# If the file's older than we allow, we should get rid of it.
(Time.now.to_i - stat.send(resource[:type]).to_i) > value
end
munge do |age|
unit = multi = nil
case age
when /^([0-9]+)(\w)\w*$/
multi = Integer($1)
unit = $2.downcase.intern
when /^([0-9]+)$/
multi = Integer($1)
unit = :d
else
self.fail "Invalid tidy age #{age}"
end
convert(unit, multi)
end
end
newparam(:size) do
desc "Tidy files whose size is equal to or greater than
the specified size. Unqualified values are in kilobytes, but
*b*, *k*, *m*, *g*, and *t* can be appended to specify *bytes*,
*kilobytes*, *megabytes*, *gigabytes*, and *terabytes*, respectively.
Only the first character is significant, so the full word can also
be used."
@@sizeconvertors = {
:b => 0,
:k => 1,
:m => 2,
:g => 3,
:t => 4
}
def convert(unit, multi)
if num = @@sizeconvertors[unit]
result = multi
num.times do result *= 1024 end
return result
else
self.fail "Invalid size unit '#{unit}'"
end
end
def tidy?(path, stat)
stat.size >= value
end
munge do |size|
case size
when /^([0-9]+)(\w)\w*$/
multi = Integer($1)
unit = $2.downcase.intern
when /^([0-9]+)$/
multi = Integer($1)
unit = :k
else
self.fail "Invalid tidy size #{age}"
end
convert(unit, multi)
end
end
newparam(:type) do
desc "Set the mechanism for determining age."
newvalues(:atime, :mtime, :ctime)
defaultto :atime
end
newparam(:rmdirs, :boolean => true) do
desc "Tidy directories in addition to files; that is, remove
directories whose age is older than the specified criteria.
This will only remove empty directories, so all contained
files must also be tidied before a directory gets removed."
newvalues :true, :false
end
# Erase PFile's validate method
validate do
end
def self.instances
[]
end
- @depthfirst = true
+ def depthfirst?
+ true
+ end
def initialize(hash)
super
# only allow backing up into filebuckets
self[:backup] = false unless self[:backup].is_a? Puppet::FileBucket::Dipper
end
# Make a file resource to remove a given file.
def mkfile(path)
# Force deletion, so directories actually get deleted.
Puppet::Type.type(:file).new :path => path, :backup => self[:backup], :ensure => :absent, :force => true
end
def retrieve
# Our ensure property knows how to retrieve everything for us.
if obj = @parameters[:ensure]
return obj.retrieve
else
return {}
end
end
# Hack things a bit so we only ever check the ensure property.
def properties
[]
end
- def eval_generate
- []
- end
-
def generate
return [] unless stat(self[:path])
case self[:recurse]
when Integer, Fixnum, Bignum, /^\d+$/
parameter = { :recurse => true, :recurselimit => self[:recurse] }
when true, :true, :inf
parameter = { :recurse => true }
end
if parameter
files = Puppet::FileServing::Fileset.new(self[:path], parameter).files.collect do |f|
f == "." ? self[:path] : ::File.join(self[:path], f)
end
else
files = [self[:path]]
end
result = files.find_all { |path| tidy?(path) }.collect { |path| mkfile(path) }.each { |file| notice "Tidying #{file.ref}" }.sort { |a,b| b[:path] <=> a[:path] }
# No need to worry about relationships if we don't have rmdirs; there won't be
# any directories.
return result unless rmdirs?
# Now make sure that all directories require the files they contain, if all are available,
# so that a directory is emptied before we try to remove it.
files_by_name = result.inject({}) { |hash, file| hash[file[:path]] = file; hash }
files_by_name.keys.sort { |a,b| b <=> b }.each do |path|
dir = ::File.dirname(path)
next unless resource = files_by_name[dir]
if resource[:require]
resource[:require] << Puppet::Resource.new(:file, path)
else
resource[:require] = [Puppet::Resource.new(:file, path)]
end
end
result
end
# Does a given path match our glob patterns, if any? Return true
# if no patterns have been provided.
def matches?(path)
return true unless self[:matches]
basename = File.basename(path)
flags = File::FNM_DOTMATCH | File::FNM_PATHNAME
if self[:matches].find {|pattern| File.fnmatch(pattern, basename, flags) }
return true
else
debug "No specified patterns match #{path}, not tidying"
return false
end
end
# Should we remove the specified file?
def tidy?(path)
return false unless stat = self.stat(path)
return false if stat.ftype == "directory" and ! rmdirs?
# The 'matches' parameter isn't OR'ed with the other tests --
# it's just used to reduce the list of files we can match.
return false if param = parameter(:matches) and ! param.tidy?(path, stat)
tested = false
[:age, :size].each do |name|
next unless param = parameter(name)
tested = true
return true if param.tidy?(path, stat)
end
# If they don't specify either, then the file should always be removed.
return true unless tested
false
end
def stat(path)
begin
::File.lstat(path)
rescue Errno::ENOENT => error
info "File does not exist"
return nil
rescue Errno::EACCES => error
warning "Could not stat; permission denied"
return nil
end
end
end
diff --git a/lib/puppet/type/whit.rb b/lib/puppet/type/whit.rb
index 55bfcfb46..55ed0386e 100644
--- a/lib/puppet/type/whit.rb
+++ b/lib/puppet/type/whit.rb
@@ -1,11 +1,17 @@
Puppet::Type.newtype(:whit) do
desc "The smallest possible resource type, for when you need a resource and naught else."
newparam :name do
desc "The name of the whit, because it must have one."
end
def to_s
- "Class[#{name}]"
+ "(#{name})"
+ end
+
+ def refresh
+ # We don't do anything with them, but we need this to
+ # show that we are "refresh aware" and not break the
+ # chain of propogation.
end
end
diff --git a/spec/integration/indirector/catalog/compiler_spec.rb b/spec/integration/indirector/catalog/compiler_spec.rb
index 1146c20b0..dafa1af7c 100755
--- a/spec/integration/indirector/catalog/compiler_spec.rb
+++ b/spec/integration/indirector/catalog/compiler_spec.rb
@@ -1,68 +1,65 @@
#!/usr/bin/env ruby
require File.expand_path(File.dirname(__FILE__) + '/../../../spec_helper')
require 'puppet/resource/catalog'
Puppet::Resource::Catalog.indirection.terminus(:compiler)
describe Puppet::Resource::Catalog::Compiler do
before do
Facter.stubs(:value).returns "something"
@catalog = Puppet::Resource::Catalog.new
-
- @one = Puppet::Resource.new(:file, "/one")
-
- @two = Puppet::Resource.new(:file, "/two")
- @catalog.add_resource(@one, @two)
+ @catalog.add_resource(@one = Puppet::Resource.new(:file, "/one"))
+ @catalog.add_resource(@two = Puppet::Resource.new(:file, "/two"))
end
after { Puppet.settings.clear }
it "should remove virtual resources when filtering" do
@one.virtual = true
Puppet::Resource::Catalog.indirection.terminus.filter(@catalog).resource_refs.should == [ @two.ref ]
end
it "should not remove exported resources when filtering" do
@one.exported = true
Puppet::Resource::Catalog.indirection.terminus.filter(@catalog).resource_refs.sort.should == [ @one.ref, @two.ref ]
end
it "should remove virtual exported resources when filtering" do
@one.exported = true
@one.virtual = true
Puppet::Resource::Catalog.indirection.terminus.filter(@catalog).resource_refs.should == [ @two.ref ]
end
it "should filter out virtual resources when finding a catalog" do
@one.virtual = true
request = stub 'request', :name => "mynode"
Puppet::Resource::Catalog.indirection.terminus.stubs(:extract_facts_from_request)
Puppet::Resource::Catalog.indirection.terminus.stubs(:node_from_request)
Puppet::Resource::Catalog.indirection.terminus.stubs(:compile).returns(@catalog)
Puppet::Resource::Catalog.indirection.find(request).resource_refs.should == [ @two.ref ]
end
it "should not filter out exported resources when finding a catalog" do
@one.exported = true
request = stub 'request', :name => "mynode"
Puppet::Resource::Catalog.indirection.terminus.stubs(:extract_facts_from_request)
Puppet::Resource::Catalog.indirection.terminus.stubs(:node_from_request)
Puppet::Resource::Catalog.indirection.terminus.stubs(:compile).returns(@catalog)
Puppet::Resource::Catalog.indirection.find(request).resource_refs.sort.should == [ @one.ref, @two.ref ]
end
it "should filter out virtual exported resources when finding a catalog" do
@one.exported = true
@one.virtual = true
request = stub 'request', :name => "mynode"
Puppet::Resource::Catalog.indirection.terminus.stubs(:extract_facts_from_request)
Puppet::Resource::Catalog.indirection.terminus.stubs(:node_from_request)
Puppet::Resource::Catalog.indirection.terminus.stubs(:compile).returns(@catalog)
Puppet::Resource::Catalog.indirection.find(request).resource_refs.should == [ @two.ref ]
end
end
diff --git a/spec/integration/type/file_spec.rb b/spec/integration/type/file_spec.rb
index 46900a0f1..513b96e41 100755
--- a/spec/integration/type/file_spec.rb
+++ b/spec/integration/type/file_spec.rb
@@ -1,509 +1,512 @@
#!/usr/bin/env ruby
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
require 'puppet_spec/files'
describe Puppet::Type.type(:file) do
include PuppetSpec::Files
before do
# stub this to not try to create state.yaml
Puppet::Util::Storage.stubs(:store)
end
it "should not attempt to manage files that do not exist if no means of creating the file is specified" do
file = Puppet::Type.type(:file).new :path => "/my/file", :mode => "755"
catalog = Puppet::Resource::Catalog.new
catalog.add_resource file
file.parameter(:mode).expects(:retrieve).never
transaction = Puppet::Transaction.new(catalog)
transaction.resource_harness.evaluate(file).should_not be_failed
end
describe "when writing files" do
it "should backup files to a filebucket when one is configured" do
bucket = Puppet::Type.type(:filebucket).new :path => tmpfile("filebucket"), :name => "mybucket"
file = Puppet::Type.type(:file).new :path => tmpfile("bucket_backs"), :backup => "mybucket", :content => "foo"
catalog = Puppet::Resource::Catalog.new
- catalog.add_resource file, bucket
+ catalog.add_resource file
+ catalog.add_resource bucket
File.open(file[:path], "w") { |f| f.puts "bar" }
md5 = Digest::MD5.hexdigest(File.read(file[:path]))
catalog.apply
bucket.bucket.getfile(md5).should == "bar\n"
end
it "should backup files in the local directory when a backup string is provided" do
file = Puppet::Type.type(:file).new :path => tmpfile("bucket_backs"), :backup => ".bak", :content => "foo"
catalog = Puppet::Resource::Catalog.new
catalog.add_resource file
File.open(file[:path], "w") { |f| f.puts "bar" }
catalog.apply
backup = file[:path] + ".bak"
FileTest.should be_exist(backup)
File.read(backup).should == "bar\n"
end
it "should fail if no backup can be performed" do
dir = tmpfile("backups")
Dir.mkdir(dir)
path = File.join(dir, "testfile")
file = Puppet::Type.type(:file).new :path => path, :backup => ".bak", :content => "foo"
catalog = Puppet::Resource::Catalog.new
catalog.add_resource file
File.open(file[:path], "w") { |f| f.puts "bar" }
# Create a directory where the backup should be so that writing to it fails
Dir.mkdir(File.join(dir, "testfile.bak"))
Puppet::Util::Log.stubs(:newmessage)
catalog.apply
File.read(file[:path]).should == "bar\n"
end
it "should not backup symlinks" do
link = tmpfile("link")
dest1 = tmpfile("dest1")
dest2 = tmpfile("dest2")
bucket = Puppet::Type.type(:filebucket).new :path => tmpfile("filebucket"), :name => "mybucket"
file = Puppet::Type.type(:file).new :path => link, :target => dest2, :ensure => :link, :backup => "mybucket"
catalog = Puppet::Resource::Catalog.new
- catalog.add_resource file, bucket
+ catalog.add_resource file
+ catalog.add_resource bucket
File.open(dest1, "w") { |f| f.puts "whatever" }
File.symlink(dest1, link)
md5 = Digest::MD5.hexdigest(File.read(file[:path]))
catalog.apply
File.readlink(link).should == dest2
Find.find(bucket[:path]) { |f| File.file?(f) }.should be_nil
end
it "should backup directories to the local filesystem by copying the whole directory" do
file = Puppet::Type.type(:file).new :path => tmpfile("bucket_backs"), :backup => ".bak", :content => "foo", :force => true
catalog = Puppet::Resource::Catalog.new
catalog.add_resource file
Dir.mkdir(file[:path])
otherfile = File.join(file[:path], "foo")
File.open(otherfile, "w") { |f| f.print "yay" }
catalog.apply
backup = file[:path] + ".bak"
FileTest.should be_directory(backup)
File.read(File.join(backup, "foo")).should == "yay"
end
it "should backup directories to filebuckets by backing up each file separately" do
bucket = Puppet::Type.type(:filebucket).new :path => tmpfile("filebucket"), :name => "mybucket"
file = Puppet::Type.type(:file).new :path => tmpfile("bucket_backs"), :backup => "mybucket", :content => "foo", :force => true
catalog = Puppet::Resource::Catalog.new
- catalog.add_resource file, bucket
+ catalog.add_resource file
+ catalog.add_resource bucket
Dir.mkdir(file[:path])
foofile = File.join(file[:path], "foo")
barfile = File.join(file[:path], "bar")
File.open(foofile, "w") { |f| f.print "fooyay" }
File.open(barfile, "w") { |f| f.print "baryay" }
foomd5 = Digest::MD5.hexdigest(File.read(foofile))
barmd5 = Digest::MD5.hexdigest(File.read(barfile))
catalog.apply
bucket.bucket.getfile(foomd5).should == "fooyay"
bucket.bucket.getfile(barmd5).should == "baryay"
end
it "should propagate failures encountered when renaming the temporary file" do
file = Puppet::Type.type(:file).new :path => tmpfile("fail_rename"), :content => "foo"
file.stubs(:remove_existing) # because it tries to make a backup
catalog = Puppet::Resource::Catalog.new
catalog.add_resource file
File.open(file[:path], "w") { |f| f.print "bar" }
File.expects(:rename).raises ArgumentError
lambda { file.write(:content) }.should raise_error(Puppet::Error)
File.read(file[:path]).should == "bar"
end
end
describe "when recursing" do
def build_path(dir)
Dir.mkdir(dir)
File.chmod(0750, dir)
@dirs = [dir]
@files = []
%w{one two}.each do |subdir|
fdir = File.join(dir, subdir)
Dir.mkdir(fdir)
File.chmod(0750, fdir)
@dirs << fdir
%w{three}.each do |file|
ffile = File.join(fdir, file)
@files << ffile
File.open(ffile, "w") { |f| f.puts "test #{file}" }
File.chmod(0640, ffile)
end
end
end
it "should be able to recurse over a nonexistent file" do
@path = tmpfile("file_integration_tests")
@file = Puppet::Type::File.new(
:name => @path,
:mode => 0644,
:recurse => true,
:backup => false
)
@catalog = Puppet::Resource::Catalog.new
@catalog.add_resource @file
lambda { @file.eval_generate }.should_not raise_error
end
it "should be able to recursively set properties on existing files" do
@path = tmpfile("file_integration_tests")
build_path(@path)
@file = Puppet::Type::File.new(
:name => @path,
:mode => 0644,
:recurse => true,
:backup => false
)
@catalog = Puppet::Resource::Catalog.new
@catalog.add_resource @file
@catalog.apply
@dirs.each do |path|
(File.stat(path).mode & 007777).should == 0755
end
@files.each do |path|
(File.stat(path).mode & 007777).should == 0644
end
end
it "should be able to recursively make links to other files" do
source = tmpfile("file_link_integration_source")
build_path(source)
dest = tmpfile("file_link_integration_dest")
@file = Puppet::Type::File.new(:name => dest, :target => source, :recurse => true, :ensure => :link, :backup => false)
@catalog = Puppet::Resource::Catalog.new
@catalog.add_resource @file
@catalog.apply
@dirs.each do |path|
link_path = path.sub(source, dest)
File.lstat(link_path).should be_directory
end
@files.each do |path|
link_path = path.sub(source, dest)
File.lstat(link_path).ftype.should == "link"
end
end
it "should be able to recursively copy files" do
source = tmpfile("file_source_integration_source")
build_path(source)
dest = tmpfile("file_source_integration_dest")
@file = Puppet::Type::File.new(:name => dest, :source => source, :recurse => true, :backup => false)
@catalog = Puppet::Resource::Catalog.new
@catalog.add_resource @file
@catalog.apply
@dirs.each do |path|
newpath = path.sub(source, dest)
File.lstat(newpath).should be_directory
end
@files.each do |path|
newpath = path.sub(source, dest)
File.lstat(newpath).ftype.should == "file"
end
end
it "should not recursively manage files managed by a more specific explicit file" do
dir = tmpfile("recursion_vs_explicit_1")
subdir = File.join(dir, "subdir")
file = File.join(subdir, "file")
FileUtils.mkdir_p(subdir)
File.open(file, "w") { |f| f.puts "" }
base = Puppet::Type::File.new(:name => dir, :recurse => true, :backup => false, :mode => "755")
sub = Puppet::Type::File.new(:name => subdir, :recurse => true, :backup => false, :mode => "644")
@catalog = Puppet::Resource::Catalog.new
@catalog.add_resource base
@catalog.add_resource sub
@catalog.apply
(File.stat(file).mode & 007777).should == 0644
end
it "should recursively manage files even if there is an explicit file whose name is a prefix of the managed file" do
dir = tmpfile("recursion_vs_explicit_2")
managed = File.join(dir, "file")
generated = File.join(dir, "file_with_a_name_starting_with_the_word_file")
FileUtils.mkdir_p(dir)
File.open(managed, "w") { |f| f.puts "" }
File.open(generated, "w") { |f| f.puts "" }
@catalog = Puppet::Resource::Catalog.new
@catalog.add_resource Puppet::Type::File.new(:name => dir, :recurse => true, :backup => false, :mode => "755")
@catalog.add_resource Puppet::Type::File.new(:name => managed, :recurse => true, :backup => false, :mode => "644")
@catalog.apply
(File.stat(generated).mode & 007777).should == 0755
end
end
describe "when generating resources" do
before do
@source = tmpfile("generating_in_catalog_source")
@dest = tmpfile("generating_in_catalog_dest")
Dir.mkdir(@source)
s1 = File.join(@source, "one")
s2 = File.join(@source, "two")
File.open(s1, "w") { |f| f.puts "uno" }
File.open(s2, "w") { |f| f.puts "dos" }
@file = Puppet::Type::File.new(:name => @dest, :source => @source, :recurse => true, :backup => false)
@catalog = Puppet::Resource::Catalog.new
@catalog.add_resource @file
end
it "should add each generated resource to the catalog" do
@catalog.apply do |trans|
@catalog.resource(:file, File.join(@dest, "one")).should be_instance_of(@file.class)
@catalog.resource(:file, File.join(@dest, "two")).should be_instance_of(@file.class)
end
end
it "should have an edge to each resource in the relationship graph" do
@catalog.apply do |trans|
one = @catalog.resource(:file, File.join(@dest, "one"))
- @catalog.relationship_graph.should be_edge(@file, one)
+ @catalog.relationship_graph.edge?(@file, one).should be
two = @catalog.resource(:file, File.join(@dest, "two"))
- @catalog.relationship_graph.should be_edge(@file, two)
+ @catalog.relationship_graph.edge?(@file, two).should be
end
end
end
describe "when copying files" do
# Ticket #285.
it "should be able to copy files with pound signs in their names" do
source = tmpfile("filewith#signs")
dest = tmpfile("destwith#signs")
File.open(source, "w") { |f| f.print "foo" }
file = Puppet::Type::File.new(:name => dest, :source => source)
catalog = Puppet::Resource::Catalog.new
catalog.add_resource file
catalog.apply
File.read(dest).should == "foo"
end
it "should be able to copy files with spaces in their names" do
source = tmpfile("filewith spaces")
dest = tmpfile("destwith spaces")
File.open(source, "w") { |f| f.print "foo" }
File.chmod(0755, source)
file = Puppet::Type::File.new(:path => dest, :source => source)
catalog = Puppet::Resource::Catalog.new
catalog.add_resource file
catalog.apply
File.read(dest).should == "foo"
(File.stat(dest).mode & 007777).should == 0755
end
it "should be able to copy individual files even if recurse has been specified" do
source = tmpfile("source")
dest = tmpfile("dest")
File.open(source, "w") { |f| f.print "foo" }
file = Puppet::Type::File.new(:name => dest, :source => source, :recurse => true)
catalog = Puppet::Resource::Catalog.new
catalog.add_resource file
catalog.apply
File.read(dest).should == "foo"
end
end
it "should be able to create files when 'content' is specified but 'ensure' is not" do
dest = tmpfile("files_with_content")
file = Puppet::Type.type(:file).new(
:name => dest,
:content => "this is some content, yo"
)
catalog = Puppet::Resource::Catalog.new
catalog.add_resource file
catalog.apply
File.read(dest).should == "this is some content, yo"
end
it "should create files with content if both 'content' and 'ensure' are set" do
dest = tmpfile("files_with_content")
file = Puppet::Type.type(:file).new(
:name => dest,
:ensure => "file",
:content => "this is some content, yo"
)
catalog = Puppet::Resource::Catalog.new
catalog.add_resource file
catalog.apply
File.read(dest).should == "this is some content, yo"
end
it "should delete files with sources but that are set for deletion" do
dest = tmpfile("dest_source_with_ensure")
source = tmpfile("source_source_with_ensure")
File.open(source, "w") { |f| f.puts "yay" }
File.open(dest, "w") { |f| f.puts "boo" }
file = Puppet::Type.type(:file).new(
:name => dest,
:ensure => :absent,
:source => source,
:backup => false
)
catalog = Puppet::Resource::Catalog.new
catalog.add_resource file
catalog.apply
File.should_not be_exist(dest)
end
describe "when purging files" do
before do
@sourcedir = tmpfile("purge_source")
@destdir = tmpfile("purge_dest")
Dir.mkdir(@sourcedir)
Dir.mkdir(@destdir)
@sourcefile = File.join(@sourcedir, "sourcefile")
@copiedfile = File.join(@destdir, "sourcefile")
@localfile = File.join(@destdir, "localfile")
@purgee = File.join(@destdir, "to_be_purged")
File.open(@localfile, "w") { |f| f.puts "rahtest" }
File.open(@sourcefile, "w") { |f| f.puts "funtest" }
# this file should get removed
File.open(@purgee, "w") { |f| f.puts "footest" }
@lfobj = Puppet::Type.newfile(
:title => "localfile",
:path => @localfile,
:content => "rahtest\n",
:ensure => :file,
:backup => false
)
@destobj = Puppet::Type.newfile(
:title => "destdir",
:path => @destdir,
:source => @sourcedir,
:backup => false,
:purge => true,
:recurse => true
)
@catalog = Puppet::Resource::Catalog.new
@catalog.add_resource @lfobj, @destobj
end
it "should still copy remote files" do
@catalog.apply
FileTest.should be_exist(@copiedfile)
end
it "should not purge managed, local files" do
@catalog.apply
FileTest.should be_exist(@localfile)
end
it "should purge files that are neither remote nor otherwise managed" do
@catalog.apply
FileTest.should_not be_exist(@purgee)
end
end
end
diff --git a/spec/unit/parser/functions/create_resources_spec.rb b/spec/unit/parser/functions/create_resources_spec.rb
index d4095b777..366fb536c 100755
--- a/spec/unit/parser/functions/create_resources_spec.rb
+++ b/spec/unit/parser/functions/create_resources_spec.rb
@@ -1,135 +1,137 @@
require 'puppet'
require File.dirname(__FILE__) + '/../../../spec_helper'
describe 'function for dynamically creating resources' do
def get_scope
@topscope = Puppet::Parser::Scope.new
# This is necessary so we don't try to use the compiler to discover our parent.
@topscope.parent = nil
@scope = Puppet::Parser::Scope.new
@scope.compiler = Puppet::Parser::Compiler.new(Puppet::Node.new("floppy", :environment => 'production'))
@scope.parent = @topscope
@compiler = @scope.compiler
end
before :each do
get_scope
Puppet::Parser::Functions.function(:create_resources)
end
it "should exist" do
Puppet::Parser::Functions.function(:create_resources).should == "function_create_resources"
end
it 'should require two arguments' do
lambda { @scope.function_create_resources(['foo']) }.should raise_error(ArgumentError, 'create_resources(): wrong number of arguments (1; must be 2)')
end
describe 'when creating native types' do
before :each do
Puppet[:code]='notify{test:}'
get_scope
@scope.resource=Puppet::Parser::Resource.new('class', 't', :scope => @scope)
end
it 'empty hash should not cause resources to be added' do
@scope.function_create_resources(['file', {}])
@compiler.catalog.resources.size == 1
end
it 'should be able to add' do
@scope.function_create_resources(['file', {'/etc/foo'=>{'ensure'=>'present'}}])
@compiler.catalog.resource(:file, "/etc/foo")['ensure'].should == 'present'
end
it 'should accept multiple types' do
type_hash = {}
type_hash['foo'] = {'message' => 'one'}
type_hash['bar'] = {'message' => 'two'}
@scope.function_create_resources(['notify', type_hash])
@compiler.catalog.resource(:notify, "foo")['message'].should == 'one'
@compiler.catalog.resource(:notify, "bar")['message'].should == 'two'
end
it 'should fail to add non-existing type' do
lambda { @scope.function_create_resources(['foo', {}]) }.should raise_error(ArgumentError, 'could not create resource of unknown type foo')
end
it 'should be able to add edges' do
@scope.function_create_resources(['notify', {'foo'=>{'require' => 'Notify[test]'}}])
@scope.compiler.compile
- edge = @scope.compiler.catalog.to_ral.relationship_graph.edges.detect do |edge|
- edge.source.title == 'test'
- end
- edge.source.title.should == 'test'
- edge.target.title.should == 'foo'
+ rg = @scope.compiler.catalog.to_ral.relationship_graph
+ test = rg.vertices.find { |v| v.title == 'test' }
+ foo = rg.vertices.find { |v| v.title == 'foo' }
+ test.should be
+ foo.should be
+ rg.path_between(test,foo).should be
end
end
describe 'when dynamically creating resource types' do
before :each do
Puppet[:code]=
'define foo($one){notify{$name: message => $one}}
notify{test:}
'
get_scope
@scope.resource=Puppet::Parser::Resource.new('class', 't', :scope => @scope)
Puppet::Parser::Functions.function(:create_resources)
end
it 'should be able to create defined resoure types' do
@scope.function_create_resources(['foo', {'blah'=>{'one'=>'two'}}])
# still have to compile for this to work...
# I am not sure if this constraint ruins the tests
@scope.compiler.compile
@compiler.catalog.resource(:notify, "blah")['message'].should == 'two'
end
it 'should fail if defines are missing params' do
@scope.function_create_resources(['foo', {'blah'=>{}}])
lambda { @scope.compiler.compile }.should raise_error(Puppet::ParseError, 'Must pass one to Foo[blah] at line 1')
end
it 'should be able to add multiple defines' do
hash = {}
hash['blah'] = {'one' => 'two'}
hash['blaz'] = {'one' => 'three'}
@scope.function_create_resources(['foo', hash])
# still have to compile for this to work...
# I am not sure if this constraint ruins the tests
@scope.compiler.compile
@compiler.catalog.resource(:notify, "blah")['message'].should == 'two'
@compiler.catalog.resource(:notify, "blaz")['message'].should == 'three'
end
it 'should be able to add edges' do
@scope.function_create_resources(['foo', {'blah'=>{'one'=>'two', 'require' => 'Notify[test]'}}])
@scope.compiler.compile
- edge = @scope.compiler.catalog.to_ral.relationship_graph.edges.detect do |edge|
- edge.source.title == 'test'
- end
- edge.source.title.should == 'test'
- edge.target.title.should == 'blah'
+ rg = @scope.compiler.catalog.to_ral.relationship_graph
+ test = rg.vertices.find { |v| v.title == 'test' }
+ blah = rg.vertices.find { |v| v.title == 'blah' }
+ test.should be
+ blah.should be
+ # (Yoda speak like we do)
+ rg.path_between(test,blah).should be
@compiler.catalog.resource(:notify, "blah")['message'].should == 'two'
end
end
describe 'when creating classes' do
before :each do
Puppet[:code]=
'class bar($one){notify{test: message => $one}}
notify{tester:}
'
get_scope
@scope.resource=Puppet::Parser::Resource.new('class', 't', :scope => @scope)
Puppet::Parser::Functions.function(:create_resources)
end
it 'should be able to create classes' do
@scope.function_create_resources(['class', {'bar'=>{'one'=>'two'}}])
@scope.compiler.compile
@compiler.catalog.resource(:notify, "test")['message'].should == 'two'
@compiler.catalog.resource(:class, "bar").should_not be_nil#['message'].should == 'two'
end
it 'should fail to create non-existing classes' do
lambda { @scope.function_create_resources(['class', {'blah'=>{'one'=>'two'}}]) }.should raise_error(ArgumentError ,'could not find hostclass blah')
end
it 'should be able to add edges' do
@scope.function_create_resources(['class', {'bar'=>{'one'=>'two', 'require' => 'Notify[tester]'}}])
@scope.compiler.compile
- edge = @scope.compiler.catalog.to_ral.relationship_graph.edges.detect do |e|
- e.source.title == 'tester'
- end
- edge.source.title.should == 'tester'
- edge.target.title.should == 'test'
- #@compiler.catalog.resource(:notify, "blah")['message'].should == 'two'
+ rg = @scope.compiler.catalog.to_ral.relationship_graph
+ test = rg.vertices.find { |v| v.title == 'test' }
+ tester = rg.vertices.find { |v| v.title == 'tester' }
+ test.should be
+ tester.should be
+ rg.path_between(tester,test).should be
end
-
end
end
diff --git a/spec/unit/resource/catalog_spec.rb b/spec/unit/resource/catalog_spec.rb
index 42850c23b..78d1b3223 100755
--- a/spec/unit/resource/catalog_spec.rb
+++ b/spec/unit/resource/catalog_spec.rb
@@ -1,1068 +1,1062 @@
#!/usr/bin/env ruby
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
describe Puppet::Resource::Catalog, "when compiling" do
before do
@basepath = Puppet.features.posix? ? "/somepath" : "C:/somepath"
# stub this to not try to create state.yaml
Puppet::Util::Storage.stubs(:store)
end
it "should be an Expirer" do
Puppet::Resource::Catalog.ancestors.should be_include(Puppet::Util::Cacher::Expirer)
end
it "should always be expired if it's not applying" do
@catalog = Puppet::Resource::Catalog.new("host")
@catalog.expects(:applying?).returns false
@catalog.should be_dependent_data_expired(Time.now)
end
it "should not be expired if it's applying and the timestamp is late enough" do
@catalog = Puppet::Resource::Catalog.new("host")
@catalog.expire
@catalog.expects(:applying?).returns true
@catalog.should_not be_dependent_data_expired(Time.now)
end
it "should be able to write its list of classes to the class file" do
@catalog = Puppet::Resource::Catalog.new("host")
@catalog.add_class "foo", "bar"
Puppet.settings.expects(:value).with(:classfile).returns "/class/file"
fh = mock 'filehandle'
File.expects(:open).with("/class/file", "w").yields fh
fh.expects(:puts).with "foo\nbar"
@catalog.write_class_file
end
it "should have a client_version attribute" do
@catalog = Puppet::Resource::Catalog.new("host")
@catalog.client_version = 5
@catalog.client_version.should == 5
end
it "should have a server_version attribute" do
@catalog = Puppet::Resource::Catalog.new("host")
@catalog.server_version = 5
@catalog.server_version.should == 5
end
describe "when compiling" do
it "should accept tags" do
config = Puppet::Resource::Catalog.new("mynode")
config.tag("one")
config.tags.should == %w{one}
end
it "should accept multiple tags at once" do
config = Puppet::Resource::Catalog.new("mynode")
config.tag("one", "two")
config.tags.should == %w{one two}
end
it "should convert all tags to strings" do
config = Puppet::Resource::Catalog.new("mynode")
config.tag("one", :two)
config.tags.should == %w{one two}
end
it "should tag with both the qualified name and the split name" do
config = Puppet::Resource::Catalog.new("mynode")
config.tag("one::two")
config.tags.include?("one").should be_true
config.tags.include?("one::two").should be_true
end
it "should accept classes" do
config = Puppet::Resource::Catalog.new("mynode")
config.add_class("one")
config.classes.should == %w{one}
config.add_class("two", "three")
config.classes.should == %w{one two three}
end
it "should tag itself with passed class names" do
config = Puppet::Resource::Catalog.new("mynode")
config.add_class("one")
config.tags.should == %w{one}
end
end
describe "when extracting transobjects" do
def mkscope
@node = Puppet::Node.new("mynode")
@compiler = Puppet::Parser::Compiler.new(@node)
# XXX This is ridiculous.
@compiler.send(:evaluate_main)
@scope = @compiler.topscope
end
def mkresource(type, name)
Puppet::Parser::Resource.new(type, name, :source => @source, :scope => @scope)
end
it "should fail if no 'main' stage can be found" do
lambda { Puppet::Resource::Catalog.new("mynode").extract }.should raise_error(Puppet::DevError)
end
it "should warn if any non-main stages are present" do
config = Puppet::Resource::Catalog.new("mynode")
@scope = mkscope
@source = mock 'source'
main = mkresource("stage", "main")
config.add_resource(main)
other = mkresource("stage", "other")
config.add_resource(other)
Puppet.expects(:warning)
config.extract
end
it "should always create a TransBucket for the 'main' stage" do
config = Puppet::Resource::Catalog.new("mynode")
@scope = mkscope
@source = mock 'source'
main = mkresource("stage", "main")
config.add_resource(main)
result = config.extract
result.type.should == "Stage"
result.name.should == "main"
end
# Now try it with a more complicated graph -- a three tier graph, each tier
it "should transform arbitrarily deep graphs into isomorphic trees" do
config = Puppet::Resource::Catalog.new("mynode")
@scope = mkscope
@scope.stubs(:tags).returns([])
@source = mock 'source'
# Create our scopes.
top = mkresource "stage", "main"
config.add_resource top
topbucket = []
topbucket.expects(:classes=).with([])
top.expects(:to_trans).returns(topbucket)
topres = mkresource "file", "/top"
topres.expects(:to_trans).returns(:topres)
config.add_edge top, topres
middle = mkresource "class", "middle"
middle.expects(:to_trans).returns([])
config.add_edge top, middle
midres = mkresource "file", "/mid"
midres.expects(:to_trans).returns(:midres)
config.add_edge middle, midres
bottom = mkresource "class", "bottom"
bottom.expects(:to_trans).returns([])
config.add_edge middle, bottom
botres = mkresource "file", "/bot"
botres.expects(:to_trans).returns(:botres)
config.add_edge bottom, botres
toparray = config.extract
# This is annoying; it should look like:
# [[[:botres], :midres], :topres]
# but we can't guarantee sort order.
toparray.include?(:topres).should be_true
midarray = toparray.find { |t| t.is_a?(Array) }
midarray.include?(:midres).should be_true
botarray = midarray.find { |t| t.is_a?(Array) }
botarray.include?(:botres).should be_true
end
end
describe " when converting to a Puppet::Resource catalog" do
before do
@original = Puppet::Resource::Catalog.new("mynode")
@original.tag(*%w{one two three})
@original.add_class *%w{four five six}
@top = Puppet::TransObject.new 'top', "class"
@topobject = Puppet::TransObject.new '/topobject', "file"
@middle = Puppet::TransObject.new 'middle', "class"
@middleobject = Puppet::TransObject.new '/middleobject', "file"
@bottom = Puppet::TransObject.new 'bottom', "class"
@bottomobject = Puppet::TransObject.new '/bottomobject', "file"
@resources = [@top, @topobject, @middle, @middleobject, @bottom, @bottomobject]
@original.add_resource(*@resources)
@original.add_edge(@top, @topobject)
@original.add_edge(@top, @middle)
@original.add_edge(@middle, @middleobject)
@original.add_edge(@middle, @bottom)
@original.add_edge(@bottom, @bottomobject)
@catalog = @original.to_resource
end
it "should copy over the version" do
@original.version = "foo"
@original.to_resource.version.should == "foo"
end
it "should convert parser resources to plain resources" do
resource = Puppet::Parser::Resource.new(:file, "foo", :scope => stub("scope", :environment => nil, :namespaces => nil), :source => stub("source"))
catalog = Puppet::Resource::Catalog.new("whev")
catalog.add_resource(resource)
new = catalog.to_resource
new.resource(:file, "foo").class.should == Puppet::Resource
end
it "should add all resources as Puppet::Resource instances" do
@resources.each { |resource| @catalog.resource(resource.ref).should be_instance_of(Puppet::Resource) }
end
it "should copy the tag list to the new catalog" do
@catalog.tags.sort.should == @original.tags.sort
end
it "should copy the class list to the new catalog" do
@catalog.classes.should == @original.classes
end
it "should duplicate the original edges" do
@original.edges.each do |edge|
@catalog.edge?(@catalog.resource(edge.source.ref), @catalog.resource(edge.target.ref)).should be_true
end
end
it "should set itself as the catalog for each converted resource" do
@catalog.vertices.each { |v| v.catalog.object_id.should equal(@catalog.object_id) }
end
end
describe "when converting to a RAL catalog" do
before do
@original = Puppet::Resource::Catalog.new("mynode")
@original.tag(*%w{one two three})
@original.add_class *%w{four five six}
@top = Puppet::Resource.new :class, 'top'
@topobject = Puppet::Resource.new :file, @basepath+'/topobject'
@middle = Puppet::Resource.new :class, 'middle'
@middleobject = Puppet::Resource.new :file, @basepath+'/middleobject'
@bottom = Puppet::Resource.new :class, 'bottom'
@bottomobject = Puppet::Resource.new :file, @basepath+'/bottomobject'
@resources = [@top, @topobject, @middle, @middleobject, @bottom, @bottomobject]
@original.add_resource(*@resources)
@original.add_edge(@top, @topobject)
@original.add_edge(@top, @middle)
@original.add_edge(@middle, @middleobject)
@original.add_edge(@middle, @bottom)
@original.add_edge(@bottom, @bottomobject)
@catalog = @original.to_ral
end
it "should add all resources as RAL instances" do
@resources.each { |resource| @catalog.resource(resource.ref).should be_instance_of(Puppet::Type) }
end
it "should copy the tag list to the new catalog" do
@catalog.tags.sort.should == @original.tags.sort
end
it "should copy the class list to the new catalog" do
@catalog.classes.should == @original.classes
end
it "should duplicate the original edges" do
@original.edges.each do |edge|
@catalog.edge?(@catalog.resource(edge.source.ref), @catalog.resource(edge.target.ref)).should be_true
end
end
it "should set itself as the catalog for each converted resource" do
@catalog.vertices.each { |v| v.catalog.object_id.should equal(@catalog.object_id) }
end
# This tests #931.
it "should not lose track of resources whose names vary" do
changer = Puppet::TransObject.new 'changer', 'test'
config = Puppet::Resource::Catalog.new('test')
config.add_resource(changer)
config.add_resource(@top)
config.add_edge(@top, changer)
resource = stub 'resource', :name => "changer2", :title => "changer2", :ref => "Test[changer2]", :catalog= => nil, :remove => nil
#changer is going to get duplicated as part of a fix for aliases 1094
changer.expects(:dup).returns(changer)
changer.expects(:to_ral).returns(resource)
newconfig = nil
proc { @catalog = config.to_ral }.should_not raise_error
@catalog.resource("Test[changer2]").should equal(resource)
end
after do
# Remove all resource instances.
@catalog.clear(true)
end
end
describe "when filtering" do
before :each do
@original = Puppet::Resource::Catalog.new("mynode")
@original.tag(*%w{one two three})
@original.add_class *%w{four five six}
@r1 = stub_everything 'r1', :ref => "File[/a]"
@r1.stubs(:respond_to?).with(:ref).returns(true)
@r1.stubs(:dup).returns(@r1)
@r1.stubs(:is_a?).returns(Puppet::Resource).returns(true)
@r2 = stub_everything 'r2', :ref => "File[/b]"
@r2.stubs(:respond_to?).with(:ref).returns(true)
@r2.stubs(:dup).returns(@r2)
@r2.stubs(:is_a?).returns(Puppet::Resource).returns(true)
@resources = [@r1,@r2]
@original.add_resource(@r1,@r2)
end
it "should transform the catalog to a resource catalog" do
@original.expects(:to_catalog).with { |h,b| h == :to_resource }
@original.filter
end
it "should scan each catalog resource in turn and apply filtering block" do
@resources.each { |r| r.expects(:test?) }
@original.filter do |r|
r.test?
end
end
it "should filter out resources which produce true when the filter block is evaluated" do
@original.filter do |r|
r == @r1
end.resource("File[/a]").should be_nil
end
it "should not consider edges against resources that were filtered out" do
@original.add_edge(@r1,@r2)
@original.filter do |r|
r == @r1
end.edge?(@r1,@r2).should_not be
end
end
describe "when functioning as a resource container" do
before do
@catalog = Puppet::Resource::Catalog.new("host")
@one = Puppet::Type.type(:notify).new :name => "one"
@two = Puppet::Type.type(:notify).new :name => "two"
@dupe = Puppet::Type.type(:notify).new :name => "one"
end
it "should provide a method to add one or more resources" do
@catalog.add_resource @one, @two
@catalog.resource(@one.ref).should equal(@one)
@catalog.resource(@two.ref).should equal(@two)
end
it "should add resources to the relationship graph if it exists" do
relgraph = @catalog.relationship_graph
@catalog.add_resource @one
relgraph.should be_vertex(@one)
end
- it "should yield added resources if a block is provided" do
- yielded = []
- @catalog.add_resource(@one, @two) { |r| yielded << r }
- yielded.length.should == 2
- end
-
it "should set itself as the resource's catalog if it is not a relationship graph" do
@one.expects(:catalog=).with(@catalog)
@catalog.add_resource @one
end
it "should make all vertices available by resource reference" do
@catalog.add_resource(@one)
@catalog.resource(@one.ref).should equal(@one)
@catalog.vertices.find { |r| r.ref == @one.ref }.should equal(@one)
end
it "should canonize how resources are referred to during retrieval when both type and title are provided" do
@catalog.add_resource(@one)
@catalog.resource("notify", "one").should equal(@one)
end
it "should canonize how resources are referred to during retrieval when just the title is provided" do
@catalog.add_resource(@one)
@catalog.resource("notify[one]", nil).should equal(@one)
end
it "should not allow two resources with the same resource reference" do
@catalog.add_resource(@one)
proc { @catalog.add_resource(@dupe) }.should raise_error(Puppet::Resource::Catalog::DuplicateResourceError)
end
it "should not store objects that do not respond to :ref" do
proc { @catalog.add_resource("thing") }.should raise_error(ArgumentError)
end
it "should remove all resources when asked" do
@catalog.add_resource @one
@catalog.add_resource @two
@one.expects :remove
@two.expects :remove
@catalog.clear(true)
end
it "should support a mechanism for finishing resources" do
@one.expects :finish
@two.expects :finish
@catalog.add_resource @one
@catalog.add_resource @two
@catalog.finalize
end
it "should make default resources when finalizing" do
@catalog.expects(:make_default_resources)
@catalog.finalize
end
it "should add default resources to the catalog upon creation" do
@catalog.make_default_resources
@catalog.resource(:schedule, "daily").should_not be_nil
end
it "should optionally support an initialization block and should finalize after such blocks" do
@one.expects :finish
@two.expects :finish
config = Puppet::Resource::Catalog.new("host") do |conf|
conf.add_resource @one
conf.add_resource @two
end
end
it "should inform the resource that it is the resource's catalog" do
@one.expects(:catalog=).with(@catalog)
@catalog.add_resource @one
end
it "should be able to find resources by reference" do
@catalog.add_resource @one
@catalog.resource(@one.ref).should equal(@one)
end
it "should be able to find resources by reference or by type/title tuple" do
@catalog.add_resource @one
@catalog.resource("notify", "one").should equal(@one)
end
it "should have a mechanism for removing resources" do
@catalog.add_resource @one
@one.expects :remove
@catalog.remove_resource(@one)
@catalog.resource(@one.ref).should be_nil
@catalog.vertex?(@one).should be_false
end
it "should have a method for creating aliases for resources" do
@catalog.add_resource @one
@catalog.alias(@one, "other")
@catalog.resource("notify", "other").should equal(@one)
end
it "should ignore conflicting aliases that point to the aliased resource" do
@catalog.alias(@one, "other")
lambda { @catalog.alias(@one, "other") }.should_not raise_error
end
it "should create aliases for resources isomorphic resources whose names do not match their titles" do
resource = Puppet::Type::File.new(:title => "testing", :path => @basepath+"/something")
@catalog.add_resource(resource)
@catalog.resource(:file, @basepath+"/something").should equal(resource)
end
it "should not create aliases for resources non-isomorphic resources whose names do not match their titles" do
resource = Puppet::Type.type(:exec).new(:title => "testing", :command => "echo", :path => %w{/bin /usr/bin /usr/local/bin})
@catalog.add_resource(resource)
# Yay, I've already got a 'should' method
@catalog.resource(:exec, "echo").object_id.should == nil.object_id
end
# This test is the same as the previous, but the behaviour should be explicit.
it "should alias using the class name from the resource reference, not the resource class name" do
@catalog.add_resource @one
@catalog.alias(@one, "other")
@catalog.resource("notify", "other").should equal(@one)
end
it "should ignore conflicting aliases that point to the aliased resource" do
@catalog.alias(@one, "other")
lambda { @catalog.alias(@one, "other") }.should_not raise_error
end
it "should fail to add an alias if the aliased name already exists" do
@catalog.add_resource @one
proc { @catalog.alias @two, "one" }.should raise_error(ArgumentError)
end
it "should not fail when a resource has duplicate aliases created" do
@catalog.add_resource @one
proc { @catalog.alias @one, "one" }.should_not raise_error
end
it "should not create aliases that point back to the resource" do
@catalog.alias(@one, "one")
@catalog.resource(:notify, "one").should be_nil
end
it "should be able to look resources up by their aliases" do
@catalog.add_resource @one
@catalog.alias @one, "two"
@catalog.resource(:notify, "two").should equal(@one)
end
it "should remove resource aliases when the target resource is removed" do
@catalog.add_resource @one
@catalog.alias(@one, "other")
@one.expects :remove
@catalog.remove_resource(@one)
@catalog.resource("notify", "other").should be_nil
end
it "should add an alias for the namevar when the title and name differ on isomorphic resource types" do
resource = Puppet::Type.type(:file).new :path => @basepath+"/something", :title => "other", :content => "blah"
resource.expects(:isomorphic?).returns(true)
@catalog.add_resource(resource)
@catalog.resource(:file, "other").should equal(resource)
@catalog.resource(:file, @basepath+"/something").ref.should == resource.ref
end
it "should not add an alias for the namevar when the title and name differ on non-isomorphic resource types" do
resource = Puppet::Type.type(:file).new :path => @basepath+"/something", :title => "other", :content => "blah"
resource.expects(:isomorphic?).returns(false)
@catalog.add_resource(resource)
@catalog.resource(:file, resource.title).should equal(resource)
# We can't use .should here, because the resources respond to that method.
raise "Aliased non-isomorphic resource" if @catalog.resource(:file, resource.name)
end
it "should provide a method to create additional resources that also registers the resource" do
args = {:name => "/yay", :ensure => :file}
resource = stub 'file', :ref => "File[/yay]", :catalog= => @catalog, :title => "/yay", :[] => "/yay"
Puppet::Type.type(:file).expects(:new).with(args).returns(resource)
@catalog.create_resource :file, args
@catalog.resource("File[/yay]").should equal(resource)
end
end
describe "when applying" do
before :each do
@catalog = Puppet::Resource::Catalog.new("host")
@transaction = mock 'transaction'
Puppet::Transaction.stubs(:new).returns(@transaction)
@transaction.stubs(:evaluate)
@transaction.stubs(:add_times)
Puppet.settings.stubs(:use)
end
it "should create and evaluate a transaction" do
@transaction.expects(:evaluate)
@catalog.apply
end
it "should provide the catalog retrieval time to the transaction" do
@catalog.retrieval_duration = 5
@transaction.expects(:add_times).with(:config_retrieval => 5)
@catalog.apply
end
it "should use a retrieval time of 0 if none is set in the catalog" do
@catalog.retrieval_duration = nil
@transaction.expects(:add_times).with(:config_retrieval => 0)
@catalog.apply
end
it "should return the transaction" do
@catalog.apply.should equal(@transaction)
end
it "should yield the transaction if a block is provided" do
@catalog.apply do |trans|
trans.should equal(@transaction)
end
end
it "should default to being a host catalog" do
@catalog.host_config.should be_true
end
it "should be able to be set to a non-host_config" do
@catalog.host_config = false
@catalog.host_config.should be_false
end
it "should pass supplied tags on to the transaction" do
@transaction.expects(:tags=).with(%w{one two})
@catalog.apply(:tags => %w{one two})
end
it "should set ignoreschedules on the transaction if specified in apply()" do
@transaction.expects(:ignoreschedules=).with(true)
@catalog.apply(:ignoreschedules => true)
end
it "should expire cached data in the resources both before and after the transaction" do
@catalog.expects(:expire).times(2)
@catalog.apply
end
describe "host catalogs" do
# super() doesn't work in the setup method for some reason
before do
@catalog.host_config = true
Puppet::Util::Storage.stubs(:store)
end
it "should initialize the state database before applying a catalog" do
Puppet::Util::Storage.expects(:load)
# Short-circuit the apply, so we know we're loading before the transaction
Puppet::Transaction.expects(:new).raises ArgumentError
proc { @catalog.apply }.should raise_error(ArgumentError)
end
it "should sync the state database after applying" do
Puppet::Util::Storage.expects(:store)
@transaction.stubs :any_failed? => false
@catalog.apply
end
after { Puppet.settings.clear }
end
describe "non-host catalogs" do
before do
@catalog.host_config = false
end
it "should never send reports" do
Puppet[:report] = true
Puppet[:summarize] = true
@catalog.apply
end
it "should never modify the state database" do
Puppet::Util::Storage.expects(:load).never
Puppet::Util::Storage.expects(:store).never
@catalog.apply
end
after { Puppet.settings.clear }
end
end
describe "when creating a relationship graph" do
before do
Puppet::Type.type(:component)
@catalog = Puppet::Resource::Catalog.new("host")
@compone = Puppet::Type::Component.new :name => "one"
@comptwo = Puppet::Type::Component.new :name => "two", :require => "Class[one]"
@file = Puppet::Type.type(:file)
@one = @file.new :path => @basepath+"/one"
@two = @file.new :path => @basepath+"/two"
@sub = @file.new :path => @basepath+"/two/subdir"
@catalog.add_edge @compone, @one
@catalog.add_edge @comptwo, @two
@three = @file.new :path => @basepath+"/three"
@four = @file.new :path => @basepath+"/four", :require => "File[#{@basepath}/three]"
@five = @file.new :path => @basepath+"/five"
@catalog.add_resource @compone, @comptwo, @one, @two, @three, @four, @five, @sub
@relationships = @catalog.relationship_graph
end
it "should be able to create a relationship graph" do
@relationships.should be_instance_of(Puppet::SimpleGraph)
end
it "should not have any components" do
@relationships.vertices.find { |r| r.instance_of?(Puppet::Type::Component) }.should be_nil
end
it "should have all non-component resources from the catalog" do
# The failures print out too much info, so i just do a class comparison
@relationships.vertex?(@five).should be_true
end
it "should have all resource relationships set as edges" do
@relationships.edge?(@three, @four).should be_true
end
it "should copy component relationships to all contained resources" do
- @relationships.edge?(@one, @two).should be_true
+ @relationships.path_between(@one, @two).should be
end
it "should add automatic relationships to the relationship graph" do
@relationships.edge?(@two, @sub).should be_true
end
it "should get removed when the catalog is cleaned up" do
@relationships.expects(:clear)
@catalog.clear
@catalog.instance_variable_get("@relationship_graph").should be_nil
end
it "should write :relationships and :expanded_relationships graph files if the catalog is a host catalog" do
@catalog.clear
graph = Puppet::SimpleGraph.new
Puppet::SimpleGraph.expects(:new).returns graph
graph.expects(:write_graph).with(:relationships)
graph.expects(:write_graph).with(:expanded_relationships)
@catalog.host_config = true
@catalog.relationship_graph
end
it "should not write graph files if the catalog is not a host catalog" do
@catalog.clear
graph = Puppet::SimpleGraph.new
Puppet::SimpleGraph.expects(:new).returns graph
graph.expects(:write_graph).never
@catalog.host_config = false
@catalog.relationship_graph
end
it "should create a new relationship graph after clearing the old one" do
@relationships.expects(:clear)
@catalog.clear
@catalog.relationship_graph.should be_instance_of(Puppet::SimpleGraph)
end
it "should remove removed resources from the relationship graph if it exists" do
@catalog.remove_resource(@one)
@catalog.relationship_graph.vertex?(@one).should be_false
end
end
describe "when writing dot files" do
before do
@catalog = Puppet::Resource::Catalog.new("host")
@name = :test
@file = File.join(Puppet[:graphdir], @name.to_s + ".dot")
end
it "should only write when it is a host catalog" do
File.expects(:open).with(@file).never
@catalog.host_config = false
Puppet[:graph] = true
@catalog.write_graph(@name)
end
after do
Puppet.settings.clear
end
end
describe "when indirecting" do
before do
@real_indirection = Puppet::Resource::Catalog.indirection
@indirection = stub 'indirection', :name => :catalog
Puppet::Util::Cacher.expire
end
it "should use the value of the 'catalog_terminus' setting to determine its terminus class" do
# Puppet only checks the terminus setting the first time you ask
# so this returns the object to the clean state
# at the expense of making this test less pure
Puppet::Resource::Catalog.indirection.reset_terminus_class
Puppet.settings[:catalog_terminus] = "rest"
Puppet::Resource::Catalog.indirection.terminus_class.should == :rest
end
it "should allow the terminus class to be set manually" do
Puppet::Resource::Catalog.indirection.terminus_class = :rest
Puppet::Resource::Catalog.indirection.terminus_class.should == :rest
end
after do
Puppet::Util::Cacher.expire
@real_indirection.reset_terminus_class
end
end
describe "when converting to yaml" do
before do
@catalog = Puppet::Resource::Catalog.new("me")
@catalog.add_edge("one", "two")
end
it "should be able to be dumped to yaml" do
YAML.dump(@catalog).should be_instance_of(String)
end
end
describe "when converting from yaml" do
before do
@catalog = Puppet::Resource::Catalog.new("me")
@catalog.add_edge("one", "two")
text = YAML.dump(@catalog)
@newcatalog = YAML.load(text)
end
it "should get converted back to a catalog" do
@newcatalog.should be_instance_of(Puppet::Resource::Catalog)
end
it "should have all vertices" do
@newcatalog.vertex?("one").should be_true
@newcatalog.vertex?("two").should be_true
end
it "should have all edges" do
@newcatalog.edge?("one", "two").should be_true
end
end
end
describe Puppet::Resource::Catalog, "when converting to pson", :if => Puppet.features.pson? do
before do
@catalog = Puppet::Resource::Catalog.new("myhost")
end
def pson_output_should
@catalog.class.expects(:pson_create).with { |hash| yield hash }.returns(:something)
end
# LAK:NOTE For all of these tests, we convert back to the resource so we can
# trap the actual data structure then.
it "should set its document_type to 'Catalog'" do
pson_output_should { |hash| hash['document_type'] == "Catalog" }
PSON.parse @catalog.to_pson
end
it "should set its data as a hash" do
pson_output_should { |hash| hash['data'].is_a?(Hash) }
PSON.parse @catalog.to_pson
end
[:name, :version, :tags, :classes].each do |param|
it "should set its #{param} to the #{param} of the resource" do
@catalog.send(param.to_s + "=", "testing") unless @catalog.send(param)
pson_output_should { |hash| hash['data'][param.to_s] == @catalog.send(param) }
PSON.parse @catalog.to_pson
end
end
it "should convert its resources to a PSON-encoded array and store it as the 'resources' data" do
one = stub 'one', :to_pson_data_hash => "one_resource", :ref => "Foo[one]"
two = stub 'two', :to_pson_data_hash => "two_resource", :ref => "Foo[two]"
@catalog.add_resource(one)
@catalog.add_resource(two)
# TODO this should really guarantee sort order
PSON.parse(@catalog.to_pson,:create_additions => false)['data']['resources'].sort.should == ["one_resource", "two_resource"].sort
end
it "should convert its edges to a PSON-encoded array and store it as the 'edges' data" do
one = stub 'one', :to_pson_data_hash => "one_resource", :ref => 'Foo[one]'
two = stub 'two', :to_pson_data_hash => "two_resource", :ref => 'Foo[two]'
three = stub 'three', :to_pson_data_hash => "three_resource", :ref => 'Foo[three]'
@catalog.add_edge(one, two)
@catalog.add_edge(two, three)
@catalog.edges_between(one, two )[0].expects(:to_pson_data_hash).returns "one_two_pson"
@catalog.edges_between(two, three)[0].expects(:to_pson_data_hash).returns "two_three_pson"
PSON.parse(@catalog.to_pson,:create_additions => false)['data']['edges'].sort.should == %w{one_two_pson two_three_pson}.sort
end
end
describe Puppet::Resource::Catalog, "when converting from pson", :if => Puppet.features.pson? do
def pson_result_should
Puppet::Resource::Catalog.expects(:new).with { |hash| yield hash }
end
before do
@data = {
'name' => "myhost"
}
@pson = {
'document_type' => 'Puppet::Resource::Catalog',
'data' => @data,
'metadata' => {}
}
@catalog = Puppet::Resource::Catalog.new("myhost")
Puppet::Resource::Catalog.stubs(:new).returns @catalog
end
it "should be extended with the PSON utility module" do
Puppet::Resource::Catalog.singleton_class.ancestors.should be_include(Puppet::Util::Pson)
end
it "should create it with the provided name" do
Puppet::Resource::Catalog.expects(:new).with('myhost').returns @catalog
PSON.parse @pson.to_pson
end
it "should set the provided version on the catalog if one is set" do
@data['version'] = 50
PSON.parse @pson.to_pson
@catalog.version.should == @data['version']
end
it "should set any provided tags on the catalog" do
@data['tags'] = %w{one two}
PSON.parse @pson.to_pson
@catalog.tags.should == @data['tags']
end
it "should set any provided classes on the catalog" do
@data['classes'] = %w{one two}
PSON.parse @pson.to_pson
@catalog.classes.should == @data['classes']
end
it 'should convert the resources list into resources and add each of them' do
@data['resources'] = [Puppet::Resource.new(:file, "/foo"), Puppet::Resource.new(:file, "/bar")]
@catalog.expects(:add_resource).times(2).with { |res| res.type == "File" }
PSON.parse @pson.to_pson
end
it 'should convert resources even if they do not include "type" information' do
@data['resources'] = [Puppet::Resource.new(:file, "/foo")]
@data['resources'][0].expects(:to_pson).returns '{"title":"/foo","tags":["file"],"type":"File"}'
@catalog.expects(:add_resource).with { |res| res.type == "File" }
PSON.parse @pson.to_pson
end
it 'should convert the edges list into edges and add each of them' do
one = Puppet::Relationship.new("osource", "otarget", :event => "one", :callback => "refresh")
two = Puppet::Relationship.new("tsource", "ttarget", :event => "two", :callback => "refresh")
@data['edges'] = [one, two]
@catalog.stubs(:resource).returns("eh")
@catalog.expects(:add_edge).with { |edge| edge.event == "one" }
@catalog.expects(:add_edge).with { |edge| edge.event == "two" }
PSON.parse @pson.to_pson
end
it "should be able to convert relationships that do not include 'type' information" do
one = Puppet::Relationship.new("osource", "otarget", :event => "one", :callback => "refresh")
one.expects(:to_pson).returns "{\"event\":\"one\",\"callback\":\"refresh\",\"source\":\"osource\",\"target\":\"otarget\"}"
@data['edges'] = [one]
@catalog.stubs(:resource).returns("eh")
@catalog.expects(:add_edge).with { |edge| edge.event == "one" }
PSON.parse @pson.to_pson
end
it "should set the source and target for each edge to the actual resource" do
edge = Puppet::Relationship.new("source", "target")
@data['edges'] = [edge]
@catalog.expects(:resource).with("source").returns("source_resource")
@catalog.expects(:resource).with("target").returns("target_resource")
@catalog.expects(:add_edge).with { |edge| edge.source == "source_resource" and edge.target == "target_resource" }
PSON.parse @pson.to_pson
end
it "should fail if the source resource cannot be found" do
edge = Puppet::Relationship.new("source", "target")
@data['edges'] = [edge]
@catalog.expects(:resource).with("source").returns(nil)
@catalog.stubs(:resource).with("target").returns("target_resource")
lambda { PSON.parse @pson.to_pson }.should raise_error(ArgumentError)
end
it "should fail if the target resource cannot be found" do
edge = Puppet::Relationship.new("source", "target")
@data['edges'] = [edge]
@catalog.stubs(:resource).with("source").returns("source_resource")
@catalog.expects(:resource).with("target").returns(nil)
lambda { PSON.parse @pson.to_pson }.should raise_error(ArgumentError)
end
describe "#title_key_for_ref" do
it "should parse a resource ref string into a pair" do
@catalog.title_key_for_ref("Title[name]").should == ["Title", "name"]
end
it "should parse a resource ref string into a pair, even if there's a newline inside the name" do
@catalog.title_key_for_ref("Title[na\nme]").should == ["Title", "na\nme"]
end
end
end
diff --git a/spec/unit/simple_graph_spec.rb b/spec/unit/simple_graph_spec.rb
index c106f550b..99db2a55c 100755
--- a/spec/unit/simple_graph_spec.rb
+++ b/spec/unit/simple_graph_spec.rb
@@ -1,865 +1,904 @@
#!/usr/bin/env ruby
#
# Created by Luke Kanies on 2007-11-1.
# Copyright (c) 2006. All rights reserved.
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
require 'puppet/simple_graph'
describe Puppet::SimpleGraph do
it "should return the number of its vertices as its length" do
@graph = Puppet::SimpleGraph.new
@graph.add_vertex("one")
@graph.add_vertex("two")
@graph.size.should == 2
end
it "should consider itself a directed graph" do
Puppet::SimpleGraph.new.directed?.should be_true
end
it "should provide a method for reversing the graph" do
@graph = Puppet::SimpleGraph.new
@graph.add_edge(:one, :two)
@graph.reversal.edge?(:two, :one).should be_true
end
it "should be able to produce a dot graph" do
@graph = Puppet::SimpleGraph.new
@graph.add_edge(:one, :two)
proc { @graph.to_dot_graph }.should_not raise_error
end
describe "when managing vertices" do
before do
@graph = Puppet::SimpleGraph.new
end
it "should provide a method to add a vertex" do
@graph.add_vertex(:test)
@graph.vertex?(:test).should be_true
end
it "should reset its reversed graph when vertices are added" do
rev = @graph.reversal
@graph.add_vertex(:test)
@graph.reversal.should_not equal(rev)
end
it "should ignore already-present vertices when asked to add a vertex" do
@graph.add_vertex(:test)
proc { @graph.add_vertex(:test) }.should_not raise_error
end
it "should return true when asked if a vertex is present" do
@graph.add_vertex(:test)
@graph.vertex?(:test).should be_true
end
it "should return false when asked if a non-vertex is present" do
@graph.vertex?(:test).should be_false
end
it "should return all set vertices when asked" do
@graph.add_vertex(:one)
@graph.add_vertex(:two)
@graph.vertices.length.should == 2
@graph.vertices.should include(:one)
@graph.vertices.should include(:two)
end
it "should remove a given vertex when asked" do
@graph.add_vertex(:one)
@graph.remove_vertex!(:one)
@graph.vertex?(:one).should be_false
end
it "should do nothing when a non-vertex is asked to be removed" do
proc { @graph.remove_vertex!(:one) }.should_not raise_error
end
end
describe "when managing edges" do
before do
@graph = Puppet::SimpleGraph.new
end
it "should provide a method to test whether a given vertex pair is an edge" do
@graph.should respond_to(:edge?)
end
it "should reset its reversed graph when edges are added" do
rev = @graph.reversal
@graph.add_edge(:one, :two)
@graph.reversal.should_not equal(rev)
end
it "should provide a method to add an edge as an instance of the edge class" do
edge = Puppet::Relationship.new(:one, :two)
@graph.add_edge(edge)
@graph.edge?(:one, :two).should be_true
end
it "should provide a method to add an edge by specifying the two vertices" do
@graph.add_edge(:one, :two)
@graph.edge?(:one, :two).should be_true
end
it "should provide a method to add an edge by specifying the two vertices and a label" do
@graph.add_edge(:one, :two, :callback => :awesome)
@graph.edge?(:one, :two).should be_true
end
describe "when retrieving edges between two nodes" do
it "should handle the case of nodes not in the graph" do
@graph.edges_between(:one, :two).should == []
end
it "should handle the case of nodes with no edges between them" do
@graph.add_vertex(:one)
@graph.add_vertex(:two)
@graph.edges_between(:one, :two).should == []
end
it "should handle the case of nodes connected by a single edge" do
edge = Puppet::Relationship.new(:one, :two)
@graph.add_edge(edge)
@graph.edges_between(:one, :two).length.should == 1
@graph.edges_between(:one, :two)[0].should equal(edge)
end
it "should handle the case of nodes connected by multiple edges" do
edge1 = Puppet::Relationship.new(:one, :two, :callback => :foo)
edge2 = Puppet::Relationship.new(:one, :two, :callback => :bar)
@graph.add_edge(edge1)
@graph.add_edge(edge2)
Set.new(@graph.edges_between(:one, :two)).should == Set.new([edge1, edge2])
end
end
it "should add the edge source as a vertex if it is not already" do
edge = Puppet::Relationship.new(:one, :two)
@graph.add_edge(edge)
@graph.vertex?(:one).should be_true
end
it "should add the edge target as a vertex if it is not already" do
edge = Puppet::Relationship.new(:one, :two)
@graph.add_edge(edge)
@graph.vertex?(:two).should be_true
end
it "should return all edges as edge instances when asked" do
one = Puppet::Relationship.new(:one, :two)
two = Puppet::Relationship.new(:two, :three)
@graph.add_edge(one)
@graph.add_edge(two)
edges = @graph.edges
edges.should be_instance_of(Array)
edges.length.should == 2
edges.should include(one)
edges.should include(two)
end
it "should remove an edge when asked" do
edge = Puppet::Relationship.new(:one, :two)
@graph.add_edge(edge)
@graph.remove_edge!(edge)
@graph.edge?(edge.source, edge.target).should be_false
end
it "should remove all related edges when a vertex is removed" do
one = Puppet::Relationship.new(:one, :two)
two = Puppet::Relationship.new(:two, :three)
@graph.add_edge(one)
@graph.add_edge(two)
@graph.remove_vertex!(:two)
@graph.edge?(:one, :two).should be_false
@graph.edge?(:two, :three).should be_false
@graph.edges.length.should == 0
end
end
describe "when finding adjacent vertices" do
before do
@graph = Puppet::SimpleGraph.new
@one_two = Puppet::Relationship.new(:one, :two)
@two_three = Puppet::Relationship.new(:two, :three)
@one_three = Puppet::Relationship.new(:one, :three)
@graph.add_edge(@one_two)
@graph.add_edge(@one_three)
@graph.add_edge(@two_three)
end
it "should return adjacent vertices" do
adj = @graph.adjacent(:one)
adj.should be_include(:three)
adj.should be_include(:two)
end
it "should default to finding :out vertices" do
@graph.adjacent(:two).should == [:three]
end
it "should support selecting :in vertices" do
@graph.adjacent(:two, :direction => :in).should == [:one]
end
it "should default to returning the matching vertices as an array of vertices" do
@graph.adjacent(:two).should == [:three]
end
it "should support returning an array of matching edges" do
@graph.adjacent(:two, :type => :edges).should == [@two_three]
end
# Bug #2111
it "should not consider a vertex adjacent just because it was asked about previously" do
@graph = Puppet::SimpleGraph.new
@graph.add_vertex("a")
@graph.add_vertex("b")
@graph.edge?("a", "b")
@graph.adjacent("a").should == []
end
end
describe "when clearing" do
before do
@graph = Puppet::SimpleGraph.new
one = Puppet::Relationship.new(:one, :two)
two = Puppet::Relationship.new(:two, :three)
@graph.add_edge(one)
@graph.add_edge(two)
@graph.clear
end
it "should remove all vertices" do
@graph.vertices.should be_empty
end
it "should remove all edges" do
@graph.edges.should be_empty
end
end
describe "when reversing graphs" do
before do
@graph = Puppet::SimpleGraph.new
end
it "should provide a method for reversing the graph" do
@graph.add_edge(:one, :two)
@graph.reversal.edge?(:two, :one).should be_true
end
it "should add all vertices to the reversed graph" do
@graph.add_edge(:one, :two)
@graph.vertex?(:one).should be_true
@graph.vertex?(:two).should be_true
end
it "should retain labels on edges" do
@graph.add_edge(:one, :two, :callback => :awesome)
edge = @graph.reversal.edges_between(:two, :one)[0]
edge.label.should == {:callback => :awesome}
end
end
- describe "when sorting the graph" do
+ describe "when reporting cycles in the graph" do
before do
@graph = Puppet::SimpleGraph.new
end
def add_edges(hash)
hash.each do |a,b|
@graph.add_edge(a, b)
end
end
- it "should sort the graph topologically" do
- add_edges :a => :b, :b => :c
- @graph.topsort.should == [:a, :b, :c]
- end
-
it "should fail on two-vertex loops" do
add_edges :a => :b, :b => :a
- proc { @graph.topsort }.should raise_error(Puppet::Error)
+ proc { @graph.report_cycles_in_graph }.should raise_error(Puppet::Error)
end
it "should fail on multi-vertex loops" do
add_edges :a => :b, :b => :c, :c => :a
- proc { @graph.topsort }.should raise_error(Puppet::Error)
+ proc { @graph.report_cycles_in_graph }.should raise_error(Puppet::Error)
end
it "should fail when a larger tree contains a small cycle" do
add_edges :a => :b, :b => :a, :c => :a, :d => :c
- proc { @graph.topsort }.should raise_error(Puppet::Error)
+ proc { @graph.report_cycles_in_graph }.should raise_error(Puppet::Error)
end
it "should succeed on trees with no cycles" do
add_edges :a => :b, :b => :e, :c => :a, :d => :c
- proc { @graph.topsort }.should_not raise_error
+ proc { @graph.report_cycles_in_graph }.should_not raise_error
end
it "should produce the correct relationship text" do
add_edges :a => :b, :b => :a
# cycle detection starts from a or b randomly
# so we need to check for either ordering in the error message
want = %r{Found 1 dependency cycle:\n\((a => b => a|b => a => b)\)\nTry}
- expect { @graph.topsort }.to raise_error(Puppet::Error, want)
+ expect { @graph.report_cycles_in_graph }.to raise_error(Puppet::Error, want)
end
it "cycle discovery should be the minimum cycle for a simple graph" do
add_edges "a" => "b"
add_edges "b" => "a"
add_edges "b" => "c"
cycles = nil
expect { cycles = @graph.find_cycles_in_graph.sort }.should_not raise_error
cycles.should be == [["a", "b"]]
end
it "cycle discovery should handle two distinct cycles" do
add_edges "a" => "a1", "a1" => "a"
add_edges "b" => "b1", "b1" => "b"
cycles = nil
expect { cycles = @graph.find_cycles_in_graph.sort }.should_not raise_error
cycles.should be == [["a", "a1"], ["b", "b1"]]
end
it "cycle discovery should handle two cycles in a connected graph" do
add_edges "a" => "b", "b" => "c", "c" => "d"
add_edges "a" => "a1", "a1" => "a"
add_edges "c" => "c1", "c1" => "c2", "c2" => "c3", "c3" => "c"
cycles = nil
expect { cycles = @graph.find_cycles_in_graph.sort }.should_not raise_error
cycles.should be == [%w{a a1}, %w{c c1 c2 c3}]
end
it "cycle discovery should handle a complicated cycle" do
add_edges "a" => "b", "b" => "c"
add_edges "a" => "c"
add_edges "c" => "c1", "c1" => "a"
add_edges "c" => "c2", "c2" => "b"
cycles = nil
expect { cycles = @graph.find_cycles_in_graph.sort }.should_not raise_error
cycles.should be == [%w{a b c c1 c2}]
end
it "cycle discovery should not fail with large data sets" do
limit = 3000
(1..(limit - 1)).each do |n| add_edges n.to_s => (n+1).to_s end
cycles = nil
expect { cycles = @graph.find_cycles_in_graph.sort }.should_not raise_error
cycles.should be == []
end
it "path finding should work with a simple cycle" do
add_edges "a" => "b", "b" => "c", "c" => "a"
cycles = @graph.find_cycles_in_graph.sort
paths = @graph.paths_in_cycle(cycles.first, 100)
paths.should be == [%w{a b c a}]
end
it "path finding should work with two independent cycles" do
add_edges "a" => "b1"
add_edges "a" => "b2"
add_edges "b1" => "a", "b2" => "a"
cycles = @graph.find_cycles_in_graph.sort
cycles.length.should be == 1
paths = @graph.paths_in_cycle(cycles.first, 100)
paths.sort.should be == [%w{a b1 a}, %w{a b2 a}]
end
it "path finding should prefer shorter paths in cycles" do
add_edges "a" => "b", "b" => "c", "c" => "a"
add_edges "b" => "a"
cycles = @graph.find_cycles_in_graph.sort
cycles.length.should be == 1
paths = @graph.paths_in_cycle(cycles.first, 100)
paths.should be == [%w{a b a}, %w{a b c a}]
end
it "path finding should respect the max_path value" do
(1..20).each do |n| add_edges "a" => "b#{n}", "b#{n}" => "a" end
cycles = @graph.find_cycles_in_graph.sort
cycles.length.should be == 1
(1..20).each do |n|
paths = @graph.paths_in_cycle(cycles.first, n)
paths.length.should be == n
end
paths = @graph.paths_in_cycle(cycles.first, 21)
paths.length.should be == 20
end
-
- # Our graph's add_edge method is smart enough not to add
- # duplicate edges, so we use the objects, which it doesn't
- # check.
- it "should be able to sort graphs with duplicate edges" do
- one = Puppet::Relationship.new(:a, :b)
- @graph.add_edge(one)
- two = Puppet::Relationship.new(:a, :b)
- @graph.add_edge(two)
- proc { @graph.topsort }.should_not raise_error
- end
end
describe "when writing dot files" do
before do
@graph = Puppet::SimpleGraph.new
@name = :test
@file = File.join(Puppet[:graphdir], @name.to_s + ".dot")
end
it "should only write when graphing is enabled" do
File.expects(:open).with(@file).never
Puppet[:graph] = false
@graph.write_graph(@name)
end
it "should write a dot file based on the passed name" do
File.expects(:open).with(@file, "w").yields(stub("file", :puts => nil))
@graph.expects(:to_dot).with("name" => @name.to_s.capitalize)
Puppet[:graph] = true
@graph.write_graph(@name)
end
after do
Puppet.settings.clear
end
end
describe Puppet::SimpleGraph do
before do
@graph = Puppet::SimpleGraph.new
end
it "should correctly clear vertices and edges when asked" do
@graph.add_edge("a", "b")
@graph.add_vertex "c"
@graph.clear
@graph.vertices.should be_empty
@graph.edges.should be_empty
end
end
describe "when matching edges" do
before do
@graph = Puppet::SimpleGraph.new
@event = Puppet::Transaction::Event.new(:name => :yay, :resource => "a")
@none = Puppet::Transaction::Event.new(:name => :NONE, :resource => "a")
@edges = {}
@edges["a/b"] = Puppet::Relationship.new("a", "b", {:event => :yay, :callback => :refresh})
@edges["a/c"] = Puppet::Relationship.new("a", "c", {:event => :yay, :callback => :refresh})
@graph.add_edge(@edges["a/b"])
end
it "should match edges whose source matches the source of the event" do
@graph.matching_edges(@event).should == [@edges["a/b"]]
end
it "should match always match nothing when the event is :NONE" do
@graph.matching_edges(@none).should be_empty
end
it "should match multiple edges" do
@graph.add_edge(@edges["a/c"])
edges = @graph.matching_edges(@event)
edges.should be_include(@edges["a/b"])
edges.should be_include(@edges["a/c"])
end
end
describe "when determining dependencies" do
before do
@graph = Puppet::SimpleGraph.new
@graph.add_edge("a", "b")
@graph.add_edge("a", "c")
@graph.add_edge("b", "d")
end
it "should find all dependents when they are on multiple levels" do
@graph.dependents("a").sort.should == %w{b c d}.sort
end
it "should find single dependents" do
@graph.dependents("b").sort.should == %w{d}.sort
end
it "should return an empty array when there are no dependents" do
@graph.dependents("c").sort.should == [].sort
end
it "should find all dependencies when they are on multiple levels" do
@graph.dependencies("d").sort.should == %w{a b}
end
it "should find single dependencies" do
@graph.dependencies("c").sort.should == %w{a}
end
it "should return an empty array when there are no dependencies" do
@graph.dependencies("a").sort.should == []
end
end
require 'puppet/util/graph'
- class Container
+ class Container < Puppet::Type::Component
include Puppet::Util::Graph
include Enumerable
attr_accessor :name
def each
@children.each do |c| yield c end
end
def initialize(name, ary)
@name = name
@children = ary
end
def push(*ary)
ary.each { |c| @children.push(c)}
end
def to_s
@name
end
end
+ require "puppet/resource/catalog"
describe "when splicing the graph" do
def container_graph
@one = Container.new("one", %w{a b})
@two = Container.new("two", ["c", "d"])
@three = Container.new("three", ["i", "j"])
@middle = Container.new("middle", ["e", "f", @two])
@top = Container.new("top", ["g", "h", @middle, @one, @three])
@empty = Container.new("empty", [])
@whit = Puppet::Type.type(:whit)
@stage = Puppet::Type.type(:stage).new(:name => "foo")
- @contgraph = @top.to_graph
+ @contgraph = @top.to_graph(Puppet::Resource::Catalog.new)
# We have to add the container to the main graph, else it won't
# be spliced in the dependency graph.
@contgraph.add_vertex(@empty)
end
+ def containers
+ @contgraph.vertices.select { |x| !x.is_a? String }
+ end
+
+ def contents_of(x)
+ @contgraph.direct_dependents_of(x)
+ end
+
def dependency_graph
@depgraph = Puppet::SimpleGraph.new
@contgraph.vertices.each do |v|
@depgraph.add_vertex(v)
end
# We have to specify a relationship to our empty container, else it
# never makes it into the dep graph in the first place.
- {@one => @two, "f" => "c", "h" => @middle, "c" => @empty}.each do |source, target|
+ @explicit_dependencies = {@one => @two, "f" => "c", "h" => @middle, "c" => @empty}
+ @explicit_dependencies.each do |source, target|
@depgraph.add_edge(source, target, :callback => :refresh)
end
end
def splice
- @depgraph.splice!(@contgraph, Container)
+ @contgraph.splice!(@depgraph)
+ end
+
+ def whit_called(name)
+ x = @depgraph.vertices.find { |v| v.is_a?(@whit) && v.name =~ /#{name}/ }
+ x.should_not be_nil
+ def x.to_s
+ "Whit[#{name}]"
+ end
+ def x.inspect
+ to_s
+ end
+ x
+ end
+
+ def admissible_sentinal_of(x)
+ @depgraph.vertex?(x) ? x : whit_called("admissible_#{x.name}")
+ end
+
+ def completed_sentinal_of(x)
+ @depgraph.vertex?(x) ? x : whit_called("completed_#{x.name}")
end
before do
container_graph
dependency_graph
splice
end
- # This is the real heart of splicing -- replacing all containers in
- # our relationship and exploding their relationships so that each
- # relationship to a container gets copied to all of its children.
+ # This is the real heart of splicing -- replacing all containers X in our
+ # relationship graph with a pair of whits { admissible_X and completed_X }
+ # such that that
+ #
+ # 0) completed_X depends on admissible_X
+ # 1) contents of X each depend on admissible_X
+ # 2) completed_X depends on each on the contents of X
+ # 3) everything which depended on X depends on completed_X
+ # 4) admissible_X depends on everything X depended on
+ # 5) the containers and their edges must be removed
+ #
+ # Note that this requires attention to the possible case of containers
+ # which contain or depend on other containers.
+ #
+ # Point by point:
+
+ # 0) completed_X depends on admissible_X
+ #
+ it "every container's completed sentinal should depend on its admissible sentinal" do
+ containers.each { |container|
+ @depgraph.path_between(admissible_sentinal_of(container),completed_sentinal_of(container)).should be
+ }
+ end
+
+ # 1) contents of X each depend on admissible_X
+ #
+ it "all contained objects should depend on their container's admissible sentinal" do
+ containers.each { |container|
+ contents_of(container).each { |leaf|
+ @depgraph.should be_edge(admissible_sentinal_of(container),admissible_sentinal_of(leaf))
+ }
+ }
+ end
+
+ # 2) completed_X depends on each on the contents of X
+ #
+ it "completed sentinals should depend on their container's contents" do
+ containers.each { |container|
+ contents_of(container).each { |leaf|
+ @depgraph.should be_edge(completed_sentinal_of(leaf),completed_sentinal_of(container))
+ }
+ }
+ end
+
+ #
+ # 3) everything which depended on X depends on completed_X
+
+ #
+ # 4) admissible_X depends on everything X depended on
+
+ # 5) the containers and their edges must be removed
+ #
it "should remove all Container objects from the dependency graph" do
@depgraph.vertices.find_all { |v| v.is_a?(Container) }.should be_empty
end
- # This is a bit hideous, but required to make stages work with relationships - they're
- # the top of the graph.
it "should remove all Stage resources from the dependency graph" do
@depgraph.vertices.find_all { |v| v.is_a?(Puppet::Type.type(:stage)) }.should be_empty
end
- it "should add container relationships to contained objects" do
- @contgraph.leaves(@middle).each do |leaf|
- @depgraph.should be_edge("h", leaf)
- end
- end
-
- it "should explode container-to-container relationships, making edges between all respective contained objects" do
- @one.each do |oobj|
- @two.each do |tobj|
- @depgraph.should be_edge(oobj, tobj)
- end
- end
- end
-
- it "should contain a whit-resource to mark the place held by the empty container" do
- @depgraph.vertices.find_all { |v| v.is_a?(@whit) }.length.should == 1
- end
-
- it "should replace edges to empty containers with edges to their residual whit" do
- emptys_whit = @depgraph.vertices.find_all { |v| v.is_a?(@whit) }.first
- @depgraph.should be_edge("c", emptys_whit)
- end
-
it "should no longer contain anything but the non-container objects" do
@depgraph.vertices.find_all { |v| ! v.is_a?(String) and ! v.is_a?(@whit)}.should be_empty
end
- it "should copy labels" do
- @depgraph.edges.each do |edge|
- edge.label.should == {:callback => :refresh}
- end
+ it "should retain labels on non-containment edges" do
+ @explicit_dependencies.each { |f,t|
+ @depgraph.edges_between(completed_sentinal_of(f),admissible_sentinal_of(t))[0].label.should == {:callback => :refresh}
+ }
end
it "should not add labels to edges that have none" do
@depgraph.add_edge(@two, @three)
splice
- @depgraph.edges_between("c", "i")[0].label.should == {}
+ @depgraph.path_between("c", "i").any? {|segment| segment.all? {|e| e.label == {} }}.should be
end
it "should copy labels over edges that have none" do
@depgraph.add_edge("c", @three, {:callback => :refresh})
splice
# And make sure the label got copied.
- @depgraph.edges_between("c", "i")[0].label.should == {:callback => :refresh}
+ @depgraph.path_between("c", "i").flatten.select {|e| e.label == {:callback => :refresh} }.should_not be_empty
end
it "should not replace a label with a nil label" do
# Lastly, add some new label-less edges and make sure the label stays.
@depgraph.add_edge(@middle, @three)
@depgraph.add_edge("c", @three, {:callback => :refresh})
splice
- @depgraph.edges_between("c", "i")[0].label.should == {:callback => :refresh}
+ @depgraph.path_between("c","i").flatten.select {|e| e.label == {:callback => :refresh} }.should_not be_empty
end
it "should copy labels to all created edges" do
@depgraph.add_edge(@middle, @three)
@depgraph.add_edge("c", @three, {:callback => :refresh})
splice
@three.each do |child|
edge = Puppet::Relationship.new("c", child)
- @depgraph.should be_edge(edge.source, edge.target)
- @depgraph.edges_between(edge.source, edge.target)[0].label.should == {:callback => :refresh}
+ (path = @depgraph.path_between(edge.source, edge.target)).should be
+ path.should_not be_empty
+ path.flatten.select {|e| e.label == {:callback => :refresh} }.should_not be_empty
end
end
end
it "should serialize to YAML using the old format by default" do
Puppet::SimpleGraph.use_new_yaml_format.should == false
end
describe "(yaml tests)" do
def empty_graph(graph)
end
def one_vertex_graph(graph)
graph.add_vertex(:a)
end
def graph_without_edges(graph)
[:a, :b, :c].each { |x| graph.add_vertex(x) }
end
def one_edge_graph(graph)
graph.add_edge(:a, :b)
end
def many_edge_graph(graph)
graph.add_edge(:a, :b)
graph.add_edge(:a, :c)
graph.add_edge(:b, :d)
graph.add_edge(:c, :d)
end
def labeled_edge_graph(graph)
graph.add_edge(:a, :b, :callback => :foo, :event => :bar)
end
def overlapping_edge_graph(graph)
graph.add_edge(:a, :b, :callback => :foo, :event => :bar)
graph.add_edge(:a, :b, :callback => :biz, :event => :baz)
end
def self.all_test_graphs
[:empty_graph, :one_vertex_graph, :graph_without_edges, :one_edge_graph, :many_edge_graph, :labeled_edge_graph,
:overlapping_edge_graph]
end
def object_ids(enumerable)
# Return a sorted list of the object id's of the elements of an
# enumerable.
enumerable.collect { |x| x.object_id }.sort
end
def graph_to_yaml(graph, which_format)
previous_use_new_yaml_format = Puppet::SimpleGraph.use_new_yaml_format
Puppet::SimpleGraph.use_new_yaml_format = (which_format == :new)
ZAML.dump(graph)
ensure
Puppet::SimpleGraph.use_new_yaml_format = previous_use_new_yaml_format
end
# Test serialization of graph to YAML.
[:old, :new].each do |which_format|
all_test_graphs.each do |graph_to_test|
it "should be able to serialize #{graph_to_test} to YAML (#{which_format} format)" do
graph = Puppet::SimpleGraph.new
send(graph_to_test, graph)
yaml_form = graph_to_yaml(graph, which_format)
# Hack the YAML so that objects in the Puppet namespace get
# changed to YAML::DomainType objects. This lets us inspect
# the serialized objects easily without invoking any
# yaml_initialize hooks.
yaml_form.gsub!('!ruby/object:Puppet::', '!hack/object:Puppet::')
serialized_object = YAML.load(yaml_form)
# Check that the object contains instance variables @edges and
# @vertices only. @reversal is also permitted, but we don't
# check it, because it is going to be phased out.
serialized_object.type_id.should == 'object:Puppet::SimpleGraph'
serialized_object.value.keys.reject { |x| x == 'reversal' }.sort.should == ['edges', 'vertices']
# Check edges by forming a set of tuples (source, target,
# callback, event) based on the graph and the YAML and make sure
# they match.
edges = serialized_object.value['edges']
edges.should be_a(Array)
expected_edge_tuples = graph.edges.collect { |edge| [edge.source, edge.target, edge.callback, edge.event] }
actual_edge_tuples = edges.collect do |edge|
edge.type_id.should == 'object:Puppet::Relationship'
%w{source target}.each { |x| edge.value.keys.should include(x) }
edge.value.keys.each { |x| ['source', 'target', 'callback', 'event'].should include(x) }
%w{source target callback event}.collect { |x| edge.value[x] }
end
Set.new(actual_edge_tuples).should == Set.new(expected_edge_tuples)
actual_edge_tuples.length.should == expected_edge_tuples.length
# Check vertices one by one.
vertices = serialized_object.value['vertices']
if which_format == :old
vertices.should be_a(Hash)
Set.new(vertices.keys).should == Set.new(graph.vertices)
vertices.each do |key, value|
value.type_id.should == 'object:Puppet::SimpleGraph::VertexWrapper'
value.value.keys.sort.should == %w{adjacencies vertex}
value.value['vertex'].should equal(key)
adjacencies = value.value['adjacencies']
adjacencies.should be_a(Hash)
Set.new(adjacencies.keys).should == Set.new([:in, :out])
[:in, :out].each do |direction|
adjacencies[direction].should be_a(Hash)
expected_adjacent_vertices = Set.new(graph.adjacent(key, :direction => direction, :type => :vertices))
Set.new(adjacencies[direction].keys).should == expected_adjacent_vertices
adjacencies[direction].each do |adj_key, adj_value|
# Since we already checked edges, just check consistency
# with edges.
desired_source = direction == :in ? adj_key : key
desired_target = direction == :in ? key : adj_key
expected_edges = edges.select do |edge|
edge.value['source'] == desired_source && edge.value['target'] == desired_target
end
adj_value.should be_a(Set)
if object_ids(adj_value) != object_ids(expected_edges)
raise "For vertex #{key.inspect}, direction #{direction.inspect}: expected adjacencies #{expected_edges.inspect} but got #{adj_value.inspect}"
end
end
end
end
else
vertices.should be_a(Array)
Set.new(vertices).should == Set.new(graph.vertices)
vertices.length.should == graph.vertices.length
end
end
end
# Test deserialization of graph from YAML. This presumes the
# correctness of serialization to YAML, which has already been
# tested.
all_test_graphs.each do |graph_to_test|
it "should be able to deserialize #{graph_to_test} from YAML (#{which_format} format)" do
reference_graph = Puppet::SimpleGraph.new
send(graph_to_test, reference_graph)
yaml_form = graph_to_yaml(reference_graph, which_format)
recovered_graph = YAML.load(yaml_form)
# Test that the recovered vertices match the vertices in the
# reference graph.
expected_vertices = reference_graph.vertices.to_a
recovered_vertices = recovered_graph.vertices.to_a
Set.new(recovered_vertices).should == Set.new(expected_vertices)
recovered_vertices.length.should == expected_vertices.length
# Test that the recovered edges match the edges in the
# reference graph.
expected_edge_tuples = reference_graph.edges.collect do |edge|
[edge.source, edge.target, edge.callback, edge.event]
end
recovered_edge_tuples = recovered_graph.edges.collect do |edge|
[edge.source, edge.target, edge.callback, edge.event]
end
Set.new(recovered_edge_tuples).should == Set.new(expected_edge_tuples)
recovered_edge_tuples.length.should == expected_edge_tuples.length
# We ought to test that the recovered graph is self-consistent
# too. But we're not going to bother with that yet because
# the internal representation of the graph is about to change.
end
end
it "should be able to serialize a graph where the vertices contain backreferences to the graph (#{which_format} format)" do
reference_graph = Puppet::SimpleGraph.new
vertex = Object.new
vertex.instance_eval { @graph = reference_graph }
reference_graph.add_edge(vertex, :other_vertex)
yaml_form = graph_to_yaml(reference_graph, which_format)
recovered_graph = YAML.load(yaml_form)
recovered_graph.vertices.length.should == 2
recovered_vertex = recovered_graph.vertices.reject { |x| x.is_a?(Symbol) }[0]
recovered_vertex.instance_eval { @graph }.should equal(recovered_graph)
recovered_graph.edges.length.should == 1
recovered_edge = recovered_graph.edges[0]
recovered_edge.source.should equal(recovered_vertex)
recovered_edge.target.should == :other_vertex
end
end
it "should serialize properly when used as a base class" do
class Puppet::TestDerivedClass < Puppet::SimpleGraph
attr_accessor :foo
end
derived = Puppet::TestDerivedClass.new
derived.add_edge(:a, :b)
derived.foo = 1234
recovered_derived = YAML.load(YAML.dump(derived))
recovered_derived.class.should equal(Puppet::TestDerivedClass)
recovered_derived.edges.length.should == 1
recovered_derived.edges[0].source.should == :a
recovered_derived.edges[0].target.should == :b
recovered_derived.vertices.length.should == 2
recovered_derived.foo.should == 1234
end
end
end
diff --git a/spec/unit/transaction_spec.rb b/spec/unit/transaction_spec.rb
index 3677bfd87..ab76130e3 100755
--- a/spec/unit/transaction_spec.rb
+++ b/spec/unit/transaction_spec.rb
@@ -1,447 +1,441 @@
#!/usr/bin/env ruby
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
require 'puppet/transaction'
def without_warnings
flag = $VERBOSE
$VERBOSE = nil
yield
$VERBOSE = flag
end
describe Puppet::Transaction do
before do
@basepath = Puppet.features.posix? ? "/what/ever" : "C:/tmp"
@transaction = Puppet::Transaction.new(Puppet::Resource::Catalog.new)
end
it "should delegate its event list to the event manager" do
@transaction = Puppet::Transaction.new(Puppet::Resource::Catalog.new)
@transaction.event_manager.expects(:events).returns %w{my events}
@transaction.events.should == %w{my events}
end
it "should delegate adding times to its report" do
@transaction = Puppet::Transaction.new(Puppet::Resource::Catalog.new)
@transaction.report.expects(:add_times).with(:foo, 10)
@transaction.report.expects(:add_times).with(:bar, 20)
@transaction.add_times :foo => 10, :bar => 20
end
it "should be able to accept resource status instances" do
resource = Puppet::Type.type(:notify).new :title => "foobar"
status = Puppet::Resource::Status.new(resource)
@transaction.add_resource_status(status)
@transaction.resource_status(resource).should equal(status)
end
it "should be able to look resource status up by resource reference" do
resource = Puppet::Type.type(:notify).new :title => "foobar"
status = Puppet::Resource::Status.new(resource)
@transaction.add_resource_status(status)
@transaction.resource_status(resource.to_s).should equal(status)
end
# This will basically only ever be used during testing.
it "should automatically create resource statuses if asked for a non-existent status" do
resource = Puppet::Type.type(:notify).new :title => "foobar"
@transaction.resource_status(resource).should be_instance_of(Puppet::Resource::Status)
end
it "should add provided resource statuses to its report" do
resource = Puppet::Type.type(:notify).new :title => "foobar"
status = Puppet::Resource::Status.new(resource)
@transaction.add_resource_status(status)
@transaction.report.resource_statuses[resource.to_s].should equal(status)
end
it "should consider a resource to be failed if a status instance exists for that resource and indicates it is failed" do
resource = Puppet::Type.type(:notify).new :name => "yayness"
status = Puppet::Resource::Status.new(resource)
status.failed = "some message"
@transaction.add_resource_status(status)
@transaction.should be_failed(resource)
end
it "should not consider a resource to be failed if a status instance exists for that resource but indicates it is not failed" do
resource = Puppet::Type.type(:notify).new :name => "yayness"
status = Puppet::Resource::Status.new(resource)
@transaction.add_resource_status(status)
@transaction.should_not be_failed(resource)
end
it "should consider there to be failed resources if any statuses are marked failed" do
resource = Puppet::Type.type(:notify).new :name => "yayness"
status = Puppet::Resource::Status.new(resource)
status.failed = "some message"
@transaction.add_resource_status(status)
@transaction.should be_any_failed
end
it "should not consider there to be failed resources if no statuses are marked failed" do
resource = Puppet::Type.type(:notify).new :name => "yayness"
status = Puppet::Resource::Status.new(resource)
@transaction.add_resource_status(status)
@transaction.should_not be_any_failed
end
it "should be possible to replace the report object" do
report = Puppet::Transaction::Report.new("apply")
@transaction.report = report
@transaction.report.should == report
end
it "should consider a resource to have failed dependencies if any of its dependencies are failed"
describe "when initializing" do
it "should create an event manager" do
@transaction = Puppet::Transaction.new(Puppet::Resource::Catalog.new)
@transaction.event_manager.should be_instance_of(Puppet::Transaction::EventManager)
@transaction.event_manager.transaction.should equal(@transaction)
end
it "should create a resource harness" do
@transaction = Puppet::Transaction.new(Puppet::Resource::Catalog.new)
@transaction.resource_harness.should be_instance_of(Puppet::Transaction::ResourceHarness)
@transaction.resource_harness.transaction.should equal(@transaction)
end
end
describe "when evaluating a resource" do
before do
@transaction = Puppet::Transaction.new(Puppet::Resource::Catalog.new)
- @transaction.stubs(:eval_children_and_apply_resource)
@transaction.stubs(:skip?).returns false
@resource = Puppet::Type.type(:file).new :path => @basepath
end
it "should check whether the resource should be skipped" do
@transaction.expects(:skip?).with(@resource).returns false
@transaction.eval_resource(@resource)
end
- it "should eval and apply children" do
- @transaction.expects(:eval_children_and_apply_resource).with(@resource, nil)
-
- @transaction.eval_resource(@resource)
- end
-
it "should process events" do
@transaction.event_manager.expects(:process_events).with(@resource)
@transaction.eval_resource(@resource)
end
describe "and the resource should be skipped" do
before do
@transaction.expects(:skip?).with(@resource).returns true
end
it "should mark the resource's status as skipped" do
@transaction.eval_resource(@resource)
@transaction.resource_status(@resource).should be_skipped
end
end
end
describe "when applying a resource" do
before do
@resource = Puppet::Type.type(:file).new :path => @basepath
@status = Puppet::Resource::Status.new(@resource)
@transaction = Puppet::Transaction.new(Puppet::Resource::Catalog.new)
@transaction.event_manager.stubs(:queue_events)
@transaction.resource_harness.stubs(:evaluate).returns(@status)
end
it "should use its resource harness to apply the resource" do
@transaction.resource_harness.expects(:evaluate).with(@resource)
@transaction.apply(@resource)
end
it "should add the resulting resource status to its status list" do
@transaction.apply(@resource)
@transaction.resource_status(@resource).should be_instance_of(Puppet::Resource::Status)
end
it "should queue any events added to the resource status" do
@status.expects(:events).returns %w{a b}
@transaction.event_manager.expects(:queue_events).with(@resource, ["a", "b"])
@transaction.apply(@resource)
end
it "should log and skip any resources that cannot be applied" do
@transaction.resource_harness.expects(:evaluate).raises ArgumentError
@resource.expects(:err)
@transaction.apply(@resource)
@transaction.report.resource_statuses[@resource.to_s].should be_nil
end
end
describe "when generating resources" do
it "should call 'generate' on all created resources" do
first = Puppet::Type.type(:notify).new(:name => "first")
second = Puppet::Type.type(:notify).new(:name => "second")
third = Puppet::Type.type(:notify).new(:name => "third")
@catalog = Puppet::Resource::Catalog.new
@transaction = Puppet::Transaction.new(@catalog)
first.expects(:generate).returns [second]
second.expects(:generate).returns [third]
third.expects(:generate)
- @transaction.generate_additional_resources(first, :generate)
+ @transaction.generate_additional_resources(first)
end
it "should finish all resources" do
generator = stub 'generator', :depthfirst? => true, :tags => []
resource = stub 'resource', :tag => nil
@catalog = Puppet::Resource::Catalog.new
@transaction = Puppet::Transaction.new(@catalog)
generator.expects(:generate).returns [resource]
@catalog.expects(:add_resource).yields(resource)
resource.expects(:finish)
- @transaction.generate_additional_resources(generator, :generate)
+ @transaction.generate_additional_resources(generator)
end
it "should skip generated resources that conflict with existing resources" do
generator = mock 'generator', :tags => []
resource = stub 'resource', :tag => nil
@catalog = Puppet::Resource::Catalog.new
@transaction = Puppet::Transaction.new(@catalog)
generator.expects(:generate).returns [resource]
@catalog.expects(:add_resource).raises(Puppet::Resource::Catalog::DuplicateResourceError.new("foo"))
resource.expects(:finish).never
resource.expects(:info) # log that it's skipped
- @transaction.generate_additional_resources(generator, :generate).should be_empty
+ @transaction.generate_additional_resources(generator)
end
it "should copy all tags to the newly generated resources" do
child = stub 'child'
generator = stub 'resource', :tags => ["one", "two"]
@catalog = Puppet::Resource::Catalog.new
@transaction = Puppet::Transaction.new(@catalog)
generator.stubs(:generate).returns [child]
@catalog.stubs(:add_resource)
child.expects(:tag).with("one", "two")
+ child.expects(:finish)
+ generator.expects(:depthfirst?)
- @transaction.generate_additional_resources(generator, :generate)
+ @transaction.generate_additional_resources(generator)
end
end
describe "when skipping a resource" do
before :each do
@resource = Puppet::Type.type(:notify).new :name => "foo"
@catalog = Puppet::Resource::Catalog.new
@resource.catalog = @catalog
@transaction = Puppet::Transaction.new(@catalog)
end
it "should skip resource with missing tags" do
@transaction.stubs(:missing_tags?).returns(true)
@transaction.should be_skip(@resource)
end
it "should skip unscheduled resources" do
@transaction.stubs(:scheduled?).returns(false)
@transaction.should be_skip(@resource)
end
it "should skip resources with failed dependencies" do
@transaction.stubs(:failed_dependencies?).returns(true)
@transaction.should be_skip(@resource)
end
it "should skip virtual resource" do
@resource.stubs(:virtual?).returns true
@transaction.should be_skip(@resource)
end
end
describe "when determining if tags are missing" do
before :each do
@resource = Puppet::Type.type(:notify).new :name => "foo"
@catalog = Puppet::Resource::Catalog.new
@resource.catalog = @catalog
@transaction = Puppet::Transaction.new(@catalog)
@transaction.stubs(:ignore_tags?).returns false
end
it "should not be missing tags if tags are being ignored" do
@transaction.expects(:ignore_tags?).returns true
@resource.expects(:tagged?).never
@transaction.should_not be_missing_tags(@resource)
end
it "should not be missing tags if the transaction tags are empty" do
@transaction.tags = []
@resource.expects(:tagged?).never
@transaction.should_not be_missing_tags(@resource)
end
it "should otherwise let the resource determine if it is missing tags" do
tags = ['one', 'two']
@transaction.tags = tags
@resource.expects(:tagged?).with(*tags).returns(false)
@transaction.should be_missing_tags(@resource)
end
end
describe "when determining if a resource should be scheduled" do
before :each do
@resource = Puppet::Type.type(:notify).new :name => "foo"
@catalog = Puppet::Resource::Catalog.new
@resource.catalog = @catalog
@transaction = Puppet::Transaction.new(@catalog)
end
it "should always schedule resources if 'ignoreschedules' is set" do
@transaction.ignoreschedules = true
@transaction.resource_harness.expects(:scheduled?).never
@transaction.should be_scheduled(@resource)
end
it "should let the resource harness determine whether the resource should be scheduled" do
@transaction.resource_harness.expects(:scheduled?).with(@transaction.resource_status(@resource), @resource).returns "feh"
@transaction.scheduled?(@resource).should == "feh"
end
end
describe "when prefetching" do
it "should match resources by name, not title" do
@catalog = Puppet::Resource::Catalog.new
@transaction = Puppet::Transaction.new(@catalog)
# Have both a title and name
resource = Puppet::Type.type(:sshkey).create :title => "foo", :name => "bar", :type => :dsa, :key => "eh"
@catalog.add_resource resource
resource.provider.class.expects(:prefetch).with("bar" => resource)
@transaction.prefetch
end
end
it "should return all resources for which the resource status indicates the resource has changed when determinig changed resources" do
@catalog = Puppet::Resource::Catalog.new
@transaction = Puppet::Transaction.new(@catalog)
names = []
2.times do |i|
name = File.join(@basepath, "file#{i}")
resource = Puppet::Type.type(:file).new :path => name
names << resource.to_s
@catalog.add_resource resource
@transaction.add_resource_status Puppet::Resource::Status.new(resource)
end
@transaction.resource_status(names[0]).changed = true
@transaction.changed?.should == [@catalog.resource(names[0])]
end
describe 'when checking application run state' do
before do
without_warnings { Puppet::Application = Class.new(Puppet::Application) }
@catalog = Puppet::Resource::Catalog.new
@transaction = Puppet::Transaction.new(@catalog)
end
after do
without_warnings { Puppet::Application = Puppet::Application.superclass }
end
it 'should return true for :stop_processing? if Puppet::Application.stop_requested? is true' do
Puppet::Application.stubs(:stop_requested?).returns(true)
@transaction.stop_processing?.should be_true
end
it 'should return false for :stop_processing? if Puppet::Application.stop_requested? is false' do
Puppet::Application.stubs(:stop_requested?).returns(false)
@transaction.stop_processing?.should be_false
end
describe 'within an evaluate call' do
before do
- @resource = stub 'resource', :ref => 'some_ref'
+ @resource = Puppet::Type.type(:notify).new :title => "foobar"
@catalog.add_resource @resource
@transaction.stubs(:prepare)
- @transaction.sorted_resources = [@resource]
end
it 'should stop processing if :stop_processing? is true' do
- @transaction.expects(:stop_processing?).returns(true)
+ @transaction.stubs(:stop_processing?).returns(true)
@transaction.expects(:eval_resource).never
@transaction.evaluate
end
it 'should continue processing if :stop_processing? is false' do
- @transaction.expects(:stop_processing?).returns(false)
+ @transaction.stubs(:stop_processing?).returns(false)
@transaction.expects(:eval_resource).returns(nil)
@transaction.evaluate
end
end
end
end
describe Puppet::Transaction, " when determining tags" do
before do
@config = Puppet::Resource::Catalog.new
@transaction = Puppet::Transaction.new(@config)
end
it "should default to the tags specified in the :tags setting" do
Puppet.expects(:[]).with(:tags).returns("one")
@transaction.tags.should == %w{one}
end
it "should split tags based on ','" do
Puppet.expects(:[]).with(:tags).returns("one,two")
@transaction.tags.should == %w{one two}
end
it "should use any tags set after creation" do
Puppet.expects(:[]).with(:tags).never
@transaction.tags = %w{one two}
@transaction.tags.should == %w{one two}
end
it "should always convert assigned tags to an array" do
@transaction.tags = "one::two"
@transaction.tags.should == %w{one::two}
end
it "should accept a comma-delimited string" do
@transaction.tags = "one, two"
@transaction.tags.should == %w{one two}
end
it "should accept an empty string" do
@transaction.tags = ""
@transaction.tags.should == []
end
end
diff --git a/spec/unit/type/whit_spec.rb b/spec/unit/type/whit_spec.rb
index 46eb0ab6e..0a3324afa 100644
--- a/spec/unit/type/whit_spec.rb
+++ b/spec/unit/type/whit_spec.rb
@@ -1,11 +1,11 @@
#!/usr/bin/env ruby
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
whit = Puppet::Type.type(:whit).new(:name => "Foo::Bar")
describe whit do
- it "should stringify as though it were the class it represents" do
- whit.to_s.should == "Class[Foo::Bar]"
+ it "should stringify in a way that users will regognise" do
+ whit.to_s.should == "(Foo::Bar)"
end
end
diff --git a/test/lib/puppettest/support/assertions.rb b/test/lib/puppettest/support/assertions.rb
index 31fa3f1da..758c126ce 100644
--- a/test/lib/puppettest/support/assertions.rb
+++ b/test/lib/puppettest/support/assertions.rb
@@ -1,64 +1,69 @@
require 'puppettest/support/utils'
require 'fileutils'
module PuppetTest
include PuppetTest::Support::Utils
def assert_logged(level, regex, msg = nil)
# Skip verifying logs that we're not supposed to send.
return unless Puppet::Util::Log.sendlevel?(level)
r = @logs.detect { |l| l.level == level and l.message =~ regex }
@logs.clear
assert(r, msg)
end
def assert_uid_gid(uid, gid, filename)
flunk "Must be uid 0 to run these tests" unless Process.uid == 0
fork do
Puppet::Util::SUIDManager.gid = gid
Puppet::Util::SUIDManager.uid = uid
# FIXME: use the tempfile method from puppettest.rb
system("mkfifo #{filename}")
f = File.open(filename, "w")
f << "#{Puppet::Util::SUIDManager.uid}\n#{Puppet::Util::SUIDManager.gid}\n"
yield if block_given?
end
# avoid a race.
true while !File.exists? filename
f = File.open(filename, "r")
a = f.readlines
assert_equal(uid, a[0].chomp.to_i, "UID was incorrect")
assert_equal(gid, a[1].chomp.to_i, "GID was incorrect")
FileUtils.rm(filename)
end
def assert_events(events, *resources)
trans = nil
comp = nil
msg = nil
raise Puppet::DevError, "Incorrect call of assert_events" unless events.is_a? Array
msg = resources.pop if resources[-1].is_a? String
config = resources2catalog(*resources)
transaction = Puppet::Transaction.new(config)
- run_events(:evaluate, transaction, events, msg)
+ transaction.evaluate
+ newevents = transaction.events.
+ reject { |e| ['failure', 'audit'].include? e.status }.
+ collect { |e| e.name }
+
+ assert_equal(events, newevents, "Incorrect evaluate #{msg} events")
transaction
end
# A simpler method that just applies what we have.
def assert_apply(*resources)
config = resources2catalog(*resources)
events = nil
assert_nothing_raised("Failed to evaluate") {
events = config.apply.events
}
events
end
end
diff --git a/test/lib/puppettest/support/utils.rb b/test/lib/puppettest/support/utils.rb
index bca5d9634..4ecc3819e 100644
--- a/test/lib/puppettest/support/utils.rb
+++ b/test/lib/puppettest/support/utils.rb
@@ -1,160 +1,141 @@
module PuppetTest::Support
end
module PuppetTest::Support::Utils
def gcdebug(type)
Puppet.warning "#{type}: #{ObjectSpace.each_object(type) { |o| }}"
end
def basedir(*list)
unless defined? @@basedir
Dir.chdir(File.dirname(__FILE__)) do
@@basedir = File.dirname(File.dirname(File.dirname(File.dirname(Dir.getwd))))
end
end
if list.empty?
@@basedir
else
File.join(@@basedir, *list)
end
end
def fakedata(dir,pat='*')
glob = "#{basedir}/test/#{dir}/#{pat}"
files = Dir.glob(glob,File::FNM_PATHNAME)
raise Puppet::DevError, "No fakedata matching #{glob}" if files.empty?
files
end
def datadir(*list)
File.join(basedir, "test", "data", *list)
end
#
# TODO: I think this method needs to be renamed to something a little more
# explanatory.
#
def newobj(type, name, hash)
transport = Puppet::TransObject.new(name, "file")
transport[:path] = path
transport[:ensure] = "file"
assert_nothing_raised {
file = transport.to_ral
}
end
# Turn a list of resources, or possibly a catalog and some resources,
# into a catalog object.
def resources2catalog(*resources)
if resources[0].is_a?(Puppet::Resource::Catalog)
config = resources.shift
resources.each { |r| config.add_resource r } unless resources.empty?
elsif resources[0].is_a?(Puppet::Type.type(:component))
raise ArgumentError, "resource2config() no longer accpts components"
comp = resources.shift
comp.delve
else
config = Puppet::Resource::Catalog.new
resources.each { |res| config.add_resource res }
end
config
end
# TODO: rewrite this to use the 'etc' module.
# Define a variable that contains the name of my user.
def setme
# retrieve the user name
id = %x{id}.chomp
if id =~ /uid=\d+\(([^\)]+)\)/
@me = $1
else
puts id
end
raise "Could not retrieve user name; 'id' did not work" unless defined?(@me)
end
# Define a variable that contains a group I'm in.
def set_mygroup
# retrieve the user name
group = %x{groups}.chomp.split(/ /)[0]
raise "Could not find group to set in @mygroup" unless group
@mygroup = group
end
- def run_events(type, trans, events, msg)
- case type
- when :evaluate, :rollback # things are hunky-dory
- else
- raise Puppet::DevError, "Incorrect run_events type"
- end
-
- method = type
-
- trans.send(method)
- newevents = trans.events.reject { |e| ['failure', 'audit'].include? e.status }.collect { |e|
- e.name
- }
-
- assert_equal(events, newevents, "Incorrect #{type} #{msg} events")
-
- trans
- end
-
def fakefile(name)
ary = [basedir, "test"]
ary += name.split("/")
file = File.join(ary)
raise Puppet::DevError, "No fakedata file #{file}" unless FileTest.exists?(file)
file
end
# wrap how to retrieve the masked mode
def filemode(file)
File.stat(file).mode & 007777
end
def memory
Puppet::Util.memory
end
# a list of files that we can parse for testing
def textfiles
textdir = datadir "snippets"
Dir.entries(textdir).reject { |f|
f =~ /^\./ or f =~ /fail/
}.each { |f|
yield File.join(textdir, f)
}
end
def failers
textdir = datadir "failers"
# only parse this one file now
files = Dir.entries(textdir).reject { |file|
file =~ %r{\.swp}
}.reject { |file|
file =~ %r{\.disabled}
}.collect { |file|
File.join(textdir,file)
}.find_all { |file|
FileTest.file?(file)
}.sort.each { |file|
Puppet.debug "Processing #{file}"
yield file
}
end
def mk_catalog(*resources)
if resources[0].is_a?(String)
name = resources.shift
else
name = :testing
end
config = Puppet::Resource::Catalog.new :testing do |conf|
resources.each { |resource| conf.add_resource resource }
end
config
end
end
diff --git a/test/other/relationships.rb b/test/other/relationships.rb
index 717353c02..e36dcda71 100755
--- a/test/other/relationships.rb
+++ b/test/other/relationships.rb
@@ -1,98 +1,91 @@
#!/usr/bin/env ruby
require File.expand_path(File.dirname(__FILE__) + '/../lib/puppettest')
require 'puppet'
require 'puppettest'
class TestRelationships < Test::Unit::TestCase
include PuppetTest
def setup
super
Puppet::Type.type(:exec)
end
def newfile
assert_nothing_raised {
-
- return Puppet::Type.type(:file).new(
-
+ return Puppet::Type.type(:file).new(
: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 #{source.ref} => #{target.ref}")
if refresher
assert_equal(:ALL_EVENTS, edge.event)
assert_equal(:refresh, edge.callback)
else
assert_nil(edge.event)
assert_nil(edge.callback, "Got a callback with no events")
end
end
end
end
def test_autorequire
# We know that execs autorequire their cwd, so we'll use that
path = tempfile
-
-
- file = Puppet::Type.type(:file).new(
- :title => "myfile", :path => path,
-
- :ensure => :directory)
-
- exec = Puppet::Type.newexec(
- :title => "myexec", :cwd => path,
-
- :command => "/bin/echo")
-
+ file = Puppet::Type.type(:file).new(
+ :title => "myfile", :path => path,
+ :ensure => :directory
+ )
+ exec = Puppet::Type.newexec(
+ :title => "myexec", :cwd => path,
+ :command => "/bin/echo"
+ )
catalog = mk_catalog(file, exec)
reqs = nil
assert_nothing_raised do
reqs = exec.autorequire
end
assert_instance_of(Puppet::Relationship, reqs[0], "Did not return a relationship edge")
assert_equal(file, reqs[0].source, "Did not set the autorequire source correctly")
assert_equal(exec, reqs[0].target, "Did not set the autorequire target correctly")
# Now make sure that these relationships are added to the
# relationship graph
catalog.apply do |trans|
- assert(catalog.relationship_graph.edge?(file, exec), "autorequire edge was not created")
+ assert(catalog.relationship_graph.path_between(file, exec), "autorequire edge was not created")
end
end
# Testing #411. It was a problem with builddepends.
def test_missing_deps
file = Puppet::Type.type(:file).new :path => tempfile, :require => Puppet::Resource.new("file", "/no/such/file")
assert_raise(Puppet::Error) do
file.builddepends
end
end
end
diff --git a/test/other/transactions.rb b/test/other/transactions.rb
index be8cef483..812e519ab 100755
--- a/test/other/transactions.rb
+++ b/test/other/transactions.rb
@@ -1,434 +1,401 @@
#!/usr/bin/env ruby
require File.expand_path(File.dirname(__FILE__) + '/../lib/puppettest')
require 'mocha'
require 'puppet'
require 'puppettest'
require 'puppettest/support/resources'
require 'puppettest/support/utils'
class TestTransactions < Test::Unit::TestCase
include PuppetTest::FileTesting
include PuppetTest::Support::Resources
include PuppetTest::Support::Utils
class Fakeprop true)
def finish
$finished << self.name
end
end
type.class_eval(&block) if block
cleanup do
Puppet::Type.rmtype(:generator)
end
type
end
# Create a new type that generates instances with shorter names.
def mkreducer(&block)
type = mkgenerator do
def eval_generate
ret = []
if title.length > 1
ret << self.class.new(:title => title[0..-2])
else
return nil
end
ret
end
end
type.class_eval(&block) if block
type
end
def test_prefetch
# Create a type just for testing prefetch
name = :prefetchtesting
$prefetched = false
type = Puppet::Type.newtype(name) do
newparam(:name) {}
end
cleanup do
Puppet::Type.rmtype(name)
end
# Now create a provider
type.provide(:prefetch) do
def self.prefetch(resources)
$prefetched = resources
end
end
# Now create an instance
inst = type.new :name => "yay"
# Create a transaction
trans = Puppet::Transaction.new(mk_catalog(inst))
# Make sure prefetch works
assert_nothing_raised do
trans.prefetch
end
assert_equal({inst.title => inst}, $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({inst.title => inst}, $prefetched, "evaluate did not call prefetch")
end
- # We need to generate resources before we prefetch them, else generated
- # resources that require prefetching don't work.
- def test_generate_before_prefetch
- config = mk_catalog
- trans = Puppet::Transaction.new(config)
-
- generate = nil
- prefetch = nil
- trans.expects(:generate).with { |*args| generate = Time.now; true }
- trans.expects(:prefetch).with { |*args| ! generate.nil? }
- trans.prepare
- return
-
- resource = Puppet::Type.type(:file).new :ensure => :present, :path => tempfile
- other_resource = mock 'generated'
- def resource.generate
- [other_resource]
- end
-
-
- config = mk_catalog(yay, rah)
- trans = Puppet::Transaction.new(config)
-
- assert_nothing_raised do
- trans.generate
- end
-
- %w{ya ra y r}.each do |name|
- assert(trans.catalog.vertex?(Puppet::Type.type(:generator)[name]), "Generated #{name} was not a vertex")
- assert($finished.include?(name), "#{name} was not finished")
- end
- end
-
def test_ignore_tags?
config = Puppet::Resource::Catalog.new
config.host_config = true
transaction = Puppet::Transaction.new(config)
assert(! transaction.ignore_tags?, "Ignoring tags when applying a host catalog")
config.host_config = false
transaction = Puppet::Transaction.new(config)
assert(transaction.ignore_tags?, "Not ignoring tags when applying a non-host catalog")
end
def test_missing_tags?
resource = Puppet::Type.type(:notify).new :title => "foo"
resource.stubs(:tagged?).returns true
config = Puppet::Resource::Catalog.new
# Mark it as a host config so we don't care which test is first
config.host_config = true
transaction = Puppet::Transaction.new(config)
assert(! transaction.missing_tags?(resource), "Considered a resource to be missing tags when none are set")
# host catalogs pay attention to tags, no one else does.
Puppet[:tags] = "three,four"
config.host_config = false
transaction = Puppet::Transaction.new(config)
assert(! transaction.missing_tags?(resource), "Considered a resource to be missing tags when not running a host catalog")
#
config.host_config = true
transaction = Puppet::Transaction.new(config)
assert(! transaction.missing_tags?(resource), "Considered a resource to be missing tags when running a host catalog and all tags are present")
transaction = Puppet::Transaction.new(config)
resource.stubs :tagged? => false
assert(transaction.missing_tags?(resource), "Considered a resource not to be missing tags when running a host catalog and tags are missing")
end
# Make sure changes in contained files still generate callback events.
def test_generated_callbacks
dir = tempfile
maker = tempfile
Dir.mkdir(dir)
file = File.join(dir, "file")
File.open(file, "w") { |f| f.puts "" }
File.chmod(0644, file)
File.chmod(0755, dir) # So only the child file causes a change
dirobj = Puppet::Type.type(:file).new :mode => "755", :recurse => true, :path => dir
exec = Puppet::Type.type(:exec).new :title => "make",
:command => "touch #{maker}", :path => ENV['PATH'], :refreshonly => true,
:subscribe => dirobj
assert_apply(dirobj, exec)
assert(FileTest.exists?(maker), "Did not make callback file")
end
# Testing #401 -- transactions are calling refresh on classes that don't support it.
def test_callback_availability
$called = []
klass = Puppet::Type.newtype(:norefresh) do
newparam(:name, :namevar => true) {}
def method_missing(method, *args)
$called << method
end
end
cleanup do
$called = nil
Puppet::Type.rmtype(:norefresh)
end
file = Puppet::Type.type(:file).new :path => tempfile, :content => "yay"
one = klass.new :name => "one", :subscribe => file
assert_apply(file, one)
assert(! $called.include?(:refresh), "Called refresh when it wasn't set as a method")
end
# Testing #437 - cyclic graphs should throw failures.
def test_fail_on_cycle
one = Puppet::Type.type(:exec).new(:name => "/bin/echo one")
two = Puppet::Type.type(:exec).new(:name => "/bin/echo two")
one[:require] = two
two[:require] = one
config = mk_catalog(one, two)
trans = Puppet::Transaction.new(config)
assert_raise(Puppet::Error) do
- trans.prepare
+ trans.evaluate
end
end
def test_errors_during_generation
type = Puppet::Type.newtype(:failer) do
newparam(:name) {}
def eval_generate
raise ArgumentError, "Invalid value"
end
def generate
raise ArgumentError, "Invalid value"
end
end
cleanup { Puppet::Type.rmtype(:failer) }
obj = type.new(:name => "testing")
assert_apply(obj)
end
def test_self_refresh_causes_triggering
type = Puppet::Type.newtype(:refresher, :self_refresh => true) do
attr_accessor :refreshed, :testing
newparam(:name) {}
newproperty(:testing) do
def retrieve
:eh
end
def sync
# noop
:ran_testing
end
end
def refresh
@refreshed = true
end
end
cleanup { Puppet::Type.rmtype(:refresher)}
obj = type.new(:name => "yay", :testing => "cool")
assert(! obj.insync?(obj.retrieve), "fake object is already in sync")
# Now make sure it gets refreshed when the change happens
assert_apply(obj)
assert(obj.refreshed, "object was not refreshed during transaction")
end
# Testing #433
def test_explicit_dependencies_beat_automatic
# Create a couple of different resource sets that have automatic relationships and make sure the manual relationships win
rels = {}
# Now add the explicit relationship
# Now files
d = tempfile
f = File.join(d, "file")
file = Puppet::Type.type(:file).new(:path => f, :content => "yay")
dir = Puppet::Type.type(:file).new(:path => d, :ensure => :directory, :require => file)
rels[dir] = file
rels.each do |after, before|
config = mk_catalog(before, after)
trans = Puppet::Transaction.new(config)
str = "from #{before} to #{after}"
assert_nothing_raised("Failed to create graph #{str}") do
trans.prepare
end
graph = trans.relationship_graph
assert(graph.edge?(before, after), "did not create manual relationship #{str}")
assert(! graph.edge?(after, before), "created automatic relationship #{str}")
end
end
# #542 - make sure resources in noop mode still notify their resources,
# so that users know if a service will get restarted.
def test_noop_with_notify
path = tempfile
epath = tempfile
spath = tempfile
file = Puppet::Type.type(:file).new(
:path => path, :ensure => :file,
:title => "file")
exec = Puppet::Type.type(:exec).new(
:command => "touch #{epath}",
:path => ENV["PATH"], :subscribe => file, :refreshonly => true,
:title => 'exec1')
exec2 = Puppet::Type.type(:exec).new(
:command => "touch #{spath}",
:path => ENV["PATH"], :subscribe => exec, :refreshonly => true,
:title => 'exec2')
Puppet[:noop] = true
assert(file.noop, "file not in noop")
assert(exec.noop, "exec not in noop")
@logs.clear
assert_apply(file, exec, exec2)
assert(! FileTest.exists?(path), "Created file in noop")
assert(! FileTest.exists?(epath), "Executed exec in noop")
assert(! FileTest.exists?(spath), "Executed second exec in noop")
assert(@logs.detect { |l|
l.message =~ /should be/ and l.source == file.property(:ensure).path},
"did not log file change")
assert(
@logs.detect { |l|
l.message =~ /Would have/ and l.source == exec.path },
"did not log first exec trigger")
assert(
@logs.detect { |l|
l.message =~ /Would have/ and l.source == exec2.path },
"did not log second exec trigger")
end
def test_only_stop_purging_with_relations
files = []
paths = []
3.times do |i|
path = tempfile
paths << path
file = Puppet::Type.type(:file).new(
:path => path, :ensure => :absent,
:backup => false, :title => "file#{i}")
File.open(path, "w") { |f| f.puts "" }
files << file
end
files[0][:ensure] = :file
files[0][:require] = files[1..2]
# Mark the second as purging
files[1].purging
assert_apply(*files)
assert(FileTest.exists?(paths[1]), "Deleted required purging file")
assert(! FileTest.exists?(paths[2]), "Did not delete non-purged file")
end
def test_flush
$state = :absent
$flushed = 0
type = Puppet::Type.newtype(:flushtest) do
newparam(:name)
newproperty(:ensure) do
newvalues :absent, :present, :other
def retrieve
$state
end
def set(value)
$state = value
:thing_changed
end
end
def flush
$flushed += 1
end
end
cleanup { Puppet::Type.rmtype(:flushtest) }
obj = type.new(:name => "test", :ensure => :present)
# first make sure it runs through and flushes
assert_apply(obj)
assert_equal(:present, $state, "Object did not make a change")
assert_equal(1, $flushed, "object was not flushed")
# Now run a noop and make sure we don't flush
obj[:ensure] = "other"
obj[:noop] = true
assert_apply(obj)
assert_equal(:present, $state, "Object made a change in noop")
assert_equal(1, $flushed, "object was flushed in noop")
end
end
diff --git a/test/ral/type/file/target.rb b/test/ral/type/file/target.rb
index 272128586..d778f2891 100755
--- a/test/ral/type/file/target.rb
+++ b/test/ral/type/file/target.rb
@@ -1,359 +1,346 @@
#!/usr/bin/env ruby
require File.expand_path(File.dirname(__FILE__) + '/../../../lib/puppettest')
require 'puppettest'
require 'puppettest/support/utils'
require 'fileutils'
class TestFileTarget < Test::Unit::TestCase
include PuppetTest::Support::Utils
include PuppetTest::FileTesting
def setup
super
@file = Puppet::Type.type(:file)
end
# Make sure we can create symlinks
def test_symlinks
path = tempfile
link = tempfile
File.open(path, "w") { |f| f.puts "yay" }
file = nil
assert_nothing_raised {
-
- file = Puppet::Type.type(:file).new(
-
+ file = Puppet::Type.type(:file).new(
:title => "somethingelse",
:ensure => path,
-
:path => link
)
}
assert_events([:link_created], file)
assert(FileTest.symlink?(link), "Link was not created")
assert_equal(path, File.readlink(link), "Link was created incorrectly")
# Make sure running it again works
assert_events([], file)
assert_events([], file)
assert_events([], file)
end
def test_simplerecursivelinking
source = tempfile
path = tempfile
subdir = File.join(source, "subdir")
file = File.join(subdir, "file")
system("mkdir -p #{subdir}")
system("touch #{file}")
link = Puppet::Type.type(:file).new(
:ensure => source,
:path => path,
:recurse => true
)
catalog = mk_catalog(link)
catalog.apply
sublink = File.join(path, "subdir")
linkpath = File.join(sublink, "file")
assert(File.directory?(path), "dest is not a dir")
assert(File.directory?(sublink), "subdest is not a dir")
assert(File.symlink?(linkpath), "path is not a link")
assert_equal(file, File.readlink(linkpath))
# Use classes for comparison, because the resource inspection is so large
assert_events([], link, "Link is not in sync")
end
def test_recursivelinking
source = tempfile
dest = tempfile
files = []
dirs = []
# Make a bunch of files and dirs
Dir.mkdir(source)
Dir.chdir(source) do
system("mkdir -p #{"some/path/of/dirs"}")
system("mkdir -p #{"other/path/of/dirs"}")
system("touch #{"file"}")
system("touch #{"other/file"}")
system("touch #{"some/path/of/file"}")
system("touch #{"some/path/of/dirs/file"}")
system("touch #{"other/path/of/file"}")
files = %x{find . -type f}.chomp.split(/\n/)
dirs = %x{find . -type d}.chomp.split(/\n/).reject{|d| d =~ /^\.+$/ }
end
link = nil
assert_nothing_raised {
-
- link = Puppet::Type.type(:file).new(
-
+ link = Puppet::Type.type(:file).new(
:ensure => source,
:path => dest,
-
:recurse => true
)
}
assert_apply(link)
files.each do |f|
f.sub!(/^\.#{File::SEPARATOR}/, '')
path = File.join(dest, f)
assert(FileTest.exists?(path), "Link #{path} was not created")
assert(FileTest.symlink?(path), "#{f} is not a link")
target = File.readlink(path)
assert_equal(File.join(source, f), target)
end
dirs.each do |d|
d.sub!(/^\.#{File::SEPARATOR}/, '')
path = File.join(dest, d)
assert(FileTest.exists?(path), "Dir #{path} was not created")
assert(FileTest.directory?(path), "#{d} is not a directory")
end
end
def test_localrelativelinks
dir = tempfile
Dir.mkdir(dir)
source = File.join(dir, "source")
File.open(source, "w") { |f| f.puts "yay" }
dest = File.join(dir, "link")
link = nil
assert_nothing_raised {
-
- link = Puppet::Type.type(:file).new(
-
+ link = Puppet::Type.type(:file).new(
:path => dest,
-
:ensure => "source"
)
}
assert_events([:link_created], link)
assert(FileTest.symlink?(dest), "Did not create link")
assert_equal("source", File.readlink(dest))
assert_equal("yay\n", File.read(dest))
end
def test_recursivelinkingmissingtarget
source = tempfile
dest = tempfile
resources = []
- resources << Puppet::Type.type(:exec).new(
-
+ resources << Puppet::Type.type(:exec).new(
:command => "mkdir #{source}; touch #{source}/file",
:title => "yay",
-
:path => ENV["PATH"]
)
- resources << Puppet::Type.type(:file).new(
-
+ resources << Puppet::Type.type(:file).new(
:ensure => source,
:path => dest,
:recurse => true,
-
:require => resources[0]
)
assert_apply(*resources)
link = File.join(dest, "file")
assert(FileTest.symlink?(link), "Did not make link")
assert_equal(File.join(source, "file"), File.readlink(link))
end
def test_insync?
source = tempfile
dest = tempfile
obj = @file.create(:path => source, :target => dest)
prop = obj.send(:property, :target)
prop.send(:instance_variable_set, "@should", [:nochange])
assert(
prop.insync?(prop.retrieve),
"Property not in sync with should == :nochange")
prop = obj.send(:property, :target)
prop.send(:instance_variable_set, "@should", [:notlink])
assert(
prop.insync?(prop.retrieve),
"Property not in sync with should == :nochange")
# Lastly, make sure that we don't try to do anything when we're
# recursing, since 'ensure' does the work.
obj[:recurse] = true
prop.should = dest
assert(
prop.insync?(prop.retrieve),
"Still out of sync during recursion")
end
def test_replacedirwithlink
Puppet[:trace] = false
path = tempfile
link = tempfile
File.open(path, "w") { |f| f.puts "yay" }
Dir.mkdir(link)
File.open(File.join(link, "yay"), "w") do |f| f.puts "boo" end
file = nil
assert_nothing_raised {
file = Puppet::Type.type(:file).new(
:ensure => path,
:path => link,
:backup => false
)
}
# First run through without :force
assert_events([], file)
assert(FileTest.directory?(link), "Link replaced dir without force")
assert_nothing_raised { file[:force] = true }
assert_events([:link_created], file)
assert(FileTest.symlink?(link), "Link was not created")
assert_equal(path, File.readlink(link), "Link was created incorrectly")
end
def test_replace_links_with_files
base = tempfile
Dir.mkdir(base)
file = File.join(base, "file")
link = File.join(base, "link")
File.open(file, "w") { |f| f.puts "yayness" }
File.symlink(file, link)
obj = Puppet::Type.type(:file).new(
:path => link,
:ensure => "file"
)
assert_apply(obj)
assert_equal(
"yayness\n", File.read(file),
"Original file got changed")
assert_equal("file", File.lstat(link).ftype, "File is still a link")
end
def test_no_erase_linkedto_files
base = tempfile
Dir.mkdir(base)
dirs = {}
%w{other source target}.each do |d|
dirs[d] = File.join(base, d)
Dir.mkdir(dirs[d])
end
file = File.join(dirs["other"], "file")
sourcefile = File.join(dirs["source"], "sourcefile")
link = File.join(dirs["target"], "sourcefile")
File.open(file, "w") { |f| f.puts "other" }
File.open(sourcefile, "w") { |f| f.puts "source" }
File.symlink(file, link)
obj = Puppet::Type.type(:file).new(
:path => dirs["target"],
:ensure => :file,
:source => dirs["source"],
:recurse => true
)
config = mk_catalog obj
config.apply
newfile = File.join(dirs["target"], "sourcefile")
assert(File.directory?(dirs["target"]), "Dir did not get created")
assert(File.file?(newfile), "File did not get copied")
assert_equal(File.read(sourcefile), File.read(newfile),
"File did not get copied correctly.")
assert_equal(
"other\n", File.read(file),
"Original file got changed")
assert_equal("file", File.lstat(link).ftype, "File is still a link")
end
def test_replace_links
dest = tempfile
otherdest = tempfile
link = tempfile
File.open(dest, "w") { |f| f.puts "boo" }
File.open(otherdest, "w") { |f| f.puts "yay" }
obj = Puppet::Type.type(:file).new(
:path => link,
:ensure => otherdest
)
assert_apply(obj)
assert_equal(otherdest, File.readlink(link), "Link did not get created")
obj[:ensure] = dest
assert_apply(obj)
assert_equal(dest, File.readlink(link), "Link did not get changed")
end
end