File: //usr/lib/ruby/site_ruby/1.8/puppet/property.rb
# The virtual base class for properties, which are the self-contained building
# blocks for actually doing work on the system.
require 'puppet'
require 'puppet/parameter'
# The Property class is the implementation of a resource's attributes of _property_ kind.
# A Property is a specialized Resource Type Parameter that has both an 'is' (current) state, and
# a 'should' (wanted state). However, even if this is conceptually true, the current _is_ value is
# obtained by asking the associated provider for the value, and hence it is not actually part of a
# property's state, and only available when a provider has been selected and can obtain the value (i.e. when
# running on an agent).
#
# A Property (also in contrast to a parameter) is intended to describe a managed attribute of
# some system entity, such as the name or mode of a file.
#
# The current value _(is)_ is read and written with the methods {#retrieve} and {#set}, and the wanted
# value _(should)_ is read and written with the methods {#value} and {#value=} which delegate to
# {#should} and {#should=}, i.e. when a property is used like any other parameter, it is the _should_ value
# that is operated on.
#
# All resource type properties in the puppet system are derived from this class.
#
# The intention is that new parameters are created by using the DSL method {Puppet::Type.newproperty}.
#
# @abstract
# @note Properties of Types are expressed using subclasses of this class. Such a class describes one
# named property of a particular Type (as opposed to describing a type of property in general). This
# limits the use of one (concrete) property class instance to occur only once for a given type's inheritance
# chain. An instance of a Property class is the value holder of one instance of the resource type (e.g. the
# mode of a file resource instance).
# A Property class may server as the superclass _(parent)_ of another; e.g. a Size property that describes
# handling of measurements such as kb, mb, gb. If a type requires two different size measurements it requires
# one concrete class per such measure; e.g. MinSize (:parent => Size), and MaxSize (:parent => Size).
#
# @todo Describe meta-parameter shadowing. This concept can not be understood by just looking at the descriptions
# of the methods involved.
#
# @see Puppet::Type
# @see Puppet::Parameter
#
# @api public
#
class Puppet::Property < Puppet::Parameter
require 'puppet/property/ensure'
# Returns the original wanted value(s) _(should)_ unprocessed by munging/unmunging.
# The original values are set by {#value=} or {#should=}.
# @return (see #should)
#
attr_reader :shouldorig
# The noop mode for this property.
# By setting a property's noop mode to `true`, any management of this property is inhibited. Calculation
# and reporting still takes place, but if a change of the underlying managed entity's state
# should take place it will not be carried out. This noop
# setting overrides the overall `Puppet[:noop]` mode as well as the noop mode in the _associated resource_
#
attr_writer :noop
class << self
# @todo Figure out what this is used for. Can not find any logic in the puppet code base that
# reads or writes this attribute.
# ??? Probably Unused
attr_accessor :unmanaged
# @return [Symbol] The name of the property as given when the property was created.
#
attr_reader :name
# @!attribute [rw] array_matching
# @comment note that $#46; is a period - char code require to not terminate sentence.
# The `is` vs. `should` array matching mode; `:first`, or `:all`.
#
# @comment there are two blank chars after the symbols to cause a break - do not remove these.
# * `:first`
# This is primarily used for single value properties. When matched against an array of values
# a match is true if the `is` value matches any of the values in the `should` array. When the `is` value
# is also an array, the matching is performed against the entire array as the `is` value.
# * `:all`
# : This is primarily used for multi-valued properties. When matched against an array of
# `should` values, the size of `is` and `should` must be the same, and all values in `is` must match
# a value in `should`.
#
# @note The semantics of these modes are implemented by the method {#insync?}. That method is the default
# implementation and it has a backwards compatible behavior that imposes additional constraints
# on what constitutes a positive match. A derived property may override that method.
# @return [Symbol] (:first) the mode in which matching is performed
# @see #insync?
# @dsl type
# @api public
#
def array_matching
@array_matching ||= :first
end
# @comment This is documented as an attribute - see the {array_matching} method.
#
def array_matching=(value)
value = value.intern if value.is_a?(String)
raise ArgumentError, "Supported values for Property#array_matching are 'first' and 'all'" unless [:first, :all].include?(value)
@array_matching = value
end
end
# Looks up a value's name among valid values, to enable option lookup with result as a key.
# @param name [Object] the parameter value to match against valid values (names).
# @return {Symbol, Regexp} a value matching predicate
# @api private
#
def self.value_name(name)
if value = value_collection.match?(name)
value.name
end
end
# Returns the value of the given option (set when a valid value with the given "name" was defined).
# @param name [Symbol, Regexp] the valid value predicate as returned by {value_name}
# @param option [Symbol] the name of the wanted option
# @return [Object] value of the option
# @raise [NoMethodError] if the option is not supported
# @todo Guessing on result of passing a non supported option (it performs send(option)).
# @api private
#
def self.value_option(name, option)
if value = value_collection.value(name)
value.send(option)
end
end
# Defines a new valid value for this property.
# A valid value is specified as a literal (typically a Symbol), but can also be
# specified with a Regexp.
#
# @param name [Symbol, Regexp] a valid literal value, or a regexp that matches a value
# @param options [Hash] a hash with options
# @option options [Symbol] :event The event that should be emitted when this value is set.
# @todo Option :event original comment says "event should be returned...", is "returned" the correct word
# to use?
# @option options [Symbol] :call When to call any associated block. The default value is `:instead` which
# means that the block should be called instead of the provider. In earlier versions (before 20081031) it
# was possible to specify a value of `:before` or `:after` for the purpose of calling
# both the block and the provider. Use of these deprecated options will now raise an exception later
# in the process when the _is_ value is set (see #set).
# @option options [Symbol] :invalidate_refreshes Indicates a change on this property should invalidate and
# remove any scheduled refreshes (from notify or subscribe) targeted at the same resource. For example, if
# a change in this property takes into account any changes that a scheduled refresh would have performed,
# then the scheduled refresh would be deleted.
# @option options [Object] any Any other option is treated as a call to a setter having the given
# option name (e.g. `:required_features` calls `required_features=` with the option's value as an
# argument).
# @todo The original documentation states that the option `:method` will set the name of the generated
# setter method, but this is not implemented. Is the documentatin or the implementation in error?
# (The implementation is in Puppet::Parameter::ValueCollection#new_value).
# @todo verify that the use of :before and :after have been deprecated (or rather - never worked, and
# was never in use. (This means, that the option :call could be removed since calls are always :instead).
#
# @dsl type
# @api public
def self.newvalue(name, options = {}, &block)
value = value_collection.newvalue(name, options, &block)
define_method(value.method, &value.block) if value.method and value.block
value
end
# Calls the provider setter method for this property with the given value as argument.
# @return [Object] what the provider returns when calling a setter for this property's name
# @raise [Puppet::Error] when the provider can not handle this property.
# @see #set
# @api private
#
def call_provider(value)
method = self.class.name.to_s + "="
unless provider.respond_to? method
self.fail "The #{provider.class.name} provider can not handle attribute #{self.class.name}"
end
provider.send(method, value)
end
# Sets the value of this property to the given value by calling the dynamically created setter method associated with the "valid value" referenced by the given name.
# @param name [Symbol, Regexp] a valid value "name" as returned by {value_name}
# @param value [Object] the value to set as the value of the property
# @raise [Puppet::DevError] if there was no method to call
# @raise [Puppet::Error] if there were problems setting the value
# @raise [Puppet::ResourceError] if there was a problem setting the value and it was not raised
# as a Puppet::Error. The original exception is wrapped and logged.
# @todo The check for a valid value option called `:method` does not seem to be fully supported
# as it seems that this option is never consulted when the method is dynamically created. Needs to
# be investigated. (Bug, or documentation needs to be changed).
# @see #set
# @api private
#
def call_valuemethod(name, value)
if method = self.class.value_option(name, :method) and self.respond_to?(method)
begin
self.send(method)
rescue Puppet::Error
raise
rescue => detail
error = Puppet::ResourceError.new("Could not set '#{value}' on #{self.class.name}: #{detail}", @resource.line, @resource.file, detail)
error.set_backtrace detail.backtrace
Puppet.log_exception(detail, error.message)
raise error
end
elsif block = self.class.value_option(name, :block)
# FIXME It'd be better here to define a method, so that
# the blocks could return values.
self.instance_eval(&block)
else
devfail "Could not find method for value '#{name}'"
end
end
# Formats a message for a property change from the given `current_value` to the given `newvalue`.
# @return [String] a message describing the property change.
# @note If called with equal values, this is reported as a change.
# @raise [Puppet::DevError] if there were issues formatting the message
#
def change_to_s(current_value, newvalue)
begin
if current_value == :absent
return "defined '#{name}' as #{self.class.format_value_for_display should_to_s(newvalue)}"
elsif newvalue == :absent or newvalue == [:absent]
return "undefined '#{name}' from #{self.class.format_value_for_display is_to_s(current_value)}"
else
return "#{name} changed #{self.class.format_value_for_display is_to_s(current_value)} to #{self.class.format_value_for_display should_to_s(newvalue)}"
end
rescue Puppet::Error, Puppet::DevError
raise
rescue => detail
message = "Could not convert change '#{name}' to string: #{detail}"
Puppet.log_exception(detail, message)
raise Puppet::DevError, message, detail.backtrace
end
end
# Produces the name of the event to use to describe a change of this property's value.
# The produced event name is either the event name configured for this property, or a generic
# event based on the name of the property with suffix `_changed`, or if the property is
# `:ensure`, the name of the resource type and one of the suffixes `_created`, `_removed`, or `_changed`.
# @return [String] the name of the event that describes the change
#
def event_name
value = self.should
event_name = self.class.value_option(value, :event) and return event_name
name == :ensure or return (name.to_s + "_changed").to_sym
return (resource.type.to_s + case value
when :present; "_created"
when :absent; "_removed"
else
"_changed"
end).to_sym
end
# Produces an event describing a change of this property.
# In addition to the event attributes set by the resource type, this method adds:
#
# * `:name` - the event_name
# * `:desired_value` - a.k.a _should_ or _wanted value_
# * `:property` - reference to this property
# * `:source_description` - the _path_ (?? See todo)
# * `:invalidate_refreshes` - if scheduled refreshes should be invalidated
#
# @todo What is the intent of this method? What is the meaning of the :source_description passed in the
# options to the created event?
# @return [Puppet::Transaction::Event] the created event
# @see Puppet::Type#event
def event
attrs = { :name => event_name, :desired_value => should, :property => self, :source_description => path }
if should and value = self.class.value_collection.match?(should)
attrs[:invalidate_refreshes] = true if value.invalidate_refreshes
end
resource.event attrs
end
# @todo What is this?
# What is this used for?
attr_reader :shadow
# Initializes a Property the same way as a Parameter and handles the special case when a property is shadowing a meta-parameter.
# @todo There is some special initialization when a property is not a metaparameter but
# Puppet::Type.metaparamclass(for this class's name) is not nil - if that is the case a
# setup_shadow is performed for that class.
#
# @param hash [Hash] options passed to the super initializer {Puppet::Parameter#initialize}
# @note New properties of a type should be created via the DSL method {Puppet::Type.newproperty}.
# @see Puppet::Parameter#initialize description of Parameter initialize options.
# @api private
def initialize(hash = {})
super
if ! self.metaparam? and klass = Puppet::Type.metaparamclass(self.class.name)
setup_shadow(klass)
end
end
# Determines whether the property is in-sync or not in a way that is protected against missing value.
# @note If the wanted value _(should)_ is not defined or is set to a non-true value then this is
# a state that can not be fixed and the property is reported to be in sync.
# @return [Boolean] the protected result of `true` or the result of calling {#insync?}.
#
# @api private
# @note Do not override this method.
#
def safe_insync?(is)
# If there is no @should value, consider the property to be in sync.
return true unless @should
# Otherwise delegate to the (possibly derived) insync? method.
insync?(is)
end
# Protects against override of the {#safe_insync?} method.
# @raise [RuntimeError] if the added method is `:safe_insync?`
# @api private
#
def self.method_added(sym)
raise "Puppet::Property#safe_insync? shouldn't be overridden; please override insync? instead" if sym == :safe_insync?
end
# Checks if the current _(is)_ value is in sync with the wanted _(should)_ value.
# The check if the two values are in sync is controlled by the result of {#match_all?} which
# specifies a match of `:first` or `:all`). The matching of the _is_ value against the entire _should_ value
# or each of the _should_ values (as controlled by {#match_all?} is performed by {#property_matches?}.
#
# A derived property typically only needs to override the {#property_matches?} method, but may also
# override this method if there is a need to have more control over the array matching logic.
#
# @note The array matching logic in this method contains backwards compatible logic that performs the
# comparison in `:all` mode by checking equality and equality of _is_ against _should_ converted to array of String,
# and that the lengths are equal, and in `:first` mode by checking if one of the _should_ values
# is included in the _is_ values. This means that the _is_ value needs to be carefully arranged to
# match the _should_.
# @todo The implementation should really do return is.zip(@should).all? {|a, b| property_matches?(a, b) }
# instead of using equality check and then check against an array with converted strings.
# @param is [Object] The current _(is)_ value to check if it is in sync with the wanted _(should)_ value(s)
# @return [Boolean] whether the values are in sync or not.
# @raise [Puppet::DevError] if wanted value _(should)_ is not an array.
# @api public
#
def insync?(is)
self.devfail "#{self.class.name}'s should is not array" unless @should.is_a?(Array)
# an empty array is analogous to no should values
return true if @should.empty?
# Look for a matching value, either for all the @should values, or any of
# them, depending on the configuration of this property.
if match_all? then
# Emulate Array#== using our own comparison function.
# A non-array was not equal to an array, which @should always is.
return false unless is.is_a? Array
# If they were different lengths, they are not equal.
return false unless is.length == @should.length
# Finally, are all the elements equal? In order to preserve the
# behaviour of previous 2.7.x releases, we need to impose some fun rules
# on "equality" here.
#
# Specifically, we need to implement *this* comparison: the two arrays
# are identical if the is values are == the should values, or if the is
# values are == the should values, stringified.
#
# This does mean that property equality is not commutative, and will not
# work unless the `is` value is carefully arranged to match the should.
return (is == @should or is == @should.map(&:to_s))
# When we stop being idiots about this, and actually have meaningful
# semantics, this version is the thing we actually want to do.
#
# return is.zip(@should).all? {|a, b| property_matches?(a, b) }
else
return @should.any? {|want| property_matches?(is, want) }
end
end
# Checks if the given current and desired values are equal.
# This default implementation performs this check in a backwards compatible way where
# the equality of the two values is checked, and then the equality of current with desired
# converted to a string.
#
# A derived implementation may override this method to perform a property specific equality check.
#
# The intent of this method is to provide an equality check suitable for checking if the property
# value is in sync or not. It is typically called from {#insync?}.
#
def property_matches?(current, desired)
# This preserves the older Puppet behaviour of doing raw and string
# equality comparisons for all equality. I am not clear this is globally
# desirable, but at least it is not a breaking change. --daniel 2011-11-11
current == desired or current == desired.to_s
end
# Produces a pretty printing string for the given value.
# This default implementation simply returns the given argument. A derived implementation
# may perform property specific pretty printing when the _is_ and _should_ values are not
# already in suitable form.
# @return [String] a pretty printing string
def is_to_s(currentvalue)
currentvalue
end
# Emits a log message at the log level specified for the associated resource.
# The log entry is associated with this property.
# @param msg [String] the message to log
# @return [void]
#
def log(msg)
Puppet::Util::Log.create(
:level => resource[:loglevel],
:message => msg,
:source => self
)
end
# @return [Boolean] whether the {array_matching} mode is set to `:all` or not
def match_all?
self.class.array_matching == :all
end
# (see Puppet::Parameter#munge)
# If this property is a meta-parameter shadow, the shadow's munge is also called.
# @todo Incomprehensible ! The concept of "meta-parameter-shadowing" needs to be explained.
#
def munge(value)
self.shadow.munge(value) if self.shadow
super
end
# @return [Symbol] the name of the property as stated when the property was created.
# @note A property class (just like a parameter class) describes one specific property and
# can only be used once within one type's inheritance chain.
def name
self.class.name
end
# @return [Boolean] whether this property is in noop mode or not.
# Returns whether this property is in noop mode or not; if a difference between the
# _is_ and _should_ values should be acted on or not.
# The noop mode is a transitive setting. The mode is checked in this property, then in
# the _associated resource_ and finally in Puppet[:noop].
# @todo This logic is different than Parameter#noop in that the resource noop mode overrides
# the property's mode - in parameter it is the other way around. Bug or feature?
#
def noop
# This is only here to make testing easier.
if @resource.respond_to?(:noop?)
@resource.noop?
else
if defined?(@noop)
@noop
else
Puppet[:noop]
end
end
end
# Retrieves the current value _(is)_ of this property from the provider.
# This implementation performs this operation by calling a provider method with the
# same name as this property (i.e. if the property name is 'gid', a call to the
# 'provider.gid' is expected to return the current value.
# @return [Object] what the provider returns as the current value of the property
#
def retrieve
provider.send(self.class.name)
end
# Sets the current _(is)_ value of this property.
# The value is set using the provider's setter method for this property ({#call_provider}) if nothing
# else has been specified. If the _valid value_ for the given value defines a `:call` option with the
# value `:instead`, the
# value is set with {#call_valuemethod} which invokes a block specified for the valid value.
#
# @note In older versions (before 20081031) it was possible to specify the call types `:before` and `:after`
# which had the effect that both the provider method and the _valid value_ block were called.
# This is no longer supported.
#
# @param value [Object] the value to set as the value of this property
# @return [Object] returns what {#call_valuemethod} or {#call_provider} returns
# @raise [Puppet::Error] when the provider setter should be used but there is no provider set in the _associated
# resource_
# @raise [Puppet::DevError] when a deprecated call form was specified (e.g. `:before` or `:after`).
# @api public
#
def set(value)
# Set a name for looking up associated options like the event.
name = self.class.value_name(value)
call = self.class.value_option(name, :call) || :none
if call == :instead
call_valuemethod(name, value)
elsif call == :none
# They haven't provided a block, and our parent does not have
# a provider, so we have no idea how to handle this.
self.fail "#{self.class.name} cannot handle values of type #{value.inspect}" unless @resource.provider
call_provider(value)
else
# LAK:NOTE 20081031 This is a change in behaviour -- you could
# previously specify :call => [;before|:after], which would call
# the setter *in addition to* the block. I'm convinced this
# was never used, and it makes things unecessarily complicated.
# If you want to specify a block and still call the setter, then
# do so in the block.
devfail "Cannot use obsolete :call value '#{call}' for property '#{self.class.name}'"
end
end
# Sets up a shadow property for a shadowing meta-parameter.
# This construct allows the creation of a property with the
# same name as a meta-parameter. The metaparam will only be stored as a shadow.
# @param klass [Class<inherits Puppet::Parameter>] the class of the shadowed meta-parameter
# @return [Puppet::Parameter] an instance of the given class (a parameter or property)
#
def setup_shadow(klass)
@shadow = klass.new(:resource => self.resource)
end
# Returns the wanted _(should)_ value of this property.
# If the _array matching mode_ {#match_all?} is true, an array of the wanted values in unmunged format
# is returned, else the first value in the array of wanted values in unmunged format is returned.
# @return [Array<Object>, Object, nil] Array of values if {#match_all?} else a single value, or nil if there are no
# wanted values.
# @raise [Puppet::DevError] if the wanted value is non nil and not an array
#
# @note This method will potentially return different values than the original values as they are
# converted via munging/unmunging. If the original values are wanted, call {#shouldorig}.
#
# @see #shouldorig
# @api public
#
def should
return nil unless defined?(@should)
self.devfail "should for #{self.class.name} on #{resource.name} is not an array" unless @should.is_a?(Array)
if match_all?
return @should.collect { |val| self.unmunge(val) }
else
return self.unmunge(@should[0])
end
end
# Sets the wanted _(should)_ value of this property.
# If the given value is not already an Array, it will be wrapped in one before being set.
# This method also sets the cached original _should_ values returned by {#shouldorig}.
#
# @param values [Array<Object>, Object] the value(s) to set as the wanted value(s)
# @raise [StandardError] when validation of a value fails (see {#validate}).
# @api public
#
def should=(values)
values = [values] unless values.is_a?(Array)
@shouldorig = values
values.each { |val| validate(val) }
@should = values.collect { |val| self.munge(val) }
end
# Formats the given newvalue (following _should_ type conventions) for inclusion in a string describing a change.
# @return [String] Returns the given newvalue in string form with space separated entries if it is an array.
# @see #change_to_s
#
def should_to_s(newvalue)
[newvalue].flatten.join(" ")
end
# Synchronizes the current value _(is)_ and the wanted value _(should)_ by calling {#set}.
# @raise [Puppet::DevError] if {#should} is nil
# @todo The implementation of this method is somewhat inefficient as it computes the should
# array twice.
def sync
devfail "Got a nil value for should" unless should
set(should)
end
# Asserts that the given value is valid.
# If the developer uses a 'validate' hook, this method will get overridden.
# @raise [Exception] if the value is invalid, or value can not be handled.
# @return [void]
# @api private
#
def unsafe_validate(value)
super
validate_features_per_value(value)
end
# Asserts that all required provider features are present for the given property value.
# @raise [ArgumentError] if a required feature is not present
# @return [void]
# @api private
#
def validate_features_per_value(value)
if features = self.class.value_option(self.class.value_name(value), :required_features)
features = Array(features)
needed_features = features.collect { |f| f.to_s }.join(", ")
raise ArgumentError, "Provider must have features '#{needed_features}' to set '#{self.class.name}' to '#{value}'" unless provider.satisfies?(features)
end
end
# @return [Object, nil] Returns the wanted _(should)_ value of this property.
def value
self.should
end
# (see #should=)
def value=(values)
self.should = values
end
end