File: file.rb

package info (click to toggle)
puppet-agent 7.23.0-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 19,092 kB
  • sloc: ruby: 245,074; sh: 456; makefile: 38; xml: 33
file content (262 lines) | stat: -rw-r--r-- 11,586 bytes parent folder | download
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
require_relative '../../../puppet/indirector/code'
require_relative '../../../puppet/file_bucket/file'
require_relative '../../../puppet/util/checksums'
require_relative '../../../puppet/util/diff'
require 'fileutils'

module Puppet::FileBucketFile
  class File < Puppet::Indirector::Code
    include Puppet::Util::Checksums
    include Puppet::Util::Diff

    desc "Store files in a directory set based on their checksums."

    def find(request)
      request.options[:bucket_path] ||= Puppet[:bucketdir]
      # If filebucket mode is 'list'
      if request.options[:list_all]
        return nil unless ::File.exist?(request.options[:bucket_path])
        return list(request)
      end
      checksum, files_original_path = request_to_checksum_and_path(request)
      contents_file = path_for(request.options[:bucket_path], checksum, 'contents')
      paths_file = path_for(request.options[:bucket_path], checksum, 'paths')

      if Puppet::FileSystem.exist?(contents_file) && matches(paths_file, files_original_path)
        if request.options[:diff_with]
          other_contents_file = path_for(request.options[:bucket_path], request.options[:diff_with], 'contents')
          raise _("could not find diff_with %{diff}") % { diff: request.options[:diff_with] } unless Puppet::FileSystem.exist?(other_contents_file)
          raise _("Unable to diff on this platform") unless Puppet[:diff] != ""
          return diff(Puppet::FileSystem.path_string(contents_file), Puppet::FileSystem.path_string(other_contents_file))
        else
          #TRANSLATORS "FileBucket" should not be translated
          Puppet.info _("FileBucket read %{checksum}") % { checksum: checksum }
          model.new(Puppet::FileSystem.binread(contents_file))
        end
      else
        nil
      end
    end

    def list(request)
      if request.remote?
        raise Puppet::Error, _("Listing remote file buckets is not allowed")
      end

      fromdate = request.options[:fromdate] || "0:0:0 1-1-1970"
      todate = request.options[:todate] || Time.now.strftime("%F %T")
      begin
        to = Time.parse(todate)
      rescue ArgumentError
        raise Puppet::Error, _("Error while parsing 'todate'")
      end
      begin
        from = Time.parse(fromdate)
      rescue ArgumentError
        raise Puppet::Error, _("Error while parsing 'fromdate'")
      end
      # Setting hash's default value to [], needed by the following loop
      bucket = Hash.new {[]}
      msg = ""
      # Get all files with mtime between 'from' and 'to'
      Pathname.new(request.options[:bucket_path]).find { |item|
        if item.file? and item.basename.to_s == "paths"
          filenames = item.read.strip.split("\n")
          filestat = Time.parse(item.stat.mtime.to_s)
          if from <= filestat and filestat <= to
            filenames.each do |filename|
              bucket[filename] += [[ item.stat.mtime , item.parent.basename ]]
            end
          end
        end
      }
      # Sort the results
      bucket.each { |filename, contents|
        contents.sort_by! do |item|
          # NOTE: Ruby 2.4 may reshuffle item order even if the keys in sequence are sorted already
          item[0]
        end
      }
      # Build the output message. Sorted by names then by dates
      bucket.sort.each { |filename,contents|
        contents.each { |mtime, chksum|
          date = mtime.strftime("%F %T")
          msg += "#{chksum} #{date} #{filename}\n"
        }
      }
      return model.new(msg)
    end

    def head(request)
      checksum, files_original_path = request_to_checksum_and_path(request)
      contents_file = path_for(request.options[:bucket_path], checksum, 'contents')
      paths_file = path_for(request.options[:bucket_path], checksum, 'paths')

      Puppet::FileSystem.exist?(contents_file) && matches(paths_file, files_original_path)
    end

    def save(request)
      instance = request.instance
      _, files_original_path = request_to_checksum_and_path(request)
      contents_file = path_for(instance.bucket_path, instance.checksum_data, 'contents')
      paths_file = path_for(instance.bucket_path, instance.checksum_data, 'paths')

      save_to_disk(instance, files_original_path, contents_file, paths_file)

      # don't echo the request content back to the agent
      model.new('')
    end

    def validate_key(request)
      # There are no ACLs on filebucket files so validating key is not important
    end

    private

    # @param paths_file [Object] Opaque file path
    # @param files_original_path [String]
    #
    def matches(paths_file, files_original_path)
      # Puppet will have already written the paths_file in the systems encoding
      # given its possible that request.options[:bucket_path] or Puppet[:bucketdir]
      # contained characters in an encoding that are not represented the
      # same way when the bytes are decoded as UTF-8, continue using system encoding
      Puppet::FileSystem.open(paths_file, 0640, 'a+:external') do |f|
        path_match(f, files_original_path)
      end
    end

    def path_match(file_handle, files_original_path)
      return true unless files_original_path # if no path was provided, it's a match
      file_handle.rewind
      file_handle.each_line do |line|
        return true if line.chomp == files_original_path
      end
      return false
    end

    # @param bucket_file [Puppet::FileBucket::File] IO object representing
    #   content to back up
    # @param files_original_path [String] Path to original source file on disk
    # @param contents_file [Pathname] Opaque file path to intended backup
    #   location
    # @param paths_file [Pathname] Opaque file path to file containing source
    #   file paths on disk
    # @return [void]
    # @raise [Puppet::FileBucket::BucketError] on possible sum collision between
    #   existing and new backup
    # @api private
    def save_to_disk(bucket_file, files_original_path, contents_file, paths_file)
      Puppet::Util.withumask(0007) do
        unless Puppet::FileSystem.dir_exist?(paths_file)
          Puppet::FileSystem.dir_mkpath(paths_file)
        end

        # Puppet will have already written the paths_file in the systems encoding
        # given its possible that request.options[:bucket_path] or Puppet[:bucketdir]
        # contained characters in an encoding that are not represented the
        # same way when the bytes are decoded as UTF-8, continue using system encoding
        Puppet::FileSystem.exclusive_open(paths_file, 0640, 'a+:external') do |f|
          if Puppet::FileSystem.exist?(contents_file)
            if verify_identical_file(contents_file, bucket_file)
              #TRANSLATORS "FileBucket" should not be translated
              Puppet.info _("FileBucket got a duplicate file %{file_checksum}") % { file_checksum: bucket_file.checksum }
              # Don't touch the contents file on Windows, since we can't update the
              # mtime of read-only files there.
              if !Puppet::Util::Platform.windows?
                Puppet::FileSystem.touch(contents_file)
              end
            elsif contents_file_matches_checksum?(contents_file, bucket_file.checksum_data, bucket_file.checksum_type)
              # If the contents or sizes don't match, but the checksum does,
              # then we've found a conflict (potential hash collision).
              # Unlikely, but quite bad. Don't remove the file in case it's
              # needed, but ask the user to validate.
              # Note: Don't print the full path to the bucket file in the
              # exception to avoid disclosing file system layout on server.
              #TRANSLATORS "FileBucket" should not be translated
              Puppet.err(_("Unable to verify existing FileBucket backup at '%{path}'.") % { path: contents_file.to_path })
              raise Puppet::FileBucket::BucketError, _("Existing backup and new file have different content but same checksum, %{value}. Verify existing backup and remove if incorrect.") %
                { value: bucket_file.checksum }
            else
              # PUP-1334 If the contents_file exists but does not match its
              # checksum, our backup has been corrupted. Warn about overwriting
              # it, and proceed with new backup.
              Puppet.warning(_("Existing backup does not match its expected sum, %{sum}. Overwriting corrupted backup.") % { sum: bucket_file.checksum })
              copy_bucket_file_to_contents_file(contents_file, bucket_file)
            end
          else
            copy_bucket_file_to_contents_file(contents_file, bucket_file)
          end

          unless path_match(f, files_original_path)
            f.seek(0, IO::SEEK_END)
            f.puts(files_original_path)
          end
        end
      end
    end

    def request_to_checksum_and_path(request)
      checksum_type, checksum, path = request.key.split(/\//, 3)
      if path == '' # Treat "md5/<checksum>/" like "md5/<checksum>"
        path = nil
      end
      raise ArgumentError, _("Unsupported checksum type %{checksum_type}") % { checksum_type: checksum_type.inspect } if checksum_type != Puppet[:digest_algorithm]
      expected = method(checksum_type + "_hex_length").call
      raise _("Invalid checksum %{checksum}") % { checksum: checksum.inspect } if checksum !~ /^[0-9a-f]{#{expected}}$/
      [checksum, path]
    end

    # @return [Object] Opaque path as constructed by the Puppet::FileSystem
    #
    def path_for(bucket_path, digest, subfile = nil)
      bucket_path ||= Puppet[:bucketdir]

      dir     = ::File.join(digest[0..7].split(""))
      basedir = ::File.join(bucket_path, dir, digest)

      Puppet::FileSystem.pathname(subfile ? ::File.join(basedir, subfile) : basedir)
    end

    # @param contents_file [Pathname] Opaque file path to intended backup
    #   location
    # @param bucket_file [Puppet::FileBucket::File] IO object representing
    #   content to back up
    # @return [Boolean] whether the data in contents_file is of the same size
    #   and content as that in the bucket_file
    # @api private
    def verify_identical_file(contents_file, bucket_file)
      (bucket_file.to_binary.bytesize == Puppet::FileSystem.size(contents_file)) &&
        (bucket_file.stream() {|s| Puppet::FileSystem.compare_stream(contents_file, s) })
    end

    # @param contents_file [Pathname] Opaque file path to intended backup
    #   location
    # @param expected_checksum_data [String] expected value of checksum of type
    #   checksum_type
    # @param checksum_type [String] type of check sum of checksum_data, ie "md5"
    # @return [Boolean] whether the checksum of the contents_file matches the
    #   supplied checksum
    # @api private
    def contents_file_matches_checksum?(contents_file, expected_checksum_data, checksum_type)
      contents_file_checksum_data = Puppet::Util::Checksums.method(:"#{checksum_type}_file").call(contents_file.to_path)
      contents_file_checksum_data == expected_checksum_data
    end

    # @param contents_file [Pathname] Opaque file path to intended backup
    #   location
    # @param bucket_file [Puppet::FileBucket::File] IO object representing
    #   content to back up
    # @return [void]
    # @api private
    def copy_bucket_file_to_contents_file(contents_file, bucket_file)
      Puppet::FileSystem.replace_file(contents_file, 0440) do |of|
        # PUP-1044 writes all of the contents
        bucket_file.stream() do |src|
          FileUtils.copy_stream(src, of)
        end
      end
    end

  end
end