summaryrefslogtreecommitdiffstats
path: root/lib/puppet/type/file/content.rb
blob: 8d832a6d1ab53788242a6862a1bf0c1cef8a3e1a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
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 '%s'" % [newvalue]
            elsif newvalue == :absent
                return "undefined content from '%s'" % [currentvalue]
            else
                return "content changed '%s' to '%s'" % [currentvalue, 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
            return 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 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)
            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 %s on SERVER: %s" % [response.code, (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