diff --git a/lib/puppet/type/file/checksum.rb b/lib/puppet/type/file/checksum.rb index 3b748631a..76e27e55d 100755 --- a/lib/puppet/type/file/checksum.rb +++ b/lib/puppet/type/file/checksum.rb @@ -1,284 +1,281 @@ require 'puppet/util/checksums' # Keep a copy of the file checksums, and notify when they change. This # property never actually modifies the system, it only notices when the system # changes on its own. Puppet::Type.type(:file).newproperty(:checksum) do include Puppet::Util::Checksums desc "How to check whether a file has changed. This state is used internally for file copying, but it can also be used to monitor files somewhat like Tripwire without managing the file contents in any way. You can specify that a file's checksum should be monitored and then subscribe to the file from another object and receive events to signify checksum changes, for instance. There are a number of checksum types available including MD5 hashing (and an md5lite variation that only hashes the first 500 characters of the file." @event = :file_changed @unmanaged = true @validtypes = %w{md5 md5lite timestamp mtime time} def self.validtype?(type) @validtypes.include?(type) end @validtypes.each do |ctype| newvalue(ctype) do handlesum() end end str = @validtypes.join("|") # This is here because Puppet sets this internally, using # {md5}...... newvalue(/^\{#{str}\}/) do handlesum() end newvalue(:nosum) do # nothing :nochange end # If they pass us a sum type, behave normally, but if they pass # us a sum type + sum, stick the sum in the cache. munge do |value| if value =~ /^\{(\w+)\}(.+)$/ type = symbolize($1) sum = $2 cache(type, sum) return type else if FileTest.directory?(@resource[:path]) return :time elsif @resource[:source] and value.to_s != "md5" self.warning("Files with source set must use md5 as checksum. Forcing to md5 from %s for %s" % [ value, @resource[:path] ]) return :md5 else return symbolize(value) end end end # Store the checksum in the data cache, or retrieve it if only the # sum type is provided. def cache(type, sum = nil) return unless c = resource.catalog and c.host_config? unless type raise ArgumentError, "A type must be specified to cache a checksum" end type = symbolize(type) type = :mtime if type == :timestamp type = :ctime if type == :time unless state = @resource.cached(:checksums) self.debug "Initializing checksum hash" state = {} @resource.cache(:checksums, state) end if sum unless sum =~ /\{\w+\}/ sum = "{%s}%s" % [type, sum] end state[type] = sum else return state[type] end end # Because source and content and whomever else need to set the checksum # and do the updating, we provide a simple mechanism for doing so. def checksum=(value) munge(@should) self.updatesum(value) end def checktype self.should || :md5 end # Checksums need to invert how changes are printed. def change_to_s(currentvalue, newvalue) begin if currentvalue == :absent return "defined '%s' as '%s'" % [self.name, self.currentsum] elsif newvalue == :absent return "undefined %s from '%s'" % [self.name, self.is_to_s(currentvalue)] else if defined? @cached and @cached return "%s changed '%s' to '%s'" % [self.name, @cached, self.is_to_s(currentvalue)] else return "%s changed '%s' to '%s'" % [self.name, self.currentsum, self.is_to_s(currentvalue)] end end rescue Puppet::Error, Puppet::DevError raise rescue => detail raise Puppet::DevError, "Could not convert change %s to string: %s" % [self.name, detail] end end def currentsum cache(checktype()) end # Retrieve the cached sum def getcachedsum hash = nil unless hash = @resource.cached(:checksums) hash = {} @resource.cache(:checksums, hash) end sumtype = self.should if hash.include?(sumtype) #self.notice "Found checksum %s for %s" % # [hash[sumtype] ,@resource[:path]] sum = hash[sumtype] unless sum =~ /^\{\w+\}/ sum = "{%s}%s" % [sumtype, sum] end return sum elsif hash.empty? #self.notice "Could not find sum of type %s" % sumtype return :nosum else #self.notice "Found checksum for %s but not of type %s" % # [@resource[:path],sumtype] return :nosum end end # Calculate the sum from disk. def getsum(checktype, file = nil) sum = "" checktype = :mtime if checktype == :timestamp checktype = :ctime if checktype == :time self.should = checktype = :md5 if @resource.property(:source) file ||= @resource[:path] return nil unless FileTest.exist?(file) if ! FileTest.file?(file) checktype = :mtime end method = checktype.to_s + "_file" self.fail("Invalid checksum type %s" % checktype) unless respond_to?(method) return "{%s}%s" % [checktype, send(method, file)] end # At this point, we don't actually modify the system, we modify # the stored state to reflect the current state, and then kick # off an event to mark any changes. def handlesum currentvalue = self.retrieve if currentvalue.nil? raise Puppet::Error, "Checksum state for %s is somehow nil" % @resource.title end if self.insync?(currentvalue) self.debug "Checksum is already in sync" return nil end # If we still can't retrieve a checksum, it means that # the file still doesn't exist if currentvalue == :absent # if they're copying, then we won't worry about the file # not existing yet - unless @resource.property(:source) - self.warning("File %s does not exist -- cannot checksum" % @resource[:path]) - end - return nil + return nil unless @resource.property(:source) end # If the sums are different, then return an event. if self.updatesum(currentvalue) return :file_changed else return nil end end def insync?(currentvalue) @should = [checktype()] if cache(checktype()) return currentvalue == currentsum() else # If there's no cached sum, then we don't want to generate # an event. return true end end # Even though they can specify multiple checksums, the insync? # mechanism can really only test against one, so we'll just retrieve # the first specified sum type. def retrieve(usecache = false) # When the 'source' is retrieving, it passes "true" here so # that we aren't reading the file twice in quick succession, yo. currentvalue = currentsum() return currentvalue if usecache and currentvalue stat = nil return :absent unless stat = @resource.stat if stat.ftype == "link" and @resource[:links] != :follow self.debug "Not checksumming symlink" # @resource.delete(:checksum) return currentvalue end # Just use the first allowed check type currentvalue = getsum(checktype()) # If there is no sum defined, then store the current value # into the cache, so that we're not marked as being # out of sync. We don't want to generate an event the first # time we get a sum. self.updatesum(currentvalue) unless cache(checktype()) # @resource.debug "checksum state is %s" % self.is return currentvalue end # Store the new sum to the state db. def updatesum(newvalue) return unless c = resource.catalog and c.host_config? result = false # if we're replacing, vs. updating if sum = cache(checktype()) return false if newvalue == sum self.debug "Replacing %s checksum %s with %s" % [@resource.title, sum, newvalue] result = true else @resource.debug "Creating checksum %s" % newvalue result = false end # Cache the sum so the log message can be right if possible. @cached = sum cache(checktype(), newvalue) return result end end diff --git a/lib/puppet/type/file/content.rb b/lib/puppet/type/file/content.rb index 385a86357..a5fe9920a 100755 --- a/lib/puppet/type/file/content.rb +++ b/lib/puppet/type/file/content.rb @@ -1,111 +1,132 @@ require 'puppet/util/checksums' module Puppet Puppet::Type.type(:file).newproperty(:content) do include Puppet::Util::Diff include Puppet::Util::Checksums desc "Specify the contents of a file as a string. Newlines, tabs, and spaces can be specified using the escaped syntax (e.g., \\n for a newline). The primary purpose of this parameter is to provide a kind of limited templating:: define resolve(nameserver1, nameserver2, domain, search) { $str = \"search $search domain $domain nameserver $nameserver1 nameserver $nameserver2 \" file { \"/etc/resolv.conf\": content => $str } } This attribute is especially useful when used with `PuppetTemplating templating`:trac:." - def string_as_checksum(string) - return "absent" if string == :absent - "{md5}" + Digest::MD5.hexdigest(string) + # Store a checksum as the value, rather than the actual content. + # Simplifies everything. + munge do |value| + if value == :absent + value + else + @actual_content = value + "{#{checksum_type}}" + send(self.checksum_type, value) + end end - def should_to_s(should) - string_as_checksum(should) + def checksum_type + if source = resource.parameter(:source) + source.checksum =~ /^\{(\w+)\}.+/ + return $1.to_sym + elsif checksum = resource.parameter(:checksum) + result = checksum.checktype + if result =~ /^\{(\w+)\}.+/ + return $1.to_sym + else + return result + end + else + return :md5 + end end - def is_to_s(is) - string_as_checksum(is) + # If content was specified, return that; else try to return the source content; + # else, return nil. + def actual_content + if defined?(@actual_content) and @actual_content + return @actual_content + end + + if s = resource.parameter(:source) + return s.content + end + return nil end def content self.should || (s = resource.parameter(:source) and s.content) end # Override this method to provide diffs if asked for. # Also, fix #872: when content is used, and replace is true, the file # should be insync when it exists def insync?(is) if resource.should_be_file? return false if is == :absent else return true end return true if ! @resource.replace? if self.should return super elsif source = resource.parameter(:source) fail "Got a remote source with no checksum" unless source.checksum - unless sum_method = sumtype(source.checksum) - fail "Could not extract checksum type from source checksum '%s'" % source.checksum - end - - newsum = "{%s}" % sum_method + send(sum_method, is) - result = (newsum == source.checksum) + result = (is == source.checksum) else # We've got no content specified, and no source from which to # get content. return true end if ! result and Puppet[:show_diff] - string_file_diff(@resource[:path], content) + string_file_diff(@resource[:path], actual_content) end return result end def retrieve return :absent unless stat = @resource.stat # Don't even try to manage the content on directories or links return nil if stat.ftype == "directory" begin - return File.read(@resource[:path]) + return "{#{checksum_type}}" + send(checksum_type.to_s + "_file", resource[:path]) rescue => detail raise Puppet::Error, "Could not read %s: %s" % [@resource.title, detail] end end # Make sure we're also managing the checksum property. def should=(value) - super @resource.newattr(:checksum) unless @resource.parameter(:checksum) + super end # Just write our content out to disk. def sync return_event = @resource.stat ? :file_changed : :file_created # We're safe not testing for the 'source' if there's no 'should' # because we wouldn't have gotten this far if there weren't at least # one valid value somewhere. - content = self.should || resource.parameter(:source).content - @resource.write(content, :content) + @resource.write(actual_content, :content) return return_event end end end diff --git a/lib/puppet/type/file/ensure.rb b/lib/puppet/type/file/ensure.rb index 7466c5e3a..5c4d98d4b 100755 --- a/lib/puppet/type/file/ensure.rb +++ b/lib/puppet/type/file/ensure.rb @@ -1,178 +1,186 @@ module Puppet Puppet::Type.type(:file).ensurable do require 'etc' desc "Whether to create files that don't currently exist. Possible values are *absent*, *present*, *file*, and *directory*. Specifying ``present`` will match any form of file existence, and if the file is missing will create an empty file. Specifying ``absent`` will delete the file (and directory if recurse => true). Anything other than those values will be considered to be a symlink. For instance, the following text creates a link:: # Useful on solaris file { \"/etc/inetd.conf\": ensure => \"/etc/inet/inetd.conf\" } You can make relative links:: # Useful on solaris file { \"/etc/inetd.conf\": ensure => \"inet/inetd.conf\" } If you need to make a relative link to a file named the same as one of the valid values, you must prefix it with ``./`` or something similar. You can also make recursive symlinks, which will create a directory structure that maps to the target directory, with directories corresponding to each directory and links corresponding to each file." # Most 'ensure' properties have a default, but with files we, um, don't. nodefault newvalue(:absent) do File.unlink(@resource[:path]) end aliasvalue(:false, :absent) newvalue(:file) do # Make sure we're not managing the content some other way if property = (@resource.property(:content) || @resource.property(:source)) property.sync else @resource.write("", :ensure) mode = @resource.should(:mode) end return :file_created end #aliasvalue(:present, :file) newvalue(:present) do # Make a file if they want something, but this will match almost # anything. set_file end newvalue(:directory) do mode = @resource.should(:mode) parent = File.dirname(@resource[:path]) unless FileTest.exists? parent raise Puppet::Error, "Cannot create %s; parent directory %s does not exist" % [@resource[:path], parent] end if mode Puppet::Util.withumask(000) do Dir.mkdir(@resource[:path],mode) end else Dir.mkdir(@resource[:path]) end @resource.send(:property_fix) @resource.setchecksum return :directory_created end newvalue(:link) do if property = @resource.property(:target) property.retrieve return property.mklink else self.fail "Cannot create a symlink without a target" end end # Symlinks. newvalue(/./) do # This code never gets executed. We need the regex to support # specifying it, but the work is done in the 'symlink' code block. end munge do |value| value = super(value) # It doesn't make sense to try to manage links unless, well, # we're managing links. resource[:links] = :manage if value == :link return value if value.is_a? Symbol @resource[:target] = value resource[:links] = :manage return :link end def change_to_s(currentvalue, newvalue) - if property = @resource.property(:content) and content = property.retrieve and ! property.insync?(content) - return property.change_to_s(content, property.should) + return super unless newvalue.to_s == "file" + + return super unless property = @resource.property(:content) + + # We know that content is out of sync if we're here, because + # it's essentially equivalent to 'ensure' in the transaction. + if source = @resource.parameter(:source) + should = source.checksum else - super(currentvalue, newvalue) + should = property.should end + + return property.change_to_s(property.retrieve, should) end # Check that we can actually create anything def check basedir = File.dirname(@resource[:path]) if ! FileTest.exists?(basedir) raise Puppet::Error, "Can not create %s; parent directory does not exist" % @resource.title elsif ! FileTest.directory?(basedir) raise Puppet::Error, "Can not create %s; %s is not a directory" % [@resource.title, dirname] end end # We have to treat :present specially, because it works with any # type of file. def insync?(currentvalue) unless currentvalue == :absent or resource.replace? return true end if self.should == :present if currentvalue.nil? or currentvalue == :absent return false else return true end else return super(currentvalue) end end def retrieve if stat = @resource.stat(false) return stat.ftype.intern else if self.should == :false return :false else return :absent end end end def sync expire @resource.remove_existing(self.should) if self.should == :absent return :file_removed end event = super return event end end end diff --git a/spec/unit/type/file/content.rb b/spec/unit/type/file/content.rb index 212bb2fdb..fd225fa17 100755 --- a/spec/unit/type/file/content.rb +++ b/spec/unit/type/file/content.rb @@ -1,194 +1,253 @@ #!/usr/bin/env ruby Dir.chdir(File.dirname(__FILE__)) { (s = lambda { |f| File.exist?(f) ? require(f) : Dir.chdir("..") { s.call(f) } }).call("spec/spec_helper.rb") } content = Puppet::Type.type(:file).attrclass(:content) describe content do before do # Wow that's a messy interface to the resource. @resource = stub 'resource', :[] => nil, :[]= => nil, :property => nil, :newattr => nil, :parameter => nil end it "should be a subclass of Property" do content.superclass.must == Puppet::Property end + describe "when determining the checksum type" do + it "should use the type specified in the source checksum if a source is set" do + source = mock 'source' + source.expects(:checksum).returns "{litemd5}eh" + + @resource.expects(:parameter).with(:source).returns source + + @content = content.new(:resource => @resource) + @content.checksum_type.should == :litemd5 + end + + it "should use the type specified by the checksum parameter if no source is set" do + checksum = mock 'checksum' + checksum.expects(:checktype).returns :litemd5 + + @resource.expects(:parameter).with(:source).returns nil + @resource.expects(:parameter).with(:checksum).returns checksum + + @content = content.new(:resource => @resource) + @content.checksum_type.should == :litemd5 + end + + it "should only return the checksum type from the checksum parameter if the parameter returns a whole checksum" do + checksum = mock 'checksum' + checksum.expects(:checktype).returns "{md5}something" + + @resource.expects(:parameter).with(:source).returns nil + @resource.expects(:parameter).with(:checksum).returns checksum + + @content = content.new(:resource => @resource) + @content.checksum_type.should == :md5 + end + + it "should use md5 if neither a source nor a checksum parameter is available" do + @content = content.new(:resource => @resource) + @content.checksum_type.should == :md5 + end + end + + describe "when determining the actual content to write" do + it "should use the set content if available" do + @content = content.new(:resource => @resource) + @content.should = "ehness" + @content.actual_content.should == "ehness" + end + + it "should use the content from the source if the source is set" do + source = mock 'source' + source.expects(:content).returns "scont" + + @resource.expects(:parameter).with(:source).returns source + + @content = content.new(:resource => @resource) + @content.actual_content.should == "scont" + end + + it "should return nil if no source is available and no content is set" do + @content = content.new(:resource => @resource) + @content.actual_content.should be_nil + end + end + + describe "when setting the desired content" do + it "should make the actual content available via an attribute" do + @content = content.new(:resource => @resource) + @content.stubs(:checksum_type).returns "md5" + @content.should = "this is some content" + + @content.actual_content.should == "this is some content" + end + + it "should store the checksum as the desired content" do + @content = content.new(:resource => @resource) + digest = Digest::MD5.hexdigest("this is some content") + + @content.stubs(:checksum_type).returns "md5" + @content.should = "this is some content" + + @content.should.must == "{md5}#{digest}" + end + + it "should not checksum 'absent'" do + @content = content.new(:resource => @resource) + @content.should = :absent + + @content.should.must == :absent + end + end + describe "when retrieving the current content" do it "should return :absent if the file does not exist" do @content = content.new(:resource => @resource) @resource.expects(:stat).returns nil @content.retrieve.should == :absent end - it "should not manage content on non-files" do - pending "Haven't decided how this should behave" - + it "should not manage content on directories" do @content = content.new(:resource => @resource) stat = mock 'stat', :ftype => "directory" @resource.expects(:stat).returns stat @content.retrieve.should be_nil end - it "should return the current content of the file if it exists and is a normal file" do + it "should return the checksum of the file if it exists and is a normal file" do @content = content.new(:resource => @resource) + @content.stubs(:checksum_type).returns "md5" stat = mock 'stat', :ftype => "file" @resource.expects(:stat).returns stat @resource.expects(:[]).with(:path).returns "/my/file" - File.expects(:read).with("/my/file").returns "some content" - @content.retrieve.should == "some content" + @content.expects(:md5_file).with("/my/file").returns "mysum" + + @content.retrieve.should == "{md5}mysum" end end describe "when testing whether the content is in sync" do before do @resource.stubs(:[]).with(:ensure).returns :file @resource.stubs(:replace?).returns true @resource.stubs(:should_be_file?).returns true @content = content.new(:resource => @resource) - @content.should = "something" + @content.stubs(:checksum_type).returns "md5" end it "should return true if the resource shouldn't be a regular file" do @resource.expects(:should_be_file?).returns false @content.must be_insync("whatever") end it "should return false if the current content is :absent" do @content.should_not be_insync(:absent) end it "should return false if the file should be a file but is not present" do @resource.expects(:should_be_file?).returns true @content.should_not be_insync(:absent) end describe "and the file exists" do before do @resource.stubs(:stat).returns mock("stat") end it "should return false if the current contents are different from the desired content" do @content.should = "some content" @content.should_not be_insync("other content") end - it "should return true if the current contents are the same as the desired content" do + it "should return true if the sum for the current contents is the same as the sum for the desired content" do @content.should = "some content" - @content.must be_insync("some content") + @content.must be_insync("{md5}" + Digest::MD5.hexdigest("some content")) end describe "and the content is specified via a remote source" do before do @metadata = stub 'metadata' @source = stub 'source', :metadata => @metadata @resource.stubs(:parameter).with(:source).returns @source - - @content.should = nil end it "should use checksums to compare remote content, rather than downloading the content" do - @content.expects(:md5).with("some content").returns "whatever" @source.stubs(:checksum).returns "{md5}whatever" - @content.insync?("some content") + @content.insync?("{md5}eh") end it "should return false if the current content is different from the remote content" do @source.stubs(:checksum).returns "{md5}whatever" @content.should_not be_insync("some content") end it "should return true if the current content is the same as the remote content" do - sum = @content.md5("some content") - @source.stubs(:checksum).returns("{md5}%s" % sum) + @source.stubs(:checksum).returns("{md5}something") - @content.must be_insync("some content") + @content.must be_insync("{md5}something") end end end describe "and :replace is false" do before do @resource.stubs(:replace?).returns false end it "should be insync if the file exists and the content is different" do @resource.stubs(:stat).returns mock('stat') @content.must be_insync("whatever") end it "should be insync if the file exists and the content is right" do @resource.stubs(:stat).returns mock('stat') @content.must be_insync("something") end it "should not be insync if the file does not exist" do @content.should_not be_insync(:absent) end end end describe "when changing the content" do before do @content = content.new(:resource => @resource) + @content.should = "some content" @resource.stubs(:[]).with(:path).returns "/boo" + @resource.stubs(:stat).returns "eh" end it "should use the file's :write method to write the content" do - pending "not switched from :source yet" - @resource.expects(:write).with("foobar", :content, 123) + @resource.expects(:write).with("some content", :content) @content.sync end it "should return :file_changed if the file already existed" do - pending "not switched from :source yet" + @resource.expects(:stat).returns "something" @resource.stubs(:write) - FileTest.expects(:exist?).with("/boo").returns true @content.sync.should == :file_changed end - it "should return :file_created if the file already existed" do - pending "not switched from :source yet" + it "should return :file_created if the file did not exist" do + @resource.expects(:stat).returns nil @resource.stubs(:write) - FileTest.expects(:exist?).with("/boo").returns false @content.sync.should == :file_created end end - - describe "when logging changes" do - before do - @resource = stub 'resource', :line => "foo", :file => "bar", :replace? => true - @resource.stubs(:[]).returns "foo" - @resource.stubs(:[]).with(:path).returns "/my/file" - @content = content.new :resource => @resource - end - - it "should not include current contents" do - @content.change_to_s("current_content", "desired").should_not be_include("current_content") - end - - it "should not include desired contents" do - @content.change_to_s("current", "desired_content").should_not be_include("desired_content") - end - - it "should not include the content when converting current content to a string" do - @content.is_to_s("my_content").should_not be_include("my_content") - end - - it "should not include the content when converting desired content to a string" do - @content.should_to_s("my_content").should_not be_include("my_content") - end - end end