diff --git a/lib/puppet/resource.rb b/lib/puppet/resource.rb
index add32b7cf..50663cb67 100644
--- a/lib/puppet/resource.rb
+++ b/lib/puppet/resource.rb
@@ -1,200 +1,200 @@
require 'puppet'
require 'puppet/util/tagging'
require 'puppet/resource/reference'
# The simplest resource class. Eventually it will function as the
# base class for all resource-like behaviour.
class Puppet::Resource
include Puppet::Util::Tagging
include Enumerable
- attr_accessor :type, :title, :file, :line, :catalog, :implicit
+ attr_accessor :type, :title, :file, :line, :catalog
# Proxy these methods to the parameters hash. It's likely they'll
# be overridden at some point, but this works for now.
%w{has_key? keys length delete empty? <<}.each do |method|
define_method(method) do |*args|
@parameters.send(method, *args)
end
end
# Set a given parameter. Converts all passed names
# to lower-case symbols.
def []=(param, value)
@parameters[parameter_name(param)] = value
end
# Return a given parameter's value. Converts all passed names
# to lower-case symbols.
def [](param)
@parameters[parameter_name(param)]
end
# Compatibility method.
def builtin?
builtin_type?
end
# Is this a builtin resource type?
def builtin_type?
@reference.builtin_type?
end
# Iterate over each param/value pair, as required for Enumerable.
def each
@parameters.each { |p,v| yield p, v }
end
# Create our resource.
def initialize(type, title, parameters = {})
@reference = Puppet::Resource::Reference.new(type, title)
@parameters = {}
parameters.each do |param, value|
self[param] = value
end
tag(@reference.type)
tag(@reference.title) if valid_tag?(@reference.title)
end
# Provide a reference to our resource in the canonical form.
def ref
@reference.to_s
end
# Get our title information from the reference, since it will canonize it for us.
def title
@reference.title
end
# Get our type information from the reference, since it will canonize it for us.
def type
@reference.type
end
# Produce a simple hash of our parameters.
def to_hash
result = @parameters.dup
unless result.include?(namevar)
result[namevar] = title
end
if result.has_key?(nil)
raise "wtf? %s" % namevar.inspect
end
result
end
def to_s
return ref
end
# Convert our resource to Puppet code.
def to_manifest
"%s { '%s':\n%s\n}" % [self.type.to_s.downcase, self.title,
@parameters.collect { |p, v|
if v.is_a? Array
" #{p} => [\'#{v.join("','")}\']"
else
" #{p} => \'#{v}\'"
end
}.join(",\n")
]
end
def to_ref
ref
end
# Convert our resource to a RAL resource instance. Creates component
# instances for resource types that don't exist.
def to_ral
if typeklass = Puppet::Type.type(self.type)
return typeklass.new(self)
else
return Puppet::Type::Component.new(self)
end
end
# Translate our object to a backward-compatible transportable object.
def to_trans
if @reference.builtin_type?
result = to_transobject
else
result = to_transbucket
end
result.file = self.file
result.line = self.line
return result
end
# Create an old-style TransObject instance, for builtin resource types.
def to_transobject
# Now convert to a transobject
result = Puppet::TransObject.new(@reference.title, @reference.type)
to_hash.each do |p, v|
if v.is_a?(Puppet::Resource::Reference)
v = v.to_trans_ref
elsif v.is_a?(Array)
v = v.collect { |av|
if av.is_a?(Puppet::Resource::Reference)
av = av.to_trans_ref
end
av
}
end
# If the value is an array with only one value, then
# convert it to a single value. This is largely so that
# the database interaction doesn't have to worry about
# whether it returns an array or a string.
result[p.to_s] = if v.is_a?(Array) and v.length == 1
v[0]
else
v
end
end
result.tags = self.tags
return result
end
private
# Produce a canonical method name.
def parameter_name(param)
param = param.to_s.downcase.to_sym
if param == :name and n = namevar()
param = namevar
end
param
end
# The namevar for our resource type. If the type doesn't exist,
# always use :name.
def namevar
if t = resource_type
t.namevar
else
:name
end
end
# Retrieve the resource type.
def resource_type
Puppet::Type.type(type)
end
# Create an old-style TransBucket instance, for non-builtin resource types.
def to_transbucket
bucket = Puppet::TransBucket.new([])
bucket.type = self.type
bucket.name = self.title
# TransBuckets don't support parameters, which is why they're being deprecated.
return bucket
end
end
diff --git a/lib/puppet/resource/catalog.rb b/lib/puppet/resource/catalog.rb
index 266f04903..88aa9517d 100644
--- a/lib/puppet/resource/catalog.rb
+++ b/lib/puppet/resource/catalog.rb
@@ -1,532 +1,521 @@
require 'puppet/indirector'
require 'puppet/simple_graph'
require 'puppet/transaction'
require 'puppet/util/cacher'
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_class => :compiler
include Puppet::Util::Tagging
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
# How we should extract the catalog for sending to the client.
attr_reader :extraction_format
# 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
# 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
# Add one or more resources 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|
unless resource.respond_to?(:ref)
raise ArgumentError, "Can only add objects that respond to :ref, not instances of %s" % resource.class
end
- end.find_all { |resource| fail_or_skip_unless_unique(resource) }.each do |resource|
+ end.each { |resource| fail_unless_unique(resource) }.each do |resource|
ref = resource.ref
@transient_resources << resource if applying?
@resource_table[ref] = resource
# If the name and title differ, set up an alias
#self.alias(resource, resource.name) if resource.respond_to?(:name) and resource.respond_to?(:title) and resource.name != resource.title
if resource.respond_to?(:name) and resource.respond_to?(:title) and resource.name != resource.title
self.alias(resource, resource.name) if resource.isomorphic?
end
resource.catalog = self if resource.respond_to?(:catalog=)
add_vertex(resource)
if @relationship_graph
@relationship_graph.add_vertex(resource)
end
yield(resource) if block_given?
end
end
# Create an alias for a resource.
def alias(resource, name)
#set $1
resource.ref =~ /^(.+)\[/
newref = "%s[%s]" % [$1 || resource.class.name, name]
# 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.
return if newref == resource.ref
if existing = @resource_table[newref]
return if existing == resource
raise(ArgumentError, "Cannot alias %s to %s; resource %s already exists" % [resource.ref, name, newref])
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.tags = options[:tags] if options[:tags]
transaction.ignoreschedules = true if options[:ignoreschedules]
transaction.addtimes :config_retrieval => @retrieval_duration
begin
transaction.evaluate
rescue Puppet::Error => detail
puts detail.backtrace if Puppet[:trace]
Puppet.err "Could not apply complete catalog: %s" % detail
rescue => detail
puts detail.backtrace if Puppet[:trace]
Puppet.err "Got an uncaught exception of type %s: %s" % [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?
transaction.send_report if host_config and (Puppet[:report] or Puppet[:summarize])
return transaction
ensure
@applying = false
cleanup()
transaction.cleanup if defined? transaction and transaction
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 defined?(@relationship_graph) and @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 %s" % type
end
return unless resource = klass.new(options)
add_resource(resource)
resource
end
def expired?(ts)
if applying?
return super
else
return true
end
end
# Make sure we support the requested extraction format.
def extraction_format=(value)
unless respond_to?("extract_to_%s" % value)
raise ArgumentError, "Invalid extraction format %s" % value
end
@extraction_format = value
end
# Turn our catalog graph into whatever the client is expecting.
def extract
send("extract_to_%s" % extraction_format)
end
# Create the traditional TransBuckets and TransObjects from our catalog
# graph. LAK:NOTE(20081211): This is a pre-0.25 backward compatibility method.
# It can be removed as soon as xmlrpc is killed.
def extract_to_transportable
top = nil
current = nil
buckets = {}
unless main = vertices.find { |res| res.type == "Class" and res.title == :main }
raise Puppet::DevError, "Could not find 'main' class; cannot generate catalog"
end
# Create a proc for examining edges, which we'll use to build our tree
# of TransBuckets and TransObjects.
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
unless bucket
raise "No bucket created for %s" % source
end
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.
unless target.builtin?
buckets[target.to_s] = child
end
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
return 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 || false
end
def initialize(name = nil)
super()
@name = name if name
@extraction_format ||= :transportable
@classes = []
@resource_table = {}
@transient_resources = []
@applying = false
@relationship_graph = nil
@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 defined? @relationship_graph and @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 %s" % [edge.source]
@relationship_graph.add_edge(edge)
else
vertex.debug "Skipping automatic relationship with %s" % (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)
@relationship_graph.write_graph(:expanded_relationships) if host_config?
end
@relationship_graph
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
ref = Puppet::Resource::Reference.new(type, title).to_s
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.
ref = Puppet::Resource::Reference.new(nil, type).to_s
end
@resource_table[ref]
end
# Return an array of all resources.
def resources
@resource_table.keys
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
# Store the classes in the classfile.
def write_class_file
begin
::File.open(Puppet[:classfile], "w") do |f|
f.puts classes.join("\n")
end
rescue => detail
Puppet.err "Could not create class file %s: %s" % [Puppet[:classfile], detail]
end
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
unless @transient_resources.empty?
remove_resource(*@transient_resources)
@transient_resources.clear
@relationship_graph = nil
end
# Expire any cached data the resources are keeping.
expire()
end
# Verify that the given resource isn't defined elsewhere.
- def fail_or_skip_unless_unique(resource)
+ def fail_unless_unique(resource)
# Short-curcuit the common case,
- return resource unless existing_resource = @resource_table[resource.ref]
-
- if resource.implicit?
- resource.debug "Generated resource conflicts with explicit resource; ignoring generated resource"
- return nil
- elsif old = resource(resource.ref) and old.implicit?
- # The existing resource is implicit; remove it and replace it with
- # the new one.
- old.debug "Replacing with new resource"
- remove_resource(old)
- return resource
- end
+ return unless existing_resource = @resource_table[resource.ref]
# If we've gotten this far, it's a real conflict
# Either it's a defined type, which are never
# isomorphic, or it's a non-isomorphic type, so
# we should throw an exception.
msg = "Duplicate definition: %s is already defined" % resource.ref
if existing_resource.file and existing_resource.line
msg << " in file %s at line %s" %
[existing_resource.file, existing_resource.line]
end
if resource.line or resource.file
msg << "; cannot redefine"
end
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 resource.respond_to?(:virtual?) and resource.virtual?
#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.is_a?(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 edge.source.respond_to?(:virtual?) and edge.source.virtual?
next if edge.target.respond_to?(:virtual?) and edge.target.virtual?
unless source = map[edge.source.ref]
raise Puppet::DevError, "Could not find resource %s when converting %s resources" % [edge.source.ref, message]
end
unless target = map[edge.target.ref]
raise Puppet::DevError, "Could not find resource %s when converting %s resources" % [edge.target.ref, message]
end
result.add_edge(source, target, edge.label)
end
map.clear
result.add_class(*self.classes)
result.tag(*self.tags)
return result
end
end
diff --git a/lib/puppet/transaction.rb b/lib/puppet/transaction.rb
index bb5adc383..37f51b2e5 100644
--- a/lib/puppet/transaction.rb
+++ b/lib/puppet/transaction.rb
@@ -1,695 +1,701 @@
# the class that actually walks our resource/property tree, collects the changes,
# and performs them
require 'puppet'
module Puppet
class Transaction
require 'puppet/transaction/change'
require 'puppet/transaction/event'
attr_accessor :component, :catalog, :ignoreschedules
attr_accessor :sorted_resources, :configurator
# The report, once generated.
attr_reader :report
# The list of events generated in this transaction.
attr_reader :events
include Puppet::Util
# Add some additional times for reporting
def addtimes(hash)
hash.each do |name, num|
@timemetrics[name] = num
end
end
# Check to see if we should actually allow processing, but this really only
# matters when a resource is getting deleted.
def allow_processing?(resource, changes)
# If a resource is going to be deleted but it still has
# dependencies, then don't delete it unless it's implicit or the
# dependency is itself being deleted.
if resource.purging? and resource.deleting?
if deps = relationship_graph.dependents(resource) and ! deps.empty? and deps.detect { |d| ! d.deleting? }
resource.warning "%s still depend%s on me -- not purging" %
[deps.collect { |r| r.ref }.join(","), deps.length > 1 ? "":"s"]
return false
end
end
return true
end
# Are there any failed resources in this transaction?
def any_failed?
failures = @failures.inject(0) { |failures, array| failures += array[1]; failures }
if failures > 0
failures
else
false
end
end
# Apply all changes for a resource, returning a list of the events
# generated.
def apply(resource)
begin
changes = resource.evaluate
rescue => detail
if Puppet[:trace]
puts detail.backtrace
end
resource.err "Failed to retrieve current state of resource: %s" % detail
# Mark that it failed
@failures[resource] += 1
# And then return
return []
end
changes = [changes] unless changes.is_a?(Array)
if changes.length > 0
@resourcemetrics[:out_of_sync] += 1
end
return [] if changes.empty? or ! allow_processing?(resource, changes)
resourceevents = apply_changes(resource, changes)
# If there were changes and the resource isn't in noop mode...
unless changes.empty? or resource.noop
# Record when we last synced
resource.cache(:synced, Time.now)
# Flush, if appropriate
if resource.respond_to?(:flush)
resource.flush
end
# And set a trigger for refreshing this resource if it's a
# self-refresher
if resource.self_refresh? and ! resource.deleting?
# Create an edge with this resource as both the source and
# target. The triggering method treats these specially for
# logging.
events = resourceevents.collect { |e| e.name }
set_trigger(Puppet::Relationship.new(resource, resource, :callback => :refresh, :event => events))
end
end
resourceevents
end
# Apply each change in turn.
def apply_changes(resource, changes)
changes.collect { |change|
@changes << change
@count += 1
events = nil
begin
# use an array, so that changes can return more than one
# event if they want
events = [change.forward].flatten.reject { |e| e.nil? }
rescue => detail
if Puppet[:trace]
puts detail.backtrace
end
change.property.err "change from %s to %s failed: %s" %
[change.property.is_to_s(change.is), change.property.should_to_s(change.should), detail]
@failures[resource] += 1
next
# FIXME this should support using onerror to determine
# behaviour; or more likely, the client calling us
# should do so
end
# Mark that our change happened, so it can be reversed
# if we ever get to that point
unless events.nil? or (events.is_a?(Array) and (events.empty?) or events.include?(:noop))
change.changed = true
@resourcemetrics[:applied] += 1
end
events
}.flatten.reject { |e| e.nil? }
end
# Find all of the changed resources.
def changed?
@changes.find_all { |change| change.changed }.collect { |change|
unless change.property.resource
raise "No resource for %s" % change.inspect
end
change.property.resource
}.uniq
end
# Do any necessary cleanup. If we don't get rid of the graphs, the
# contained resources might never get cleaned up.
def cleanup
if defined? @generated
catalog.remove_resource(*@generated)
end
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 %s" % gen_child
end
end
end
# Are we deleting this resource?
def deleting?(changes)
changes.detect { |change|
change.property.name == :ensure and change.should == :absent
}
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, checkskip = true)
events = []
if resource.is_a?(Puppet::Type::Component)
raise Puppet::DevError, "Got a component to evaluate"
end
if checkskip and skip?(resource)
@resourcemetrics[:skipped] += 1
else
@resourcemetrics[:scheduled] += 1
changecount = @changes.length
# We need to generate first regardless, because the recursive
# actions sometimes change how the top resource is applied.
children = eval_generate(resource)
if children and resource.depthfirst?
children.each do |child|
# The child will never be skipped when the parent isn't
events += eval_resource(child, false)
end
end
# Perform the actual changes
seconds = thinmark do
events += apply(resource)
end
if children and ! resource.depthfirst?
children.each do |child|
events += eval_resource(child, false)
end
end
# A bit of hackery here -- if skipcheck is true, then we're the
# top-level resource. If that's the case, then make sure all of
# the changes list this resource as a proxy. This is really only
# necessary for rollback, since we know the generating resource
# during forward changes.
if children and checkskip
@changes[changecount..-1].each { |change| change.proxy = resource }
end
# Keep track of how long we spend in each type of resource
@timemetrics[resource.class.name] += seconds
end
# Check to see if there are any events for this resource
if triggedevents = trigger(resource)
events += triggedevents
end
# 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.
relationship_graph.matching_edges(events, resource).each do |orig_edge|
# We have to dup the label here, else we modify the original edge label,
# which affects whether a given event will match on the next run, which is,
# of course, bad.
edge = orig_edge.class.new(orig_edge.source, orig_edge.target, orig_edge.label)
edge.event = events.collect { |e| e.name }
set_trigger(edge)
end
# And return the events for collection
events
end
# This method does all the actual work of running a transaction. It
# collects all of the changes, executes them, and responds to any
# necessary events.
def evaluate
@count = 0
# Start logging.
Puppet::Util::Log.newdestination(@report)
prepare()
begin
allevents = @sorted_resources.collect { |resource|
if resource.is_a?(Puppet::Type::Component)
Puppet.warning "Somehow left a component in the relationship graph"
next
end
ret = nil
seconds = thinmark do
ret = eval_resource(resource)
end
if Puppet[:evaltrace] and @catalog.host_config?
resource.info "Evaluated in %0.2f seconds" % seconds
end
ret
}.flatten.reject { |e| e.nil? }
ensure
# And then close the transaction log.
Puppet::Util::Log.close(@report)
end
Puppet.debug "Finishing transaction %s with %s changes" %
[self.object_id, @count]
@events = allevents
allevents
end
# Determine whether a given resource has failed.
def failed?(obj)
if @failures[obj] > 0
return @failures[obj]
else
return false
end
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.
skip = false
deps = relationship_graph.dependencies(resource)
deps.each do |dep|
if fails = failed?(dep)
resource.notice "Dependency %s[%s] has %s failures" %
[dep.class.name, dep.name, @failures[dep]]
skip = true
end
end
return skip
end
# A general method for recursively generating new resources from a
# resource.
def generate_additional_resources(resource, method)
return [] unless resource.respond_to?(method)
begin
made = resource.send(method)
rescue => detail
puts detail.backtrace if Puppet[:trace]
resource.err "Failed to generate additional resources using '%s': %s" % [method, detail]
end
return [] unless made
made = [made] unless made.is_a?(Array)
made.uniq!
made.each do |res|
- @catalog.add_resource(res) { |r| r.finish }
+ begin
+ @catalog.add_resource(res) do |r|
+ r.finish
+ make_parent_child_relationship(resource, [r])
+ end
+ rescue Puppet::Resource::Catalog::DuplicateResourceError
+ next
+ end
end
- make_parent_child_relationship(resource, made)
made
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
end
# Generate a transaction report.
def generate_report
@resourcemetrics[:failed] = @failures.find_all do |name, num|
num > 0
end.length
# Get the total time spent
@timemetrics[:total] = @timemetrics.inject(0) do |total, vals|
total += vals[1]
total
end
# Add all of the metrics related to resource count and status
@report.newmetric(:resources, @resourcemetrics)
# Record the relative time spent in each resource.
@report.newmetric(:time, @timemetrics)
# Then all of the change-related metrics
@report.newmetric(:changes,
:total => @changes.length
)
@report.time = Time.now
return @report
end
# 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(resources)
if resources.is_a?(Puppet::Resource::Catalog)
@catalog = resources
elsif resources.is_a?(Puppet::SimpleGraph)
raise "Transactions should get catalogs now, not SimpleGraph"
else
raise "Transactions require catalogs"
end
@resourcemetrics = {
:total => @catalog.vertices.length,
:out_of_sync => 0, # The number of resources that had changes
:applied => 0, # The number of resources fixed
:skipped => 0, # The number of resources skipped
:restarted => 0, # The number of resources triggered
:failed_restarts => 0, # The number of resources that fail a trigger
:scheduled => 0 # The number of resources scheduled
}
# Metrics for distributing times across the different types.
@timemetrics = Hash.new(0)
# The number of resources that were triggered in this run
@triggered = Hash.new { |hash, key|
hash[key] = Hash.new(0)
}
# Targets of being triggered.
@targets = Hash.new do |hash, key|
hash[key] = []
end
# The changes we're performing
@changes = []
# The resources that have failed and the number of failures each. This
# is used for skipping resources because of failed dependencies.
@failures = Hash.new do |h, key|
h[key] = 0
end
@report = Report.new
@count = 0
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 %s resources for %s" % [provider.name, provider.resource_type.name]
begin
provider.prefetch(resources)
rescue => detail
if Puppet[:trace]
puts detail.backtrace
end
Puppet.err "Could not prefetch %s provider '%s': %s" % [provider.resource_type.name, provider.name, detail]
end
end
end
# Prepare to evaluate the resources in a transaction.
def prepare
# Now add any dynamically generated resources
generate()
# Then prefetch. It's important that we generate and then prefetch,
# so that any generated resources also get prefetched.
prefetch()
# This will throw an error if there are cycles in the graph.
@sorted_resources = relationship_graph.topsort
end
def relationship_graph
catalog.relationship_graph
end
# Send off the transaction report.
def send_report
begin
report = generate_report()
rescue => detail
Puppet.err "Could not generate report: %s" % detail
return
end
if Puppet[:rrdgraph] == true
report.graph()
end
if Puppet[:summarize]
puts report.summary
end
if Puppet[:report]
begin
report.save()
rescue => detail
Puppet.err "Reporting failed: %s" % detail
end
end
end
# Roll all completed changes back.
def rollback
@targets.clear
@triggered.clear
allevents = @changes.reverse.collect { |change|
# skip changes that were never actually run
unless change.changed
Puppet.debug "%s was not changed" % change.to_s
next
end
begin
events = change.backward
rescue => detail
Puppet.err("%s rollback failed: %s" % [change,detail])
if Puppet[:trace]
puts detail.backtrace
end
next
# at this point, we would normally do error handling
# but i haven't decided what to do for that yet
# so just record that a sync failed for a given resource
#@@failures[change.property.parent] += 1
# this still could get hairy; what if file contents changed,
# but a chmod failed? how would i handle that error? dern
end
# FIXME This won't work right now.
relationship_graph.matching_edges(events).each do |edge|
@targets[edge.target] << edge
end
# Now check to see if there are any events for this child.
# Kind of hackish, since going backwards goes a change at a
# time, not a child at a time.
trigger(change.property.resource)
# And return the events for collection
events
}.flatten.reject { |e| e.nil? }
end
# Is the resource currently scheduled?
def scheduled?(resource)
self.ignoreschedules or resource.scheduled?
end
# Set an edge to be triggered when we evaluate its target.
def set_trigger(edge)
return unless method = edge.callback
return unless edge.target.respond_to?(method)
if edge.target.respond_to?(:ref)
unless edge.source == edge.target
edge.source.info "Scheduling %s of %s" % [edge.callback, edge.target.ref]
end
end
@targets[edge.target] << edge
end
# Should this resource be skipped?
def skip?(resource)
skip = false
if missing_tags?(resource)
resource.debug "Not tagged with %s" % tags.join(", ")
elsif ! scheduled?(resource)
resource.debug "Not scheduled"
elsif failed_dependencies?(resource)
resource.warning "Skipping because of failed dependencies"
else
return false
end
return true
end
# The tags we should be checking.
def tags
unless defined? @tags
tags = Puppet[:tags]
if tags.nil? or tags == ""
@tags = []
else
@tags = tags.split(/\s*,\s*/)
end
end
@tags
end
def tags=(tags)
tags = [tags] unless tags.is_a?(Array)
@tags = tags
end
# Is this resource tagged appropriately?
def missing_tags?(resource)
return false if self.ignore_tags? or tags.empty?
return true unless resource.tagged?(tags)
end
# Are there any edges that target this resource?
def targeted?(resource)
# The default value is a new array so we have to test the length of it.
@targets.include?(resource) and @targets[resource].length > 0
end
# Trigger any subscriptions to a child. This does an upwardly recursive
# search -- it triggers the passed resource, but also the resource's parent
# and so on up the tree.
def trigger(resource)
return nil unless targeted?(resource)
callbacks = Hash.new { |hash, key| hash[key] = [] }
trigged = []
@targets[resource].each do |edge|
# Collect all of the subs for each callback
callbacks[edge.callback] << edge
end
callbacks.each do |callback, subs|
noop = true
subs.each do |edge|
if edge.event.nil? or ! edge.event.include?(:noop)
noop = false
end
end
if noop
resource.notice "Would have triggered %s from %s dependencies" %
[callback, subs.length]
# And then add an event for it.
return [Puppet::Transaction::Event.new(:noop, resource)]
end
if subs.length == 1 and subs[0].source == resource
message = "Refreshing self"
else
message = "Triggering '%s' from %s dependencies" %
[callback, subs.length]
end
resource.notice message
# At this point, just log failures, don't try to react
# to them in any way.
begin
resource.send(callback)
@resourcemetrics[:restarted] += 1
rescue => detail
resource.err "Failed to call %s on %s: %s" %
[callback, resource, detail]
@resourcemetrics[:failed_restarts] += 1
if Puppet[:trace]
puts detail.backtrace
end
end
# And then add an event for it.
trigged << Puppet::Transaction::Event.new(:triggered, resource)
triggered(resource, callback)
end
if trigged.empty?
return nil
else
return trigged
end
end
def triggered(resource, method)
@triggered[resource][method] += 1
end
def triggered?(resource, method)
@triggered[resource][method]
end
end
end
require 'puppet/transaction/report'
diff --git a/lib/puppet/type.rb b/lib/puppet/type.rb
index 64cfc2540..311878841 100644
--- a/lib/puppet/type.rb
+++ b/lib/puppet/type.rb
@@ -1,2110 +1,2092 @@
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/resource/reference'
require 'puppet/util/cacher'
require 'puppet/file_collection/lookup'
# 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
###############################
# 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 namevar comes 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
# Cache this, since it gets called multiple times
namevar = self.namevar
order = [namevar]
if self.parameters.include?(:provider)
order << :provider
end
order << [self.properties.collect { |property| property.name },
self.parameters - [:provider],
self.metaparams].flatten.reject { |param|
# we don't want our namevar in there multiple times
param == namevar
}
order.flatten!
return order
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
# Copy an existing class parameter. This allows other types to avoid
# duplicating a parameter definition, and is mostly used by subclasses
# of the File class.
def self.copyparam(klass, name)
param = klass.attrclass(name)
unless param
raise Puppet::DevError, "Class %s has no param %s" % [klass, name]
end
@parameters << param
@parameters.each { |p| @paramhash[name] = p }
if param.isnamevar?
@namevar = param.name
end
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)
}
return 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.
if options[:required_features]
param.required_features = options[:required_features]
end
handle_param_options(name, options)
param.metaparam = true
return param
end
# Find the namevar
def self.namevar
unless defined? @namevar
params = @parameters.find_all { |param|
param.isnamevar? or param.name == :name
}
if params.length > 1
raise Puppet::DevError, "Found multiple namevars for %s" % self.name
elsif params.length == 1
@namevar = params[0].name
else
raise Puppet::DevError, "No namevar for %s" % self.name
end
end
@namevar
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.
if options[:required_features]
param.required_features = options[:required_features]
end
param.isnamevar if options[:namevar]
if param.isnamevar?
@namevar = param.name
end
return param
end
def self.newstate(name, options = {}, &block)
Puppet.warning "newstate() has been deprecrated; use newproperty(%s)" %
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 %s" % options.inspect
end
if @validproperties.include?(name)
raise Puppet::DevError, "Class %s already has a property named %s" %
[self.name, name]
end
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
if block
class_eval(&block)
end
end
# If it's the 'ensure' property, always put it first.
if name == :ensure
@properties.unshift prop
else
@properties << prop
end
return 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)
if self.validproperty?(name) or self.validparameter?(name) or self.metaparam?(name)
@validattrs[name] = true
else
@validattrs[name] = false
end
end
@validattrs[name]
end
# does the name reflect a valid property?
def self.validproperty?(name)
name = symbolize(name)
if @validproperties.include?(name)
return @validproperties[name]
else
return false
end
end
# Return the list of validproperties
def self.validproperties
return {} unless defined? @parameters
return @validproperties.keys
end
# does the name reflect a valid parameter?
def self.validparameter?(name)
unless defined? @parameters
raise Puppet::DevError, "Class %s has not defined parameters" % self
end
if @paramhash.include?(name) or @@metaparamhash.include?(name)
return true
else
return false
end
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
return false
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)
unless self.class.validattr?(name)
fail("Invalid parameter %s(%s)" % [name, name.inspect])
end
if name == :name
name = self.class.namevar
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)
unless self.class.validattr?(name)
fail("Invalid parameter %s" % [name])
end
if name == :name
name = self.class.namevar
end
if value.nil?
raise Puppet::Error.new("Got nil value for %s" % name)
end
if obj = @parameters[name]
obj.value = value
return nil
else
self.newattr(name, :value => value)
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
# 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)
if prop = @parameters[name] and prop.is_a?(Puppet::Property)
return prop.should
else
return nil
end
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, options = {})
if name.is_a?(Class)
klass = name
name = klass.name
end
unless klass = self.class.attrclass(name)
raise Puppet::Error, "Resource type %s does not support parameter %s" % [self.class.name, name]
end
if @parameters.include?(name)
raise Puppet::Error, "Parameter '%s' is already defined in %s" %
[name, self.ref]
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
# Add resource information at creation time, so it's available
# during validation.
options[:resource] = self
begin
# make sure the parameter doesn't have any errors
return @parameters[name] = klass.new(options)
rescue => detail
error = Puppet::Error.new("Parameter %s failed: %s" %
[name, detail])
error.set_backtrace(detail.backtrace)
raise error
end
end
# return the value of a parameter
def parameter(name)
@parameters[name.to_sym]
end
# Is the named property defined?
def propertydefined?(name)
unless name.is_a? Symbol
name = name.intern
end
return @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)
if obj = @parameters[symbolize(name)] and obj.is_a?(Puppet::Property)
return obj
else
return nil
end
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
# Return a specific value for an attribute.
def value(name)
name = attr_alias(name)
if obj = @parameters[name] and obj.respond_to?(:value)
return obj.value
else
return nil
end
end
# Meta-parameter methods: These methods deal with the results
# of specifying metaparameters
private
# Return all of the property objects, in the order specified in the
# class.
def properties
#debug "%s has %s properties" % [self,@parameters.length]
props = self.class.properties.collect { |prop|
@parameters[prop.name]
}.find_all { |p|
! p.nil?
}.each do |prop|
unless prop.is_a?(Puppet::Property)
raise Puppet::DevError, "got a non-property %s(%s)" %
[prop.class, prop.class.name]
end
end
props
end
public
- ###############################
- # Code related to the closure-like behaviour of the resource classes.
- attr_accessor :implicit
-
# 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 implicit?
- if defined? @implicit and @implicit
- return true
- else
- return false
- 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 defined? @managed and @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?
if defined? @depthfirst
return @depthfirst
else
return false
end
end
def depthfirst?
self.class.depthfirst?
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.
# This method is responsible for collecting property changes we always
# descend into the children before we evaluate our current properties.
# This returns any changes resulting from testing, thus 'collect' rather
# than 'each'.
def evaluate
if self.provider.is_a?(Puppet::Provider)
unless provider.class.suitable?
raise Puppet::Error, "Provider %s is not functional on this platform" % provider.class.name
end
end
# this only operates on properties, not properties + children
# it's important that we call retrieve() on the type instance,
# not directly on the property, because it allows the type to override
# the method, like pfile does
currentvalues = self.retrieve
changes = propertychanges(currentvalues).flatten
# now record how many changes we've resulted in
if changes.length > 0
self.debug "%s change(s)" %
[changes.length]
end
# If we're in noop mode, we don't want to store the checked time,
# because it will result in the resource not getting scheduled if
# someone were to apply the catalog in non-noop mode.
# We're going to go ahead and record that we checked if there were
# no changes, since it's unlikely it will affect the scheduling.
noop = noop?
if ! noop or (noop && changes.length == 0)
self.cache(:checked, Time.now)
end
return changes.flatten
end
# Flush the provider, if it supports it. This is called by the
# transaction.
def flush
if self.provider and self.provider.respond_to?(:flush)
self.provider.flush
end
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 '%s'" %
[property.name]
end
ensureis = is[property]
if property.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 '%s'" %
[property.name]
end
propis = is[property]
unless property.insync?(propis)
property.debug("Not in sync: %s vs %s" %
[propis.inspect, property.should.inspect])
insync = false
#else
# property.debug("In sync")
end
}
#self.debug("%s sync status is %s" % [self,insync])
return insync
end
# retrieve the current value of all contained properties
def retrieve
return currentpropvalues
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 defined?(@noop)
@noop
else
Puppet[:noop]
end
end
def noop
noop?
end
# Retrieve the changes associated with all of the properties.
def propertychanges(currentvalues)
# If we are changing the existence of the object, then none of
# the other properties matter.
changes = []
ensureparam = @parameters[:ensure]
# This allows resource types to have 'ensure' be a parameter, which allows them to
# just pass the parameter on to other generated resources.
ensureparam = nil unless ensureparam.is_a?(Puppet::Property)
if ensureparam && !currentvalues.include?(ensureparam)
raise Puppet::DevError, "Parameter ensure defined but missing from current values"
end
if ensureparam and ! ensureparam.insync?(currentvalues[ensureparam])
changes << Puppet::Transaction::Change.new(ensureparam, currentvalues[ensureparam])
# Else, if the 'ensure' property is correctly absent, then do
# nothing
elsif ensureparam and currentvalues[ensureparam] == :absent
return []
else
changes = properties().find_all { |property|
currentvalues[property] ||= :absent
! property.insync?(currentvalues[property])
}.collect { |property|
Puppet::Transaction::Change.new(property, currentvalues[property])
}
end
if Puppet[:debug] and changes.length > 0
self.debug("Changing " + changes.collect { |ch| ch.property.name }.join(","))
end
changes
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 '%s[%s]' already exists" %
[newobj.class.name, name]
if exobj.file and exobj.line
msg += ("in file %s at line %s" %
[object.file, object.line])
end
if object.file and object.line
msg += ("and cannot be redefined in file %s at line %s" %
[object.file, object.line])
end
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 %s: object already exists" %
[name]
)
end
end
if @aliases.include?(name)
unless @aliases[name] == obj
raise Puppet::Error.new(
"Object %s already has alias %s" %
[@aliases[name].name, 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
if defined? @aliases
@aliases.clear
end
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
if @objects.include?(resource.title)
@objects.delete(resource.title)
end
if @aliases.include?(resource.title)
@aliases.delete(resource.title)
end
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"
return @objects.has_key?(name)
end
# Retrieve all known instances. Either requires providers or must be overridden.
def self.instances
unless defined?(@providers) and ! @providers.empty?
raise Puppet::DevError, "%s has no providers and has not overridden 'instances'" % self.name
end
# 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, :check => :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. This is a convenience method,
# so people can create RAL resources with a hash and get the same behaviour
# as we get internally when we use Resource instances.
# This should only be used directly from Ruby -- it's not used when going through
# normal Puppet usage.
def self.hash2resource(hash)
hash = hash.inject({}) { |result, ary| result[ary[0].to_sym] = ary[1]; result }
if title = hash[:title]
hash.delete(:title)
else
if self.namevar != :name
if hash.include?(:name) and hash.include?(self.namevar)
raise Puppet::Error, "Cannot provide both name and %s to resources of type %s" % [self.namevar, self.name]
end
if title = hash[self.namevar]
hash.delete(self.namevar)
end
end
unless title ||= hash[:name]
raise Puppet::Error, "You must specify a name or title for resources"
end
end
# Now create our resource.
resource = Puppet::Resource.new(self.name, title)
- [:catalog, :implicit].each do |attribute|
+ [: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
return 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(:check) do
desc "Propertys which should have their values retrieved
but which should not actually be modified. This is currently used
internally, but will eventually be used for querying, so that you
could specify that you wanted to check the install state of all
packages, and then query the Puppet client daemon to get reports
on all packages."
munge do |args|
# If they've specified all, collect all known properties
if args == :all
args = @resource.class.properties.find_all do |property|
# Only get properties supported by our provider
if @resource.provider
@resource.provider.class.supports_parameter?(property)
else
true
end
end.collect do |property|
property.name
end
end
unless args.is_a?(Array)
args = [args]
end
unless defined? @resource
self.devfail "No parent for %s, %s?" %
[self.class, self.name]
end
args.each { |property|
unless property.is_a?(Symbol)
property = property.intern
end
next if @resource.propertydefined?(property)
unless propertyklass = @resource.class.validproperty?(property)
if @resource.class.validattr?(property)
next
else
raise Puppet::Error, "%s is not a valid attribute for %s" %
[property, self.class.name]
end
end
next unless propertyklass.checkable?
@resource.newattr(property)
}
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 `LanguageTutorial language tutorial`:trac: for more information.
"
munge do |aliases|
unless aliases.is_a?(Array)
aliases = [aliases]
end
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("%s can not create alias %s: object already exists" % [@resource.title, other])
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::
puppetd --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::Reference)
ref
else
Puppet::Resource::Reference.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 %s %s for %s" % [description, ref.to_s, 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 '%s' of %s" % [reference, @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 %s" % [related_resource.ref])
else
# If there's no callback, there's no point in even adding
# a label.
subargs = nil
self.debug("requires %s" % [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:
running => true,
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
###############################
# 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 defined? @defaultprovider and @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.defaultnum }.max
defaults = defaults.find_all { |provider| provider.defaultnum == max }
retval = nil
if defaults.length > 1
Puppet.warning(
"Found multiple default providers for %s: %s; using %s" %
[self.name, defaults.collect { |i| i.name.to_s }.join(", "),
defaults[0].name]
)
retval = defaults.shift
elsif defaults.length == 1
retval = defaults.shift
else
raise Puppet::DevError, "Could not find a default provider for %s" %
self.name
end
@defaultprovider = retval
end
return @defaultprovider
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.
unless @providers.has_key?(name)
@providerloader.load(name)
end
return @providers[name]
end
# Just list all of the providers.
def self.providers
@providers.keys
end
def self.validprovider?(name)
name = Puppet::Util.symbolize(name)
return (@providers.has_key?(name) && @providers[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 = @providers[name]
Puppet.debug "Reloading %s %s provider" % [name, self.name]
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 %s of %s" %
[pname, name]
end
end
else
Puppet::Provider
end
options[:resource_type] ||= self
self.providify
provider = genclass(name,
:parent => parent,
:hash => @providers,
:prefix => "Provider",
:block => block,
:include => feature_module,
:extend => feature_module,
:attributes => options
)
return 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|
"* **%s**: %s" % [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
if provider_class.is_a?(Puppet::Provider)
provider_class = provider_class.class.name
end
unless provider = @resource.class.provider(provider_class)
raise ArgumentError, "Invalid %s provider '%s'" % [@resource.class.name, provider_class]
end
end
munge do |provider|
provider = provider[0] if provider.is_a? Array
if provider.is_a? String
provider = provider.intern
end
@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 @providers.has_key? name
rmclass(name,
:hash => @providers,
: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
if @providers.empty?
providerloader.loadall
end
@providers.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 %s provider of %s" %
[name, 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)
unless list.is_a?(Array)
list = [list]
end
# Collect the current prereqs
list.each { |dep|
obj = nil
# Support them passing objects directly, to save some effort.
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)
}
}
return 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
###############################
# All of the scheduling code.
# Look up the schedule and set it appropriately. This is done after
# the instantiation phase, so that the schedule can be anywhere in the
# file.
def schedule
unless catalog
warning "Cannot schedule without a schedule-containing catalog"
return nil
end
unless defined? @schedule
if name = self[:schedule]
if sched = catalog.resource(:schedule, name)
@schedule = sched
else
self.fail "Could not find schedule %s" % name
end
else
@schedule = nil
end
end
@schedule
end
# Check whether we are scheduled to run right now or not.
def scheduled?
return true if Puppet[:ignoreschedules]
return true unless schedule = self.schedule
# We use 'checked' here instead of 'synced' because otherwise we'll
# end up checking most resources most times, because they will generally
# have been synced a long time ago (e.g., a file only gets updated
# once a month on the server and its schedule is daily; the last sync time
# will have been a month ago, so we'd end up checking every run).
return schedule.match?(self.cached(:checked).to_i)
end
###############################
# All of the tagging code.
attr_reader :tags
# Add a new tag.
def tag(tag)
tag = tag.intern if tag.is_a? String
unless @tags.include? tag
@tags << tag
end
end
# Define the initial list of tags.
def tags=(list)
list = [list] unless list.is_a? Array
@tags = list.collect do |t|
case t
when String; t.intern
when Symbol; t
else
self.warning "Ignoring tag %s of type %s" % [tag.inspect, tag.class]
end
end
@tags << self.class.name unless @tags.include?(self.class.name)
end
# Figure out of any of the specified tags apply to this object. This is an
# OR operation.
def tagged?(tags)
tags = [tags] unless tags.is_a? Array
tags = tags.collect { |t| t.intern }
return tags.find { |tag| @tags.include? tag }
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
@providers = Hash.new
@defaults = {}
unless defined? @parameters
@parameters = []
end
@validproperties = {}
@properties = []
@parameters = []
@paramhash = {}
@attr_aliases = {}
@paramdoc = Hash.new { |hash,key|
if key.is_a?(String)
key = key.intern
end
if hash.include?(key)
hash[key]
else
"Param Documentation for %s not found" % key
end
}
unless defined? @doc
@doc = ""
end
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
# 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, :implicit].each do |getter|
+ [:file, :line, :catalog].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)
n = self.class.namevar
self[n] = hash[n]
hash.delete(n)
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 %s on %s: %s" % [attr, 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
# Scheduling has to be done when the whole config is instantiated, so
# that file order doesn't matter in finding them.
self.schedule
# 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
# Return a cached value
def cached(name)
Puppet::Util::Storage.cache(self)[name]
#@cache[name] ||= nil
end
# Cache a value
def cache(name, value)
Puppet::Util::Storage.cache(self)[name] = value
#@cache[name] = value
end
# For now, leave the 'name' method functioning like it used to. Once 'title'
# works everywhere, I'll switch it.
def name
return self[:name]
end
# Look up our parent in the catalog, if we have one.
def parent
return nil unless catalog
unless defined?(@parent)
- # This is kinda weird.
- if implicit?
- parents = catalog.relationship_graph.adjacent(self, :direction => :in)
- else
- parents = catalog.adjacent(self, :direction => :in)
- end
- if parents
+ 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
"%s[%s]" % [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 defined? @title and @title
namevar = self.class.namevar
if self.class.validparameter?(namevar)
@title = self[:name]
elsif self.class.validproperty?(namevar)
@title = self.should(namevar)
else
self.devfail "Could not find namevar %s for %s" %
[namevar, self.class.name]
end
end
return @title
end
# convert to a string
def to_s
self.ref
end
# Convert to a transportable object
def to_trans(ret = true)
trans = TransObject.new(self.title, self.class.name)
values = retrieve()
values.each do |name, value|
trans[name.name] = value
end
@parameters.each do |name, param|
# Avoid adding each instance name as both the name and the namevar
next if param.class.isnamevar? and param.value == self.title
# 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'
return trans
end
end # Puppet::Type
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 0cd9ebece..344fcb713 100644
--- a/lib/puppet/type/file.rb
+++ b/lib/puppet/type/file.rb
@@ -1,924 +1,924 @@
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'
module Puppet
newtype(:file) do
include Puppet::Util::MethodHelper
include Puppet::Util::Checksums
@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
Reductive Labs and we can hopefully work with you to develop a
native resource to support what you are doing."
newparam(:path) do
desc "The path to the file to manage. Must be fully qualified."
isnamevar
validate do |value|
unless value =~ /^#{File::SEPARATOR}/
raise Puppet::Error, "File paths must be fully qualified, not '%s'" % value
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 ``puppetmasterd`` 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 /^\./
value
when String
# We can't depend on looking this up right now,
# we have to do it after all of the objects
# have been instantiated.
if resource.catalog and bucketobj = resource.catalog.resource(:filebucket, value)
@resource.bucket = bucketobj.bucket
bucketobj.title
else
# Set it to the string; finish() turns it into a
# filebucket.
@resource.bucket = value
value
end
when Puppet::Network::Client.client(:Dipper)
@resource.bucket = value
value.name
else
self.fail "Invalid backup type %s" %
value.inspect
end
end
end
newparam(:recurse) do
desc "Whether and how deeply to do recursive
management."
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
raise ArgumentError, "Invalid recurse value %s" % 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
raise ArgumentError, "Invalid recurselimit value %s" % 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
attr_accessor :bucket
# Autorequire any parent directories.
autorequire(:file) do
if self[:path]
File.dirname(self[:path])
else
Puppet.err "no path for %s, somehow; cannot setup autorequires" % self.ref
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]
validate do
count = 0
CREATORS.each do |param|
count += 1 if self.should(param)
end
if @parameters.include?(:source)
count += 1
end
if count > 1
self.fail "You cannot specify more than one of %s" % CREATORS.collect { |p| p.to_s}.join(", ")
end
if !self[:source] and self[:recurse] == :remote
self.fail "You cannot specify a remote recursion without a source"
end
if !self[:recurse] and self[:recurselimit]
self.warning "Possible error: recurselimit is set but not recurse, no recursion will happen"
end
end
def self.[](path)
return nil unless path
super(path.gsub(/\/+/, '/').sub(/\/$/, ''))
end
# List files, but only one level deep.
def self.instances(base = "/")
unless FileTest.directory?(base)
return []
end
files = []
Dir.entries(base).reject { |e|
e == "." or e == ".."
}.each do |name|
path = File.join(base, name)
if obj = self[path]
obj[:check] = :all
files << obj
else
files << self.new(
:name => path, :check => :all
)
end
end
files
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.
if writeable
asuser = self.should(:owner)
end
end
return asuser
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
if bucket = self.bucket
case bucket
when String
if catalog and obj = catalog.resource(:filebucket, bucket)
self.bucket = obj.bucket
elsif bucket == "puppet"
obj = Puppet::Network::Client.client(:Dipper).new(
:Path => Puppet[:clientbucketdir]
)
self.bucket = obj
else
self.fail "Could not find filebucket '%s'" % bucket
end
when Puppet::Network::Client.client(:Dipper) # things are hunky-dorey
when Puppet::Type::Filebucket # things are hunky-dorey
self.bucket = bucket.bucket
else
self.fail "Invalid bucket type %s" % bucket.class
end
end
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
# Deal with backups.
def handlebackup(file = nil)
# let the path be specified
file ||= self[:path]
# if they specifically don't want a backup, then just say
# we're good
unless FileTest.exists?(file)
return true
end
unless self[:backup]
return true
end
case File.stat(file).ftype
when "directory"
if self[:recurse]
# we don't need to backup directories when recurse is on
return true
else
backup = self.bucket || self[:backup]
case backup
when Puppet::Network::Client.client(:Dipper)
notice "Recursively backing up to filebucket"
require 'find'
Find.find(self[:path]) do |f|
if File.file?(f)
sum = backup.backup(f)
self.notice "Filebucketed %s to %s with sum %s" %
[f, backup.name, sum]
end
end
return true
when String
newfile = file + backup
# Just move it, since it's a directory.
if FileTest.exists?(newfile)
remove_backup(newfile)
end
begin
bfile = file + backup
# Ruby 1.8.1 requires the 'preserve' addition, but
# later versions do not appear to require it.
FileUtils.cp_r(file, bfile, :preserve => true)
return true
rescue => detail
# since they said they want a backup, let's error out
# if we couldn't make one
self.fail "Could not back %s up: %s" %
[file, detail.message]
end
else
self.err "Invalid backup type %s" % backup.inspect
return false
end
end
when "file"
backup = self.bucket || self[:backup]
case backup
when Puppet::Network::Client.client(:Dipper)
sum = backup.backup(file)
self.notice "Filebucketed to %s with sum %s" %
[backup.name, sum]
return true
when String
newfile = file + backup
if FileTest.exists?(newfile)
remove_backup(newfile)
end
begin
# FIXME Shouldn't this just use a Puppet object with
# 'source' specified?
bfile = file + backup
# Ruby 1.8.1 requires the 'preserve' addition, but
# later versions do not appear to require it.
FileUtils.cp(file, bfile, :preserve => true)
return true
rescue => detail
# since they said they want a backup, let's error out
# if we couldn't make one
self.fail "Could not back %s up: %s" %
[file, detail.message]
end
else
self.err "Invalid backup type %s" % backup.inspect
return false
end
when "link"; return true
else
self.notice "Cannot backup files of type %s" % File.stat(file).ftype
return false
end
end
def initialize(hash)
# Used for caching clients
@clients = {}
super
# Get rid of any duplicate slashes, and remove any trailing slashes.
@title = @title.gsub(/\/+/, "/")
@title.sub!(/\/$/, "") unless @title == "/"
@stat = nil
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, :implicit => true).reject { |param, value| value.nil? }
+ 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
return 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 = {}
if self[:recurse] != :remote
children = recurse_local
end
if self[:target]
recurse_link(children)
elsif self[:source]
recurse_remote(children)
end
return children.values.sort { |a, b| a[:path] <=> b[:path] }
end
# A simple method for determining whether we should be recursing.
def recurse?
return false unless @parameters.include?(:recurse)
val = @parameters[:recurse].value
if val and (val == true or val == :remote)
return true
else
return false
end
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 = "%s/%s" % [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
# If we're purging resources, then delete any resource that isn't on the
# remote system.
if self.purge?
# Make a hash of all of the resources we found remotely -- all we need is the
# fast lookup, the values don't matter.
remotes = total.inject({}) { |hash, meta| hash[meta.relative_path] = true; hash }
children.each do |name, child|
unless remotes.include?(name)
child[:ensure] = :absent
end
end
end
children
end
def perform_recursion(path)
Puppet::FileServing::Metadata.search(path, :links => self[:links], :recurse => (self[:recurse] == :remote ? true : self[:recurse]), :recurselimit => self[:recurselimit], :ignore => self[:ignore])
end
# Remove the old backup.
def remove_backup(newfile)
if self.class.name == :file and self[:links] != :follow
method = :lstat
else
method = :stat
end
old = File.send(method, newfile).ftype
if old == "directory"
raise Puppet::Error,
"Will not remove directory backup %s; use a filebucket" %
newfile
end
info "Removing old backup of type %s" %
File.send(method, newfile).ftype
begin
File.unlink(newfile)
rescue => detail
puts detail.backtrace if Puppet[:trace]
self.err "Could not remove old backup: %s" % detail
return false
end
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 handlebackup
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 %s" % should
FileUtils.rmtree(self[:path])
else
notice "Not removing directory; use 'force' to override"
end
when "link", "file"
debug "Removing existing %s for replacement with %s" %
[s.ftype, should]
File.unlink(self[:path])
else
self.fail "Could not back up files of type %s" % s.ftype
end
expire
end
# a wrapper method to make sure the file exists before doing anything
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 true if s.ftype == "file"
return 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"
return 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
if obj[:target] == :notlink
obj.delete(:target)
end
obj
end
# Write out the file. Requires the content to be written,
# the property name for logging, and the checksum for validation.
def write(content, property, checksum = nil)
if validate = validate_checksum?
# Use the appropriate checksum type -- md5, md5lite, etc.
sumtype = property(:checksum).checktype
checksum ||= "{#{sumtype}}" + property(:checksum).send(sumtype, content)
end
remove_existing(:file)
use_temporary_file = (content.length != 0)
path = self[:path]
path += ".puppettmp" if use_temporary_file
mode = self.should(:mode) # might be nil
umask = mode ? 000 : 022
Puppet::Util.withumask(umask) do
File.open(path, File::CREAT|File::WRONLY|File::TRUNC, mode) { |f| f.print content }
end
# 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, checksum) if validate
File.rename(path, self[:path])
rescue => detail
self.err "Could not rename tmp %s for replacing: %s" % [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
# And then update our checksum, so the next run doesn't find it.
self.setchecksum(checksum)
end
# Should we validate the checksum of the file we're writing?
def validate_checksum?
if sumparam = @parameters[:checksum]
return sumparam.checktype.to_s !~ /time/
else
return false
end
end
private
# Make sure the file we wrote out is what we think it is.
def fail_if_checksum_is_wrong(path, checksum)
if checksum =~ /^\{(\w+)\}.+/
sumtype = $1
else
# This shouldn't happen, but if it happens to, it's nicer
# to just use a default sumtype than fail.
sumtype = "md5"
end
newsum = property(:checksum).getsum(sumtype, path)
return if newsum == checksum
self.fail "File written to disk did not match checksum; discarding changes (%s vs %s)" % [checksum, newsum]
end
# Override the parent method, because we don't want to generate changes
# when the file is missing and there is no 'ensure' state.
def propertychanges(currentvalues)
unless self.stat
found = false
([:ensure] + CREATORS).each do |prop|
if @parameters.include?(prop)
found = true
break
end
end
unless found
return []
end
end
super
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
self.stat(true)
currentvalue = thing.retrieve
unless thing.insync?(currentvalue)
thing.sync
end
end
end
end # Puppet::Type.type(:pfile)
# 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
end
diff --git a/spec/unit/resource.rb b/spec/unit/resource.rb
index 0fcf51b8b..141986f90 100755
--- a/spec/unit/resource.rb
+++ b/spec/unit/resource.rb
@@ -1,349 +1,349 @@
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../spec_helper'
require 'puppet/resource'
describe Puppet::Resource do
- [:catalog, :file, :line, :implicit].each do |attr|
+ [:catalog, :file, :line].each do |attr|
it "should have an #{attr} attribute" do
resource = Puppet::Resource.new("file", "/my/file")
resource.should respond_to(attr)
resource.should respond_to(attr.to_s + "=")
end
end
describe "when initializing" do
it "should require the type and title" do
lambda { Puppet::Resource.new }.should raise_error(ArgumentError)
end
it "should create a resource reference with its type and title" do
ref = Puppet::Resource::Reference.new("file", "/f")
Puppet::Resource::Reference.expects(:new).with("file", "/f").returns ref
Puppet::Resource.new("file", "/f")
end
it "should allow setting of parameters" do
Puppet::Resource.new("file", "/f", :noop => true)[:noop].should be_true
end
it "should tag itself with its type" do
Puppet::Resource.new("file", "/f").should be_tagged("file")
end
it "should tag itself with its title if the title is a valid tag" do
Puppet::Resource.new("file", "bar").should be_tagged("bar")
end
it "should not tag itself with its title if the title is a not valid tag" do
Puppet::Resource.new("file", "/bar").should_not be_tagged("/bar")
end
end
it "should use the resource reference to determine its type" do
ref = Puppet::Resource::Reference.new("file", "/f")
Puppet::Resource::Reference.expects(:new).returns ref
resource = Puppet::Resource.new("file", "/f")
ref.expects(:type).returns "mytype"
resource.type.should == "mytype"
end
it "should use its resource reference to determine its title" do
ref = Puppet::Resource::Reference.new("file", "/f")
Puppet::Resource::Reference.expects(:new).returns ref
resource = Puppet::Resource.new("file", "/f")
ref.expects(:title).returns "mytitle"
resource.title.should == "mytitle"
end
it "should use its resource reference to determine whether it is builtin" do
ref = Puppet::Resource::Reference.new("file", "/f")
Puppet::Resource::Reference.expects(:new).returns ref
resource = Puppet::Resource.new("file", "/f")
ref.expects(:builtin_type?).returns "yep"
resource.builtin_type?.should == "yep"
end
it "should call its builtin_type? method when 'builtin?' is called" do
resource = Puppet::Resource.new("file", "/f")
resource.expects(:builtin_type?).returns "foo"
resource.builtin?.should == "foo"
end
it "should use its resource reference to produce its canonical reference string" do
ref = Puppet::Resource::Reference.new("file", "/f")
Puppet::Resource::Reference.expects(:new).returns ref
resource = Puppet::Resource.new("file", "/f")
ref.expects(:to_s).returns "Foo[bar]"
resource.ref.should == "Foo[bar]"
end
it "should be taggable" do
Puppet::Resource.ancestors.should be_include(Puppet::Util::Tagging)
end
describe "when managing parameters" do
before do
@resource = Puppet::Resource.new("file", "/my/file")
end
it "should allow setting and retrieving of parameters" do
@resource[:foo] = "bar"
@resource[:foo].should == "bar"
end
it "should canonicalize retrieved parameter names to treat symbols and strings equivalently" do
@resource[:foo] = "bar"
@resource["foo"].should == "bar"
end
it "should canonicalize set parameter names to treat symbols and strings equivalently" do
@resource["foo"] = "bar"
@resource[:foo].should == "bar"
end
it "should set the namevar when asked to set the name" do
Puppet::Type.type(:file).stubs(:namevar).returns :myvar
@resource[:name] = "/foo"
@resource[:myvar].should == "/foo"
end
it "should return the namevar when asked to return the name" do
Puppet::Type.type(:file).stubs(:namevar).returns :myvar
@resource[:myvar] = "/foo"
@resource[:name].should == "/foo"
end
it "should be able to set the name for non-builtin types" do
resource = Puppet::Resource.new(:foo, "bar")
lambda { resource[:name] = "eh" }.should_not raise_error
end
it "should be able to return the name for non-builtin types" do
resource = Puppet::Resource.new(:foo, "bar")
resource[:name] = "eh"
resource[:name].should == "eh"
end
it "should be able to iterate over parameters" do
@resource[:foo] = "bar"
@resource[:fee] = "bare"
params = {}
@resource.each do |key, value|
params[key] = value
end
params.should == {:foo => "bar", :fee => "bare"}
end
it "should include Enumerable" do
@resource.class.ancestors.should be_include(Enumerable)
end
it "should have a method for testing whether a parameter is included" do
@resource[:foo] = "bar"
@resource.should be_has_key(:foo)
@resource.should_not be_has_key(:eh)
end
it "should have a method for providing the list of parameters" do
@resource[:foo] = "bar"
@resource[:bar] = "foo"
keys = @resource.keys
keys.should be_include(:foo)
keys.should be_include(:bar)
end
it "should have a method for providing the number of parameters" do
@resource[:foo] = "bar"
@resource.length.should == 1
end
it "should have a method for deleting parameters" do
@resource[:foo] = "bar"
@resource.delete(:foo)
@resource[:foo].should be_nil
end
it "should have a method for testing whether the parameter list is empty" do
@resource.should be_empty
@resource[:foo] = "bar"
@resource.should_not be_empty
end
it "should be able to produce a hash of all existing parameters" do
@resource[:foo] = "bar"
@resource[:fee] = "yay"
hash = @resource.to_hash
hash[:foo].should == "bar"
hash[:fee].should == "yay"
end
it "should not provide direct access to the internal parameters hash when producing a hash" do
hash = @resource.to_hash
hash[:foo] = "bar"
@resource[:foo].should be_nil
end
it "should use the title as the namevar to the hash if no namevar is present" do
Puppet::Type.type(:file).stubs(:namevar).returns :myvar
@resource.to_hash[:myvar].should == "/my/file"
end
it "should set :name to the title if :name is not present for non-builtin types" do
resource = Puppet::Resource.new :foo, "bar"
resource.to_hash[:name].should == "bar"
end
end
describe "when serializing" do
before do
@resource = Puppet::Resource.new("file", "/my/file")
@resource["one"] = "test"
@resource["two"] = "other"
end
it "should be able to be dumped to yaml" do
proc { YAML.dump(@resource) }.should_not raise_error
end
it "should produce an equivalent yaml object" do
text = YAML.dump(@resource)
newresource = YAML.load(text)
newresource.title.should == @resource.title
newresource.type.should == @resource.type
%w{one two}.each do |param|
newresource[param].should == @resource[param]
end
end
end
describe "when converting to a RAL resource" do
before do
@resource = Puppet::Resource.new("file", "/my/file")
@resource["one"] = "test"
@resource["two"] = "other"
end
it "should use the resource type's :create method to create the resource if the resource is of a builtin type" do
type = mock 'resource type'
type.expects(:new).with(@resource).returns(:myresource)
Puppet::Type.expects(:type).with(@resource.type).returns(type)
@resource.to_ral.should == :myresource
end
it "should convert to a component instance if the resource type is not of a builtin type" do
component = mock 'component type'
Puppet::Type::Component.expects(:new).with(@resource).returns "meh"
Puppet::Type.expects(:type).with(@resource.type).returns(nil)
@resource.to_ral.should == "meh"
end
end
it "should be able to convert itself to Puppet code" do
Puppet::Resource.new("one::two", "/my/file").should respond_to(:to_manifest)
end
describe "when converting to puppet code" do
before do
@resource = Puppet::Resource.new("one::two", "/my/file", :noop => true, :foo => %w{one two})
end
it "should print the type and title" do
@resource.to_manifest.should be_include("one::two { '/my/file':\n")
end
it "should print each parameter, with the value single-quoted" do
@resource.to_manifest.should be_include(" noop => 'true'")
end
it "should print array values appropriately" do
@resource.to_manifest.should be_include(" foo => ['one','two']")
end
end
it "should be able to convert itself to a TransObject instance" do
Puppet::Resource.new("one::two", "/my/file").should respond_to(:to_trans)
end
describe "when converting to a TransObject" do
describe "and the resource is not an instance of a builtin type" do
before do
@resource = Puppet::Resource.new("foo", "bar")
end
it "should return a simple TransBucket if it is not an instance of a builtin type" do
bucket = @resource.to_trans
bucket.should be_instance_of(Puppet::TransBucket)
bucket.type.should == @resource.type
bucket.name.should == @resource.title
end
it "should copy over the resource's file" do
@resource.file = "/foo/bar"
@resource.to_trans.file.should == "/foo/bar"
end
it "should copy over the resource's line" do
@resource.line = 50
@resource.to_trans.line.should == 50
end
end
describe "and the resource is an instance of a builtin type" do
before do
@resource = Puppet::Resource.new("file", "bar")
end
it "should return a TransObject if it is an instance of a builtin resource type" do
trans = @resource.to_trans
trans.should be_instance_of(Puppet::TransObject)
trans.type.should == "file"
trans.name.should == @resource.title
end
it "should copy over the resource's file" do
@resource.file = "/foo/bar"
@resource.to_trans.file.should == "/foo/bar"
end
it "should copy over the resource's line" do
@resource.line = 50
@resource.to_trans.line.should == 50
end
# Only TransObjects support tags, annoyingly
it "should copy over the resource's tags" do
@resource.tag "foo"
@resource.to_trans.tags.should == @resource.tags
end
it "should copy the resource's parameters into the transobject and convert the parameter name to a string" do
@resource[:foo] = "bar"
@resource.to_trans["foo"].should == "bar"
end
it "should be able to copy arrays of values" do
@resource[:foo] = %w{yay fee}
@resource.to_trans["foo"].should == %w{yay fee}
end
it "should reduce single-value arrays to just a value" do
@resource[:foo] = %w{yay}
@resource.to_trans["foo"].should == "yay"
end
it "should convert resource references into the backward-compatible form" do
@resource[:foo] = Puppet::Resource::Reference.new(:file, "/f")
@resource.to_trans["foo"].should == %w{file /f}
end
it "should convert resource references into the backward-compatible form even when within arrays" do
@resource[:foo] = ["a", Puppet::Resource::Reference.new(:file, "/f")]
@resource.to_trans["foo"].should == ["a", %w{file /f}]
end
end
end
end
diff --git a/spec/unit/resource/catalog.rb b/spec/unit/resource/catalog.rb
index f72162b39..6a5922e2e 100755
--- a/spec/unit/resource/catalog.rb
+++ b/spec/unit/resource/catalog.rb
@@ -1,872 +1,828 @@
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../../spec_helper'
describe Puppet::Resource::Catalog, "when compiling" do
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_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_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
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" do
it "should return extraction result as the method result" do
config = Puppet::Resource::Catalog.new("mynode")
config.expects(:extraction_format).returns(:whatever)
config.expects(:extract_to_whatever).returns(:result)
config.extract.should == :result
end
end
describe "when extracting transobjects" do
def mkscope
@parser = Puppet::Parser::Parser.new :Code => ""
@node = Puppet::Node.new("mynode")
@compiler = Puppet::Parser::Compiler.new(@node, @parser)
# XXX This is ridiculous.
@compiler.send(:evaluate_main)
@scope = @compiler.topscope
end
def mkresource(type, name)
Puppet::Parser::Resource.new(:type => type, :title => name, :source => @source, :scope => @scope)
end
it "should always create a TransBucket for the 'main' class" do
config = Puppet::Resource::Catalog.new("mynode")
@scope = mkscope
@source = mock 'source'
main = mkresource("class", :main)
config.add_vertex(main)
bucket = stub 'bucket', :file= => nil, :line= => nil, :classes= => nil
bucket.expects(:type=).with("Class")
bucket.expects(:name=).with(:main)
main.stubs(:builtin?).returns(false)
Puppet::TransBucket.expects(:new).returns bucket
config.extract_to_transportable.should equal(bucket)
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 "class", :main
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_to_transportable
# 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 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, '/topobject'
@middle = Puppet::Resource.new :class, 'middle'
@middleobject = Puppet::Resource.new :file, '/middleobject'
@bottom = Puppet::Resource.new :class, 'bottom'
@bottomobject = Puppet::Resource.new :file, '/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 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 ignore implicit resources that conflict with existing resources" do
- @catalog.add_resource(@one)
-
- @dupe.implicit = true
-
- yielded = []
- @catalog.add_resource(@dupe) { |r| yielded << r }
- yielded.should be_empty
- end
-
- it "should not set the catalog for ignored implicit resources" do
- @catalog.add_resource(@one)
-
- @dupe.implicit = true
-
- @catalog.add_resource(@dupe)
- @dupe.catalog.should be_nil
- end
-
- it "should log when implicit resources are ignored" do
- @catalog.add_resource(@one)
-
- @dupe.implicit = true
-
- @dupe.expects(:debug)
- @catalog.add_resource(@dupe)
- end
-
- it "should replace implicit resources if a conflicting explicit resource is added" do
- @catalog.add_resource(@one)
- @one.implicit = true
-
- proc { @catalog.add_resource(@dupe) }.should_not raise_error
- @catalog.resource(:notify, "one").should equal(@dupe)
- end
-
- it "should log when implicit resources are removed from the catalog" do
- @catalog.add_resource(@one)
- @one.implicit = true
-
- @one.expects(:debug)
- @catalog.add_resource(@dupe)
- 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 => "/something")
@catalog.add_resource(resource)
@catalog.resource(:file, "/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 => "/something", :title => "other", :content => "blah"
resource.expects(:isomorphic?).returns(true)
@catalog.add_resource(resource)
@catalog.resource(:file, "other").should equal(resource)
@catalog.resource(:file, "/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 => "/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.
if @catalog.resource(:file, resource.name)
raise "Aliased non-isomorphic resource"
end
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")
@catalog.retrieval_duration = Time.now
@transaction = mock 'transaction'
Puppet::Transaction.stubs(:new).returns(@transaction)
@transaction.stubs(:evaluate)
@transaction.stubs(:cleanup)
@transaction.stubs(:addtimes)
end
it "should create and evaluate a transaction" do
@transaction.expects(:evaluate)
@catalog.apply
end
it "should provide the catalog time to the transaction" do
@transaction.expects(:addtimes).with do |arg|
arg[:config_retrieval].should be_instance_of(Time)
true
end
@catalog.apply
end
it "should clean up the transaction" do
@transaction.expects :cleanup
@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 not being a host catalog" do
@catalog.host_config.should be_nil
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 remove resources created mid-transaction" do
args = {:name => "/yay", :ensure => :file}
resource = stub 'file', :ref => "File[/yay]", :catalog= => @catalog, :title => "/yay", :[] => "/yay"
@transaction = mock 'transaction'
Puppet::Transaction.stubs(:new).returns(@transaction)
@transaction.stubs(:evaluate)
@transaction.stubs(:cleanup)
@transaction.stubs(:addtimes)
Puppet::Type.type(:file).expects(:new).with(args).returns(resource)
resource.expects :remove
@catalog.apply do |trans|
@catalog.create_resource :file, args
@catalog.resource("File[/yay]").should equal(resource)
end
@catalog.resource("File[/yay]").should be_nil
end
it "should remove resources added mid-transaction" do
@transaction = mock 'transaction'
Puppet::Transaction.stubs(:new).returns(@transaction)
@transaction.stubs(:evaluate)
@transaction.stubs(:cleanup)
@transaction.stubs(:addtimes)
file = Puppet::Type.type(:file).new(:name => "/yay", :ensure => :file)
@catalog.apply do |trans|
@catalog.add_resource file
@catalog.resource("File[/yay]").should_not be_nil
end
@catalog.resource("File[/yay]").should be_nil
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 send a report if reporting is enabled" do
Puppet[:report] = true
@transaction.expects :send_report
@transaction.stubs :any_failed? => false
@catalog.apply
end
it "should send a report if report summaries are enabled" do
Puppet[:summarize] = true
@transaction.expects :send_report
@transaction.stubs :any_failed? => false
@catalog.apply
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
@transaction.expects(:send_report).never
@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 => "/one"
@two = @file.new :path => "/two"
@sub = @file.new :path => "/two/subdir"
@catalog.add_edge @compone, @one
@catalog.add_edge @comptwo, @two
@three = @file.new :path => "/three"
@four = @file.new :path => "/four", :require => "File[/three]"
@five = @file.new :path => "/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
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
@indirection = stub 'indirection', :name => :catalog
Puppet::Util::Cacher.expire
end
it "should redirect to the indirection for retrieval" do
Puppet::Resource::Catalog.stubs(:indirection).returns(@indirection)
@indirection.expects(:find)
Puppet::Resource::Catalog.find(:myconfig)
end
it "should default to the 'compiler' terminus" do
Puppet::Resource::Catalog.indirection.terminus_class.should == :compiler
end
after do
Puppet::Util::Cacher.expire
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
diff --git a/spec/unit/transaction.rb b/spec/unit/transaction.rb
index 60705c7fb..86ee02ce5 100755
--- a/spec/unit/transaction.rb
+++ b/spec/unit/transaction.rb
@@ -1,48 +1,82 @@
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../spec_helper'
require 'puppet/transaction'
describe Puppet::Transaction do
it "should match resources by name, not title, when prefetching" 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
+
+ describe "when generating resources" do
+ it "should finish all resources" do
+ generator = stub 'generator', :depthfirst? => true
+ resource = stub 'resource'
+
+ @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)
+ end
+
+ it "should skip generated resources that conflict with existing resources" do
+ generator = mock 'generator'
+ resource = stub 'resource'
+
+ @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
+
+ @transaction.generate_additional_resources(generator, :generate)
+ 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
end
diff --git a/spec/unit/type.rb b/spec/unit/type.rb
index 3523d4e8b..304eb4017 100755
--- a/spec/unit/type.rb
+++ b/spec/unit/type.rb
@@ -1,353 +1,353 @@
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../spec_helper'
describe Puppet::Type do
it "should include the Cacher module" do
Puppet::Type.ancestors.should be_include(Puppet::Util::Cacher)
end
it "should use its catalog as its expirer" do
catalog = Puppet::Resource::Catalog.new
resource = Puppet::Type.type(:mount).new(:name => "foo", :fstype => "bar", :pass => 1, :ensure => :present)
resource.catalog = catalog
resource.expirer.should equal(catalog)
end
it "should do nothing when asked to expire when it has no catalog" do
resource = Puppet::Type.type(:mount).new(:name => "foo", :fstype => "bar", :pass => 1, :ensure => :present)
lambda { resource.expire }.should_not raise_error
end
it "should be able to retrieve a property by name" do
resource = Puppet::Type.type(:mount).new(:name => "foo", :fstype => "bar", :pass => 1, :ensure => :present)
resource.property(:fstype).must be_instance_of(Puppet::Type.type(:mount).attrclass(:fstype))
end
it "should be able to retrieve a parameter by name" do
resource = Puppet::Type.type(:mount).new(:name => "foo", :fstype => "bar", :pass => 1, :ensure => :present)
resource.parameter(:name).must be_instance_of(Puppet::Type.type(:mount).attrclass(:name))
end
it "should be able to retrieve a property by name using the :parameter method" do
resource = Puppet::Type.type(:mount).new(:name => "foo", :fstype => "bar", :pass => 1, :ensure => :present)
resource.parameter(:fstype).must be_instance_of(Puppet::Type.type(:mount).attrclass(:fstype))
end
it "should have a method for setting default values for resources" do
Puppet::Type.type(:mount).new(:name => "foo").should respond_to(:set_default)
end
it "should do nothing for attributes that have no defaults and no specified value" do
Puppet::Type.type(:mount).new(:name => "foo").parameter(:noop).should be_nil
end
describe "when initializing" do
describe "and passed a TransObject" do
it "should fail" do
trans = Puppet::TransObject.new("/foo", :mount)
lambda { Puppet::Type.type(:mount).new(trans) }.should raise_error(Puppet::DevError)
end
end
describe "and passed a Puppet::Resource instance" do
it "should set its title to the title of the resource if the resource type is equal to the current type" do
resource = Puppet::Resource.new(:mount, "/foo", :name => "/other")
Puppet::Type.type(:mount).new(resource).title.should == "/foo"
end
it "should set its title to the resource reference if the resource type is not equal to the current type" do
resource = Puppet::Resource.new(:user, "foo")
Puppet::Type.type(:mount).new(resource).title.should == "User[foo]"
end
- [:line, :file, :catalog, :implicit].each do |param|
+ [:line, :file, :catalog].each do |param|
it "should copy '#{param}' from the resource if present" do
resource = Puppet::Resource.new(:mount, "/foo")
resource.send(param.to_s + "=", "foo")
resource.send(param.to_s + "=", "foo")
Puppet::Type.type(:mount).new(resource).send(param).should == "foo"
end
end
it "should copy any tags from the resource" do
resource = Puppet::Resource.new(:mount, "/foo")
resource.tag "one", "two"
tags = Puppet::Type.type(:mount).new(resource).tags
tags.should be_include("one")
tags.should be_include("two")
end
it "should copy the resource's parameters as its own" do
resource = Puppet::Resource.new(:mount, "/foo", :atboot => true, :fstype => "boo")
params = Puppet::Type.type(:mount).new(resource).to_hash
params[:fstype].should == "boo"
params[:atboot].should == true
end
end
describe "and passed a Hash" do
it "should extract the title from the hash" do
Puppet::Type.type(:mount).new(:title => "/yay").title.should == "/yay"
end
it "should work when hash keys are provided as strings" do
Puppet::Type.type(:mount).new("title" => "/yay").title.should == "/yay"
end
it "should work when hash keys are provided as symbols" do
Puppet::Type.type(:mount).new(:title => "/yay").title.should == "/yay"
end
it "should use the name from the hash as the title if no explicit title is provided" do
Puppet::Type.type(:mount).new(:name => "/yay").title.should == "/yay"
end
it "should use the Resource Type's namevar to determine how to find the name in the hash" do
Puppet::Type.type(:file).new(:path => "/yay").title.should == "/yay"
end
it "should fail if the namevar is not equal to :name and both :name and the namevar are provided" do
lambda { Puppet::Type.type(:file).new(:path => "/yay", :name => "/foo") }.should raise_error(Puppet::Error)
@type.stubs(:namevar).returns :myname
end
- [:catalog, :implicit].each do |param|
+ [:catalog].each do |param|
it "should extract '#{param}' from the hash if present" do
Puppet::Type.type(:mount).new(:name => "/yay", param => "foo").send(param).should == "foo"
end
end
it "should use any remaining hash keys as its parameters" do
resource = Puppet::Type.type(:mount).new(:title => "/foo", :catalog => "foo", :atboot => true, :fstype => "boo")
resource[:fstype].must == "boo"
resource[:atboot].must == true
end
end
it "should fail if any invalid attributes have been provided" do
lambda { Puppet::Type.type(:mount).new(:title => "/foo", :nosuchattr => "whatever") }.should raise_error(Puppet::Error)
end
it "should set its name to the resource's title if the resource does not have a :name or namevar parameter set" do
resource = Puppet::Resource.new(:mount, "/foo")
Puppet::Type.type(:mount).new(resource).name.should == "/foo"
end
it "should fail if no title, name, or namevar are provided" do
lambda { Puppet::Type.type(:file).new(:atboot => true) }.should raise_error(Puppet::Error)
end
it "should set the attributes in the order returned by the class's :allattrs method" do
Puppet::Type.type(:mount).stubs(:allattrs).returns([:name, :atboot, :noop])
resource = Puppet::Resource.new(:mount, "/foo", :name => "myname", :atboot => "myboot", :noop => "whatever")
set = []
Puppet::Type.type(:mount).any_instance.stubs(:newattr).with do |param, hash|
set << param
true
end
Puppet::Type.type(:mount).new(resource)
set[-1].should == :noop
set[-2].should == :atboot
end
it "should always set the name and then default provider before anything else" do
Puppet::Type.type(:mount).stubs(:allattrs).returns([:provider, :name, :atboot])
resource = Puppet::Resource.new(:mount, "/foo", :name => "myname", :atboot => "myboot")
set = []
Puppet::Type.type(:mount).any_instance.stubs(:newattr).with do |param, hash|
set << param
true
end
Puppet::Type.type(:mount).new(resource)
set[0].should == :name
set[1].should == :provider
end
# This one is really hard to test :/
it "should each default immediately if no value is provided" do
defaults = []
Puppet::Type.type(:package).any_instance.stubs(:set_default).with { |value| defaults << value; true }
Puppet::Type.type(:package).new :name => "whatever"
defaults[0].should == :provider
end
it "should retain a copy of the originally provided parameters" do
Puppet::Type.type(:mount).new(:name => "foo", :atboot => true, :noop => false).original_parameters.should == {:atboot => true, :noop => false}
end
it "should delete the name via the namevar from the originally provided parameters" do
Puppet::Type.type(:file).new(:name => "/foo").original_parameters[:path].should be_nil
end
end
it "should have a class method for converting a hash into a Puppet::Resource instance" do
Puppet::Type.type(:mount).must respond_to(:hash2resource)
end
describe "when converting a hash to a Puppet::Resource instance" do
before do
@type = Puppet::Type.type(:mount)
end
it "should treat a :title key as the title of the resource" do
@type.hash2resource(:name => "/foo", :title => "foo").title.should == "foo"
end
it "should use the name from the hash as the title if no explicit title is provided" do
@type.hash2resource(:name => "foo").title.should == "foo"
end
it "should use the Resource Type's namevar to determine how to find the name in the hash" do
@type.stubs(:namevar).returns :myname
@type.hash2resource(:myname => "foo").title.should == "foo"
end
it "should fail if the namevar is not equal to :name and both :name and the namevar are provided" do
@type.stubs(:namevar).returns :myname
lambda { @type.hash2resource(:myname => "foo", :name => 'bar') }.should raise_error(Puppet::Error)
end
- [:catalog, :implicit].each do |attr|
+ [:catalog].each do |attr|
it "should use any provided #{attr}" do
@type.hash2resource(:name => "foo", attr => "eh").send(attr).should == "eh"
end
end
it "should set all provided parameters on the resource" do
@type.hash2resource(:name => "foo", :fstype => "boo", :boot => "fee").to_hash.should == {:name => "foo", :fstype => "boo", :boot => "fee"}
end
it "should not set the title as a parameter on the resource" do
@type.hash2resource(:name => "foo", :title => "eh")[:title].should be_nil
end
it "should not set the catalog as a parameter on the resource" do
@type.hash2resource(:name => "foo", :catalog => "eh")[:catalog].should be_nil
end
it "should treat hash keys equivalently whether provided as strings or symbols" do
resource = @type.hash2resource("name" => "foo", "title" => "eh", "fstype" => "boo")
resource.title.should == "eh"
resource[:name].should == "foo"
resource[:fstype].should == "boo"
end
end
describe "when retrieving current property values" do
# Use 'mount' as an example, because it doesn't override 'retrieve'
before do
@resource = Puppet::Type.type(:mount).new(:name => "foo", :fstype => "bar", :pass => 1, :ensure => :present)
@properties = {}
end
it "should return a hash containing values for all set properties" do
values = @resource.retrieve
[@resource.property(:fstype), @resource.property(:pass)].each { |property| values.should be_include(property) }
end
it "should not call retrieve on non-ensure properties if the resource is absent" do
@resource.property(:ensure).expects(:retrieve).returns :absent
@resource.property(:fstype).expects(:retrieve).never
@resource.retrieve[@resource.property(:fstype)]
end
it "should set all values to :absent if the resource is absent" do
@resource.property(:ensure).expects(:retrieve).returns :absent
@resource.retrieve[@resource.property(:fstype)].should == :absent
end
it "should include the result of retrieving each property's current value if the resource is present" do
@resource.property(:ensure).expects(:retrieve).returns :present
@resource.property(:fstype).expects(:retrieve).returns 15
@resource.retrieve[@resource.property(:fstype)].should == 15
end
end
describe "when in a catalog" do
before do
@catalog = Puppet::Resource::Catalog.new
@container = Puppet::Type.type(:component).new(:name => "container")
@one = Puppet::Type.type(:file).new(:path => "/file/one")
@two = Puppet::Type.type(:file).new(:path => "/file/two")
@catalog.add_resource @container
@catalog.add_resource @one
@catalog.add_resource @two
@catalog.add_edge @container, @one
@catalog.add_edge @container, @two
end
it "should have no parent if there is no in edge" do
@container.parent.should be_nil
end
it "should set its parent to its in edge" do
@one.parent.ref.should == @container.ref
end
after do
@catalog.clear(true)
end
end
describe "when managing relationships" do
end
end
describe Puppet::Type::RelationshipMetaparam do
it "should be a subclass of Puppet::Parameter" do
Puppet::Type::RelationshipMetaparam.superclass.should equal(Puppet::Parameter)
end
it "should be able to produce a list of subclasses" do
Puppet::Type::RelationshipMetaparam.should respond_to(:subclasses)
end
describe "when munging relationships" do
before do
@resource = Puppet::Type.type(:mount).new :name => "/foo"
@metaparam = Puppet::Type.metaparamclass(:require).new :resource => @resource
end
it "should accept Puppet::Resource::Reference instances" do
ref = Puppet::Resource::Reference.new(:file, "/foo")
@metaparam.munge(ref)[0].should equal(ref)
end
it "should turn any string into a Puppet::Resource::Reference" do
@metaparam.munge("File[/ref]")[0].should be_instance_of(Puppet::Resource::Reference)
end
end
it "should be able to validate relationships" do
Puppet::Type.metaparamclass(:require).new(:resource => mock("resource")).should respond_to(:validate_relationship)
end
it "should fail if any specified resource is not found in the catalog" do
catalog = mock 'catalog'
resource = stub 'resource', :catalog => catalog, :ref => "resource"
param = Puppet::Type.metaparamclass(:require).new(:resource => resource, :value => %w{Foo[bar] Class[test]})
catalog.expects(:resource).with("Foo[bar]").returns "something"
catalog.expects(:resource).with("Class[test]").returns nil
param.expects(:fail).with { |string| string.include?("Class[test]") }
param.validate_relationship
end
end
diff --git a/spec/unit/type/file.rb b/spec/unit/type/file.rb
index 094596966..1c6976440 100755
--- a/spec/unit/type/file.rb
+++ b/spec/unit/type/file.rb
@@ -1,655 +1,651 @@
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../../spec_helper'
describe Puppet::Type.type(:file) do
before do
Puppet.settings.stubs(:use)
@path = Tempfile.new("puppetspec")
pathname = @path.path
@path.close!()
@path = pathname
@file = Puppet::Type::File.new(:name => @path)
@catalog = mock 'catalog'
@catalog.stub_everything
@file.catalog = @catalog
end
it "should have a method for determining if the file is present" do
@file.must respond_to(:exist?)
end
it "should be considered existent if it can be stat'ed" do
@file.expects(:stat).returns mock('stat')
@file.must be_exist
end
it "should be considered nonexistent if it can not be stat'ed" do
@file.expects(:stat).returns nil
@file.must_not be_exist
end
it "should have a method for determining if the file should be a normal file" do
@file.must respond_to(:should_be_file?)
end
it "should be a file if :ensure is set to :file" do
@file[:ensure] = :file
@file.must be_should_be_file
end
it "should be a file if :ensure is set to :present and the file exists as a normal file" do
@file.stubs(:stat).returns(mock('stat', :ftype => "file"))
@file[:ensure] = :present
@file.must be_should_be_file
end
it "should not be a file if :ensure is set to something other than :file" do
@file[:ensure] = :directory
@file.must_not be_should_be_file
end
it "should not be a file if :ensure is set to :present and the file exists but is not a normal file" do
@file.stubs(:stat).returns(mock('stat', :ftype => "directory"))
@file[:ensure] = :present
@file.must_not be_should_be_file
end
it "should be a file if :ensure is not set and :content is" do
@file[:content] = "foo"
@file.must be_should_be_file
end
it "should be a file if neither :ensure nor :content is set but the file exists as a normal file" do
@file.stubs(:stat).returns(mock("stat", :ftype => "file"))
@file.must be_should_be_file
end
it "should not be a file if neither :ensure nor :content is set but the file exists but not as a normal file" do
@file.stubs(:stat).returns(mock("stat", :ftype => "directory"))
@file.must_not be_should_be_file
end
describe "when validating attributes" do
%w{path backup recurse recurselimit source replace force ignore links purge sourceselect}.each do |attr|
it "should have a '#{attr}' parameter" do
Puppet::Type.type(:file).attrtype(attr.intern).should == :param
end
end
%w{checksum content target ensure owner group mode type}.each do |attr|
it "should have a '#{attr}' property" do
Puppet::Type.type(:file).attrtype(attr.intern).should == :property
end
end
it "should have its 'path' attribute set as its namevar" do
Puppet::Type.type(:file).namevar.should == :path
end
end
describe "when managing links" do
require 'puppettest/support/assertions'
include PuppetTest
require 'tempfile'
before do
@basedir = tempfile
Dir.mkdir(@basedir)
@file = File.join(@basedir, "file")
@link = File.join(@basedir, "link")
File.open(@file, "w", 0644) { |f| f.puts "yayness"; f.flush }
File.symlink(@file, @link)
@resource = Puppet::Type.type(:file).new(
:path => @link,
:mode => "755"
)
@catalog = Puppet::Resource::Catalog.new
@catalog.add_resource @resource
end
after do
remove_tmp_files
end
it "should default to managing the link" do
@catalog.apply
# I convert them to strings so they display correctly if there's an error.
("%o" % (File.stat(@file).mode & 007777)).should == "%o" % 0644
end
it "should be able to follow links" do
@resource[:links] = :follow
@catalog.apply
("%o" % (File.stat(@file).mode & 007777)).should == "%o" % 0755
end
end
it "should be able to retrieve a stat instance for the file it is managing" do
Puppet::Type.type(:file).new(:path => "/foo/bar", :source => "/bar/foo").should respond_to(:stat)
end
describe "when stat'ing its file" do
before do
@resource = Puppet::Type.type(:file).new(:path => "/foo/bar")
@resource[:links] = :manage # so we always use :lstat
end
it "should use :stat if it is following links" do
@resource[:links] = :follow
File.expects(:stat)
@resource.stat
end
it "should use :lstat if is it not following links" do
@resource[:links] = :manage
File.expects(:lstat)
@resource.stat
end
it "should stat the path of the file" do
File.expects(:lstat).with("/foo/bar")
@resource.stat
end
# This only happens in testing.
it "should return nil if the stat does not exist" do
File.expects(:lstat).returns nil
@resource.stat.should be_nil
end
it "should return nil if the file does not exist" do
File.expects(:lstat).raises(Errno::ENOENT)
@resource.stat.should be_nil
end
it "should return nil if the file cannot be stat'ed" do
File.expects(:lstat).raises(Errno::EACCES)
@resource.stat.should be_nil
end
it "should return the stat instance" do
File.expects(:lstat).returns "mystat"
@resource.stat.should == "mystat"
end
it "should cache the stat instance if it has a catalog and is applying" do
stat = mock 'stat'
File.expects(:lstat).returns stat
catalog = Puppet::Resource::Catalog.new
@resource.catalog = catalog
catalog.stubs(:applying?).returns true
@resource.stat.should equal(@resource.stat)
end
end
describe "when flushing" do
it "should flush all properties that respond to :flush" do
@resource = Puppet::Type.type(:file).new(:path => "/foo/bar", :source => "/bar/foo")
@resource.parameter(:source).expects(:flush)
@resource.flush
end
it "should reset its stat reference" do
@resource = Puppet::Type.type(:file).new(:path => "/foo/bar")
File.expects(:lstat).times(2).returns("stat1").then.returns("stat2")
@resource.stat.should == "stat1"
@resource.flush
@resource.stat.should == "stat2"
end
end
it "should have a method for performing recursion" do
@file.must respond_to(:perform_recursion)
end
describe "when executing a recursive search" do
it "should use Metadata to do its recursion" do
Puppet::FileServing::Metadata.expects(:search)
@file.perform_recursion(@file[:path])
end
it "should use the provided path as the key to the search" do
Puppet::FileServing::Metadata.expects(:search).with { |key, options| key == "/foo" }
@file.perform_recursion("/foo")
end
it "should return the results of the metadata search" do
Puppet::FileServing::Metadata.expects(:search).returns "foobar"
@file.perform_recursion(@file[:path]).should == "foobar"
end
it "should pass its recursion value to the search" do
@file[:recurse] = true
Puppet::FileServing::Metadata.expects(:search).with { |key, options| options[:recurse] == true }
@file.perform_recursion(@file[:path])
end
it "should pass true if recursion is remote" do
@file[:recurse] = :remote
Puppet::FileServing::Metadata.expects(:search).with { |key, options| options[:recurse] == true }
@file.perform_recursion(@file[:path])
end
it "should pass its recursion limit value to the search" do
@file[:recurselimit] = 10
Puppet::FileServing::Metadata.expects(:search).with { |key, options| options[:recurselimit] == 10 }
@file.perform_recursion(@file[:path])
end
it "should configure the search to ignore or manage links" do
@file[:links] = :manage
Puppet::FileServing::Metadata.expects(:search).with { |key, options| options[:links] == :manage }
@file.perform_recursion(@file[:path])
end
it "should pass its 'ignore' setting to the search if it has one" do
@file[:ignore] = %w{.svn CVS}
Puppet::FileServing::Metadata.expects(:search).with { |key, options| options[:ignore] == %w{.svn CVS} }
@file.perform_recursion(@file[:path])
end
end
it "should have a method for performing local recursion" do
@file.must respond_to(:recurse_local)
end
describe "when doing local recursion" do
before do
@metadata = stub 'metadata', :relative_path => "my/file"
end
it "should pass its path to the :perform_recursion method" do
@file.expects(:perform_recursion).with(@file[:path]).returns [@metadata]
@file.stubs(:newchild)
@file.recurse_local
end
it "should return an empty hash if the recursion returns nothing" do
@file.expects(:perform_recursion).returns nil
@file.recurse_local.should == {}
end
it "should create a new child resource with each generated metadata instance's relative path" do
@file.expects(:perform_recursion).returns [@metadata]
@file.expects(:newchild).with(@metadata.relative_path).returns "fiebar"
@file.recurse_local
end
it "should not create a new child resource for the '.' directory" do
@metadata.stubs(:relative_path).returns "."
@file.expects(:perform_recursion).returns [@metadata]
@file.expects(:newchild).never
@file.recurse_local
end
it "should return a hash of the created resources with the relative paths as the hash keys" do
@file.expects(:perform_recursion).returns [@metadata]
@file.expects(:newchild).with("my/file").returns "fiebar"
@file.recurse_local.should == {"my/file" => "fiebar"}
end
end
it "should have a method for performing link recursion" do
@file.must respond_to(:recurse_link)
end
describe "when doing link recursion" do
before do
@first = stub 'first', :relative_path => "first", :full_path => "/my/first", :ftype => "directory"
@second = stub 'second', :relative_path => "second", :full_path => "/my/second", :ftype => "file"
@resource = stub 'file', :[]= => nil
end
it "should pass its target to the :perform_recursion method" do
@file[:target] = "mylinks"
@file.expects(:perform_recursion).with("mylinks").returns [@first]
@file.stubs(:newchild).returns @resource
@file.recurse_link({})
end
it "should ignore the recursively-found '.' file and configure the top-level file to create a directory" do
@first.stubs(:relative_path).returns "."
@file[:target] = "mylinks"
@file.expects(:perform_recursion).with("mylinks").returns [@first]
@file.stubs(:newchild).never
@file.expects(:[]=).with(:ensure, :directory)
@file.recurse_link({})
end
it "should create a new child resource for each generated metadata instance's relative path that doesn't already exist in the children hash" do
@file.expects(:perform_recursion).returns [@first, @second]
@file.expects(:newchild).with(@first.relative_path).returns @resource
@file.recurse_link("second" => @resource)
end
it "should not create a new child resource for paths that already exist in the children hash" do
@file.expects(:perform_recursion).returns [@first]
@file.expects(:newchild).never
@file.recurse_link("first" => @resource)
end
it "should set the target to the full path of discovered file and set :ensure to :link if the file is not a directory" do
file = stub 'file'
file.expects(:[]=).with(:target, "/my/second")
file.expects(:[]=).with(:ensure, :link)
@file.stubs(:perform_recursion).returns [@first, @second]
@file.recurse_link("first" => @resource, "second" => file)
end
it "should :ensure to :directory if the file is a directory" do
file = stub 'file'
file.expects(:[]=).with(:ensure, :directory)
@file.stubs(:perform_recursion).returns [@first, @second]
@file.recurse_link("first" => file, "second" => @resource)
end
it "should return a hash with both created and existing resources with the relative paths as the hash keys" do
file = stub 'file', :[]= => nil
@file.expects(:perform_recursion).returns [@first, @second]
@file.stubs(:newchild).returns file
@file.recurse_link("second" => @resource).should == {"second" => @resource, "first" => file}
end
end
it "should have a method for performing remote recursion" do
@file.must respond_to(:recurse_remote)
end
describe "when doing remote recursion" do
before do
@file[:source] = "puppet://foo/bar"
@first = Puppet::FileServing::Metadata.new("/my", :relative_path => "first")
@second = Puppet::FileServing::Metadata.new("/my", :relative_path => "second")
@first.stubs(:ftype).returns "directory"
@second.stubs(:ftype).returns "directory"
@parameter = stub 'property', :metadata= => nil
@resource = stub 'file', :[]= => nil, :parameter => @parameter
end
it "should pass its source to the :perform_recursion method" do
data = Puppet::FileServing::Metadata.new("/whatever", :relative_path => "foobar")
@file.expects(:perform_recursion).with("puppet://foo/bar").returns [data]
@file.stubs(:newchild).returns @resource
@file.recurse_remote({})
end
it "should not recurse when the remote file is not a directory" do
data = Puppet::FileServing::Metadata.new("/whatever", :relative_path => ".")
data.stubs(:ftype).returns "file"
@file.expects(:perform_recursion).with("puppet://foo/bar").returns [data]
@file.expects(:newchild).never
@file.recurse_remote({})
end
it "should set the source of each returned file to the searched-for URI plus the found relative path" do
@first.expects(:source=).with File.join("puppet://foo/bar", @first.relative_path)
@file.expects(:perform_recursion).returns [@first]
@file.stubs(:newchild).returns @resource
@file.recurse_remote({})
end
it "should create a new resource for any relative file paths that do not already have a resource" do
@file.stubs(:perform_recursion).returns [@first]
@file.expects(:newchild).with("first").returns @resource
@file.recurse_remote({}).should == {"first" => @resource}
end
it "should not create a new resource for any relative file paths that do already have a resource" do
@file.stubs(:perform_recursion).returns [@first]
@file.expects(:newchild).never
@file.recurse_remote("first" => @resource)
end
it "should set the source of each resource to the source of the metadata" do
@file.stubs(:perform_recursion).returns [@first]
@resource.stubs(:[]=)
@resource.expects(:[]=).with(:source, File.join("puppet://foo/bar", @first.relative_path))
@file.recurse_remote("first" => @resource)
end
# LAK:FIXME This is a bug, but I can't think of a fix for it. Fortunately it's already
# filed, and when it's fixed, we'll just fix the whole flow.
it "should set the checksum type to :md5 if the remote file is a file" do
@first.stubs(:ftype).returns "file"
@file.stubs(:perform_recursion).returns [@first]
@resource.stubs(:[]=)
@resource.expects(:[]=).with(:checksum, :md5)
@file.recurse_remote("first" => @resource)
end
it "should store the metadata in the source property for each resource so the source does not have to requery the metadata" do
@file.stubs(:perform_recursion).returns [@first]
@resource.expects(:parameter).with(:source).returns @parameter
@parameter.expects(:metadata=).with(@first)
@file.recurse_remote("first" => @resource)
end
it "should not create a new resource for the '.' file" do
@first.stubs(:relative_path).returns "."
@file.stubs(:perform_recursion).returns [@first]
@file.expects(:newchild).never
@file.recurse_remote({})
end
it "should store the metadata in the main file's source property if the relative path is '.'" do
@first.stubs(:relative_path).returns "."
@file.stubs(:perform_recursion).returns [@first]
@file.parameter(:source).expects(:metadata=).with @first
@file.recurse_remote("first" => @resource)
end
describe "and purging is enabled" do
before do
@file[:purge] = true
end
it "should configure each file not on the remote system to be removed" do
@file.stubs(:perform_recursion).returns [@second]
@resource.expects(:[]=).with(:ensure, :absent)
@file.expects(:newchild).returns stub('secondfile', :[]= => nil, :parameter => @parameter)
@file.recurse_remote("first" => @resource)
end
end
describe "and multiple sources are provided" do
describe "and :sourceselect is set to :first" do
it "should create file instances for the results for the first source to return any values" do
data = Puppet::FileServing::Metadata.new("/whatever", :relative_path => "foobar")
@file[:source] = %w{/one /two /three /four}
@file.expects(:perform_recursion).with("/one").returns nil
@file.expects(:perform_recursion).with("/two").returns []
@file.expects(:perform_recursion).with("/three").returns [data]
@file.expects(:perform_recursion).with("/four").never
@file.expects(:newchild).with("foobar").returns @resource
@file.recurse_remote({})
end
end
describe "and :sourceselect is set to :all" do
before do
@file[:sourceselect] = :all
end
it "should return every found file that is not in a previous source" do
klass = Puppet::FileServing::Metadata
@file[:source] = %w{/one /two /three /four}
@file.stubs(:newchild).returns @resource
one = [klass.new("/one", :relative_path => "a")]
@file.expects(:perform_recursion).with("/one").returns one
@file.expects(:newchild).with("a").returns @resource
two = [klass.new("/two", :relative_path => "a"), klass.new("/two", :relative_path => "b")]
@file.expects(:perform_recursion).with("/two").returns two
@file.expects(:newchild).with("b").returns @resource
three = [klass.new("/three", :relative_path => "a"), klass.new("/three", :relative_path => "c")]
@file.expects(:perform_recursion).with("/three").returns three
@file.expects(:newchild).with("c").returns @resource
@file.expects(:perform_recursion).with("/four").returns []
@file.recurse_remote({})
end
end
end
end
describe "when returning resources with :eval_generate" do
before do
@catalog = mock 'catalog'
@catalog.stub_everything
@graph = stub 'graph', :add_edge => nil
@catalog.stubs(:relationship_graph).returns @graph
@file.catalog = @catalog
@file[:recurse] = true
end
it "should recurse if recursion is enabled" do
resource = stub('resource', :[] => "resource")
@file.expects(:recurse?).returns true
@file.expects(:recurse).returns [resource]
@file.eval_generate.should == [resource]
end
it "should not recurse if recursion is disabled" do
@file.expects(:recurse?).returns false
@file.expects(:recurse).never
@file.eval_generate.should == []
end
it "should return each resource found through recursion" do
foo = stub 'foo', :[] => "/foo"
bar = stub 'bar', :[] => "/bar"
bar2 = stub 'bar2', :[] => "/bar"
@file.expects(:recurse).returns [foo, bar]
@file.eval_generate.should == [foo, bar]
end
end
describe "when recursing" do
before do
@file[:recurse] = true
@metadata = Puppet::FileServing::Metadata
end
describe "and a source is set" do
before { @file[:source] = "/my/source" }
it "should pass the already-discovered resources to recurse_remote" do
@file.stubs(:recurse_local).returns(:foo => "bar")
@file.expects(:recurse_remote).with(:foo => "bar").returns []
@file.recurse
end
end
describe "and a target is set" do
before { @file[:target] = "/link/target" }
it "should use recurse_link" do
@file.stubs(:recurse_local).returns(:foo => "bar")
@file.expects(:recurse_link).with(:foo => "bar").returns []
@file.recurse
end
end
it "should use recurse_local if recurse is not remote" do
@file.expects(:recurse_local).returns({})
@file.recurse
end
it "should not use recurse_local if recurse remote" do
@file[:recurse] = :remote
@file.expects(:recurse_local).never
@file.recurse
end
it "should return the generated resources as an array sorted by file path" do
one = stub 'one', :[] => "/one"
two = stub 'two', :[] => "/one/two"
three = stub 'three', :[] => "/three"
@file.expects(:recurse_local).returns(:one => one, :two => two, :three => three)
@file.recurse.should == [one, two, three]
end
describe "and making a new child resource" do
- it "should create an implicit resource using the provided relative path joined with the file's path" do
- @file.newchild("my/path").should be_implicit
- end
-
it "should not copy the parent resource's parent" do
Puppet::Type.type(:file).expects(:new).with { |options| ! options.include?(:parent) }
@file.newchild("my/path")
end
{:recurse => true, :target => "/foo/bar", :ensure => :present, :alias => "yay", :source => "/foo/bar"}.each do |param, value|
it "should not pass on #{param} to the sub resource" do
@file = Puppet::Type::File.new(:name => @path, param => value, :catalog => @catalog)
@file.class.expects(:new).with { |params| params[param].nil? }
@file.newchild("sub/file")
end
end
it "should copy all of the parent resource's 'should' values that were set at initialization" do
file = @file.class.new(:path => "/foo/bar", :owner => "root", :group => "wheel")
@catalog.add_resource(file)
file.class.expects(:new).with { |options| options[:owner] == "root" and options[:group] == "wheel" }
file.newchild("my/path")
end
it "should not copy default values to the new child" do
@file.class.expects(:new).with { |params| params[:backup].nil? }
@file.newchild("my/path")
end
it "should not copy values to the child which were set by the source" do
@file[:source] = "/foo/bar"
metadata = stub 'metadata', :owner => "root", :group => "root", :mode => 0755, :ftype => "file", :checksum => "{md5}whatever"
@file.parameter(:source).stubs(:metadata).returns metadata
@file.parameter(:source).copy_source_values
@file.class.expects(:new).with { |params| params[:group].nil? }
@file.newchild("my/path")
end
end
end
end