summaryrefslogtreecommitdiffstats
path: root/lib/puppet/type/tidy.rb
blob: d9469aee46b2ce8aaf2612eefcf01e61b07661b0 (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
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
Puppet::Type.newtype(:tidy) do
    require 'puppet/file_serving/fileset'

    @doc = "Remove unwanted files based on specific criteria.  Multiple
        criteria are OR'd together, so a file that is too large but is not
        old enough will still get tidied.

        If you don't specify either 'age' or 'size', then all files will
        be removed.

        This resource type works by generating a file resource for every file
        that should be deleted and then letting that resource perform the
        actual deletion.
        "

    newparam(:path) do
        desc "The path to the file or directory to manage.  Must be fully
            qualified."
        isnamevar
    end

    newparam(:matches) do
        desc "One or more (shell type) file glob patterns, which restrict
            the list of files to be tidied to those whose basenames match
            at least one of the patterns specified. Multiple patterns can
            be specified using an array.

            Example::

                    tidy { \"/tmp\":
                        age => \"1w\",
                        recurse => false,
                        matches => [ \"[0-9]pub*.tmp\", \"*.temp\", \"tmpfile?\" ]
                    }

            This removes files from \/tmp if they are one week old or older,
            are not in a subdirectory and match one of the shell globs given.

            Note that the patterns are matched against the
            basename of each file -- that is, your glob patterns should not
            have any '/' characters in them, since you are only specifying
            against the last bit of the file."

        # Make sure we convert to an array.
        munge do |value|
            value = [value] unless value.is_a?(Array)
            value
        end

        # Does a given path match our glob patterns, if any?  Return true
        # if no patterns have been provided.
        def tidy?(path, stat)
            basename = File.basename(path)
            flags = File::FNM_DOTMATCH | File::FNM_PATHNAME
            return true if value.find {|pattern| File.fnmatch(pattern, basename, flags) }
            return false
        end
    end

    newparam(:backup) do
        desc "Whether tidied files should be backed up.  Any values are passed
            directly to the file resources used for actual file deletion, so use
            its backup documentation to determine valid values."
    end

    newparam(:age) do
        desc "Tidy files whose age is equal to or greater than
            the specified time.  You can choose seconds, minutes,
            hours, days, or weeks by specifying the first letter of any
            of those words (e.g., '1w').
        
            Specifying 0 will remove all files."

        @@ageconvertors = {
            :s => 1,
            :m => 60
        }

        @@ageconvertors[:h] = @@ageconvertors[:m] * 60
        @@ageconvertors[:d] = @@ageconvertors[:h] * 24
        @@ageconvertors[:w] = @@ageconvertors[:d] * 7

        def convert(unit, multi)
            if num = @@ageconvertors[unit]
                return num * multi
            else
                self.fail "Invalid age unit '%s'" % unit
            end
        end

        def tidy?(path, stat)
            # If the file's older than we allow, we should get rid of it.
            if (Time.now.to_i - stat.send(resource[:type]).to_i) > value
                return true 
            else
                return false
            end
        end

        munge do |age|
            unit = multi = nil
            case age
            when /^([0-9]+)(\w)\w*$/
                multi = Integer($1)
                unit = $2.downcase.intern
            when /^([0-9]+)$/
                multi = Integer($1)
                unit = :d
            else
                self.fail "Invalid tidy age %s" % age
            end

            convert(unit, multi)
        end
    end

    newparam(:size) do
        desc "Tidy files whose size is equal to or greater than
            the specified size.  Unqualified values are in kilobytes, but
            *b*, *k*, and *m* can be appended to specify *bytes*, *kilobytes*,
            and *megabytes*, respectively.  Only the first character is
            significant, so the full word can also be used."

        @@sizeconvertors = {
            :b => 0,
            :k => 1,
            :m => 2,
            :g => 3
        }

        def convert(unit, multi)
            if num = @@sizeconvertors[unit]
                result = multi
                num.times do result *= 1024 end
                return result
            else
                self.fail "Invalid size unit '%s'" % unit
            end
        end
        
        def tidy?(path, stat)
            if stat.size > value
                return true
            else
                return false
            end
        end
        
        munge do |size|
            case size
            when /^([0-9]+)(\w)\w*$/
                multi = Integer($1)
                unit = $2.downcase.intern
            when /^([0-9]+)$/
                multi = Integer($1)
                unit = :k
            else
                self.fail "Invalid tidy size %s" % age
            end

            convert(unit, multi)
        end
    end

    newparam(:type) do
        desc "Set the mechanism for determining age."
        
        newvalues(:atime, :mtime, :ctime)

        defaultto :atime
    end

    newparam(:recurse) do
        desc "If target is a directory, recursively descend
            into the directory looking for files to tidy."

        newvalues(:true, :false, :inf, /^[0-9]+$/)

        # Replace the validation so that we allow numbers in
        # addition to string representations of them.
        validate { |arg| }
        munge do |value|
            newval = super(value)
            case newval
            when :true, :inf; true
            when :false; false
            when Integer, Fixnum, Bignum; value
            when /^\d+$/; Integer(value)
            else
                raise ArgumentError, "Invalid recurse value %s" % value.inspect
            end
        end
    end

    newparam(:rmdirs, :boolean => true) do
        desc "Tidy directories in addition to files; that is, remove
            directories whose age is older than the specified criteria.
            This will only remove empty directories, so all contained
            files must also be tidied before a directory gets removed."

        newvalues :true, :false
    end
    
    # Erase PFile's validate method
    validate do
    end

    def self.instances
        []
    end

    @depthfirst = true

    def initialize(hash)
        super

        # only allow backing up into filebuckets
        unless self[:backup].is_a? Puppet::Network::Client.dipper
            self[:backup] = false
        end
    end
    
    # Make a file resource to remove a given file.
    def mkfile(path)
        # Force deletion, so directories actually get deleted.
        Puppet::Type.type(:file).new :path => path, :backup => self[:backup], :ensure => :absent, :force => true
    end

    def retrieve
        # Our ensure property knows how to retrieve everything for us.
        if obj = @parameters[:ensure] 
            return obj.retrieve
        else
            return {}
        end
    end
    
    # Hack things a bit so we only ever check the ensure property.
    def properties
        []
    end

    def eval_generate
        []
    end

    def generate
        return [] unless stat(self[:path])

        if self[:recurse]
            files = Puppet::FileServing::Fileset.new(self[:path], :recurse => self[:recurse]).files.collect do |f|
                f == "." ? self[:path] : File.join(self[:path], f)
            end
        else
            files = [self[:path]]
        end
        result = files.find_all { |path| tidy?(path) }.collect { |path| mkfile(path) }.each { |file| notice "Tidying %s" % file.ref }.sort { |a,b| b[:path] <=> a[:path] }

        # No need to worry about relationships if we don't have rmdirs; there won't be
        # any directories.
        return result unless rmdirs?

        # Now make sure that all directories require the files they contain, if all are available,
        # so that a directory is emptied before we try to remove it.
        files_by_name = result.inject({}) { |hash, file| hash[file[:path]] = file; hash }

        files_by_name.keys.sort { |a,b| b <=> b }.each do |path|
            dir = File.dirname(path)
            next unless resource = files_by_name[dir]
            if resource[:require] 
                resource[:require] << Puppet::Resource::Reference.new(:file, path)
            else
                resource[:require] = [Puppet::Resource::Reference.new(:file, path)]
            end
        end

        return result
    end

    # Does a given path match our glob patterns, if any?  Return true
    # if no patterns have been provided.
    def matches?(path)
        return true unless self[:matches]

        basename = File.basename(path)
        flags = File::FNM_DOTMATCH | File::FNM_PATHNAME
        if self[:matches].find {|pattern| File.fnmatch(pattern, basename, flags) }
            return true
        else
            debug "No specified patterns match %s, not tidying" % path
            return false
        end
    end

    # Should we remove the specified file?
    def tidy?(path)
        return false unless stat = self.stat(path)

        return false if stat.ftype == "directory" and ! rmdirs?

        # The 'matches' parameter isn't OR'ed with the other tests --
        # it's just used to reduce the list of files we can match.
        return false if param = parameter(:matches) and ! param.tidy?(path, stat)

        tested = false
        [:age, :size].each do |name|
            next unless param = parameter(name)
            tested = true
            return true if param.tidy?(path, stat)
        end

        # If they don't specify either, then the file should always be removed.
        return true unless tested
        return false
    end

    def stat(path)
        begin
            File.lstat(path)
        rescue Errno::ENOENT => error
            info "File does not exist"
            return nil
        rescue Errno::EACCES => error
            warning "Could not stat; permission denied"
            return nil
        end
    end
end