1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611
|
# The virtual base class for properties, which are the self-contained building
# blocks for actually doing work on the system.
require_relative '../puppet'
require_relative '../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).
#
# @see Puppet::Type
# @see Puppet::Parameter
#
# @api public
#
class Puppet::Property < Puppet::Parameter
require_relative '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)
#TRANSLATORS 'Property#array_matching', 'first', and 'all' should not be translated
raise ArgumentError, _("Supported values for Property#array_matching are 'first' and 'all'") unless [:first, :all].include?(value)
@array_matching = value
end
# Used to mark a type property as having or lacking idempotency (on purpose
# generally). This is used to avoid marking the property as a
# corrective_change when there is known idempotency issues with the property
# rendering a corrective_change flag as useless.
# @return [Boolean] true if the property is marked as idempotent
def idempotent
@idempotent.nil? ? @idempotent = true : @idempotent
end
# Attribute setter for the idempotent attribute.
# @param [bool] value boolean indicating if the property is idempotent.
# @see idempotent
def idempotent=(value)
@idempotent = 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)
value = value_collection.match?(name)
value.name if value
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)
value = value_collection.value(name)
value.send(option) if value
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] :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).
#
# @dsl type
# @api public
def self.newvalue(name, options = {}, &block)
value = value_collection.newvalue(name, options, &block)
unless value.method.nil?
method = value.method.to_sym
if value.block
if instance_methods(false).include?(method)
raise ArgumentError, _("Attempt to redefine method %{method} with block") % { method: method }
end
define_method(method, &value.block)
else
# Let the method be an alias for calling the providers setter unless we already have this method
alias_method(method, :call_provider) unless method_defined?(method)
end
end
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)
# We have no idea how to handle this unless our parent have a provider
self.fail "#{self.class.name} cannot handle values of type #{value.inspect}" unless @resource.provider
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
# 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 #{should_to_s(newvalue)}"
elsif newvalue == :absent or newvalue == [:absent]
return "undefined '#{name}' from #{is_to_s(current_value)}"
else
return "#{name} changed #{is_to_s(current_value)} to #{should_to_s(newvalue)}"
end
rescue Puppet::Error
raise
rescue => detail
message = _("Could not convert change '%{name}' to string: %{detail}") % { name: name, detail: 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 containment path of this property, indicating what resource this
# property is associated with and in what stage and class that resource
# was declared, e.g. "/Stage[main]/Myclass/File[/tmp/example]/ensure"
# * `:invalidate_refreshes` - if scheduled refreshes should be invalidated
# * `:redacted` - if the event will be redacted (due to this property being sensitive)
#
# @return [Puppet::Transaction::Event] the created event
# @see Puppet::Type#event
def event(options = {})
attrs = { :name => event_name, :desired_value => should, :property => self, :source_description => path }.merge(options)
value = self.class.value_collection.match?(should) if should
attrs[:invalidate_refreshes] = true if value && value.invalidate_refreshes
attrs[:redacted] = @sensitive
resource.event attrs
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
# This method tests if two values are insync? outside of the properties current
# should value. This works around the requirement for corrective_change analysis
# that requires two older values to be compared with the properties potentially
# custom insync? code.
#
# @param [Object] should the value it should be
# @param [Object] is the value it is
# @return [Boolean] whether or not the values are in sync or not
# @api private
def insync_values?(should, is)
# Here be dragons. We're setting the should value of a property purely just to
# call its insync? method, as it lacks a way to pass in a should.
# Unfortunately there isn't an API compatible way of avoiding this, as both should
# an insync? behaviours are part of the public API. Future API work should factor
# this kind of arbitrary comparisons into the API to remove this complexity. -ken
# Backup old should, set it to the new value, then call insync? on the property.
old_should = @should
begin
@should = should
insync?(is)
rescue
# Certain operations may fail, but we don't want to fail the transaction if we can
# avoid it
#TRANSLATORS 'insync_values?' should not be translated
msg = _("Unknown failure using insync_values? on type: %{type} / property: %{name} to compare values %{should} and %{is}") %
{ type: self.resource.ref, name: self.name, should: should, is: is }
Puppet.info(msg)
# Return nil, ie. unknown
nil
ensure
# Always restore old should
@should = old_should
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 calls {#format_value_for_display} on the class. A derived
# implementation may perform property specific pretty printing when the _is_ values
# are not already in suitable form.
# @param value [Object] the value to format as a string
# @return [String] a pretty printing string
def is_to_s(value)
self.class.format_value_for_display(value)
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
# @return [Boolean] whether the property is marked as idempotent for the purposes
# of calculating corrective change.
def idempotent?
self.class.idempotent
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 _name_ associated with the value is first obtained by calling {value_name}. A dynamically created setter
# method associated with this _name_ is called if it exists, otherwise the value is set using using the provider's
# setter method for this property by calling ({#call_provider}).
#
# @param value [Object] the value to set
# @return [Object] returns the result of calling the setter method or {#call_provider}
# @raise [Puppet::Error] if there were problems setting the value using the setter method or when the provider
# setter should be used but there is no provider in the associated resource_
# @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.
# @api public
#
def set(value)
# Set a name for looking up associated options like the event.
name = self.class.value_name(value)
method = self.class.value_option(name, :method)
if method && self.respond_to?(method)
begin
self.send(method)
rescue Puppet::Error
raise
rescue => detail
error = Puppet::ResourceError.new(_("Could not set '%{value}' on %{class_name}: %{detail}") %
{ value: value, class_name: self.class.name, detail: detail }, @resource.file, @resource.line, detail)
error.set_backtrace detail.backtrace
Puppet.log_exception(detail, error.message)
raise error
end
else
block = self.class.value_option(name, :block)
if block
# FIXME It'd be better here to define a method, so that
# the blocks could return values.
self.instance_eval(&block)
else
call_provider(value)
end
end
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
# Produces a pretty printing string for the given value.
# This default implementation calls {#format_value_for_display} on the class. A derived
# implementation may perform property specific pretty printing when the _should_ values
# are not already in suitable form.
# @param value [Object] the value to format as a string
# @return [String] a pretty printing string
def should_to_s(value)
self.class.format_value_for_display(value)
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)
features = self.class.value_option(self.class.value_name(value), :required_features)
if features
features = Array(features)
needed_features = features.collect { |f| f.to_s }.join(", ")
unless provider.satisfies?(features)
#TRANSLATORS 'Provider' refers to a Puppet provider class
raise ArgumentError, _("Provider %{provider} must have features '%{needed_features}' to set '%{property}' to '%{value}'") %
{ provider: provider.class.name, needed_features: needed_features, property: self.class.name, value: value }
end
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
|