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

[View source]

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

[View source]

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

[View source]

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

[View source]

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

[View source]

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.

[View source]

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

[View source]

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

[View source]

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

[View source]

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

[View source]

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)
[View source]

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

[View source]

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

[View source]

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.

[View source]

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

[View source]

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

[View source]

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)
[View source]

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

def whyrun_supported?
  true
end