summaryrefslogtreecommitdiffstats
path: root/lib/puppet/type/file/checksum.rb
blob: 76e27e55d920c11235a6f2d262e653ca86830aa1 (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
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
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
            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