diff --git a/lib/puppet/type/file/content.rb b/lib/puppet/type/file/content.rb index 74b380f55..0d0bb5571 100755 --- a/lib/puppet/type/file/content.rb +++ b/lib/puppet/type/file/content.rb @@ -1,210 +1,210 @@ require 'net/http' require 'uri' require 'tempfile' require 'puppet/util/checksums' require 'puppet/network/http/api/v1' require 'puppet/network/http/compression' module Puppet Puppet::Type.type(:file).newproperty(:content) do include Puppet::Util::Diff include Puppet::Util::Checksums include Puppet::Network::HTTP::API::V1 include Puppet::Network::HTTP::Compression.module attr_reader :actual_content 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:." # Store a checksum as the value, rather than the actual content. # Simplifies everything. munge do |value| if value == :absent value elsif checksum?(value) # XXX This is potentially dangerous because it means users can't write a file whose # entire contents are a plain checksum value else @actual_content = value resource.parameter(:checksum).sum(value) end end # Checksums need to invert how changes are printed. def change_to_s(currentvalue, newvalue) # Our "new" checksum value is provided by the source. if source = resource.parameter(:source) and tmp = source.checksum newvalue = tmp end if currentvalue == :absent return "defined content as '#{newvalue}'" elsif newvalue == :absent return "undefined content from '#{currentvalue}'" else return "content changed '#{currentvalue}' to '#{newvalue}'" end end def checksum_type if source = resource.parameter(:source) result = source.checksum else checksum = resource.parameter(:checksum) result = resource[:checksum] end if result =~ /^\{(\w+)\}.+/ return $1.to_sym else return result end end def length (actual_content and actual_content.length) || 0 end def content self.should 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? return true unless self.should result = super if ! result and Puppet[:show_diff] write_temporarily do |path| print diff(@resource[:path], path) end end result end def retrieve return :absent unless stat = @resource.stat ftype = stat.ftype # Don't even try to manage the content on directories or links return nil if ["directory","link"].include?(ftype) begin resource.parameter(:checksum).sum_file(resource[:path]) rescue => detail raise Puppet::Error, "Could not read #{ftype} #{@resource.title}: #{detail}" end end # Make sure we're also managing the checksum property. def should=(value) @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. @resource.write(:content) return_event end def write_temporarily tempfile = Tempfile.new("puppet-file") tempfile.open write(tempfile) tempfile.close yield tempfile.path tempfile.delete end def write(file) resource.parameter(:checksum).sum_stream { |sum| each_chunk_from(actual_content || resource.parameter(:source)) { |chunk| sum << chunk file.print chunk } } end def each_chunk_from(source_or_content) if source_or_content.is_a?(String) yield source_or_content elsif source_or_content.nil? yield read_file_from_filebucket elsif source_or_content.local? chunk_file_from_disk(source_or_content) { |chunk| yield chunk } else chunk_file_from_source(source_or_content) { |chunk| yield chunk } end end private def chunk_file_from_disk(source_or_content) File.open(source_or_content.full_path, "r") do |src| while chunk = src.read(8192) yield chunk end end end def chunk_file_from_source(source_or_content) - request = Puppet::Indirector::Request.new(:file_content, :find, source_or_content.full_path) + request = Puppet::Indirector::Request.new(:file_content, :find, source_or_content.full_path.sub(/^\//,'')) connection = Puppet::Network::HttpPool.http_instance(source_or_content.server, source_or_content.port) connection.request_get(indirection2uri(request), add_accept_encoding({"Accept" => "raw"})) do |response| case response.code when "404"; nil when /^2/; uncompress(response) { |uncompressor| response.read_body { |chunk| yield uncompressor.uncompress(chunk) } } else # Raise the http error if we didn't get a 'success' of some kind. message = "Error #{response.code} on SERVER: #{(response.body||'').empty? ? response.message : uncompress_body(response)}" raise Net::HTTPError.new(message, response) end end end def read_file_from_filebucket raise "Could not get filebucket from file" unless dipper = resource.bucket sum = should.sub(/\{\w+\}/, '') dipper.getfile(sum) rescue => detail fail "Could not retrieve content for #{should} from filebucket: #{detail}" end end end diff --git a/spec/unit/type/file/content_spec.rb b/spec/unit/type/file/content_spec.rb index e4b9f9bc6..a01a14a59 100755 --- a/spec/unit/type/file/content_spec.rb +++ b/spec/unit/type/file/content_spec.rb @@ -1,460 +1,460 @@ #!/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 @resource = Puppet::Type.type(:file).new :path => "/foo/bar" 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 @resource[:source] = "/foo" @resource.parameter(:source).expects(:checksum).returns "{md5lite}eh" @content = content.new(:resource => @resource) @content.checksum_type.should == :md5lite end it "should use the type specified by the checksum parameter if no source is set" do @resource[:checksum] = :md5lite @content = content.new(:resource => @resource) @content.checksum_type.should == :md5lite 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 not use the content from the source if the source is set" do source = mock 'source' @resource.expects(:parameter).never.with(:source).returns source @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 it "should accept a checksum as the desired content" do @content = content.new(:resource => @resource) digest = Digest::MD5.hexdigest("this is some content") string = "{md5}#{digest}" @content.should = string @content.should.must == string 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 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 not manage content on links" do @content = content.new(:resource => @resource) stat = mock 'stat', :ftype => "link" @resource.expects(:stat).returns stat @content.retrieve.should be_nil end it "should always return the checksum as a string" do @content = content.new(:resource => @resource) @resource[:checksum] = :mtime stat = mock 'stat', :ftype => "file" @resource.expects(:stat).returns stat time = Time.now @resource.parameter(:checksum).expects(:mtime_file).with(@resource[:path]).returns time @content.retrieve.should == "{mtime}#{time}" end it "should return the checksum of the file if it exists and is a normal file" do @content = content.new(:resource => @resource) stat = mock 'stat', :ftype => "file" @resource.expects(:stat).returns stat @resource.parameter(:checksum).expects(:md5_file).with(@resource[:path]).returns "mysum" @content.retrieve.should == "{md5}mysum" end end describe "when testing whether the content is in sync" do before do @resource[:ensure] = :file @content = content.new(:resource => @resource) 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 sum for the current contents is the same as the sum for the desired content" do @content.should = "some content" @content.must be_insync("{md5}" + Digest::MD5.hexdigest("some content")) end describe "and Puppet[:show_diff] is set" do before do Puppet[:show_diff] = true end it "should display a diff if the current contents are different from the desired content" do @content.should = "some content" @content.expects(:diff).returns("my diff").once @content.expects(:print).with("my diff").once @content.insync?("other content") end it "should not display a diff if the sum for the current contents is the same as the sum for the desired content" do @content.should = "some content" @content.expects(:diff).never @content.insync?("{md5}" + Digest::MD5.hexdigest("some content")) 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 @resource.expects(:write).with(:content) @content.sync end it "should return :file_changed if the file already existed" do @resource.expects(:stat).returns "something" @resource.stubs(:write) @content.sync.should == :file_changed end it "should return :file_created if the file did not exist" do @resource.expects(:stat).returns nil @resource.stubs(:write) @content.sync.should == :file_created end end describe "when writing" do before do @content = content.new(:resource => @resource) @fh = stub_everything end it "should attempt to read from the filebucket if no actual content nor source exists" do @content.should = "{md5}foo" @content.resource.bucket.class.any_instance.stubs(:getfile).returns "foo" @content.write(@fh) end describe "from actual content" do before(:each) do @content.stubs(:actual_content).returns("this is content") end it "should write to the given file handle" do @fh.expects(:print).with("this is content") @content.write(@fh) end it "should return the current checksum value" do @resource.parameter(:checksum).expects(:sum_stream).returns "checksum" @content.write(@fh).should == "checksum" end end describe "from a file bucket" do it "should fail if a file bucket cannot be retrieved" do @content.should = "{md5}foo" @content.resource.expects(:bucket).returns nil lambda { @content.write(@fh) }.should raise_error(Puppet::Error) end it "should fail if the file bucket cannot find any content" do @content.should = "{md5}foo" bucket = stub 'bucket' @content.resource.expects(:bucket).returns bucket bucket.expects(:getfile).with("foo").raises "foobar" lambda { @content.write(@fh) }.should raise_error(Puppet::Error) end it "should write the returned content to the file" do @content.should = "{md5}foo" bucket = stub 'bucket' @content.resource.expects(:bucket).returns bucket bucket.expects(:getfile).with("foo").returns "mycontent" @fh.expects(:print).with("mycontent") @content.write(@fh) end end describe "from local source" do before(:each) do @content.stubs(:actual_content).returns(nil) @source = stub_everything 'source', :local? => true, :full_path => "/path/to/source" @resource.stubs(:parameter).with(:source).returns @source @sum = stub_everything 'sum' @resource.stubs(:parameter).with(:checksum).returns(@sum) @digest = stub_everything 'digest' @sum.stubs(:sum_stream).yields(@digest) @file = stub_everything 'file' File.stubs(:open).yields(@file) @file.stubs(:read).with(8192).returns("chunk1").then.returns("chunk2").then.returns(nil) end it "should open the local file" do File.expects(:open).with("/path/to/source", "r") @content.write(@fh) end it "should read the local file by chunks" do @file.expects(:read).with(8192).returns("chunk1").then.returns(nil) @content.write(@fh) end it "should write each chunk to the file" do @fh.expects(:print).with("chunk1").then.with("chunk2") @content.write(@fh) end it "should pass each chunk to the current sum stream" do @digest.expects(:<<).with("chunk1").then.with("chunk2") @content.write(@fh) end it "should return the checksum computed" do @sum.stubs(:sum_stream).yields(@digest).returns("checksum") @content.write(@fh).should == "checksum" end end describe "from remote source" do before(:each) do @response = stub_everything 'mock response', :code => "404" @conn = stub_everything 'connection' @conn.stubs(:request_get).yields(@response) Puppet::Network::HttpPool.stubs(:http_instance).returns @conn @content.stubs(:actual_content).returns(nil) @source = stub_everything 'source', :local? => false, :full_path => "/path/to/source", :server => "server", :port => 1234 @resource.stubs(:parameter).with(:source).returns @source @sum = stub_everything 'sum' @resource.stubs(:parameter).with(:checksum).returns(@sum) @digest = stub_everything 'digest' @sum.stubs(:sum_stream).yields(@digest) end it "should open a network connection to source server and port" do Puppet::Network::HttpPool.expects(:http_instance).with("server", 1234).returns @conn @content.write(@fh) end it "should send the correct indirection uri" do - @conn.expects(:request_get).with { |uri,headers| uri == "/production/file_content//path/to/source" }.yields(@response) + @conn.expects(:request_get).with { |uri,headers| uri == "/production/file_content/path/to/source" }.yields(@response) @content.write(@fh) end it "should return nil if source is not found" do @response.expects(:code).returns("404") @content.write(@fh).should == nil end it "should not write anything if source is not found" do @response.expects(:code).returns("404") @fh.expects(:print).never @content.write(@fh).should == nil end it "should raise an HTTP error in case of server error" do @response.expects(:code).returns("500") lambda { @content.write(@fh) }.should raise_error end it "should write content by chunks" do @response.expects(:code).returns("200") @response.expects(:read_body).multiple_yields("chunk1","chunk2") @fh.expects(:print).with("chunk1").then.with("chunk2") @content.write(@fh) end it "should pass each chunk to the current sum stream" do @response.expects(:code).returns("200") @response.expects(:read_body).multiple_yields("chunk1","chunk2") @digest.expects(:<<).with("chunk1").then.with("chunk2") @content.write(@fh) end it "should return the checksum computed" do @response.expects(:code).returns("200") @response.expects(:read_body).multiple_yields("chunk1","chunk2") @sum.expects(:sum_stream).yields(@digest).returns("checksum") @content.write(@fh).should == "checksum" end it "should get the current accept encoding header value" do @content.expects(:add_accept_encoding) @content.write(@fh) end it "should uncompress body on error" do @response.expects(:code).returns("500") @response.expects(:body).returns("compressed body") @content.expects(:uncompress_body).with(@response).returns("uncompressed") lambda { @content.write(@fh) }.should raise_error { |e| e.message =~ /uncompressed/ } end it "should uncompress chunk by chunk" do uncompressor = stub_everything 'uncompressor' @content.expects(:uncompress).with(@response).yields(uncompressor) @response.expects(:code).returns("200") @response.expects(:read_body).multiple_yields("chunk1","chunk2") uncompressor.expects(:uncompress).with("chunk1").then.with("chunk2") @content.write(@fh) end it "should write uncompressed chunks to the file" do uncompressor = stub_everything 'uncompressor' @content.expects(:uncompress).with(@response).yields(uncompressor) @response.expects(:code).returns("200") @response.expects(:read_body).multiple_yields("chunk1","chunk2") uncompressor.expects(:uncompress).with("chunk1").returns("uncompressed1") uncompressor.expects(:uncompress).with("chunk2").returns("uncompressed2") @fh.expects(:print).with("uncompressed1") @fh.expects(:print).with("uncompressed2") @content.write(@fh) end it "should pass each uncompressed chunk to the current sum stream" do uncompressor = stub_everything 'uncompressor' @content.expects(:uncompress).with(@response).yields(uncompressor) @response.expects(:code).returns("200") @response.expects(:read_body).multiple_yields("chunk1","chunk2") uncompressor.expects(:uncompress).with("chunk1").returns("uncompressed1") uncompressor.expects(:uncompress).with("chunk2").returns("uncompressed2") @digest.expects(:<<).with("uncompressed1").then.with("uncompressed2") @content.write(@fh) end end describe "from a filebucket" do end end end