diff options
Diffstat (limited to 'lib/chef/provider/file.rb')
-rw-r--r-- | lib/chef/provider/file.rb | 368 |
1 files changed, 144 insertions, 224 deletions
diff --git a/lib/chef/provider/file.rb b/lib/chef/provider/file.rb index 26c1618b60..b21651ee05 100644 --- a/lib/chef/provider/file.rb +++ b/lib/chef/provider/file.rb @@ -1,6 +1,7 @@ # # Author:: Adam Jacob (<adam@opscode.com>) -# Copyright:: Copyright (c) 2008 Opscode, Inc. +# Author:: Lamont Granquist (<lamont@opscode.com>) +# Copyright:: Copyright (c) 2008-2013 Opscode, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,230 +20,74 @@ require 'chef/config' require 'chef/log' require 'chef/resource/file' -require 'chef/mixin/checksum' require 'chef/provider' require 'etc' require 'fileutils' require 'chef/scan_access_control' -require 'chef/mixin/shell_out' +require 'chef/mixin/checksum' +require 'chef/util/backup' +require 'chef/util/diff' + +# The Tao of File Providers: +# - the content provider must always return a tempfile that we can delete/mv +# - do_create_file shall always create the file first and obey umask when perms are not specified +# - do_contents_changes may assume the destination file exists (simplifies exception checking, +# and always gives us something to diff against) +# - do_contents_changes must restore the perms to the dest file and not obliterate them with +# random tempfile permissions +# - do_acl_changes may assume perms were not modified between lcr and when it runs (although the +# file may have been created) class Chef - class Provider class File < Chef::Provider include Chef::Mixin::EnforceOwnershipAndPermissions include Chef::Mixin::Checksum - include Chef::Mixin::ShellOut - def negative_complement(big) - if big > 1073741823 # Fixnum max - big -= (2**32) # diminished radix wrap to negative - end - big - end + attr_reader :deployment_strategy - def octal_mode(mode) - ((mode.respond_to?(:oct) ? mode.oct : mode.to_i) & 007777) - end - - private :negative_complement, :octal_mode - - 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 + def initialize(new_resource, run_context) + @content_class ||= Chef::Provider::File::Content::File + @deployment_strategy = new_resource.deployment_strategy.new() if new_resource.respond_to?(:deployment_strategy) + super end - 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 - - - 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 - def whyrun_supported? true end def load_current_resource - # Every child should be specifying their own constructor, so this - # should only be run in the file case. + # Let children resources override constructing the @current_resource @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 - + load_resource_attributes_from_file(@current_resource) @current_resource end - def setup_acl - return if Chef::Platform.windows? - acl_scanner = ScanAccessControl.new(@new_resource, @current_resource) - acl_scanner.set_all! - end - def define_resource_requirements - # this must be evaluated before whyrun messages are printed - access_controls.requires_changes? - + # Make sure the parent directory exists, otherwise fail. For why-run assume it would have been created. 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 - - # Compare the content of a file. Returns true if they are the same, false if they are not. - def compare_content - checksum(@current_resource.path) == new_resource_content_checksum - end - - # Set the content of the file, assuming it is not set correctly already. - 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") + # Make sure the file is deletable if it exists, otherwise fail. + if ::File.exists?(@new_resource.path) + requirements.assert(:delete) do |a| + a.assertion { ::File.writable?(@new_resource.path) } + a.failure_message(Chef::Exceptions::InsufficientPermissions,"File #{@new_resource.path} exists but is not writable so it cannot be deleted") end end end - # 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 - 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 - 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 - - 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 + do_create_file + do_contents_changes + do_acl_changes + load_resource_attributes_from_file(@new_resource) end def action_create_if_missing @@ -255,7 +100,7 @@ class Chef def action_delete if ::File.exists?(@new_resource.path) - converge_by("delete file #{@new_resource.path}") do + 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}") @@ -272,57 +117,132 @@ class Chef end end - 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 + # deprecated methods to support + + def set_content + end + + def compare_content + end + + def diff_current + end + + def diff_current_from_content + end + + def is_binary?(path) + end + + def update_new_file_state + end + + def whyrun_mode? + Chef::Log.warn("The method Chef::Provider::File#whyrun_mode? is deprecated and will be removed in Chef 12") + Chef::Config[:why_run] + end + + private + + def content + @content ||= begin + load_current_resource if @current_resource.nil? + @content_class.new(@new_resource, @current_resource, @run_context) + end + end + + def do_create_file + @file_created = false + unless ::File.exists?(@new_resource.path) + description = "create new file #{@new_resource.path}" + converge_by(description) do + deployment_strategy.create(@new_resource.path) + Chef::Log.info("#{@new_resource} created file #{@new_resource.path}") + @file_created = true end end end - def deploy_tempfile - Tempfile.open(::File.basename(@new_resource.name)) do |tempfile| - yield tempfile + # do_contents_changes needs to know if do_create_file created a file or not + def file_created? + @file_created == true + end - 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) + def backup(file = nil) + Chef::Util::Backup.new(@new_resource, file).backup! + end + + def diff + @diff ||= Chef::Util::Diff.new + end + + def do_contents_changes + # a nil tempfile is okay, means the resource has no content or no new content + return if tempfile.nil? + # but a tempfile that has no path or doesn't exist should not happen + if tempfile.path.nil? || !::File.exists?(tempfile.path) + raise "chef-client is confused, trying to deploy a file that has no path or does not exist..." end + if contents_changed? + diff.diff(@current_resource.path, tempfile.path) + @new_resource.diff( diff.for_reporting ) unless file_created? + description = [ "update content in file #{@new_resource.path} from #{short_cksum(@current_resource.checksum)} to #{short_cksum(checksum(tempfile.path))}" ] + description << diff.for_output + converge_by(description) do + backup unless file_created? + deployment_strategy.deploy(tempfile.path, @new_resource.path) + Chef::Log.info("#{@new_resource} updated file contents #{@new_resource.path}") + end + end + # unlink necessary to clean up in why-run mode + tempfile.unlink end - private + def do_acl_changes + if access_controls.requires_changes? + converge_by(access_controls.describe_changes) do + access_controls.set_all + end + end + end + + def contents_changed? + checksum(tempfile.path) != @current_resource.checksum + end + + def tempfile + content.tempfile + end def short_cksum(checksum) return "none" if checksum.nil? checksum.slice(0,6) end - def new_resource_content_checksum - @new_resource.content && Digest::SHA2.hexdigest(@new_resource.content) + # 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 + def load_resource_attributes_from_file(resource) + if resource.respond_to?(:checksum) + if ::File.exists?(resource.path) && !::File.directory?(resource.path) + if @action != :create_if_missing # XXX: don't we break current_resource semantics by skipping this? + resource.checksum(checksum(resource.path)) + end + end + 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, resource) + acl_scanner.set_all! end + end end end + |