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