Class: Chef::Provider::File

Inherits:
Chef::Provider show all
Includes:
Mixin::Checksum, Mixin::EnforceOwnershipAndPermissions, Mixin::ShellOut
Defined in:
lib/chef/provider/file.rb

Direct Known Subclasses

CookbookFile, Directory, RemoteFile, Template

Constant Summary

Constants included from Mixin::ShellOut

Mixin::ShellOut::DEPRECATED_OPTIONS

Instance Attribute Summary

Attributes inherited from Chef::Provider

#action, #current_resource, #new_resource, #run_context

Instance Method Summary collapse

Methods included from Mixin::ShellOut

#run_command_compatible_options, #shell_out, #shell_out!

Methods included from Mixin::Checksum

#checksum

Methods included from Mixin::EnforceOwnershipAndPermissions

#access_controls, #enforce_ownership_and_permissions

Methods inherited from Chef::Provider

#action_nothing, #cleanup_after_converge, #cookbook_name, #events, #initialize, #node, #process_resource_requirements, #requirements, #resource_collection, #run_action, #set_updated_status, #whyrun_mode?

Methods included from DSL::Recipe

#method_missing

Methods included from Mixin::ConvertToClassName

#convert_to_class_name, #convert_to_snake_case, #filename_to_qualified_string, #snake_case_basename

Constructor Details

This class inherits a constructor from Chef::Provider

Dynamic Method Handling

This class handles dynamic methods through the method_missing method in the class Chef::DSL::Recipe

Instance Method Details

#action_createObject



217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
# File 'lib/chef/provider/file.rb', line 217

def action_create
  if !::File.exists?(@new_resource.path)
    description = []
    desc = "create new file #{@new_resource.path}"
    desc << " with content checksum #{short_cksum(new_resource_content_checksum)}" if new_resource.content
    description << desc
    description << diff_current_from_content(@new_resource.content) 

    converge_by(description) do
      Chef::Log.info("entered create")
      ::File.open(@new_resource.path, "w+") {|f| f.write @new_resource.content }
      access_controls.set_all
      Chef::Log.info("#{@new_resource} created file #{@new_resource.path}")
      update_new_file_state
    end
  else
    set_content unless @new_resource.content.nil?
    set_all_access_controls
  end
end

#action_create_if_missingObject



248
249
250
251
252
253
254
# File 'lib/chef/provider/file.rb', line 248

def action_create_if_missing
  if ::File.exists?(@new_resource.path)
    Chef::Log.debug("#{@new_resource} exists at #{@new_resource.path} taking no action.")
  else
    action_create
  end
end

#action_deleteObject



256
257
258
259
260
261
262
263
264
# File 'lib/chef/provider/file.rb', line 256

def action_delete
  if ::File.exists?(@new_resource.path)
    converge_by("delete file #{@new_resource.path}") do 
      backup unless ::File.symlink?(@new_resource.path)
      ::File.delete(@new_resource.path)
      Chef::Log.info("#{@new_resource} deleted file at #{@new_resource.path}")
    end
  end
end

#action_touchObject



266
267
268
269
270
271
272
273
# File 'lib/chef/provider/file.rb', line 266

def action_touch
  action_create
  converge_by("update utime on file #{@new_resource.path}") do
    time = Time.now
    ::File.utime(time, time, @new_resource.path)
    Chef::Log.info("#{@new_resource} updated atime and mtime to #{time}")
  end
end

#backup(file = nil) ⇒ Object



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
# File 'lib/chef/provider/file.rb', line 275

def backup(file=nil)
  file ||= @new_resource.path
  if @new_resource.backup != false && @new_resource.backup > 0 && ::File.exist?(file)
    time = Time.now
    savetime = time.strftime("%Y%m%d%H%M%S")
    backup_filename = "#{@new_resource.path}.chef-#{savetime}"
    backup_filename = backup_filename.sub(/^([A-Za-z]:)/, "") #strip drive letter on Windows
    # if :file_backup_path is nil, we fallback to the old behavior of
    # keeping the backup in the same directory. We also need to to_s it
    # so we don't get a type error around implicit to_str conversions.
    prefix = Chef::Config[:file_backup_path].to_s
    backup_path = ::File.join(prefix, backup_filename)
    FileUtils.mkdir_p(::File.dirname(backup_path)) if Chef::Config[:file_backup_path]
    FileUtils.cp(file, backup_path, :preserve => true)
    Chef::Log.info("#{@new_resource} backed up to #{backup_path}")

    # Clean up after the number of backups
    slice_number = @new_resource.backup
    backup_files = Dir[::File.join(prefix, ".#{@new_resource.path}.chef-*")].sort { |a,b| b <=> a }
    if backup_files.length >= @new_resource.backup
      remainder = backup_files.slice(slice_number..-1)
      remainder.each do |backup_to_delete|
        FileUtils.rm(backup_to_delete)
        Chef::Log.info("#{@new_resource} removed backup at #{backup_to_delete}")
      end
    end
  end
end

#compare_contentObject

Compare the content of a file. Returns true if they are the same, false if they are not.



180
181
182
# File 'lib/chef/provider/file.rb', line 180

def compare_content
  checksum(@current_resource.path) == new_resource_content_checksum
end

#define_resource_requirementsObject



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
# File 'lib/chef/provider/file.rb', line 152

def define_resource_requirements
  # this must be evaluated before whyrun messages are printed
  access_controls.requires_changes?

  requirements.assert(:create, :create_if_missing, :touch) do |a|
    # Make sure the parent dir exists, or else fail.
    # for why run, print a message explaining the potential error.
    parent_directory = ::File.dirname(@new_resource.path)

    a.assertion { ::File.directory?(parent_directory) }
    a.failure_message(Chef::Exceptions::EnclosingDirectoryDoesNotExist, "Parent directory #{parent_directory} does not exist.")
    a.whyrun("Assuming directory #{parent_directory} would have been created")
  end

  # Make sure the file is deletable if it exists. Otherwise, fail.
  requirements.assert(:delete) do |a|
    a.assertion do
      if ::File.exists?(@new_resource.path) 
        ::File.writable?(@new_resource.path)
      else
        true
      end
    end
    a.failure_message(Chef::Exceptions::InsufficientPermissions,"File #{@new_resource.path} exists but is not writable so it cannot be deleted")
  end
end

#deploy_tempfileObject



304
305
306
307
308
309
310
311
312
313
314
# File 'lib/chef/provider/file.rb', line 304

def deploy_tempfile
  Tempfile.open(::File.basename(@new_resource.name)) do |tempfile|
    yield tempfile

    temp_res = Chef::Resource::CookbookFile.new(@new_resource.name)
    temp_res.path(tempfile.path)
    ac = Chef::FileAccessControl.new(temp_res, @new_resource, self)
    ac.set_all!
    FileUtils.mv(tempfile.path, @new_resource.path)
  end
end

#diff_current(temp_path) ⇒ Object



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
# File 'lib/chef/provider/file.rb', line 70

def diff_current(temp_path)
  suppress_resource_reporting = false

  return [ "(diff output suppressed by config)" ] if Chef::Config[:diff_disabled]
  return [ "(no temp file with new content, diff output suppressed)" ] unless ::File.exists?(temp_path)  # should never happen?

  # solaris does not support diff -N, so create tempfile to diff against if we are creating a new file
  target_path = if ::File.exists?(@current_resource.path)
                  @current_resource.path
                else
                  suppress_resource_reporting = true  # suppress big diffs going to resource reporting service
                  tempfile = Tempfile.new('chef-tempfile')
                  tempfile.path
                end

  diff_filesize_threshold = Chef::Config[:diff_filesize_threshold]
  diff_output_threshold = Chef::Config[:diff_output_threshold]

  if ::File.size(target_path) > diff_filesize_threshold || ::File.size(temp_path) > diff_filesize_threshold
    return [ "(file sizes exceed #{diff_filesize_threshold} bytes, diff output suppressed)" ]
  end

  # MacOSX(BSD?) diff will *sometimes* happily spit out nasty binary diffs
  return [ "(current file is binary, diff output suppressed)"] if is_binary?(target_path)
  return [ "(new content is binary, diff output suppressed)"] if is_binary?(temp_path)

  begin
    # -u: Unified diff format
    result = shell_out("diff -u #{target_path} #{temp_path}" )
  rescue Exception => e
    # Should *not* receive this, but in some circumstances it seems that 
    # an exception can be thrown even using shell_out instead of shell_out!
    return [ "Could not determine diff. Error: #{e.message}" ]
  end

  # diff will set a non-zero return code even when there's 
  # valid stdout results, if it encounters something unexpected
  # So as long as we have output, we'll show it.
  if not result.stdout.empty?
    if result.stdout.length > diff_output_threshold
      [ "(long diff of over #{diff_output_threshold} characters, diff output suppressed)" ]
    else
      val = result.stdout.split("\n")
      val.delete("\\ No newline at end of file")
      @new_resource.diff(val.join("\\n")) unless suppress_resource_reporting
      val
    end
  elsif not result.stderr.empty?
    [ "Could not determine diff. Error: #{result.stderr}" ]
  else
    [ "(no diff)" ]
  end
end

#diff_current_from_content(new_content) ⇒ Object



50
51
52
53
54
55
56
57
58
# File 'lib/chef/provider/file.rb', line 50

def diff_current_from_content(new_content)
  result = nil
  Tempfile.open("chef-diff") do |file| 
    file.write new_content
    file.close 
    result = diff_current file.path
  end
  result
end

#is_binary?(path) ⇒ Boolean

Returns:

  • (Boolean)


60
61
62
63
64
65
66
67
# File 'lib/chef/provider/file.rb', line 60

def is_binary?(path)
  ::File.open(path) do |file|

    buff = file.read(Chef::Config[:diff_filesize_threshold])
    buff = "" if buff.nil?
    return buff !~ /^[\r[:print:]]*$/
  end
end

#load_current_resourceObject



128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/chef/provider/file.rb', line 128

def load_current_resource
  # Every child should be specifying their own constructor, so this
  # should only be run in the file case.
  @current_resource ||= Chef::Resource::File.new(@new_resource.name)
  @new_resource.path.gsub!(/\\/, "/") # for Windows
  @current_resource.path(@new_resource.path)
  if !::File.directory?(@new_resource.path)
    if ::File.exist?(@new_resource.path)
      if @action != :create_if_missing  
        @current_resource.checksum(checksum(@new_resource.path))
      end
    end
  end
  setup_acl

  @current_resource
end

#set_all_access_controlsObject



238
239
240
241
242
243
244
245
246
# File 'lib/chef/provider/file.rb', line 238

def set_all_access_controls
  if access_controls.requires_changes?
    converge_by(access_controls.describe_changes) do 
      access_controls.set_all
      #Update file state with new access values
      update_new_file_state
    end
  end
end

#set_contentObject

Set the content of the file, assuming it is not set correctly already.



185
186
187
188
189
190
191
192
193
194
195
196
# File 'lib/chef/provider/file.rb', line 185

def set_content
  unless compare_content
    description = []
    description << "update content in file #{@new_resource.path} from #{short_cksum(@current_resource.checksum)} to #{short_cksum(new_resource_content_checksum)}"
    description << diff_current_from_content(@new_resource.content) 
    converge_by(description) do
      backup @new_resource.path if ::File.exists?(@new_resource.path)
      ::File.open(@new_resource.path, "w") {|f| f.write @new_resource.content }
      Chef::Log.info("#{@new_resource} contents updated")
    end
  end
end

#setup_aclObject



146
147
148
149
150
# File 'lib/chef/provider/file.rb', line 146

def setup_acl
  return if Chef::Platform.windows?
  acl_scanner = ScanAccessControl.new(@new_resource, @current_resource)
  acl_scanner.set_all!
end

#update_new_file_state(path = @new_resource.path) ⇒ Object

if you are using a tempfile before creating, you must override the default with the tempfile, since the file at @new_resource.path will not be updated on converge



201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
# File 'lib/chef/provider/file.rb', line 201

def update_new_file_state(path=@new_resource.path)
  if !::File.directory?(path) 
    @new_resource.checksum(checksum(path))
  end

  if Chef::Platform.windows?
    # TODO: To work around CHEF-3554, add support for Windows
    # equivalent, or implicit resource reporting won't work for
    # Windows.
    return
  end

  acl_scanner = ScanAccessControl.new(@new_resource, @new_resource)
  acl_scanner.set_all!
end

#whyrun_supported?Boolean

Returns:

  • (Boolean)


124
125
126
# File 'lib/chef/provider/file.rb', line 124

def whyrun_supported?
  true
end