diff options
32 files changed, 1833 insertions, 963 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 + diff --git a/spec/functional/resource/cookbook_file_spec.rb b/spec/functional/resource/cookbook_file_spec.rb index 9977cd6c99..d61668853c 100644 --- a/spec/functional/resource/cookbook_file_spec.rb +++ b/spec/functional/resource/cookbook_file_spec.rb @@ -32,7 +32,7 @@ describe Chef::Resource::CookbookFile do content end - let(:default_mode) { "600" } + let(:default_mode) { ((0100666 - File.umask) & 07777).to_s(8) } it_behaves_like "a securable resource with reporting" diff --git a/spec/functional/resource/directory_spec.rb b/spec/functional/resource/directory_spec.rb index 9ae0503336..0401f506c3 100644 --- a/spec/functional/resource/directory_spec.rb +++ b/spec/functional/resource/directory_spec.rb @@ -23,7 +23,7 @@ describe Chef::Resource::Directory do let(:directory_base) { "directory_spec" } - let(:default_mode) { "755" } + let(:default_mode) { ((0100777 - File.umask) & 07777).to_s(8) } def create_resource events = Chef::EventDispatch::Dispatcher.new diff --git a/spec/functional/resource/file_spec.rb b/spec/functional/resource/file_spec.rb index 2c18d07520..7da15ff2e5 100644 --- a/spec/functional/resource/file_spec.rb +++ b/spec/functional/resource/file_spec.rb @@ -58,6 +58,22 @@ describe Chef::Resource::File do it_behaves_like "a securable resource with reporting" + describe "when running action :create without content" do + before do + resource_without_content.run_action(:create) + end + + context "and the target file does not exist" do + it "creates the file" do + File.should exist(path) + end + + it "is marked updated by last action" do + resource_without_content.should be_updated_by_last_action + end + end + end + describe "when running action :touch" do context "and the target file does not exist" do before do diff --git a/spec/functional/resource/remote_directory_spec.rb b/spec/functional/resource/remote_directory_spec.rb index b4e26a59b2..10d4d973e9 100644 --- a/spec/functional/resource/remote_directory_spec.rb +++ b/spec/functional/resource/remote_directory_spec.rb @@ -22,7 +22,7 @@ describe Chef::Resource::RemoteDirectory do include_context Chef::Resource::Directory let(:directory_base) { "directory_spec" } - let(:default_mode) { "755" } + let(:default_mode) { ((0100777 - File.umask) & 07777).to_s(8) } def create_resource cookbook_repo = File.expand_path(File.join(CHEF_SPEC_DATA, "cookbooks")) diff --git a/spec/functional/resource/remote_file_spec.rb b/spec/functional/resource/remote_file_spec.rb index fbb921d48c..0354cc1aa3 100644 --- a/spec/functional/resource/remote_file_spec.rb +++ b/spec/functional/resource/remote_file_spec.rb @@ -37,21 +37,7 @@ describe Chef::Resource::RemoteFile do create_resource end - let(:default_mode) do - # TODO: Lots of ugly here :( - # RemoteFile uses FileUtils.cp. FileUtils does a copy by opening the - # destination file and writing to it. Before 1.9.3, it does not preserve - # the mode of the copied file. In 1.9.3 and after, it does. So we have to - # figure out what the default mode ought to be via heuristic. - - t = Tempfile.new("get-the-mode") - path = t.path - path_2 = t.path + "fileutils-mode-test" - FileUtils.cp(path, path_2) - t.close - m = File.stat(path_2).mode - (07777 & m).to_s(8) - end + let(:default_mode) { ((0100666 - File.umask) & 07777).to_s(8) } before(:all) do @server = TinyServer::Manager.new diff --git a/spec/functional/resource/template_spec.rb b/spec/functional/resource/template_spec.rb index 0987aabf05..ae568a496a 100644 --- a/spec/functional/resource/template_spec.rb +++ b/spec/functional/resource/template_spec.rb @@ -49,22 +49,7 @@ describe Chef::Resource::Template do create_resource end - let(:default_mode) do - # TODO: Lots of ugly here :( - # RemoteFile uses FileUtils.cp. FileUtils does a copy by opening the - # destination file and writing to it. Before 1.9.3, it does not preserve - # the mode of the copied file. In 1.9.3 and after, it does. So we have to - # figure out what the default mode ought to be via heuristic. - - t = Tempfile.new("get-the-mode") - path = t.path - path_2 = t.path + "fileutils-mode-test" - FileUtils.cp(path, path_2) - t.close - m = File.stat(path_2).mode - (07777 & m).to_s(8) - end - + let(:default_mode) { ((0100666 - File.umask) & 07777).to_s(8) } it_behaves_like "a file resource" diff --git a/spec/unit/provider/file_spec.rb b/spec/unit/provider/file_spec.rb index 5b1cbbbdb8..7e99e6755b 100644 --- a/spec/unit/provider/file_spec.rb +++ b/spec/unit/provider/file_spec.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"); @@ -16,499 +17,575 @@ # limitations under the License. # - require 'spec_helper' require 'tmpdir' describe Chef::Provider::File do - before(:each) do - @node = Chef::Node.new - @node.name "latte" - @events = Chef::EventDispatch::Dispatcher.new - @run_context = Chef::RunContext.new(@node, {}, @events) - @resource = Chef::Resource::File.new("seattle") - @resource.path(File.expand_path(File.join(CHEF_SPEC_DATA, "templates", "seattle.txt"))) + # Mocksplosion + + let(:node) { double('Chef::Node') } + let(:events) { double('Chef::Events').as_null_object } # mock all the methods + let(:run_context) { double('Chef::RunContext', :node => node, :events => events) } + let(:enclosing_directory) { File.expand_path(File.join(CHEF_SPEC_DATA, "templates")) } + let(:resource_path) { File.expand_path(File.join(enclosing_directory, "seattle.txt")) } - @provider = Chef::Provider::File.new(@resource, @run_context) + let(:resource) do + # need to check for/against mutating state within the new_resource, so don't mock + resource = Chef::Resource::File.new("seattle") + resource.path(resource_path) + resource end - it "should return a Chef::Provider::File" do - @provider.should be_a_kind_of(Chef::Provider::File) + # Subject + + let(:provider) do + Chef::Provider::File.new(resource, run_context) end - it "should store the resource passed to new as new_resource" do - @provider.new_resource.should eql(@resource) + # Filesystem stubs + + def setup_normal_file + File.stub!(:exists?).with(resource_path).and_return(true) + File.stub!(:directory?).with(resource_path).and_return(false) + File.stub!(:directory?).with(enclosing_directory).and_return(true) + File.stub!(:writable?).with(resource_path).and_return(true) + File.stub!(:symlink?).with(resource_path).and_return(false) end - it "should store the node passed to new as node" do - @provider.node.should eql(@node) + def setup_missing_file + File.stub!(:exists?).with(resource_path).and_return(false) + File.stub!(:directory?).with(resource_path).and_return(false) + File.stub!(:directory?).with(enclosing_directory).and_return(true) + File.stub!(:writable?).with(resource_path).and_return(false) + File.stub!(:symlink?).with(resource_path).and_return(false) end - it "should load a current resource based on the one specified at construction" do - @provider.load_current_resource - @provider.current_resource.should be_a_kind_of(Chef::Resource::File) - @provider.current_resource.name.should eql(@resource.name) - @provider.current_resource.path.should eql(@resource.path) - @provider.current_resource.content.should eql(nil) + def setup_symlink + File.stub!(:exists?).with(resource_path).and_return(true) + File.stub!(:directory?).with(resource_path).and_return(false) + File.stub!(:directory?).with(enclosing_directory).and_return(true) + File.stub!(:writable?).with(resource_path).and_return(true) + File.stub!(:symlink?).with(resource_path).and_return(true) end - describe "examining file security metadata on Unix" do - before do - Chef::Platform.stub!(:windows?).and_return(false) - end - it "should collect the current state of the file on the filesystem and populate current_resource" do - # test setup - stat_struct = mock("::File.stat", :mode => 0600, :uid => 0, :gid => 0, :mtime => 10000) - ::File.should_receive(:stat).exactly(1).times.with(@resource.path).and_return(stat_struct) + def setup_unwritable_file + File.stub!(:exists?).with(resource_path).and_return(true) + File.stub!(:directory?).with(resource_path).and_return(false) + File.stub!(:directory?).with(enclosing_directory).and_return(true) + File.stub!(:writable?).with(resource_path).and_return(false) + File.stub!(:symlink?).with(resource_path).and_return(false) + end - # test execution + def setup_missing_enclosing_directory + File.stub!(:exists?).with(resource_path).and_return(false) + File.stub!(:directory?).with(resource_path).and_return(false) + File.stub!(:directory?).with(enclosing_directory).and_return(false) + File.stub!(:writable?).with(resource_path).and_return(false) + File.stub!(:symlink?).with(resource_path).and_return(false) + end - Etc.should_receive(:getgrgid).with(0).and_return(mock("Group Ent", :name => "wheel")) - Etc.should_receive(:getpwuid).with(0).and_return(mock("User Ent", :name => "root")) + # Tests - # test execution - @provider.load_current_resource + it "should return a Chef::Provider::File" do + provider.should be_a_kind_of(Chef::Provider::File) + end - # post-condition checks - @provider.current_resource.mode.should == "0600" - @provider.current_resource.owner.should == "root" - @provider.current_resource.group.should == "wheel" - end + it "should store the resource passed to new as new_resource" do + provider.new_resource.should eql(resource) + end - it "should NOT update the new_resource state with the current_resourse state if new_resource state is already specified" do - # test setup - stat_struct = mock("::File.stat", :mode => 0600, :uid => 0, :gid => 0, :mtime => 10000) - ::File.should_receive(:stat).exactly(1).times.with(@resource.path).and_return(stat_struct) + it "should store the node passed to new as node" do + provider.node.should eql(node) + end - @provider.new_resource.group(1) - @provider.new_resource.owner(1) - @provider.new_resource.mode(0644) + context "when loading the current resource" do - # test execution - @provider.load_current_resource + context "when running load_current_resource and the file exists" do + before do + File.should_receive(:exist?).with(resource_path).at_least(:once).and_return(true) + provider.load_current_resource + end - # post-condition checks - @provider.new_resource.group.should == 1 - @provider.new_resource.owner.should == 1 - @provider.new_resource.mode.should == 0644 - end + it "should load a current resource based on the one specified at construction" do + provider.current_resource.should be_a_kind_of(Chef::Resource::File) + end - context "when the new_resource does not specify the desired access control" do - it "records access control information in the new resource after modifying the file" do - # test setup - stat_struct = mock("::File.stat", :mode => 0600, :uid => 0, :gid => 0, :mtime => 10000) - # called once in update_new_file_state and once in checksum - ::File.should_receive(:stat).once.with(@provider.new_resource.path).and_return(stat_struct) - ::File.should_receive(:directory?).once.with(@provider.new_resource.path).and_return(false) + it "the loaded current_resource name should be the same as the resource name" do + provider.current_resource.name.should eql(resource.name) + end - Etc.should_receive(:getpwuid).with(0).and_return(mock("User Ent", :name => "root")) - Etc.should_receive(:getgrgid).with(0).and_return(mock("Group Ent", :name => "wheel")) + it "the loaded current_resource path should be the same as the resoure path" do + provider.current_resource.path.should eql(resource.path) + end - @provider.new_resource.group(nil) - @provider.new_resource.owner(nil) - @provider.new_resource.mode(nil) + it "the loaded current_resource content should be nil" do + provider.current_resource.content.should eql(nil) + end + end - # test exectution - @provider.update_new_file_state + context "when running load_current_resource and the file does not exist" do + before do + File.should_receive(:exist?).with(resource_path).at_least(:once).and_return(false) + provider.load_current_resource + end - # post-condition checks - @provider.new_resource.group.should == "wheel" - @provider.new_resource.owner.should == "root" - @provider.new_resource.mode.should == "0600" + it "the current_resource should be a Chef::Resource::File" do + provider.current_resource.should be_a_kind_of(Chef::Resource::File) end - end - end - describe "when reporting security metadata on windows" do + it "the current_resource name should be the same as the resource name" do + provider.current_resource.name.should eql(resource.name) + end - it "records the file owner" do - pending - end + it "the current_resource path should be the same as the resource path" do + provider.current_resource.path.should eql(resource.path) + end - it "records rights for each user in the ACL" do - pending + it "the loaded current_resource content should be nil" do + provider.current_resource.content.should eql(nil) + end end - it "records deny_rights for each user in the ACL" do - pending - end - end + context "examining file security metadata on Unix with a file that exists" do + before do + # fake that we're on unix even if we're on windows + Chef::Platform.stub!(:windows?).and_return(false) + # mock up the filesystem to behave like unix + stat_struct = mock("::File.stat", :mode => 0600, :uid => 0, :gid => 0, :mtime => 10000) + File.should_receive(:exist?).with(resource_path).at_least(:once).and_return(true) + File.should_receive(:stat).with(resource.path).at_least(:once).and_return(stat_struct) + Etc.stub!(:getgrgid).with(0).and_return(mock("Group Ent", :name => "wheel")) + Etc.stub!(:getpwuid).with(0).and_return(mock("User Ent", :name => "root")) + end - it "should load a mostly blank current resource if the file specified in new_resource doesn't exist/isn't readable" do - resource = Chef::Resource::File.new("seattle") - resource.path(File.expand_path(File.join(CHEF_SPEC_DATA, "templates", "woot.txt"))) - node = Chef::Node.new - node.name "latte" - provider = Chef::Provider::File.new(resource, @run_context) - provider.load_current_resource - provider.current_resource.should be_a_kind_of(Chef::Resource::File) - provider.current_resource.name.should eql(resource.name) - provider.current_resource.path.should eql(resource.path) - end + context "when the new_resource does not specify any state" do + before do + provider.load_current_resource + end - it "should not backup symbolic links on delete" do - path = File.expand_path(File.join(CHEF_SPEC_DATA, "detroit.txt")) - ::File.open(path, "w") do |file| - file.write("Detroit's not so nice, so you should come to Seattle instead and buy me a beer instead.") - end - @resource = Chef::Resource::File.new("detroit") - @resource.path(path) - @node = Chef::Node.new - @node.name "latte" - @provider = Chef::Provider::File.new(@resource, @run_context) - - ::File.stub!(:symlink?).and_return(true) - @provider.should_not_receive(:backup) - @provider.run_action(:delete) - @resource.should be_updated_by_last_action - end + it "should load the permissions into the current_resource" do + provider.current_resource.mode.should == "0600" + provider.current_resource.owner.should == "root" + provider.current_resource.group.should == "wheel" + end - it "should compare the current content with the requested content" do - @provider.load_current_resource + it "should not set the new_resource permissions" do + provider.new_resource.group.should be_nil + provider.new_resource.owner.should be_nil + provider.new_resource.mode.should be_nil + end + end - @provider.new_resource.content "foobar" - @provider.compare_content.should eql(false) + context "when the new_resource explicitly specifies resource state as numbers" do + before do + resource.owner(1) + resource.group(1) + resource.mode(0644) + provider.load_current_resource + end - @provider.new_resource.content IO.read(@resource.path) - @provider.compare_content.should eql(true) - end + it "should load the permissions into the current_resource as numbers (BUT DOESN'T, BUG?)" do + # FIXME: inconsistency, hmmmm.... + provider.current_resource.mode.should == "0600" + provider.current_resource.owner.should == 0 + provider.current_resource.group.should == 0 + end - it "should set the content of the file to the requested content" do - io = StringIO.new - @provider.load_current_resource - @provider.new_resource.content "foobar" - @provider.should_receive(:diff_current_from_content).and_return("") - @provider.should_receive(:backup) - # checksum check - File.should_receive(:open).with(@provider.new_resource.path, "rb").and_yield(io) - File.should_receive(:open).with(@provider.new_resource.path, "w").and_yield(io) - @provider.set_content - io.string.should == "foobar" - end + it "should not set the new_resource permissions" do + provider.new_resource.group.should == 1 + provider.new_resource.owner.should == 1 + provider.new_resource.mode.should == 0644 + end + end - it "should not set the content of the file if it already matches the requested content" do - @provider.load_current_resource - @provider.new_resource.content IO.read(@resource.path) - # Checksum check: - File.should_receive(:open).with(@resource.path, "rb").and_yield(StringIO.new(@resource.content)) - File.should_not_receive(:open).with(@provider.new_resource.path, "w") - lambda { @provider.set_content }.should_not raise_error - @resource.should_not be_updated_by_last_action - end + context "when the new_resource explicitly specifies resource state as symbols" do + before do + resource.owner("macklemore") + resource.group("seattlehiphop") + resource.mode("0321") + provider.load_current_resource + end - it "should create the file if it is missing, then set the attributes on action_create" do - @provider.load_current_resource - @provider.stub!(:update_new_file_state) - @provider.new_resource.stub!(:path).and_return(File.join(Dir.tmpdir, "monkeyfoo")) - @provider.access_controls.should_receive(:set_all) - @provider.should_receive(:diff_current_from_content).and_return("") - File.stub!(:open).and_return(1) - #File.should_receive(:directory?).with("/tmp").and_return(true) - File.should_receive(:open).with(@provider.new_resource.path, "w+") - @provider.run_action(:create) - @resource.should be_updated_by_last_action - end + it "should load the permissions into the current_resource as symbols" do + provider.current_resource.mode.should == "0600" + provider.current_resource.owner.should == "root" + provider.current_resource.group.should == "wheel" + end - it "should create the file with the proper content if it is missing, then set attributes on action_create" do - io = StringIO.new - @provider.load_current_resource - @provider.new_resource.content "foobar" - @provider.new_resource.stub!(:path).and_return(File.join(Dir.tmpdir, "monkeyfoo")) - @provider.should_receive(:diff_current_from_content).and_return("") - @provider.stub!(:update_new_file_state) - File.should_receive(:open).with(@provider.new_resource.path, "w+").and_yield(io) - @provider.access_controls.should_receive(:set_all) - @provider.run_action(:create) - io.string.should == "foobar" - @resource.should be_updated_by_last_action - end + it "should not set the new_resource permissions" do + provider.new_resource.group.should == "seattlehiphop" + provider.new_resource.owner.should == "macklemore" + provider.new_resource.mode.should == "0321" + end + end - it "should delete the file if it exists and is writable on action_delete" do - @provider.new_resource.stub!(:path).and_return(File.join(Dir.tmpdir, "monkeyfoo")) - @provider.stub!(:backup).and_return(true) - File.should_receive("exists?").exactly(2).times.with(@provider.new_resource.path).and_return(true) - File.should_receive("writable?").with(@provider.new_resource.path).and_return(true) - File.should_receive(:delete).with(@provider.new_resource.path).and_return(true) - @provider.run_action(:delete) - @resource.should be_updated_by_last_action - end + end - it "should not raise an error if it cannot delete the file because it does not exist" do - @provider.new_resource.stub!(:path).and_return(File.join(Dir.tmpdir, "monkeyfoo")) - @provider.stub!(:backup).and_return(true) - File.should_receive("exists?").exactly(2).times.with(@provider.new_resource.path).and_return(false) - lambda { @provider.run_action(:delete) }.should_not raise_error() - @resource.should_not be_updated_by_last_action - end + context "examining file security metadata on Unix with a file that does not exist" do + before do + # fake that we're on unix even if we're on windows + Chef::Platform.stub!(:windows?).and_return(false) + File.should_receive(:exist?).with(resource_path).at_least(:once).and_return(false) + end - it "should update the atime/mtime on action_touch" do - @provider.load_current_resource - @provider.new_resource.stub!(:path).and_return(File.join(Dir.tmpdir, "monkeyfoo")) - @provider.should_receive(:diff_current_from_content).and_return("") - @provider.stub!(:update_new_file_state) - File.should_receive(:utime).once.and_return(1) - File.stub!(:open).and_return(1) - @provider.access_controls.should_receive(:set_all).once - @provider.run_action(:touch) - @resource.should be_updated_by_last_action - end + context "when the new_resource does not specify any state" do + before do + provider.load_current_resource + end - it "should keep 1 backup copy if specified" do - @provider.load_current_resource - @provider.new_resource.stub!(:path).and_return("/tmp/s-20080705111233") - @provider.new_resource.stub!(:backup).and_return(1) - Dir.stub!(:[]).and_return([ "/tmp/s-20080705111233", "/tmp/s-20080705111232", "/tmp/s-20080705111223"]) - FileUtils.should_receive(:rm).with("/tmp/s-20080705111223").once.and_return(true) - FileUtils.should_receive(:rm).with("/tmp/s-20080705111232").once.and_return(true) - FileUtils.stub!(:cp).and_return(true) - FileUtils.stub!(:mkdir_p).and_return(true) - File.stub!(:exist?).and_return(true) - @provider.backup - end + it "the current_resource permissions should be nil" do + provider.current_resource.mode.should be_nil + provider.current_resource.owner.should be_nil + provider.current_resource.group.should be_nil + end - it "should backup a file no more than :backup times" do - @provider.load_current_resource - @provider.new_resource.stub!(:path).and_return("/tmp/s-20080705111233") - @provider.new_resource.stub!(:backup).and_return(2) - Dir.stub!(:[]).and_return([ "/tmp/s-20080705111233", "/tmp/s-20080705111232", "/tmp/s-20080705111223"]) - FileUtils.should_receive(:rm).with("/tmp/s-20080705111223").once.and_return(true) - FileUtils.stub!(:cp).and_return(true) - FileUtils.stub!(:mkdir_p).and_return(true) - File.stub!(:exist?).and_return(true) - @provider.backup - end + it "should not set the new_resource permissions" do + provider.new_resource.group.should be_nil + provider.new_resource.owner.should be_nil + provider.new_resource.mode.should be_nil + end + end - it "should not attempt to backup a file if :backup == 0" do - @provider.load_current_resource - @provider.new_resource.stub!(:path).and_return("/tmp/s-20080705111233") - @provider.new_resource.stub!(:backup).and_return(0) - FileUtils.stub!(:cp).and_return(true) - File.stub!(:exist?).and_return(true) - FileUtils.should_not_receive(:cp) - @provider.backup - end + context "when the new_resource explicitly specifies resource state" do + before do + resource.owner(63945) + resource.group(51948) + resource.mode(0123) + provider.load_current_resource + end - it "should put the backup backup file in the directory specified by Chef::Config[:file_backup_path]" do - @provider.load_current_resource - @provider.new_resource.stub!(:path).and_return("/tmp/s-20080705111233") - @provider.new_resource.stub!(:backup).and_return(1) - Chef::Config.stub!(:[]).with(:file_backup_path).and_return("/some_prefix") - Dir.stub!(:[]).and_return([ "/some_prefix/tmp/s-20080705111233", "/some_prefix/tmp/s-20080705111232", "/some_prefix/tmp/s-20080705111223"]) - FileUtils.should_receive(:mkdir_p).with("/some_prefix/tmp").once - FileUtils.should_receive(:rm).with("/some_prefix/tmp/s-20080705111232").once.and_return(true) - FileUtils.should_receive(:rm).with("/some_prefix/tmp/s-20080705111223").once.and_return(true) - FileUtils.stub!(:cp).and_return(true) - File.stub!(:exist?).and_return(true) - @provider.backup - end + it "the current_resource permissions should be nil" do + provider.current_resource.mode.should be_nil + provider.current_resource.owner.should be_nil + provider.current_resource.group.should be_nil + end - it "should strip the drive letter from the backup resource path (for Windows platforms)" do - @provider.load_current_resource - @provider.new_resource.stub!(:path).and_return("C:/tmp/s-20080705111233") - @provider.new_resource.stub!(:backup).and_return(1) - Chef::Config.stub!(:[]).with(:file_backup_path).and_return("C:/some_prefix") - Dir.stub!(:[]).and_return([ "C:/some_prefix/tmp/s-20080705111233", "C:/some_prefix/tmp/s-20080705111232", "C:/some_prefix/tmp/s-20080705111223"]) - FileUtils.should_receive(:mkdir_p).with("C:/some_prefix/tmp").once - FileUtils.should_receive(:rm).with("C:/some_prefix/tmp/s-20080705111232").once.and_return(true) - FileUtils.should_receive(:rm).with("C:/some_prefix/tmp/s-20080705111223").once.and_return(true) - FileUtils.stub!(:cp).and_return(true) - File.stub!(:exist?).and_return(true) - @provider.backup + it "should not set the new_resource permissions" do + provider.new_resource.group.should == 51948 + provider.new_resource.owner.should == 63945 + provider.new_resource.mode.should == 0123 + end + end + end end - it "should keep the same ownership on backed up files" do - @provider.load_current_resource - @provider.new_resource.stub!(:path).and_return("/tmp/s-20080705111233") - @provider.new_resource.stub!(:backup).and_return(1) - Chef::Config.stub!(:[]).with(:file_backup_path).and_return("/some_prefix") - Dir.stub!(:[]).and_return([ "/some_prefix/tmp/s-20080705111233", "/some_prefix/tmp/s-20080705111232", "/some_prefix/tmp/s-20080705111223"]) - FileUtils.stub!(:mkdir_p).and_return(true) - FileUtils.stub!(:rm).and_return(true) - File.stub!(:exist?).and_return(true) - Time.stub!(:now).and_return(Time.at(1272147455).getgm) - FileUtils.should_receive(:cp).with("/tmp/s-20080705111233", "/some_prefix/tmp/s-20080705111233.chef-20100424221735", {:preserve => true}).and_return(true) - @provider.backup - end + context "when loading the new_resource after the run" do - describe "when the enclosing directory does not exist" do before do - @resource.path("/tmp/no-such-path/file.txt") + # fake that we're on unix even if we're on windows + Chef::Platform.stub!(:windows?).and_return(false) + # mock up the filesystem to behave like unix + stat_struct = mock("::File.stat", :mode => 0600, :uid => 0, :gid => 0, :mtime => 10000) + File.stub!(:stat).with(resource.path).and_return(stat_struct) + Etc.stub!(:getgrgid).with(0).and_return(mock("Group Ent", :name => "wheel")) + Etc.stub!(:getpwuid).with(0).and_return(mock("User Ent", :name => "root")) + provider.send(:load_resource_attributes_from_file, resource) end - it "raises a specific error describing the problem" do - lambda {@provider.run_action(:create)}.should raise_error(Chef::Exceptions::EnclosingDirectoryDoesNotExist) + it "new_resource should record the new permission information" do + provider.new_resource.group.should == "wheel" + provider.new_resource.owner.should == "root" + provider.new_resource.mode.should == "0600" end end - describe "when creating a file which may be missing" do - it "should not call action create if the file exists" do - @resource.path(File.expand_path(File.join(CHEF_SPEC_DATA, "templates", "seattle.txt"))) - @provider = Chef::Provider::File.new(@resource, @run_context) - File.should_not_receive(:open) - @provider.run_action(:create_if_missing) - @resource.should_not be_updated_by_last_action + context "when reporting security metadata on windows (FIXME: moar tests)" do + + it "records the file owner" do + pending + end + + it "records rights for each user in the ACL" do + pending end - it "should call action create if the does not file exist" do - @resource.path("/tmp/example-dir/non_existant_file") - @provider = Chef::Provider::File.new(@resource, @run_context) - @provider.should_receive(:diff_current_from_content).and_return("") - ::File.stub!(:exists?).with(@resource.path).and_return(false) - ::File.stub!(:directory?).with("/tmp/example-dir/non_existant_file").and_return(false) - ::File.stub!(:directory?).with("/tmp/example-dir").and_return(true) - @provider.stub!(:update_new_file_state) - io = StringIO.new - File.should_receive(:open).with(@provider.new_resource.path, "w+").and_yield(io) - #@provider.should_receive(:action_create).and_return(true) - @provider.run_action(:create_if_missing) - @resource.should be_updated_by_last_action + it "records deny_rights for each user in the ACL" do + pending end end - describe "when a diff is requested", :uses_diff => true do - - before(:each) do - @original_config = Chef::Config.hash_dup + context "define_resource_requirements" do + context "when the enclosing directory does not exist" do + before { setup_missing_enclosing_directory } + + [:create, :create_if_missing, :touch].each do |action| + describe "action #{action}" do + it "raises EnclosingDirectoryDoesNotExist" do + lambda {provider.run_action(action)}.should raise_error(Chef::Exceptions::EnclosingDirectoryDoesNotExist) + end + + it "does not raise an exception in why-run mode" do + Chef::Config[:why_run] = true + lambda {provider.run_action(action)}.should_not raise_error(Chef::Exceptions::EnclosingDirectoryDoesNotExist) + Chef::Config[:why_run] = false + end + end + end end - after(:each) do - Chef::Config.configuration = @original_config if @original_config + context "when the file exists but is not deletable" do + before { setup_unwritable_file } + + it "action delete raises InsufficientPermissions" do + lambda {provider.run_action(:delete)}.should raise_error(Chef::Exceptions::InsufficientPermissions) + end + + it "action delete also raises InsufficientPermissions in why-run mode" do + Chef::Config[:why_run] = true + lambda {provider.run_action(:delete)}.should raise_error(Chef::Exceptions::InsufficientPermissions) + Chef::Config[:why_run] = false + end end + end - describe "when identifying files as binary or text" do + context "action create" do + it "should create the file, update its contents and then set the acls on the file" do + setup_missing_file + provider.should_receive(:do_create_file) + provider.should_receive(:do_contents_changes) + provider.should_receive(:do_acl_changes) + provider.should_receive(:load_resource_attributes_from_file).twice # current_resource + new_resource + provider.run_action(:create) + end - it "should identify zero-length files as text" do - Tempfile.open("some-temp") do |file| - @resource.path(file.path) - @provider = Chef::Provider::File.new(@resource, @run_context) - @provider.is_binary?(file.path).should be_false + context "do_create_file" do + context "when the file exists" do + before { setup_normal_file } + it "should not create the file" do + provider.deployment_strategy.should_not_receive(:create).with(resource_path) + provider.send(:do_create_file) + provider.send(:file_created?).should == false end end - - it "should correctly identify text files as being text" do - Tempfile.open("some-temp") do |file| - @resource.path(file.path) - file.puts("This is a text file.") - file.puts("That has a couple of lines in it.") - file.puts("And lets make sure that other printable chars work too: ~!@\#$%^&*()`:\"<>?{}|_+,./;'[]\\-=") - file.close - @provider = Chef::Provider::File.new(@resource, @run_context) - @provider.is_binary?(file.path).should be_false + context "when the file does not exist" do + before { setup_missing_file } + it "should create the file" do + provider.deployment_strategy.should_receive(:create).with(resource_path) + provider.send(:do_create_file) + provider.send(:file_created?).should == true end end + end + + context "do_contents_changes" do + context "when there is content to deploy" do + before do + tempfile = double('Tempfile', :path => "/tmp/foo-bar-baz") + provider.send(:content).should_receive(:tempfile).at_least(:once).and_return(tempfile) + File.should_receive(:exists?).with("/tmp/foo-bar-baz").and_return(true) + tempfile.should_receive(:unlink).once + end - it "should identify a null-terminated string as binary" do - Tempfile.open("some-temp") do |file| - @resource.path(file.path) - file.write("This is a binary file.\0") - file.close - @provider = Chef::Provider::File.new(@resource, @run_context) - @provider.is_binary?(file.path).should be_true + context "when the contents have changed" do + let (:tempfile_path) { "/tmp/foo-bar-baz" } + let (:tempfile_md5) { "71f3811d0472fbef15d90a779615b254" } + let (:diff_for_reporting) { "+++\n---\n+foo\n-bar\n" } + before do + provider.stub!(:contents_changed?).and_return(true) + diff = double('Diff', :for_output => ['+++','---','+foo','-bar'], + :for_reporting => diff_for_reporting ) + diff.stub!(:diff).with(resource_path, tempfile_path).and_return(true) + provider.should_receive(:diff).at_least(:once).and_return(diff) + provider.should_receive(:checksum).with(tempfile_path).and_return(tempfile_md5) + provider.deployment_strategy.should_receive(:deploy).with(tempfile_path, resource_path) + end + context "when the file was created" do + before { provider.should_receive(:file_created?).at_least(:once).and_return(true) } + it "does not backup the file and does not produce a diff for reporting" do + provider.should_not_receive(:backup) + provider.send(:do_contents_changes) + resource.diff.should be_nil + end + end + context "when the file was not created" do + before { provider.should_receive(:file_created?).at_least(:once).and_return(false) } + it "backs up the file and produces a diff for reporting" do + provider.should_receive(:backup) + provider.send(:do_contents_changes) + resource.diff.should == diff_for_reporting + end + end end + + it "does nothing when the contents have not changed" do + provider.stub!(:contents_changed?).and_return(false) + provider.should_not_receive(:diff) + provider.send(:do_contents_changes) + end + end + + it "does nothing when there is no content to deploy (tempfile returned from contents is nil)" do + provider.send(:content).should_receive(:tempfile).at_least(:once).and_return(nil) + provider.should_not_receive(:diff) + lambda{ provider.send(:do_contents_changes) }.should_not raise_error end + it "raises an exception when the content object returns a tempfile with a nil path" do + tempfile = double('Tempfile', :path => nil) + provider.send(:content).should_receive(:tempfile).at_least(:once).and_return(tempfile) + lambda{ provider.send(:do_contents_changes) }.should raise_error + end + + it "raises an exception when the content object returns a tempfile that does not exist" do + tempfile = double('Tempfile', :path => "/tmp/foo-bar-baz") + provider.send(:content).should_receive(:tempfile).at_least(:once).and_return(tempfile) + File.should_receive(:exists?).with("/tmp/foo-bar-baz").and_return(false) + lambda{ provider.send(:do_contents_changes) }.should raise_error + end end - it "should not return diff output when chef config has disabled it" do - Chef::Config[:diff_disabled] = true - Tempfile.open("some-temp") do |file| - @resource.path(file.path) - @provider = Chef::Provider::File.new(@resource, @run_context) - @provider.load_current_resource - result = @provider.diff_current_from_content "foo baz" - result.should == [ "(diff output suppressed by config)" ] - @resource.diff.should be_nil + context "do_acl_changes" do + it "needs tests" do + pending end end - it "should not return diff output when there is no new file to compare it to" do - Tempfile.open("some-temp") do |file| - Tempfile.open("other-temp") do |missing_file| - missing_path = missing_file.path - missing_file.close - missing_file.unlink - @resource.path(file.path) - @provider = Chef::Provider::File.new(@resource, @run_context) - @provider.load_current_resource - result = @provider.diff_current missing_path - result.should == [ "(no temp file with new content, diff output suppressed)" ] - @resource.diff.should be_nil + # it "should compare the current content with the requested content" do + # @provider.load_current_resource + # + # @provider.new_resource.content "foobar" + # @provider.compare_content.should eql(false) + # + # @provider.new_resource.content IO.read(@resource.path) + # @provider.compare_content.should eql(true) + # end + # + # it "should set the content of the file to the requested content" do + # io = StringIO.new + # @provider.load_current_resource + # @provider.new_resource.content "foobar" + # @provider.should_receive(:diff_current_from_content).and_return("") + # @provider.should_receive(:backup) + # # checksum check + # File.should_receive(:open).with(@provider.new_resource.path, "rb").and_yield(io) + # File.should_receive(:open).with(@provider.new_resource.path, "w").and_yield(io) + # @provider.set_content + # io.string.should == "foobar" + # end + # + # it "should not set the content of the file if it already matches the requested content" do + # @provider.load_current_resource + # @provider.new_resource.content IO.read(@resource.path) + # # Checksum check: + # File.should_receive(:open).with(@resource.path, "rb").and_yield(StringIO.new(@resource.content)) + # File.should_not_receive(:open).with(@provider.new_resource.path, "w") + # lambda { @provider.set_content }.should_not raise_error + # @resource.should_not be_updated_by_last_action + # end + # + # it "should create the file if it is missing, then set the attributes on action_create" do + # @provider.load_current_resource + # @provider.stub!(:update_new_file_state) + # @provider.new_resource.stub!(:path).and_return(File.join(Dir.tmpdir, "monkeyfoo")) + # @provider.access_controls.should_receive(:set_all) + # @provider.should_receive(:diff_current_from_content).and_return("") + # File.stub!(:open).and_return(1) + # #File.should_receive(:directory?).with("/tmp").and_return(true) + # File.should_receive(:open).with(@provider.new_resource.path, "w+") + # @provider.run_action(:create) + # @resource.should be_updated_by_last_action + # end + # + # it "should create the file with the proper content if it is missing, then set attributes on action_create" do + # io = StringIO.new + # @provider.load_current_resource + # @provider.new_resource.content "foobar" + # @provider.new_resource.stub!(:path).and_return(File.join(Dir.tmpdir, "monkeyfoo")) + # @provider.should_receive(:diff_current_from_content).and_return("") + # @provider.stub!(:update_new_file_state) + # File.should_receive(:open).with(@provider.new_resource.path, "w+").and_yield(io) + # @provider.access_controls.should_receive(:set_all) + # @provider.run_action(:create) + # io.string.should == "foobar" + # @resource.should be_updated_by_last_action + # end + end + + context "action delete" do + context "when the file exists" do + context "when the file is writable" do + context "when the file is not a symlink" do + before { setup_normal_file } + it "should backup and delete the file and be updated by the last action" do + provider.should_receive(:backup).at_least(:once).and_return(true) + File.should_receive(:delete).with(resource_path).and_return(true) + provider.run_action(:delete) + resource.should be_updated_by_last_action + end + end + context "when the file is a symlink" do + before { setup_symlink } + it "should not backup the symlink" do + provider.should_not_receive(:backup) + File.should_receive(:delete).with(resource_path).and_return(true) + provider.run_action(:delete) + resource.should be_updated_by_last_action + end + end + end + context "when the file is not writable" do + before { setup_unwritable_file } + it "should not try to backup or delete the file, and should not be updated by last action" do + provider.should_not_receive(:backup) + File.should_not_receive(:delete) + lambda { provider.run_action(:delete) }.should raise_error() + resource.should_not be_updated_by_last_action end end end - it "should produce diff output when the file does not exist yet, but suppress reporting it" do - Tempfile.open("some-temp") do |file| - @resource.path(file.path) - file.close - file.unlink - @provider = Chef::Provider::File.new(@resource, @run_context) - @provider.load_current_resource - result = @provider.diff_current_from_content "foo baz" - result.length.should == 4 - @resource.diff.should be_nil + context "when the file does not exist" do + before { setup_missing_file } + + it "should not try to backup or delete the file, and should not be updated by last action" do + provider.should_not_receive(:backup) + File.should_not_receive(:delete) + lambda { provider.run_action(:delete) }.should_not raise_error() + resource.should_not be_updated_by_last_action end end + end - it "should not produce a diff when the current resource file is above the filesize threshold" do - Chef::Config[:diff_filesize_threshold] = 5 - Tempfile.open("some-temp") do |file| - @resource.path(file.path) - file.puts("this is a line which is longer than 5 characters") - file.flush - @provider = Chef::Provider::File.new(@resource, @run_context) - @provider.load_current_resource - result = @provider.diff_current_from_content "foo" # not longer than 5 - result.should == [ "(file sizes exceed 5 bytes, diff output suppressed)" ] - @resource.diff.should be_nil + context "action touch" do + context "when the file does not exist" do + before { setup_missing_file } + it "should update the atime/mtime on action_touch" do + File.should_receive(:utime).once + provider.should_receive(:action_create) + provider.run_action(:touch) + resource.should be_updated_by_last_action end end - - it "should not produce a diff when the new content is above the filesize threshold" do - Chef::Config[:diff_filesize_threshold] = 5 - Tempfile.open("some-temp") do |file| - @resource.path(file.path) - file.puts("foo") - file.flush - @provider = Chef::Provider::File.new(@resource, @run_context) - @provider.load_current_resource - result = @provider.diff_current_from_content "this is a line that is longer than 5 characters" - result.should == [ "(file sizes exceed 5 bytes, diff output suppressed)" ] - @resource.diff.should be_nil + context "when the file exists" do + before { setup_normal_file } + it "should update the atime/mtime on action_touch" do + File.should_receive(:utime).once + provider.should_receive(:action_create) + provider.run_action(:touch) + resource.should be_updated_by_last_action end end + end - it "should not produce a diff when the generated diff size is above the diff size threshold" do - Chef::Config[:diff_output_threshold] = 5 - Tempfile.open("some-temp") do |file| - @resource.path(file.path) - file.puts("some text to increase the size of the diff") - file.flush - @provider = Chef::Provider::File.new(@resource, @run_context) - @provider.load_current_resource - result = @provider.diff_current_from_content "this is a line that is longer than 5 characters" - result.should == [ "(long diff of over 5 characters, diff output suppressed)" ] - @resource.diff.should be_nil + context "action create_if_missing" do + context "when the file does not exist" do + before { setup_missing_file } + it "should call action_create" do + provider.should_receive(:action_create) + provider.run_action(:create_if_missing) end end - it "should return valid diff output when content does not match the string content provided" do - Tempfile.open("some-temp") do |file| - @resource.path file.path - @provider = Chef::Provider::File.new(@resource, @run_context) - @provider.load_current_resource - result = @provider.diff_current_from_content "foo baz" - # remove the file name info which varies. - result.shift(2) - # Result appearance seems to vary slightly under solaris diff - # So we'll compare the second line which is common to both. - # Solaris: -1,1 +1,0 @@, "+foo baz" - # Linux/Mac: -1,0, +1 @@, "+foo baz" - result.length.should == 2 - result[1].should == "+foo baz" - @resource.diff.should_not be_nil - end + context "when the file exists" do + before { setup_normal_file } + it "should not call action_create" do + provider.should_not_receive(:action_create) + provider.run_action(:create_if_missing) + end end + end + end + diff --git a/spec/unit/util/backup_spec.rb b/spec/unit/util/backup_spec.rb new file mode 100644 index 0000000000..e08d5c846f --- /dev/null +++ b/spec/unit/util/backup_spec.rb @@ -0,0 +1,151 @@ +# +# 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 'spec_helper' +require 'tmpdir' + +describe Chef::Util::Backup do + before(:all) do + @original_config = Chef::Config.configuration + end + + after(:all) do + Chef::Config.configuration.replace(@original_config) + end + + let (:tempfile) do + Tempfile.new("chef-util-backup-spec-test") + end + + before(:each) do + @new_resource = mock("new_resource") + @new_resource.should_receive(:path).at_least(:once).and_return(tempfile.path) + @backup = Chef::Util::Backup.new(@new_resource) + end + + it "should store the resource passed to new as new_resource" do + @backup.new_resource.should eql(@new_resource) + end + + describe "for cases when we don't want to back anything up" do + + before(:each) do + @backup.should_not_receive(:do_backup) + end + + it "should not attempt to backup a file if :backup is false" do + @new_resource.should_receive(:backup).at_least(:once).and_return(false) + @backup.backup! + end + + it "should not attempt to backup a file if :backup == 0" do + @new_resource.should_receive(:backup).at_least(:once).and_return(0) + @backup.backup! + end + + it "should not attempt to backup a file if it does not exist" do + @new_resource.should_receive(:backup).at_least(:once).and_return(1) + File.should_receive(:exist?).with(tempfile.path).at_least(:once).and_return(false) + @backup.backup! + end + + end + + describe "for cases when we want to back things up" do + before(:each) do + @backup.should_receive(:do_backup) + end + + describe "when the number of backups is specified as 1" do + before(:each) do + @new_resource.should_receive(:backup).at_least(:once).and_return(1) + end + + it "should not delete anything if this is the only backup" do + @backup.should_receive(:sorted_backup_files).and_return(['a']) + @backup.should_not_receive(:delete_backup) + @backup.backup! + end + + it "should keep only 1 backup copy" do + @backup.should_receive(:sorted_backup_files).and_return(['a', 'b', 'c']) + @backup.should_receive(:delete_backup).with('b') + @backup.should_receive(:delete_backup).with('c') + @backup.backup! + end + end + + describe "when the number of backups is specified as 2" do + before(:each) do + @new_resource.should_receive(:backup).at_least(:once).and_return(2) + end + + it "should not delete anything if we only have one other backup" do + @backup.should_receive(:sorted_backup_files).and_return(['a', 'b']) + @backup.should_not_receive(:delete_backup) + @backup.backup! + end + + it "should keep only 2 backup copies" do + @backup.should_receive(:sorted_backup_files).and_return(['a', 'b', 'c', 'd']) + @backup.should_receive(:delete_backup).with('c') + @backup.should_receive(:delete_backup).with('d') + @backup.backup! + end + end + end + + describe "backup_filename" do + it "should return a timestamped path" do + @backup.should_receive(:path).and_return('/a/b/c.txt') + @backup.send(:backup_filename).should =~ %r|^/a/b/c.txt.chef-\d{14}$| + end + it "should strip the drive letter off for windows" do + @backup.should_receive(:path).and_return('c:\a\b\c.txt') + @backup.send(:backup_filename).should =~ %r|^\\a\\b\\c.txt.chef-\d{14}$| + end + it "should strip the drive letter off for windows (with forwardslashes)" do + @backup.should_receive(:path).and_return('c:/a/b/c.txt') + @backup.send(:backup_filename).should =~ %r|^/a/b/c.txt.chef-\d{14}$| + end + end + + describe "backup_path" do + it "uses the file's directory when Chef::Config[:file_backup_path] is nil" do + @backup.should_receive(:path).and_return('/a/b/c.txt') + Chef::Config[:file_backup_path] = nil + @backup.send(:backup_path).should =~ %r|^/a/b/c.txt.chef-\d{14}$| + end + + it "uses the configured Chef::Config[:file_backup_path]" do + @backup.should_receive(:path).and_return('/a/b/c.txt') + Chef::Config[:file_backup_path] = '/backupdir' + @backup.send(:backup_path).should =~ %r|^/backupdir[\\/]+a/b/c.txt.chef-\d{14}$| + end + + it "uses the configured Chef::Config[:file_backup_path] and strips the drive on windows" do + @backup.should_receive(:path).and_return('c:\\a\\b\\c.txt') + Chef::Config[:file_backup_path] = 'c:\backupdir' + @backup.send(:backup_path).should =~ %r|^c:\\backupdir[\\/]+a\\b\\c.txt.chef-\d{14}$| + end + end + + # it "should keep the same ownership on backed up files" do (FIXME: functional test) + +end diff --git a/spec/unit/util/diff_spec.rb b/spec/unit/util/diff_spec.rb new file mode 100644 index 0000000000..ffb0cd6455 --- /dev/null +++ b/spec/unit/util/diff_spec.rb @@ -0,0 +1,254 @@ +# +# 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 'spec_helper' +require 'tmpdir' + +describe Chef::Util::Diff, :uses_diff => true do + before(:all) do + @original_config = Chef::Config.hash_dup + end + + after(:all) do + Chef::Config.configuration = @original_config if @original_config + end + + let!(:old_tempfile) { Tempfile.new("chef-util-diff-spec") } + let!(:new_tempfile) { Tempfile.new("chef-util-diff-spec") } + let!(:old_file) { old_tempfile.path } + let!(:new_file) { new_tempfile.path } + + let(:differ) do # subject + differ = Chef::Util::Diff.new + differ.diff(old_file, new_file) + differ + end + + it "should return a Chef::Util::Diff" do + expect(differ).to be_a_kind_of(Chef::Util::Diff) + end + + it "should raise an exception if the old_file does not exist" do + old_tempfile.unlink + expect { differ.diff(old_file, new_file) }.to raise_error + end + + it "should raise an exception if the new_file does not exist" do + new_tempfile.unlink + expect { differ.diff(old_file, new_file) }.to raise_error + end + + describe "when the two files exist with no content" do + it "calling for_output should return the error message" do + expect(differ.for_output).to eql(["(no diff)"]) + end + + it "calling for_reporting should be nil" do + expect(differ.for_reporting).to be_nil + end + end + + describe "when diffs are disabled" do + before do + Chef::Config[:diff_disabled] = true + end + + after do + Chef::Config[:diff_disabled] = false + end + + it "calling for_output should return the error message" do + expect(differ.for_output).to eql( [ "(diff output suppressed by config)" ] ) + end + + it "calling for_reporting should be nil" do + expect(differ.for_reporting).to be_nil + end + end + + describe "when the old_file has binary content" do + before do + old_tempfile.write("\x01\xff") + old_tempfile.close + end + + it "calling for_output should return the error message" do + expect(differ.for_output).to eql( [ "(current file is binary, diff output suppressed)" ] ) + end + + it "calling for_reporting should be nil" do + expect(differ.for_reporting).to be_nil + end + end + + describe "when the new_file has binary content" do + before do + new_tempfile.write("\x01\xff") + new_tempfile.close + end + + it "calling for_output should return the error message" do + expect(differ.for_output).to eql( [ "(new content is binary, diff output suppressed)" ]) + end + + it "calling for_reporting should be nil" do + expect(differ.for_reporting).to be_nil + end + end + + describe "when testing the diff_filesize_threshold" do + before do + @diff_filesize_threshold_saved = Chef::Config[:diff_filesize_threshold] + Chef::Config[:diff_filesize_threshold] = 10 + end + + after do + Chef::Config[:diff_filesize_threshold] = @diff_filesize_threshold_saved + end + + describe "when the old_file goes over the threshold" do + before do + old_tempfile.write("But thats what you get when Wu-Tang raised you") + old_tempfile.close + end + + it "calling for_output should return the error message" do + expect(differ.for_output).to eql( [ "(file sizes exceed 10 bytes, diff output suppressed)" ]) + end + + it "calling for_reporting should be nil" do + expect(differ.for_reporting).to be_nil + end + end + + describe "when the new_file goes over the threshold" do + before do + new_tempfile.write("But thats what you get when Wu-Tang raised you") + new_tempfile.close + end + + it "calling for_output should return the error message" do + expect(differ.for_output).to eql( [ "(file sizes exceed 10 bytes, diff output suppressed)" ]) + end + + it "calling for_reporting should be nil" do + expect(differ.for_reporting).to be_nil + end + end + end + + describe "when generating a valid diff" do + before do + old_tempfile.write("foo") + old_tempfile.close + new_tempfile.write("bar") + new_tempfile.close + end + + it "calling for_output should return a unified diff" do + differ.for_output.size.should eql(5) + differ.for_output.join("\\n").should match(/^--- .*\\n\+\+\+ .*\\n@@ .* @@\\n-foo\\n\+bar$/) + end + + it "calling for_reporting should return a unified diff" do + differ.for_reporting.should match(/^--- .*\\n\+\+\+ .*\\n@@ .* @@\\n-foo\\n\+bar$/) + end + + describe "when the diff output is too long" do + + before do + @diff_output_threshold_saved = Chef::Config[:diff_output_threshold] + Chef::Config[:diff_output_threshold] = 10 + end + + after do + Chef::Config[:diff_output_threshold] = @diff_output_threshold_saved + end + + it "calling for_output should return the error message" do + expect(differ.for_output).to eql(["(long diff of over 10 characters, diff output suppressed)"]) + end + + it "calling for_reporting should be nil" do + expect(differ.for_reporting).to be_nil + end + end + end + + describe "when errors are thrown from shell_out" do + before do + differ.stub!(:shell_out).and_raise('boom') + differ.diff(old_file, new_file) + end + + it "calling for_output should return the error message" do + expect(differ.for_output).to eql(["Could not determine diff. Error: boom"]) + end + + it "calling for_reporting should be nil" do + expect(differ.for_reporting).to be_nil + end + end + + describe "when shell_out returns stderr output" do + before do + @result = mock('result', :stdout => "", :stderr => "boom") + differ.stub!(:shell_out).and_return(@result) + differ.diff(old_file, new_file) + end + + it "calling for_output should return the error message" do + expect(differ.for_output).to eql(["Could not determine diff. Error: boom"]) + end + + it "calling for_reporting should be nil" do + expect(differ.for_reporting).to be_nil + end + end + + describe "when checking if files are binary or text" do + + it "should identify zero-length files as text" do + Tempfile.new("chef-util-diff-spec") do |file| + differ.is_binary?(file.path).should be_false + end + end + + it "should identify text files as text" do + Tempfile.new("chef-util-diff-spec") do |file| + file.write("This is a text file.") + file.write("With more than one line.") + file.write("And lets make sure that other printable chars work too: ~!@\#$%^&*()`:\"<>?{}|_+,./;'[]\\-=") + file.close + differ.is_binary?(file.path).should be_false + end + end + + it "should identify a null-terminated string files as binary" do + Tempfile.new("chef-util-diff-spec") do |file| + file.write("This is a binary file.\0") + file.close + differ.is_binary?(file.path).should be_false + end + end + + end + +end + |