diff --git a/lib/puppet/transaction.rb b/lib/puppet/transaction.rb index 6a0346169..823ad8c63 100644 --- a/lib/puppet/transaction.rb +++ b/lib/puppet/transaction.rb @@ -1,384 +1,398 @@ # the class that actually walks our resource/property tree, collects the changes, # and performs them require 'puppet' require 'puppet/util/tagging' require 'puppet/application' require 'digest/sha1' class Puppet::Transaction require 'puppet/transaction/event' require 'puppet/transaction/event_manager' require 'puppet/transaction/resource_harness' require 'puppet/resource/status' attr_accessor :component, :catalog, :ignoreschedules, :for_network_device attr_accessor :configurator # The report, once generated. attr_reader :report # Routes and stores any events and subscriptions. attr_reader :event_manager # Handles most of the actual interacting with resources attr_reader :resource_harness include Puppet::Util include Puppet::Util::Tagging # Wraps application run state check to flag need to interrupt processing def stop_processing? Puppet::Application.stop_requested? end # Add some additional times for reporting def add_times(hash) hash.each do |name, num| report.add_times(name, num) end end # Are there any failed resources in this transaction? def any_failed? report.resource_statuses.values.detect { |status| status.failed? } end # Apply all changes for a resource def apply(resource, ancestor = nil) status = resource_harness.evaluate(resource) add_resource_status(status) event_manager.queue_events(ancestor || resource, status.events) unless status.failed? rescue => detail resource.err "Could not evaluate: #{detail}" end # Find all of the changed resources. def changed? report.resource_statuses.values.find_all { |status| status.changed }.collect { |status| catalog.resource(status.resource) } end # Find all of the applied resources (including failed attempts). def applied_resources report.resource_statuses.values.collect { |status| catalog.resource(status.resource) } end # Copy an important relationships from the parent to the newly-generated # child resource. def add_conditional_directed_dependency(parent, child, label=nil) relationship_graph.add_vertex(child) edge = parent.depthfirst? ? [child, parent] : [parent, child] if relationship_graph.edge?(*edge.reverse) parent.debug "Skipping automatic relationship to #{child}" else relationship_graph.add_edge(edge[0],edge[1],label) end end # Evaluate a single resource. def eval_resource(resource, ancestor = nil) if skip?(resource) resource_status(resource).skipped = true else resource_status(resource).scheduled = true apply(resource, ancestor) end # Check to see if there are any events queued for this resource event_manager.process_events(resource) end # This method does all the actual work of running a transaction. It # collects all of the changes, executes them, and responds to any # necessary events. def evaluate prepare Puppet.info "Applying configuration version '#{catalog.version}'" if catalog.version relationship_graph.traverse do |resource| if resource.is_a?(Puppet::Type::Component) Puppet.warning "Somehow left a component in the relationship graph" else seconds = thinmark { eval_resource(resource) } resource.info "Evaluated in %0.2f seconds" % seconds if Puppet[:evaltrace] and @catalog.host_config? end end Puppet.debug "Finishing transaction #{object_id}" end def events event_manager.events end def failed?(resource) s = resource_status(resource) and s.failed? end # Does this resource have any failed dependencies? def failed_dependencies?(resource) # First make sure there are no failed dependencies. To do this, # we check for failures in any of the vertexes above us. It's not # enough to check the immediate dependencies, which is why we use # a tree from the reversed graph. found_failed = false # When we introduced the :whit into the graph, to reduce the combinatorial # explosion of edges, we also ended up reporting failures for containers # like class and stage. This is undesirable; while just skipping the # output isn't perfect, it is RC-safe. --daniel 2011-06-07 suppress_report = (resource.class == Puppet::Type.type(:whit)) relationship_graph.dependencies(resource).each do |dep| next unless failed?(dep) found_failed = true # See above. --daniel 2011-06-06 unless suppress_report then resource.notice "Dependency #{dep} has failures: #{resource_status(dep).failed}" end end found_failed end def eval_generate(resource) raise Puppet::DevError,"Depthfirst resources are not supported by eval_generate" if resource.depthfirst? begin - made = resource.eval_generate.uniq.reverse + made = resource.eval_generate.uniq + made = Hash[made.map(&:name).zip(made)] rescue => detail puts detail.backtrace if Puppet[:trace] resource.err "Failed to generate additional resources using 'eval_generate: #{detail}" return end - made.each do |res| + made.values.each do |res| begin res.tag(*resource.tags) @catalog.add_resource(res) res.finish rescue Puppet::Resource::Catalog::DuplicateResourceError res.info "Duplicate generated resource; skipping" end end - sentinel = Puppet::Type::Whit.new(:name => "completed_#{resource.title}", :catalog => resource.catalog) + sentinel = Puppet::Type.type(:whit).new(:name => "completed_#{resource.title}", :catalog => resource.catalog) + + # The completed whit is now the thing that represents the resource is done relationship_graph.adjacent(resource,:direction => :out,:type => :edges).each { |e| add_conditional_directed_dependency(sentinel, e.target, e.label) relationship_graph.remove_edge! e } + default_label = Puppet::Resource::Catalog::Default_label - made.each do |res| - add_conditional_directed_dependency(made.find { |r| r != res && r.name == res.name[0,r.name.length]} || resource, res) + made.values.each do |res| + # Depend on the nearest ancestor we generated, falling back to the + # resource if we have none + parent_name = res.ancestors.find { |a| made[a] and made[a] != res } + parent = made[parent_name] || resource + + add_conditional_directed_dependency(parent, res) + + # This resource isn't 'completed' until each child has run add_conditional_directed_dependency(res, sentinel, default_label) end + + # This edge allows the resource's events to propagate, though it isn't + # strictly necessary for ordering purposes add_conditional_directed_dependency(resource, sentinel, default_label) end # A general method for recursively generating new resources from a # resource. def generate_additional_resources(resource) return unless resource.respond_to?(:generate) begin made = resource.generate rescue => detail puts detail.backtrace if Puppet[:trace] resource.err "Failed to generate additional resources using 'generate': #{detail}" end return unless made made = [made] unless made.is_a?(Array) made.uniq.each do |res| begin res.tag(*resource.tags) @catalog.add_resource(res) res.finish add_conditional_directed_dependency(resource, res) generate_additional_resources(res) rescue Puppet::Resource::Catalog::DuplicateResourceError res.info "Duplicate generated resource; skipping" end end end # Collect any dynamically generated resources. This method is called # before the transaction starts. def xgenerate @catalog.vertices.each { |resource| generate_additional_resources(resource) } end # Should we ignore tags? def ignore_tags? ! (@catalog.host_config? or Puppet[:name] == "puppet") end # this should only be called by a Puppet::Type::Component resource now # and it should only receive an array def initialize(catalog, report = nil) @catalog = catalog @report = report || Puppet::Transaction::Report.new("apply", catalog.version) @event_manager = Puppet::Transaction::EventManager.new(self) @resource_harness = Puppet::Transaction::ResourceHarness.new(self) end # Prefetch any providers that support it. We don't support prefetching # types, just providers. def prefetch prefetchers = {} @catalog.vertices.each do |resource| if provider = resource.provider and provider.class.respond_to?(:prefetch) prefetchers[provider.class] ||= {} prefetchers[provider.class][resource.name] = resource end end # Now call prefetch, passing in the resources so that the provider instances can be replaced. prefetchers.each do |provider, resources| Puppet.debug "Prefetching #{provider.name} resources for #{provider.resource_type.name}" begin provider.prefetch(resources) rescue => detail puts detail.backtrace if Puppet[:trace] Puppet.err "Could not prefetch #{provider.resource_type.name} provider '#{provider.name}': #{detail}" end end end # Prepare to evaluate the resources in a transaction. def prepare # Now add any dynamically generated resources xgenerate # Then prefetch. It's important that we generate and then prefetch, # so that any generated resources also get prefetched. prefetch end # We want to monitor changes in the relationship graph of our # catalog but this is complicated by the fact that the catalog # both is_a graph and has_a graph, by the fact that changes to # the structure of the object can have adverse serialization # effects, by threading issues, by order-of-initialization issues, # etc. # # Since the proper lifetime/scope of the monitoring is a transaction # and the transaction is already commiting a mild law-of-demeter # transgression, we cut the Gordian knot here by simply wrapping the # transaction's view of the resource graph to capture and maintain # the information we need. Nothing outside the transaction needs # this information, and nothing outside the transaction can see it # except via the Transaction#relationship_graph class Relationship_graph_wrapper attr_reader :real_graph,:transaction,:ready,:generated,:done,:unguessable_deterministic_key def initialize(real_graph,transaction) @real_graph = real_graph @transaction = transaction @ready = {} @generated = {} @done = {} @unguessable_deterministic_key = Hash.new { |h,k| h[k] = Digest::SHA1.hexdigest("NaCl, MgSO4 (salts) and then #{k.title}") } vertices.each { |v| check_if_now_ready(v) } end def method_missing(*args,&block) real_graph.send(*args,&block) end def add_vertex(v) real_graph.add_vertex(v) check_if_now_ready(v) # ????????????????????????????????????????? end def add_edge(f,t,label=nil) ready.delete(t) real_graph.add_edge(f,t,label) end def check_if_now_ready(r) ready[r] = true if direct_dependencies_of(r).all? { |r2| done[r2] } end def next_resource ready.keys.sort_by { |r0| unguessable_deterministic_key[r0] }.first end def traverse(&block) real_graph.report_cycles_in_graph while (r = next_resource) && !transaction.stop_processing? if !generated[r] && r.respond_to?(:eval_generate) transaction.eval_generate(r) generated[r] = true else ready.delete(r) yield r done[r] = true direct_dependents_of(r).each { |v| check_if_now_ready(v) } end end end end def relationship_graph @relationship_graph ||= Relationship_graph_wrapper.new(catalog.relationship_graph,self) end def add_resource_status(status) report.add_resource_status status end def resource_status(resource) report.resource_statuses[resource.to_s] || add_resource_status(Puppet::Resource::Status.new(resource)) end # Is the resource currently scheduled? def scheduled?(resource) self.ignoreschedules or resource_harness.scheduled?(resource_status(resource), resource) end # Should this resource be skipped? def skip?(resource) if missing_tags?(resource) resource.debug "Not tagged with #{tags.join(", ")}" elsif ! scheduled?(resource) resource.debug "Not scheduled" elsif failed_dependencies?(resource) # When we introduced the :whit into the graph, to reduce the combinatorial # explosion of edges, we also ended up reporting failures for containers # like class and stage. This is undesirable; while just skipping the # output isn't perfect, it is RC-safe. --daniel 2011-06-07 unless resource.class == Puppet::Type.type(:whit) then resource.warning "Skipping because of failed dependencies" end elsif resource.virtual? resource.debug "Skipping because virtual" elsif resource.appliable_to_device? ^ for_network_device resource.debug "Skipping #{resource.appliable_to_device? ? 'device' : 'host'} resources because running on a #{for_network_device ? 'device' : 'host'}" else return false end true end # The tags we should be checking. def tags self.tags = Puppet[:tags] unless defined?(@tags) super end def handle_qualified_tags( qualified ) # The default behavior of Puppet::Util::Tagging is # to split qualified tags into parts. That would cause # qualified tags to match too broadly here. return end # Is this resource tagged appropriately? def missing_tags?(resource) return false if ignore_tags? return false if tags.empty? not resource.tagged?(*tags) end end require 'puppet/transaction/report' diff --git a/lib/puppet/type.rb b/lib/puppet/type.rb index 4f18d8400..6aaf9e746 100644 --- a/lib/puppet/type.rb +++ b/lib/puppet/type.rb @@ -1,1939 +1,1943 @@ require 'puppet' require 'puppet/util/log' require 'puppet/util/metric' require 'puppet/property' require 'puppet/parameter' require 'puppet/util' require 'puppet/util/autoload' require 'puppet/metatype/manager' require 'puppet/util/errors' require 'puppet/util/log_paths' require 'puppet/util/logging' require 'puppet/file_collection/lookup' require 'puppet/util/tagging' # see the bottom of the file for the rest of the inclusions module Puppet class Type include Puppet::Util include Puppet::Util::Errors include Puppet::Util::LogPaths include Puppet::Util::Logging include Puppet::FileCollection::Lookup include Puppet::Util::Tagging ############################### # Code related to resource type attributes. class << self include Puppet::Util::ClassGen include Puppet::Util::Warnings attr_reader :properties end def self.states warnonce "The states method is deprecated; use properties" properties end # All parameters, in the appropriate order. The key_attributes come first, then # the provider, then the properties, and finally the params and metaparams # in the order they were specified in the files. def self.allattrs key_attributes | (parameters & [:provider]) | properties.collect { |property| property.name } | parameters | metaparams end # Retrieve an attribute alias, if there is one. def self.attr_alias(param) @attr_aliases[symbolize(param)] end # Create an alias to an existing attribute. This will cause the aliased # attribute to be valid when setting and retrieving values on the instance. def self.set_attr_alias(hash) hash.each do |new, old| @attr_aliases[symbolize(new)] = symbolize(old) end end # Find the class associated with any given attribute. def self.attrclass(name) @attrclasses ||= {} # We cache the value, since this method gets called such a huge number # of times (as in, hundreds of thousands in a given run). unless @attrclasses.include?(name) @attrclasses[name] = case self.attrtype(name) when :property; @validproperties[name] when :meta; @@metaparamhash[name] when :param; @paramhash[name] end end @attrclasses[name] end # What type of parameter are we dealing with? Cache the results, because # this method gets called so many times. def self.attrtype(attr) @attrtypes ||= {} unless @attrtypes.include?(attr) @attrtypes[attr] = case when @validproperties.include?(attr); :property when @paramhash.include?(attr); :param when @@metaparamhash.include?(attr); :meta end end @attrtypes[attr] end def self.eachmetaparam @@metaparams.each { |p| yield p.name } end # Create the 'ensure' class. This is a separate method so other types # can easily call it and create their own 'ensure' values. def self.ensurable(&block) if block_given? self.newproperty(:ensure, :parent => Puppet::Property::Ensure, &block) else self.newproperty(:ensure, :parent => Puppet::Property::Ensure) do self.defaultvalues end end end # Should we add the 'ensure' property to this class? def self.ensurable? # If the class has all three of these methods defined, then it's # ensurable. ens = [:exists?, :create, :destroy].inject { |set, method| set &&= self.public_method_defined?(method) } ens end def self.apply_to_device @apply_to = :device end def self.apply_to_host @apply_to = :host end def self.apply_to_all @apply_to = :both end def self.apply_to @apply_to ||= :host end def self.can_apply_to(target) [ target == :device ? :device : :host, :both ].include?(apply_to) end # Deal with any options passed into parameters. def self.handle_param_options(name, options) # If it's a boolean parameter, create a method to test the value easily if options[:boolean] define_method(name.to_s + "?") do val = self[name] if val == :true or val == true return true end end end end # Is the parameter in question a meta-parameter? def self.metaparam?(param) @@metaparamhash.include?(symbolize(param)) end # Find the metaparameter class associated with a given metaparameter name. def self.metaparamclass(name) @@metaparamhash[symbolize(name)] end def self.metaparams @@metaparams.collect { |param| param.name } end def self.metaparamdoc(metaparam) @@metaparamhash[metaparam].doc end # Create a new metaparam. Requires a block and a name, stores it in the # @parameters array, and does some basic checking on it. def self.newmetaparam(name, options = {}, &block) @@metaparams ||= [] @@metaparamhash ||= {} name = symbolize(name) param = genclass( name, :parent => options[:parent] || Puppet::Parameter, :prefix => "MetaParam", :hash => @@metaparamhash, :array => @@metaparams, :attributes => options[:attributes], &block ) # Grr. param.required_features = options[:required_features] if options[:required_features] handle_param_options(name, options) param.metaparam = true param end def self.key_attribute_parameters @key_attribute_parameters ||= ( params = @parameters.find_all { |param| param.isnamevar? or param.name == :name } ) end def self.key_attributes key_attribute_parameters.collect { |p| p.name } end def self.title_patterns case key_attributes.length when 0; [] when 1; identity = lambda {|x| x} [ [ /(.*)/m, [ [key_attributes.first, identity ] ] ] ] else raise Puppet::DevError,"you must specify title patterns when there are two or more key attributes" end end def uniqueness_key self.class.key_attributes.sort_by { |attribute_name| attribute_name.to_s }.map{ |attribute_name| self[attribute_name] } end # Create a new parameter. Requires a block and a name, stores it in the # @parameters array, and does some basic checking on it. def self.newparam(name, options = {}, &block) options[:attributes] ||= {} param = genclass( name, :parent => options[:parent] || Puppet::Parameter, :attributes => options[:attributes], :block => block, :prefix => "Parameter", :array => @parameters, :hash => @paramhash ) handle_param_options(name, options) # Grr. param.required_features = options[:required_features] if options[:required_features] param.isnamevar if options[:namevar] param end def self.newstate(name, options = {}, &block) Puppet.warning "newstate() has been deprecrated; use newproperty(#{name})" newproperty(name, options, &block) end # Create a new property. The first parameter must be the name of the property; # this is how users will refer to the property when creating new instances. # The second parameter is a hash of options; the options are: # * :parent: The parent class for the property. Defaults to Puppet::Property. # * :retrieve: The method to call on the provider or @parent object (if # the provider is not set) to retrieve the current value. def self.newproperty(name, options = {}, &block) name = symbolize(name) # This is here for types that might still have the old method of defining # a parent class. unless options.is_a? Hash raise Puppet::DevError, "Options must be a hash, not #{options.inspect}" end raise Puppet::DevError, "Class #{self.name} already has a property named #{name}" if @validproperties.include?(name) if parent = options[:parent] options.delete(:parent) else parent = Puppet::Property end # We have to create our own, new block here because we want to define # an initial :retrieve method, if told to, and then eval the passed # block if available. prop = genclass(name, :parent => parent, :hash => @validproperties, :attributes => options) do # If they've passed a retrieve method, then override the retrieve # method on the class. if options[:retrieve] define_method(:retrieve) do provider.send(options[:retrieve]) end end class_eval(&block) if block end # If it's the 'ensure' property, always put it first. if name == :ensure @properties.unshift prop else @properties << prop end prop end def self.paramdoc(param) @paramhash[param].doc end # Return the parameter names def self.parameters return [] unless defined?(@parameters) @parameters.collect { |klass| klass.name } end # Find the parameter class associated with a given parameter name. def self.paramclass(name) @paramhash[name] end # Return the property class associated with a name def self.propertybyname(name) @validproperties[name] end def self.validattr?(name) name = symbolize(name) return true if name == :name @validattrs ||= {} unless @validattrs.include?(name) @validattrs[name] = !!(self.validproperty?(name) or self.validparameter?(name) or self.metaparam?(name)) end @validattrs[name] end # does the name reflect a valid property? def self.validproperty?(name) name = symbolize(name) @validproperties.include?(name) && @validproperties[name] end # Return the list of validproperties def self.validproperties return {} unless defined?(@parameters) @validproperties.keys end # does the name reflect a valid parameter? def self.validparameter?(name) raise Puppet::DevError, "Class #{self} has not defined parameters" unless defined?(@parameters) !!(@paramhash.include?(name) or @@metaparamhash.include?(name)) end # This is a forward-compatibility method - it's the validity interface we'll use in Puppet::Resource. def self.valid_parameter?(name) validattr?(name) end # Return either the attribute alias or the attribute. def attr_alias(name) name = symbolize(name) if synonym = self.class.attr_alias(name) return synonym else return name end end # Are we deleting this resource? def deleting? obj = @parameters[:ensure] and obj.should == :absent end # Create a new property if it is valid but doesn't exist # Returns: true if a new parameter was added, false otherwise def add_property_parameter(prop_name) if self.class.validproperty?(prop_name) && !@parameters[prop_name] self.newattr(prop_name) return true end false end # # The name_var is the key_attribute in the case that there is only one. # def name_var key_attributes = self.class.key_attributes (key_attributes.length == 1) && key_attributes.first end # abstract accessing parameters and properties, and normalize # access to always be symbols, not strings # This returns a value, not an object. It returns the 'is' # value, but you can also specifically return 'is' and 'should' # values using 'object.is(:property)' or 'object.should(:property)'. def [](name) name = attr_alias(name) fail("Invalid parameter #{name}(#{name.inspect})") unless self.class.validattr?(name) if name == :name && 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 # Abstract setting parameters and properties, and normalize # access to always be symbols, not strings. This sets the 'should' # value on properties, and otherwise just sets the appropriate parameter. def []=(name,value) name = attr_alias(name) fail("Invalid parameter #{name}") unless self.class.validattr?(name) if name == :name && 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 => detail error = Puppet::Error.new("Parameter #{name} failed: #{detail}") error.set_backtrace(detail.backtrace) raise error end end nil end # remove a property from the object; useful in testing or in cleanup # when an error has been encountered def delete(attr) attr = symbolize(attr) if @parameters.has_key?(attr) @parameters.delete(attr) else raise Puppet::DevError.new("Undefined attribute '#{attr}' in #{self}") end end # iterate across the existing properties def eachproperty # properties is a private method properties.each { |property| yield property } end # Create a transaction event. Called by Transaction or by # a property. def event(options = {}) Puppet::Transaction::Event.new({:resource => self, :file => file, :line => line, :tags => tags}.merge(options)) end # retrieve the 'should' value for a specified property def should(name) name = attr_alias(name) (prop = @parameters[name] and prop.is_a?(Puppet::Property)) ? prop.should : nil end # Create the actual attribute instance. Requires either the attribute # name or class as the first argument, then an optional hash of # attributes to set during initialization. def newattr(name) if name.is_a?(Class) klass = name name = klass.name end unless klass = self.class.attrclass(name) raise Puppet::Error, "Resource type #{self.class.name} does not support parameter #{name}" end if provider and ! provider.class.supports_parameter?(klass) missing = klass.required_features.find_all { |f| ! provider.class.feature?(f) } info "Provider %s does not support features %s; not managing attribute %s" % [provider.class.name, missing.join(", "), name] return nil end return @parameters[name] if @parameters.include?(name) @parameters[name] = klass.new(:resource => self) end # return the value of a parameter def parameter(name) @parameters[name.to_sym] end def parameters @parameters.dup end # Is the named property defined? def propertydefined?(name) name = name.intern unless name.is_a? Symbol @parameters.include?(name) end # Return an actual property instance by name; to return the value, use 'resource[param]' # LAK:NOTE(20081028) Since the 'parameter' method is now a superset of this method, # this one should probably go away at some point. def property(name) (obj = @parameters[symbolize(name)] and obj.is_a?(Puppet::Property)) ? obj : nil end # For any parameters or properties that have defaults and have not yet been # set, set them now. This method can be handed a list of attributes, # and if so it will only set defaults for those attributes. def set_default(attr) return unless klass = self.class.attrclass(attr) return unless klass.method_defined?(:default) return if @parameters.include?(klass.name) return unless parameter = newattr(klass.name) if value = parameter.default and ! value.nil? parameter.value = value else @parameters.delete(parameter.name) end end # Convert our object to a hash. This just includes properties. def to_hash rethash = {} @parameters.each do |name, obj| rethash[name] = obj.value end rethash end def type self.class.name end # Return a specific value for an attribute. def value(name) name = attr_alias(name) (obj = @parameters[name] and obj.respond_to?(:value)) ? obj.value : nil end def version return 0 unless catalog catalog.version end # Return all of the property objects, in the order specified in the # class. def properties self.class.properties.collect { |prop| @parameters[prop.name] }.compact end # Is this type's name isomorphic with the object? That is, if the # name conflicts, does it necessarily mean that the objects conflict? # Defaults to true. def self.isomorphic? if defined?(@isomorphic) return @isomorphic else return true end end def isomorphic? self.class.isomorphic? end # is the instance a managed instance? A 'yes' here means that # the instance was created from the language, vs. being created # in order resolve other questions, such as finding a package # in a list def managed? # Once an object is managed, it always stays managed; but an object # that is listed as unmanaged might become managed later in the process, # so we have to check that every time if @managed return @managed else @managed = false properties.each { |property| s = property.should if s and ! property.class.unmanaged @managed = true break end } return @managed end end ############################### # Code related to the container behaviour. def depthfirst? false end # Remove an object. The argument determines whether the object's # subscriptions get eliminated, too. def remove(rmdeps = true) # This is hackish (mmm, cut and paste), but it works for now, and it's # better than warnings. @parameters.each do |name, obj| obj.remove end @parameters.clear @parent = nil # Remove the reference to the provider. if self.provider @provider.clear @provider = nil end end ############################### # Code related to evaluating the resources. + def ancestors + [] + end + # Flush the provider, if it supports it. This is called by the # transaction. def flush self.provider.flush if self.provider and self.provider.respond_to?(:flush) end # if all contained objects are in sync, then we're in sync # FIXME I don't think this is used on the type instances any more, # it's really only used for testing def insync?(is) insync = true if property = @parameters[:ensure] unless is.include? property raise Puppet::DevError, "The is value is not in the is array for '#{property.name}'" end ensureis = is[property] if property.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 # retrieve the current value of all contained properties def retrieve fail "Provider #{provider.class.name} is not functional on this host" if self.provider.is_a?(Puppet::Provider) and ! provider.class.suitable? result = Puppet::Resource.new(type, title) # Provide the name, so we know we'll always refer to a real thing result[:name] = self[:name] unless self[:name] == title if ensure_prop = property(:ensure) or (self.class.validattr?(:ensure) and ensure_prop = newattr(:ensure)) result[:ensure] = ensure_state = ensure_prop.retrieve else ensure_state = nil end properties.each do |property| next if property.name == :ensure if ensure_state == :absent result[property] = :absent else result[property] = property.retrieve end end result end def retrieve_resource resource = retrieve resource = Resource.new(type, title, :parameters => resource) if resource.is_a? Hash resource end # Get a hash of the current properties. Returns a hash with # the actual property instance as the key and the current value # as the, um, value. def currentpropvalues # It's important to use the 'properties' method here, as it follows the order # in which they're defined in the class. It also guarantees that 'ensure' # is the first property, which is important for skipping 'retrieve' on # all the properties if the resource is absent. ensure_state = false return properties.inject({}) do | prophash, property| if property.name == :ensure ensure_state = property.retrieve prophash[property] = ensure_state else if ensure_state == :absent prophash[property] = :absent else prophash[property] = property.retrieve end end prophash end end # Are we running in noop mode? def noop? # If 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 def noop noop? end ############################### # Code related to managing resource instances. require 'puppet/transportable' # retrieve a named instance of the current type def self.[](name) raise "Global resource access is deprecated" @objects[name] || @aliases[name] end # add an instance by name to the class list of instances def self.[]=(name,object) raise "Global resource storage is deprecated" newobj = nil if object.is_a?(Puppet::Type) newobj = object else raise Puppet::DevError, "must pass a Puppet::Type object" end if exobj = @objects[name] and self.isomorphic? msg = "Object '#{newobj.class.name}[#{name}]' already exists" msg += ("in file #{object.file} at line #{object.line}") if exobj.file and exobj.line msg += ("and cannot be redefined in file #{object.file} at line #{object.line}") if object.file and object.line error = Puppet::Error.new(msg) raise error else #Puppet.info("adding %s of type %s to class list" % # [name,object.class]) @objects[name] = newobj end end # Create an alias. We keep these in a separate hash so that we don't encounter # the objects multiple times when iterating over them. def self.alias(name, obj) raise "Global resource aliasing is deprecated" if @objects.include?(name) unless @objects[name] == obj raise Puppet::Error.new( "Cannot create alias #{name}: object already exists" ) end end if @aliases.include?(name) unless @aliases[name] == obj raise Puppet::Error.new( "Object #{@aliases[name].name} already has alias #{name}" ) end end @aliases[name] = obj end # remove all of the instances of a single type def self.clear raise "Global resource removal is deprecated" if defined?(@objects) @objects.each do |name, obj| obj.remove(true) end @objects.clear end @aliases.clear if defined?(@aliases) end # Force users to call this, so that we can merge objects if # necessary. def self.create(args) # LAK:DEP Deprecation notice added 12/17/2008 Puppet.warning "Puppet::Type.create is deprecated; use Puppet::Type.new" new(args) end # remove a specified object def self.delete(resource) raise "Global resource removal is deprecated" return unless defined?(@objects) @objects.delete(resource.title) if @objects.include?(resource.title) @aliases.delete(resource.title) if @aliases.include?(resource.title) if @aliases.has_value?(resource) names = [] @aliases.each do |name, otherres| if otherres == resource names << name end end names.each { |name| @aliases.delete(name) } end end # iterate across each of the type's instances def self.each raise "Global resource iteration is deprecated" return unless defined?(@objects) @objects.each { |name,instance| yield instance } end # does the type have an object with the given name? def self.has_key?(name) raise "Global resource access is deprecated" @objects.has_key?(name) end # Retrieve all known instances. Either requires providers or must be overridden. def self.instances raise Puppet::DevError, "#{self.name} has no providers and has not overridden 'instances'" if provider_hash.empty? # Put the default provider first, then the rest of the suitable providers. provider_instances = {} providers_by_source.collect do |provider| all_properties = 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.warning "%s %s found in both %s and %s; skipping the %s version" % [self.name.to_s.capitalize, instance.name, other.class.name, instance.class.name, instance.class.name] next end provider_instances[instance.name] = instance result = new(:name => instance.name, :provider => instance) properties.each { |name| result.newattr(name) } result end end.flatten.compact end # Return a list of one suitable provider per source, with the default provider first. def self.providers_by_source # Put the default provider first, then the rest of the suitable providers. sources = [] [defaultprovider, suitableprovider].flatten.uniq.collect do |provider| next if sources.include?(provider.source) sources << provider.source provider end.compact end # Convert a simple hash into a Resource instance. def self.hash2resource(hash) hash = hash.inject({}) { |result, ary| result[ary[0].to_sym] = ary[1]; result } title = hash.delete(:title) title ||= hash[:name] title ||= hash[key_attributes.first] if key_attributes.length == 1 raise Puppet::Error, "Title or name must be provided" unless title # Now create our resource. resource = Puppet::Resource.new(self.name, title) [:catalog].each do |attribute| if value = hash[attribute] hash.delete(attribute) resource.send(attribute.to_s + "=", value) end end hash.each do |param, value| resource[param] = value end resource end # Create the path for logging and such. def pathbuilder if p = parent [p.pathbuilder, self.ref].flatten else [self.ref] end end ############################### # Add all of the meta parameters. newmetaparam(:noop) do desc "Boolean flag indicating whether work should actually be done." newvalues(:true, :false) munge do |value| case value when true, :true, "true"; @resource.noop = true when false, :false, "false"; @resource.noop = false end end end newmetaparam(:schedule) do desc "On what schedule the object should be managed. You must create a schedule object, and then reference the name of that object to use that for your schedule: schedule { 'daily': period => daily, range => \"2-4\" } exec { \"/usr/bin/apt-get update\": schedule => 'daily' } The creation of the schedule object does not need to appear in the configuration before objects that use it." end newmetaparam(:audit) do desc "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(:check) do desc "Audit specified attributes of resources over time, and report if any have changed. This parameter has been deprecated in favor of 'audit'." munge do |args| resource.warning "'check' attribute is deprecated; use 'audit' instead" resource[:audit] = args end end newmetaparam(:loglevel) do desc "Sets the level that information will be logged. The log levels have the biggest impact when logs are sent to syslog (which is currently the default)." defaultto :notice newvalues(*Puppet::Util::Log.levels) newvalues(:verbose) munge do |loglevel| val = super(loglevel) if val == :verbose val = :info end val end end newmetaparam(:alias) do desc "Creates an alias for the object. Puppet uses this internally when you provide a symbolic title: 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 only the library can use these aliases; for instance, the following code will not work: file { \"/etc/ssh/sshd_config\": owner => root, group => root, alias => 'sshdconfig' } file { 'sshdconfig': mode => 644 } There's no way here for the Puppet parser to know that these two stanzas should be affecting the same file. See the [Language Guide](http://docs.puppetlabs.com/guides/language_guide.html) for more information. " munge do |aliases| aliases = [aliases] unless aliases.is_a?(Array) raise(ArgumentError, "Cannot add aliases without a catalog") unless @resource.catalog aliases.each do |other| if obj = @resource.catalog.resource(@resource.class.name, other) unless obj.object_id == @resource.object_id self.fail("#{@resource.title} can not create alias #{other}: object already exists") end next end # Newschool, add it to the catalog. @resource.catalog.alias(@resource, other) end end end newmetaparam(:tag) do desc "Add the specified tags to the associated resource. While all resources are automatically tagged with as much information as possible (e.g., each class and definition containing the resource), it can be useful to add your own tags to a given resource. 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): puppet agent --test --tags bootstrap This way, you can easily isolate the portion of the configuration you're trying to test." munge do |tags| tags = [tags] unless tags.is_a? Array tags.each do |tag| @resource.tag(tag) end end end class RelationshipMetaparam < Puppet::Parameter class << self attr_accessor :direction, :events, :callback, :subclasses end @subclasses = [] def self.inherited(sub) @subclasses << sub end def munge(references) references = [references] unless references.is_a?(Array) references.collect do |ref| if ref.is_a?(Puppet::Resource) ref else Puppet::Resource.new(ref) end end end def validate_relationship @value.each do |ref| unless @resource.catalog.resource(ref.to_s) description = self.class.direction == :in ? "dependency" : "dependent" fail "Could not find #{description} #{ref} for #{resource.ref}" end end end # Create edges from each of our relationships. :in # relationships are specified by the event-receivers, and :out # relationships are specified by the event generator. This # way 'source' and 'target' are consistent terms in both edges # and events -- that is, an event targets edges whose source matches # the event's source. The direction of the relationship determines # which resource is applied first and which resource is considered # to be the event generator. def to_edges @value.collect do |reference| reference.catalog = resource.catalog # Either of the two retrieval attempts could have returned # nil. unless related_resource = reference.resolve self.fail "Could not retrieve dependency '#{reference}' of #{@resource.ref}" end # Are we requiring them, or vice versa? See the method docs # for futher info on this. if self.class.direction == :in source = related_resource target = @resource else source = @resource target = related_resource end if method = self.class.callback subargs = { :event => self.class.events, :callback => method } self.debug("subscribes to #{related_resource.ref}") else # If there's no callback, there's no point in even adding # a label. subargs = nil self.debug("requires #{related_resource.ref}") end rel = Puppet::Relationship.new(source, target, subargs) end end end def self.relationship_params RelationshipMetaparam.subclasses end # Note that the order in which the relationships params is defined # matters. The labelled params (notify and subcribe) must be later, # so that if both params are used, those ones win. It's a hackish # solution, but it works. newmetaparam(:require, :parent => RelationshipMetaparam, :attributes => {:direction => :in, :events => :NONE}) do desc "References to one or more objects that this object depends on. This is used purely for guaranteeing that changes to required objects happen before the dependent object. For instance: # Create the destination directory before you copy things down file { \"/usr/local/scripts\": ensure => directory } file { \"/usr/local/scripts/myscript\": source => \"puppet://server/module/myscript\", mode => 755, require => File[\"/usr/local/scripts\"] } Multiple dependencies can be specified by providing a comma-seperated list of resources, enclosed in square brackets: require => [ File[\"/usr/local\"], File[\"/usr/local/scripts\"] ] Note that Puppet will autorequire everything that it can, and there are hooks in place so that it's easy for resources to add new ways to autorequire objects, so if you think Puppet could be smarter here, let us know. In fact, the above code was redundant --- Puppet will autorequire any parent directories that are being managed; it will automatically realize that the parent directory should be created before the script is pulled down. Currently, exec resources will autorequire their CWD (if it is specified) plus any fully qualified paths that appear in the command. For instance, if you had an `exec` command that ran the `myscript` mentioned above, the above code that pulls the file down would be automatically listed as a requirement to the `exec` code, so that you would always be running againts the most recent version. " end newmetaparam(:subscribe, :parent => RelationshipMetaparam, :attributes => {:direction => :in, :events => :ALL_EVENTS, :callback => :refresh}) do desc "References to one or more objects that this object depends on. This metaparameter creates a dependency relationship like **require,** and also causes the dependent object to be refreshed when the subscribed object is changed. For instance: class nagios { file { 'nagconf': path => \"/etc/nagios/nagios.conf\" source => \"puppet://server/module/nagios.conf\", } service { 'nagios': ensure => running, subscribe => File['nagconf'] } } Currently the `exec`, `mount` and `service` types support refreshing. " end newmetaparam(:before, :parent => RelationshipMetaparam, :attributes => {:direction => :out, :events => :NONE}) do desc %{References to one or more objects that depend on this object. This parameter is the opposite of **require** --- it guarantees that the specified object is applied later than the specifying object: file { "/var/nagios/configuration": source => "...", recurse => true, before => Exec["nagios-rebuid"] } exec { "nagios-rebuild": command => "/usr/bin/make", cwd => "/var/nagios/configuration" } This will make sure all of the files are up to date before the make command is run.} end newmetaparam(:notify, :parent => RelationshipMetaparam, :attributes => {:direction => :out, :events => :ALL_EVENTS, :callback => :refresh}) do desc %{References to one or more objects that depend on this object. This parameter is the opposite of **subscribe** --- it creates a dependency relationship like **before,** and also causes the dependent object(s) to be refreshed when this object is changed. For instance: file { "/etc/sshd_config": source => "....", notify => Service['sshd'] } service { 'sshd': ensure => running } This will restart the sshd service if the sshd config file changes.} end newmetaparam(:stage) do desc %{Which run stage a given resource should reside in. This just creates a dependency on or from the named milestone. For instance, saying that this is in the 'bootstrap' stage creates a dependency on the 'bootstrap' milestone. By default, all classes get directly added to the 'main' stage. You can create new stages as resources: stage { ['pre', 'post']: } To order stages, use standard relationships: stage { 'pre': before => Stage['main'] } Or use the new relationship syntax: Stage['pre'] -> Stage['main'] -> Stage['post'] Then use the new class parameters to specify a stage: class { 'foo': stage => 'pre' } Stages can only be set on classes, not individual resources. This will fail: file { '/foo': stage => 'pre', ensure => file } } end ############################### # All of the provider plumbing for the resource types. require 'puppet/provider' require 'puppet/util/provider_features' # Add the feature handling module. extend Puppet::Util::ProviderFeatures attr_reader :provider # the Type class attribute accessors class << self attr_accessor :providerloader attr_writer :defaultprovider end # Find the default provider. def self.defaultprovider unless @defaultprovider suitable = suitableprovider # Find which providers are a default for this system. defaults = suitable.find_all { |provider| provider.default? } # If we don't have any default we use suitable providers defaults = suitable if defaults.empty? max = defaults.collect { |provider| provider.specificity }.max defaults = defaults.find_all { |provider| provider.specificity == max } retval = nil if defaults.length > 1 Puppet.warning( "Found multiple default providers for #{self.name}: #{defaults.collect { |i| i.name.to_s }.join(", ")}; using #{defaults[0].name}" ) retval = defaults.shift elsif defaults.length == 1 retval = defaults.shift else raise Puppet::DevError, "Could not find a default provider for #{self.name}" end @defaultprovider = retval end @defaultprovider end def self.provider_hash_by_type(type) @provider_hashes ||= {} @provider_hashes[type] ||= {} end def self.provider_hash Puppet::Type.provider_hash_by_type(self.name) end # Retrieve a provider by name. def self.provider(name) name = Puppet::Util.symbolize(name) # If we don't have it yet, try loading it. @providerloader.load(name) unless provider_hash.has_key?(name) provider_hash[name] end # Just list all of the providers. def self.providers provider_hash.keys end def self.validprovider?(name) name = Puppet::Util.symbolize(name) (provider_hash.has_key?(name) && provider_hash[name].suitable?) end # Create a new provider of a type. This method must be called # directly on the type that it's implementing. def self.provide(name, options = {}, &block) name = Puppet::Util.symbolize(name) if 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 # Make sure we have a :provider parameter defined. Only gets called if there # are providers. def self.providify return if @paramhash.has_key? :provider newparam(:provider) do # 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 attr_accessor :parenttype end # We need to add documentation for each provider. def self.doc @doc + " Available providers are:\n\n" + parenttype.providers.sort { |a,b| a.to_s <=> b.to_s }.collect { |i| "* **#{i}**: #{parenttype().provider(i).doc}" }.join("\n") end defaultto { @resource.class.defaultprovider.name } validate do |provider_class| provider_class = provider_class[0] if provider_class.is_a? Array provider_class = provider_class.class.name if provider_class.is_a?(Puppet::Provider) unless provider = @resource.class.provider(provider_class) raise ArgumentError, "Invalid #{@resource.class.name} provider '#{provider_class}'" end end munge do |provider| provider = provider[0] if provider.is_a? Array provider = provider.intern if provider.is_a? String @resource.provider = provider if provider.is_a?(Puppet::Provider) provider.class.name else provider end end end.parenttype = self end def self.unprovide(name) if @defaultprovider and @defaultprovider.name == name @defaultprovider = nil end rmclass(name, :hash => provider_hash, :prefix => "Provider") end # Return an array of all of the suitable providers. def self.suitableprovider providerloader.loadall if provider_hash.empty? provider_hash.find_all { |name, provider| provider.suitable? }.collect { |name, provider| provider }.reject { |p| p.name == :fake } # For testing end def provider=(name) if name.is_a?(Puppet::Provider) @provider = name @provider.resource = self elsif klass = self.class.provider(name) @provider = klass.new(self) else raise ArgumentError, "Could not find #{name} provider of #{self.class.name}" end end ############################### # All of the relationship code. # Specify a block for generating a list of objects to autorequire. This # makes it so that you don't have to manually specify things that you clearly # require. def self.autorequire(name, &block) @autorequires ||= {} @autorequires[name] = block end # Yield each of those autorequires in turn, yo. def self.eachautorequire @autorequires ||= {} @autorequires.each { |type, block| yield(type, block) } end # Figure out of there are any objects we can automatically add as # dependencies. def autorequire(rel_catalog = nil) rel_catalog ||= catalog raise(Puppet::DevError, "You cannot add relationships without a catalog") unless rel_catalog reqs = [] self.class.eachautorequire { |type, block| # Ignore any types we can't find, although that would be a bit odd. next unless typeobj = Puppet::Type.type(type) # Retrieve the list of names from the block. next unless list = self.instance_eval(&block) list = [list] unless list.is_a?(Array) # Collect the current prereqs list.each { |dep| # Support them passing objects directly, to save some effort. unless dep.is_a? Puppet::Type # Skip autorequires that we aren't managing unless dep = rel_catalog.resource(type, dep) next end end reqs << Puppet::Relationship.new(dep, self) } } reqs end # Build the dependencies associated with an individual object. def builddepends # Handle the requires self.class.relationship_params.collect do |klass| if param = @parameters[klass.name] param.to_edges end end.flatten.reject { |r| r.nil? } end # Define the initial list of tags. def tags=(list) tag(self.class.name) tag(*list) end # Types (which map to resources in the languages) are entirely composed of # attribute value pairs. Generally, Puppet calls any of these things an # 'attribute', but these attributes always take one of three specific # forms: parameters, metaparams, or properties. # In naming methods, I have tried to consistently name the method so # that it is clear whether it operates on all attributes (thus has 'attr' in # the method name, or whether it operates on a specific type of attributes. attr_writer :title attr_writer :noop include Enumerable # class methods dealing with Type management public # the Type class attribute accessors class << self attr_reader :name attr_accessor :self_refresh include Enumerable, Puppet::Util::ClassGen include Puppet::MetaType::Manager include Puppet::Util include Puppet::Util::Logging end # all of the variables that must be initialized for each subclass def self.initvars # all of the instances of this class @objects = Hash.new @aliases = Hash.new @defaults = {} @parameters ||= [] @validproperties = {} @properties = [] @parameters = [] @paramhash = {} @attr_aliases = {} @paramdoc = Hash.new { |hash,key| key = key.intern if key.is_a?(String) if hash.include?(key) hash[key] else "Param Documentation for #{key} not found" end } @doc ||= "" end def self.to_s if defined?(@name) "Puppet::Type::#{@name.to_s.capitalize}" else super end end # Create a block to validate that our object is set up entirely. This will # be run before the object is operated on. def self.validate(&block) define_method(:validate, &block) #@validate = block end # The catalog that this resource is stored in. attr_accessor :catalog # is the resource exported attr_accessor :exported # is the resource virtual (it should not :-)) attr_accessor :virtual # create a log at specified level def log(msg) Puppet::Util::Log.create( :level => @parameters[:loglevel].value, :message => msg, :source => self ) end # instance methods related to instance intrinsics # e.g., initialize and name public attr_reader :original_parameters # initialize the type instance def initialize(resource) raise Puppet::DevError, "Got TransObject instead of Resource or hash" if resource.is_a?(Puppet::TransObject) resource = self.class.hash2resource(resource) unless resource.is_a?(Puppet::Resource) # The list of parameter/property instances. @parameters = {} # Set the title first, so any failures print correctly. if resource.type.to_s.downcase.to_sym == self.class.name self.title = resource.title else # This should only ever happen for components self.title = resource.ref end [:file, :line, :catalog, :exported, :virtual].each do |getter| setter = getter.to_s + "=" if val = resource.send(getter) self.send(setter, val) end end @tags = resource.tags @original_parameters = resource.to_hash set_name(@original_parameters) set_default(:provider) set_parameters(@original_parameters) self.validate if self.respond_to?(:validate) end private # Set our resource's name. def set_name(hash) self[name_var] = hash.delete(name_var) if name_var end # Set all of the parameters from a hash, in the appropriate order. def set_parameters(hash) # Use the order provided by allattrs, but add in any # extra attributes from the resource so we get failures # on invalid attributes. no_values = [] (self.class.allattrs + hash.keys).uniq.each do |attr| begin # Set any defaults immediately. This is mostly done so # that the default provider is available for any other # property validation. if hash.has_key?(attr) self[attr] = hash[attr] else no_values << attr end rescue ArgumentError, Puppet::Error, TypeError raise rescue => detail error = Puppet::DevError.new( "Could not set #{attr} on #{self.class.name}: #{detail}") error.set_backtrace(detail.backtrace) raise error end end no_values.each do |attr| set_default(attr) end end public # Set up all of our autorequires. def finish # Make sure all of our relationships are valid. Again, must be done # when the entire catalog is instantiated. self.class.relationship_params.collect do |klass| if param = @parameters[klass.name] param.validate_relationship end end.flatten.reject { |r| r.nil? } end # For now, leave the 'name' method functioning like it used to. Once 'title' # works everywhere, I'll switch it. def name self[:name] end # Look up our parent in the catalog, if we have one. def parent return nil unless catalog unless defined?(@parent) if parents = catalog.adjacent(self, :direction => :in) # We should never have more than one parent, so let's just ignore # it if we happen to. @parent = parents.shift else @parent = nil end end @parent end # Return the "type[name]" style reference. def ref "#{self.class.name.to_s.capitalize}[#{self.title}]" end def self_refresh? self.class.self_refresh end # Mark that we're purging. def purging @purging = true end # Is this resource being purged? Used by transactions to forbid # deletion when there are dependencies. def purging? if defined?(@purging) @purging else false end end # Retrieve the title of an object. If no title was set separately, # then use the object's name. def title unless @title if self.class.validparameter?(name_var) @title = self[:name] elsif self.class.validproperty?(name_var) @title = self.should(name_var) else self.devfail "Could not find namevar #{name_var} for #{self.class.name}" end end @title end # convert to a string def to_s self.ref end # Convert to a transportable object def to_trans(ret = true) trans = TransObject.new(self.title, self.class.name) values = retrieve_resource values.each do |name, value| name = name.name if name.respond_to? :name trans[name] = value end @parameters.each do |name, param| # Avoid adding each instance name twice next if param.class.isnamevar? and param.value == self.title # We've already got property values next if param.is_a?(Puppet::Property) trans[name] = param.value end trans.tags = self.tags # FIXME I'm currently ignoring 'parent' and 'path' trans end def to_resource # this 'type instance' versus 'resource' distinction seems artificial # I'd like to see it collapsed someday ~JW self.to_trans.to_resource end def virtual?; !!@virtual; end def exported?; !!@exported; end def appliable_to_device? self.class.can_apply_to(:device) end 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 b301e7996..282ebf418 100644 --- a/lib/puppet/type/file.rb +++ b/lib/puppet/type/file.rb @@ -1,806 +1,812 @@ require 'digest/md5' require 'cgi' require 'etc' require 'uri' require 'fileutils' require 'enumerator' require 'pathname' require 'puppet/network/handler' require 'puppet/util/diff' require 'puppet/util/checksums' require 'puppet/util/backups' Puppet::Type.newtype(:file) do include Puppet::Util::MethodHelper include Puppet::Util::Checksums include Puppet::Util::Backups @doc = "Manages local files, including setting ownership and permissions, creation of both files and directories, and retrieving entire files from remote servers. As Puppet matures, it expected that the `file` resource will be used less and less to manage content, and instead native resources will be used to do so. If you find that you are often copying files in from a central location, rather than using native resources, please contact Puppet Labs and we can hopefully work with you to develop a native resource to support what you are doing. **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." def self.title_patterns [ [ /^(.*?)\/*\Z/m, [ [ :path, lambda{|x| x} ] ] ] ] end newparam(:path) do desc "The path to the file to manage. Must be fully qualified." isnamevar validate do |value| unless Puppet::Util.absolute_path?(value) fail Puppet::Error, "File paths must be fully qualified, not '#{value}'" end end # convert the current path in an index into the collection and the last # path name. The aim is to use less storage for all common paths in a hierarchy munge do |value| # We know the value is absolute, so expanding it will just standardize it. path, name = ::File.split(::File.expand_path(value)) { :index => Puppet::FileCollection.collection.index(path), :name => name } end # and the reverse unmunge do |value| basedir = Puppet::FileCollection.collection.path(value[:index]) ::File.join( basedir, value[:name] ) end end newparam(:backup) do desc "Whether files should be backed up before being replaced. The preferred method of backing files up is via a `filebucket`, which stores files by their MD5 sums and allows easy retrieval without littering directories with backups. You can specify a local filebucket or a network-accessible server-based filebucket by setting `backup => bucket-name`. Alternatively, if you specify any value that begins with a `.` (e.g., `.puppet-bak`), then Puppet will use copy the file in the same directory with that value as the extension of the backup. Setting `backup => false` disables all backups of the file in question. Puppet automatically creates a local filebucket named `puppet` and defaults to backing up there. To use a server-based filebucket, you must specify one in your configuration. filebucket { main: server => puppet, path => false, # The path => false line works around a known issue with the filebucket type. } The `puppet master` daemon creates a filebucket by default, so you can usually back up to your main server with this configuration. Once you've described the bucket in your configuration, you can use it in any file's backup attribute: file { \"/my/file\": source => \"/path/in/nfs/or/something\", backup => main } This will back the file up to the central server. At this point, the benefits of using a central filebucket are that you do not have backup files lying around on each of your machines, a given version of a file is only backed up once, you can restore any given file manually (no matter how old), and you can use Puppet Dashboard to view file contents. Eventually, transactional support will be able to automatically restore filebucketed files. " defaultto "puppet" munge do |value| # I don't really know how this is happening. value = value.shift if value.is_a?(Array) case value when false, "false", :false false when true, "true", ".puppet-bak", :true ".puppet-bak" when String value else self.fail "Invalid backup type #{value.inspect}" end end end newparam(:recurse) do desc "Whether and how deeply to do recursive management. Options are: * `inf,true` --- Regular style recursion on both remote and local directory structure. * `remote` --- Descends recursively into the remote directory but not the local directory. Allows copying of a few files into a directory containing many unmanaged files without scanning all the local files. * `false` --- Default of no recursion. * `[0-9]+` --- Same as true, but limit recursion. Warning: this syntax has been deprecated in favor of the `recurselimit` attribute. " newvalues(:true, :false, :inf, :remote, /^[0-9]+$/) # Replace the validation so that we allow numbers in # addition to string representations of them. validate { |arg| } munge do |value| newval = super(value) case newval when :true, :inf; true when :false; false when :remote; :remote when Integer, Fixnum, Bignum self.warning "Setting recursion depth with the recurse parameter is now deprecated, please use recurselimit" # recurse == 0 means no recursion return false if value == 0 resource[:recurselimit] = value true when /^\d+$/ self.warning "Setting recursion depth with the recurse parameter is now deprecated, please use recurselimit" value = Integer(value) # recurse == 0 means no recursion return false if value == 0 resource[:recurselimit] = value true else 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) do desc "Whether or not to replace a file that is sourced but exists. This is useful for using file sources purely for initialization." newvalues(:true, :false) aliasvalue(:yes, :true) aliasvalue(:no, :false) defaultto :true end newparam(:force, :boolean => true) do desc "Force the file operation. Currently only used when replacing directories with links." newvalues(:true, :false) defaultto false end newparam(:ignore) do desc "A parameter which omits action on files matching specified patterns during recursion. Uses Ruby's builtin globbing engine, so shell metacharacters are fully supported, e.g. `[a-z]*`. Matches that would descend into the directory structure are ignored, e.g., `*/*`." validate do |value| unless value.is_a?(Array) or value.is_a?(String) or value == false self.devfail "Ignore must be a string or an Array" end end end newparam(:links) do desc "How to handle links during file actions. During file copying, `follow` will copy the target file instead of the link, `manage` will copy the link itself, and `ignore` will just pass it by. When not copying, `manage` and `ignore` behave equivalently (because you cannot really ignore links entirely during local recursion), and `follow` will manage the file to which the link points." newvalues(:follow, :manage) defaultto :manage end newparam(:purge, :boolean => true) do desc "Whether unmanaged files should be purged. If you have a filebucket configured the purged files will be uploaded, but if you do not, this will destroy data. Only use this option for generated files unless you really know what you are doing. This option only makes sense when recursively managing directories. Note that when using `purge` with `source`, Puppet will purge any files that are not on the remote system." defaultto :false newvalues(:true, :false) end newparam(:sourceselect) do desc "Whether to copy all valid sources, or just the first one. This parameter is only used in recursive copies; by default, the first valid source is the only one used as a recursive source, but if this parameter is set to `all`, then all valid sources will have all of their contents copied to the local host, and for sources that have the same file, the source earlier in the list will be used." defaultto :first newvalues(:first, :all) end # Autorequire the nearest ancestor directory found in the catalog. autorequire(:file) do path = Pathname(self[:path]) if !path.root? # Start at our parent, to avoid autorequiring ourself parents = path.parent.enum_for(:ascend) found = parents.find { |p| catalog.resource(:file, p.to_s) } found and found.to_s end end # Autorequire the owner and group of the file. {:user => :owner, :group => :group}.each do |type, property| autorequire(type) do if @parameters.include?(property) # The user/group property automatically converts to IDs next unless should = @parameters[property].shouldorig val = should[0] if val.is_a?(Integer) or val =~ /^\d+$/ nil else val end end end end CREATORS = [:content, :source, :target] 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 # 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 #recurse.reject do |resource| # catalog.resource(:file, resource[:path]) #end.each do |child| # catalog.add_resource child # catalog.relationship_graph.add_edge self, child #end end + def 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] = :symlink 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 # Should we be purging? def purge? @parameters.include?(:purge) and (self[:purge] == :true or self[:purge] == "true") end # Recursively generate a list of file resources, which will # be used to copy remote files, manage local files, and/or make links # to map to another directory. def recurse children = (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? 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) 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 # 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 ) end # Remove any existing data. This is only used when dealing with # links or directories. def remove_existing(should) return unless s = stat self.fail "Could not back up; will not replace" unless perform_backup unless should.to_s == "link" return if s.ftype.to_s == should.to_s end case s.ftype when "directory" if self[:force] == :true debug "Removing existing directory for replacement with #{should}" FileUtils.rmtree(self[:path]) else notice "Not removing directory; use 'force' to override" return end when "link", "file" debug "Removing existing #{s.ftype} for replacement with #{should}" ::File.unlink(self[:path]) else self.fail "Could not back up files of type #{s.ftype}" end @stat = :needs_stat true 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 ::File.send(method, self[:path]) rescue Errno::ENOENT => error nil rescue Errno::EACCES => error warning "Could not stat; permission denied" nil end end # We have to hack this just a little bit, because otherwise we'll get # an error when the target and the contents are created as properties on # the far side. def to_trans(retrieve = true) obj = super obj.delete(:target) if obj[:target] == :notlink obj 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) use_temporary_file = write_temporary_file? if use_temporary_file path = "#{self[:path]}.puppettmp_#{rand(10000)}" path = "#{self[:path]}.puppettmp_#{rand(10000)}" while ::File.exists?(path) or ::File.symlink?(path) else path = self[:path] end mode = self.should(:mode) # might be nil umask = mode ? 000 : 022 mode_int = mode ? mode.to_i(8) : nil content_checksum = Puppet::Util.withumask(umask) { ::File.open(path, 'wb', mode_int ) { |f| write_content(f) } } # And put our new file in place if use_temporary_file # This is only not true when our file is empty. begin fail_if_checksum_is_wrong(path, content_checksum) if validate_checksum? ::File.rename(path, self[:path]) rescue => detail fail "Could not rename temporary file #{path} to #{self[:path]}: #{detail}" ensure # Make sure the created file gets removed ::File.unlink(path) if FileTest.exists?(path) end end # make sure all of the modes are actually correct property_fix end private # 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 private 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) || (s = @parameters[:source] and 1) 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/spec/unit/transaction_spec.rb b/spec/unit/transaction_spec.rb index 3f34f65bd..b70577dc1 100755 --- a/spec/unit/transaction_spec.rb +++ b/spec/unit/transaction_spec.rb @@ -1,464 +1,556 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/transaction' +require 'fileutils' def without_warnings flag = $VERBOSE $VERBOSE = nil yield $VERBOSE = flag end describe Puppet::Transaction do include PuppetSpec::Files before do @basepath = make_absolute("/what/ever") @transaction = Puppet::Transaction.new(Puppet::Resource::Catalog.new) end it "should delegate its event list to the event manager" do @transaction = Puppet::Transaction.new(Puppet::Resource::Catalog.new) @transaction.event_manager.expects(:events).returns %w{my events} @transaction.events.should == %w{my events} end it "should delegate adding times to its report" do @transaction = Puppet::Transaction.new(Puppet::Resource::Catalog.new) @transaction.report.expects(:add_times).with(:foo, 10) @transaction.report.expects(:add_times).with(:bar, 20) @transaction.add_times :foo => 10, :bar => 20 end it "should be able to accept resource status instances" do resource = Puppet::Type.type(:notify).new :title => "foobar" status = Puppet::Resource::Status.new(resource) @transaction.add_resource_status(status) @transaction.resource_status(resource).should equal(status) end it "should be able to look resource status up by resource reference" do resource = Puppet::Type.type(:notify).new :title => "foobar" status = Puppet::Resource::Status.new(resource) @transaction.add_resource_status(status) @transaction.resource_status(resource.to_s).should equal(status) end # This will basically only ever be used during testing. it "should automatically create resource statuses if asked for a non-existent status" do resource = Puppet::Type.type(:notify).new :title => "foobar" @transaction.resource_status(resource).should be_instance_of(Puppet::Resource::Status) end it "should add provided resource statuses to its report" do resource = Puppet::Type.type(:notify).new :title => "foobar" status = Puppet::Resource::Status.new(resource) @transaction.add_resource_status(status) @transaction.report.resource_statuses[resource.to_s].should equal(status) end it "should consider a resource to be failed if a status instance exists for that resource and indicates it is failed" do resource = Puppet::Type.type(:notify).new :name => "yayness" status = Puppet::Resource::Status.new(resource) status.failed = "some message" @transaction.add_resource_status(status) @transaction.should be_failed(resource) end it "should not consider a resource to be failed if a status instance exists for that resource but indicates it is not failed" do resource = Puppet::Type.type(:notify).new :name => "yayness" status = Puppet::Resource::Status.new(resource) @transaction.add_resource_status(status) @transaction.should_not be_failed(resource) end it "should consider there to be failed resources if any statuses are marked failed" do resource = Puppet::Type.type(:notify).new :name => "yayness" status = Puppet::Resource::Status.new(resource) status.failed = "some message" @transaction.add_resource_status(status) @transaction.should be_any_failed end it "should not consider there to be failed resources if no statuses are marked failed" do resource = Puppet::Type.type(:notify).new :name => "yayness" status = Puppet::Resource::Status.new(resource) @transaction.add_resource_status(status) @transaction.should_not be_any_failed end it "should use the provided report object" do report = Puppet::Transaction::Report.new("apply") @transaction = Puppet::Transaction.new(Puppet::Resource::Catalog.new, report) @transaction.report.should == report end it "should create a report if none is provided" do @transaction = Puppet::Transaction.new(Puppet::Resource::Catalog.new) @transaction.report.should be_kind_of Puppet::Transaction::Report end describe "when initializing" do it "should create an event manager" do @transaction = Puppet::Transaction.new(Puppet::Resource::Catalog.new) @transaction.event_manager.should be_instance_of(Puppet::Transaction::EventManager) @transaction.event_manager.transaction.should equal(@transaction) end it "should create a resource harness" do @transaction = Puppet::Transaction.new(Puppet::Resource::Catalog.new) @transaction.resource_harness.should be_instance_of(Puppet::Transaction::ResourceHarness) @transaction.resource_harness.transaction.should equal(@transaction) end end describe "when evaluating a resource" do before do @transaction = Puppet::Transaction.new(Puppet::Resource::Catalog.new) @transaction.stubs(:skip?).returns false @resource = Puppet::Type.type(:file).new :path => @basepath end it "should check whether the resource should be skipped" do @transaction.expects(:skip?).with(@resource).returns false @transaction.eval_resource(@resource) end it "should process events" do @transaction.event_manager.expects(:process_events).with(@resource) @transaction.eval_resource(@resource) end describe "and the resource should be skipped" do before do @transaction.expects(:skip?).with(@resource).returns true end it "should mark the resource's status as skipped" do @transaction.eval_resource(@resource) @transaction.resource_status(@resource).should be_skipped end end end describe "when applying a resource" do before do @resource = Puppet::Type.type(:file).new :path => @basepath @status = Puppet::Resource::Status.new(@resource) @transaction = Puppet::Transaction.new(Puppet::Resource::Catalog.new) @transaction.event_manager.stubs(:queue_events) @transaction.resource_harness.stubs(:evaluate).returns(@status) end it "should use its resource harness to apply the resource" do @transaction.resource_harness.expects(:evaluate).with(@resource) @transaction.apply(@resource) end it "should add the resulting resource status to its status list" do @transaction.apply(@resource) @transaction.resource_status(@resource).should be_instance_of(Puppet::Resource::Status) end it "should queue any events added to the resource status" do @status.expects(:events).returns %w{a b} @transaction.event_manager.expects(:queue_events).with(@resource, ["a", "b"]) @transaction.apply(@resource) end it "should log and skip any resources that cannot be applied" do @transaction.resource_harness.expects(:evaluate).raises ArgumentError @resource.expects(:err) @transaction.apply(@resource) @transaction.report.resource_statuses[@resource.to_s].should be_nil end end + describe "#eval_generate" do + let(:path) { tmpdir('eval_generate') } + let(:resource) { Puppet::Type.type(:file).new(:path => path, :recurse => true) } + let(:graph) { @transaction.relationship_graph } + + def find_vertex(type, title) + graph.vertices.find {|v| v.type == type and v.title == title} + end + + before :each do + @filenames = [] + + 'a'.upto('c') do |x| + + 'a'.upto('c') do |y| + FileUtils.mkdir_p(File.join(path,x,y)) + + 'a'.upto('c') do |z| + @filenames << File.join(path,x,y,z) + FileUtils.touch(File.join(path,x,y,z)) + end + end + end + + @transaction.catalog.add_resource(resource) + end + + it "should add the generated resources to the catalog" do + @transaction.eval_generate(resource) + + @filenames.each do |file| + @transaction.catalog.resource(:file, file).should be_a(Puppet::Type.type(:file)) + end + end + + it "should add a sentinel whit for the resource" do + @transaction.eval_generate(resource) + + find_vertex(:whit, "completed_#{path}").should be_a(Puppet::Type.type(:whit)) + end + + it "should replace dependencies on the resource with dependencies on the sentinel" do + dependent = Puppet::Type.type(:notify).new(:name => "hello", :require => resource) + + @transaction.catalog.add_resource(dependent) + + res = find_vertex(resource.type, resource.title) + generated = find_vertex(dependent.type, dependent.title) + + graph.should be_edge(res, generated) + + @transaction.eval_generate(resource) + + sentinel = find_vertex(:whit, "completed_#{path}") + + graph.should be_edge(sentinel, generated) + graph.should_not be_edge(res, generated) + end + + it "should add an edge from the nearest ancestor to the generated resource" do + @transaction.eval_generate(resource) + + @filenames.each do |file| + v = find_vertex(:file, file) + p = find_vertex(:file, File.dirname(file)) + + graph.should be_edge(p, v) + end + end + + it "should add an edge from each generated resource to the sentinel" do + @transaction.eval_generate(resource) + + sentinel = find_vertex(:whit, "completed_#{path}") + @filenames.each do |file| + v = find_vertex(:file, file) + + graph.should be_edge(v, sentinel) + end + end + + it "should add an edge from the resource to the sentinel" do + @transaction.eval_generate(resource) + + res = find_vertex(:file, path) + sentinel = find_vertex(:whit, "completed_#{path}") + + graph.should be_edge(res, sentinel) + end + end + describe "when generating resources" do it "should call 'generate' on all created resources" do first = Puppet::Type.type(:notify).new(:name => "first") second = Puppet::Type.type(:notify).new(:name => "second") third = Puppet::Type.type(:notify).new(:name => "third") @catalog = Puppet::Resource::Catalog.new @transaction = Puppet::Transaction.new(@catalog) first.expects(:generate).returns [second] second.expects(:generate).returns [third] third.expects(:generate) @transaction.generate_additional_resources(first) end it "should finish all resources" do generator = stub 'generator', :depthfirst? => true, :tags => [] resource = stub 'resource', :tag => nil @catalog = Puppet::Resource::Catalog.new @transaction = Puppet::Transaction.new(@catalog) generator.expects(:generate).returns [resource] @catalog.expects(:add_resource).yields(resource) resource.expects(:finish) @transaction.generate_additional_resources(generator) end it "should skip generated resources that conflict with existing resources" do generator = mock 'generator', :tags => [] resource = stub 'resource', :tag => nil @catalog = Puppet::Resource::Catalog.new @transaction = Puppet::Transaction.new(@catalog) generator.expects(:generate).returns [resource] @catalog.expects(:add_resource).raises(Puppet::Resource::Catalog::DuplicateResourceError.new("foo")) resource.expects(:finish).never resource.expects(:info) # log that it's skipped @transaction.generate_additional_resources(generator) end it "should copy all tags to the newly generated resources" do child = stub 'child' generator = stub 'resource', :tags => ["one", "two"] @catalog = Puppet::Resource::Catalog.new @transaction = Puppet::Transaction.new(@catalog) generator.stubs(:generate).returns [child] @catalog.stubs(:add_resource) child.expects(:tag).with("one", "two") child.expects(:finish) generator.expects(:depthfirst?) @transaction.generate_additional_resources(generator) end end describe "when skipping a resource" do before :each do @resource = Puppet::Type.type(:notify).new :name => "foo" @catalog = Puppet::Resource::Catalog.new @resource.catalog = @catalog @transaction = Puppet::Transaction.new(@catalog) end it "should skip resource with missing tags" do @transaction.stubs(:missing_tags?).returns(true) @transaction.should be_skip(@resource) end it "should skip unscheduled resources" do @transaction.stubs(:scheduled?).returns(false) @transaction.should be_skip(@resource) end it "should skip resources with failed dependencies" do @transaction.stubs(:failed_dependencies?).returns(true) @transaction.should be_skip(@resource) end it "should skip virtual resource" do @resource.stubs(:virtual?).returns true @transaction.should be_skip(@resource) end it "should skip device only resouce on normal host" do @resource.stubs(:appliable_to_device?).returns true @transaction.for_network_device = false @transaction.should be_skip(@resource) end it "should not skip device only resouce on remote device" do @resource.stubs(:appliable_to_device?).returns true @transaction.for_network_device = true @transaction.should_not be_skip(@resource) end it "should skip host resouce on device" do @resource.stubs(:appliable_to_device?).returns false @transaction.for_network_device = true @transaction.should be_skip(@resource) end end describe "when determining if tags are missing" do before :each do @resource = Puppet::Type.type(:notify).new :name => "foo" @catalog = Puppet::Resource::Catalog.new @resource.catalog = @catalog @transaction = Puppet::Transaction.new(@catalog) @transaction.stubs(:ignore_tags?).returns false end it "should not be missing tags if tags are being ignored" do @transaction.expects(:ignore_tags?).returns true @resource.expects(:tagged?).never @transaction.should_not be_missing_tags(@resource) end it "should not be missing tags if the transaction tags are empty" do @transaction.tags = [] @resource.expects(:tagged?).never @transaction.should_not be_missing_tags(@resource) end it "should otherwise let the resource determine if it is missing tags" do tags = ['one', 'two'] @transaction.tags = tags @resource.expects(:tagged?).with(*tags).returns(false) @transaction.should be_missing_tags(@resource) end end describe "when determining if a resource should be scheduled" do before :each do @resource = Puppet::Type.type(:notify).new :name => "foo" @catalog = Puppet::Resource::Catalog.new @resource.catalog = @catalog @transaction = Puppet::Transaction.new(@catalog) end it "should always schedule resources if 'ignoreschedules' is set" do @transaction.ignoreschedules = true @transaction.resource_harness.expects(:scheduled?).never @transaction.should be_scheduled(@resource) end it "should let the resource harness determine whether the resource should be scheduled" do @transaction.resource_harness.expects(:scheduled?).with(@transaction.resource_status(@resource), @resource).returns "feh" @transaction.scheduled?(@resource).should == "feh" end end describe "when prefetching" do it "should match resources by name, not title" do @catalog = Puppet::Resource::Catalog.new @transaction = Puppet::Transaction.new(@catalog) # Have both a title and name resource = Puppet::Type.type(:sshkey).create :title => "foo", :name => "bar", :type => :dsa, :key => "eh" @catalog.add_resource resource resource.provider.class.expects(:prefetch).with("bar" => resource) @transaction.prefetch end end it "should return all resources for which the resource status indicates the resource has changed when determinig changed resources" do @catalog = Puppet::Resource::Catalog.new @transaction = Puppet::Transaction.new(@catalog) names = [] 2.times do |i| name = File.join(@basepath, "file#{i}") resource = Puppet::Type.type(:file).new :path => name names << resource.to_s @catalog.add_resource resource @transaction.add_resource_status Puppet::Resource::Status.new(resource) end @transaction.resource_status(names[0]).changed = true @transaction.changed?.should == [@catalog.resource(names[0])] end describe 'when checking application run state' do before do without_warnings { Puppet::Application = Class.new(Puppet::Application) } @catalog = Puppet::Resource::Catalog.new @transaction = Puppet::Transaction.new(@catalog) end after do without_warnings { Puppet::Application = Puppet::Application.superclass } end it 'should return true for :stop_processing? if Puppet::Application.stop_requested? is true' do Puppet::Application.stubs(:stop_requested?).returns(true) @transaction.stop_processing?.should be_true end it 'should return false for :stop_processing? if Puppet::Application.stop_requested? is false' do Puppet::Application.stubs(:stop_requested?).returns(false) @transaction.stop_processing?.should be_false end describe 'within an evaluate call' do before do @resource = Puppet::Type.type(:notify).new :title => "foobar" @catalog.add_resource @resource @transaction.stubs(:prepare) end it 'should stop processing if :stop_processing? is true' do @transaction.stubs(:stop_processing?).returns(true) @transaction.expects(:eval_resource).never @transaction.evaluate end it 'should continue processing if :stop_processing? is false' do @transaction.stubs(:stop_processing?).returns(false) @transaction.expects(:eval_resource).returns(nil) @transaction.evaluate end end end end describe Puppet::Transaction, " when determining tags" do before do @config = Puppet::Resource::Catalog.new @transaction = Puppet::Transaction.new(@config) end it "should default to the tags specified in the :tags setting" do Puppet.expects(:[]).with(:tags).returns("one") @transaction.tags.should == %w{one} end it "should split tags based on ','" do Puppet.expects(:[]).with(:tags).returns("one,two") @transaction.tags.should == %w{one two} end it "should use any tags set after creation" do Puppet.expects(:[]).with(:tags).never @transaction.tags = %w{one two} @transaction.tags.should == %w{one two} end it "should always convert assigned tags to an array" do @transaction.tags = "one::two" @transaction.tags.should == %w{one::two} end it "should accept a comma-delimited string" do @transaction.tags = "one, two" @transaction.tags.should == %w{one two} end it "should accept an empty string" do @transaction.tags = "" @transaction.tags.should == [] end end diff --git a/spec/unit/type/file_spec.rb b/spec/unit/type/file_spec.rb index 3efd5a362..cf278b4f8 100755 --- a/spec/unit/type/file_spec.rb +++ b/spec/unit/type/file_spec.rb @@ -1,1474 +1,1489 @@ #!/usr/bin/env rspec require 'spec_helper' describe Puppet::Type.type(:file) do include PuppetSpec::Files let(:path) { tmpfile('file_testing') } let(:file) { described_class.new(:path => path, :catalog => catalog) } let(:provider) { file.provider } let(:catalog) { Puppet::Resource::Catalog.new } before do @real_posix = Puppet.features.posix? Puppet.features.stubs("posix?").returns(true) end describe "the path parameter" do describe "on POSIX systems", :if => Puppet.features.posix? do it "should remove trailing slashes" do file[:path] = "/foo/bar/baz/" file[:path].should == "/foo/bar/baz" end it "should remove double slashes" do file[:path] = "/foo/bar//baz" file[:path].should == "/foo/bar/baz" end it "should remove trailing double slashes" do file[:path] = "/foo/bar/baz//" file[:path].should == "/foo/bar/baz" end it "should leave a single slash alone" do file[:path] = "/" file[:path].should == "/" end it "should accept a double-slash at the start of the path" do expect { file[:path] = "//tmp/xxx" # REVISIT: This should be wrong, later. See the next test. # --daniel 2011-01-31 file[:path].should == '/tmp/xxx' }.should_not raise_error end # REVISIT: This is pending, because I don't want to try and audit the # entire codebase to make sure we get this right. POSIX treats two (and # exactly two) '/' characters at the start of the path specially. # # See sections 3.2 and 4.11, which allow DomainOS to be all special like # and still have the POSIX branding and all. --daniel 2011-01-31 it "should preserve the double-slash at the start of the path" end describe "on Windows systems", :if => Puppet.features.microsoft_windows? do it "should remove trailing slashes" do file[:path] = "X:/foo/bar/baz/" file[:path].should == "X:/foo/bar/baz" end it "should remove double slashes" do file[:path] = "X:/foo/bar//baz" file[:path].should == "X:/foo/bar/baz" end it "should remove trailing double slashes" do file[:path] = "X:/foo/bar/baz//" file[:path].should == "X:/foo/bar/baz" end it "should leave a drive letter with a slash alone", :'fails_on_ruby_1.9.2' => true do file[:path] = "X:/" file[:path].should == "X:/" end it "should not accept a drive letter without a slash", :'fails_on_ruby_1.9.2' => true do lambda { file[:path] = "X:" }.should raise_error(/File paths must be fully qualified/) end describe "when using UNC filenames", :if => Puppet.features.microsoft_windows?, :'fails_on_ruby_1.9.2' => true do before :each do pending("UNC file paths not yet supported") end it "should remove trailing slashes" do file[:path] = "//server/foo/bar/baz/" file[:path].should == "//server/foo/bar/baz" end it "should remove double slashes" do file[:path] = "//server/foo/bar//baz" file[:path].should == "//server/foo/bar/baz" end it "should remove trailing double slashes" do file[:path] = "//server/foo/bar/baz//" file[:path].should == "//server/foo/bar/baz" end it "should remove a trailing slash from a sharename" do file[:path] = "//server/foo/" file[:path].should == "//server/foo" end it "should not modify a sharename" do file[:path] = "//server/foo" file[:path].should == "//server/foo" end end end end describe "the backup parameter" do [false, 'false', :false].each do |value| it "should disable backup if the value is #{value.inspect}" do file[:backup] = value file[:backup].should == false end end [true, 'true', '.puppet-bak'].each do |value| it "should use .puppet-bak if the value is #{value.inspect}" do file[:backup] = value file[:backup].should == '.puppet-bak' end end it "should use the provided value if it's any other string" do file[:backup] = "over there" file[:backup].should == "over there" end it "should fail if backup is set to anything else" do expect do file[:backup] = 97 end.to raise_error(Puppet::Error, /Invalid backup type 97/) end end describe "the recurse parameter" do it "should default to recursion being disabled" do file[:recurse].should be_false end [true, "true", 10, "inf", "remote"].each do |value| it "should consider #{value} to enable recursion" do file[:recurse] = value file[:recurse].should be_true end end [false, "false", 0].each do |value| it "should consider #{value} to disable recursion" do file[:recurse] = value file[:recurse].should be_false end end it "should warn if recurse is specified as a number" do file[:recurse] = 3 message = /Setting recursion depth with the recurse parameter is now deprecated, please use recurselimit/ @logs.find { |log| log.level == :warning and log.message =~ message}.should_not be_nil end end describe "the recurselimit parameter" do it "should accept integers" do file[:recurselimit] = 12 file[:recurselimit].should == 12 end it "should munge string numbers to number numbers" do file[:recurselimit] = '12' file[:recurselimit].should == 12 end it "should fail if given a non-number" do expect do file[:recurselimit] = 'twelve' end.to raise_error(Puppet::Error, /Invalid value "twelve"/) end end describe "the replace parameter" do [true, :true, :yes].each do |value| it "should consider #{value} to be true" do file[:replace] = value file[:replace].should == :true end end [false, :false, :no].each do |value| it "should consider #{value} to be false" do file[:replace] = value file[:replace].should == :false end end end describe "#[]" do it "should raise an exception" do expect do described_class['anything'] end.to raise_error("Global resource access is deprecated") end end describe ".instances" do it "should return an empty array" do described_class.instances.should == [] end end describe "#asuser" do before :each do # Mocha won't let me just stub SUIDManager.asuser to yield and return, # but it will do exactly that if we're not root. Puppet.features.stubs(:root?).returns false end it "should return the desired owner if they can write to the parent directory" do file[:owner] = 1001 FileTest.stubs(:writable?).with(File.dirname file[:path]).returns true file.asuser.should == 1001 end it "should return nil if the desired owner can't write to the parent directory" do file[:owner] = 1001 FileTest.stubs(:writable?).with(File.dirname file[:path]).returns false file.asuser.should == nil end it "should return nil if not managing owner" do file.asuser.should == nil end end describe "#bucket" do it "should return nil if backup is off" do file[:backup] = false file.bucket.should == nil end it "should not return a bucket if using a file extension for backup" do file[:backup] = '.backup' file.bucket.should == nil end it "should return the default filebucket if using the 'puppet' filebucket" do file[:backup] = 'puppet' bucket = stub('bucket') file.stubs(:default_bucket).returns bucket file.bucket.should == bucket end it "should fail if using a remote filebucket and no catalog exists" do file.catalog = nil file[:backup] = 'my_bucket' expect { file.bucket }.to raise_error(Puppet::Error, "Can not find filebucket for backups without a catalog") end it "should fail if the specified filebucket isn't in the catalog" do file[:backup] = 'my_bucket' expect { file.bucket }.to raise_error(Puppet::Error, "Could not find filebucket my_bucket specified in backup") end it "should use the specified filebucket if it is in the catalog" do file[:backup] = 'my_bucket' filebucket = Puppet::Type.type(:filebucket).new(:name => 'my_bucket') catalog.add_resource(filebucket) file.bucket.should == filebucket.bucket end end describe "#asuser" do before :each do # Mocha won't let me just stub SUIDManager.asuser to yield and return, # but it will do exactly that if we're not root. Puppet.features.stubs(:root?).returns false end it "should return the desired owner if they can write to the parent directory" do file[:owner] = 1001 FileTest.stubs(:writable?).with(File.dirname file[:path]).returns true file.asuser.should == 1001 end it "should return nil if the desired owner can't write to the parent directory" do file[:owner] = 1001 FileTest.stubs(:writable?).with(File.dirname file[:path]).returns false file.asuser.should == nil end it "should return nil if not managing owner" do file.asuser.should == nil end end describe "#bucket" do it "should return nil if backup is off" do file[:backup] = false file.bucket.should == nil end it "should return nil if using a file extension for backup" do file[:backup] = '.backup' file.bucket.should == nil end it "should return the default filebucket if using the 'puppet' filebucket" do file[:backup] = 'puppet' bucket = stub('bucket') file.stubs(:default_bucket).returns bucket file.bucket.should == bucket end it "should fail if using a remote filebucket and no catalog exists" do file.catalog = nil file[:backup] = 'my_bucket' expect { file.bucket }.to raise_error(Puppet::Error, "Can not find filebucket for backups without a catalog") end it "should fail if the specified filebucket isn't in the catalog" do file[:backup] = 'my_bucket' expect { file.bucket }.to raise_error(Puppet::Error, "Could not find filebucket my_bucket specified in backup") end it "should use the specified filebucket if it is in the catalog" do file[:backup] = 'my_bucket' filebucket = Puppet::Type.type(:filebucket).new(:name => 'my_bucket') catalog.add_resource(filebucket) file.bucket.should == filebucket.bucket end end describe "#exist?" do it "should be considered existent if it can be stat'ed" do file.expects(:stat).returns mock('stat') file.must be_exist end it "should be considered nonexistent if it can not be stat'ed" do file.expects(:stat).returns nil file.must_not be_exist end end describe "#eval_generate" do before do @graph = stub 'graph', :add_edge => nil catalog.stubs(:relationship_graph).returns @graph end it "should recurse if recursion is enabled" do resource = stub('resource', :[] => 'resource') file.expects(:recurse).returns [resource] file[:recurse] = true file.eval_generate.should == [resource] end it "should not recurse if recursion is disabled" do file.expects(:recurse).never file[:recurse] = false file.eval_generate.should == [] end end + describe "#ancestors" do + it "should return the ancestors of the file, in ascending order" do + file = described_class.new(:path => make_absolute("/tmp/foo/bar/baz/qux")) + + pieces = %W[#{make_absolute('/')} tmp foo bar baz] + + ancestors = file.ancestors + + ancestors.should_not be_empty + ancestors.reverse.each_with_index do |path,i| + path.should == File.join(*pieces[0..i]) + end + end + end + describe "#flush" do it "should flush all properties that respond to :flush" do file[:source] = File.expand_path(__FILE__) file.parameter(:source).expects(:flush) file.flush end it "should reset its stat reference" do FileUtils.touch(path) stat1 = file.stat file.stat.should equal(stat1) file.flush file.stat.should_not equal(stat1) end end describe "#initialize" do it "should remove a trailing slash from the title to create the path" do title = File.expand_path("/abc/\n\tdef/") file = described_class.new(:title => title) file[:path].should == title end it "should set a desired 'ensure' value if none is set and 'content' is set" do file = described_class.new(:path => path, :content => "/foo/bar") file[:ensure].should == :file end it "should set a desired 'ensure' value if none is set and 'target' is set" do file = described_class.new(:path => path, :target => File.expand_path(__FILE__)) file[:ensure].should == :symlink end end describe "#mark_children_for_purging" do it "should set each child's ensure to absent" do paths = %w[foo bar baz] children = paths.inject({}) do |children,child| children.merge child => described_class.new(:path => File.join(path, child), :ensure => :present) end file.mark_children_for_purging(children) children.length.should == 3 children.values.each do |child| child[:ensure].should == :absent end end it "should skip children which have a source" do child = described_class.new(:path => path, :ensure => :present, :source => File.expand_path(__FILE__)) file.mark_children_for_purging('foo' => child) child[:ensure].should == :present end end describe "#newchild" do it "should create a new resource relative to the parent" do child = file.newchild('bar') child.should be_a(described_class) child[:path].should == File.join(file[:path], 'bar') end { :ensure => :present, :recurse => true, :recurselimit => 5, :target => "some_target", :source => File.expand_path("some_source"), }.each do |param, value| it "should omit the #{param} parameter" do # Make a new file, because we have to set the param at initialization # or it wouldn't be copied regardless. file = described_class.new(:path => path, param => value) child = file.newchild('bar') child[param].should_not == value end end it "should copy all of the parent resource's 'should' values that were set at initialization" do parent = described_class.new(:path => path, :owner => 'root', :group => 'wheel') child = parent.newchild("my/path") child[:owner].should == 'root' child[:group].should == 'wheel' end it "should not copy default values to the new child" do child = file.newchild("my/path") child.original_parameters.should_not include(:backup) end it "should not copy values to the child which were set by the source" do file[:source] = File.expand_path(__FILE__) metadata = stub 'metadata', :owner => "root", :group => "root", :mode => 0755, :ftype => "file", :checksum => "{md5}whatever" file.parameter(:source).stubs(:metadata).returns metadata file.parameter(:source).copy_source_values file.class.expects(:new).with { |params| params[:group].nil? } file.newchild("my/path") end end describe "#purge?" do it "should return false if purge is not set" do file.must_not be_purge end it "should return true if purge is set to true" do file[:purge] = true file.must be_purge end it "should return false if purge is set to false" do file[:purge] = false file.must_not be_purge end end describe "#recurse" do before do file[:recurse] = true @metadata = Puppet::FileServing::Metadata end describe "and a source is set" do it "should pass the already-discovered resources to recurse_remote" do file[:source] = File.expand_path(__FILE__) file.stubs(:recurse_local).returns(:foo => "bar") file.expects(:recurse_remote).with(:foo => "bar").returns [] file.recurse end end describe "and a target is set" do it "should use recurse_link" do file[:target] = File.expand_path(__FILE__) file.stubs(:recurse_local).returns(:foo => "bar") file.expects(:recurse_link).with(:foo => "bar").returns [] file.recurse end end it "should use recurse_local if recurse is not remote" do file.expects(:recurse_local).returns({}) file.recurse end it "should not use recurse_local if recurse is remote" do file[:recurse] = :remote file.expects(:recurse_local).never file.recurse end it "should return the generated resources as an array sorted by file path" do one = stub 'one', :[] => "/one" two = stub 'two', :[] => "/one/two" three = stub 'three', :[] => "/three" file.expects(:recurse_local).returns(:one => one, :two => two, :three => three) file.recurse.should == [one, two, three] end describe "and purging is enabled" do before do file[:purge] = true end it "should mark each file for removal" do local = described_class.new(:path => path, :ensure => :present) file.expects(:recurse_local).returns("local" => local) file.recurse local[:ensure].should == :absent end it "should not remove files that exist in the remote repository" do file[:source] = File.expand_path(__FILE__) file.expects(:recurse_local).returns({}) remote = described_class.new(:path => path, :source => File.expand_path(__FILE__), :ensure => :present) file.expects(:recurse_remote).with { |hash| hash["remote"] = remote } file.recurse remote[:ensure].should_not == :absent end end end describe "#remove_less_specific_files" do it "should remove any nested files that are already in the catalog" do foo = described_class.new :path => File.join(file[:path], 'foo') bar = described_class.new :path => File.join(file[:path], 'bar') baz = described_class.new :path => File.join(file[:path], 'baz') catalog.add_resource(foo) catalog.add_resource(bar) file.remove_less_specific_files([foo, bar, baz]).should == [baz] end end describe "#remove_less_specific_files" do it "should remove any nested files that are already in the catalog" do foo = described_class.new :path => File.join(file[:path], 'foo') bar = described_class.new :path => File.join(file[:path], 'bar') baz = described_class.new :path => File.join(file[:path], 'baz') catalog.add_resource(foo) catalog.add_resource(bar) file.remove_less_specific_files([foo, bar, baz]).should == [baz] end end describe "#recurse?" do it "should be true if recurse is true" do file[:recurse] = true file.must be_recurse end it "should be true if recurse is remote" do file[:recurse] = :remote file.must be_recurse end it "should be false if recurse is false" do file[:recurse] = false file.must_not be_recurse end end describe "#recurse_link" do before do @first = stub 'first', :relative_path => "first", :full_path => "/my/first", :ftype => "directory" @second = stub 'second', :relative_path => "second", :full_path => "/my/second", :ftype => "file" @resource = stub 'file', :[]= => nil end it "should pass its target to the :perform_recursion method" do file[:target] = "mylinks" file.expects(:perform_recursion).with("mylinks").returns [@first] file.stubs(:newchild).returns @resource file.recurse_link({}) end it "should ignore the recursively-found '.' file and configure the top-level file to create a directory" do @first.stubs(:relative_path).returns "." file[:target] = "mylinks" file.expects(:perform_recursion).with("mylinks").returns [@first] file.stubs(:newchild).never file.expects(:[]=).with(:ensure, :directory) file.recurse_link({}) end it "should create a new child resource for each generated metadata instance's relative path that doesn't already exist in the children hash" do file.expects(:perform_recursion).returns [@first, @second] file.expects(:newchild).with(@first.relative_path).returns @resource file.recurse_link("second" => @resource) end it "should not create a new child resource for paths that already exist in the children hash" do file.expects(:perform_recursion).returns [@first] file.expects(:newchild).never file.recurse_link("first" => @resource) end it "should set the target to the full path of discovered file and set :ensure to :link if the file is not a directory" do file.stubs(:perform_recursion).returns [@first, @second] file.recurse_link("first" => @resource, "second" => file) file[:ensure].should == :link file[:target].should == "/my/second" end it "should :ensure to :directory if the file is a directory" do file.stubs(:perform_recursion).returns [@first, @second] file.recurse_link("first" => file, "second" => @resource) file[:ensure].should == :directory end it "should return a hash with both created and existing resources with the relative paths as the hash keys" do file.expects(:perform_recursion).returns [@first, @second] file.stubs(:newchild).returns file file.recurse_link("second" => @resource).should == {"second" => @resource, "first" => file} end end describe "#recurse_local" do before do @metadata = stub 'metadata', :relative_path => "my/file" end it "should pass its path to the :perform_recursion method" do file.expects(:perform_recursion).with(file[:path]).returns [@metadata] file.stubs(:newchild) file.recurse_local end it "should return an empty hash if the recursion returns nothing" do file.expects(:perform_recursion).returns nil file.recurse_local.should == {} end it "should create a new child resource with each generated metadata instance's relative path" do file.expects(:perform_recursion).returns [@metadata] file.expects(:newchild).with(@metadata.relative_path).returns "fiebar" file.recurse_local end it "should not create a new child resource for the '.' directory" do @metadata.stubs(:relative_path).returns "." file.expects(:perform_recursion).returns [@metadata] file.expects(:newchild).never file.recurse_local end it "should return a hash of the created resources with the relative paths as the hash keys" do file.expects(:perform_recursion).returns [@metadata] file.expects(:newchild).with("my/file").returns "fiebar" file.recurse_local.should == {"my/file" => "fiebar"} end it "should set checksum_type to none if this file checksum is none" do file[:checksum] = :none Puppet::FileServing::Metadata.indirection.expects(:search).with { |path,params| params[:checksum_type] == :none }.returns [@metadata] file.expects(:newchild).with("my/file").returns "fiebar" file.recurse_local end end describe "#recurse_remote" do before do file[:source] = "puppet://foo/bar" @first = Puppet::FileServing::Metadata.new("/my", :relative_path => "first") @second = Puppet::FileServing::Metadata.new("/my", :relative_path => "second") @first.stubs(:ftype).returns "directory" @second.stubs(:ftype).returns "directory" @parameter = stub 'property', :metadata= => nil @resource = stub 'file', :[]= => nil, :parameter => @parameter end it "should pass its source to the :perform_recursion method" do data = Puppet::FileServing::Metadata.new("/whatever", :relative_path => "foobar") file.expects(:perform_recursion).with("puppet://foo/bar").returns [data] file.stubs(:newchild).returns @resource file.recurse_remote({}) end it "should not recurse when the remote file is not a directory" do data = Puppet::FileServing::Metadata.new("/whatever", :relative_path => ".") data.stubs(:ftype).returns "file" file.expects(:perform_recursion).with("puppet://foo/bar").returns [data] file.expects(:newchild).never file.recurse_remote({}) end it "should set the source of each returned file to the searched-for URI plus the found relative path" do @first.expects(:source=).with File.join("puppet://foo/bar", @first.relative_path) file.expects(:perform_recursion).returns [@first] file.stubs(:newchild).returns @resource file.recurse_remote({}) end it "should create a new resource for any relative file paths that do not already have a resource" do file.stubs(:perform_recursion).returns [@first] file.expects(:newchild).with("first").returns @resource file.recurse_remote({}).should == {"first" => @resource} end it "should not create a new resource for any relative file paths that do already have a resource" do file.stubs(:perform_recursion).returns [@first] file.expects(:newchild).never file.recurse_remote("first" => @resource) end it "should set the source of each resource to the source of the metadata" do file.stubs(:perform_recursion).returns [@first] @resource.stubs(:[]=) @resource.expects(:[]=).with(:source, File.join("puppet://foo/bar", @first.relative_path)) file.recurse_remote("first" => @resource) end # LAK:FIXME This is a bug, but I can't think of a fix for it. Fortunately it's already # filed, and when it's fixed, we'll just fix the whole flow. it "should set the checksum type to :md5 if the remote file is a file" do @first.stubs(:ftype).returns "file" file.stubs(:perform_recursion).returns [@first] @resource.stubs(:[]=) @resource.expects(:[]=).with(:checksum, :md5) file.recurse_remote("first" => @resource) end it "should store the metadata in the source property for each resource so the source does not have to requery the metadata" do file.stubs(:perform_recursion).returns [@first] @resource.expects(:parameter).with(:source).returns @parameter @parameter.expects(:metadata=).with(@first) file.recurse_remote("first" => @resource) end it "should not create a new resource for the '.' file" do @first.stubs(:relative_path).returns "." file.stubs(:perform_recursion).returns [@first] file.expects(:newchild).never file.recurse_remote({}) end it "should store the metadata in the main file's source property if the relative path is '.'" do @first.stubs(:relative_path).returns "." file.stubs(:perform_recursion).returns [@first] file.parameter(:source).expects(:metadata=).with @first file.recurse_remote("first" => @resource) end describe "and multiple sources are provided" do let(:sources) do h = {} %w{/one /two /three /four}.each do |key| h[key] = URI.unescape(Puppet::Util.path_to_uri(File.expand_path(key)).to_s) end h end describe "and :sourceselect is set to :first" do it "should create file instances for the results for the first source to return any values" do data = Puppet::FileServing::Metadata.new("/whatever", :relative_path => "foobar") file[:source] = sources.keys.map { |key| File.expand_path(key) } file.expects(:perform_recursion).with(sources['/one']).returns nil file.expects(:perform_recursion).with(sources['/two']).returns [] file.expects(:perform_recursion).with(sources['/three']).returns [data] file.expects(:perform_recursion).with(sources['/four']).never file.expects(:newchild).with("foobar").returns @resource file.recurse_remote({}) end end describe "and :sourceselect is set to :all" do before do file[:sourceselect] = :all end it "should return every found file that is not in a previous source" do klass = Puppet::FileServing::Metadata file[:source] = %w{/one /two /three /four}.map {|f| File.expand_path(f) } file.stubs(:newchild).returns @resource one = [klass.new("/one", :relative_path => "a")] file.expects(:perform_recursion).with(sources['/one']).returns one file.expects(:newchild).with("a").returns @resource two = [klass.new("/two", :relative_path => "a"), klass.new("/two", :relative_path => "b")] file.expects(:perform_recursion).with(sources['/two']).returns two file.expects(:newchild).with("b").returns @resource three = [klass.new("/three", :relative_path => "a"), klass.new("/three", :relative_path => "c")] file.expects(:perform_recursion).with(sources['/three']).returns three file.expects(:newchild).with("c").returns @resource file.expects(:perform_recursion).with(sources['/four']).returns [] file.recurse_remote({}) end end end end describe "#perform_recursion" do it "should use Metadata to do its recursion" do Puppet::FileServing::Metadata.indirection.expects(:search) file.perform_recursion(file[:path]) end it "should use the provided path as the key to the search" do Puppet::FileServing::Metadata.indirection.expects(:search).with { |key, options| key == "/foo" } file.perform_recursion("/foo") end it "should return the results of the metadata search" do Puppet::FileServing::Metadata.indirection.expects(:search).returns "foobar" file.perform_recursion(file[:path]).should == "foobar" end it "should pass its recursion value to the search" do file[:recurse] = true Puppet::FileServing::Metadata.indirection.expects(:search).with { |key, options| options[:recurse] == true } file.perform_recursion(file[:path]) end it "should pass true if recursion is remote" do file[:recurse] = :remote Puppet::FileServing::Metadata.indirection.expects(:search).with { |key, options| options[:recurse] == true } file.perform_recursion(file[:path]) end it "should pass its recursion limit value to the search" do file[:recurselimit] = 10 Puppet::FileServing::Metadata.indirection.expects(:search).with { |key, options| options[:recurselimit] == 10 } file.perform_recursion(file[:path]) end it "should configure the search to ignore or manage links" do file[:links] = :manage Puppet::FileServing::Metadata.indirection.expects(:search).with { |key, options| options[:links] == :manage } file.perform_recursion(file[:path]) end it "should pass its 'ignore' setting to the search if it has one" do file[:ignore] = %w{.svn CVS} Puppet::FileServing::Metadata.indirection.expects(:search).with { |key, options| options[:ignore] == %w{.svn CVS} } file.perform_recursion(file[:path]) end end describe "#remove_existing" do it "should do nothing if the file doesn't exist" do file.remove_existing(:file).should == nil end it "should fail if it can't backup the file" do file.stubs(:stat).returns stub('stat') file.stubs(:perform_backup).returns false expect { file.remove_existing(:file) }.to raise_error(Puppet::Error, /Could not back up; will not replace/) end it "should not do anything if the file is already the right type and not a link" do file.stubs(:stat).returns stub('stat', :ftype => 'file') file.remove_existing(:file).should == nil end it "should not remove directories and should not invalidate the stat unless force is set" do # Actually call stat to set @needs_stat to nil file.stat file.stubs(:stat).returns stub('stat', :ftype => 'directory') file.remove_existing(:file) file.instance_variable_get(:@stat).should == nil @logs.should be_any {|log| log.level == :notice and log.message =~ /Not removing directory; use 'force' to override/} end it "should remove a directory if force is set" do file[:force] = true file.stubs(:stat).returns stub('stat', :ftype => 'directory') FileUtils.expects(:rmtree).with(file[:path]) file.remove_existing(:file).should == true end it "should remove an existing file" do file.stubs(:perform_backup).returns true FileUtils.touch(path) file.remove_existing(:directory).should == true File.exists?(file[:path]).should == false end it "should remove an existing link", :unless => Puppet.features.microsoft_windows? do file.stubs(:perform_backup).returns true target = tmpfile('link_target') FileUtils.touch(target) FileUtils.symlink(target, path) file[:target] = target file.remove_existing(:directory).should == true File.exists?(file[:path]).should == false end it "should fail if the file is not a file, link, or directory" do file.stubs(:stat).returns stub('stat', :ftype => 'socket') expect { file.remove_existing(:file) }.to raise_error(Puppet::Error, /Could not back up files of type socket/) end it "should invalidate the existing stat of the file" do # Actually call stat to set @needs_stat to nil file.stat file.stubs(:stat).returns stub('stat', :ftype => 'file') File.stubs(:unlink) file.remove_existing(:directory).should == true file.instance_variable_get(:@stat).should == :needs_stat end end describe "#retrieve" do it "should copy the source values if the 'source' parameter is set" do file[:source] = File.expand_path('/foo/bar') file.parameter(:source).expects(:copy_source_values) file.retrieve end end describe "#should_be_file?" do it "should have a method for determining if the file should be a normal file" do file.must respond_to(:should_be_file?) end it "should be a file if :ensure is set to :file" do file[:ensure] = :file file.must be_should_be_file end it "should be a file if :ensure is set to :present and the file exists as a normal file" do file.stubs(:stat).returns(mock('stat', :ftype => "file")) file[:ensure] = :present file.must be_should_be_file end it "should not be a file if :ensure is set to something other than :file" do file[:ensure] = :directory file.must_not be_should_be_file end it "should not be a file if :ensure is set to :present and the file exists but is not a normal file" do file.stubs(:stat).returns(mock('stat', :ftype => "directory")) file[:ensure] = :present file.must_not be_should_be_file end it "should be a file if :ensure is not set and :content is" do file[:content] = "foo" file.must be_should_be_file end it "should be a file if neither :ensure nor :content is set but the file exists as a normal file" do file.stubs(:stat).returns(mock("stat", :ftype => "file")) file.must be_should_be_file end it "should not be a file if neither :ensure nor :content is set but the file exists but not as a normal file" do file.stubs(:stat).returns(mock("stat", :ftype => "directory")) file.must_not be_should_be_file end end describe "#stat", :unless => Puppet.features.microsoft_windows? do before do target = tmpfile('link_target') FileUtils.touch(target) FileUtils.symlink(target, path) file[:target] = target file[:links] = :manage # so we always use :lstat end it "should stat the target if it is following links" do file[:links] = :follow file.stat.ftype.should == 'file' end it "should stat the link if is it not following links" do file[:links] = :manage file.stat.ftype.should == 'link' end it "should return nil if the file does not exist" do file[:path] = '/foo/bar/baz/non-existent' file.stat.should be_nil end it "should return nil if the file cannot be stat'ed" do dir = tmpfile('link_test_dir') child = File.join(dir, 'some_file') Dir.mkdir(dir) File.chmod(0, dir) file[:path] = child file.stat.should be_nil # chmod it back so we can clean it up File.chmod(0777, dir) end it "should return the stat instance" do file.stat.should be_a(File::Stat) end it "should cache the stat instance" do file.stat.should equal(file.stat) end end describe "#write" do it "should propagate failures encountered when renaming the temporary file" do File.stubs(:open) File.expects(:rename).raises ArgumentError file[:backup] = 'puppet' file.stubs(:validate_checksum?).returns(false) property = stub('content_property', :actual_content => "something", :length => "something".length) file.stubs(:property).with(:content).returns(property) lambda { file.write(:content) }.should raise_error(Puppet::Error) end it "should delegate writing to the content property" do filehandle = stub_everything 'fh' File.stubs(:open).yields(filehandle) File.stubs(:rename) property = stub('content_property', :actual_content => "something", :length => "something".length) file[:backup] = 'puppet' file.stubs(:validate_checksum?).returns(false) file.stubs(:property).with(:content).returns(property) property.expects(:write).with(filehandle) file.write(:content) end describe "when validating the checksum" do before { file.stubs(:validate_checksum?).returns(true) } it "should fail if the checksum parameter and content checksums do not match" do checksum = stub('checksum_parameter', :sum => 'checksum_b', :sum_file => 'checksum_b') file.stubs(:parameter).with(:checksum).returns(checksum) property = stub('content_property', :actual_content => "something", :length => "something".length, :write => 'checksum_a') file.stubs(:property).with(:content).returns(property) lambda { file.write :NOTUSED }.should raise_error(Puppet::Error) end end describe "when not validating the checksum" do before { file.stubs(:validate_checksum?).returns(false) } it "should not fail if the checksum property and content checksums do not match" do checksum = stub('checksum_parameter', :sum => 'checksum_b') file.stubs(:parameter).with(:checksum).returns(checksum) property = stub('content_property', :actual_content => "something", :length => "something".length, :write => 'checksum_a') file.stubs(:property).with(:content).returns(property) lambda { file.write :NOTUSED }.should_not raise_error(Puppet::Error) end end end describe "#fail_if_checksum_is_wrong" do it "should fail if the checksum of the file doesn't match the expected one" do expect do file.instance_eval do parameter(:checksum).stubs(:sum_file).returns('wrong!!') fail_if_checksum_is_wrong(self[:path], 'anything!') end end.to raise_error(Puppet::Error, /File written to disk did not match checksum/) end it "should not fail if the checksum is correct" do file.instance_eval do parameter(:checksum).stubs(:sum_file).returns('anything!') fail_if_checksum_is_wrong(self[:path], 'anything!').should == nil end end it "should not fail if the checksum is absent" do file.instance_eval do parameter(:checksum).stubs(:sum_file).returns(nil) fail_if_checksum_is_wrong(self[:path], 'anything!').should == nil end end end describe "#write_content" do it "should delegate writing the file to the content property" do io = stub('io') file[:content] = "some content here" file.property(:content).expects(:write).with(io) file.send(:write_content, io) end end describe "#write_temporary_file?" do it "should be true if the file has specified content" do file[:content] = 'some content' file.send(:write_temporary_file?).should be_true end it "should be true if the file has specified source" do file[:source] = File.expand_path('/tmp/foo') file.send(:write_temporary_file?).should be_true end it "should be false if the file has neither content nor source" do file.send(:write_temporary_file?).should be_false end end describe "#property_fix" do { :mode => 0777, :owner => 'joeuser', :group => 'joeusers', :seluser => 'seluser', :selrole => 'selrole', :seltype => 'seltype', :selrange => 'selrange' }.each do |name,value| it "should sync the #{name} property if it's not in sync" do file[name] = value prop = file.property(name) prop.expects(:retrieve) prop.expects(:safe_insync?).returns false prop.expects(:sync) file.send(:property_fix) end end end describe "when autorequiring" do describe "directories" do it "should autorequire its parent directory" do dir = described_class.new(:path => File.dirname(path)) catalog.add_resource file catalog.add_resource dir reqs = file.autorequire reqs[0].source.must == dir reqs[0].target.must == file end it "should autorequire its nearest ancestor directory" do dir = described_class.new(:path => File.dirname(path)) grandparent = described_class.new(:path => File.dirname(File.dirname(path))) catalog.add_resource file catalog.add_resource dir catalog.add_resource grandparent reqs = file.autorequire reqs.length.must == 1 reqs[0].source.must == dir reqs[0].target.must == file end it "should not autorequire anything when there is no nearest ancestor directory" do catalog.add_resource file file.autorequire.should be_empty end it "should not autorequire its parent dir if its parent dir is itself" do file[:path] = File.expand_path('/') catalog.add_resource file file.autorequire.should be_empty end describe "on Windows systems", :if => Puppet.features.microsoft_windows? do describe "when using UNC filenames" do it "should autorequire its parent directory" do file[:path] = '//server/foo/bar/baz' dir = described_class.new(:path => "//server/foo/bar") catalog.add_resource file catalog.add_resource dir reqs = file.autorequire reqs[0].source.must == dir reqs[0].target.must == file end it "should autorequire its nearest ancestor directory" do file = described_class.new(:path => "//server/foo/bar/baz/qux") dir = described_class.new(:path => "//server/foo/bar/baz") grandparent = described_class.new(:path => "//server/foo/bar") catalog.add_resource file catalog.add_resource dir catalog.add_resource grandparent reqs = file.autorequire reqs.length.must == 1 reqs[0].source.must == dir reqs[0].target.must == file end it "should not autorequire anything when there is no nearest ancestor directory" do file = described_class.new(:path => "//server/foo/bar/baz/qux") catalog.add_resource file file.autorequire.should be_empty end it "should not autorequire its parent dir if its parent dir is itself" do file = described_class.new(:path => "//server/foo") catalog.add_resource file puts file.autorequire file.autorequire.should be_empty end end end end end describe "when managing links" do require 'tempfile' if @real_posix describe "on POSIX systems" do before do Dir.mkdir(path) @target = File.join(path, "target") @link = File.join(path, "link") File.open(@target, "w", 0644) { |f| f.puts "yayness" } File.symlink(@target, @link) file[:path] = @link file[:mode] = 0755 catalog.add_resource file end it "should default to managing the link" do catalog.apply # I convert them to strings so they display correctly if there's an error. (File.stat(@target).mode & 007777).to_s(8).should == '644' end it "should be able to follow links" do file[:links] = :follow catalog.apply (File.stat(@target).mode & 007777).to_s(8).should == '755' end end else # @real_posix # should recode tests using expectations instead of using the filesystem end describe "on Microsoft Windows systems" do before do Puppet.features.stubs(:posix?).returns(false) Puppet.features.stubs(:microsoft_windows?).returns(true) end it "should refuse to work with links" end end describe "when using source" do before do file[:source] = File.expand_path('/one') end Puppet::Type::File::ParameterChecksum.value_collection.values.reject {|v| v == :none}.each do |checksum_type| describe "with checksum '#{checksum_type}'" do before do file[:checksum] = checksum_type end it 'should validate' do lambda { file.validate }.should_not raise_error end end end describe "with checksum 'none'" do before do file[:checksum] = :none end it 'should raise an exception when validating' do lambda { file.validate }.should raise_error(/You cannot specify source when using checksum 'none'/) end end end describe "when using content" do before do file[:content] = 'file contents' end (Puppet::Type::File::ParameterChecksum.value_collection.values - SOURCE_ONLY_CHECKSUMS).each do |checksum_type| describe "with checksum '#{checksum_type}'" do before do file[:checksum] = checksum_type end it 'should validate' do lambda { file.validate }.should_not raise_error end end end SOURCE_ONLY_CHECKSUMS.each do |checksum_type| describe "with checksum '#{checksum_type}'" do it 'should raise an exception when validating' do file[:checksum] = checksum_type lambda { file.validate }.should raise_error(/You cannot specify content when using checksum '#{checksum_type}'/) end end end end describe "when auditing" do before :each do # to prevent the catalog from trying to write state.yaml Puppet::Util::Storage.stubs(:store) end it "should not fail if creating a new file if group is not set" do file = described_class.new(:path => path, :audit => 'all', :content => 'content') catalog.add_resource(file) report = catalog.apply.report report.resource_statuses["File[#{path}]"].should_not be_failed File.read(path).should == 'content' end it "should not log errors if creating a new file with ensure present and no content" do file[:audit] = 'content' file[:ensure] = 'present' catalog.add_resource(file) catalog.apply File.should be_exist(path) @logs.should_not be_any {|l| l.level != :notice } end end describe "when specifying both source and checksum" do it 'should use the specified checksum when source is first' do file[:source] = File.expand_path('/foo') file[:checksum] = :md5lite file[:checksum].should == :md5lite end it 'should use the specified checksum when source is last' do file[:checksum] = :md5lite file[:source] = File.expand_path('/foo') file[:checksum].should == :md5lite end end describe "when validating" do [[:source, :target], [:source, :content], [:target, :content]].each do |prop1,prop2| it "should fail if both #{prop1} and #{prop2} are specified" do file[prop1] = prop1 == :source ? File.expand_path("prop1 value") : "prop1 value" file[prop2] = "prop2 value" expect do file.validate end.to raise_error(Puppet::Error, /You cannot specify more than one of/) end end end end