diff --git a/lib/puppet/provider/parsedfile.rb b/lib/puppet/provider/parsedfile.rb index 03ad1e194..37d0ec483 100644 --- a/lib/puppet/provider/parsedfile.rb +++ b/lib/puppet/provider/parsedfile.rb @@ -1,443 +1,459 @@ require 'puppet' require 'puppet/util/filetype' require 'puppet/util/fileparsing' # This provider can be used as the parent class for a provider that # parses and generates files. Its content must be loaded via the # 'prefetch' method, and the file will be written when 'flush' is called # on the provider instance. At this point, the file is written once # for every provider instance. # # Once the provider prefetches the data, it's the resource's job to copy # that data over to the @is variables. class Puppet::Provider::ParsedFile < Puppet::Provider extend Puppet::Util::FileParsing class << self attr_accessor :default_target, :target end attr_accessor :property_hash def self.clean(hash) newhash = hash.dup [:record_type, :on_disk].each do |p| newhash.delete(p) if newhash.include?(p) end newhash end def self.clear @target_objects.clear @records.clear end def self.filetype @filetype ||= Puppet::Util::FileType.filetype(:flat) end def self.filetype=(type) if type.is_a?(Class) @filetype = type elsif klass = Puppet::Util::FileType.filetype(type) @filetype = klass else raise ArgumentError, "Invalid filetype #{type}" end end # Flush all of the targets for which there are modified records. The only # reason we pass a record here is so that we can add it to the stack if # necessary -- it's passed from the instance calling 'flush'. def self.flush(record) # Make sure this record is on the list to be flushed. unless record[:on_disk] record[:on_disk] = true @records << record # If we've just added the record, then make sure our # target will get flushed. modified(record[:target] || default_target) end return unless defined?(@modified) and ! @modified.empty? flushed = [] begin @modified.sort { |a,b| a.to_s <=> b.to_s }.uniq.each do |target| Puppet.debug "Flushing #{@resource_type.name} provider target #{target}" flushed << target flush_target(target) end ensure @modified.reject! { |t| flushed.include?(t) } end end # Make sure our file is backed up, but only back it up once per transaction. # We cheat and rely on the fact that @records is created on each prefetch. def self.backup_target(target) return nil unless target_object(target).respond_to?(:backup) @backup_stats ||= {} return nil if @backup_stats[target] == @records.object_id target_object(target).backup @backup_stats[target] = @records.object_id end # Flush all of the records relating to a specific target. def self.flush_target(target) backup_target(target) records = target_records(target).reject { |r| r[:ensure] == :absent } target_object(target).write(to_file(records)) end # Return the header placed at the top of each generated file, warning # users that modifying this file manually is probably a bad idea. def self.header %{# HEADER: This file was autogenerated at #{Time.now} # HEADER: by puppet. While it can still be managed manually, it # HEADER: is definitely not recommended.\n} end # An optional regular expression matched by third party headers. # # For example, this can be used to filter the vixie cron headers as # erronously exported by older cron versions. # # @api private # @abstract Providers based on ParsedFile may implement this to make it # possible to identify a header maintained by a third party tool. # The provider can then allow that header to remain near the top of the # written file, or remove it after composing the file content. # If implemented, the function must return a Regexp object. # The expression must be tailored to match exactly one third party header. # @see drop_native_header # @note When specifying regular expressions in multiline mode, avoid # greedy repititions such as '.*' (use .*? instead). Otherwise, the # provider may drop file content between sparse headers. def self.native_header_regex nil end # How to handle third party headers. # @api private # @abstract Providers based on ParsedFile that make use of the support for # third party headers may override this method to return +true+. # When this is done, headers that are matched by the native_header_regex # are not written back to disk. # @see native_header_regex def self.drop_native_header false end # Add another type var. def self.initvars @records = [] @target_objects = {} @target = nil # Default to flat files @filetype ||= Puppet::Util::FileType.filetype(:flat) super end # Return a list of all of the records we can find. def self.instances targets.collect do |target| prefetch_target(target) end.flatten.reject { |r| skip_record?(r) }.collect do |record| new(record) end end # Override the default method with a lot more functionality. def self.mk_resource_methods [resource_type.validproperties, resource_type.parameters].flatten.each do |attr| attr = attr.intern define_method(attr) do # If it's not a valid field for this record type (which can happen # when different platforms support different fields), then just # return the should value, so the resource shuts up. if @property_hash[attr] or self.class.valid_attr?(self.class.name, attr) @property_hash[attr] || :absent else if defined?(@resource) @resource.should(attr) else nil end end end define_method(attr.to_s + "=") do |val| mark_target_modified @property_hash[attr] = val end end end # Always make the resource methods. def self.resource_type=(resource) super mk_resource_methods end # Mark a target as modified so we know to flush it. This only gets # used within the attr= methods. def self.modified(target) @modified ||= [] @modified << target unless @modified.include?(target) end # Retrieve all of the data from disk. There are three ways to know # which files to retrieve: We might have a list of file objects already # set up, there might be instances of our associated resource and they # will have a path parameter set, and we will have a default path # set. We need to turn those three locations into a list of files, # prefetch each one, and make sure they're associated with each appropriate # resource instance. def self.prefetch(resources = nil) # Reset the record list. @records = prefetch_all_targets(resources) match_providers_with_resources(resources) end # Match a list of catalog resources with provider instances # # @api private # # @param [Array] resources A list of resources using this class as a provider def self.match_providers_with_resources(resources) return unless resources matchers = resources.dup @records.each do |record| # Skip things like comments and blank lines next if skip_record?(record) if (resource = resource_for_record(record, resources)) resource.provider = new(record) elsif respond_to?(:match) if resource = match(record, matchers) matchers.delete(resource.title) record[:name] = resource[:name] resource.provider = new(record) end end end end # Look up a resource based on a parsed file record # # @api private # # @param [Hash] record # @param [Array] resources # # @return [Puppet::Resource, nil] The resource if found, else nil def self.resource_for_record(record, resources) name = record[:name] if name resources[name] end end def self.prefetch_all_targets(resources) records = [] targets(resources).each do |target| records += prefetch_target(target) end records end # Prefetch an individual target. def self.prefetch_target(target) begin target_records = retrieve(target) rescue Puppet::Util::FileType::FileReadError => detail puts detail.backtrace if Puppet[:trace] Puppet.err "Could not prefetch #{self.resource_type.name} provider '#{self.name}' target '#{target}': #{detail}. Treating as empty" target_records = [] end target_records.each do |r| r[:on_disk] = true r[:target] = target r[:ensure] = :present end target_records = prefetch_hook(target_records) if respond_to?(:prefetch_hook) raise Puppet::DevError, "Prefetching #{target} for provider #{self.name} returned nil" unless target_records target_records end # Is there an existing record with this name? def self.record?(name) return nil unless @records @records.find { |r| r[:name] == name } end # Retrieve the text for the file. Returns nil in the unlikely # event that it doesn't exist. def self.retrieve(path) # XXX We need to be doing something special here in case of failure. text = target_object(path).read if text.nil? or text == "" # there is no file return [] else # Set the target, for logging. old = @target begin @target = path return self.parse(text) rescue Puppet::Error => detail detail.file = @target if detail.respond_to?(:file=) raise detail ensure @target = old end end end # Should we skip the record? Basically, we skip text records. # This is only here so subclasses can override it. def self.skip_record?(record) record_type(record[:record_type]).text? end + # The mode for generated files if they are newly created. + # No mode will be set on existing files. + # + # @abstract Providers inheriting parsedfile can override this method + # to provide a mode. The value should be suitable for File.chmod + def self.default_mode + nil + end + # Initialize the object if necessary. def self.target_object(target) - @target_objects[target] ||= filetype.new(target) + # only send the default mode if the actual provider defined it, + # because certain filetypes (e.g. the crontab variants) do not + # expect it in their initialize method + if default_mode + @target_objects[target] ||= filetype.new(target, default_mode) + else + @target_objects[target] ||= filetype.new(target) + end @target_objects[target] end # Find all of the records for a given target def self.target_records(target) @records.find_all { |r| r[:target] == target } end # Find a list of all of the targets that we should be reading. This is # used to figure out what targets we need to prefetch. def self.targets(resources = nil) targets = [] # First get the default target raise Puppet::DevError, "Parsed Providers must define a default target" unless self.default_target targets << self.default_target # Then get each of the file objects targets += @target_objects.keys # Lastly, check the file from any resource instances if resources resources.each do |name, resource| if value = resource.should(:target) targets << value end end end targets.uniq.compact end # Compose file contents from the set of records. # # If self.native_header_regex is not nil, possible vendor headers are # identified by matching the return value against the expression. # If one (or several consecutive) such headers, are found, they are # either moved in front of the self.header if self.drop_native_header # is false (this is the default), or removed from the return value otherwise. # # @api private def self.to_file(records) text = super if native_header_regex and (match = text.match(native_header_regex)) if drop_native_header # concatenate the text in front of and after the native header text = match.pre_match + match.post_match else native_header = match[0] return native_header + header + match.pre_match + match.post_match end end header + text end def create @resource.class.validproperties.each do |property| if value = @resource.should(property) @property_hash[property] = value end end mark_target_modified (@resource.class.name.to_s + "_created").intern end def destroy # We use the method here so it marks the target as modified. self.ensure = :absent (@resource.class.name.to_s + "_deleted").intern end def exists? !(@property_hash[:ensure] == :absent or @property_hash[:ensure].nil?) end # Write our data to disk. def flush # Make sure we've got a target and name set. # If the target isn't set, then this is our first modification, so # mark it for flushing. unless @property_hash[:target] @property_hash[:target] = @resource.should(:target) || self.class.default_target self.class.modified(@property_hash[:target]) end @resource.class.key_attributes.each do |attr| @property_hash[attr] ||= @resource[attr] end self.class.flush(@property_hash) end def initialize(record) super # The 'record' could be a resource or a record, depending on how the provider # is initialized. If we got an empty property hash (probably because the resource # is just being initialized), then we want to set up some defaults. @property_hash = self.class.record?(resource[:name]) || {:record_type => self.class.name, :ensure => :absent} if @property_hash.empty? end # Retrieve the current state from disk. def prefetch raise Puppet::DevError, "Somehow got told to prefetch with no resource set" unless @resource self.class.prefetch(@resource[:name] => @resource) end def record_type @property_hash[:record_type] end private # Mark both the resource and provider target as modified. def mark_target_modified if defined?(@resource) and restarget = @resource.should(:target) and restarget != @property_hash[:target] self.class.modified(restarget) end self.class.modified(@property_hash[:target]) if @property_hash[:target] != :absent and @property_hash[:target] end end diff --git a/lib/puppet/provider/sshkey/parsed.rb b/lib/puppet/provider/sshkey/parsed.rb index f874683b7..29f345916 100644 --- a/lib/puppet/provider/sshkey/parsed.rb +++ b/lib/puppet/provider/sshkey/parsed.rb @@ -1,35 +1,40 @@ require 'puppet/provider/parsedfile' known = nil case Facter.value(:operatingsystem) when "Darwin"; known = "/etc/ssh_known_hosts" else known = "/etc/ssh/ssh_known_hosts" end Puppet::Type.type(:sshkey).provide( :parsed, :parent => Puppet::Provider::ParsedFile, :default_target => known, :filetype => :flat ) do desc "Parse and generate host-wide known hosts files for SSH." text_line :comment, :match => /^#/ text_line :blank, :match => /^\s+/ record_line :parsed, :fields => %w{name type key}, :post_parse => proc { |hash| names = hash[:name].split(",", -1) hash[:name] = names.shift hash[:host_aliases] = names }, :pre_gen => proc { |hash| if hash[:host_aliases] hash[:name] = [hash[:name], hash[:host_aliases]].flatten.join(",") hash.delete(:host_aliases) end } + + # Make sure to use mode 644 if ssh_known_hosts is newly created + def self.default_mode + 0644 + end end diff --git a/lib/puppet/util/filetype.rb b/lib/puppet/util/filetype.rb index 9fc3b289a..08f763ee5 100644 --- a/lib/puppet/util/filetype.rb +++ b/lib/puppet/util/filetype.rb @@ -1,299 +1,303 @@ # Basic classes for reading, writing, and emptying files. Not much # to see here. require 'puppet/util/selinux' require 'tempfile' require 'fileutils' class Puppet::Util::FileType attr_accessor :loaded, :path, :synced class FileReadError < Puppet::Error; end include Puppet::Util::SELinux class << self attr_accessor :name include Puppet::Util::ClassGen end # Create a new filetype. def self.newfiletype(name, &block) @filetypes ||= {} klass = genclass( name, :block => block, :prefix => "FileType", :hash => @filetypes ) # Rename the read and write methods, so that we're sure they # maintain the stats. klass.class_eval do # Rename the read method define_method(:real_read, instance_method(:read)) define_method(:read) do begin val = real_read @loaded = Time.now if val return val.gsub(/# HEADER.*\n/,'') else return "" end rescue Puppet::Error => detail raise rescue => detail message = "#{self.class} could not read #{@path}: #{detail}" Puppet.log_exception(detail, message) raise Puppet::Error, message, detail.backtrace end end # And then the write method define_method(:real_write, instance_method(:write)) define_method(:write) do |text| begin val = real_write(text) @synced = Time.now return val rescue Puppet::Error => detail raise rescue => detail message = "#{self.class} could not write #{@path}: #{detail}" Puppet.log_exception(detail, message) raise Puppet::Error, message, detail.backtrace end end end end def self.filetype(type) @filetypes[type] end # Pick or create a filebucket to use. def bucket @bucket ||= Puppet::Type.type(:filebucket).mkdefaultbucket.bucket end - def initialize(path) + def initialize(path, default_mode = nil) raise ArgumentError.new("Path is nil") if path.nil? @path = path + @default_mode = default_mode end # Arguments that will be passed to the execute method. Will set the uid # to the target user if the target user and the current user are not # the same def cronargs if uid = Puppet::Util.uid(@path) and uid == Puppet::Util::SUIDManager.uid {:failonfail => true, :combine => true} else {:failonfail => true, :combine => true, :uid => @path} end end # Operate on plain files. newfiletype(:flat) do # Back the file up before replacing it. def backup bucket.backup(@path) if Puppet::FileSystem.exist?(@path) end # Read the file. def read if Puppet::FileSystem.exist?(@path) File.read(@path) else return nil end end # Remove the file. def remove Puppet::FileSystem.unlink(@path) if Puppet::FileSystem.exist?(@path) end # Overwrite the file. def write(text) tf = Tempfile.new("puppet") tf.print text; tf.flush + File.chmod(@default_mode, tf.path) if @default_mode FileUtils.cp(tf.path, @path) tf.close # If SELinux is present, we need to ensure the file has its expected context set_selinux_default_context(@path) end end # Operate on plain files. newfiletype(:ram) do @@tabs = {} def self.clear @@tabs.clear end - def initialize(path) + def initialize(path, default_mode = nil) + # default_mode is meaningless for this filetype, + # supported only for compatibility with :flat super @@tabs[@path] ||= "" end # Read the file. def read Puppet.info "Reading #{@path} from RAM" @@tabs[@path] end # Remove the file. def remove Puppet.info "Removing #{@path} from RAM" @@tabs[@path] = "" end # Overwrite the file. def write(text) Puppet.info "Writing #{@path} to RAM" @@tabs[@path] = text end end # Handle Linux-style cron tabs. newfiletype(:crontab) do def initialize(user) self.path = user end def path=(user) begin @uid = Puppet::Util.uid(user) rescue Puppet::Error => detail raise FileReadError, "Could not retrieve user #{user}: #{detail}", detail.backtrace end # XXX We have to have the user name, not the uid, because some # systems *cough*linux*cough* require it that way @path = user end # Read a specific @path's cron tab. def read %x{#{cmdbase} -l 2>/dev/null} end # Remove a specific @path's cron tab. def remove if %w{Darwin FreeBSD DragonFly}.include?(Facter.value("operatingsystem")) %x{/bin/echo yes | #{cmdbase} -r 2>/dev/null} else %x{#{cmdbase} -r 2>/dev/null} end end # Overwrite a specific @path's cron tab; must be passed the @path name # and the text with which to create the cron tab. def write(text) IO.popen("#{cmdbase()} -", "w") { |p| p.print text } end private # Only add the -u flag when the @path is different. Fedora apparently # does not think I should be allowed to set the @path to my own user name def cmdbase if @uid == Puppet::Util::SUIDManager.uid || Facter.value(:operatingsystem) == "HP-UX" return "crontab" else return "crontab -u #{@path}" end end end # SunOS has completely different cron commands; this class implements # its versions. newfiletype(:suntab) do # Read a specific @path's cron tab. def read Puppet::Util::Execution.execute(%w{crontab -l}, cronargs) rescue => detail case detail.to_s when /can't open your crontab/ return "" when /you are not authorized to use cron/ raise FileReadError, "User #{@path} not authorized to use cron", detail.backtrace else raise FileReadError, "Could not read crontab for #{@path}: #{detail}", detail.backtrace end end # Remove a specific @path's cron tab. def remove Puppet::Util::Execution.execute(%w{crontab -r}, cronargs) rescue => detail raise FileReadError, "Could not remove crontab for #{@path}: #{detail}", detail.backtrace end # Overwrite a specific @path's cron tab; must be passed the @path name # and the text with which to create the cron tab. def write(text) output_file = Tempfile.new("puppet_suntab") begin output_file.print text output_file.close # We have to chown the stupid file to the user. File.chown(Puppet::Util.uid(@path), nil, output_file.path) Puppet::Util::Execution.execute(["crontab", output_file.path], cronargs) rescue => detail raise FileReadError, "Could not write crontab for #{@path}: #{detail}", detail.backtrace ensure output_file.close output_file.unlink end end end # Support for AIX crontab with output different than suntab's crontab command. newfiletype(:aixtab) do # Read a specific @path's cron tab. def read Puppet::Util::Execution.execute(%w{crontab -l}, cronargs) rescue => detail case detail.to_s when /Cannot open a file in the .* directory/ return "" when /You are not authorized to use the cron command/ raise FileReadError, "User #{@path} not authorized to use cron", detail.backtrace else raise FileReadError, "Could not read crontab for #{@path}: #{detail}", detail.backtrace end end # Remove a specific @path's cron tab. def remove Puppet::Util::Execution.execute(%w{crontab -r}, cronargs) rescue => detail raise FileReadError, "Could not remove crontab for #{@path}: #{detail}", detail.backtrace end # Overwrite a specific @path's cron tab; must be passed the @path name # and the text with which to create the cron tab. def write(text) output_file = Tempfile.new("puppet_aixtab") begin output_file.print text output_file.close # We have to chown the stupid file to the user. File.chown(Puppet::Util.uid(@path), nil, output_file.path) Puppet::Util::Execution.execute(["crontab", output_file.path], cronargs) rescue => detail raise FileReadError, "Could not write crontab for #{@path}: #{detail}", detail.backtrace ensure output_file.close output_file.unlink end end end end diff --git a/spec/integration/type/nagios_spec.rb b/spec/integration/type/nagios_spec.rb index 818b61649..6a1008880 100644 --- a/spec/integration/type/nagios_spec.rb +++ b/spec/integration/type/nagios_spec.rb @@ -1,80 +1,72 @@ #!/usr/bin/env ruby require 'spec_helper' require 'puppet/file_bucket/dipper' describe "Nagios file creation" do include PuppetSpec::Files before :each do FileUtils.touch(target_file) File.chmod(0600, target_file) Puppet::FileBucket::Dipper.any_instance.stubs(:backup) # Don't backup to filebucket end let :target_file do tmpfile('nagios_integration_specs') end # Copied from the crontab integration spec. # # @todo This should probably live in the PuppetSpec module instead then. def run_in_catalog(*resources) catalog = Puppet::Resource::Catalog.new catalog.host_config = false resources.each do |resource| resource.expects(:err).never catalog.add_resource(resource) end # the resources are not properly contained and generated resources # will end up with dangling edges without this stubbing: catalog.stubs(:container_of).returns resources[0] catalog.apply end - # These three helpers are from file_spec.rb - # - # @todo Define those centrally as well? - def get_mode(file) - Puppet::FileSystem.stat(file).mode - end - context "when creating a nagios config file" do context "which is not managed" do it "should choose the file mode if requested" do resource = Puppet::Type.type(:nagios_host).new( :name => 'spechost', :use => 'spectemplate', :ensure => 'present', :target => target_file, :mode => '0640' ) run_in_catalog(resource) # sticky bit only applies to directories in Windows - mode = Puppet.features.microsoft_windows? ? "640" : "100640" - ( "%o" % get_mode(target_file) ).should == mode + expect_file_mode(target_file, "640") end end context "which is managed" do - it "should not the mode" do + it "should not override the mode" do file_res = Puppet::Type.type(:file).new( :name => target_file, :ensure => :present ) nag_res = Puppet::Type.type(:nagios_host).new( :name => 'spechost', :use => 'spectemplate', :ensure => :present, :target => target_file, :mode => '0640' ) run_in_catalog(file_res, nag_res) - ( "%o" % get_mode(target_file) ).should_not == "100640" + expect_file_mode(target_file, "600") end end end end diff --git a/spec/integration/type/sshkey_spec.rb b/spec/integration/type/sshkey_spec.rb new file mode 100644 index 000000000..d1b1e01c7 --- /dev/null +++ b/spec/integration/type/sshkey_spec.rb @@ -0,0 +1,22 @@ +#! /usr/bin/env ruby +require 'spec_helper' +require 'puppet_spec/files' +require 'puppet_spec/compiler' + +describe Puppet::Type.type(:sshkey), '(integration)', :unless => Puppet.features.microsoft_windows? do + include PuppetSpec::Files + include PuppetSpec::Compiler + + let(:target) { tmpfile('ssh_known_hosts') } + let(:manifest) { "sshkey { 'test': + ensure => 'present', + type => 'rsa', + key => 'TESTKEY', + target => '#{target}' }" + } + + it "should create a new known_hosts file with mode 0644" do + apply_compiled_manifest(manifest) + expect_file_mode(target, "644") + end +end diff --git a/spec/lib/puppet_spec/files.rb b/spec/lib/puppet_spec/files.rb index 1e1076b91..312c4fc95 100755 --- a/spec/lib/puppet_spec/files.rb +++ b/spec/lib/puppet_spec/files.rb @@ -1,78 +1,88 @@ require 'fileutils' require 'tempfile' require 'tmpdir' require 'pathname' # A support module for testing files. module PuppetSpec::Files def self.cleanup $global_tempfiles ||= [] while path = $global_tempfiles.pop do begin Dir.unstub(:entries) FileUtils.rm_rf path, :secure => true rescue Errno::ENOENT # nothing to do end end end def make_absolute(path) PuppetSpec::Files.make_absolute(path) end def self.make_absolute(path) path = File.expand_path(path) path[0] = 'c' if Puppet.features.microsoft_windows? path end def tmpfile(name, dir = nil) PuppetSpec::Files.tmpfile(name, dir) end def self.tmpfile(name, dir = nil) # Generate a temporary file, just for the name... source = dir ? Tempfile.new(name, dir) : Tempfile.new(name) path = source.path source.close! record_tmp(File.expand_path(path)) path end def file_containing(name, contents) PuppetSpec::Files.file_containing(name, contents) end def self.file_containing(name, contents) file = tmpfile(name) File.open(file, 'wb') { |f| f.write(contents) } file end def tmpdir(name) PuppetSpec::Files.tmpdir(name) end def self.tmpdir(name) dir = Dir.mktmpdir(name) record_tmp(dir) dir end def dir_containing(name, contents_hash) PuppetSpec::Files.dir_containing(name, contents_hash) end def self.dir_containing(name, contents_hash) dir_contained_in(tmpdir(name), contents_hash) end def self.dir_contained_in(dir, contents_hash) contents_hash.each do |k,v| if v.is_a?(Hash) Dir.mkdir(tmp = File.join(dir,k)) dir_contained_in(tmp, v) else file = File.join(dir, k) File.open(file, 'wb') {|f| f.write(v) } end end dir end def self.record_tmp(tmp) # ...record it for cleanup, $global_tempfiles ||= [] $global_tempfiles << tmp end + + def expect_file_mode(file, mode) + actual_mode = "%o" % Puppet::FileSystem.stat(file).mode + target_mode = if Puppet.features.microsoft_windows? + mode + else + "10" + "%04i" % mode.to_i + end + actual_mode.should == target_mode + end end diff --git a/spec/unit/provider/parsedfile_spec.rb b/spec/unit/provider/parsedfile_spec.rb index f8a1773de..b814bc7ee 100755 --- a/spec/unit/provider/parsedfile_spec.rb +++ b/spec/unit/provider/parsedfile_spec.rb @@ -1,228 +1,228 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet_spec/files' require 'puppet' require 'puppet/provider/parsedfile' Puppet::Type.newtype(:parsedfile_type) do newparam(:name) newproperty(:target) end # Most of the tests for this are still in test/ral/provider/parsedfile.rb. describe Puppet::Provider::ParsedFile do # The ParsedFile provider class is meant to be used as an abstract base class # but also stores a lot of state within the singleton class. To avoid # sharing data between classes we construct an anonymous class that inherits # the ParsedFile provider instead of directly working with the ParsedFile # provider itself. let(:parsed_type) do Puppet::Type.type(:parsedfile_type) end let!(:provider) { parsed_type.provide(:parsedfile_provider, :parent => described_class) } describe "when looking up records loaded from disk" do it "should return nil if no records have been loaded" do provider.record?("foo").should be_nil end end describe "when generating a list of instances" do it "should return an instance for each record parsed from all of the registered targets" do provider.expects(:targets).returns %w{/one /two} provider.stubs(:skip_record?).returns false one = [:uno1, :uno2] two = [:dos1, :dos2] provider.expects(:prefetch_target).with("/one").returns one provider.expects(:prefetch_target).with("/two").returns two results = [] (one + two).each do |inst| results << inst.to_s + "_instance" provider.expects(:new).with(inst).returns(results[-1]) end provider.instances.should == results end it "should ignore target when retrieve fails" do provider.expects(:targets).returns %w{/one /two /three} provider.stubs(:skip_record?).returns false provider.expects(:retrieve).with("/one").returns [ {:name => 'target1_record1'}, {:name => 'target1_record2'} ] provider.expects(:retrieve).with("/two").raises Puppet::Util::FileType::FileReadError, "some error" provider.expects(:retrieve).with("/three").returns [ {:name => 'target3_record1'}, {:name => 'target3_record2'} ] Puppet.expects(:err).with('Could not prefetch parsedfile_type provider \'parsedfile_provider\' target \'/two\': some error. Treating as empty') provider.expects(:new).with(:name => 'target1_record1', :on_disk => true, :target => '/one', :ensure => :present).returns 'r1' provider.expects(:new).with(:name => 'target1_record2', :on_disk => true, :target => '/one', :ensure => :present).returns 'r2' provider.expects(:new).with(:name => 'target3_record1', :on_disk => true, :target => '/three', :ensure => :present).returns 'r3' provider.expects(:new).with(:name => 'target3_record2', :on_disk => true, :target => '/three', :ensure => :present).returns 'r4' provider.instances.should == %w{r1 r2 r3 r4} end it "should skip specified records" do provider.expects(:targets).returns %w{/one} provider.expects(:skip_record?).with(:uno).returns false provider.expects(:skip_record?).with(:dos).returns true one = [:uno, :dos] provider.expects(:prefetch_target).returns one provider.expects(:new).with(:uno).returns "eh" provider.expects(:new).with(:dos).never provider.instances end end describe "when matching resources to existing records" do let(:first_resource) { stub(:one, :name => :one) } let(:second_resource) { stub(:two, :name => :two) } let(:resources) {{:one => first_resource, :two => second_resource}} it "returns a resource if the record name matches the resource name" do record = {:name => :one} provider.resource_for_record(record, resources).should be first_resource end it "doesn't return a resource if the record name doesn't match any resource names" do record = {:name => :three} provider.resource_for_record(record, resources).should be_nil end end describe "when flushing a file's records to disk" do before do # This way we start with some @records, like we would in real life. provider.stubs(:retrieve).returns [] provider.default_target = "/foo/bar" provider.initvars provider.prefetch @filetype = Puppet::Util::FileType.filetype(:flat).new("/my/file") - Puppet::Util::FileType.filetype(:flat).stubs(:new).with("/my/file").returns @filetype + Puppet::Util::FileType.filetype(:flat).stubs(:new).with("/my/file",nil).returns @filetype @filetype.stubs(:write) end it "should back up the file being written if the filetype can be backed up" do @filetype.expects(:backup) provider.flush_target("/my/file") end it "should not try to back up the file if the filetype cannot be backed up" do @filetype = Puppet::Util::FileType.filetype(:ram).new("/my/file") Puppet::Util::FileType.filetype(:flat).expects(:new).returns @filetype @filetype.stubs(:write) provider.flush_target("/my/file") end it "should not back up the file more than once between calls to 'prefetch'" do @filetype.expects(:backup).once provider.flush_target("/my/file") provider.flush_target("/my/file") end it "should back the file up again once the file has been reread" do @filetype.expects(:backup).times(2) provider.flush_target("/my/file") provider.prefetch provider.flush_target("/my/file") end end describe "when flushing multiple files" do describe "and an error is encountered" do it "the other file does not fail" do provider.stubs(:backup_target) bad_file = 'broken' good_file = 'writable' bad_writer = mock 'bad' bad_writer.expects(:write).raises(Exception, "Failed to write to bad file") good_writer = mock 'good' good_writer.expects(:write).returns(nil) provider.stubs(:target_object).with(bad_file).returns(bad_writer) provider.stubs(:target_object).with(good_file).returns(good_writer) bad_resource = parsed_type.new(:name => 'one', :target => bad_file) good_resource = parsed_type.new(:name => 'two', :target => good_file) expect { bad_resource.flush }.to raise_error(Exception, "Failed to write to bad file") good_resource.flush end end end end describe "A very basic provider based on ParsedFile" do include PuppetSpec::Files let(:input_text) { File.read(my_fixture('simple.txt')) } let(:target) { tmpfile('parsedfile_spec') } let(:provider) do example_provider_class = Class.new(Puppet::Provider::ParsedFile) example_provider_class.default_target = target # Setup some record rules example_provider_class.instance_eval do text_line :text, :match => %r{.} end example_provider_class.initvars example_provider_class.prefetch # evade a race between multiple invocations of the header method example_provider_class.stubs(:header). returns("# HEADER As added by puppet.\n") example_provider_class end context "writing file contents back to disk" do it "should not change anything except from adding a header" do input_records = provider.parse(input_text) provider.to_file(input_records). should match provider.header + input_text end end context "rewriting a file containing a native header" do let(:regex) { %r/^# HEADER.*third party\.\n/ } let(:input_records) { provider.parse(input_text) } before :each do provider.stubs(:native_header_regex).returns(regex) end it "should move the native header to the top" do provider.to_file(input_records).should_not match /\A#{provider.header}/ end context "and dropping native headers found in input" do before :each do provider.stubs(:drop_native_header).returns(true) end it "should not include the native header in the output" do provider.to_file(input_records).should_not match regex end end end end