diff --git a/lib/puppet/parser/compiler.rb b/lib/puppet/parser/compiler.rb
index 85980722c..a901c0dd6 100644
--- a/lib/puppet/parser/compiler.rb
+++ b/lib/puppet/parser/compiler.rb
@@ -1,493 +1,476 @@
# Created by Luke A. Kanies on 2007-08-13.
# Copyright (c) 2007. All rights reserved.
require 'puppet/node'
require 'puppet/resource/catalog'
require 'puppet/util/errors'
require 'puppet/resource/type_collection_helper'
# Maintain a graph of scopes, along with a bunch of data
# about the individual catalog we're compiling.
class Puppet::Parser::Compiler
include Puppet::Util
include Puppet::Util::Errors
include Puppet::Resource::TypeCollectionHelper
def self.compile(node)
new(node).compile.to_resource
rescue => detail
puts detail.backtrace if Puppet[:trace]
raise Puppet::Error, "#{detail} on node #{node.name}"
end
attr_reader :node, :facts, :collections, :catalog, :node_scope, :resources, :relationships
# Add a collection to the global list.
def add_collection(coll)
@collections << coll
end
def add_relationship(dep)
@relationships << dep
end
# Store a resource override.
def add_override(override)
# If possible, merge the override in immediately.
if resource = @catalog.resource(override.ref)
resource.merge(override)
else
# Otherwise, store the override for later; these
# get evaluated in Resource#finish.
@resource_overrides[override.ref] << override
end
end
# Store a resource in our resource table.
def add_resource(scope, resource)
@resources << resource
# Note that this will fail if the resource is not unique.
@catalog.add_resource(resource)
set_container_resource(scope, resource)
end
# Add our container edge. If we're a class, then we get treated specially - we can
# control the stage that the class is applied in. Otherwise, we just
# get added to our parent container.
def set_container_resource(scope, resource)
return if resource.type.to_s.downcase == "stage"
if resource.type.to_s.downcase != "class"
raise ArgumentError, "Only classes can set 'stage'; normal resources like #{resource} cannot change run stage" if resource[:stage]
return @catalog.add_edge(scope.resource, resource)
end
unless stage = @catalog.resource(:stage, resource[:stage] || :main)
raise ArgumentError, "Could not find stage #{resource[:stage] || :main} specified by #{resource}"
end
@catalog.add_edge(stage, resource)
end
private :set_container_resource
# Do we use nodes found in the code, vs. the external node sources?
def ast_nodes?
known_resource_types.nodes?
end
# Store the fact that we've evaluated a class
def add_class(name)
@catalog.add_class(name) unless name == ""
end
# Return a list of all of the defined classes.
def classlist
@catalog.classes
end
# Compiler our catalog. This mostly revolves around finding and evaluating classes.
# This is the main entry into our catalog.
def compile
# Set the client's parameters into the top scope.
set_node_parameters
create_settings_scope
evaluate_main
evaluate_ast_node
evaluate_node_classes
evaluate_generators
finish
fail_on_unevaluated
@catalog
end
# LAK:FIXME There are no tests for this.
def delete_collection(coll)
@collections.delete(coll) if @collections.include?(coll)
end
# Return the node's environment.
def environment
unless defined?(@environment)
@environment = (node.environment and node.environment != "") ? node.environment : nil
end
Puppet::Node::Environment.current = @environment
@environment
end
# Evaluate all of the classes specified by the node.
def evaluate_node_classes
evaluate_classes(@node.classes, topscope)
end
# Evaluate each specified class in turn. If there are any classes we can't
# find, just tag the catalog and move on. This method really just
# creates resource objects that point back to the classes, and then the
# resources are themselves evaluated later in the process.
def evaluate_classes(classes, scope, lazy_evaluate = true)
raise Puppet::DevError, "No source for scope passed to evaluate_classes" unless scope.source
found = []
classes.each do |name|
# If we can find the class, then make a resource that will evaluate it.
if klass = scope.find_hostclass(name)
found << name and next if scope.class_scope(klass)
resource = klass.mk_plain_resource(scope)
# If they've disabled lazy evaluation (which the :include function does),
# then evaluate our resource immediately.
resource.evaluate unless lazy_evaluate
found << name
else
Puppet.info "Could not find class #{name} for #{node.name}"
@catalog.tag(name)
end
end
found
end
def evaluate_relationships
@relationships.each { |rel| rel.evaluate(catalog) }
end
# Return a resource by either its ref or its type and title.
def findresource(*args)
@catalog.resource(*args)
end
def initialize(node, options = {})
@node = node
options.each do |param, value|
begin
send(param.to_s + "=", value)
rescue NoMethodError
raise ArgumentError, "Compiler objects do not accept #{param}"
end
end
initvars
end
# Create a new scope, with either a specified parent scope or
# using the top scope.
def newscope(parent, options = {})
parent ||= topscope
options[:compiler] = self
scope = Puppet::Parser::Scope.new(options)
scope.parent = parent
scope
end
# Return any overrides for the given resource.
def resource_overrides(resource)
@resource_overrides[resource.ref]
end
# The top scope is usually the top-level scope, but if we're using AST nodes,
# then it is instead the node's scope.
def topscope
node_scope || @topscope
end
private
# If ast nodes are enabled, then see if we can find and evaluate one.
def evaluate_ast_node
return unless ast_nodes?
# Now see if we can find the node.
astnode = nil
@node.names.each do |name|
break if astnode = known_resource_types.node(name.to_s.downcase)
end
unless (astnode ||= known_resource_types.node("default"))
raise Puppet::ParseError, "Could not find default node or by name with '#{node.names.join(", ")}'"
end
# Create a resource to model this node, and then add it to the list
# of resources.
resource = astnode.mk_plain_resource(topscope)
resource.evaluate
# Now set the node scope appropriately, so that :topscope can
# behave differently.
@node_scope = topscope.class_scope(astnode)
end
# Evaluate our collections and return true if anything returned an object.
# The 'true' is used to continue a loop, so it's important.
def evaluate_collections
return false if @collections.empty?
found_something = false
exceptwrap do
# We have to iterate over a dup of the array because
# collections can delete themselves from the list, which
# changes its length and causes some collections to get missed.
@collections.dup.each do |collection|
found_something = true if collection.evaluate
end
end
found_something
end
# Make sure all of our resources have been evaluated into native resources.
# We return true if any resources have, so that we know to continue the
# evaluate_generators loop.
def evaluate_definitions
exceptwrap do
- if ary = unevaluated_resources
- evaluated = false
- ary.each do |resource|
- if not resource.virtual?
- resource.evaluate
- evaluated = true
- end
- end
- # If we evaluated, let the loop know.
- return evaluated
- else
- return false
- end
+ !unevaluated_resources.each { |resource| resource.evaluate }.empty?
end
end
# Iterate over collections and resources until we're sure that the whole
# compile is evaluated. This is necessary because both collections
# and defined resources can generate new resources, which themselves could
# be defined resources.
def evaluate_generators
count = 0
loop do
done = true
# Call collections first, then definitions.
done = false if evaluate_collections
done = false if evaluate_definitions
break if done
count += 1
if count > 1000
raise Puppet::ParseError, "Somehow looped more than 1000 times while evaluating host catalog"
end
end
end
# Find and evaluate our main object, if possible.
def evaluate_main
@main = known_resource_types.find_hostclass([""], "") || known_resource_types.add(Puppet::Resource::Type.new(:hostclass, ""))
@topscope.source = @main
@main_resource = Puppet::Parser::Resource.new("class", :main, :scope => @topscope, :source => @main)
@topscope.resource = @main_resource
@resources << @main_resource
@catalog.add_resource(@main_resource)
set_container_resource(@topscope, @main_resource)
@main_resource.evaluate
end
# Make sure the entire catalog is evaluated.
def fail_on_unevaluated
fail_on_unevaluated_overrides
fail_on_unevaluated_resource_collections
end
# If there are any resource overrides remaining, then we could
# not find the resource they were supposed to override, so we
# want to throw an exception.
def fail_on_unevaluated_overrides
remaining = []
@resource_overrides.each do |name, overrides|
remaining += overrides
end
unless remaining.empty?
fail Puppet::ParseError,
"Could not find resource(s) %s for overriding" % remaining.collect { |o|
o.ref
}.join(", ")
end
end
# Make sure we don't have any remaining collections that specifically
# look for resources, because we want to consider those to be
# parse errors.
def fail_on_unevaluated_resource_collections
remaining = []
@collections.each do |coll|
# We're only interested in the 'resource' collections,
# which result from direct calls of 'realize'. Anything
# else is allowed not to return resources.
# Collect all of them, so we have a useful error.
if r = coll.resources
if r.is_a?(Array)
remaining += r
else
remaining << r
end
end
end
raise Puppet::ParseError, "Failed to realize virtual resources #{remaining.join(', ')}" unless remaining.empty?
end
# Make sure all of our resources and such have done any last work
# necessary.
def finish
evaluate_relationships
resources.each do |resource|
# Add in any resource overrides.
if overrides = resource_overrides(resource)
overrides.each do |over|
resource.merge(over)
end
# Remove the overrides, so that the configuration knows there
# are none left.
overrides.clear
end
resource.finish if resource.respond_to?(:finish)
end
add_resource_metaparams
end
def add_resource_metaparams
unless main = catalog.resource(:class, :main)
raise "Couldn't find main"
end
names = []
Puppet::Type.eachmetaparam do |name|
next if Puppet::Parser::Resource.relationship_parameter?(name)
names << name
end
data = {}
catalog.walk(main, :out) do |source, target|
if source_data = data[source] || metaparams_as_data(source, names)
# only store anything in the data hash if we've actually got
# data
data[source] ||= source_data
source_data.each do |param, value|
target[param] = value if target[param].nil?
end
data[target] = source_data.merge(metaparams_as_data(target, names))
end
target.tag(*(source.tags))
end
end
def metaparams_as_data(resource, params)
data = nil
params.each do |param|
unless resource[param].nil?
# Because we could be creating a hash for every resource,
# and we actually probably don't often have any data here at all,
# we're optimizing a bit by only creating a hash if there's
# any data to put in it.
data ||= {}
data[param] = resource[param]
end
end
data
end
# Set up all of our internal variables.
def initvars
# The list of objects that will available for export.
@exported_resources = {}
# The list of overrides. This is used to cache overrides on objects
# that don't exist yet. We store an array of each override.
@resource_overrides = Hash.new do |overs, ref|
overs[ref] = []
end
# The list of collections that have been created. This is a global list,
# but they each refer back to the scope that created them.
@collections = []
# The list of relationships to evaluate.
@relationships = []
# For maintaining the relationship between scopes and their resources.
@catalog = Puppet::Resource::Catalog.new(@node.name)
@catalog.version = known_resource_types.version
# Create our initial scope and a resource that will evaluate main.
@topscope = Puppet::Parser::Scope.new(:compiler => self)
@main_stage_resource = Puppet::Parser::Resource.new("stage", :main, :scope => @topscope)
@catalog.add_resource(@main_stage_resource)
# local resource array to maintain resource ordering
@resources = []
# Make sure any external node classes are in our class list
@catalog.add_class(*@node.classes)
end
# Set the node's parameters into the top-scope as variables.
def set_node_parameters
node.parameters.each do |param, value|
@topscope.setvar(param, value)
end
# These might be nil.
catalog.client_version = node.parameters["clientversion"]
catalog.server_version = node.parameters["serverversion"]
end
def create_settings_scope
unless settings_type = environment.known_resource_types.hostclass("settings")
settings_type = Puppet::Resource::Type.new :hostclass, "settings"
environment.known_resource_types.add(settings_type)
end
settings_resource = Puppet::Parser::Resource.new("class", "settings", :scope => @topscope)
settings_type.evaluate_code(settings_resource)
@catalog.add_resource(settings_resource)
scope = @topscope.class_scope(settings_type)
Puppet.settings.each do |name, setting|
next if name.to_s == "name"
scope.setvar name.to_s, environment[name]
end
end
# Return an array of all of the unevaluated resources. These will be definitions,
# which need to get evaluated into native resources.
def unevaluated_resources
- ary = resources.reject { |resource| resource.builtin? or resource.evaluated? }
-
- if ary.empty?
- return nil
- else
- return ary
- end
+ # The order of these is significant for speed due to short-circuting
+ resources.reject { |resource| resource.evaluated? or resource.virtual? or resource.builtin_type? }
end
end
diff --git a/lib/puppet/parser/resource.rb b/lib/puppet/parser/resource.rb
index 3cccf4f3e..c956a1106 100644
--- a/lib/puppet/parser/resource.rb
+++ b/lib/puppet/parser/resource.rb
@@ -1,326 +1,323 @@
require 'puppet/resource'
# The primary difference between this class and its
# parent is that this class has rules on who can set
# parameters
class Puppet::Parser::Resource < Puppet::Resource
require 'puppet/parser/resource/param'
require 'puppet/util/tagging'
require 'puppet/file_collection/lookup'
require 'puppet/parser/yaml_trimmer'
require 'puppet/resource/type_collection_helper'
include Puppet::FileCollection::Lookup
include Puppet::Resource::TypeCollectionHelper
include Puppet::Util
include Puppet::Util::MethodHelper
include Puppet::Util::Errors
include Puppet::Util::Logging
include Puppet::Util::Tagging
include Puppet::Parser::YamlTrimmer
attr_accessor :source, :scope, :rails_id
attr_accessor :virtual, :override, :translated, :catalog, :evaluated
attr_reader :exported, :parameters
# Determine whether the provided parameter name is a relationship parameter.
def self.relationship_parameter?(name)
@relationship_names ||= Puppet::Type.relationship_params.collect { |p| p.name }
@relationship_names.include?(name)
end
# Set up some boolean test methods
- [:translated, :override, :evaluated].each do |method|
- newmeth = (method.to_s + "?").intern
- define_method(newmeth) do
- self.send(method)
- end
- end
+ def translated?; !!@translated; end
+ def override?; !!@override; end
+ def evaluated?; !!@evaluated; end
def [](param)
param = symbolize(param)
if param == :title
return self.title
end
if @parameters.has_key?(param)
@parameters[param].value
else
nil
end
end
def []=(param, value)
set_parameter(param, value)
end
def eachparam
@parameters.each do |name, param|
yield param
end
end
def environment
scope.environment
end
# Retrieve the associated definition and evaluate it.
def evaluate
if klass = resource_type and ! builtin_type?
finish
return klass.evaluate_code(self)
elsif builtin?
devfail "Cannot evaluate a builtin type (#{type})"
else
self.fail "Cannot find definition #{type}"
end
ensure
@evaluated = true
end
# Mark this resource as both exported and virtual,
# or remove the exported mark.
def exported=(value)
if value
@virtual = true
@exported = value
else
@exported = value
end
end
# Do any finishing work on this object, called before evaluation or
# before storage/translation.
def finish
return if finished?
@finished = true
add_defaults
add_metaparams
validate
end
# Has this resource already been finished?
def finished?
@finished
end
def initialize(*args)
super
raise ArgumentError, "Resources require a scope" unless scope
@source ||= scope.source
end
# Is this resource modeling an isomorphic resource type?
def isomorphic?
if builtin_type?
return resource_type.isomorphic?
else
return true
end
end
# Merge an override resource in. This will throw exceptions if
# any overrides aren't allowed.
def merge(resource)
# Test the resource scope, to make sure the resource is even allowed
# to override.
unless self.source.object_id == resource.source.object_id || resource.source.child_of?(self.source)
raise Puppet::ParseError.new("Only subclasses can override parameters", resource.line, resource.file)
end
# Some of these might fail, but they'll fail in the way we want.
resource.parameters.each do |name, param|
override_parameter(param)
end
end
# Unless we're running >= 0.25, we're in compat mode.
def metaparam_compatibility_mode?
! (catalog and ver = (catalog.client_version||'0.0.0').split(".") and (ver[0] > "0" or ver[1].to_i >= 25))
end
def name
self[:name] || self.title
end
def namespaces
scope.namespaces
end
# A temporary occasion, until I get paths in the scopes figured out.
def path
to_s
end
# Define a parameter in our resource.
# if we ever receive a parameter named 'tag', set
# the resource tags with its value.
def set_parameter(param, value = nil)
if ! value.nil?
param = Puppet::Parser::Resource::Param.new(
:name => param, :value => value, :source => self.source
)
elsif ! param.is_a?(Puppet::Parser::Resource::Param)
raise ArgumentError, "Must pass a parameter or all necessary values"
end
tag(*param.value) if param.name == :tag
# And store it in our parameter hash.
@parameters[param.name] = param
end
def to_hash
@parameters.inject({}) do |hash, ary|
param = ary[1]
# Skip "undef" values.
hash[param.name] = param.value if param.value != :undef
hash
end
end
# Create a Puppet::Resource instance from this parser resource.
# We plan, at some point, on not needing to do this conversion, but
# it's sufficient for now.
def to_resource
result = Puppet::Resource.new(type, title)
to_hash.each do |p, v|
if v.is_a?(Puppet::Resource)
v = Puppet::Resource.new(v.type, v.title)
elsif v.is_a?(Array)
# flatten resource references arrays
v = v.flatten if v.flatten.find { |av| av.is_a?(Puppet::Resource) }
v = v.collect do |av|
av = Puppet::Resource.new(av.type, av.title) if av.is_a?(Puppet::Resource)
av
end
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] = if v.is_a?(Array) and v.length == 1
v[0]
else
v
end
end
result.file = self.file
result.line = self.line
result.exported = self.exported
result.virtual = self.virtual
result.tag(*self.tags)
result
end
# Translate our object to a transportable object.
def to_trans
return nil if virtual?
to_resource.to_trans
end
# Convert this resource to a RAL resource. We hackishly go via the
# transportable stuff.
def to_ral
to_resource.to_ral
end
private
# Add default values from our definition.
def add_defaults
scope.lookupdefaults(self.type).each do |name, param|
unless @parameters.include?(name)
self.debug "Adding default for #{name}"
@parameters[name] = param.dup
end
end
end
def add_backward_compatible_relationship_param(name)
# Skip metaparams for which we get no value.
return unless val = scope.lookupvar(name.to_s, false) and val != :undefined
# The default case: just set the value
set_parameter(name, val) and return unless @parameters[name]
# For relationship params, though, join the values (a la #446).
@parameters[name].value = [@parameters[name].value, val].flatten
end
# Add any metaparams defined in our scope. This actually adds any metaparams
# from any parent scope, and there's currently no way to turn that off.
def add_metaparams
compat_mode = metaparam_compatibility_mode?
Puppet::Type.eachmetaparam do |name|
next unless self.class.relationship_parameter?(name)
add_backward_compatible_relationship_param(name) if compat_mode
end
end
# Accept a parameter from an override.
def override_parameter(param)
# This can happen if the override is defining a new parameter, rather
# than replacing an existing one.
(set_parameter(param) and return) unless current = @parameters[param.name]
# The parameter is already set. Fail if they're not allowed to override it.
unless param.source.child_of?(current.source)
puts caller if Puppet[:trace]
msg = "Parameter '#{param.name}' is already set on #{self}"
msg += " by #{current.source}" if current.source.to_s != ""
if current.file or current.line
fields = []
fields << current.file if current.file
fields << current.line.to_s if current.line
msg += " at #{fields.join(":")}"
end
msg += "; cannot redefine"
raise Puppet::ParseError.new(msg, param.line, param.file)
end
# If we've gotten this far, we're allowed to override.
# Merge with previous value, if the parameter was generated with the +>
# syntax. It's important that we use a copy of the new param instance
# here, not the old one, and not the original new one, so that the source
# is registered correctly for later overrides but the values aren't
# implcitly shared when multiple resources are overrriden at once (see
# ticket #3556).
if param.add
param = param.dup
param.value = [current.value, param.value].flatten
end
set_parameter(param)
end
# Make sure the resource's parameters are all valid for the type.
def validate
@parameters.each do |name, param|
validate_parameter(name)
end
rescue => detail
fail Puppet::ParseError, detail.to_s
end
private
def extract_parameters(params)
params.each do |param|
# Don't set the same parameter twice
self.fail Puppet::ParseError, "Duplicate parameter '#{param.name}' for on #{self}" if @parameters[param.name]
set_parameter(param)
end
end
end
diff --git a/lib/puppet/resource.rb b/lib/puppet/resource.rb
index 31237e3b1..d163d93f0 100644
--- a/lib/puppet/resource.rb
+++ b/lib/puppet/resource.rb
@@ -1,486 +1,486 @@
require 'puppet'
require 'puppet/util/tagging'
require 'puppet/util/pson'
# The simplest resource class. Eventually it will function as the
# base class for all resource-like behaviour.
class Puppet::Resource
include Puppet::Util::Tagging
require 'puppet/resource/type_collection_helper'
include Puppet::Resource::TypeCollectionHelper
extend Puppet::Util::Pson
include Enumerable
attr_accessor :file, :line, :catalog, :exported, :virtual, :validate_parameters, :strict
attr_reader :namespaces
require 'puppet/indirector'
extend Puppet::Indirector
indirects :resource, :terminus_class => :ral
ATTRIBUTES = [:file, :line, :exported]
def self.from_pson(pson)
raise ArgumentError, "No resource type provided in pson data" unless type = pson['type']
raise ArgumentError, "No resource title provided in pson data" unless title = pson['title']
resource = new(type, title)
if params = pson['parameters']
params.each { |param, value| resource[param] = value }
end
if tags = pson['tags']
tags.each { |tag| resource.tag(tag) }
end
ATTRIBUTES.each do |a|
if value = pson[a.to_s]
resource.send(a.to_s + "=", value)
end
end
resource.exported ||= false
resource
end
def to_pson_data_hash
data = ([:type, :title, :tags] + ATTRIBUTES).inject({}) do |hash, param|
next hash unless value = self.send(param)
hash[param.to_s] = value
hash
end
data["exported"] ||= false
params = self.to_hash.inject({}) do |hash, ary|
param, value = ary
# Don't duplicate the title as the namevar
next hash if param == namevar and value == title
hash[param] = Puppet::Resource.value_to_pson_data(value)
hash
end
data["parameters"] = params unless params.empty?
data
end
def self.value_to_pson_data(value)
if value.is_a? Array
value.map{|v| value_to_pson_data(v) }
elsif value.is_a? Puppet::Resource
value.to_s
else
value
end
end
def to_pson(*args)
to_pson_data_hash.to_pson(*args)
end
# 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)
validate_parameter(param) if validate_parameters
@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
def ==(other)
return false unless other.respond_to?(:title) and self.type == other.type and self.title == other.title
return false unless to_hash == other.to_hash
true
end
# Compatibility method.
def builtin?
builtin_type?
end
# Is this a builtin resource type?
def builtin_type?
resource_type.is_a?(Class)
end
# Iterate over each param/value pair, as required for Enumerable.
def each
@parameters.each { |p,v| yield p, v }
end
def include?(parameter)
super || @parameters.keys.include?( parameter_name(parameter) )
end
# These two methods are extracted into a Helper
# module, but file load order prevents me
# from including them in the class, and I had weird
# behaviour (i.e., sometimes it didn't work) when
# I directly extended each resource with the helper.
def environment
Puppet::Node::Environment.new(@environment)
end
def environment=(env)
if env.is_a?(String) or env.is_a?(Symbol)
@environment = env
else
@environment = env.name
end
end
%w{exported virtual strict}.each do |m|
define_method(m+"?") do
self.send(m)
end
end
# Create our resource.
def initialize(type, title = nil, attributes = {})
@parameters = {}
@namespaces = [""]
# Set things like namespaces and strictness first.
attributes.each do |attr, value|
next if attr == :parameters
send(attr.to_s + "=", value)
end
# We do namespaces first, and use tmp variables, so our title
# canonicalization works (i.e., namespaces are set and resource
# types can be looked up)
tmp_type, tmp_title = extract_type_and_title(type, title)
self.type = tmp_type
self.title = tmp_title
if params = attributes[:parameters]
extract_parameters(params)
end
resolve_type_and_title
tag(self.type)
tag(self.title) if valid_tag?(self.title)
raise ArgumentError, "Invalid resource type #{type}" if strict? and ! resource_type
end
def ref
to_s
end
# Find our resource.
def resolve
return(catalog ? catalog.resource(to_s) : nil)
end
def title=(value)
@unresolved_title = value
@title = nil
end
def resource_type
- case type
+ @resource_type ||= case type
when "Class"; find_hostclass(title)
when "Node"; find_node(title)
else
find_resource_type(type)
end
end
# Produce a simple hash of our parameters.
def to_hash
parse_title.merge @parameters
end
def to_s
"#{type}[#{title}]"
end
def uniqueness_key
self.to_hash.values_at(*key_attributes.sort_by { |k| k.to_s })
end
def key_attributes
return(resource_type.respond_to? :key_attributes) ? resource_type.key_attributes : [:name]
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 builtin_type? and type.downcase.to_s != "stage"
result = to_transobject
else
result = to_transbucket
end
result.file = self.file
result.line = self.line
result
end
def to_trans_ref
[type.to_s, title.to_s]
end
# Create an old-style TransObject instance, for builtin resource types.
def to_transobject
# Now convert to a transobject
result = Puppet::TransObject.new(title, type)
to_hash.each do |p, v|
if v.is_a?(Puppet::Resource)
v = v.to_trans_ref
elsif v.is_a?(Array)
v = v.collect { |av|
av = av.to_trans_ref if av.is_a?(Puppet::Resource)
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
result
end
def name
# this is potential namespace conflict
# between the notion of an "indirector name"
# and a "resource name"
[ type, title ].join('/')
end
def to_resource
self
end
# We have to lazy-evaluate this.
def title=(value)
@title = nil
@unresolved_title = value
end
# We have to lazy-evaluate this.
def type=(value)
@type = nil
@unresolved_type = value || "Class"
end
def title
resolve_type_and_title unless @title
@title
end
def type
resolve_type_and_title unless @type
@type
end
def valid_parameter?(name)
resource_type.valid_parameter?(name)
end
def validate_parameter(name)
raise ArgumentError, "Invalid parameter #{name}" unless valid_parameter?(name)
end
private
def find_node(name)
known_resource_types.node(name)
end
def find_hostclass(title)
name = title == :main ? "" : title
known_resource_types.find_hostclass(namespaces, name)
end
def find_resource_type(type)
# It still works fine without the type == 'class' short-cut, but it is a lot slower.
find_builtin_resource_type(type) || find_defined_resource_type(type) unless type.to_s.downcase == 'class'
end
def find_builtin_resource_type(type)
Puppet::Type.type(type.to_s.downcase.to_sym)
end
def find_defined_resource_type(type)
known_resource_types.find_definition(namespaces, type.to_s.downcase)
end
# 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
def namespaces=(ns)
@namespaces = Array(ns)
end
# The namevar for our resource type. If the type doesn't exist,
# always use :name.
def namevar
if builtin_type? and t = resource_type and t.key_attributes.length == 1
t.key_attributes.first
else
:name
end
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.
bucket
end
def extract_parameters(params)
params.each do |param, value|
validate_parameter(param) if strict?
self[param] = value
end
end
def extract_type_and_title(argtype, argtitle)
if (argtitle || argtype) =~ /^([^\[\]]+)\[(.+)\]$/m then [ $1, $2 ]
elsif argtitle then [ argtype, argtitle ]
elsif argtype.is_a?(Puppet::Type) then [ argtype.class.name, argtype.title ]
else raise ArgumentError, "No title provided and #{argtype.inspect} is not a valid resource reference"
end
end
def munge_type_name(value)
return :main if value == :main
return "Class" if value == "" or value.nil? or value.to_s.downcase == "component"
value.to_s.split("::").collect { |s| s.capitalize }.join("::")
end
# This is an annoyingly complicated method for resolving qualified
# types as necessary, and putting them in type or title attributes.
def resolve_type_and_title
if @unresolved_type
@type = resolve_type
@unresolved_type = nil
end
if @unresolved_title
@title = resolve_title
@unresolved_title = nil
end
end
def resolve_type
case type = munge_type_name(@unresolved_type)
when "Class", "Node";
type
else
# Otherwise, some kind of builtin or defined resource type
munge_type_name( (r = find_resource_type(type)) ? r.name : type)
end
end
# This method only works if resolve_type was called first
def resolve_title
case @type
when "Node"; return @unresolved_title
when "Class";
resolve_title_for_class(@unresolved_title)
else
@unresolved_title
end
end
def resolve_title_for_class(title)
if title == "" or title == :main
return :main
end
if klass = find_hostclass(title)
result = klass.name
end
munge_type_name(result || title)
end
def parse_title
h = {}
type = find_resource_type(@type)
if type.respond_to? :title_patterns
type.title_patterns.each { |regexp, symbols_and_lambdas|
if captures = regexp.match(title.to_s)
symbols_and_lambdas.zip(captures[1..-1]).each { |symbol_and_lambda,capture|
sym, lam = symbol_and_lambda
#self[sym] = lam.call(capture)
h[sym] = lam.call(capture)
}
return h
end
}
else
return { :name => title.to_s }
end
end
end
diff --git a/lib/puppet/type.rb b/lib/puppet/type.rb
index d0fea9731..5676b5d12 100644
--- a/lib/puppet/type.rb
+++ b/lib/puppet/type.rb
@@ -1,1889 +1,1885 @@
require 'puppet'
require 'puppet/util/log'
require 'puppet/util/metric'
require 'puppet/property'
require 'puppet/parameter'
require 'puppet/util'
require 'puppet/util/autoload'
require 'puppet/metatype/manager'
require 'puppet/util/errors'
require 'puppet/util/log_paths'
require 'puppet/util/logging'
require 'puppet/util/cacher'
require 'puppet/file_collection/lookup'
require 'puppet/util/tagging'
# see the bottom of the file for the rest of the inclusions
module Puppet
class Type
include Puppet::Util
include Puppet::Util::Errors
include Puppet::Util::LogPaths
include Puppet::Util::Logging
include Puppet::Util::Cacher
include Puppet::FileCollection::Lookup
include Puppet::Util::Tagging
###############################
# Code related to resource type attributes.
class << self
include Puppet::Util::ClassGen
include Puppet::Util::Warnings
attr_reader :properties
end
def self.states
warnonce "The states method is deprecated; use properties"
properties
end
# All parameters, in the appropriate order. The key_attributes come first, then
# the provider, then the properties, and finally the params and metaparams
# in the order they were specified in the files.
def self.allattrs
key_attributes | (parameters & [:provider]) | properties.collect { |property| property.name } | parameters | metaparams
end
# Retrieve an attribute alias, if there is one.
def self.attr_alias(param)
@attr_aliases[symbolize(param)]
end
# Create an alias to an existing attribute. This will cause the aliased
# attribute to be valid when setting and retrieving values on the instance.
def self.set_attr_alias(hash)
hash.each do |new, old|
@attr_aliases[symbolize(new)] = symbolize(old)
end
end
# Find the class associated with any given attribute.
def self.attrclass(name)
@attrclasses ||= {}
# We cache the value, since this method gets called such a huge number
# of times (as in, hundreds of thousands in a given run).
unless @attrclasses.include?(name)
@attrclasses[name] = case self.attrtype(name)
when :property; @validproperties[name]
when :meta; @@metaparamhash[name]
when :param; @paramhash[name]
end
end
@attrclasses[name]
end
# What type of parameter are we dealing with? Cache the results, because
# this method gets called so many times.
def self.attrtype(attr)
@attrtypes ||= {}
unless @attrtypes.include?(attr)
@attrtypes[attr] = case
when @validproperties.include?(attr); :property
when @paramhash.include?(attr); :param
when @@metaparamhash.include?(attr); :meta
end
end
@attrtypes[attr]
end
def self.eachmetaparam
@@metaparams.each { |p| yield p.name }
end
# Create the 'ensure' class. This is a separate method so other types
# can easily call it and create their own 'ensure' values.
def self.ensurable(&block)
if block_given?
self.newproperty(:ensure, :parent => Puppet::Property::Ensure, &block)
else
self.newproperty(:ensure, :parent => Puppet::Property::Ensure) do
self.defaultvalues
end
end
end
# Should we add the 'ensure' property to this class?
def self.ensurable?
# If the class has all three of these methods defined, then it's
# ensurable.
ens = [:exists?, :create, :destroy].inject { |set, method|
set &&= self.public_method_defined?(method)
}
ens
end
# Deal with any options passed into parameters.
def self.handle_param_options(name, options)
# If it's a boolean parameter, create a method to test the value easily
if options[:boolean]
define_method(name.to_s + "?") do
val = self[name]
if val == :true or val == true
return true
end
end
end
end
# Is the parameter in question a meta-parameter?
def self.metaparam?(param)
@@metaparamhash.include?(symbolize(param))
end
# Find the metaparameter class associated with a given metaparameter name.
def self.metaparamclass(name)
@@metaparamhash[symbolize(name)]
end
def self.metaparams
@@metaparams.collect { |param| param.name }
end
def self.metaparamdoc(metaparam)
@@metaparamhash[metaparam].doc
end
# Create a new metaparam. Requires a block and a name, stores it in the
# @parameters array, and does some basic checking on it.
def self.newmetaparam(name, options = {}, &block)
@@metaparams ||= []
@@metaparamhash ||= {}
name = symbolize(name)
param = genclass(
name,
:parent => options[:parent] || Puppet::Parameter,
:prefix => "MetaParam",
:hash => @@metaparamhash,
:array => @@metaparams,
:attributes => options[:attributes],
&block
)
# Grr.
param.required_features = options[:required_features] if options[:required_features]
handle_param_options(name, options)
param.metaparam = true
param
end
def self.key_attribute_parameters
@key_attribute_parameters ||= (
params = @parameters.find_all { |param|
param.isnamevar? or param.name == :name
}
)
end
def self.key_attributes
key_attribute_parameters.collect { |p| p.name }
end
def self.title_patterns
case key_attributes.length
when 0; []
when 1;
identity = lambda {|x| x}
[ [ /(.*)/, [ [key_attributes.first, identity ] ] ] ]
else
raise Puppet::DevError,"you must specify title patterns when there are two or more key attributes"
end
end
def uniqueness_key
to_resource.uniqueness_key
end
# Create a new parameter. Requires a block and a name, stores it in the
# @parameters array, and does some basic checking on it.
def self.newparam(name, options = {}, &block)
options[:attributes] ||= {}
param = genclass(
name,
:parent => options[:parent] || Puppet::Parameter,
:attributes => options[:attributes],
:block => block,
:prefix => "Parameter",
:array => @parameters,
:hash => @paramhash
)
handle_param_options(name, options)
# Grr.
param.required_features = options[:required_features] if options[:required_features]
param.isnamevar if options[:namevar]
param
end
def self.newstate(name, options = {}, &block)
Puppet.warning "newstate() has been deprecrated; use newproperty(#{name})"
newproperty(name, options, &block)
end
# Create a new property. The first parameter must be the name of the property;
# this is how users will refer to the property when creating new instances.
# The second parameter is a hash of options; the options are:
# * :parent: The parent class for the property. Defaults to Puppet::Property.
# * :retrieve: The method to call on the provider or @parent object (if
# the provider is not set) to retrieve the current value.
def self.newproperty(name, options = {}, &block)
name = symbolize(name)
# This is here for types that might still have the old method of defining
# a parent class.
unless options.is_a? Hash
raise Puppet::DevError,
"Options must be a hash, not #{options.inspect}"
end
raise Puppet::DevError, "Class #{self.name} already has a property named #{name}" if @validproperties.include?(name)
if parent = options[:parent]
options.delete(:parent)
else
parent = Puppet::Property
end
# We have to create our own, new block here because we want to define
# an initial :retrieve method, if told to, and then eval the passed
# block if available.
prop = genclass(name, :parent => parent, :hash => @validproperties, :attributes => options) do
# If they've passed a retrieve method, then override the retrieve
# method on the class.
if options[:retrieve]
define_method(:retrieve) do
provider.send(options[:retrieve])
end
end
class_eval(&block) if block
end
# If it's the 'ensure' property, always put it first.
if name == :ensure
@properties.unshift prop
else
@properties << prop
end
prop
end
def self.paramdoc(param)
@paramhash[param].doc
end
# Return the parameter names
def self.parameters
return [] unless defined?(@parameters)
@parameters.collect { |klass| klass.name }
end
# Find the parameter class associated with a given parameter name.
def self.paramclass(name)
@paramhash[name]
end
# Return the property class associated with a name
def self.propertybyname(name)
@validproperties[name]
end
def self.validattr?(name)
name = symbolize(name)
return true if name == :name
@validattrs ||= {}
unless @validattrs.include?(name)
@validattrs[name] = !!(self.validproperty?(name) or self.validparameter?(name) or self.metaparam?(name))
end
@validattrs[name]
end
# does the name reflect a valid property?
def self.validproperty?(name)
name = symbolize(name)
@validproperties.include?(name) && @validproperties[name]
end
# Return the list of validproperties
def self.validproperties
return {} unless defined?(@parameters)
@validproperties.keys
end
# does the name reflect a valid parameter?
def self.validparameter?(name)
raise Puppet::DevError, "Class #{self} has not defined parameters" unless defined?(@parameters)
!!(@paramhash.include?(name) or @@metaparamhash.include?(name))
end
# This is a forward-compatibility method - it's the validity interface we'll use in Puppet::Resource.
def self.valid_parameter?(name)
validattr?(name)
end
# Return either the attribute alias or the attribute.
def attr_alias(name)
name = symbolize(name)
if synonym = self.class.attr_alias(name)
return synonym
else
return name
end
end
# Are we deleting this resource?
def deleting?
obj = @parameters[:ensure] and obj.should == :absent
end
# Create a new property if it is valid but doesn't exist
# Returns: true if a new parameter was added, false otherwise
def add_property_parameter(prop_name)
if self.class.validproperty?(prop_name) && !@parameters[prop_name]
self.newattr(prop_name)
return true
end
false
end
#
# The name_var is the key_attribute in the case that there is only one.
#
def name_var
key_attributes = self.class.key_attributes
(key_attributes.length == 1) && key_attributes.first
end
# abstract accessing parameters and properties, and normalize
# access to always be symbols, not strings
# This returns a value, not an object. It returns the 'is'
# value, but you can also specifically return 'is' and 'should'
# values using 'object.is(:property)' or 'object.should(:property)'.
def [](name)
name = attr_alias(name)
fail("Invalid parameter #{name}(#{name.inspect})") unless self.class.validattr?(name)
if name == :name
name = name_var
end
if obj = @parameters[name]
# Note that if this is a property, then the value is the "should" value,
# not the current value.
obj.value
else
return nil
end
end
# Abstract setting parameters and properties, and normalize
# access to always be symbols, not strings. This sets the 'should'
# value on properties, and otherwise just sets the appropriate parameter.
def []=(name,value)
name = attr_alias(name)
fail("Invalid parameter #{name}") unless self.class.validattr?(name)
if name == :name
name = name_var
end
raise Puppet::Error.new("Got nil value for #{name}") if value.nil?
property = self.newattr(name)
begin
# make sure the parameter doesn't have any errors
property.value = value
rescue => detail
error = Puppet::Error.new("Parameter #{name} failed: #{detail}")
error.set_backtrace(detail.backtrace)
raise error
end
nil
end
# remove a property from the object; useful in testing or in cleanup
# when an error has been encountered
def delete(attr)
attr = symbolize(attr)
if @parameters.has_key?(attr)
@parameters.delete(attr)
else
raise Puppet::DevError.new("Undefined attribute '#{attr}' in #{self}")
end
end
# iterate across the existing properties
def eachproperty
# properties is a private method
properties.each { |property|
yield property
}
end
# Create a transaction event. Called by Transaction or by
# a property.
def event(options = {})
Puppet::Transaction::Event.new({:resource => self, :file => file, :line => line, :tags => tags, :version => version}.merge(options))
end
# Let the catalog determine whether a given cached value is
# still valid or has expired.
def expirer
catalog
end
# retrieve the 'should' value for a specified property
def should(name)
name = attr_alias(name)
(prop = @parameters[name] and prop.is_a?(Puppet::Property)) ? prop.should : nil
end
# Create the actual attribute instance. Requires either the attribute
# name or class as the first argument, then an optional hash of
# attributes to set during initialization.
def newattr(name)
if name.is_a?(Class)
klass = name
name = klass.name
end
unless klass = self.class.attrclass(name)
raise Puppet::Error, "Resource type #{self.class.name} does not support parameter #{name}"
end
return @parameters[name] if @parameters.include?(name)
@parameters[name] = klass.new(:resource => self)
end
# return the value of a parameter
def parameter(name)
@parameters[name.to_sym]
end
def parameters
@parameters.dup
end
# Is the named property defined?
def propertydefined?(name)
name = name.intern unless name.is_a? Symbol
@parameters.include?(name)
end
# Return an actual property instance by name; to return the value, use 'resource[param]'
# LAK:NOTE(20081028) Since the 'parameter' method is now a superset of this method,
# this one should probably go away at some point.
def property(name)
(obj = @parameters[symbolize(name)] and obj.is_a?(Puppet::Property)) ? obj : nil
end
# For any parameters or properties that have defaults and have not yet been
# set, set them now. This method can be handed a list of attributes,
# and if so it will only set defaults for those attributes.
def set_default(attr)
return unless klass = self.class.attrclass(attr)
return unless klass.method_defined?(:default)
return if @parameters.include?(klass.name)
return unless parameter = newattr(klass.name)
if value = parameter.default and ! value.nil?
parameter.value = value
else
@parameters.delete(parameter.name)
end
end
# Convert our object to a hash. This just includes properties.
def to_hash
rethash = {}
@parameters.each do |name, obj|
rethash[name] = obj.value
end
rethash
end
def type
self.class.name
end
# Return a specific value for an attribute.
def value(name)
name = attr_alias(name)
(obj = @parameters[name] and obj.respond_to?(:value)) ? obj.value : nil
end
def version
return 0 unless catalog
catalog.version
end
# Return all of the property objects, in the order specified in the
# class.
def properties
self.class.properties.collect { |prop| @parameters[prop.name] }.compact
end
# Is this type's name isomorphic with the object? That is, if the
# name conflicts, does it necessarily mean that the objects conflict?
# Defaults to true.
def self.isomorphic?
if defined?(@isomorphic)
return @isomorphic
else
return true
end
end
def isomorphic?
self.class.isomorphic?
end
# is the instance a managed instance? A 'yes' here means that
# the instance was created from the language, vs. being created
# in order resolve other questions, such as finding a package
# in a list
def managed?
# Once an object is managed, it always stays managed; but an object
# that is listed as unmanaged might become managed later in the process,
# so we have to check that every time
if @managed
return @managed
else
@managed = false
properties.each { |property|
s = property.should
if s and ! property.class.unmanaged
@managed = true
break
end
}
return @managed
end
end
###############################
# Code related to the container behaviour.
# this is a retarded hack method to get around the difference between
# component children and file children
def self.depthfirst?
@depthfirst
end
def depthfirst?
self.class.depthfirst?
end
# Remove an object. The argument determines whether the object's
# subscriptions get eliminated, too.
def remove(rmdeps = true)
# This is hackish (mmm, cut and paste), but it works for now, and it's
# better than warnings.
@parameters.each do |name, obj|
obj.remove
end
@parameters.clear
@parent = nil
# Remove the reference to the provider.
if self.provider
@provider.clear
@provider = nil
end
end
###############################
# Code related to evaluating the resources.
# Flush the provider, if it supports it. This is called by the
# transaction.
def flush
self.provider.flush if self.provider and self.provider.respond_to?(:flush)
end
# if all contained objects are in sync, then we're in sync
# FIXME I don't think this is used on the type instances any more,
# it's really only used for testing
def insync?(is)
insync = true
if property = @parameters[:ensure]
unless is.include? property
raise Puppet::DevError,
"The is value is not in the is array for '#{property.name}'"
end
ensureis = is[property]
if property.insync?(ensureis) and property.should == :absent
return true
end
end
properties.each { |property|
unless is.include? property
raise Puppet::DevError,
"The is value is not in the is array for '#{property.name}'"
end
propis = is[property]
unless property.insync?(propis)
property.debug("Not in sync: #{propis.inspect} vs #{property.should.inspect}")
insync = false
#else
# property.debug("In sync")
end
}
#self.debug("#{self} sync status is #{insync}")
insync
end
# retrieve the current value of all contained properties
def retrieve
fail "Provider #{provider.class.name} is not functional on this host" if self.provider.is_a?(Puppet::Provider) and ! provider.class.suitable?
result = Puppet::Resource.new(type, title)
# Provide the name, so we know we'll always refer to a real thing
result[:name] = self[:name] unless self[:name] == title
if ensure_prop = property(:ensure) or (self.class.validattr?(:ensure) and ensure_prop = newattr(:ensure))
result[:ensure] = ensure_state = ensure_prop.retrieve
else
ensure_state = nil
end
properties.each do |property|
next if property.name == :ensure
if ensure_state == :absent
result[property] = :absent
else
result[property] = property.retrieve
end
end
result
end
def retrieve_resource
resource = retrieve
resource = Resource.new(type, title, :parameters => resource) if resource.is_a? Hash
resource
end
# Get a hash of the current properties. Returns a hash with
# the actual property instance as the key and the current value
# as the, um, value.
def currentpropvalues
# It's important to use the 'properties' method here, as it follows the order
# in which they're defined in the class. It also guarantees that 'ensure'
# is the first property, which is important for skipping 'retrieve' on
# all the properties if the resource is absent.
ensure_state = false
return properties.inject({}) do | prophash, property|
if property.name == :ensure
ensure_state = property.retrieve
prophash[property] = ensure_state
else
if ensure_state == :absent
prophash[property] = :absent
else
prophash[property] = property.retrieve
end
end
prophash
end
end
# Are we running in noop mode?
def noop?
if defined?(@noop)
@noop
else
Puppet[:noop]
end
end
def noop
noop?
end
###############################
# Code related to managing resource instances.
require 'puppet/transportable'
# retrieve a named instance of the current type
def self.[](name)
raise "Global resource access is deprecated"
@objects[name] || @aliases[name]
end
# add an instance by name to the class list of instances
def self.[]=(name,object)
raise "Global resource storage is deprecated"
newobj = nil
if object.is_a?(Puppet::Type)
newobj = object
else
raise Puppet::DevError, "must pass a Puppet::Type object"
end
if exobj = @objects[name] and self.isomorphic?
msg = "Object '#{newobj.class.name}[#{name}]' already exists"
msg += ("in file #{object.file} at line #{object.line}") if exobj.file and exobj.line
msg += ("and cannot be redefined in file #{object.file} at line #{object.line}") if object.file and object.line
error = Puppet::Error.new(msg)
raise error
else
#Puppet.info("adding %s of type %s to class list" %
# [name,object.class])
@objects[name] = newobj
end
end
# Create an alias. We keep these in a separate hash so that we don't encounter
# the objects multiple times when iterating over them.
def self.alias(name, obj)
raise "Global resource aliasing is deprecated"
if @objects.include?(name)
unless @objects[name] == obj
raise Puppet::Error.new(
"Cannot create alias #{name}: object already exists"
)
end
end
if @aliases.include?(name)
unless @aliases[name] == obj
raise Puppet::Error.new(
"Object #{@aliases[name].name} already has alias #{name}"
)
end
end
@aliases[name] = obj
end
# remove all of the instances of a single type
def self.clear
raise "Global resource removal is deprecated"
if defined?(@objects)
@objects.each do |name, obj|
obj.remove(true)
end
@objects.clear
end
@aliases.clear if defined?(@aliases)
end
# Force users to call this, so that we can merge objects if
# necessary.
def self.create(args)
# LAK:DEP Deprecation notice added 12/17/2008
Puppet.warning "Puppet::Type.create is deprecated; use Puppet::Type.new"
new(args)
end
# remove a specified object
def self.delete(resource)
raise "Global resource removal is deprecated"
return unless defined?(@objects)
@objects.delete(resource.title) if @objects.include?(resource.title)
@aliases.delete(resource.title) if @aliases.include?(resource.title)
if @aliases.has_value?(resource)
names = []
@aliases.each do |name, otherres|
if otherres == resource
names << name
end
end
names.each { |name| @aliases.delete(name) }
end
end
# iterate across each of the type's instances
def self.each
raise "Global resource iteration is deprecated"
return unless defined?(@objects)
@objects.each { |name,instance|
yield instance
}
end
# does the type have an object with the given name?
def self.has_key?(name)
raise "Global resource access is deprecated"
@objects.has_key?(name)
end
# Retrieve all known instances. Either requires providers or must be overridden.
def self.instances
raise Puppet::DevError, "#{self.name} has no providers and has not overridden 'instances'" if provider_hash.empty?
# Put the default provider first, then the rest of the suitable providers.
provider_instances = {}
providers_by_source.collect do |provider|
provider.instances.collect do |instance|
# We always want to use the "first" provider instance we find, unless the resource
# is already managed and has a different provider set
if other = provider_instances[instance.name]
Puppet.warning "%s %s found in both %s and %s; skipping the %s version" %
[self.name.to_s.capitalize, instance.name, other.class.name, instance.class.name, instance.class.name]
next
end
provider_instances[instance.name] = instance
new(:name => instance.name, :provider => instance, :audit => :all)
end
end.flatten.compact
end
# Return a list of one suitable provider per source, with the default provider first.
def self.providers_by_source
# Put the default provider first, then the rest of the suitable providers.
sources = []
[defaultprovider, suitableprovider].flatten.uniq.collect do |provider|
next if sources.include?(provider.source)
sources << provider.source
provider
end.compact
end
# Convert a simple hash into a Resource instance.
def self.hash2resource(hash)
hash = hash.inject({}) { |result, ary| result[ary[0].to_sym] = ary[1]; result }
title = hash.delete(:title)
title ||= hash[:name]
title ||= hash[key_attributes.first] if key_attributes.length == 1
raise Puppet::Error, "Title or name must be provided" unless title
# Now create our resource.
resource = Puppet::Resource.new(self.name, title)
[:catalog].each do |attribute|
if value = hash[attribute]
hash.delete(attribute)
resource.send(attribute.to_s + "=", value)
end
end
hash.each do |param, value|
resource[param] = value
end
resource
end
# Create the path for logging and such.
def pathbuilder
if p = parent
[p.pathbuilder, self.ref].flatten
else
[self.ref]
end
end
###############################
# Add all of the meta parameters.
newmetaparam(:noop) do
desc "Boolean flag indicating whether work should actually
be done."
newvalues(:true, :false)
munge do |value|
case value
when true, :true, "true"; @resource.noop = true
when false, :false, "false"; @resource.noop = false
end
end
end
newmetaparam(:schedule) do
desc "On what schedule the object should be managed. You must create a
schedule object, and then reference the name of that object to use
that for your schedule::
schedule { daily:
period => daily,
range => \"2-4\"
}
exec { \"/usr/bin/apt-get update\":
schedule => daily
}
The creation of the schedule object does not need to appear in the
configuration before objects that use it."
end
newmetaparam(:audit) do
desc "Audit specified attributes of resources over time, and report if any have changed.
This attribute can be used to track changes to any resource over time, and can
provide an audit trail of every change that happens on any given machine.
Note that you cannot both audit and manage an attribute - managing it guarantees
the value, and any changes already get logged."
validate do |list|
list = Array(list)
unless list == [:all]
list.each do |param|
next if @resource.class.validattr?(param)
fail "Cannot audit #{param}: not a valid attribute for #{resource}"
end
end
end
munge do |args|
properties_to_audit(args).each do |param|
next unless resource.class.validproperty?(param)
resource.newattr(param)
end
end
def all_properties
resource.class.properties.find_all do |property|
resource.provider.nil? or resource.provider.class.supports_parameter?(property)
end.collect do |property|
property.name
end
end
def properties_to_audit(list)
if list == :all
list = all_properties if list == :all
else
list = Array(list).collect { |p| p.to_sym }
end
end
end
newmetaparam(:check) do
desc "Audit specified attributes of resources over time, and report if any have changed.
This parameter has been deprecated in favor of 'audit'."
munge do |args|
resource.warning "'check' attribute is deprecated; use 'audit' instead"
resource[:audit] = args
end
end
newmetaparam(:loglevel) do
desc "Sets the level that information will be logged.
The log levels have the biggest impact when logs are sent to
syslog (which is currently the default)."
defaultto :notice
newvalues(*Puppet::Util::Log.levels)
newvalues(:verbose)
munge do |loglevel|
val = super(loglevel)
if val == :verbose
val = :info
end
val
end
end
newmetaparam(:alias) do
desc "Creates an alias for the object. Puppet uses this internally when you
provide a symbolic name::
file { sshdconfig:
path => $operatingsystem ? {
solaris => \"/usr/local/etc/ssh/sshd_config\",
default => \"/etc/ssh/sshd_config\"
},
source => \"...\"
}
service { sshd:
subscribe => file[sshdconfig]
}
When you use this feature, the parser sets ``sshdconfig`` as the name,
and the library sets that as an alias for the file so the dependency
lookup for ``sshd`` works. You can use this parameter yourself,
but note that only the library can use these aliases; for instance,
the following code will not work::
file { \"/etc/ssh/sshd_config\":
owner => root,
group => root,
alias => sshdconfig
}
file { sshdconfig:
mode => 644
}
There's no way here for the Puppet parser to know that these two stanzas
should be affecting the same file.
See the `LanguageTutorial language tutorial`:trac: for more information.
"
munge do |aliases|
aliases = [aliases] unless aliases.is_a?(Array)
raise(ArgumentError, "Cannot add aliases without a catalog") unless @resource.catalog
aliases.each do |other|
if obj = @resource.catalog.resource(@resource.class.name, other)
unless obj.object_id == @resource.object_id
self.fail("#{@resource.title} can not create alias #{other}: object already exists")
end
next
end
# Newschool, add it to the catalog.
@resource.catalog.alias(@resource, other)
end
end
end
newmetaparam(:tag) do
desc "Add the specified tags to the associated resource. While all resources
are automatically tagged with as much information as possible
(e.g., each class and definition containing the resource), it can
be useful to add your own tags to a given resource.
Tags are currently useful for things like applying a subset of a
host's configuration::
puppet agent --test --tags mytag
This way, when you're testing a configuration you can run just the
portion you're testing."
munge do |tags|
tags = [tags] unless tags.is_a? Array
tags.each do |tag|
@resource.tag(tag)
end
end
end
class RelationshipMetaparam < Puppet::Parameter
class << self
attr_accessor :direction, :events, :callback, :subclasses
end
@subclasses = []
def self.inherited(sub)
@subclasses << sub
end
def munge(references)
references = [references] unless references.is_a?(Array)
references.collect do |ref|
if ref.is_a?(Puppet::Resource)
ref
else
Puppet::Resource.new(ref)
end
end
end
def validate_relationship
@value.each do |ref|
unless @resource.catalog.resource(ref.to_s)
description = self.class.direction == :in ? "dependency" : "dependent"
fail "Could not find #{description} #{ref} for #{resource.ref}"
end
end
end
# Create edges from each of our relationships. :in
# relationships are specified by the event-receivers, and :out
# relationships are specified by the event generator. This
# way 'source' and 'target' are consistent terms in both edges
# and events -- that is, an event targets edges whose source matches
# the event's source. The direction of the relationship determines
# which resource is applied first and which resource is considered
# to be the event generator.
def to_edges
@value.collect do |reference|
reference.catalog = resource.catalog
# Either of the two retrieval attempts could have returned
# nil.
unless related_resource = reference.resolve
self.fail "Could not retrieve dependency '#{reference}' of #{@resource.ref}"
end
# Are we requiring them, or vice versa? See the method docs
# for futher info on this.
if self.class.direction == :in
source = related_resource
target = @resource
else
source = @resource
target = related_resource
end
if method = self.class.callback
subargs = {
:event => self.class.events,
:callback => method
}
self.debug("subscribes to #{related_resource.ref}")
else
# If there's no callback, there's no point in even adding
# a label.
subargs = nil
self.debug("requires #{related_resource.ref}")
end
rel = Puppet::Relationship.new(source, target, subargs)
end
end
end
def self.relationship_params
RelationshipMetaparam.subclasses
end
# Note that the order in which the relationships params is defined
# matters. The labelled params (notify and subcribe) must be later,
# so that if both params are used, those ones win. It's a hackish
# solution, but it works.
newmetaparam(:require, :parent => RelationshipMetaparam, :attributes => {:direction => :in, :events => :NONE}) do
desc "One or more objects that this object depends on.
This is used purely for guaranteeing that changes to required objects
happen before the dependent object. For instance::
# Create the destination directory before you copy things down
file { \"/usr/local/scripts\":
ensure => directory
}
file { \"/usr/local/scripts/myscript\":
source => \"puppet://server/module/myscript\",
mode => 755,
require => File[\"/usr/local/scripts\"]
}
Multiple dependencies can be specified by providing a comma-seperated list
of resources, enclosed in square brackets::
require => [ File[\"/usr/local\"], File[\"/usr/local/scripts\"] ]
Note that Puppet will autorequire everything that it can, and
there are hooks in place so that it's easy for resources to add new
ways to autorequire objects, so if you think Puppet could be
smarter here, let us know.
In fact, the above code was redundant -- Puppet will autorequire
any parent directories that are being managed; it will
automatically realize that the parent directory should be created
before the script is pulled down.
Currently, exec resources will autorequire their CWD (if it is
specified) plus any fully qualified paths that appear in the
command. For instance, if you had an ``exec`` command that ran
the ``myscript`` mentioned above, the above code that pulls the
file down would be automatically listed as a requirement to the
``exec`` code, so that you would always be running againts the
most recent version.
"
end
newmetaparam(:subscribe, :parent => RelationshipMetaparam, :attributes => {:direction => :in, :events => :ALL_EVENTS, :callback => :refresh}) do
desc "One or more objects that this object depends on. Changes in the
subscribed to objects result in the dependent objects being
refreshed (e.g., a service will get restarted). For instance::
class nagios {
file { \"/etc/nagios/nagios.conf\":
source => \"puppet://server/module/nagios.conf\",
alias => nagconf # just to make things easier for me
}
service { nagios:
ensure => running,
subscribe => File[nagconf]
}
}
Currently the ``exec``, ``mount`` and ``service`` type support
refreshing.
"
end
newmetaparam(:before, :parent => RelationshipMetaparam, :attributes => {:direction => :out, :events => :NONE}) do
desc %{This parameter is the opposite of **require** -- it guarantees
that the specified object is applied later than the specifying
object::
file { "/var/nagios/configuration":
source => "...",
recurse => true,
before => Exec["nagios-rebuid"]
}
exec { "nagios-rebuild":
command => "/usr/bin/make",
cwd => "/var/nagios/configuration"
}
This will make sure all of the files are up to date before the
make command is run.}
end
newmetaparam(:notify, :parent => RelationshipMetaparam, :attributes => {:direction => :out, :events => :ALL_EVENTS, :callback => :refresh}) do
desc %{This parameter is the opposite of **subscribe** -- it sends events
to the specified object::
file { "/etc/sshd_config":
source => "....",
notify => Service[sshd]
}
service { sshd:
ensure => running
}
This will restart the sshd service if the sshd config file changes.}
end
newmetaparam(:stage) do
desc %{Which run stage a given resource should reside in. This just creates
a dependency on or from the named milestone. For instance, saying that
this is in the 'bootstrap' stage creates a dependency on the 'bootstrap'
milestone.
By default, all classes get directly added to the
'main' stage. You can create new stages as resources:
stage { [pre, post]: }
To order stages, use standard relationships:
stage { pre: before => Stage[main] }
Or use the new relationship syntax:
Stage[pre] -> Stage[main] -> Stage[post]
Then use the new class parameters to specify a stage:
class { foo: stage => pre }
Stages can only be set on classes, not individual resources. This will
fail::
file { '/foo': stage => pre, ensure => file }
}
end
###############################
# All of the provider plumbing for the resource types.
require 'puppet/provider'
require 'puppet/util/provider_features'
# Add the feature handling module.
extend Puppet::Util::ProviderFeatures
attr_reader :provider
# the Type class attribute accessors
class << self
attr_accessor :providerloader
attr_writer :defaultprovider
end
# Find the default provider.
def self.defaultprovider
unless @defaultprovider
suitable = suitableprovider
# Find which providers are a default for this system.
defaults = suitable.find_all { |provider| provider.default? }
# If we don't have any default we use suitable providers
defaults = suitable if defaults.empty?
max = defaults.collect { |provider| provider.specificity }.max
defaults = defaults.find_all { |provider| provider.specificity == max }
retval = nil
if defaults.length > 1
Puppet.warning(
"Found multiple default providers for #{self.name}: #{defaults.collect { |i| i.name.to_s }.join(", ")}; using #{defaults[0].name}"
)
retval = defaults.shift
elsif defaults.length == 1
retval = defaults.shift
else
raise Puppet::DevError, "Could not find a default provider for #{self.name}"
end
@defaultprovider = retval
end
@defaultprovider
end
def self.provider_hash_by_type(type)
@provider_hashes ||= {}
@provider_hashes[type] ||= {}
end
def self.provider_hash
Puppet::Type.provider_hash_by_type(self.name)
end
# Retrieve a provider by name.
def self.provider(name)
name = Puppet::Util.symbolize(name)
# If we don't have it yet, try loading it.
@providerloader.load(name) unless provider_hash.has_key?(name)
provider_hash[name]
end
# Just list all of the providers.
def self.providers
provider_hash.keys
end
def self.validprovider?(name)
name = Puppet::Util.symbolize(name)
(provider_hash.has_key?(name) && provider_hash[name].suitable?)
end
# Create a new provider of a type. This method must be called
# directly on the type that it's implementing.
def self.provide(name, options = {}, &block)
name = Puppet::Util.symbolize(name)
if obj = provider_hash[name]
Puppet.debug "Reloading #{name} #{self.name} provider"
unprovide(name)
end
parent = if pname = options[:parent]
options.delete(:parent)
if pname.is_a? Class
pname
else
if provider = self.provider(pname)
provider
else
raise Puppet::DevError,
"Could not find parent provider #{pname} of #{name}"
end
end
else
Puppet::Provider
end
options[:resource_type] ||= self
self.providify
provider = genclass(
name,
:parent => parent,
:hash => provider_hash,
:prefix => "Provider",
:block => block,
:include => feature_module,
:extend => feature_module,
:attributes => options
)
provider
end
# Make sure we have a :provider parameter defined. Only gets called if there
# are providers.
def self.providify
return if @paramhash.has_key? :provider
newparam(:provider) do
desc "The specific backend for #{self.name.to_s} to use. You will
seldom need to specify this -- Puppet will usually discover the
appropriate provider for your platform."
# This is so we can refer back to the type to get a list of
# providers for documentation.
class << self
attr_accessor :parenttype
end
# We need to add documentation for each provider.
def self.doc
@doc + " Available providers are:\n\n" + parenttype.providers.sort { |a,b|
a.to_s <=> b.to_s
}.collect { |i|
"* **#{i}**: #{parenttype().provider(i).doc}"
}.join("\n")
end
defaultto {
@resource.class.defaultprovider.name
}
validate do |provider_class|
provider_class = provider_class[0] if provider_class.is_a? Array
provider_class = provider_class.class.name if provider_class.is_a?(Puppet::Provider)
unless provider = @resource.class.provider(provider_class)
raise ArgumentError, "Invalid #{@resource.class.name} provider '#{provider_class}'"
end
end
munge do |provider|
provider = provider[0] if provider.is_a? Array
provider = provider.intern if provider.is_a? String
@resource.provider = provider
if provider.is_a?(Puppet::Provider)
provider.class.name
else
provider
end
end
end.parenttype = self
end
def self.unprovide(name)
if provider_hash.has_key? name
rmclass(
name,
:hash => provider_hash,
:prefix => "Provider"
)
if @defaultprovider and @defaultprovider.name == name
@defaultprovider = nil
end
end
end
# Return an array of all of the suitable providers.
def self.suitableprovider
providerloader.loadall if provider_hash.empty?
provider_hash.find_all { |name, provider|
provider.suitable?
}.collect { |name, provider|
provider
}.reject { |p| p.name == :fake } # For testing
end
def provider=(name)
if name.is_a?(Puppet::Provider)
@provider = name
@provider.resource = self
elsif klass = self.class.provider(name)
@provider = klass.new(self)
else
raise ArgumentError, "Could not find #{name} provider of #{self.class.name}"
end
end
###############################
# All of the relationship code.
# Specify a block for generating a list of objects to autorequire. This
# makes it so that you don't have to manually specify things that you clearly
# require.
def self.autorequire(name, &block)
@autorequires ||= {}
@autorequires[name] = block
end
# Yield each of those autorequires in turn, yo.
def self.eachautorequire
@autorequires ||= {}
@autorequires.each { |type, block|
yield(type, block)
}
end
# Figure out of there are any objects we can automatically add as
# dependencies.
def autorequire(rel_catalog = nil)
rel_catalog ||= catalog
raise(Puppet::DevError, "You cannot add relationships without a catalog") unless rel_catalog
reqs = []
self.class.eachautorequire { |type, block|
# Ignore any types we can't find, although that would be a bit odd.
next unless typeobj = Puppet::Type.type(type)
# Retrieve the list of names from the block.
next unless list = self.instance_eval(&block)
list = [list] unless list.is_a?(Array)
# Collect the current prereqs
list.each { |dep|
obj = nil
# Support them passing objects directly, to save some effort.
unless dep.is_a? Puppet::Type
# Skip autorequires that we aren't managing
unless dep = rel_catalog.resource(type, dep)
next
end
end
reqs << Puppet::Relationship.new(dep, self)
}
}
reqs
end
# Build the dependencies associated with an individual object.
def builddepends
# Handle the requires
self.class.relationship_params.collect do |klass|
if param = @parameters[klass.name]
param.to_edges
end
end.flatten.reject { |r| r.nil? }
end
# Define the initial list of tags.
def tags=(list)
tag(self.class.name)
tag(*list)
end
# Types (which map to resources in the languages) are entirely composed of
# attribute value pairs. Generally, Puppet calls any of these things an
# 'attribute', but these attributes always take one of three specific
# forms: parameters, metaparams, or properties.
# In naming methods, I have tried to consistently name the method so
# that it is clear whether it operates on all attributes (thus has 'attr' in
# the method name, or whether it operates on a specific type of attributes.
attr_writer :title
attr_writer :noop
include Enumerable
# class methods dealing with Type management
public
# the Type class attribute accessors
class << self
attr_reader :name
attr_accessor :self_refresh
include Enumerable, Puppet::Util::ClassGen
include Puppet::MetaType::Manager
include Puppet::Util
include Puppet::Util::Logging
end
# all of the variables that must be initialized for each subclass
def self.initvars
# all of the instances of this class
@objects = Hash.new
@aliases = Hash.new
@defaults = {}
@parameters ||= []
@validproperties = {}
@properties = []
@parameters = []
@paramhash = {}
@attr_aliases = {}
@paramdoc = Hash.new { |hash,key|
key = key.intern if key.is_a?(String)
if hash.include?(key)
hash[key]
else
"Param Documentation for #{key} not found"
end
}
@doc ||= ""
end
def self.to_s
if defined?(@name)
"Puppet::Type::#{@name.to_s.capitalize}"
else
super
end
end
# Create a block to validate that our object is set up entirely. This will
# be run before the object is operated on.
def self.validate(&block)
define_method(:validate, &block)
#@validate = block
end
# The catalog that this resource is stored in.
attr_accessor :catalog
# is the resource exported
attr_accessor :exported
# is the resource virtual (it should not :-))
attr_accessor :virtual
# create a log at specified level
def log(msg)
Puppet::Util::Log.create(
:level => @parameters[:loglevel].value,
:message => msg,
:source => self
)
end
# instance methods related to instance intrinsics
# e.g., initialize and name
public
attr_reader :original_parameters
# initialize the type instance
def initialize(resource)
raise Puppet::DevError, "Got TransObject instead of Resource or hash" if resource.is_a?(Puppet::TransObject)
resource = self.class.hash2resource(resource) unless resource.is_a?(Puppet::Resource)
# The list of parameter/property instances.
@parameters = {}
# Set the title first, so any failures print correctly.
if resource.type.to_s.downcase.to_sym == self.class.name
self.title = resource.title
else
# This should only ever happen for components
self.title = resource.ref
end
[:file, :line, :catalog, :exported, :virtual].each do |getter|
setter = getter.to_s + "="
if val = resource.send(getter)
self.send(setter, val)
end
end
@tags = resource.tags
@original_parameters = resource.to_hash
set_name(@original_parameters)
set_default(:provider)
set_parameters(@original_parameters)
self.validate if self.respond_to?(:validate)
end
private
# Set our resource's name.
def set_name(hash)
self[name_var] = hash.delete(name_var) if name_var
end
# Set all of the parameters from a hash, in the appropriate order.
def set_parameters(hash)
# Use the order provided by allattrs, but add in any
# extra attributes from the resource so we get failures
# on invalid attributes.
no_values = []
(self.class.allattrs + hash.keys).uniq.each do |attr|
begin
# Set any defaults immediately. This is mostly done so
# that the default provider is available for any other
# property validation.
if hash.has_key?(attr)
self[attr] = hash[attr]
else
no_values << attr
end
rescue ArgumentError, Puppet::Error, TypeError
raise
rescue => detail
error = Puppet::DevError.new( "Could not set #{attr} on #{self.class.name}: #{detail}")
error.set_backtrace(detail.backtrace)
raise error
end
end
no_values.each do |attr|
set_default(attr)
end
end
public
# Set up all of our autorequires.
def finish
# Make sure all of our relationships are valid. Again, must be done
# when the entire catalog is instantiated.
self.class.relationship_params.collect do |klass|
if param = @parameters[klass.name]
param.validate_relationship
end
end.flatten.reject { |r| r.nil? }
end
# For now, leave the 'name' method functioning like it used to. Once 'title'
# works everywhere, I'll switch it.
def name
self[:name]
end
# Look up our parent in the catalog, if we have one.
def parent
return nil unless catalog
unless defined?(@parent)
if parents = catalog.adjacent(self, :direction => :in)
# We should never have more than one parent, so let's just ignore
# it if we happen to.
@parent = parents.shift
else
@parent = nil
end
end
@parent
end
# Return the "type[name]" style reference.
def ref
"#{self.class.name.to_s.capitalize}[#{self.title}]"
end
def self_refresh?
self.class.self_refresh
end
# Mark that we're purging.
def purging
@purging = true
end
# Is this resource being purged? Used by transactions to forbid
# deletion when there are dependencies.
def purging?
if defined?(@purging)
@purging
else
false
end
end
# Retrieve the title of an object. If no title was set separately,
# then use the object's name.
def title
unless @title
if self.class.validparameter?(name_var)
@title = self[:name]
elsif self.class.validproperty?(name_var)
@title = self.should(name_var)
else
self.devfail "Could not find namevar #{name_var} for #{self.class.name}"
end
end
@title
end
# convert to a string
def to_s
self.ref
end
# Convert to a transportable object
def to_trans(ret = true)
trans = TransObject.new(self.title, self.class.name)
values = retrieve_resource
values.each do |name, value|
name = name.name if name.respond_to? :name
trans[name] = value
end
@parameters.each do |name, param|
# Avoid adding each instance name twice
next if param.class.isnamevar? and param.value == self.title
# We've already got property values
next if param.is_a?(Puppet::Property)
trans[name] = param.value
end
trans.tags = self.tags
# FIXME I'm currently ignoring 'parent' and 'path'
trans
end
def to_resource
# this 'type instance' versus 'resource' distinction seems artificial
# I'd like to see it collapsed someday ~JW
self.to_trans.to_resource
end
- %w{exported virtual}.each do |m|
- define_method(m+"?") do
- self.send(m)
- end
- end
-
+ def virtual?; !!@virtual; end
+ def exported?; !!@exported; end
end
end
require 'puppet/provider'
# Always load these types.
require 'puppet/type/component'
diff --git a/spec/unit/parser/compiler_spec.rb b/spec/unit/parser/compiler_spec.rb
index a3fe56c0e..e8c06dd0b 100755
--- a/spec/unit/parser/compiler_spec.rb
+++ b/spec/unit/parser/compiler_spec.rb
@@ -1,754 +1,754 @@
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../../spec_helper'
class CompilerTestResource
attr_accessor :builtin, :virtual, :evaluated, :type, :title
def initialize(type, title)
@type = type
@title = title
end
def [](attr)
return nil if attr == :stage
:main
end
def ref
"#{type.to_s.capitalize}[#{title}]"
end
def evaluated?
@evaluated
end
- def builtin?
+ def builtin_type?
@builtin
end
def virtual?
@virtual
end
def evaluate
end
end
describe Puppet::Parser::Compiler do
def resource(type, title)
Puppet::Parser::Resource.new(type, title, :scope => @scope)
end
before :each do
@node = Puppet::Node.new "testnode"
@known_resource_types = Puppet::Resource::TypeCollection.new "development"
@compiler = Puppet::Parser::Compiler.new(@node)
@scope = Puppet::Parser::Scope.new(:compiler => @compiler, :source => stub('source'))
@scope_resource = Puppet::Parser::Resource.new(:file, "/my/file", :scope => @scope)
@scope.resource = @scope_resource
@compiler.environment.stubs(:known_resource_types).returns @known_resource_types
end
it "should have a class method that compiles, converts, and returns a catalog" do
compiler = stub 'compiler'
Puppet::Parser::Compiler.expects(:new).with(@node).returns compiler
catalog = stub 'catalog'
compiler.expects(:compile).returns catalog
converted_catalog = stub 'converted_catalog'
catalog.expects(:to_resource).returns converted_catalog
Puppet::Parser::Compiler.compile(@node).should equal(converted_catalog)
end
it "should fail intelligently when a class-level compile fails" do
Puppet::Parser::Compiler.expects(:new).raises ArgumentError
lambda { Puppet::Parser::Compiler.compile(@node) }.should raise_error(Puppet::Error)
end
it "should use the node's environment as its environment" do
@compiler.environment.should equal(@node.environment)
end
it "should include the resource type collection helper" do
Puppet::Parser::Compiler.ancestors.should be_include(Puppet::Resource::TypeCollectionHelper)
end
it "should be able to return a class list containing all added classes" do
@compiler.add_class ""
@compiler.add_class "one"
@compiler.add_class "two"
@compiler.classlist.sort.should == %w{one two}.sort
end
describe "when initializing" do
it "should set its node attribute" do
@compiler.node.should equal(@node)
end
it "should detect when ast nodes are absent" do
@compiler.ast_nodes?.should be_false
end
it "should detect when ast nodes are present" do
@known_resource_types.expects(:nodes?).returns true
@compiler.ast_nodes?.should be_true
end
it "should copy the known_resource_types version to the catalog" do
@compiler.catalog.version.should == @known_resource_types.version
end
it "should copy any node classes into the class list" do
node = Puppet::Node.new("mynode")
node.classes = %w{foo bar}
compiler = Puppet::Parser::Compiler.new(node)
compiler.classlist.should include("foo")
compiler.classlist.should include("bar")
end
it "should add a 'main' stage to the catalog" do
@compiler.catalog.resource(:stage, :main).should be_instance_of(Puppet::Parser::Resource)
end
end
describe "when managing scopes" do
it "should create a top scope" do
@compiler.topscope.should be_instance_of(Puppet::Parser::Scope)
end
it "should be able to create new scopes" do
@compiler.newscope(@compiler.topscope).should be_instance_of(Puppet::Parser::Scope)
end
it "should correctly set the level of newly created scopes" do
@compiler.newscope(@compiler.topscope, :level => 5).level.should == 5
end
it "should set the parent scope of the new scope to be the passed-in parent" do
scope = mock 'scope'
newscope = @compiler.newscope(scope)
newscope.parent.should equal(scope)
end
it "should set the parent scope of the new scope to its topscope if the parent passed in is nil" do
scope = mock 'scope'
newscope = @compiler.newscope(nil)
newscope.parent.should equal(@compiler.topscope)
end
end
describe "when compiling" do
def compile_methods
[:set_node_parameters, :evaluate_main, :evaluate_ast_node, :evaluate_node_classes, :evaluate_generators, :fail_on_unevaluated,
:finish, :store, :extract, :evaluate_relationships]
end
# Stub all of the main compile methods except the ones we're specifically interested in.
def compile_stub(*except)
(compile_methods - except).each { |m| @compiler.stubs(m) }
end
it "should set node parameters as variables in the top scope" do
params = {"a" => "b", "c" => "d"}
@node.stubs(:parameters).returns(params)
compile_stub(:set_node_parameters)
@compiler.compile
@compiler.topscope.lookupvar("a").should == "b"
@compiler.topscope.lookupvar("c").should == "d"
end
it "should set the client and server versions on the catalog" do
params = {"clientversion" => "2", "serverversion" => "3"}
@node.stubs(:parameters).returns(params)
compile_stub(:set_node_parameters)
@compiler.compile
@compiler.catalog.client_version.should == "2"
@compiler.catalog.server_version.should == "3"
end
it "should evaluate any existing classes named in the node" do
classes = %w{one two three four}
main = stub 'main'
one = stub 'one', :name => "one"
three = stub 'three', :name => "three"
@node.stubs(:name).returns("whatever")
@node.stubs(:classes).returns(classes)
@compiler.expects(:evaluate_classes).with(classes, @compiler.topscope)
@compiler.class.publicize_methods(:evaluate_node_classes) { @compiler.evaluate_node_classes }
end
it "should evaluate the main class if it exists" do
compile_stub(:evaluate_main)
main_class = @known_resource_types.add Puppet::Resource::Type.new(:hostclass, "")
main_class.expects(:evaluate_code).with { |r| r.is_a?(Puppet::Parser::Resource) }
@compiler.topscope.expects(:source=).with(main_class)
@compiler.compile
end
it "should create a new, empty 'main' if no main class exists" do
compile_stub(:evaluate_main)
@compiler.compile
@known_resource_types.find_hostclass([""], "").should be_instance_of(Puppet::Resource::Type)
end
it "should add an edge between the main stage and main class" do
@compiler.compile
(stage = @compiler.catalog.resource(:stage, "main")).should be_instance_of(Puppet::Parser::Resource)
(klass = @compiler.catalog.resource(:class, "")).should be_instance_of(Puppet::Parser::Resource)
@compiler.catalog.edge?(stage, klass).should be_true
end
it "should evaluate any node classes" do
@node.stubs(:classes).returns(%w{one two three four})
@compiler.expects(:evaluate_classes).with(%w{one two three four}, @compiler.topscope)
@compiler.send(:evaluate_node_classes)
end
it "should evaluate all added collections" do
colls = []
# And when the collections fail to evaluate.
colls << mock("coll1-false")
colls << mock("coll2-false")
colls.each { |c| c.expects(:evaluate).returns(false) }
@compiler.add_collection(colls[0])
@compiler.add_collection(colls[1])
compile_stub(:evaluate_generators)
@compiler.compile
end
it "should ignore builtin resources" do
resource = resource(:file, "testing")
@compiler.add_resource(@scope, resource)
resource.expects(:evaluate).never
@compiler.compile
end
it "should evaluate unevaluated resources" do
resource = CompilerTestResource.new(:file, "testing")
@compiler.add_resource(@scope, resource)
# We have to now mark the resource as evaluated
resource.expects(:evaluate).with { |*whatever| resource.evaluated = true }
@compiler.compile
end
it "should not evaluate already-evaluated resources" do
resource = resource(:file, "testing")
resource.stubs(:evaluated?).returns true
@compiler.add_resource(@scope, resource)
resource.expects(:evaluate).never
@compiler.compile
end
it "should evaluate unevaluated resources created by evaluating other resources" do
resource = CompilerTestResource.new(:file, "testing")
@compiler.add_resource(@scope, resource)
resource2 = CompilerTestResource.new(:file, "other")
# We have to now mark the resource as evaluated
resource.expects(:evaluate).with { |*whatever| resource.evaluated = true; @compiler.add_resource(@scope, resource2) }
resource2.expects(:evaluate).with { |*whatever| resource2.evaluated = true }
@compiler.compile
end
describe "when finishing" do
before do
@compiler.send(:evaluate_main)
@catalog = @compiler.catalog
end
def add_resource(name, parent = nil)
resource = Puppet::Parser::Resource.new "file", name, :scope => @scope
@compiler.add_resource(@scope, resource)
@catalog.add_edge(parent, resource) if parent
resource
end
it "should call finish() on all resources" do
# Add a resource that does respond to :finish
resource = Puppet::Parser::Resource.new "file", "finish", :scope => @scope
resource.expects(:finish)
@compiler.add_resource(@scope, resource)
# And one that does not
dnf_resource = stub_everything "dnf", :ref => "File[dnf]", :type => "file"
@compiler.add_resource(@scope, dnf_resource)
@compiler.send(:finish)
end
it "should call finish() in add_resource order" do
resources = sequence('resources')
resource1 = add_resource("finish1")
resource1.expects(:finish).in_sequence(resources)
resource2 = add_resource("finish2")
resource2.expects(:finish).in_sequence(resources)
@compiler.send(:finish)
end
it "should add each container's metaparams to its contained resources" do
main = @catalog.resource(:class, :main)
main[:noop] = true
resource1 = add_resource("meh", main)
@compiler.send(:finish)
resource1[:noop].should be_true
end
it "should add metaparams recursively" do
main = @catalog.resource(:class, :main)
main[:noop] = true
resource1 = add_resource("meh", main)
resource2 = add_resource("foo", resource1)
@compiler.send(:finish)
resource2[:noop].should be_true
end
it "should prefer metaparams from immediate parents" do
main = @catalog.resource(:class, :main)
main[:noop] = true
resource1 = add_resource("meh", main)
resource2 = add_resource("foo", resource1)
resource1[:noop] = false
@compiler.send(:finish)
resource2[:noop].should be_false
end
it "should merge tags downward" do
main = @catalog.resource(:class, :main)
main.tag("one")
resource1 = add_resource("meh", main)
resource1.tag "two"
resource2 = add_resource("foo", resource1)
@compiler.send(:finish)
resource2.tags.should be_include("one")
resource2.tags.should be_include("two")
end
it "should work if only middle resources have metaparams set" do
main = @catalog.resource(:class, :main)
resource1 = add_resource("meh", main)
resource1[:noop] = true
resource2 = add_resource("foo", resource1)
@compiler.send(:finish)
resource2[:noop].should be_true
end
end
it "should return added resources in add order" do
resource1 = resource(:file, "yay")
@compiler.add_resource(@scope, resource1)
resource2 = resource(:file, "youpi")
@compiler.add_resource(@scope, resource2)
@compiler.resources.should == [resource1, resource2]
end
it "should add resources that do not conflict with existing resources" do
resource = resource(:file, "yay")
@compiler.add_resource(@scope, resource)
@compiler.catalog.should be_vertex(resource)
end
it "should fail to add resources that conflict with existing resources" do
path = Puppet.features.posix? ? "/foo" : "C:/foo"
file1 = Puppet::Type.type(:file).new :path => path
file2 = Puppet::Type.type(:file).new :path => path
@compiler.add_resource(@scope, file1)
lambda { @compiler.add_resource(@scope, file2) }.should raise_error(Puppet::Resource::Catalog::DuplicateResourceError)
end
it "should add an edge from the scope resource to the added resource" do
resource = resource(:file, "yay")
@compiler.add_resource(@scope, resource)
@compiler.catalog.should be_edge(@scope.resource, resource)
end
it "should add an edge to any specified stage for class resources" do
other_stage = resource(:stage, "other")
@compiler.add_resource(@scope, other_stage)
resource = resource(:class, "foo")
resource[:stage] = 'other'
@compiler.add_resource(@scope, resource)
@compiler.catalog.edge?(other_stage, resource).should be_true
end
it "should fail if a non-class resource attempts to set a stage" do
other_stage = resource(:stage, "other")
@compiler.add_resource(@scope, other_stage)
resource = resource(:file, "foo")
resource[:stage] = 'other'
lambda { @compiler.add_resource(@scope, resource) }.should raise_error(ArgumentError)
end
it "should fail if an unknown stage is specified" do
resource = resource(:class, "foo")
resource[:stage] = 'other'
lambda { @compiler.add_resource(@scope, resource) }.should raise_error(ArgumentError)
end
it "should add edges from the class resources to the main stage if no stage is specified" do
main = @compiler.catalog.resource(:stage, :main)
resource = resource(:class, "foo")
@compiler.add_resource(@scope, resource)
@compiler.catalog.should be_edge(main, resource)
end
it "should not add non-class resources that don't specify a stage to the 'main' stage" do
main = @compiler.catalog.resource(:stage, :main)
resource = resource(:file, "foo")
@compiler.add_resource(@scope, resource)
@compiler.catalog.should_not be_edge(main, resource)
end
it "should not add any parent-edges to stages" do
stage = resource(:stage, "other")
@compiler.add_resource(@scope, stage)
@scope.resource = resource(:class, "foo")
@compiler.catalog.edge?(@scope.resource, stage).should be_false
end
it "should not attempt to add stages to other stages" do
other_stage = resource(:stage, "other")
second_stage = resource(:stage, "second")
@compiler.add_resource(@scope, other_stage)
@compiler.add_resource(@scope, second_stage)
second_stage[:stage] = "other"
@compiler.catalog.edge?(other_stage, second_stage).should be_false
end
it "should have a method for looking up resources" do
resource = resource(:yay, "foo")
@compiler.add_resource(@scope, resource)
@compiler.findresource("Yay[foo]").should equal(resource)
end
it "should be able to look resources up by type and title" do
resource = resource(:yay, "foo")
@compiler.add_resource(@scope, resource)
@compiler.findresource("Yay", "foo").should equal(resource)
end
it "should not evaluate virtual defined resources" do
resource = resource(:file, "testing")
resource.virtual = true
@compiler.add_resource(@scope, resource)
resource.expects(:evaluate).never
@compiler.compile
end
end
describe "when evaluating collections" do
it "should evaluate each collection" do
2.times { |i|
coll = mock 'coll%s' % i
@compiler.add_collection(coll)
# This is the hard part -- we have to emulate the fact that
# collections delete themselves if they are done evaluating.
coll.expects(:evaluate).with do
@compiler.delete_collection(coll)
end
}
@compiler.class.publicize_methods(:evaluate_collections) { @compiler.evaluate_collections }
end
it "should not fail when there are unevaluated resource collections that do not refer to specific resources" do
coll = stub 'coll', :evaluate => false
coll.expects(:resources).returns(nil)
@compiler.add_collection(coll)
lambda { @compiler.compile }.should_not raise_error
end
it "should fail when there are unevaluated resource collections that refer to a specific resource" do
coll = stub 'coll', :evaluate => false
coll.expects(:resources).returns(:something)
@compiler.add_collection(coll)
lambda { @compiler.compile }.should raise_error(Puppet::ParseError)
end
it "should fail when there are unevaluated resource collections that refer to multiple specific resources" do
coll = stub 'coll', :evaluate => false
coll.expects(:resources).returns([:one, :two])
@compiler.add_collection(coll)
lambda { @compiler.compile }.should raise_error(Puppet::ParseError)
end
end
describe "when evaluating relationships" do
it "should evaluate each relationship with its catalog" do
dep = stub 'dep'
dep.expects(:evaluate).with(@compiler.catalog)
@compiler.add_relationship dep
@compiler.evaluate_relationships
end
end
describe "when told to evaluate missing classes" do
it "should fail if there's no source listed for the scope" do
scope = stub 'scope', :source => nil
proc { @compiler.evaluate_classes(%w{one two}, scope) }.should raise_error(Puppet::DevError)
end
it "should tag the catalog with the name of each not-found class" do
@compiler.catalog.expects(:tag).with("notfound")
@scope.expects(:find_hostclass).with("notfound").returns(nil)
@compiler.evaluate_classes(%w{notfound}, @scope)
end
end
describe "when evaluating found classes" do
before do
@class = stub 'class', :name => "my::class"
@scope.stubs(:find_hostclass).with("myclass").returns(@class)
@resource = stub 'resource', :ref => "Class[myclass]", :type => "file"
end
it "should evaluate each class" do
@compiler.catalog.stubs(:tag)
@class.expects(:mk_plain_resource).with(@scope)
@scope.stubs(:class_scope).with(@class)
@compiler.evaluate_classes(%w{myclass}, @scope)
end
it "should not evaluate the resources created for found classes unless asked" do
@compiler.catalog.stubs(:tag)
@resource.expects(:evaluate).never
@class.expects(:mk_plain_resource).returns(@resource)
@scope.stubs(:class_scope).with(@class)
@compiler.evaluate_classes(%w{myclass}, @scope)
end
it "should immediately evaluate the resources created for found classes when asked" do
@compiler.catalog.stubs(:tag)
@resource.expects(:evaluate)
@class.expects(:mk_plain_resource).returns(@resource)
@scope.stubs(:class_scope).with(@class)
@compiler.evaluate_classes(%w{myclass}, @scope, false)
end
it "should skip classes that have already been evaluated" do
@compiler.catalog.stubs(:tag)
@scope.stubs(:class_scope).with(@class).returns("something")
@compiler.expects(:add_resource).never
@resource.expects(:evaluate).never
Puppet::Parser::Resource.expects(:new).never
@compiler.evaluate_classes(%w{myclass}, @scope, false)
end
it "should skip classes previously evaluated with different capitalization" do
@compiler.catalog.stubs(:tag)
@scope.stubs(:find_hostclass).with("MyClass").returns(@class)
@scope.stubs(:class_scope).with(@class).returns("something")
@compiler.expects(:add_resource).never
@resource.expects(:evaluate).never
Puppet::Parser::Resource.expects(:new).never
@compiler.evaluate_classes(%w{MyClass}, @scope, false)
end
it "should return the list of found classes" do
@compiler.catalog.stubs(:tag)
@compiler.stubs(:add_resource)
@scope.stubs(:find_hostclass).with("notfound").returns(nil)
@scope.stubs(:class_scope).with(@class)
Puppet::Parser::Resource.stubs(:new).returns(@resource)
@class.stubs :mk_plain_resource
@compiler.evaluate_classes(%w{myclass notfound}, @scope).should == %w{myclass}
end
end
describe "when evaluating AST nodes with no AST nodes present" do
it "should do nothing" do
@compiler.expects(:ast_nodes?).returns(false)
@compiler.known_resource_types.expects(:nodes).never
Puppet::Parser::Resource.expects(:new).never
@compiler.send(:evaluate_ast_node)
end
end
describe "when evaluating AST nodes with AST nodes present" do
before do
@compiler.known_resource_types.stubs(:nodes?).returns true
# Set some names for our test
@node.stubs(:names).returns(%w{a b c})
@compiler.known_resource_types.stubs(:node).with("a").returns(nil)
@compiler.known_resource_types.stubs(:node).with("b").returns(nil)
@compiler.known_resource_types.stubs(:node).with("c").returns(nil)
# It should check this last, of course.
@compiler.known_resource_types.stubs(:node).with("default").returns(nil)
end
it "should fail if the named node cannot be found" do
proc { @compiler.send(:evaluate_ast_node) }.should raise_error(Puppet::ParseError)
end
it "should evaluate the first node class matching the node name" do
node_class = stub 'node', :name => "c", :evaluate_code => nil
@compiler.known_resource_types.stubs(:node).with("c").returns(node_class)
node_resource = stub 'node resource', :ref => "Node[c]", :evaluate => nil, :type => "node"
node_class.expects(:mk_plain_resource).returns(node_resource)
@compiler.compile
end
it "should match the default node if no matching node can be found" do
node_class = stub 'node', :name => "default", :evaluate_code => nil
@compiler.known_resource_types.stubs(:node).with("default").returns(node_class)
node_resource = stub 'node resource', :ref => "Node[default]", :evaluate => nil, :type => "node"
node_class.expects(:mk_plain_resource).returns(node_resource)
@compiler.compile
end
it "should evaluate the node resource immediately rather than using lazy evaluation" do
node_class = stub 'node', :name => "c"
@compiler.known_resource_types.stubs(:node).with("c").returns(node_class)
node_resource = stub 'node resource', :ref => "Node[c]", :type => "node"
node_class.expects(:mk_plain_resource).returns(node_resource)
node_resource.expects(:evaluate)
@compiler.send(:evaluate_ast_node)
end
it "should set the node's scope as the top scope" do
node_resource = stub 'node resource', :ref => "Node[c]", :evaluate => nil, :type => "node"
node_class = stub 'node', :name => "c", :mk_plain_resource => node_resource
@compiler.known_resource_types.stubs(:node).with("c").returns(node_class)
# The #evaluate method normally does this.
scope = stub 'scope', :source => "mysource"
@compiler.topscope.expects(:class_scope).with(node_class).returns(scope)
node_resource.stubs(:evaluate)
@compiler.stubs :create_settings_scope
@compiler.compile
@compiler.topscope.should equal(scope)
end
end
describe "when managing resource overrides" do
before do
@override = stub 'override', :ref => "File[/foo]", :type => "my"
@resource = resource(:file, "/foo")
end
it "should be able to store overrides" do
lambda { @compiler.add_override(@override) }.should_not raise_error
end
it "should apply overrides to the appropriate resources" do
@compiler.add_resource(@scope, @resource)
@resource.expects(:merge).with(@override)
@compiler.add_override(@override)
@compiler.compile
end
it "should accept overrides before the related resource has been created" do
@resource.expects(:merge).with(@override)
# First store the override
@compiler.add_override(@override)
# Then the resource
@compiler.add_resource(@scope, @resource)
# And compile, so they get resolved
@compiler.compile
end
it "should fail if the compile is finished and resource overrides have not been applied" do
@compiler.add_override(@override)
lambda { @compiler.compile }.should raise_error(Puppet::ParseError)
end
end
end