diff options
Diffstat (limited to 'lib/chef')
23 files changed, 935 insertions, 534 deletions
diff --git a/lib/chef/config.rb b/lib/chef/config.rb index ca912b65ab..0be2eb7d31 100644 --- a/lib/chef/config.rb +++ b/lib/chef/config.rb @@ -344,5 +344,19 @@ class Chef # returns a platform specific path to the user home dir windows_home_path = ENV['SYSTEMDRIVE'] + ENV['HOMEPATH'] if ENV['SYSTEMDRIVE'] && ENV['HOMEPATH'] user_home(ENV['HOME'] || windows_home_path || ENV['USERPROFILE']) + + # selinux command to restore file contexts + selinux_restorecon_command "/sbin/restorecon -R" + # guess if you're running selinux or not -- override this if it guesses wrong + selinux_enabled system( "/usr/sbin/selinuxenabled" ) + + # set this to something like Chef::Provider::File::Deploy::CpUnix if you want to override behavior globally + file_deployment_strategy nil + + # do we create /tmp or %TEMP% files, or do we create temp files in the destination directory of the file? + # - on windows this avoids issues with permission inheritance with the %TEMP% directory (do not set this to false) + # - on unix this creates temp files like /etc/.sudoers.X-Y-Z and may create noise and make for itchy neckbeards + # - with selinux and other ACLs approaches it may still be useful or to avoid copying across filesystems + file_deployment_uses_destdir ( RUBY_PLATFORM =~ /mswin|mingw|windows/ ) end end diff --git a/lib/chef/file_access_control/unix.rb b/lib/chef/file_access_control/unix.rb index 1dbfe40f2f..dbfe67c0fa 100644 --- a/lib/chef/file_access_control/unix.rb +++ b/lib/chef/file_access_control/unix.rb @@ -59,8 +59,10 @@ class Chef uid_from_resource(current_resource) end + # target_uid.nil? means the new_resource.owner is nil and the requesting owner doesn't care + # current_uid.nil? means the file does not exist def should_update_owner? - !target_uid.nil? && target_uid != current_uid + !target_uid.nil? && ( current_uid.nil? || target_uid != current_uid ) end def set_owner! @@ -103,7 +105,7 @@ class Chef end def should_update_group? - !target_gid.nil? && target_gid != current_gid + !target_gid.nil? && ( current_gid.nil? || target_gid != current_gid ) end def set_group! @@ -136,7 +138,7 @@ class Chef end def should_update_mode? - !target_mode.nil? && current_mode != target_mode + !target_mode.nil? && ( current_mode.nil? || current_mode != target_mode ) end def set_mode! @@ -192,7 +194,7 @@ class Chef end end - def uid_from_resource(resource) + def uid_from_resource(resource) return nil if resource == nil or resource.owner.nil? if resource.owner.kind_of?(String) diminished_radix_complement( Etc.getpwnam(resource.owner).uid ) diff --git a/lib/chef/provider.rb b/lib/chef/provider.rb index 3fe057bd9b..302a9fb106 100644 --- a/lib/chef/provider.rb +++ b/lib/chef/provider.rb @@ -103,7 +103,7 @@ class Chef define_resource_requirements process_resource_requirements - # user-defined providers including LWRPs may + # user-defined providers including LWRPs may # not include whyrun support - if they don't support it # we can't execute any actions while we're running in # whyrun mode. Instead we 'fake' whyrun by documenting that @@ -133,7 +133,7 @@ class Chef events.resource_up_to_date(@new_resource, @action) else events.resource_updated(@new_resource, @action) - new_resource.updated_by_last_action(true) + new_resource.updated_by_last_action(true) end end @@ -141,17 +141,16 @@ class Chef @requirements ||= ResourceRequirements.new(@new_resource, run_context) end + def converge_by(descriptions, &block) + converge_actions.add_action(descriptions, &block) + end + protected def converge_actions @converge_actions ||= ConvergeActions.new(@new_resource, run_context, @action) end - def converge_by(descriptions, &block) - converge_actions.add_action(descriptions, &block) - end - - def recipe_eval(&block) # This block has new resource definitions within it, which # essentially makes it an in-line Chef run. Save our current diff --git a/lib/chef/provider/cookbook_file.rb b/lib/chef/provider/cookbook_file.rb index cde8dade61..e80da7593d 100644 --- a/lib/chef/provider/cookbook_file.rb +++ b/lib/chef/provider/cookbook_file.rb @@ -16,18 +16,15 @@ # limitations under the License. # -require 'chef/file_access_control' require 'chef/provider/file' -require 'tempfile' class Chef class Provider class CookbookFile < Chef::Provider::File - include Chef::Mixin::EnforceOwnershipAndPermissions - - def whyrun_supported? - true + def initialize(new_resource, run_context) + @content_class = Chef::Provider::File::Content::CookbookFile + super end def load_current_resource @@ -35,79 +32,7 @@ class Chef super end - def action_create - if file_cache_location && content_stale? - description = [] - description << "create a new cookbook_file #{@new_resource.path}" - description << diff_current(file_cache_location) - converge_by(description) do - Chef::Log.debug("#{@new_resource} has new contents") - backup_new_resource - deploy_tempfile do |tempfile| - Chef::Log.debug("#{@new_resource} staging #{file_cache_location} to #{tempfile.path}") - tempfile.close - FileUtils.cp(file_cache_location, tempfile.path) - enforce_tempfile_inheritance(tempfile.path) - end - update_new_file_state - Chef::Log.info("#{@new_resource} created file #{@new_resource.path}") - end - else - set_all_access_controls - end - end - - def file_cache_location - @file_cache_location ||= begin - cookbook = run_context.cookbook_collection[resource_cookbook] - cookbook.preferred_filename_on_disk_location(node, :files, @new_resource.source, @new_resource.path) - end - end - - # Determine the cookbook to get the file from. If new resource sets an - # explicit cookbook, use it, otherwise fall back to the implicit cookbook - # i.e., the cookbook the resource was declared in. - def resource_cookbook - @new_resource.cookbook || @new_resource.cookbook_name - end - - def backup_new_resource - if ::File.exists?(@new_resource.path) - backup @new_resource.path - end - end - - def content_stale? - ( ! ::File.exist?(@new_resource.path)) || ( ! compare_content) - end - - protected - - def enforce_tempfile_inheritance(tempfile_path) - # On the Windows platform, files in the temp directory - # default to not inherit unless the new resource - # specifies rights of - # some sort. Here we ensure that even when no rights - # are - # specified, the dacl's inheritance flag is set. - if Chef::Platform.windows? && - @new_resource.rights.nil? && - @new_resource.group.nil? && - @new_resource.owner.nil? && - @new_resource.deny_rights.nil? - - securable_tempfile = Chef::ReservedNames::Win32::Security::SecurableObject.new(tempfile_path) - - # No rights were specified, so the dacl will have - # no explicit aces - default_dacl = Chef::ReservedNames::Win32::Security::ACL.create([]) - - # In setting this default dacl, set inheritance to - # true - securable_tempfile.set_dacl(default_dacl, true) - end - end - end end end + diff --git a/lib/chef/provider/directory.rb b/lib/chef/provider/directory.rb index 8fdc070c77..e6886dde59 100644 --- a/lib/chef/provider/directory.rb +++ b/lib/chef/provider/directory.rb @@ -27,24 +27,16 @@ class Chef class Provider class Directory < Chef::Provider::File - include Chef::Mixin::EnforceOwnershipAndPermissions - def whyrun_supported? true end def load_current_resource @current_resource = Chef::Resource::Directory.new(@new_resource.name) - @current_resource.path(@new_resource.path) - setup_acl - - @current_resource + super end def define_resource_requirements - # this must be evaluated before whyrun messages are printed - access_controls.requires_changes? - requirements.assert(:create) do |a| # Make sure the parent dir exists, or else fail. # for why run, print a message explaining the potential error. @@ -61,8 +53,8 @@ class Chef # find the lowest-level directory in @new_resource.path that already exists # make sure we have write permissions to that directory is_parent_writable = lambda do |base_dir| - base_dir = ::File.dirname(base_dir) - if ::File.exist?(base_dir) + base_dir = ::File.dirname(base_dir) + if ::File.exist?(base_dir) ::File.writable?(base_dir) else is_parent_writable.call(base_dir) @@ -72,27 +64,27 @@ class Chef else # in why run mode & parent directory does not exist no permissions check is required # If not in why run, permissions must be valid and we rely on prior assertion that dir exists - if !whyrun_mode? || ::File.exist?(parent_directory) + if !whyrun_mode? || ::File.exist?(parent_directory) ::File.writable?(parent_directory) else true end end end - a.failure_message(Chef::Exceptions::InsufficientPermissions, + a.failure_message(Chef::Exceptions::InsufficientPermissions, "Cannot create #{@new_resource} at #{@new_resource.path} due to insufficient permissions") end - requirements.assert(:delete) do |a| - a.assertion do + requirements.assert(:delete) do |a| + a.assertion do if ::File.exist?(@new_resource.path) - ::File.directory?(@new_resource.path) && ::File.writable?(@new_resource.path) + ::File.directory?(@new_resource.path) && ::File.writable?(@new_resource.path) else true end end a.failure_message(RuntimeError, "Cannot delete #{@new_resource} at #{@new_resource.path}!") - # No why-run handling here: + # No why-run handling here: # * if we don't have permissions, this is unlikely to be changed earlier in the run # * if the target is a file (not a dir), there's no reasonable path by which this would have been changed end @@ -100,17 +92,17 @@ class Chef def action_create unless ::File.exist?(@new_resource.path) - converge_by("create new directory #{@new_resource.path}") do + converge_by("create new directory #{@new_resource.path}") do if @new_resource.recursive == true ::FileUtils.mkdir_p(@new_resource.path) else ::Dir.mkdir(@new_resource.path) end Chef::Log.info("#{@new_resource} created directory #{@new_resource.path}") - end + end end - set_all_access_controls - update_new_file_state + do_acl_changes + load_resource_attributes_from_file(@new_resource) end def action_delete 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 + diff --git a/lib/chef/provider/file/content.rb b/lib/chef/provider/file/content.rb new file mode 100644 index 0000000000..dd5d98852c --- /dev/null +++ b/lib/chef/provider/file/content.rb @@ -0,0 +1,75 @@ +# +# Author:: Lamont Granquist (<lamont@opscode.com>) +# Copyright:: Copyright (c) 2013 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +class Chef + class Provider + class File + class Content + + def initialize(new_resource, current_resource, run_context) + @new_resource = new_resource + @current_resource = current_resource + @run_context = run_context + end + + def run_context + @run_context + end + + def new_resource + @new_resource + end + + def current_resource + @current_resource + end + + def tempfile + @tempfile ||= file_for_provider + end + + private + + # + # Return something that looks like a File or Tempfile and + # you must assume the provider will unlink this file. Copy + # the contents to a Tempfile if you need to. + # + def file_for_provider + raise "class must implement file_for_provider!" + end + + # + # These are important for windows to get permissions right, and may + # be useful for SELinux and other ACL approaches. Please use them + # as the arguments to Tempfile.new() consistently. + # + def tempfile_basename + basename = ::File.basename(@new_resource.name) + basename.insert 0, "." unless Chef::Platform.windows? # dotfile if we're not on windows + basename + end + + def tempfile_dirname + Chef::Config[:file_deployment_uses_destdir] ? ::File.dirname(@new_resource.path) : Dir::tmpdir + end + + end + end + end +end diff --git a/lib/chef/provider/file/content/cookbook_file.rb b/lib/chef/provider/file/content/cookbook_file.rb new file mode 100644 index 0000000000..9071aa4a3a --- /dev/null +++ b/lib/chef/provider/file/content/cookbook_file.rb @@ -0,0 +1,50 @@ +# +# Author:: Lamont Granquist (<lamont@opscode.com>) +# Copyright:: Copyright (c) 2013 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/provider/file/content' + +class Chef + class Provider + class File + class Content + class CookbookFile < Chef::Provider::File::Content + + private + + def file_for_provider + cookbook = run_context.cookbook_collection[resource_cookbook] + file_cache_location = cookbook.preferred_filename_on_disk_location(run_context.node, :files, @new_resource.source, @new_resource.path) + if file_cache_location.nil? + nil + else + tempfile = Tempfile.open(tempfile_basename, tempfile_dirname) + tempfile.close + Chef::Log.debug("#{@new_resource} staging #{file_cache_location} to #{tempfile.path}") + FileUtils.cp(file_cache_location, tempfile.path) + tempfile + end + end + + def resource_cookbook + @new_resource.cookbook || @new_resource.cookbook_name + end + end + end + end + end +end diff --git a/lib/chef/provider/file/content/file.rb b/lib/chef/provider/file/content/file.rb new file mode 100644 index 0000000000..b0a1234956 --- /dev/null +++ b/lib/chef/provider/file/content/file.rb @@ -0,0 +1,48 @@ +# +# Author:: Lamont Granquist (<lamont@opscode.com>) +# Copyright:: Copyright (c) 2013 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/provider/file/content' + +class Chef + class Provider + class File + class Content + class File < Chef::Provider::File::Content + def file_for_provider + if @new_resource.content + tempfile = Tempfile.open(tempfile_basename, tempfile_dirname) + tempfile.write(@new_resource.content) + tempfile.close + tempfile + else + nil + end + end + + private + + def tempfile_basename + basename = ::File.basename(@new_resource.name) + basename.insert 0, "." unless Chef::Platform.windows? # dotfile if we're not on windows + basename + end + end + end + end + end +end diff --git a/lib/chef/provider/file/content/remote_file.rb b/lib/chef/provider/file/content/remote_file.rb new file mode 100644 index 0000000000..7e006d5415 --- /dev/null +++ b/lib/chef/provider/file/content/remote_file.rb @@ -0,0 +1,112 @@ +# +# Author:: Jesse Campbell (<hikeit@gmail.com>) +# Author:: Lamont Granquist (<lamont@opscode.com>) +# Copyright:: Copyright (c) 2013 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'rest_client' +require 'uri' +require 'tempfile' +require 'chef/provider/file/content' + +class Chef + class Provider + class File + class Content + class RemoteFile < Chef::Provider::File::Content + + attr_reader :raw_file_source + + private + + def file_for_provider + Chef::Log.debug("#{@new_resource} checking for changes") + + if current_resource_matches_target_checksum? + Chef::Log.debug("#{@new_resource} checksum matches target checksum (#{@new_resource.checksum}) - not updating") + else + sources = @new_resource.source + raw_file, @raw_file_source = try_multiple_sources(sources) + end + raw_file + end + + private + + # Given an array of source uris, iterate through them until one does not fail + def try_multiple_sources(sources) + sources = sources.dup + source = sources.shift + begin + uri = URI.parse(source) + raw_file = grab_file_from_uri(uri) + rescue SocketError, Errno::ECONNREFUSED, Errno::ENOENT, Errno::EACCES, Timeout::Error, Net::HTTPFatalError, Net::FTPError => e + Chef::Log.warn("#{@new_resource} cannot be downloaded from #{source}: #{e.to_s}") + if source = sources.shift + Chef::Log.info("#{@new_resource} trying to download from another mirror") + retry + else + raise e + end + end + if uri.userinfo + uri.password = "********" + end + return raw_file, uri.to_s + end + + # Given a source uri, return a Tempfile, or a File that acts like a Tempfile (close! method) + def grab_file_from_uri(uri) + if_modified_since = @new_resource.last_modified + if_none_match = @new_resource.etag + uri_dup = uri.dup + if uri_dup.userinfo + uri_dup.password = "********" + end + if uri_dup.to_s == @current_resource.source[0] + if_modified_since ||= @current_resource.last_modified + if_none_match ||= @current_resource.etag + end + if URI::HTTP === uri + #HTTP or HTTPS + raw_file, mtime, etag = Chef::Provider::RemoteFile::HTTP.fetch(uri, if_modified_since, if_none_match) + elsif URI::FTP === uri + #FTP + raw_file, mtime = Chef::Provider::RemoteFile::FTP.fetch(uri, @new_resource.ftp_active_mode, if_modified_since) + etag = nil + elsif uri.scheme == "file" + #local/network file + raw_file, mtime = Chef::Provider::RemoteFile::LocalFile.fetch(uri, if_modified_since) + etag = nil + else + raise ArgumentError, "Invalid uri. Only http(s), ftp, and file are currently supported" + end + unless raw_file.nil? + @new_resource.etag etag unless @new_resource.etag + @new_resource.last_modified mtime unless @new_resource.last_modified + end + return raw_file + end + + def current_resource_matches_target_checksum? + @new_resource.checksum && @current_resource.checksum && @current_resource.checksum =~ /^#{Regexp.escape(@new_resource.checksum)}/ + end + + end + end + end + end +end diff --git a/lib/chef/provider/file/content/template.rb b/lib/chef/provider/file/content/template.rb new file mode 100644 index 0000000000..68673e2116 --- /dev/null +++ b/lib/chef/provider/file/content/template.rb @@ -0,0 +1,58 @@ +# +# Author:: Lamont Granquist (<lamont@opscode.com>) +# Copyright:: Copyright (c) 2013 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/mixin/template' +require 'chef/provider/file/content' + +class Chef + class Provider + class File + class Content + class Template < Chef::Provider::File::Content + + include Chef::Mixin::Template + + def template_location + @template_file_cache_location ||= begin + template_finder.find(@new_resource.source, :local => @new_resource.local, :cookbook => @new_resource.cookbook) + end + end + + private + + def file_for_provider + context = {} + context.merge!(@new_resource.variables) + context[:node] = @run_context.node + context[:template_finder] = template_finder + file = nil + render_template(IO.read(template_location), context) { |t| file = t } + file + end + + def template_finder + @template_finder ||= begin + TemplateFinder.new(run_context, @new_resource.cookbook_name, @run_context.node) + end + end + end + end + end + end +end + diff --git a/lib/chef/provider/file/deploy/cp_unix.rb b/lib/chef/provider/file/deploy/cp_unix.rb new file mode 100644 index 0000000000..1640c68538 --- /dev/null +++ b/lib/chef/provider/file/deploy/cp_unix.rb @@ -0,0 +1,52 @@ +# +# Author:: Lamont Granquist (<lamont@opscode.com>) +# Copyright:: Copyright (c) 2013 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# +# PURPOSE: this strategy should be cross-platform and maintain SELinux contexts +# and windows ACL inheritance, but it uses cp and is both slower and is +# not atomic and may result in a corrupted destination file in low +# disk or power outage situations. +# + +class Chef + class Provider + class File + class Deploy + class CpUnix + def create(file) + Chef::Log.debug("touching #{file} to create it") + FileUtils.touch(file) + end + + def deploy(src, dst) + # we are only responsible for content so restore the dst files perms + mode = ::File.stat(dst).mode & 07777 + uid = ::File.stat(dst).uid + gid = ::File.stat(dst).gid + Chef::Log.debug("saved mode = #{mode.to_s(8)}, uid = #{uid}, gid = #{gid} from #{dst}") + Chef::Log.debug("copying temporary file #{src} into place at #{dst}") + FileUtils.cp(src, dst) + ::File.chmod(mode, dst) + ::File.chown(uid, gid, dst) + Chef::Log.debug("restored mode = #{mode.to_s(8)}, uid = #{uid}, gid = #{gid} to #{dst}") + end + end + end + end + end +end diff --git a/lib/chef/provider/file/deploy/mv_unix.rb b/lib/chef/provider/file/deploy/mv_unix.rb new file mode 100644 index 0000000000..5e8e5ce72e --- /dev/null +++ b/lib/chef/provider/file/deploy/mv_unix.rb @@ -0,0 +1,60 @@ +# +# Author:: Lamont Granquist (<lamont@opscode.com>) +# Copyright:: Copyright (c) 2013 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# +# PURPOSE: this strategy is atomic, does not mutate file modes, and supports selinux +# +# Note the FileUtils.mv does not have a preserve flag, and the preserve behavior of it is different +# on different rubies (1.8.7 vs 1.9.x) so we are explicit about making certain the tempfile metadata +# is not deployed (technically implementing preserve = false ourselves). +# + +class Chef + class Provider + class File + class Deploy + class MvUnix + def create(file) + Chef::Log.debug("touching #{file} to create it") + FileUtils.touch(file) + end + + def deploy(src, dst) + # we are only responsible for content so restore the dst files perms + Chef::Log.debug("reading modes from #{dst} file") + mode = ::File.stat(dst).mode & 07777 + uid = ::File.stat(dst).uid + gid = ::File.stat(dst).gid + Chef::Log.debug("applying mode = #{mode.to_s(8)}, uid = #{uid}, gid = #{gid} to #{src}") + ::File.chmod(mode, src) + ::File.chown(uid, gid, src) + Chef::Log.debug("moving temporary file #{src} into place at #{dst}") + FileUtils.mv(src, dst) + + # handle selinux if we need to run restorecon + if Chef::Config[:selinux_enabled] + Chef::Log.debug("selinux is enabled, fixing selinux permissions") + cmd = "#{Chef::Config[:selinux_restorecon_comand]} #{dst}" + shell_out!(cmd) + end + end + end + end + end + end +end diff --git a/lib/chef/provider/file/deploy/mv_windows.rb b/lib/chef/provider/file/deploy/mv_windows.rb new file mode 100644 index 0000000000..2c4d01c6ae --- /dev/null +++ b/lib/chef/provider/file/deploy/mv_windows.rb @@ -0,0 +1,51 @@ +# +# Author:: Lamont Granquist (<lamont@opscode.com>) +# Copyright:: Copyright (c) 2013 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# +# PURPOSE: this strategy is atomic and preserves default umasks, but on windows you must +# not be copying from the temp directory, and will not correctly restore +# SELinux contexts. +# + +class Chef + class Provider + class File + class Deploy + class MvWindows + def create(file) + Chef::Log.debug("touching #{file} to create it") + FileUtils.touch(file) + end + + def deploy(src, dst) + if ::File.dirname(src) != ::File.dirname(dst) + # internal warning for now - in a Windows/SElinux/ACLs world its better to write + # a tempfile to your destination directory and then rename it + Chef::Log.debug("WARNING: moving tempfile across different directories -- this may break permissions") + end + + # FIXME: save all the windows perms off the dst + FileUtils.mv(src, dst) + # FIXME: restore all the windows perms onto the dst + + end + end + end + end + end +end diff --git a/lib/chef/provider/file_strategies.rb b/lib/chef/provider/file_strategies.rb new file mode 100644 index 0000000000..ca2e9d7be2 --- /dev/null +++ b/lib/chef/provider/file_strategies.rb @@ -0,0 +1,7 @@ +require 'chef/provider/file_strategy/content_from_resource' +require 'chef/provider/file_strategy/content_from_cookbook_file' +require 'chef/provider/file_strategy/content_from_remote_file' +require 'chef/provider/file_strategy/content_from_template' +require 'chef/provider/file_strategy/deploy_cp' +require 'chef/provider/file_strategy/deploy_mv' +require 'chef/provider/file_strategy/deploy_mv_with_restorecon' diff --git a/lib/chef/provider/link.rb b/lib/chef/provider/link.rb index b8017c4558..5a29aff623 100644 --- a/lib/chef/provider/link.rb +++ b/lib/chef/provider/link.rb @@ -112,7 +112,7 @@ class Chef end if @new_resource.link_type == :symbolic if access_controls.requires_changes? - converge_by(access_controls.describe_changes) do + converge_by(access_controls.describe_changes) do access_controls.set_all end end diff --git a/lib/chef/provider/remote_directory.rb b/lib/chef/provider/remote_directory.rb index 0412e8a668..bd4243cd84 100644 --- a/lib/chef/provider/remote_directory.rb +++ b/lib/chef/provider/remote_directory.rb @@ -31,7 +31,6 @@ class Chef class Provider class RemoteDirectory < Chef::Provider::Directory - include Chef::Mixin::EnforceOwnershipAndPermissions include Chef::Mixin::FileClass def action_create @@ -67,7 +66,7 @@ class Chef if @new_resource.purge unmanaged_files.sort.reverse.each do |f| # file_class comes from Chef::Mixin::FileClass - if ::File.directory?(f) && !Chef::Platform.windows? && !file_class.symlink?(f.dup) + if ::File.directory?(f) && !Chef::Platform.windows? && !file_class.symlink?(f.dup) # Linux treats directory symlinks as files # Remove a directory as a directory when not on windows if it is not a symlink purge_directory(f) diff --git a/lib/chef/provider/remote_file.rb b/lib/chef/provider/remote_file.rb index 655a21a75c..df95be176b 100644 --- a/lib/chef/provider/remote_file.rb +++ b/lib/chef/provider/remote_file.rb @@ -18,14 +18,15 @@ # require 'chef/provider/file' -require 'uri' -require 'tempfile' class Chef class Provider class RemoteFile < Chef::Provider::File - include Chef::Mixin::EnforceOwnershipAndPermissions + def initialize(new_resource, run_context) + @content_class = Chef::Provider::File::Content::RemoteFile + super + end def load_current_resource @current_resource = Chef::Resource::RemoteFile.new(@new_resource.name) @@ -39,124 +40,12 @@ class Chef end def action_create - Chef::Log.debug("#{@new_resource} checking for changes") - - if current_resource_matches_target_checksum? - Chef::Log.debug("#{@new_resource} checksum matches target checksum (#{@new_resource.checksum}) - not updating") - else - sources = @new_resource.source - raw_file, raw_file_source = try_multiple_sources(sources) - if raw_file.nil? - Chef::Log.info("#{@new_resource} matched #{raw_file_source}, not updating") - elsif matches_current_checksum?(raw_file) - Chef::Log.info("#{@new_resource} downloaded from #{raw_file_source}, checksums match, not updating") - raw_file.close! - else - Chef::Log.info("#{@new_resource} downloaded from #{raw_file_source}") - description = [] - description << "copy file downloaded from #{raw_file_source} into #{@new_resource.path}" - description << diff_current(raw_file.path) - converge_by(description) do - backup_new_resource - FileUtils.cp raw_file.path, @new_resource.path - Chef::Log.info "#{@new_resource} updated" - raw_file.close! - save_fileinfo(raw_file_source) - end - # whyrun mode cleanup - the temp file will never be used, - # so close/unlink it here. - if whyrun_mode? - raw_file.close! - end - end - end - set_all_access_controls - update_new_file_state - end - - def current_resource_matches_target_checksum? - @new_resource.checksum && @current_resource.checksum && @current_resource.checksum =~ /^#{Regexp.escape(@new_resource.checksum)}/ - end - - def matches_current_checksum?(candidate_file) - Chef::Log.debug "#{@new_resource} checking for file existence of #{@new_resource.path}" - @new_resource.checksum(checksum(candidate_file.path)) - if ::File.exists?(@new_resource.path) - Chef::Log.debug "#{@new_resource} file exists at #{@new_resource.path}" - Chef::Log.debug "#{@new_resource} target checksum: #{@current_resource.checksum}" - Chef::Log.debug "#{@new_resource} source checksum: #{@new_resource.checksum}" - - @new_resource.checksum == @current_resource.checksum - else - Chef::Log.debug "#{@new_resource} creating #{@new_resource.path}" - false - end - end - - def backup_new_resource - if ::File.exists?(@new_resource.path) - Chef::Log.debug "#{@new_resource} checksum changed from #{@current_resource.checksum} to #{@new_resource.checksum}" - backup @new_resource.path - end + super + save_fileinfo(@content.raw_file_source) end private - # Given an array of source uris, iterate through them until one does not fail - def try_multiple_sources(sources) - sources = sources.dup - source = sources.shift - begin - uri = URI.parse(source) - raw_file = grab_file_from_uri(uri) - rescue SocketError, Errno::ECONNREFUSED, Errno::ENOENT, Errno::EACCES, Timeout::Error, Net::HTTPFatalError, Net::FTPError => e - Chef::Log.warn("#{@new_resource} cannot be downloaded from #{source}: #{e.to_s}") - if source = sources.shift - Chef::Log.info("#{@new_resource} trying to download from another mirror") - retry - else - raise e - end - end - if uri.userinfo - uri.password = "********" - end - return raw_file, uri.to_s - end - - # Given a source uri, return a Tempfile, or a File that acts like a Tempfile (close! method) - def grab_file_from_uri(uri) - if_modified_since = @new_resource.last_modified - if_none_match = @new_resource.etag - uri_dup = uri.dup - if uri_dup.userinfo - uri_dup.password = "********" - end - if uri_dup.to_s == @current_resource.source[0] - if_modified_since ||= @current_resource.last_modified - if_none_match ||= @current_resource.etag - end - if URI::HTTP === uri - #HTTP or HTTPS - raw_file, mtime, etag = RemoteFile::HTTP.fetch(uri, if_modified_since, if_none_match) - elsif URI::FTP === uri - #FTP - raw_file, mtime = RemoteFile::FTP.fetch(uri, @new_resource.ftp_active_mode, if_modified_since) - etag = nil - elsif uri.scheme == "file" - #local/network file - raw_file, mtime = RemoteFile::LocalFile.fetch(uri, if_modified_since) - etag = nil - else - raise ArgumentError, "Invalid uri. Only http(s), ftp, and file are currently supported" - end - unless raw_file.nil? - @new_resource.etag etag unless @new_resource.etag - @new_resource.last_modified mtime unless @new_resource.last_modified - end - return raw_file - end - def load_fileinfo begin Chef::JSONCompat.from_json(Chef::FileCache.load("remote_file/#{new_resource.name}")) @@ -177,3 +66,4 @@ class Chef end end end + diff --git a/lib/chef/provider/template.rb b/lib/chef/provider/template.rb index 6df671c8e5..064f9fde74 100644 --- a/lib/chef/provider/template.rb +++ b/lib/chef/provider/template.rb @@ -19,18 +19,15 @@ require 'chef/provider/template_finder' require 'chef/provider/file' -require 'chef/mixin/template' -require 'chef/mixin/checksum' -require 'chef/file_access_control' class Chef class Provider - class Template < Chef::Provider::File - include Chef::Mixin::EnforceOwnershipAndPermissions - include Chef::Mixin::Checksum - include Chef::Mixin::Template + def initialize(new_resource, run_context) + @content_class = Chef::Provider::File::Content::Template + super + end def load_current_resource @current_resource = Chef::Resource::Template.new(@new_resource.name) @@ -40,81 +37,15 @@ class Chef def define_resource_requirements super - requirements.assert(:create, :create_if_missing) do |a| - a.assertion { ::File::exist?(template_location) } - a.failure_message "Template source #{template_location} could not be found." - a.whyrun "Template source #{template_location} does not exist. Assuming it would have been created." + requirements.assert(:create, :create_if_missing) do |a| + a.assertion { ::File::exist?(content.template_location) } + a.failure_message "Template source #{content.template_location} could not be found." + a.whyrun "Template source #{content.template_location} does not exist. Assuming it would have been created." a.block_action! end end - def action_create - render_with_context(template_location) do |rendered_template| - rendered(rendered_template) - if file_already_exists? && content_matches? - Chef::Log.debug("#{@new_resource} content has not changed.") - set_all_access_controls - update_new_file_state(@new_resource.path) - else - description = [] - action_message = if file_already_exists? - "update #{@current_resource} from #{short_cksum(@current_resource.checksum)} to #{short_cksum(@new_resource.checksum)}" - else - "create #{@new_resource}" - end - description << action_message - description << diff_current(rendered_template.path) - converge_by(description) do - backup - FileUtils.cp(rendered_template.path, @new_resource.path) - Chef::Log.info("#{@new_resource} updated content") - access_controls.set_all! - update_new_file_state(@new_resource.path) - end - end - end - end - - def template_finder - @template_finder ||= begin - TemplateFinder.new(run_context, cookbook_name, node) - end - end - - def template_location - @template_file_cache_location ||= begin - template_finder.find(@new_resource.source, :local => @new_resource.local, :cookbook => @new_resource.cookbook) - end - end - - def resource_cookbook - @new_resource.cookbook || @new_resource.cookbook_name - end - - def rendered(rendered_template) - @new_resource.checksum(checksum(rendered_template.path)) - Chef::Log.debug("Current content's checksum: #{@current_resource.checksum}") - Chef::Log.debug("Rendered content's checksum: #{@new_resource.checksum}") - end - - def content_matches? - @current_resource.checksum == @new_resource.checksum - end - - private - - def file_already_exists? - ::File.exist?(@new_resource.path) - end - - def render_with_context(template_location, &block) - context = {} - context.merge!(@new_resource.variables) - context[:node] = node - context[:template_finder] = template_finder - render_template(IO.read(template_location), context, &block) - end - end end end + diff --git a/lib/chef/providers.rb b/lib/chef/providers.rb index f4b3175546..500bc727c8 100644 --- a/lib/chef/providers.rb +++ b/lib/chef/providers.rb @@ -107,3 +107,13 @@ require 'chef/provider/remote_file/local_file' require "chef/provider/lwrp_base" require 'chef/provider/registry_key' + +require 'chef/provider/file/content' +require 'chef/provider/file/content/file' +require 'chef/provider/file/content/remote_file' +require 'chef/provider/file/content/cookbook_file' +require 'chef/provider/file/content/template' +require 'chef/provider/file/deploy/cp_unix' +require 'chef/provider/file/deploy/mv_unix' +require 'chef/provider/file/deploy/mv_windows' + diff --git a/lib/chef/resource/file.rb b/lib/chef/resource/file.rb index 0b92f3332d..cec70857cd 100644 --- a/lib/chef/resource/file.rb +++ b/lib/chef/resource/file.rb @@ -46,6 +46,13 @@ class Chef @action = "create" @allowed_actions.push(:create, :delete, :touch, :create_if_missing) @provider = Chef::Provider::File + @deployment_strategy = Chef::Config[:file_deployment_strategy] + @deployment_strategy ||= if Chef::Platform.windows? + Chef::Provider::File::Deploy::MvWindows + else + Chef::Provider::File::Deploy::MvUnix + end + @diff = nil end @@ -81,7 +88,7 @@ class Chef :kind_of => String ) end - + def diff(arg=nil) set_or_return( :diff, @@ -90,6 +97,18 @@ class Chef ) end + def deployment_strategy(arg=nil) + klass = if arg.kind_of?(String) || arg.kind_of?(Symbol) + lookup_provider_constant(arg) + else + arg + end + set_or_return( + :deployment_strategy, + klass, + :kind_of => [ Class ] + ) + end end end diff --git a/lib/chef/util/backup.rb b/lib/chef/util/backup.rb new file mode 100644 index 0000000000..95c85d9751 --- /dev/null +++ b/lib/chef/util/backup.rb @@ -0,0 +1,84 @@ +# +# Author:: Lamont Granquist (<lamont@opscode.com>) +# Copyright:: Copyright (c) 2013 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +class Chef + class Util + class Backup + attr_reader :new_resource + attr_accessor :path + + def initialize(new_resource, path = nil) + @new_resource = new_resource + @path = path.nil? ? new_resource.path : path + end + + def backup! + if @new_resource.backup != false && @new_resource.backup > 0 && ::File.exist?(path) + do_backup + # Clean up after the number of backups + slice_number = @new_resource.backup + backup_files = sorted_backup_files + if backup_files.length >= @new_resource.backup + remainder = backup_files.slice(slice_number..-1) + remainder.each do |backup_to_delete| + delete_backup(backup_to_delete) + end + end + end + end + + private + + def backup_filename + @backup_filename ||= begin + time = Time.now + savetime = time.strftime("%Y%m%d%H%M%S") + backup_filename = "#{path}.chef-#{savetime}" + backup_filename = backup_filename.sub(/^([A-Za-z]:)/, "") #strip drive letter on Windows + end + end + + def prefix + # 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 + end + + def backup_path + @backup_path ||= ::File.join(prefix, backup_filename) + end + + def do_backup + FileUtils.mkdir_p(::File.dirname(backup_path)) if Chef::Config[:file_backup_path] + FileUtils.cp(path, backup_path, :preserve => true) + Chef::Log.info("#{@new_resource} backed up to #{backup_path}") + end + + def delete_backup(backup_file) + FileUtils.rm(backup_file) + Chef::Log.info("#{@new_resource} removed backup at #{backup_file}") + end + + def sorted_backup_files + Dir[::File.join(prefix, ".#{path}.chef-*")].sort { |a,b| b <=> a } + end + end + end +end + diff --git a/lib/chef/util/diff.rb b/lib/chef/util/diff.rb new file mode 100644 index 0000000000..72459642de --- /dev/null +++ b/lib/chef/util/diff.rb @@ -0,0 +1,103 @@ +# Author:: Lamont Granquist (<lamont@opscode.com>) +# Copyright:: Copyright (c) 2013 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/mixin/shell_out' + +class Chef + class Util + class Diff + include Chef::Mixin::ShellOut + + def for_output + # formatted output to a terminal uses arrays of strings and returns error strings + @diff.nil? ? [ @error ] : @diff + end + + def for_reporting + # caller needs to ensure that new files aren't posted to resource reporting + return nil if @diff.nil? + @diff.join("\\n") + end + + def diff(old_file, new_file) + # indicates calling code bug: caller is reponsible for making certain both + # files exist + raise "old file #{old_file} does not exist" unless File.exists?(old_file) + raise "new file #{new_file} does not exist" unless File.exists?(new_file) + @error = catch (:nodiff) do + do_diff(old_file, new_file) + end + end + + private + + def do_diff(old_file, new_file) + if Chef::Config[:diff_disabled] + throw :nodiff, "(diff output suppressed by config)" + end + + diff_filesize_threshold = Chef::Config[:diff_filesize_threshold] + diff_output_threshold = Chef::Config[:diff_output_threshold] + + if ::File.size(old_file) > diff_filesize_threshold || ::File.size(new_file) > diff_filesize_threshold + throw :nodiff, "(file sizes exceed #{diff_filesize_threshold} bytes, diff output suppressed)" + end + + # MacOSX(BSD?) diff will *sometimes* happily spit out nasty binary diffs + throw :nodiff, "(current file is binary, diff output suppressed)" if is_binary?(old_file) + throw :nodiff, "(new content is binary, diff output suppressed)" if is_binary?(new_file) + + begin + # -u: Unified diff format + result = shell_out("diff -u #{old_file} #{new_file}") + 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! + throw :nodiff, "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 + throw :nodiff, "(long diff of over #{diff_output_threshold} characters, diff output suppressed)" + else + @diff = result.stdout.split("\n") + @diff.delete("\\ No newline at end of file") + # XXX: successful return of the diff is here, we return nil as no error... ugh... + return nil + end + elsif not result.stderr.empty? + throw :nodiff, "Could not determine diff. Error: #{result.stderr}" + else + throw :nodiff, "(no diff)" + end + 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 + + end + end +end + |