diff --git a/lib/puppet/provider/parsedfile.rb b/lib/puppet/provider/parsedfile.rb index 45eae57ff..40e172785 100755 --- a/lib/puppet/provider/parsedfile.rb +++ b/lib/puppet/provider/parsedfile.rb @@ -1,393 +1,395 @@ 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| if newhash.include?(p) newhash.delete(p) end end return newhash end def self.clear @target_objects.clear @records.clear end def self.filetype unless defined? @filetype @filetype = Puppet::Util::FileType.filetype(:flat) end return @filetype 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 %s" % 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 = [] @modified.sort { |a,b| a.to_s <=> b.to_s }.uniq.each do |target| Puppet.debug "Flushing %s provider target %s" % [@resource_type.name, target] flush_target(target) flushed << target end @modified.reject! { |t| flushed.include?(t) } 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) + unless defined?(@backup_stats) @backup_stats = {} end 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 # 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 = symbolize(attr) define_method(attr) do # if @property_hash.empty? # # Note that this swaps the provider out from under us. # prefetch() # if @resource.provider == self # return @property_hash[attr] # else # return @resource.provider.send(attr) # end # end # 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 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 name = record[:name] and resource = resources[name] resource.provider = new(record) elsif respond_to?(:match) if resource = match(record, matchers) # Remove this resource from circulation so we don't unnecessarily try to match matchers.delete(resource.title) record[:name] = resource[:name] resource.provider = new(record) end end 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) target_records = retrieve(target).each do |r| r[:on_disk] = true r[:target] = target r[:ensure] = :present end if respond_to?(:prefetch_hook) target_records = prefetch_hook(target_records) end unless target_records raise Puppet::DevError, "Prefetching %s for provider %s returned nil" % [target, self.name] end 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 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 # Initialize the object if necessary. def self.target_object(target) @target_objects[target] ||= filetype.new(target) @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 unless self.default_target raise Puppet::DevError, "Parsed Providers must define a default target" end 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 def self.to_file(records) text = super 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() return (@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 return (@resource.class.name.to_s + "_deleted").intern end def exists? if @property_hash[:ensure] == :absent or @property_hash[:ensure].nil? return false else return true end 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 @property_hash[:name] ||= @resource.name self.class.flush(@property_hash) #@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 defualts. if @property_hash.empty? @property_hash = self.class.record?(resource[:name]) || {:record_type => self.class.name, :ensure => :absent} end end # Retrieve the current state from disk. def prefetch unless @resource raise Puppet::DevError, "Somehow got told to prefetch with no resource set" end 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 if @property_hash[:target] != :absent and @property_hash[:target] self.class.modified(@property_hash[:target]) end end end diff --git a/lib/puppet/util/filetype.rb b/lib/puppet/util/filetype.rb index 5d4ba1440..40c028cc2 100755 --- a/lib/puppet/util/filetype.rb +++ b/lib/puppet/util/filetype.rb @@ -1,252 +1,252 @@ # Basic classes for reading, writing, and emptying files. Not much # to see here. require 'puppet/util/selinux' class Puppet::Util::FileType attr_accessor :loaded, :path, :synced 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 if Puppet[:trace] puts detail.backtrace end raise Puppet::Error, "%s could not read %s: %s" % [self.class, @path, detail] 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 if Puppet[:debug] puts detail.backtrace end raise Puppet::Error, "%s could not write %s: %s" % [self.class, @path, detail] end end end end def self.filetype(type) @filetypes[type] end - # Back the file up before replacing it. - def backup - bucket.backup(@path) if File.exists?(@path) - end - # Pick or create a filebucket to use. def bucket filebucket = Puppet::Type.type(:filebucket) (filebucket["puppet"] || filebucket.mkdefaultbucket).bucket end def initialize(path) raise ArgumentError.new("Path is nil") if path.nil? @path = path end # Operate on plain files. newfiletype(:flat) do + # Back the file up before replacing it. + def backup + bucket.backup(@path) if File.exists?(@path) + end + # Read the file. def read if File.exist?(@path) File.read(@path) else return nil end end # Remove the file. def remove if File.exist?(@path) File.unlink(@path) end end # Overwrite the file. def write(text) require "tempfile" tf = Tempfile.new("puppet") tf.print text; tf.flush 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) super @@tabs[@path] ||= "" end # Read the file. def read Puppet.info "Reading %s from RAM" % @path @@tabs[@path] end # Remove the file. def remove Puppet.info "Removing %s from RAM" % @path @@tabs[@path] = "" end # Overwrite the file. def write(text) Puppet.info "Writing %s to RAM" % @path @@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 Puppet::Error, "Could not retrieve user %s" % user 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}.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 cmd = nil if @uid == Puppet::Util::SUIDManager.uid 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 begin output = Puppet::Util.execute(%w{crontab -l}, :uid => @path) return "" if output.include?("can't open your crontab") raise Puppet::Error, "User %s not authorized to use cron" % @path if output.include?("you are not authorized to use cron") return output rescue => detail raise Puppet::Error, "Could not read crontab for %s: %s" % [@path, detail] end end # Remove a specific @path's cron tab. def remove begin Puppet::Util.execute(%w{crontab -r}, :uid => @path) rescue => detail raise Puppet::Error, "Could not remove crontab for %s: %s" % [@path, detail] 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) puts text require "tempfile" output_file = Tempfile.new("puppet") fh = output_file.open fh.print text fh.close # We have to chown the stupid file to the user. File.chown(Puppet::Util.uid(@path), nil, output_file.path) begin Puppet::Util.execute(["crontab", output_file.path], :uid => @path) rescue => detail raise Puppet::Error, "Could not write crontab for %s: %s" % [@path, detail] end output_file.delete end end end diff --git a/spec/unit/provider/parsedfile.rb b/spec/unit/provider/parsedfile.rb index 11a91c8d7..f20b6b235 100755 --- a/spec/unit/provider/parsedfile.rb +++ b/spec/unit/provider/parsedfile.rb @@ -1,86 +1,95 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet/provider/parsedfile' # Most of the tests for this are still in test/ral/provider/parsedfile.rb. describe Puppet::Provider::ParsedFile do before do @class = Class.new(Puppet::Provider::ParsedFile) end describe "when looking up records loaded from disk" do it "should return nil if no records have been loaded" do @class.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 @class.expects(:targets).returns %w{/one /two} @class.stubs(:skip_record?).returns false one = [:uno1, :uno2] two = [:dos1, :dos2] @class.expects(:prefetch_target).with("/one").returns one @class.expects(:prefetch_target).with("/two").returns two results = [] (one + two).each do |inst| results << inst.to_s + "_instance" @class.expects(:new).with(inst).returns(results[-1]) end @class.instances.should == results end it "should skip specified records" do @class.expects(:targets).returns %w{/one} @class.expects(:skip_record?).with(:uno).returns false @class.expects(:skip_record?).with(:dos).returns true one = [:uno, :dos] @class.expects(:prefetch_target).returns one @class.expects(:new).with(:uno).returns "eh" @class.expects(:new).with(:dos).never @class.instances 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. @class.stubs(:retrieve).returns [] @class.default_target = "/foo/bar" @class.initvars @class.prefetch - @filetype = mock 'filetype' - Puppet::Util::FileType.filetype(:flat).expects(:new).with("/my/file").returns @filetype + @filetype = Puppet::Util::FileType.filetype(:flat).new("/my/file") + Puppet::Util::FileType.filetype(:flat).stubs(:new).with("/my/file").returns @filetype @filetype.stubs(:write) end - it "should back up the file being written" do + it "should back up the file being written if the filetype can be backed up" do @filetype.expects(:backup) @class.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) + + @class.flush_target("/my/file") + end + it "should not back up the file more than once between calls to 'prefetch'" do @filetype.expects(:backup).once @class.flush_target("/my/file") @class.flush_target("/my/file") end it "should back the file up again once the file has been reread" do @filetype.expects(:backup).times(2) @class.flush_target("/my/file") @class.prefetch @class.flush_target("/my/file") end end end