diff --git a/lib/puppet/transaction/resource_harness.rb b/lib/puppet/transaction/resource_harness.rb index 3efcabaf6..3f02c1436 100644 --- a/lib/puppet/transaction/resource_harness.rb +++ b/lib/puppet/transaction/resource_harness.rb @@ -1,238 +1,238 @@ require 'puppet/resource/status' class Puppet::Transaction::ResourceHarness + NO_ACTION = Object.new + extend Forwardable def_delegators :@transaction, :relationship_graph attr_reader :transaction def initialize(transaction) @transaction = transaction end def evaluate(resource) status = Puppet::Resource::Status.new(resource) begin context = ResourceApplicationContext.from_resource(resource, status) perform_changes(resource, context) if status.changed? && ! resource.noop? cache(resource, :synced, Time.now) resource.flush if resource.respond_to?(:flush) end rescue => detail status.failed_because(detail) ensure status.evaluation_time = Time.now - status.time end status end def scheduled?(resource) return true if Puppet[:ignoreschedules] return true unless schedule = schedule(resource) # We use 'checked' here instead of 'synced' because otherwise we'll # end up checking most resources most times, because they will generally # have been synced a long time ago (e.g., a file only gets updated # once a month on the server and its schedule is daily; the last sync time # will have been a month ago, so we'd end up checking every run). schedule.match?(cached(resource, :checked).to_i) end def schedule(resource) unless resource.catalog resource.warning "Cannot schedule without a schedule-containing catalog" return nil end return nil unless name = resource[:schedule] resource.catalog.resource(:schedule, name) || resource.fail("Could not find schedule #{name}") end # Used mostly for scheduling and auditing at this point. def cached(resource, name) Puppet::Util::Storage.cache(resource)[name] end # Used mostly for scheduling and auditing at this point. def cache(resource, name, value) Puppet::Util::Storage.cache(resource)[name] = value end private def perform_changes(resource, context) cache(resource, :checked, Time.now) return [] if ! allow_changes?(resource) # Record the current state in state.yml. context.audited_params.each do |param| cache(resource, param, context.current_values[param]) end - managed_via_ensure = manage_via_ensure_if_possible(resource, context) + ensure_param = resource.parameter(:ensure) + if ensure_param && ensure_param.should + ensure_event = sync_if_needed(ensure_param, context) + else + ensure_event = NO_ACTION + end - if !managed_via_ensure + if ensure_event == NO_ACTION if context.resource_present? resource.properties.each do |param| sync_if_needed(param, context) end else resource.debug("Nothing to manage: no ensure and the resource doesn't exist") end end capture_audit_events(resource, context) end def allow_changes?(resource) if resource.purging? and resource.deleting? and deps = relationship_graph.dependents(resource) \ and ! deps.empty? and deps.detect { |d| ! d.deleting? } deplabel = deps.collect { |r| r.ref }.join(",") plurality = deps.length > 1 ? "":"s" resource.warning "#{deplabel} still depend#{plurality} on me -- not purging" false else true end end - def manage_via_ensure_if_possible(resource, context) - ensure_param = resource.parameter(:ensure) - if ensure_param && ensure_param.should - sync_if_needed(ensure_param, context) - else - false - end - end - def sync_if_needed(param, context) historical_value = context.historical_values[param.name] current_value = context.current_values[param.name] do_audit = context.audited_params.include?(param.name) begin if param.should && !param.safe_insync?(current_value) event = create_change_event(param, current_value, historical_value) if do_audit event = audit_event(event, param) end brief_audit_message = audit_message(param, do_audit, historical_value, current_value) if param.noop noop(event, param, current_value, brief_audit_message) else sync(event, param, current_value, brief_audit_message) end - true + event else - false + NO_ACTION end rescue => detail # Execution will continue on StandardErrors, just store the event Puppet.log_exception(detail) event = create_change_event(param, current_value, historical_value) event.status = "failure" event.message = "change from #{param.is_to_s(current_value)} to #{param.should_to_s(param.should)} failed: #{detail}" - false + event rescue Exception => detail # Execution will halt on Exceptions, they get raised to the application event = create_change_event(param, current_value, historical_value) event.status = "failure" event.message = "change from #{param.is_to_s(current_value)} to #{param.should_to_s(param.should)} failed: #{detail}" raise ensure if event context.record(event) event.send_log context.synced_params << param.name end end end def create_change_event(property, current_value, historical_value) event = property.event event.previous_value = current_value event.desired_value = property.should event.historical_value = historical_value event end def audit_event(event, property) event.audited = true event.status = "audit" if event.historical_value != event.previous_value event.message = "audit change: previously recorded value #{property.is_to_s(event.historical_value)} has been changed to #{property.is_to_s(event.previous_value)}" end event end def audit_message(param, do_audit, historical_value, current_value) if do_audit && historical_value && historical_value != current_value " (previously recorded value was #{param.is_to_s(historical_value)})" else "" end end def noop(event, param, current_value, audit_message) event.message = "current_value #{param.is_to_s(current_value)}, should be #{param.should_to_s(param.should)} (noop)#{audit_message}" event.status = "noop" end def sync(event, param, current_value, audit_message) param.sync event.message = "#{param.change_to_s(current_value, param.should)}#{audit_message}" event.status = "success" end def capture_audit_events(resource, context) context.audited_params.each do |param_name| if context.historical_values.include?(param_name) if context.historical_values[param_name] != context.current_values[param_name] && !context.synced_params.include?(param_name) parameter = resource.parameter(param_name) event = audit_event(create_change_event(parameter, context.current_values[param_name], context.historical_values[param_name]), parameter) event.send_log context.record(event) end else resource.property(param_name).notice "audit change: newly-recorded value #{context.current_values[param_name]}" end end end # @api private - ResourceApplicationContext = Struct.new(:current_values, + ResourceApplicationContext = Struct.new(:resource, + :current_values, :historical_values, :audited_params, :synced_params, :status) do def self.from_resource(resource, status) - ResourceApplicationContext.new(resource.retrieve_resource.to_hash, + ResourceApplicationContext.new(resource, + resource.retrieve_resource.to_hash, Puppet::Util::Storage.cache(resource).dup, (resource[:audit] || []).map { |p| p.to_sym }, [], status) end def resource_present? - current_values[:ensure] != :absent + resource.present?(current_values) end def record(event) status << event end end end diff --git a/lib/puppet/type.rb b/lib/puppet/type.rb index 4c7924ad3..5cdee64b6 100644 --- a/lib/puppet/type.rb +++ b/lib/puppet/type.rb @@ -1,2425 +1,2432 @@ 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/logging' require 'puppet/util/tagging' # see the bottom of the file for the rest of the inclusions module Puppet # The base class for all Puppet types. # # A type describes: #-- # * **Attributes** - properties, parameters, and meta-parameters are different types of attributes of a type. # * **Properties** - these are the properties of the managed resource (attributes of the entity being managed; like # a file's owner, group and mode). A property describes two states; the 'is' (current state) and the 'should' (wanted # state). # * **Ensurable** - a set of traits that control the lifecycle (create, remove, etc.) of a managed entity. # There is a default set of operations associated with being _ensurable_, but this can be changed. # * **Name/Identity** - one property is the name/identity of a resource, the _namevar_ that uniquely identifies # one instance of a type from all others. # * **Parameters** - additional attributes of the type (that does not directly related to an instance of the managed # resource; if an operation is recursive or not, where to look for things, etc.). A Parameter (in contrast to Property) # has one current value where a Property has two (current-state and wanted-state). # * **Meta-Parameters** - parameters that are available across all types. A meta-parameter typically has # additional semantics; like the `require` meta-parameter. A new type typically does not add new meta-parameters, # but you need to be aware of their existence so you do not inadvertently shadow an existing meta-parameters. # * **Parent** - a type can have a super type (that it inherits from). # * **Validation** - If not just a basic data type, or an enumeration of symbolic values, it is possible to provide # validation logic for a type, properties and parameters. # * **Munging** - munging/unmunging is the process of turning a value in external representation (as used # by a provider) into an internal representation and vice versa. A Type supports adding custom logic for these. # * **Auto Requirements** - a type can specify automatic relationships to resources to ensure that if they are being # managed, they will be processed before this type. # * **Providers** - a provider is an implementation of a type's behavior - the management of a resource in the # system being managed. A provider is often platform specific and is selected at runtime based on # criteria/predicates specified in the configured providers. See {Puppet::Provider} for details. # * **Device Support** - A type has some support for being applied to a device; i.e. something that is managed # by running logic external to the device itself. There are several methods that deals with type # applicability for these special cases such as {apply_to_device}. # # Additional Concepts: # -- # * **Resource-type** - A _resource type_ is a term used to denote the type of a resource; internally a resource # is really an instance of a Ruby class i.e. {Puppet::Resource} which defines its behavior as "resource data". # Conceptually however, a resource is an instance of a subclass of Type (e.g. File), where such a class describes # its interface (what can be said/what is known about a resource of this type), # * **Managed Entity** - This is not a term in general use, but is used here when there is a need to make # a distinction between a resource (a description of what/how something should be managed), and what it is # managing (a file in the file system). The term _managed entity_ is a reference to the "file in the file system" # * **Isomorphism** - the quality of being _isomorphic_ means that two resource instances with the same name # refers to the same managed entity. Or put differently; _an isomorphic name is the identity of a resource_. # As an example, `exec` resources (that executes some command) have the command (i.e. the command line string) as # their name, and these resources are said to be non-isomorphic. # # @note The Type class deals with multiple concerns; some methods provide an internal DSL for convenient definition # of types, other methods deal with various aspects while running; wiring up a resource (expressed in Puppet DSL # or Ruby DSL) with its _resource type_ (i.e. an instance of Type) to enable validation, transformation of values # (munge/unmunge), etc. Lastly, Type is also responsible for dealing with Providers; the concrete implementations # of the behavior that constitutes how a particular Type behaves on a particular type of system (e.g. how # commands are executed on a flavor of Linux, on Windows, etc.). This means that as you are reading through the # documentation of this class, you will be switching between these concepts, as well as switching between # the conceptual level "a resource is an instance of a resource-type" and the actual implementation classes # (Type, Resource, Provider, and various utility and helper classes). # # @api public # # class Type include Puppet::Util include Puppet::Util::Errors include Puppet::Util::Logging include Puppet::Util::Tagging # Comparing type instances. include Comparable # Compares this type against the given _other_ (type) and returns -1, 0, or +1 depending on the order. # @param other [Object] the object to compare against (produces nil, if not kind of Type} # @return [-1, 0, +1, nil] produces -1 if this type is before the given _other_ type, 0 if equals, and 1 if after. # Returns nil, if the given _other_ is not a kind of Type. # @see Comparable # def <=>(other) # We only order against other types, not arbitrary objects. return nil unless other.is_a? Puppet::Type # Our natural order is based on the reference name we use when comparing # against other type instances. self.ref <=> other.ref end # Code related to resource type attributes. class << self include Puppet::Util::ClassGen include Puppet::Util::Warnings # @return [Array] The list of declared properties for the resource type. # The returned lists contains instances if Puppet::Property or its subclasses. attr_reader :properties end # Returns all the attribute names of the type in the appropriate order. # The {key_attributes} come first, then the {provider}, then the {properties}, and finally # the {parameters} and {metaparams}, # all in the order they were specified in the respective files. # @return [Array] all type attribute names in a defined order. # def self.allattrs key_attributes | (parameters & [:provider]) | properties.collect { |property| property.name } | parameters | metaparams end # Returns the class associated with the given attribute name. # @param name [String] the name of the attribute to obtain the class for # @return [Class, nil] the class for the given attribute, or nil if the name does not refer to an existing 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 # Returns the attribute type (`:property`, `;param`, `:meta`). # @comment What type of parameter are we dealing with? Cache the results, because # this method gets called so many times. # @return [Symbol] a symbol describing the type of attribute (`:property`, `;param`, `:meta`) # 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 # Provides iteration over meta-parameters. # @yieldparam p [Puppet::Parameter] each meta parameter # @return [void] # def self.eachmetaparam @@metaparams.each { |p| yield p.name } end # Creates a new `ensure` property with configured default values or with configuration by an optional block. # This method is a convenience method for creating a property `ensure` with default accepted values. # If no block is specified, the new `ensure` property will accept the default symbolic # values `:present`, and `:absent` - see {Puppet::Property::Ensure}. # If something else is wanted, pass a block and make calls to {Puppet::Property.newvalue} from this block # to define each possible value. If a block is passed, the defaults are not automatically added to the set of # valid values. # # @note This method will be automatically called without a block if the type implements the methods # specified by {ensurable?}. It is recommended to always call this method and not rely on this automatic # specification to clearly state that the type is ensurable. # # @overload ensurable() # @overload ensurable({|| ... }) # @yield [ ] A block evaluated in scope of the new Parameter # @yieldreturn [void] # @return [void] # @dsl type # @api public # 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 # Returns true if the type implements the default behavior expected by being _ensurable_ "by default". # A type is _ensurable_ by default if it responds to `:exists`, `:create`, and `:destroy`. # If a type implements these methods and have not already specified that it is _ensurable_, it will be # made so with the defaults specified in {ensurable}. # @return [Boolean] whether the type is _ensurable_ or not. # def self.ensurable? # If the class has all three of these methods defined, then it's # ensurable. [:exists?, :create, :destroy].all? { |method| self.public_method_defined?(method) } end # @comment These `apply_to` methods are horrible. They should really be implemented # as part of the usual system of constraints that apply to a type and # provider pair, but were implemented as a separate shadow system. # # @comment We should rip them out in favour of a real constraint pattern around the # target device - whatever that looks like - and not have this additional # magic here. --daniel 2012-03-08 # # Makes this type applicable to `:device`. # @return [Symbol] Returns `:device` # @api private # def self.apply_to_device @apply_to = :device end # Makes this type applicable to `:host`. # @return [Symbol] Returns `:host` # @api private # def self.apply_to_host @apply_to = :host end # Makes this type applicable to `:both` (i.e. `:host` and `:device`). # @return [Symbol] Returns `:both` # @api private # def self.apply_to_all @apply_to = :both end # Makes this type apply to `:host` if not already applied to something else. # @return [Symbol] a `:device`, `:host`, or `:both` enumeration # @api private def self.apply_to @apply_to ||= :host end # Returns true if this type is applicable to the given target. # @param target [Symbol] should be :device, :host or :target, if anything else, :host is enforced # @return [Boolean] true # @api private # def self.can_apply_to(target) [ target == :device ? :device : :host, :both ].include?(apply_to) end # Processes the options for a named parameter. # @param name [String] the name of a parameter # @param options [Hash] a hash of options # @option options [Boolean] :boolean if option set to true, an access method on the form _name_? is added for the param # @return [void] # 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 given parameter a meta-parameter? # @return [Boolean] true if the given parameter is a meta-parameter. # def self.metaparam?(param) @@metaparamhash.include?(param.intern) end # Returns the meta-parameter class associated with the given meta-parameter name. # Accepts a `nil` name, and return nil. # @param name [String, nil] the name of a meta-parameter # @return [Class,nil] the class for the given meta-parameter, or `nil` if no such meta-parameter exists, (or if # the given meta-parameter name is `nil`. # def self.metaparamclass(name) return nil if name.nil? @@metaparamhash[name.intern] end # Returns all meta-parameter names. # @return [Array] all meta-parameter names # def self.metaparams @@metaparams.collect { |param| param.name } end # Returns the documentation for a given meta-parameter of this type. # @todo the type for the param metaparam # @param metaparam [??? Puppet::Parameter] the meta-parameter to get documentation for. # @return [String] the documentation associated with the given meta-parameter, or nil of not such documentation # exists. # @raise if the given metaparam is not a meta-parameter in this type # def self.metaparamdoc(metaparam) @@metaparamhash[metaparam].doc end # Creates a new meta-parameter. # This creates a new meta-parameter that is added to all types. # @param name [Symbol] the name of the parameter # @param options [Hash] a hash with options. # @option options [Class] :parent (Puppet::Parameter) the super class of this parameter # @option options [Hash{String => Object}] :attributes a hash that is applied to the generated class # by calling setter methods corresponding to this hash's keys/value pairs. This is done before the given # block is evaluated. # @option options [Boolean] :boolean (false) specifies if this is a boolean parameter # @option options [Boolean] :namevar (false) specifies if this parameter is the namevar # @option options [Symbol, Array] :required_features specifies required provider features by name # @return [Class] the created parameter # @yield [ ] a required block that is evaluated in the scope of the new meta-parameter # @api public # @dsl type # @todo Verify that this description is ok # def self.newmetaparam(name, options = {}, &block) @@metaparams ||= [] @@metaparamhash ||= {} name = name.intern 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 # Returns parameters that act as a key. # All parameters that return true from #isnamevar? or is named `:name` are included in the returned result. # @todo would like a better explanation # @return [Array] WARNING: this return type is uncertain def self.key_attribute_parameters @key_attribute_parameters ||= ( @parameters.find_all { |param| param.isnamevar? or param.name == :name } ) end # Returns cached {key_attribute_parameters} names # @todo what is a 'key_attribute' ? # @return [Array] cached key_attribute names # def self.key_attributes # This is a cache miss around 0.05 percent of the time. --daniel 2012-07-17 @key_attributes_cache ||= key_attribute_parameters.collect { |p| p.name } end # Returns a mapping from the title string to setting of attribute value(s). # This default implementation provides a mapping of title to the one and only _namevar_ present # in the type's definition. # @note Advanced: some logic requires this mapping to be done differently, using a different # validation/pattern, breaking up the title # into several parts assigning each to an individual attribute, or even use a composite identity where # all namevars are seen as part of the unique identity (such computation is done by the {#uniqueness} method. # These advanced options are rarely used (only one of the built in puppet types use this, and then only # a small part of the available functionality), and the support for these advanced mappings is not # implemented in a straight forward way. For these reasons, this method has been marked as private). # # @raise [Puppet::DevError] if there is no title pattern and there are two or more key attributes # @return [Array>>>, nil] a structure with a regexp and the first key_attribute ??? # @comment This wonderful piece of logic creates a structure used by Resource.parse_title which # has the capability to assign parts of the title to one or more attributes; It looks like an implementation # of a composite identity key (all parts of the key_attributes array are in the key). This can also # be seen in the method uniqueness_key. # The implementation in this method simply assigns the title to the one and only namevar (which is name # or a variable marked as namevar). # If there are multiple namevars (any in addition to :name?) then this method MUST be implemented # as it raises an exception if there is more than 1. Note that in puppet, it is only File that uses this # to create a different pattern for assigning to the :path attribute # This requires further digging. # The entire construct is somewhat strange, since resource checks if the method "title_patterns" is # implemented (it seems it always is) - why take this more expensive regexp mathching route for all # other types? # @api private # def self.title_patterns case key_attributes.length when 0; [] when 1; [ [ /(.*)/m, [ [key_attributes.first] ] ] ] else raise Puppet::DevError,"you must specify title patterns when there are two or more key attributes" end end # Produces a _uniqueness_key_ # @todo Explain what a uniqueness_key is # @return [Object] an object that is a _uniqueness_key_ for this object # def uniqueness_key self.class.key_attributes.sort_by { |attribute_name| attribute_name.to_s }.map{ |attribute_name| self[attribute_name] } end # Creates a new parameter. # @param name [Symbol] the name of the parameter # @param options [Hash] a hash with options. # @option options [Class] :parent (Puppet::Parameter) the super class of this parameter # @option options [Hash{String => Object}] :attributes a hash that is applied to the generated class # by calling setter methods corresponding to this hash's keys/value pairs. This is done before the given # block is evaluated. # @option options [Boolean] :boolean (false) specifies if this is a boolean parameter # @option options [Boolean] :namevar (false) specifies if this parameter is the namevar # @option options [Symbol, Array] :required_features specifies required provider features by name # @return [Class] the created parameter # @yield [ ] a required block that is evaluated in the scope of the new parameter # @api public # @dsl type # 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 # Creates a new property. # @param name [Symbol] the name of the property # @param options [Hash] a hash with options. # @option options [Symbol] :array_matching (:first) specifies how the current state is matched against # the wanted state. Use `:first` if the property is single valued, and (`:all`) otherwise. # @option options [Class] :parent (Puppet::Property) the super class of this property # @option options [Hash{String => Object}] :attributes a hash that is applied to the generated class # by calling setter methods corresponding to this hash's keys/value pairs. This is done before the given # block is evaluated. # @option options [Boolean] :boolean (false) specifies if this is a boolean parameter # @option options [Symbol] :retrieve the method to call on the provider (or `parent` if `provider` is not set) # to retrieve the current value of this property. # @option options [Symbol, Array] :required_features specifies required provider features by name # @return [Class] the created property # @yield [ ] a required block that is evaluated in the scope of the new property # @api public # @dsl type # def self.newproperty(name, options = {}, &block) name = name.intern # 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 [Array] Returns the parameter names def self.parameters return [] unless defined?(@parameters) @parameters.collect { |klass| klass.name } end # @return [Puppet::Parameter] Returns the parameter class associated with the given parameter name. def self.paramclass(name) @paramhash[name] end # @return [Puppet::Property] Returns the property class ??? associated with the given property name def self.propertybyname(name) @validproperties[name] end # Returns whether or not the given name is the name of a property, parameter or meta-parameter # @return [Boolean] true if the given attribute name is the name of an existing property, parameter or meta-parameter # def self.validattr?(name) name = name.intern 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 # @return [Boolean] Returns true if the given name is the name of an existing property def self.validproperty?(name) name = name.intern @validproperties.include?(name) && @validproperties[name] end # @return [Array, {}] Returns a list of valid property names, or an empty hash if there are none. # @todo An empty hash is returned if there are no defined parameters (not an empty array). This looks like # a bug. # def self.validproperties return {} unless defined?(@parameters) @validproperties.keys end # @return [Boolean] Returns true if the given name is the name of an existing 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 # (see validattr?) # @note see comment in code - how should this be documented? Are some of the other query methods deprecated? # (or should be). # @comment 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 [Boolean] Returns true if the wanted state of the resoure is that it should be absent (i.e. to be deleted). def deleting? obj = @parameters[:ensure] and obj.should == :absent end # Creates a new property value holder for the resource if it is valid and does not already exist # @return [Boolean] 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 # @return [Symbol, Boolean] Returns the name of the namevar if there is only one or false otherwise. # @comment This is really convoluted and part of the support for multiple namevars (?). # If there is only one namevar, the produced value is naturally this namevar, but if there are several? # The logic caches the name of the namevar if it is a single name, but otherwise always # calls key_attributes, and then caches the first if there was only one, otherwise it returns # false and caches this (which is then subsequently returned as a cache hit). # def name_var return @name_var_cache unless @name_var_cache.nil? key_attributes = self.class.key_attributes @name_var_cache = (key_attributes.length == 1) && key_attributes.first end # Gets the 'should' (wanted state) value of a parameter or property by name. # To explicitly get the 'is' (current state) value use `o.is(:name)`, and to explicitly get the 'should' value # use `o.should(:name)` # @param name [String] the name of the attribute to obtain the 'should' value for. # @return [Object] 'should'/wanted value of the given attribute def [](name) name = name.intern fail("Invalid parameter #{name}(#{name.inspect})") unless self.class.validattr?(name) if name == :name && nv = name_var name = nv end if obj = @parameters[name] # Note that if this is a property, then the value is the "should" value, # not the current value. obj.value else return nil end end # Sets the 'should' (wanted state) value of a property, or the value of a parameter. # @return # @raise [Puppet::Error] if the setting of the value fails, or if the given name is nil. # @raise [Puppet::ResourceError] when the parameter validation raises Puppet::Error or # ArgumentError def []=(name,value) name = name.intern fail("Invalid parameter #{name}") unless self.class.validattr?(name) if name == :name && nv = name_var name = nv end raise Puppet::Error.new("Got nil value for #{name}") if value.nil? property = self.newattr(name) if property begin # make sure the parameter doesn't have any errors property.value = value rescue Puppet::Error, ArgumentError => detail error = Puppet::ResourceError.new("Parameter #{name} failed on #{ref}: #{detail}") adderrorcontext(error, detail) raise error end end nil end # Removes a property from the object; useful in testing or in cleanup # when an error has been encountered # @todo Incomprehensible - the comment says "Remove a property", the code refers to @parameters, and # the method parameter is called "attr" - What is it, property, parameter, both (i.e an attribute) or what? # @todo Don't know what the attr is (name or Property/Parameter?). Guessing it is a String name... # @todo Is it possible to delete a meta-parameter? # @todo What does delete mean? Is it deleted from the type or is its value state 'is'/'should' deleted? # @param attr [String] the attribute to delete from this object. WHAT IS THE TYPE? # @raise [Puppet::DecError] when an attempt is made to delete an attribute that does not exists. # def delete(attr) attr = attr.intern if @parameters.has_key?(attr) @parameters.delete(attr) else raise Puppet::DevError.new("Undefined attribute '#{attr}' in #{self}") end end # Iterates over the existing properties. # @todo what does this mean? As opposed to iterating over the "non existing properties" ??? Is it an # iteration over those properties that have state? CONFUSING. # @yieldparam property [Puppet::Property] each property # @return [void] def eachproperty # properties is a private method properties.each { |property| yield property } end # Return the parameters, metaparams, and properties that have a value or were set by a default. Properties are # included since they are a subclass of parameter. # @return [Array] Array of parameter objects ( or subclass thereof ) def parameters_with_value self.class.allattrs.collect { |attr| parameter(attr) }.compact end # Iterates over all parameters with value currently set. # @yieldparam parameter [Puppet::Parameter] or a subclass thereof # @return [void] def eachparameter parameters_with_value.each { |parameter| yield parameter } end # Creates a transaction event. # Called by Transaction or by a property. # Merges the given options with the options `:resource`, `:file`, `:line`, and `:tags`, initialized from # values in this object. For possible options to pass (if any ????) see {Puppet::Transaction::Event}. # @todo Needs a better explanation "Why should I care who is calling this method?", What do I need to know # about events and how they work? Where can I read about them? # @param options [Hash] options merged with a fixed set of options defined by this method, passed on to {Puppet::Transaction::Event}. # @return [Puppet::Transaction::Event] the created event def event(options = {}) Puppet::Transaction::Event.new({:resource => self, :file => file, :line => line, :tags => tags}.merge(options)) end # @return [Object, nil] Returns the 'should' (wanted state) value for a specified property, or nil if the # given attribute name is not a property (i.e. if it is a parameter, meta-parameter, or does not exist). def should(name) name = name.intern (prop = @parameters[name] and prop.is_a?(Puppet::Property)) ? prop.should : nil end # Creates an instance to represent/manage the given attribute. # Requires either the attribute name or class as the first argument, then an optional hash of # attributes to set during initialization. # @todo The original comment is just wrong - the method does not accept a hash of options # @todo Detective work required; this method interacts with provider to ask if it supports a parameter of # the given class. it then returns the parameter if it exists, otherwise creates a parameter # with its :resource => self. # @overload newattr(name) # @param name [String] Unclear what name is (probably a symbol) - Needs investigation. # @overload newattr(klass) # @param klass [Class] a class supported as an attribute class - Needs clarification what that means. # @return [???] Probably returns a new instance of the class - Needs investigation. # def newattr(name) if name.is_a?(Class) klass = name name = klass.name end unless klass = self.class.attrclass(name) raise Puppet::Error, "Resource type #{self.class.name} does not support parameter #{name}" end if provider and ! provider.class.supports_parameter?(klass) missing = klass.required_features.find_all { |f| ! provider.class.feature?(f) } debug "Provider %s does not support features %s; not managing attribute %s" % [provider.class.name, missing.join(", "), name] return nil end return @parameters[name] if @parameters.include?(name) @parameters[name] = klass.new(:resource => self) end # Returns a string representation of the resource's containment path in # the catalog. # @return [String] def path @path ||= '/' + pathbuilder.join('/') end # Returns the value of this object's parameter given by name # @param name [String] the name of the parameter # @return [Object] the value def parameter(name) @parameters[name.to_sym] end # Returns a shallow copy of this object's hash of parameters. # @todo Add that this is not only "parameters", but also "properties" and "meta-parameters" ? # Changes to the contained parameters will have an effect on the parameters of this type, but changes to # the returned hash does not. # @return [Hash{String => Puppet:???Parameter}] a new hash being a shallow copy of the parameters map name to parameter def parameters @parameters.dup end # @return [Boolean] Returns whether the property given by name is defined or not. # @todo what does it mean to be defined? def propertydefined?(name) name = name.intern unless name.is_a? Symbol @parameters.include?(name) end # Returns a {Puppet::Property} instance by name. # To return the value, use 'resource[param]' # @todo LAK:NOTE(20081028) Since the 'parameter' method is now a superset of this method, # this one should probably go away at some point. - Does this mean it should be deprecated ? # @return [Puppet::Property] the property with the given name, or nil if not a property or does not exist. def property(name) (obj = @parameters[name.intern] and obj.is_a?(Puppet::Property)) ? obj : nil end # @todo comment says "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." # @todo Needs a better explanation, and investigation about the claim an array can be passed (it is passed # to self.class.attrclass to produce a class on which a check is made if it has a method class :default (does # not seem to support an array... # @return [void] # 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 # @todo the comment says: "Convert our object to a hash. This just includes properties." # @todo this is confused, again it is the @parameters instance variable that is consulted, and # each value is copied - does it contain "properties" and "parameters" or both? Does it contain # meta-parameters? # # @return [Hash{ ??? => ??? }] a hash of WHAT?. The hash is a shallow copy, any changes to the # objects returned in this hash will be reflected in the original resource having these attributes. # def to_hash rethash = {} @parameters.each do |name, obj| rethash[name] = obj.value end rethash end # @return [String] the name of this object's class # @todo Would that be "file" for the "File" resource type? of "File" or something else? # def type self.class.name end # @todo Comment says "Return a specific value for an attribute.", as opposed to what "An upspecific value"??? # @todo is this the 'is' or the 'should' value? # @todo why is the return restricted to things that respond to :value? (Only non structural basic data types # supported? # # @return [Object, nil] the value of the attribute having the given name, or nil if the given name is not # an attribute, or the referenced attribute does not respond to `:value`. def value(name) name = name.intern (obj = @parameters[name] and obj.respond_to?(:value)) ? obj.value : nil end # @todo What is this used for? Needs a better explanation. # @return [???] the version of the catalog or 0 if there is no catalog. def version return 0 unless catalog catalog.version end # @return [Array] Returns all of the property objects, in the order specified in the # class. # @todo "what does the 'order specified in the class' mean? The order the properties where added in the # ruby file adding a new type with new properties? # def properties self.class.properties.collect { |prop| @parameters[prop.name] }.compact end # Returns true if the type's notion of name is the identity of a resource. # See the overview of this class for a longer explanation of the concept _isomorphism_. # Defaults to true. # # @return [Boolan] true, if this type's name is isomorphic with the object def self.isomorphic? if defined?(@isomorphic) return @isomorphic else return true end end # @todo check that this gets documentation (it is at the class level as well as instance). # (see isomorphic?) def isomorphic? self.class.isomorphic? end # Returns true if the instance is 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. # @note An object that is managed always stays managed, but an object that is not managed # may become managed later in its lifecycle. # @return [Boolean] true if the object is managed 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. # Returns true if the search should be done in depth-first order. # This implementation always returns false. # @todo What is this used for? # # @return [Boolean] true if the search should be done in depth first order. # def depthfirst? false end # Removes this object (FROM WHERE?) # @todo removes if from where? # @overload remove(rmdeps) # @deprecated Use remove() # @param rmdeps [Boolean] intended to indicate that all subscriptions should also be removed, ignored. # @overload remove() # @return [void] # 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. # Returns the ancestors - WHAT? # This implementation always returns an empty list. # @todo WHAT IS THIS ? # @return [Array] returns a list of ancestors. def ancestors [] end # Flushes the provider if supported by the provider, else no action. # This is called by the transaction. # @todo What does Flushing the provider mean? Why is it interesting to know that this is # called by the transaction? (It is not explained anywhere what a transaction is). # # @return [???, nil] WHAT DOES IT RETURN? GUESS IS VOID def flush self.provider.flush if self.provider and self.provider.respond_to?(:flush) end # Returns true if all contained objects are in sync. # @todo "contained in what?" in the given "in" parameter? # # @todo deal with the comment _"FIXME I don't think this is used on the type instances any more, # it's really only used for testing"_ # @return [Boolean] true if in sync, false otherwise. # def insync?(is) insync = true if property = @parameters[:ensure] unless is.include? property raise Puppet::DevError, "The is value is not in the is array for '#{property.name}'" end ensureis = is[property] if property.safe_insync?(ensureis) and property.should == :absent return true end end properties.each { |property| unless is.include? property raise Puppet::DevError, "The is value is not in the is array for '#{property.name}'" end propis = is[property] unless property.safe_insync?(propis) property.debug("Not in sync: #{propis.inspect} vs #{property.should.inspect}") insync = false #else # property.debug("In sync") end } #self.debug("#{self} sync status is #{insync}") insync end # Retrieves the current value of all contained properties. # Parameters and meta-parameters are not included in the result. # @todo As oposed to all non contained properties? How is this different than any of the other # methods that also "gets" properties/parameters/etc. ? # @return [Puppet::Resource] array of all property values (mix of types) # @raise [fail???] if there is a provider and it is not suitable for the host this is evaluated for. 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 # Retrieve the current state of the system as a Puppet::Resource. For # the base Puppet::Type this does the same thing as #retrieve, but # specific types are free to implement #retrieve as returning a hash, # and this will call #retrieve and convert the hash to a resource. # This is used when determining when syncing a resource. # # @return [Puppet::Resource] A resource representing the current state # of the system. # # @api private def retrieve_resource resource = retrieve resource = Resource.new(type, title, :parameters => resource) if resource.is_a? Hash resource end + # Given the hash of current properties, should this resource be treated as if it + # currently exists on the system. May need to be overridden by types that offer up + # more than just :absent and :present. + def present?(current_values) + current_values[:ensure] != :absent + end + # Returns a hash of the current properties and their values. # If a resource is absent, its value is the symbol `:absent` # @return [Hash{Puppet::Property => Object}] mapping of property instance to its 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 # Returns the `noop` run mode status of this. # @return [Boolean] true if running in noop mode. def noop? # If we're not a host_config, we're almost certainly part of # Settings, and we want to ignore 'noop' return false if catalog and ! catalog.host_config? if defined?(@noop) @noop else Puppet[:noop] end end # (see #noop?) def noop noop? end # Retrieves all known instances. # @todo Retrieves them from where? Known to whom? # Either requires providers or must be overridden. # @raise [Puppet::DevError] when there are no providers and the implementation has not overridded this method. 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| self.properties.find_all do |property| provider.supports_parameter?(property) end.collect do |property| property.name end 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.debug "%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 result = new(:name => instance.name, :provider => instance) properties.each { |name| result.newattr(name) } result end end.flatten.compact end # Returns a list of one suitable provider per source, with the default provider first. # @todo Needs better explanation; what does "source" mean in this context? # @return [Array] list of providers # def self.providers_by_source # Put the default provider first (can be nil), then the rest of the suitable providers. sources = [] [defaultprovider, suitableprovider].flatten.uniq.collect do |provider| next if provider.nil? next if sources.include?(provider.source) sources << provider.source provider end.compact end # Converts a simple hash into a Resource instance. # @todo as opposed to a complex hash? Other raised exceptions? # @param [Hash{Symbol, String => Object}] hash resource attribute to value map to initialize the created resource from # @return [Puppet::Resource] the resource created from the hash # @raise [Puppet::Error] if a title is missing in the given hash 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) resource.catalog = hash.delete(:catalog) hash.each do |param, value| resource[param] = value end resource end # Returns an array of strings representing the containment heirarchy # (types/classes) that make up the path to the resource from the root # of the catalog. This is mostly used for logging purposes. # # @api private 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 "Whether to apply this resource in noop mode. When applying a resource in noop mode, Puppet will check whether it is in sync, like it does when running normally. However, if a resource attribute is not in the desired state (as declared in the catalog), Puppet will take no action, and will instead report the changes it _would_ have made. These simulated changes will appear in the report sent to the puppet master, or be shown on the console if running puppet agent or puppet apply in the foreground. The simulated changes will not send refresh events to any subscribing or notified resources, although Puppet will log that a refresh event _would_ have been sent. **Important note:** [The `noop` setting](http://docs.puppetlabs.com/references/latest/configuration.html#noop) allows you to globally enable or disable noop mode, but it will _not_ override the `noop` metaparameter on individual resources. That is, the value of the global `noop` setting will _only_ affect resources that do not have an explicit value set for their `noop` attribute." 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 "A schedule to govern when Puppet is allowed to manage this resource. The value of this metaparameter must be the `name` of a `schedule` resource. This means you must declare a schedule resource, then refer to it by name; see [the docs for the `schedule` type](http://docs.puppetlabs.com/references/latest/type.html#schedule) for more info. schedule { 'everyday': period => daily, range => \"2-4\" } exec { \"/usr/bin/apt-get update\": schedule => 'everyday' } Note that you can declare the schedule resource anywhere in your manifests, as long as it ends up in the final compiled catalog." end newmetaparam(:audit) do desc "Marks a subset of this resource's unmanaged attributes for auditing. Accepts an attribute name, an array of attribute names, or `all`. Auditing a resource attribute has two effects: First, whenever a catalog is applied with puppet apply or puppet agent, Puppet will check whether that attribute of the resource has been modified, comparing its current value to the previous run; any change will be logged alongside any actions performed by Puppet while applying the catalog. Secondly, marking a resource attribute for auditing will include that attribute in inspection reports generated by puppet inspect; see the puppet inspect documentation for more details. Managed attributes for a resource can also be audited, but note that changes made by Puppet will be logged as additional modifications. (I.e. if a user manually edits a file whose contents are audited and managed, puppet agent's next two runs will both log an audit notice: the first run will log the user's edit and then revert the file to the desired state, and the second run will log the edit made by Puppet.)" validate do |list| list = Array(list).collect {|p| p.to_sym} unless list == [:all] list.each do |param| next if @resource.class.validattr?(param) fail "Cannot audit #{param}: not a valid attribute for #{resource}" end end end munge do |args| properties_to_audit(args).each do |param| next unless resource.class.validproperty?(param) resource.newattr(param) end end def all_properties resource.class.properties.find_all do |property| resource.provider.nil? or resource.provider.class.supports_parameter?(property) end.collect do |property| property.name end end def properties_to_audit(list) if !list.kind_of?(Array) && list.to_sym == :all list = all_properties else list = Array(list).collect { |p| p.to_sym } end end end newmetaparam(: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 %q{Creates an alias for the resource. Puppet uses this internally when you provide a symbolic title and an explicit namevar value: 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 title, and the library sets that as an alias for the file so the dependency lookup in `Service['sshd']` works. You can use this metaparameter yourself, but note that aliases generally only work for creating relationships; anything else that refers to an existing resource (such as amending or overriding resource attributes in an inherited class) must use the resource's exact title. For example, 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. } 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. Multiple tags can be specified as an array: file {'/etc/hosts': ensure => file, source => 'puppet:///modules/site/hosts', mode => 0644, tag => ['bootstrap', 'minimumrun', 'mediumrun'], } Tags are useful for things like applying a subset of a host's configuration with [the `tags` setting](/references/latest/configuration.html#tags) (e.g. `puppet agent --test --tags bootstrap`) or filtering alerts with [the `tagmail` report processor](http://docs.puppetlabs.com/references/latest/report.html#tagmail)." munge do |tags| tags = [tags] unless tags.is_a? Array tags.each do |tag| @resource.tag(tag) end end end # RelationshipMetaparam is an implementation supporting the meta-parameters `:require`, `:subscribe`, # `:notify`, and `:before`. # # class RelationshipMetaparam < Puppet::Parameter class << self attr_accessor :direction, :events, :callback, :subclasses end @subclasses = [] def self.inherited(sub) @subclasses << sub end # @return [Array] turns attribute value(s) into list of resources 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 # Checks each reference to assert that what it references exists in the catalog. # # @raise [???fail] if the referenced resource can not be found # @return [void] def validate_relationship @value.each do |ref| unless @resource.catalog.resource(ref.to_s) description = self.class.direction == :in ? "dependency" : "dependent" fail ResourceError, "Could not find #{description} #{ref} for #{resource.ref}" end end end # Creates edges for all relationships. # The `:in` relationships are specified by the event-receivers, and `:out` # relationships are specified by the event generator. # @todo references to "event-receivers" and "event generator" means in this context - are those just # the resources at the two ends of the relationship? # This way 'source' and 'target' are consistent terms in both edges # and events, i.e. 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. # @return [Array] # @raise [???fail] when a reference can not be resolved # 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 Puppet::Relationship.new(source, target, subargs) end end end # @todo document this, have no clue what this does... it retuns "RelationshipMetaparam.subclasses" # 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 resources that this resource depends on, expressed as [resource references](http://docs.puppetlabs.com/puppet/latest/reference/lang_datatypes.html#resource-references). Multiple resources can be specified as an array of references. When this attribute is present: * The required resource(s) will be applied **before** this resource. This is one of the four relationship metaparameters, along with `before`, `notify`, and `subscribe`. For more context, including the alternate chaining arrow (`->` and `~>`) syntax, see [the language page on relationships](http://docs.puppetlabs.com/puppet/latest/reference/lang_relationships.html)." end newmetaparam(:subscribe, :parent => RelationshipMetaparam, :attributes => {:direction => :in, :events => :ALL_EVENTS, :callback => :refresh}) do desc "One or more resources that this resource depends on, expressed as [resource references](http://docs.puppetlabs.com/puppet/latest/reference/lang_datatypes.html#resource-references). Multiple resources can be specified as an array of references. When this attribute is present: * The subscribed resource(s) will be applied _before_ this resource. * If Puppet makes changes to any of the subscribed resources, it will cause this resource to _refresh._ (Refresh behavior varies by resource type: services will restart, mounts will unmount and re-mount, etc. Not all types can refresh.) This is one of the four relationship metaparameters, along with `before`, `require`, and `notify`. For more context, including the alternate chaining arrow (`->` and `~>`) syntax, see [the language page on relationships](http://docs.puppetlabs.com/puppet/latest/reference/lang_relationships.html)." end newmetaparam(:before, :parent => RelationshipMetaparam, :attributes => {:direction => :out, :events => :NONE}) do desc "One or more resources that depend on this resource, expressed as [resource references](http://docs.puppetlabs.com/puppet/latest/reference/lang_datatypes.html#resource-references). Multiple resources can be specified as an array of references. When this attribute is present: * This resource will be applied _before_ the dependent resource(s). This is one of the four relationship metaparameters, along with `require`, `notify`, and `subscribe`. For more context, including the alternate chaining arrow (`->` and `~>`) syntax, see [the language page on relationships](http://docs.puppetlabs.com/puppet/latest/reference/lang_relationships.html)." end newmetaparam(:notify, :parent => RelationshipMetaparam, :attributes => {:direction => :out, :events => :ALL_EVENTS, :callback => :refresh}) do desc "One or more resources that depend on this resource, expressed as [resource references](http://docs.puppetlabs.com/puppet/latest/reference/lang_datatypes.html#resource-references). Multiple resources can be specified as an array of references. When this attribute is present: * This resource will be applied _before_ the notified resource(s). * If Puppet makes changes to this resource, it will cause all of the notified resources to _refresh._ (Refresh behavior varies by resource type: services will restart, mounts will unmount and re-mount, etc. Not all types can refresh.) This is one of the four relationship metaparameters, along with `before`, `require`, and `subscribe`. For more context, including the alternate chaining arrow (`->` and `~>`) syntax, see [the language page on relationships](http://docs.puppetlabs.com/puppet/latest/reference/lang_relationships.html)." end newmetaparam(:stage) do desc %{Which run stage this class should reside in. **Note: This metaparameter can only be used on classes,** and only when declaring them with the resource-like syntax. It cannot be used on normal resources or on classes declared with `include`. By default, all classes are declared in the `main` stage. To assign a class to a different stage, you must: * Declare the new stage as a [`stage` resource](http://docs.puppetlabs.com/references/latest/type.html#stage). * Declare an order relationship between the new stage and the `main` stage. * Use the resource-like syntax to declare the class, and set the `stage` metaparameter to the name of the desired stage. For example: stage { 'pre': before => Stage['main'], } class { 'apt-updates': stage => 'pre', } } 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 # The provider that has been selected for the instance of the resource type. # @return [Puppet::Provider,nil] the selected provider or nil, if none has been selected # attr_reader :provider # the Type class attribute accessors class << self # The loader of providers to use when loading providers from disk. # Although it looks like this attribute provides a way to operate with different loaders of # providers that is not the case; the attribute is written when a new type is created, # and should not be changed thereafter. # @api private # attr_accessor :providerloader # @todo Don't know if this is a name, or a reference to a Provider instance (now marked up as an instance # of Provider. # @return [Puppet::Provider, nil] The default provider for this type, or nil if non is defines # attr_writer :defaultprovider end # The default provider, or the most suitable provider if no default provider was set. # @note a warning will be issued if no default provider has been configured and a search for the most # suitable provider returns more than one equally suitable provider. # @return [Puppet::Provider, nil] the default or most suitable provider, or nil if no provider was found # def self.defaultprovider return @defaultprovider if @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 } 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}" ) end @defaultprovider = defaults.shift unless defaults.empty? end # @return [Hash{??? => Puppet::Provider}] Returns a hash of WHAT EXACTLY for the given type # @todo what goes into this hash? def self.provider_hash_by_type(type) @provider_hashes ||= {} @provider_hashes[type] ||= {} end # @return [Hash{ ??? => Puppet::Provider}] Returns a hash of WHAT EXACTLY for this type. # @see provider_hash_by_type method to get the same for some other type def self.provider_hash Puppet::Type.provider_hash_by_type(self.name) end # Returns the provider having the given name. # This will load a provider if it is not already loaded. The returned provider is the first found provider # having the given name, where "first found" semantics is defined by the {providerloader} in use. # # @param name [String] the name of the provider to get # @return [Puppet::Provider, nil] the found provider, or nil if no provider of the given name was found # def self.provider(name) name = name.intern # If we don't have it yet, try loading it. @providerloader.load(name) unless provider_hash.has_key?(name) provider_hash[name] end # Returns a list of loaded providers by name. # This method will not load/search for available providers. # @return [Array] list of loaded provider names # def self.providers provider_hash.keys end # Returns true if the given name is a reference to a provider and if this is a suitable provider for # this type. # @todo How does the provider know if it is suitable for the type? Is it just suitable for the platform/ # environment where this method is executing? # @param name [String] the name of the provider for which validity is checked # @return [Boolean] true if the given name references a provider that is suitable # def self.validprovider?(name) name = name.intern (provider_hash.has_key?(name) && provider_hash[name].suitable?) end # Creates a new provider of a type. # This method must be called directly on the type that it's implementing. # @todo Fix Confusing Explanations! # Is this a new provider of a Type (metatype), or a provider of an instance of Type (a resource), or # a Provider (the implementation of a Type's behavior). CONFUSED. It calls magically named methods like # "providify" ... # @param name [String, Symbol] the name of the WHAT? provider? type? # @param options [Hash{Symbol => Object}] a hash of options, used by this method, and passed on to {#genclass}, (see # it for additional options to pass). # @option options [Puppet::Provider] :parent the parent provider (what is this?) # @option options [Puppet::Type] :resource_type the resource type, defaults to this type if unspecified # @return [Puppet::Provider] a provider ??? # @raise [Puppet::DevError] when the parent provider could not be found. # def self.provide(name, options = {}, &block) name = name.intern if unprovide(name) Puppet.debug "Reloading #{name} #{self.name} provider" 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 # Ensures there is a `:provider` parameter defined. # Should only be called if there are providers. # @return [void] def self.providify return if @paramhash.has_key? :provider newparam(:provider) do # We're using a hacky way to get the name of our type, since there doesn't # seem to be a correct way to introspect this at the time this code is run. # We expect that the class in which this code is executed will be something # like Puppet::Type::Ssh_authorized_key::ParameterProvider. desc <<-EOT The specific backend to use for this `#{self.to_s.split('::')[2].downcase}` resource. You will seldom need to specify this --- Puppet will usually discover the appropriate provider for your platform. EOT # This is so we can refer back to the type to get a list of # providers for documentation. class << self # The reference to a parent type for the parameter `:provider` used to get a list of # providers for documentation purposes. # attr_accessor :parenttype end # Provides the ability to add documentation to a provider. # def self.doc # Since we're mixing @doc with text from other sources, we must normalize # its indentation with scrub. But we don't need to manually scrub the # provider's doc string, since markdown_definitionlist sanitizes its inputs. scrub(@doc) + "Available providers are:\n\n" + parenttype.providers.sort { |a,b| a.to_s <=> b.to_s }.collect { |i| markdown_definitionlist( i, scrub(parenttype().provider(i).doc) ) }.join end # @todo this does what? where and how? # @return [String] the name of the provider defaultto { prov = @resource.class.defaultprovider prov.name if prov } 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 @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 # @todo this needs a better explanation # Removes the implementation class of a given provider. # @return [Object] returns what {Puppet::Util::ClassGen#rmclass} returns def self.unprovide(name) if @defaultprovider and @defaultprovider.name == name @defaultprovider = nil end rmclass(name, :hash => provider_hash, :prefix => "Provider") end # Returns a list of suitable providers for the given type. # A call to this method will load all providers if not already loaded and ask each if it is # suitable - those that are are included in the result. # @note This method also does some special processing which rejects a provider named `:fake` (for testing purposes). # @return [Array] Returns an array of all 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 # @return [Boolean] Returns true if this is something else than a `:provider`, or if it # is a provider and it is suitable, or if there is a default provider. Otherwise, false is returned. # def suitable? # If we don't use providers, then we consider it suitable. return true unless self.class.paramclass(:provider) # We have a provider and it is suitable. return true if provider && provider.class.suitable? # We're using the default provider and there is one. if !provider and self.class.defaultprovider self.provider = self.class.defaultprovider.name return true end # We specified an unsuitable provider, or there isn't any suitable # provider. false end # Sets the provider to the given provider/name. # @overload provider=(name) # Sets the provider to the result of resolving the name to an instance of Provider. # @param name [String] the name of the provider # @overload provider=(provider) # Sets the provider to the given instances of Provider. # @param provider [Puppet::Provider] the provider to set # @return [Puppet::Provider] the provider set # @raise [ArgumentError] if the provider could not be found/resolved. # 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. # Adds a block producing a single name (or list of names) of the given resource type name to autorequire. # @example Autorequire the files File['foo', 'bar'] # autorequire( 'file', {|| ['foo', 'bar'] }) # # @todo original = _"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."_ # @param name [String] the name of a type of which one or several resources should be autorequired e.g. "file" # @yield [ ] a block returning list of names of given type to auto require # @yieldreturn [String, Array] one or several resource names for the named type # @return [void] # @dsl type # @api public # def self.autorequire(name, &block) @autorequires ||= {} @autorequires[name] = block end # Provides iteration over added auto-requirements (see {autorequire}). # @yieldparam type [String] the name of the type to autoriquire an instance of # @yieldparam block [Proc] a block producing one or several dependencies to auto require (see {autorequire}). # @yieldreturn [void] # @return [void] def self.eachautorequire @autorequires ||= {} @autorequires.each { |type, block| yield(type, block) } end # Adds dependencies to the catalog from added autorequirements. # See {autorequire} for how to add an auto-requirement. # @todo needs details - see the param rel_catalog, and type of this param # @param rel_catalog [Puppet::Resource::Catalog, nil] the catalog to # add dependencies to. Defaults to the current catalog (set when the # type instance was added to a catalog) # @raise [Puppet::DevError] if there is no catalog # 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 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| # 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 # Builds the dependencies associated with an individual object. # @todo Which object is the "individual object", as opposed to "object as a group?" or should it simply # be "this object" as in "this resource" ? # @todo Does this method "build dependencies" or "build what it depends on" ... CONFUSING # # @return [Array] list of WHAT? resources? edges? 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 # Sets the initial list of tags... # @todo The initial list of tags, that ... that what? # @return [void] ??? def tags=(list) tag(self.class.name) tag(*list) end # @comment - these two comments were floating around here, and turned up as documentation # for the attribute "title", much to my surprise and amusement. Clearly these comments # are orphaned ... I think they can just be removed as what they say should be covered # by the now added yardoc. (Yo! to quote some of the other actual awsome specific comments applicable # to objects called from elsewhere, or not. ;-) # # @comment 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. # @comment 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. # The title attribute of WHAT ??? # @todo Figure out what this is the title attribute of (it appears on line 1926 currently). # @return [String] the title attr_writer :title # The noop attribute of WHAT ??? does WHAT??? # @todo Figure out what this is the noop attribute of (it appears on line 1931 currently). # @return [???] the noop WHAT ??? (mode? if so of what, or noop for an instance of the type, or for all # instances of a type, or for what??? # attr_writer :noop include Enumerable # class methods dealing with Type management public # The Type class attribute accessors class << self # @return [String] the name of the resource type; e.g., "File" # attr_reader :name # @return [Boolean] true if the type should send itself a refresh event on change. # attr_accessor :self_refresh include Enumerable, Puppet::Util::ClassGen include Puppet::MetaType::Manager include Puppet::Util include Puppet::Util::Logging end # Initializes all of the variables that must be initialized for each subclass. # @todo Does the explanation make sense? # @return [void] def self.initvars # all of the instances of this class @objects = Hash.new @aliases = Hash.new @defaults = {} @parameters ||= [] @validproperties = {} @properties = [] @parameters = [] @paramhash = {} @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 # Returns the name of this type (if specified) or the parent type #to_s. # The returned name is on the form "Puppet::Type::", where the first letter of name is # capitalized. # @return [String] the fully qualified name Puppet::Type:: where the first letter of name is captialized # def self.to_s if defined?(@name) "Puppet::Type::#{@name.to_s.capitalize}" else super end end # Creates a `validate` method that is used to validate a resource before it is operated on. # The validation should raise exceptions if the validation finds errors. (It is not recommended to # issue warnings as this typically just ends up in a logfile - you should fail if a validation fails). # The easiest way to raise an appropriate exception is to call the method {Puppet::Util::Errors.fail} with # the message as an argument. # # @yield [ ] a required block called with self set to the instance of a Type class representing a resource. # @return [void] # @dsl type # @api public # def self.validate(&block) define_method(:validate, &block) end # @return [String] The file from which this type originates from attr_accessor :file # @return [Integer] The line in {#file} from which this type originates from attr_accessor :line # @todo what does this mean "this resource" (sounds like this if for an instance of the type, not the meta Type), # but not sure if this is about the catalog where the meta Type is included) # @return [??? TODO] The catalog that this resource is stored in. attr_accessor :catalog # @return [Boolean] Flag indicating if this type is exported attr_accessor :exported # @return [Boolean] Flag indicating if the type is virtual (it should not be). attr_accessor :virtual # Creates a log entry with the given message at the log level specified by the parameter `loglevel` # @return [void] # 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 # @return [Hash] hash of parameters originally defined # @api private attr_reader :original_parameters # Creates an instance of Type from a hash or a {Puppet::Resource}. # @todo Unclear if this is a new Type or a new instance of a given type (the initialization ends # with calling validate - which seems like validation of an instance of a given type, not a new # meta type. # # @todo Explain what the Hash and Resource are. There seems to be two different types of # resources; one that causes the title to be set to resource.title, and one that # causes the title to be resource.ref ("for components") - what is a component? # # @overload initialize(hash) # @param [Hash] hash # @raise [Puppet::ResourceError] when the type validation raises # Puppet::Error or ArgumentError # @overload initialize(resource) # @param resource [Puppet:Resource] # @raise [Puppet::ResourceError] when the type validation raises # Puppet::Error or ArgumentError # def initialize(resource) 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) begin self.validate if self.respond_to?(:validate) rescue Puppet::Error, ArgumentError => detail error = Puppet::ResourceError.new("Validation of #{ref} failed: #{detail}") adderrorcontext(error, detail) raise error end end private # Sets the name of the resource from a hash containing a mapping of `name_var` to value. # Sets the value of the property/parameter appointed by the `name_var` (if it is defined). The value set is # given by the corresponding entry in the given hash - e.g. if name_var appoints the name `:path` the value # of `:path` is set to the value at the key `:path` in the given hash. As a side effect this key/value is then # removed from the given hash. # # @note This method mutates the given hash by removing the entry with a key equal to the value # returned from name_var! # @param hash [Hash] a hash of what # @return [void] def set_name(hash) self[name_var] = hash.delete(name_var) if name_var end # Sets parameters from the given hash. # Values are set in _attribute order_ i.e. higher priority attributes before others, otherwise in # the order they were specified (as opposed to just setting them in the order they happen to appear in # when iterating over the given hash). # # Attributes that are not included in the given hash are set to their default value. # # @todo Is this description accurate? Is "ensure" an example of such a higher priority attribute? # @return [void] # @raise [Puppet::DevError] when impossible to set the value due to some problem # @raise [ArgumentError, TypeError, Puppet::Error] when faulty arguments have been passed # 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 # Finishes any outstanding processing. # This method should be called as a final step in setup, # to allow the parameters that have associated auto-require needs to be processed. # # @todo what is the expected sequence here - who is responsible for calling this? When? # Is the returned type correct? # @return [Array] the validated list/set of attributes # def finish # Call post_compile hook on every parameter that implements it. This includes all subclasses # of parameter including, but not limited to, regular parameters, metaparameters, relationship # parameters, and properties. eachparameter do |parameter| parameter.post_compile if parameter.respond_to? :post_compile end # 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 # @comment For now, leave the 'name' method functioning like it used to. Once 'title' # works everywhere, I'll switch it. # Returns the resource's name # @todo There is a comment in source that this is not quite the same as ':title' and that a switch should # be made... # @return [String] the name of a resource def name self[:name] end # Returns the parent of this in the catalog. In case of an erroneous catalog # where multiple parents have been produced, the first found (non # deterministic) parent is returned. # @return [Puppet::Type, nil] the # containing resource or nil if there is no catalog or no containing # resource. def parent return nil unless catalog @parent ||= if parents = catalog.adjacent(self, :direction => :in) parents.shift else nil end end # Returns a reference to this as a string in "Type[name]" format. # @return [String] a reference to this object on the form 'Type[name]' # def ref # memoizing this is worthwhile ~ 3 percent of calls are the "first time # around" in an average run of Puppet. --daniel 2012-07-17 @ref ||= "#{self.class.name.to_s.capitalize}[#{self.title}]" end # (see self_refresh) # @todo check that meaningful yardoc is produced - this method delegates to "self.class.self_refresh" # @return [Boolean] - ??? returns true when ... what? # def self_refresh? self.class.self_refresh end # Marks the object as "being purged". # This method is used by transactions to forbid deletion when there are dependencies. # @todo what does this mean; "mark that we are purging" (purging what from where). How to use/when? # Is this internal API in transactions? # @see purging? def purging @purging = true end # Returns whether this resource is being purged or not. # This method is used by transactions to forbid deletion when there are dependencies. # @return [Boolean] the current "purging" state # def purging? if defined?(@purging) @purging else false end end # Returns the title of this object, or its name if title was not explicetly set. # If the title is not already set, it will be computed by looking up the {#name_var} and using # that value as the title. # @todo it is somewhat confusing that if the name_var is a valid parameter, it is assumed to # be the name_var called :name, but if it is a property, it uses the name_var. # It is further confusing as Type in some respects supports multiple namevars. # # @return [String] Returns the title of this object, or its name if title was not explicetly set. # @raise [??? devfail] if title is not set, and name_var can not be found. 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 # Produces a reference to this in reference format. # @see #ref # def to_s self.ref end # @todo What to resource? Which one of the resource forms is prroduced? returned here? # @return [??? Resource] a resource that WHAT??? # def to_resource resource = self.retrieve_resource resource.tag(*self.tags) @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) resource[name] = param.value end resource end # @return [Boolean] Returns whether the resource is virtual or not def virtual?; !!@virtual; end # @return [Boolean] Returns whether the resource is exported or not def exported?; !!@exported; end # @return [Boolean] Returns whether the resource is applicable to `:device` # Returns true if a resource of this type can be evaluated on a 'network device' kind # of hosts. # @api private def appliable_to_device? self.class.can_apply_to(:device) end # @return [Boolean] Returns whether the resource is applicable to `:host` # Returns true if a resource of this type can be evaluated on a regular generalized computer (ie not an appliance like a network device) # @api private def appliable_to_host? self.class.can_apply_to(:host) end end end require 'puppet/provider' # Always load these types. Puppet::Type.type(:component) diff --git a/lib/puppet/type/file.rb b/lib/puppet/type/file.rb index eee50e284..58a004821 100644 --- a/lib/puppet/type/file.rb +++ b/lib/puppet/type/file.rb @@ -1,849 +1,853 @@ require 'digest/md5' require 'cgi' require 'etc' require 'uri' require 'fileutils' require 'enumerator' require 'pathname' require 'puppet/parameter/boolean' require 'puppet/util/diff' require 'puppet/util/checksums' require 'puppet/util/backups' require 'puppet/util/symbolic_file_mode' Puppet::Type.newtype(:file) do include Puppet::Util::MethodHelper include Puppet::Util::Checksums include Puppet::Util::Backups include Puppet::Util::SymbolicFileMode @doc = "Manages files, including their content, ownership, and permissions. The `file` type can manage normal files, directories, and symlinks; the type should be specified in the `ensure` attribute. Note that symlinks cannot be managed on Windows systems. File contents can be managed directly with the `content` attribute, or downloaded from a remote source using the `source` attribute; the latter can also be used to recursively serve directories (when the `recurse` attribute is set to `true` or `local`). On Windows, note that file contents are managed in binary mode; Puppet never automatically translates line endings. **Autorequires:** If Puppet is managing the user or group that owns a file, the file resource will autorequire them. If Puppet is managing any parent directories of a file, the file resource will autorequire them." feature :manages_symlinks, "The provider can manage symbolic links." def self.title_patterns [ [ /^(.*?)\/*\Z/m, [ [ :path ] ] ] ] end newparam(:path) do desc <<-'EOT' The path to the file to manage. Must be fully qualified. On Windows, the path should include the drive letter and should use `/` as the separator character (rather than `\\`). EOT isnamevar validate do |value| unless Puppet::Util.absolute_path?(value) fail Puppet::Error, "File paths must be fully qualified, not '#{value}'" end end munge do |value| if value.start_with?('//') and ::File.basename(value) == "/" # This is a UNC path pointing to a share, so don't add a trailing slash ::File.expand_path(value) else ::File.join(::File.split(::File.expand_path(value))) end end end newparam(:backup) do desc <<-EOT Whether (and how) file content should be backed up before being replaced. This attribute works best as a resource default in the site manifest (`File { backup => main }`), so it can affect all file resources. * If set to `false`, file content won't be backed up. * If set to a string beginning with `.` (e.g., `.puppet-bak`), Puppet will use copy the file in the same directory with that value as the extension of the backup. (A value of `true` is a synonym for `.puppet-bak`.) * If set to any other string, Puppet will try to back up to a filebucket with that title. See the `filebucket` resource type for more details. (This is the preferred method for backup, since it can be centralized and queried.) Default value: `puppet`, which backs up to a filebucket of the same name. (Puppet automatically creates a **local** filebucket named `puppet` if one doesn't already exist.) Backing up to a local filebucket isn't particularly useful. If you want to make organized use of backups, you will generally want to use the puppet master server's filebucket service. This requires declaring a filebucket resource and a resource default for the `backup` attribute in site.pp: # /etc/puppet/manifests/site.pp filebucket { 'main': path => false, # This is required for remote filebuckets. server => 'puppet.example.com', # Optional; defaults to the configured puppet master. } File { backup => main, } If you are using multiple puppet master servers, you will want to centralize the contents of the filebucket. Either configure your load balancer to direct all filebucket traffic to a single master, or use something like an out-of-band rsync task to synchronize the content on all masters. EOT defaultto "puppet" munge do |value| # I don't really know how this is happening. value = value.shift if value.is_a?(Array) case value when false, "false", :false false when true, "true", ".puppet-bak", :true ".puppet-bak" when String value else self.fail "Invalid backup type #{value.inspect}" end end end newparam(:recurse) do desc "Whether and how to do recursive file management. Options are: * `inf,true` --- Regular style recursion on both remote and local directory structure. See `recurselimit` to specify a limit to the recursion depth. * `remote` --- Descends recursively into the remote (source) directory but not the local (destination) directory. Allows copying of a few files into a directory containing many unmanaged files without scanning all the local files. This can only be used when a source parameter is specified. * `false` --- Default of no recursion. " newvalues(:true, :false, :inf, :remote) validate { |arg| } munge do |value| newval = super(value) case newval when :true, :inf; true when :false; false when :remote; :remote else self.fail "Invalid recurse value #{value.inspect}" end end end newparam(:recurselimit) do desc "How deeply to do recursive management." newvalues(/^[0-9]+$/) munge do |value| newval = super(value) case newval when Integer, Fixnum, Bignum; value when /^\d+$/; Integer(value) else self.fail "Invalid recurselimit value #{value.inspect}" end end end newparam(:replace, :boolean => true, :parent => Puppet::Parameter::Boolean) do desc "Whether to replace a file or symlink that already exists on the local system but whose content doesn't match what the `source` or `content` attribute specifies. Setting this to false allows file resources to initialize files without overwriting future changes. Note that this only affects content; Puppet will still manage ownership and permissions. Defaults to `true`." defaultto :true end newparam(:force, :boolean => true, :parent => Puppet::Parameter::Boolean) do desc "Perform the file operation even if it will destroy one or more directories. You must use `force` in order to: * `purge` subdirectories * Replace directories with files or links * Remove a directory when `ensure => absent`" defaultto false end newparam(:ignore) do desc "A parameter which omits action on files matching specified patterns during recursion. Uses Ruby's builtin globbing engine, so shell metacharacters are fully supported, e.g. `[a-z]*`. Matches that would descend into the directory structure are ignored, e.g., `*/*`." validate do |value| unless value.is_a?(Array) or value.is_a?(String) or value == false self.devfail "Ignore must be a string or an Array" end end end newparam(:links) do desc "How to handle links during file actions. During file copying, `follow` will copy the target file instead of the link, `manage` will copy the link itself, and `ignore` will just pass it by. When not copying, `manage` and `ignore` behave equivalently (because you cannot really ignore links entirely during local recursion), and `follow` will manage the file to which the link points." newvalues(:follow, :manage) defaultto :manage end newparam(:purge, :boolean => true, :parent => Puppet::Parameter::Boolean) do desc "Whether unmanaged files should be purged. This option only makes sense when managing directories with `recurse => true`. * When recursively duplicating an entire directory with the `source` attribute, `purge => true` will automatically purge any files that are not in the source directory. * When managing files in a directory as individual resources, setting `purge => true` will purge any files that aren't being specifically managed. If you have a filebucket configured, the purged files will be uploaded, but if you do not, this will destroy data." defaultto :false end newparam(:sourceselect) do desc "Whether to copy all valid sources, or just the first one. This parameter only affects recursive directory copies; by default, the first valid source is the only one used, but if this parameter is set to `all`, then all valid sources will have all of their contents copied to the local system. If a given file exists in more than one source, the version from the earliest source in the list will be used." defaultto :first newvalues(:first, :all) end newparam(:show_diff, :boolean => true, :parent => Puppet::Parameter::Boolean) do desc "Whether to display differences when the file changes, defaulting to true. This parameter is useful for files that may contain passwords or other secret data, which might otherwise be included in Puppet reports or other insecure outputs. If the global ``show_diff` configuration parameter is false, then no diffs will be shown even if this parameter is true." defaultto :true end # Autorequire the nearest ancestor directory found in the catalog. autorequire(:file) do req = [] path = Pathname.new(self[:path]) if !path.root? # Start at our parent, to avoid autorequiring ourself parents = path.parent.enum_for(:ascend) if found = parents.find { |p| catalog.resource(:file, p.to_s) } req << found.to_s end end # if the resource is a link, make sure the target is created first req << self[:target] if self[:target] req end # Autorequire the owner and group of the file. {:user => :owner, :group => :group}.each do |type, property| autorequire(type) do if @parameters.include?(property) # The user/group property automatically converts to IDs next unless should = @parameters[property].shouldorig val = should[0] if val.is_a?(Integer) or val =~ /^\d+$/ nil else val end end end end CREATORS = [:content, :source, :target] SOURCE_ONLY_CHECKSUMS = [:none, :ctime, :mtime] validate do creator_count = 0 CREATORS.each do |param| creator_count += 1 if self.should(param) end creator_count += 1 if @parameters.include?(:source) self.fail "You cannot specify more than one of #{CREATORS.collect { |p| p.to_s}.join(", ")}" if creator_count > 1 self.fail "You cannot specify a remote recursion without a source" if !self[:source] and self[:recurse] == :remote self.fail "You cannot specify source when using checksum 'none'" if self[:checksum] == :none && !self[:source].nil? SOURCE_ONLY_CHECKSUMS.each do |checksum_type| self.fail "You cannot specify content when using checksum '#{checksum_type}'" if self[:checksum] == checksum_type && !self[:content].nil? end self.warning "Possible error: recurselimit is set but not recurse, no recursion will happen" if !self[:recurse] and self[:recurselimit] provider.validate if provider.respond_to?(:validate) end def self.[](path) return nil unless path super(path.gsub(/\/+/, '/').sub(/\/$/, '')) end def self.instances return [] end # Determine the user to write files as. def asuser if self.should(:owner) and ! self.should(:owner).is_a?(Symbol) writeable = Puppet::Util::SUIDManager.asuser(self.should(:owner)) { FileTest.writable?(::File.dirname(self[:path])) } # If the parent directory is writeable, then we execute # as the user in question. Otherwise we'll rely on # the 'owner' property to do things. asuser = self.should(:owner) if writeable end asuser end def bucket return @bucket if @bucket backup = self[:backup] return nil unless backup return nil if backup =~ /^\./ unless catalog or backup == "puppet" fail "Can not find filebucket for backups without a catalog" end unless catalog and filebucket = catalog.resource(:filebucket, backup) or backup == "puppet" fail "Could not find filebucket #{backup} specified in backup" end return default_bucket unless filebucket @bucket = filebucket.bucket @bucket end def default_bucket Puppet::Type.type(:filebucket).mkdefaultbucket.bucket end # Does the file currently exist? Just checks for whether # we have a stat def exist? stat ? true : false end + def present?(current_values) + super && current_values[:ensure] != :false + end + # We have to do some extra finishing, to retrieve our bucket if # there is one. def finish # Look up our bucket, if there is one bucket super end # Create any children via recursion or whatever. def eval_generate return [] unless self.recurse? recurse end def ancestors ancestors = Pathname.new(self[:path]).enum_for(:ascend).map(&:to_s) ancestors.delete(self[:path]) ancestors end def flush # We want to make sure we retrieve metadata anew on each transaction. @parameters.each do |name, param| param.flush if param.respond_to?(:flush) end @stat = :needs_stat end def initialize(hash) # Used for caching clients @clients = {} super # If they've specified a source, we get our 'should' values # from it. unless self[:ensure] if self[:target] self[:ensure] = :link elsif self[:content] self[:ensure] = :file end end @stat = :needs_stat end # Configure discovered resources to be purged. def mark_children_for_purging(children) children.each do |name, child| next if child[:source] child[:ensure] = :absent end end # Create a new file or directory object as a child to the current # object. def newchild(path) full_path = ::File.join(self[:path], path) # Add some new values to our original arguments -- these are the ones # set at initialization. We specifically want to exclude any param # values set by the :source property or any default values. # LAK:NOTE This is kind of silly, because the whole point here is that # the values set at initialization should live as long as the resource # but values set by default or by :source should only live for the transaction # or so. Unfortunately, we don't have a straightforward way to manage # the different lifetimes of this data, so we kludge it like this. # The right-side hash wins in the merge. options = @original_parameters.merge(:path => full_path).reject { |param, value| value.nil? } # These should never be passed to our children. [:parent, :ensure, :recurse, :recurselimit, :target, :alias, :source].each do |param| options.delete(param) if options.include?(param) end self.class.new(options) end # Files handle paths specially, because they just lengthen their # path names, rather than including the full parent's title each # time. def pathbuilder # We specifically need to call the method here, so it looks # up our parent in the catalog graph. if parent = parent() # We only need to behave specially when our parent is also # a file if parent.is_a?(self.class) # Remove the parent file name list = parent.pathbuilder list.pop # remove the parent's path info return list << self.ref else return super end else return [self.ref] end end # Recursively generate a list of file resources, which will # be used to copy remote files, manage local files, and/or make links # to map to another directory. def recurse children = (self[:recurse] == :remote) ? {} : recurse_local if self[:target] recurse_link(children) elsif self[:source] recurse_remote(children) end # If we're purging resources, then delete any resource that isn't on the # remote system. mark_children_for_purging(children) if self.purge? # REVISIT: sort_by is more efficient? result = children.values.sort { |a, b| a[:path] <=> b[:path] } remove_less_specific_files(result) end # This is to fix bug #2296, where two files recurse over the same # set of files. It's a rare case, and when it does happen you're # not likely to have many actual conflicts, which is good, because # this is a pretty inefficient implementation. def remove_less_specific_files(files) # REVISIT: is this Windows safe? AltSeparator? mypath = self[:path].split(::File::Separator) other_paths = catalog.vertices. select { |r| r.is_a?(self.class) and r[:path] != self[:path] }. collect { |r| r[:path].split(::File::Separator) }. select { |p| p[0,mypath.length] == mypath } return files if other_paths.empty? files.reject { |file| path = file[:path].split(::File::Separator) other_paths.any? { |p| path[0,p.length] == p } } end # A simple method for determining whether we should be recursing. def recurse? self[:recurse] == true or self[:recurse] == :remote end # Recurse the target of the link. def recurse_link(children) perform_recursion(self[:target]).each do |meta| if meta.relative_path == "." self[:ensure] = :directory next end children[meta.relative_path] ||= newchild(meta.relative_path) if meta.ftype == "directory" children[meta.relative_path][:ensure] = :directory else children[meta.relative_path][:ensure] = :link children[meta.relative_path][:target] = meta.full_path end end children end # Recurse the file itself, returning a Metadata instance for every found file. def recurse_local result = perform_recursion(self[:path]) return {} unless result result.inject({}) do |hash, meta| next hash if meta.relative_path == "." hash[meta.relative_path] = newchild(meta.relative_path) hash end end # Recurse against our remote file. def recurse_remote(children) sourceselect = self[:sourceselect] total = self[:source].collect do |source| next unless result = perform_recursion(source) return if top = result.find { |r| r.relative_path == "." } and top.ftype != "directory" result.each { |data| data.source = "#{source}/#{data.relative_path}" } break result if result and ! result.empty? and sourceselect == :first result end.flatten.compact # This only happens if we have sourceselect == :all unless sourceselect == :first found = [] total.reject! do |data| result = found.include?(data.relative_path) found << data.relative_path unless found.include?(data.relative_path) result end end total.each do |meta| if meta.relative_path == "." parameter(:source).metadata = meta next end children[meta.relative_path] ||= newchild(meta.relative_path) children[meta.relative_path][:source] = meta.source children[meta.relative_path][:checksum] = :md5 if meta.ftype == "file" children[meta.relative_path].parameter(:source).metadata = meta end children end def perform_recursion(path) Puppet::FileServing::Metadata.indirection.search( path, :links => self[:links], :recurse => (self[:recurse] == :remote ? true : self[:recurse]), :recurselimit => self[:recurselimit], :ignore => self[:ignore], :checksum_type => (self[:source] || self[:content]) ? self[:checksum] : :none, :environment => catalog.environment ) end # Back up and remove the file or directory at `self[:path]`. # # @param [Symbol] should The file type replacing the current content. # @return [Boolean] True if the file was removed, else False # @raises [fail???] If the current file isn't one of %w{file link directory} and can't be removed. def remove_existing(should) wanted_type = should.to_s current_type = read_current_type if current_type.nil? return false end if can_backup?(current_type) backup_existing end if wanted_type != "link" and current_type == wanted_type return false end case current_type when "directory" return remove_directory(wanted_type) when "link", "file" return remove_file(current_type, wanted_type) else self.fail "Could not back up files of type #{current_type}" end end def retrieve if source = parameter(:source) source.copy_source_values end super end # Set the checksum, from another property. There are multiple # properties that modify the contents of a file, and they need the # ability to make sure that the checksum value is in sync. def setchecksum(sum = nil) if @parameters.include? :checksum if sum @parameters[:checksum].checksum = sum else # If they didn't pass in a sum, then tell checksum to # figure it out. currentvalue = @parameters[:checksum].retrieve @parameters[:checksum].checksum = currentvalue end end end # Should this thing be a normal file? This is a relatively complex # way of determining whether we're trying to create a normal file, # and it's here so that the logic isn't visible in the content property. def should_be_file? return true if self[:ensure] == :file # I.e., it's set to something like "directory" return false if e = self[:ensure] and e != :present # The user doesn't really care, apparently if self[:ensure] == :present return true unless s = stat return(s.ftype == "file" ? true : false) end # If we've gotten here, then :ensure isn't set return true if self[:content] return true if stat and stat.ftype == "file" false end # Stat our file. Depending on the value of the 'links' attribute, we # use either 'stat' or 'lstat', and we expect the properties to use the # resulting stat object accordingly (mostly by testing the 'ftype' # value). # # We use the initial value :needs_stat to ensure we only stat the file once, # but can also keep track of a failed stat (@stat == nil). This also allows # us to re-stat on demand by setting @stat = :needs_stat. def stat return @stat unless @stat == :needs_stat method = :stat # Files are the only types that support links if (self.class.name == :file and self[:links] != :follow) or self.class.name == :tidy method = :lstat end @stat = begin Puppet::FileSystem::File.new(self[:path]).send(method) rescue Errno::ENOENT => error nil rescue Errno::ENOTDIR => error nil rescue Errno::EACCES => error warning "Could not stat; permission denied" nil end end def to_resource resource = super resource.delete(:target) if resource[:target] == :notlink resource end # Write out the file. Requires the property name for logging. # Write will be done by the content property, along with checksum computation def write(property) remove_existing(:file) mode = self.should(:mode) # might be nil mode_int = mode ? symbolic_mode_to_int(mode, Puppet::Util::DEFAULT_POSIX_MODE) : nil if write_temporary_file? Puppet::Util.replace_file(self[:path], mode_int) do |file| file.binmode content_checksum = write_content(file) file.flush fail_if_checksum_is_wrong(file.path, content_checksum) if validate_checksum? end else umask = mode ? 000 : 022 Puppet::Util.withumask(umask) { ::File.open(self[:path], 'wb', mode_int ) { |f| write_content(f) } } end # make sure all of the modes are actually correct property_fix end private # @return [String] The type of the current file, cast to a string. def read_current_type stat_info = stat if stat_info stat_info.ftype.to_s else nil end end # @return [Boolean] If the current file can be backed up and needs to be backed up. def can_backup?(type) if type == "directory" and not force? # (#18110) Directories cannot be removed without :force, so it doesn't # make sense to back them up. false else true end end # @return [Boolean] True if the directory was removed # @api private def remove_directory(wanted_type) if force? debug "Removing existing directory for replacement with #{wanted_type}" FileUtils.rmtree(self[:path]) stat_needed true else notice "Not removing directory; use 'force' to override" false end end # @return [Boolean] if the file was removed (which is always true currently) # @api private def remove_file(current_type, wanted_type) debug "Removing existing #{current_type} for replacement with #{wanted_type}" Puppet::FileSystem::File.unlink(self[:path]) stat_needed true end def stat_needed @stat = :needs_stat end # Back up the existing file at a given prior to it being removed # @api private # @raise [Puppet::Error] if the file backup failed # @return [void] def backup_existing unless perform_backup raise Puppet::Error, "Could not back up; will not replace" end end # Should we validate the checksum of the file we're writing? def validate_checksum? self[:checksum] !~ /time/ end # Make sure the file we wrote out is what we think it is. def fail_if_checksum_is_wrong(path, content_checksum) newsum = parameter(:checksum).sum_file(path) return if [:absent, nil, content_checksum].include?(newsum) self.fail "File written to disk did not match checksum; discarding changes (#{content_checksum} vs #{newsum})" end # write the current content. Note that if there is no content property # simply opening the file with 'w' as done in write is enough to truncate # or write an empty length file. def write_content(file) (content = property(:content)) && content.write(file) end def write_temporary_file? # unfortunately we don't know the source file size before fetching it # so let's assume the file won't be empty (c = property(:content) and c.length) || @parameters[:source] end # There are some cases where all of the work does not get done on # file creation/modification, so we have to do some extra checking. def property_fix properties.each do |thing| next unless [:mode, :owner, :group, :seluser, :selrole, :seltype, :selrange].include?(thing.name) # Make sure we get a new stat objct @stat = :needs_stat currentvalue = thing.retrieve thing.sync unless thing.safe_insync?(currentvalue) end end end # We put all of the properties in separate files, because there are so many # of them. The order these are loaded is important, because it determines # the order they are in the property lit. require 'puppet/type/file/checksum' require 'puppet/type/file/content' # can create the file require 'puppet/type/file/source' # can create the file require 'puppet/type/file/target' # creates a different type of file require 'puppet/type/file/ensure' # can create the file require 'puppet/type/file/owner' require 'puppet/type/file/group' require 'puppet/type/file/mode' require 'puppet/type/file/type' require 'puppet/type/file/selcontext' # SELinux file context require 'puppet/type/file/ctime' require 'puppet/type/file/mtime' diff --git a/lib/puppet/type/interface.rb b/lib/puppet/type/interface.rb index 40fb8176b..7362695e9 100644 --- a/lib/puppet/type/interface.rb +++ b/lib/puppet/type/interface.rb @@ -1,112 +1,116 @@ # # Manages an interface on a given router or switch # require 'puppet/util/network_device/ipcalc' Puppet::Type.newtype(:interface) do @doc = "This represents a router or switch interface. It is possible to manage interface mode (access or trunking, native vlan and encapsulation) and switchport characteristics (speed, duplex)." apply_to_device ensurable do defaultvalues aliasvalue :shutdown, :absent aliasvalue :no_shutdown, :present defaultto { :no_shutdown } end newparam(:name) do desc "The interface's name." end newparam(:device_url) do desc "The URL at which the router or switch can be reached." end newproperty(:description) do desc "Interface description." defaultto { @resource[:name] } end newproperty(:speed) do desc "Interface speed." newvalues(:auto, /^\d+/) end newproperty(:duplex) do desc "Interface duplex." newvalues(:auto, :full, :half) end newproperty(:native_vlan) do desc "Interface native vlan (for access mode only)." newvalues(/^\d+/) end newproperty(:encapsulation) do desc "Interface switchport encapsulation." newvalues(:none, :dot1q, :isl ) end newproperty(:mode) do desc "Interface switchport mode." newvalues(:access, :trunk) end newproperty(:allowed_trunk_vlans) do desc "Allowed list of Vlans that this trunk can forward." newvalues(:all, /./) end newproperty(:etherchannel) do desc "Channel group this interface is part of." newvalues(/^\d+/) end newproperty(:ipaddress, :array_matching => :all) do include Puppet::Util::NetworkDevice::IPCalc desc "IP Address of this interface. Note that it might not be possible to set an interface IP address; it depends on the interface type and device type. Valid format of ip addresses are: * IPV4, like 127.0.0.1 * IPV4/prefixlength like 127.0.1.1/24 * IPV6/prefixlength like FE80::21A:2FFF:FE30:ECF0/128 * an optional suffix for IPV6 addresses from this list: `eui-64`, `link-local` It is also possible to supply an array of values. " validate do |values| values = [values] unless values.is_a?(Array) values.each do |value| self.fail "Invalid interface ip address" unless parse(value.gsub(/\s*(eui-64|link-local)\s*$/,'')) end end munge do |value| option = value =~ /eui-64|link-local/i ? value.gsub(/^.*?\s*(eui-64|link-local)\s*$/,'\1') : nil [parse(value.gsub(/\s*(eui-64|link-local)\s*$/,'')), option].flatten end def value_to_s(value) value = [value] unless value.is_a?(Array) value.map{ |v| "#{v[1].to_s}/#{v[0]} #{v[2]}"}.join(",") end def change_to_s(currentvalue, newvalue) currentvalue = value_to_s(currentvalue) if currentvalue != :absent newvalue = value_to_s(newvalue) super(currentvalue, newvalue) end end + + def present?(current_values) + super && current_values[:ensure] != :shutdown + end end diff --git a/lib/puppet/type/package.rb b/lib/puppet/type/package.rb index e7ea1125b..50356cedf 100644 --- a/lib/puppet/type/package.rb +++ b/lib/puppet/type/package.rb @@ -1,380 +1,384 @@ # Define the different packaging systems. Each package system is implemented # in a module, which then gets used to individually extend each package object. # This allows packages to exist on the same machine using different packaging # systems. require 'puppet/parameter/package_options' module Puppet newtype(:package) do @doc = "Manage packages. There is a basic dichotomy in package support right now: Some package types (e.g., yum and apt) can retrieve their own package files, while others (e.g., rpm and sun) cannot. For those package formats that cannot retrieve their own files, you can use the `source` parameter to point to the correct file. Puppet will automatically guess the packaging format that you are using based on the platform you are on, but you can override it using the `provider` parameter; each provider defines what it requires in order to function, and you must meet those requirements to use a given provider. **Autorequires:** If Puppet is managing the files specified as a package's `adminfile`, `responsefile`, or `source`, the package resource will autorequire those files." feature :installable, "The provider can install packages.", :methods => [:install] feature :uninstallable, "The provider can uninstall packages.", :methods => [:uninstall] feature :upgradeable, "The provider can upgrade to the latest version of a package. This feature is used by specifying `latest` as the desired value for the package.", :methods => [:update, :latest] feature :purgeable, "The provider can purge packages. This generally means that all traces of the package are removed, including existing configuration files. This feature is thus destructive and should be used with the utmost care.", :methods => [:purge] feature :versionable, "The provider is capable of interrogating the package database for installed version(s), and can select which out of a set of available versions of a package to install if asked." feature :holdable, "The provider is capable of placing packages on hold such that they are not automatically upgraded as a result of other package dependencies unless explicit action is taken by a user or another package. Held is considered a superset of installed.", :methods => [:hold] feature :install_options, "The provider accepts options to be passed to the installer command." feature :uninstall_options, "The provider accepts options to be passed to the uninstaller command." ensurable do desc <<-EOT What state the package should be in. On packaging systems that can retrieve new packages on their own, you can choose which package to retrieve by specifying a version number or `latest` as the ensure value. On packaging systems that manage configuration files separately from "normal" system files, you can uninstall config files by specifying `purged` as the ensure value. This defaults to `installed`. EOT attr_accessor :latest newvalue(:present, :event => :package_installed) do provider.install end newvalue(:absent, :event => :package_removed) do provider.uninstall end newvalue(:purged, :event => :package_purged, :required_features => :purgeable) do provider.purge end newvalue(:held, :event => :package_held, :required_features => :holdable) do provider.hold end # Alias the 'present' value. aliasvalue(:installed, :present) newvalue(:latest, :required_features => :upgradeable) do # Because yum always exits with a 0 exit code, there's a retrieve # in the "install" method. So, check the current state now, # to compare against later. current = self.retrieve begin provider.update rescue => detail self.fail "Could not update: #{detail}" end if current == :absent :package_installed else :package_changed end end newvalue(/./, :required_features => :versionable) do begin provider.install rescue => detail self.fail "Could not update: #{detail}" end if self.retrieve == :absent :package_installed else :package_changed end end defaultto :installed # Override the parent method, because we've got all kinds of # funky definitions of 'in sync'. def insync?(is) @lateststamp ||= (Time.now.to_i - 1000) # Iterate across all of the should values, and see how they # turn out. @should.each { |should| case should when :present return true unless [:absent, :purged, :held].include?(is) when :latest # Short-circuit packages that are not present return false if is == :absent or is == :purged # Don't run 'latest' more than about every 5 minutes if @latest and ((Time.now.to_i - @lateststamp) / 60) < 5 #self.debug "Skipping latest check" else begin @latest = provider.latest @lateststamp = Time.now.to_i rescue => detail error = Puppet::Error.new("Could not get latest version: #{detail}") error.set_backtrace(detail.backtrace) raise error end end case when is.is_a?(Array) && is.include?(@latest) return true when is == @latest return true when is == :present # This will only happen on retarded packaging systems # that can't query versions. return true else self.debug "#{@resource.name} #{is.inspect} is installed, latest is #{@latest.inspect}" end when :absent return true if is == :absent or is == :purged when :purged return true if is == :purged # this handles version number matches and # supports providers that can have multiple versions installed when *Array(is) return true end } false end # This retrieves the current state. LAK: I think this method is unused. def retrieve provider.properties[:ensure] end # Provide a bit more information when logging upgrades. def should_to_s(newvalue = @should) if @latest @latest.to_s else super(newvalue) end end end newparam(:name) do desc "The package name. This is the name that the packaging system uses internally, which is sometimes (especially on Solaris) a name that is basically useless to humans. If you want to abstract package installation, then you can use aliases to provide a common name to packages: # In the 'openssl' class $ssl = $operatingsystem ? { solaris => SMCossl, default => openssl } # It is not an error to set an alias to the same value as the # object name. package { $ssl: ensure => installed, alias => openssl } . etc. . $ssh = $operatingsystem ? { solaris => SMCossh, default => openssh } # Use the alias to specify a dependency, rather than # having another selector to figure it out again. package { $ssh: ensure => installed, alias => openssh, require => Package[openssl] } " isnamevar validate do |value| if !value.is_a?(String) raise ArgumentError, "Name must be a String not #{value.class}" end end end newparam(:source) do desc "Where to find the actual package. This must be a local file (or on a network file system) or a URL that your specific packaging type understands; Puppet will not retrieve files for you, although you can manage packages as `file` resources." validate do |value| provider.validate_source(value) end end newparam(:instance) do desc "A read-only parameter set by the package." end newparam(:status) do desc "A read-only parameter set by the package." end newparam(:adminfile) do desc "A file containing package defaults for installing packages. This is currently only used on Solaris. The value will be validated according to system rules, which in the case of Solaris means that it should either be a fully qualified path or it should be in `/var/sadm/install/admin`." end newparam(:responsefile) do desc "A file containing any necessary answers to questions asked by the package. This is currently used on Solaris and Debian. The value will be validated according to system rules, but it should generally be a fully qualified path." end newparam(:configfiles) do desc "Whether configfiles should be kept or replaced. Most packages types do not support this parameter. Defaults to `keep`." defaultto :keep newvalues(:keep, :replace) end newparam(:category) do desc "A read-only parameter set by the package." end newparam(:platform) do desc "A read-only parameter set by the package." end newparam(:root) do desc "A read-only parameter set by the package." end newparam(:vendor) do desc "A read-only parameter set by the package." end newparam(:description) do desc "A read-only parameter set by the package." end newparam(:allowcdrom) do desc "Tells apt to allow cdrom sources in the sources.list file. Normally apt will bail if you try this." newvalues(:true, :false) end newparam(:flavor) do desc "OpenBSD supports 'flavors', which are further specifications for which type of package you want." end newparam(:install_options, :parent => Puppet::Parameter::PackageOptions, :required_features => :install_options) do desc <<-EOT An array of additional options to pass when installing a package. These options are package-specific, and should be documented by the software vendor. One commonly implemented option is `INSTALLDIR`: package { 'mysql': ensure => installed, source => 'N:/packages/mysql-5.5.16-winx64.msi', install_options => [ '/S', { 'INSTALLDIR' => 'C:\\mysql-5.5' } ], } Each option in the array can either be a string or a hash, where each key and value pair are interpreted in a provider specific way. Each option will automatically be quoted when passed to the install command. On Windows, this is the **only** place in Puppet where backslash separators should be used. Note that backslashes in double-quoted strings _must_ be double-escaped and backslashes in single-quoted strings _may_ be double-escaped. EOT end newparam(:uninstall_options, :parent => Puppet::Parameter::PackageOptions, :required_features => :uninstall_options) do desc <<-EOT An array of additional options to pass when uninstalling a package. These options are package-specific, and should be documented by the software vendor. For example: package { 'VMware Tools': ensure => absent, uninstall_options => [ { 'REMOVE' => 'Sync,VSS' } ], } Each option in the array can either be a string or a hash, where each key and value pair are interpreted in a provider specific way. Each option will automatically be quoted when passed to the uninstall command. On Windows, this is the **only** place in Puppet where backslash separators should be used. Note that backslashes in double-quoted strings _must_ be double-escaped and backslashes in single-quoted strings _may_ be double-escaped. EOT end autorequire(:file) do autos = [] [:responsefile, :adminfile].each { |param| if val = self[param] autos << val end } if source = self[:source] and absolute_path?(source) autos << source end autos end # This only exists for testing. def clear if obj = @parameters[:ensure] obj.latest = nil end end # The 'query' method returns a hash of info if the package # exists and returns nil if it does not. def exists? @provider.get(:ensure) != :absent end + + def present?(current_values) + super && current_values[:ensure] != :purged + end end end diff --git a/spec/unit/transaction/resource_harness_spec.rb b/spec/unit/transaction/resource_harness_spec.rb index afe1ec912..a487c8199 100755 --- a/spec/unit/transaction/resource_harness_spec.rb +++ b/spec/unit/transaction/resource_harness_spec.rb @@ -1,370 +1,478 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/transaction/resource_harness' describe Puppet::Transaction::ResourceHarness do include PuppetSpec::Files before do @mode_750 = Puppet.features.microsoft_windows? ? '644' : '750' @mode_755 = Puppet.features.microsoft_windows? ? '644' : '755' path = make_absolute("/my/file") @transaction = Puppet::Transaction.new(Puppet::Resource::Catalog.new, nil, nil) @resource = Puppet::Type.type(:file).new :path => path @harness = Puppet::Transaction::ResourceHarness.new(@transaction) @current_state = Puppet::Resource.new(:file, path) @resource.stubs(:retrieve).returns @current_state end it "should accept a transaction at initialization" do harness = Puppet::Transaction::ResourceHarness.new(@transaction) harness.transaction.should equal(@transaction) end it "should delegate to the transaction for its relationship graph" do @transaction.expects(:relationship_graph).returns "relgraph" Puppet::Transaction::ResourceHarness.new(@transaction).relationship_graph.should == "relgraph" end describe "when evaluating a resource" do it "produces a resource state that describes what happened with the resource" do status = @harness.evaluate(@resource) status.resource.should == @resource.ref status.should_not be_failed status.events.should be_empty end it "retrieves the current state of the resource" do @resource.expects(:retrieve).returns @current_state @harness.evaluate(@resource) end it "produces a failure status for the resource when an error occurs" do the_message = "retrieve failed in testing" @resource.expects(:retrieve).raises(ArgumentError.new(the_message)) status = @harness.evaluate(@resource) status.should be_failed events_to_hash(status.events).collect do |event| { :@status => event[:@status], :@message => event[:@message] } end.should == [{ :@status => "failure", :@message => the_message }] end it "records the time it took to evaluate the resource" do before = Time.now status = @harness.evaluate(@resource) after = Time.now status.evaluation_time.should be <= after - before end end def events_to_hash(events) events.map do |event| hash = {} event.instance_variables.each do |varname| hash[varname.to_sym] = event.instance_variable_get(varname) end hash end end def make_stub_provider stubProvider = Class.new(Puppet::Type) stubProvider.instance_eval do initvars newparam(:name) do desc "The name var" isnamevar end newproperty(:foo) do desc "A property that can be changed successfully" def sync end def retrieve :absent end def insync?(reference_value) false end end newproperty(:bar) do desc "A property that raises an exception when you try to change it" def sync raise ZeroDivisionError.new('bar') end def retrieve :absent end def insync?(reference_value) false end end newproperty(:baz) do desc "A property that raises an Exception (not StandardError) when you try to change it" def sync raise Exception.new('baz') end def retrieve :absent end def insync?(reference_value) false end end newproperty(:brillig) do desc "A property that raises a StandardError exception when you test if it's insync?" def sync end def retrieve :absent end def insync?(reference_value) raise ZeroDivisionError.new('brillig') end end newproperty(:slithy) do desc "A property that raises an Exception when you test if it's insync?" def sync end def retrieve :absent end def insync?(reference_value) raise Exception.new('slithy') end end end stubProvider end + + context "interaction of ensure with other properties" do + def an_ensurable_resource_reacting_as(behaviors) + stub_type = Class.new(Puppet::Type) + stub_type.class_eval do + initvars + ensurable do + def sync + (@resource.behaviors[:on_ensure] || proc {}).call + end + + def insync?(value) + @resource.behaviors[:ensure_insync?] + end + end + + newparam(:name) do + desc "The name var" + isnamevar + end + + newproperty(:prop) do + newvalue("new") do + #noop + end + + def retrieve + "old" + end + end + + attr_reader :behaviors + + def initialize(options) + @behaviors = options.delete(:behaviors) + super + end + + def exists? + @behaviors[:present?] + end + + def present?(resource) + @behaviors[:present?] + end + + def self.name + "Testing" + end + end + stub_type.new(:behaviors => behaviors, + :ensure => :present, + :name => "testing", + :prop => "new") + end + + it "ensure errors means that the rest doesn't happen" do + resource = an_ensurable_resource_reacting_as(:ensure_insync? => false, :on_ensure => proc { raise StandardError }, :present? => true) + + status = @harness.evaluate(resource) + + expect(status.events.length).to eq(1) + expect(status.events[0].property).to eq('ensure') + expect(status.events[0].name.to_s).to eq('Testing_created') + expect(status.events[0].status).to eq('failure') + end + + it "ensure fails completely means that the rest doesn't happen" do + resource = an_ensurable_resource_reacting_as(:ensure_insync? => false, :on_ensure => proc { raise Exception }, :present? => false) + + expect do + @harness.evaluate(resource) + end.to raise_error(Exception) + + @logs.first.message.should == "change from absent to present failed: Exception" + @logs.first.level.should == :err + end + + it "ensure succeeds means that the rest doesn't happen" do + resource = an_ensurable_resource_reacting_as(:ensure_insync? => false, :on_ensure => proc { }, :present? => true) + + status = @harness.evaluate(resource) + + expect(status.events.length).to eq(1) + expect(status.events[0].property).to eq('ensure') + expect(status.events[0].name.to_s).to eq('Testing_created') + expect(status.events[0].status).to eq('success') + end + + it "ensure is in sync means that the rest *does* happen" do + resource = an_ensurable_resource_reacting_as(:ensure_insync? => true, :present? => true) + + status = @harness.evaluate(resource) + + expect(status.events.length).to eq(1) + expect(status.events[0].property).to eq('prop') + expect(status.events[0].name.to_s).to eq('prop_changed') + expect(status.events[0].status).to eq('success') + end + + it "ensure is in sync but resource not present, means that the rest doesn't happen" do + resource = an_ensurable_resource_reacting_as(:ensure_insync? => true, :present? => false) + + status = @harness.evaluate(resource) + + expect(status.events).to be_empty + end + end + describe "when a caught error occurs" do before :each do stub_provider = make_stub_provider resource = stub_provider.new :name => 'name', :foo => 1, :bar => 2 resource.expects(:err).never @status = @harness.evaluate(resource) end it "should record previous successful events" do @status.events[0].property.should == 'foo' @status.events[0].status.should == 'success' end it "should record a failure event" do @status.events[1].property.should == 'bar' @status.events[1].status.should == 'failure' end end describe "when an Exception occurs during sync" do before :each do stub_provider = make_stub_provider @resource = stub_provider.new :name => 'name', :baz => 1 @resource.expects(:err).never end it "should log and pass the exception through" do lambda { @harness.evaluate(@resource) }.should raise_error(Exception, /baz/) @logs.first.message.should == "change from absent to 1 failed: baz" @logs.first.level.should == :err end end describe "when a StandardError exception occurs during insync?" do before :each do stub_provider = make_stub_provider @resource = stub_provider.new :name => 'name', :brillig => 1 @resource.expects(:err).never end it "should record a failure event" do @status = @harness.evaluate(@resource) @status.events[0].name.to_s.should == 'brillig_changed' @status.events[0].property.should == 'brillig' @status.events[0].status.should == 'failure' end end describe "when an Exception occurs during insync?" do before :each do stub_provider = make_stub_provider @resource = stub_provider.new :name => 'name', :slithy => 1 @resource.expects(:err).never end it "should log and pass the exception through" do lambda { @harness.evaluate(@resource) }.should raise_error(Exception, /slithy/) @logs.first.message.should == "change from absent to 1 failed: slithy" @logs.first.level.should == :err end end describe "when auditing" do it "should not call insync? on parameters that are merely audited" do stub_provider = make_stub_provider resource = stub_provider.new :name => 'name', :audit => ['foo'] resource.property(:foo).expects(:insync?).never status = @harness.evaluate(resource) - status.events.each do |event| - event.status.should != 'failure' - end + + expect(status.events).to be_empty end it "should be able to audit a file's group" do # see bug #5710 test_file = tmpfile('foo') File.open(test_file, 'w').close resource = Puppet::Type.type(:file).new :path => test_file, :audit => ['group'], :backup => false resource.expects(:err).never # make sure no exceptions get swallowed status = @harness.evaluate(resource) status.events.each do |event| event.status.should != 'failure' end end end describe "when applying changes" do it "should not apply changes if allow_changes?() returns false" do test_file = tmpfile('foo') resource = Puppet::Type.type(:file).new :path => test_file, :backup => false, :ensure => :file resource.expects(:err).never # make sure no exceptions get swallowed @harness.expects(:allow_changes?).with(resource).returns false status = @harness.evaluate(resource) Puppet::FileSystem::File.exist?(test_file).should == false end end describe "when determining whether the resource can be changed" do before do @resource.stubs(:purging?).returns true @resource.stubs(:deleting?).returns true end it "should be true if the resource is not being purged" do @resource.expects(:purging?).returns false @harness.should be_allow_changes(@resource) end it "should be true if the resource is not being deleted" do @resource.expects(:deleting?).returns false @harness.should be_allow_changes(@resource) end it "should be true if the resource has no dependents" do @harness.relationship_graph.expects(:dependents).with(@resource).returns [] @harness.should be_allow_changes(@resource) end it "should be true if all dependents are being deleted" do dep = stub 'dependent', :deleting? => true @harness.relationship_graph.expects(:dependents).with(@resource).returns [dep] @resource.expects(:purging?).returns true @harness.should be_allow_changes(@resource) end it "should be false if the resource's dependents are not being deleted" do dep = stub 'dependent', :deleting? => false, :ref => "myres" @resource.expects(:warning) @harness.relationship_graph.expects(:dependents).with(@resource).returns [dep] @harness.should_not be_allow_changes(@resource) end end describe "when finding the schedule" do before do @catalog = Puppet::Resource::Catalog.new @resource.catalog = @catalog end it "should warn and return nil if the resource has no catalog" do @resource.catalog = nil @resource.expects(:warning) @harness.schedule(@resource).should be_nil end it "should return nil if the resource specifies no schedule" do @harness.schedule(@resource).should be_nil end it "should fail if the named schedule cannot be found" do @resource[:schedule] = "whatever" @resource.expects(:fail) @harness.schedule(@resource) end it "should return the named schedule if it exists" do sched = Puppet::Type.type(:schedule).new(:name => "sched") @catalog.add_resource(sched) @resource[:schedule] = "sched" @harness.schedule(@resource).to_s.should == sched.to_s end end describe "when determining if a resource is scheduled" do before do @catalog = Puppet::Resource::Catalog.new @resource.catalog = @catalog end it "should return true if 'ignoreschedules' is set" do Puppet[:ignoreschedules] = true @resource[:schedule] = "meh" @harness.should be_scheduled(@resource) end it "should return true if the resource has no schedule set" do @harness.should be_scheduled(@resource) end it "should return the result of matching the schedule with the cached 'checked' time if a schedule is set" do t = Time.now @harness.expects(:cached).with(@resource, :checked).returns(t) sched = Puppet::Type.type(:schedule).new(:name => "sched") @catalog.add_resource(sched) @resource[:schedule] = "sched" sched.expects(:match?).with(t.to_i).returns "feh" @harness.scheduled?(@resource).should == "feh" end end it "should be able to cache data in the Storage module" do data = {} Puppet::Util::Storage.expects(:cache).with(@resource).returns data @harness.cache(@resource, :foo, "something") data[:foo].should == "something" end it "should be able to retrieve data from the cache" do data = {:foo => "other"} Puppet::Util::Storage.expects(:cache).with(@resource).returns data @harness.cached(@resource, :foo).should == "other" end end