Class: Chef::Provider::File

Inherits:
Chef::Provider show all
Includes:
Mixin::Checksum, 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 inherited from Chef::Provider

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

Methods included from Mixin::ConvertToClassName

#convert_to_class_name, #convert_to_snake_case, #filename_to_qualified_string, #snake_case_basename

Methods included from Mixin::EnforceOwnershipAndPermissions

#access_controls, #enforce_ownership_and_permissions

Methods included from Mixin::RecipeDefinitionDSLCore

#method_missing

Methods included from Mixin::Language

#data_bag, #data_bag_item, #platform?, #platform_family?, #search, #value_for_platform, #value_for_platform_family

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::Mixin::RecipeDefinitionDSLCore

Instance Method Details

#action_createObject


244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
# File 'lib/chef/provider/file.rb', line 244

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


275
276
277
278
279
280
281
# File 'lib/chef/provider/file.rb', line 275

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


283
284
285
286
287
288
289
290
291
# File 'lib/chef/provider/file.rb', line 283

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


293
294
295
296
297
298
299
300
# File 'lib/chef/provider/file.rb', line 293

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


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

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.


205
206
207
# File 'lib/chef/provider/file.rb', line 205

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

#define_resource_requirementsObject


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

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


331
332
333
334
335
336
337
338
339
340
341
# File 'lib/chef/provider/file.rb', line 331

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


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

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


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

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)

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

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


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

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
  load_current_resource_attrs
  setup_acl
  
  @current_resource
end

#load_current_resource_attrsObject


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

def load_current_resource_attrs
  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

  if ::File.exist?(@new_resource.path)
    stat = ::File.stat(@new_resource.path)
    @current_resource.owner(stat.uid)
    @current_resource.mode(stat.mode & 07777)
    @current_resource.group(stat.gid)

    if @new_resource.group.nil?
      @new_resource.group(@current_resource.group)
    end 
    if @new_resource.owner.nil?
      @new_resource.owner(@current_resource.owner)
    end
    if @new_resource.mode.nil?
      @new_resource.mode(@current_resource.mode)
    end
  end
end

#set_all_access_controlsObject


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

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.


210
211
212
213
214
215
216
217
218
219
220
221
# File 'lib/chef/provider/file.rb', line 210

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


172
173
174
175
# File 'lib/chef/provider/file.rb', line 172

def setup_acl
  @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


226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
# File 'lib/chef/provider/file.rb', line 226

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

  stat = ::File.stat(path)
  @new_resource.owner(stat.uid)
  @new_resource.mode(stat.mode & 07777)
  @new_resource.group(stat.gid)
end

#whyrun_supported?Boolean

Returns:

  • (Boolean)

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

def whyrun_supported?
  true
end