diff --git a/lib/puppet/file_system.rb b/lib/puppet/file_system.rb index c8312a289..ab4873eb1 100644 --- a/lib/puppet/file_system.rb +++ b/lib/puppet/file_system.rb @@ -1,343 +1,357 @@ module Puppet::FileSystem require 'puppet/file_system/path_pattern' require 'puppet/file_system/file_impl' require 'puppet/file_system/memory_file' require 'puppet/file_system/memory_impl' require 'puppet/file_system/tempfile' # create instance of the file system implementation to use for the current platform @impl = if RUBY_VERSION =~ /^1\.8/ require 'puppet/file_system/file18' Puppet::FileSystem::File18 elsif Puppet::Util::Platform.windows? require 'puppet/file_system/file19windows' Puppet::FileSystem::File19Windows else require 'puppet/file_system/file19' Puppet::FileSystem::File19 end.new() # Allows overriding the filesystem for the duration of the given block. # The filesystem will only contain the given file(s). # # @param files [Puppet::FileSystem::MemoryFile] the files to have available # # @api private # def self.overlay(*files, &block) old_impl = @impl @impl = Puppet::FileSystem::MemoryImpl.new(*files) yield ensure @impl = old_impl end # Opens the given path with given mode, and options and optionally yields it to the given block. # # @api public # def self.open(path, mode, options, &block) @impl.open(assert_path(path), mode, options, &block) end # @return [Object] The directory of this file as an opaque handle # # @api public # def self.dir(path) @impl.dir(assert_path(path)) end # @return [String] The directory of this file as a String # # @api public # def self.dir_string(path) @impl.path_string(@impl.dir(assert_path(path))) end # @return [Boolean] Does the directory of the given path exist? def self.dir_exist?(path) @impl.exist?(@impl.dir(assert_path(path))) end # Creates all directories down to (inclusive) the dir of the given path def self.dir_mkpath(path) @impl.mkpath(@impl.dir(assert_path(path))) end # @return [Object] the name of the file as a opaque handle # # @api public # def self.basename(path) @impl.basename(assert_path(path)) end # @return [String] the name of the file # # @api public # def self.basename_string(path) @impl.path_string(@impl.basename(assert_path(path))) end # @return [Integer] the size of the file # # @api public # def self.size(path) @impl.size(assert_path(path)) end # Allows exclusive updates to a file to be made by excluding concurrent # access using flock. This means that if the file is on a filesystem that # does not support flock, this method will provide no protection. # # While polling to aquire the lock the process will wait ever increasing # amounts of time in order to prevent multiple processes from wasting # resources. # # @param path [Pathname] the path to the file to operate on # @param mode [Integer] The mode to apply to the file if it is created # @param options [Integer] Extra file operation mode information to use # (defaults to read-only mode) # @param timeout [Integer] Number of seconds to wait for the lock (defaults to 300) # @yield The file handle, in read-write mode # @return [Void] # @raise [Timeout::Error] If the timeout is exceeded while waiting to acquire the lock # # @api public # def self.exclusive_open(path, mode, options = 'r', timeout = 300, &block) @impl.exclusive_open(assert_path(path), mode, options, timeout, &block) end # Processes each line of the file by yielding it to the given block # # @api public # def self.each_line(path, &block) @impl.each_line(assert_path(path), &block) end # @return [String] The contents of the file # # @api public # def self.read(path) @impl.read(assert_path(path)) end # @return [String] The binary contents of the file # # @api public # def self.binread(path) @impl.binread(assert_path(path)) end # Determines if a file exists by verifying that the file can be stat'd. # Will follow symlinks and verify that the actual target path exists. # # @return [Boolean] true if the named file exists. # # @api public # def self.exist?(path) @impl.exist?(assert_path(path)) end # Determines if a file is a directory. # # @return [Boolean] true if the given file is a directory. # # @api public def self.directory?(path) @impl.directory?(assert_path(path)) end # Determines if a file is executable. # # @todo Should this take into account extensions on the windows platform? # # @return [Boolean] true if this file can be executed # # @api public # def self.executable?(path) @impl.executable?(assert_path(path)) end # @return [Boolean] Whether the file is writable by the current process # # @api public # def self.writable?(path) @impl.writable?(assert_path(path)) end # Touches the file. On most systems this updates the mtime of the file. # # @api public # def self.touch(path) @impl.touch(assert_path(path)) end # Creates directories for all parts of the given path. # # @api public # def self.mkpath(path) @impl.mkpath(assert_path(path)) end # @return [Array] references to all of the children of the given # directory path, excluding `.` and `..`. # @api public def self.children(path) @impl.children(assert_path(path)) end # Creates a symbolic link dest which points to the current file. # If dest already exists: # # * and is a file, will raise Errno::EEXIST # * and is a directory, will return 0 but perform no action # * and is a symlink referencing a file, will raise Errno::EEXIST # * and is a symlink referencing a directory, will return 0 but perform no action # # With the :force option set to true, when dest already exists: # # * and is a file, will replace the existing file with a symlink (DANGEROUS) # * and is a directory, will return 0 but perform no action # * and is a symlink referencing a file, will modify the existing symlink # * and is a symlink referencing a directory, will return 0 but perform no action # # @param dest [String] The path to create the new symlink at # @param [Hash] options the options to create the symlink with # @option options [Boolean] :force overwrite dest # @option options [Boolean] :noop do not perform the operation # @option options [Boolean] :verbose verbose output # # @raise [Errno::EEXIST] dest already exists as a file and, :force is not set # # @return [Integer] 0 # # @api public # def self.symlink(path, dest, options = {}) @impl.symlink(assert_path(path), dest, options) end # @return [Boolean] true if the file is a symbolic link. - # + # # @api public # def self.symlink?(path) @impl.symlink?(assert_path(path)) end # @return [String] the name of the file referenced by the given link. # # @api public # def self.readlink(path) @impl.readlink(assert_path(path)) end # Deletes the given paths, returning the number of names passed as arguments. # See also Dir::rmdir. # # @raise an exception on any error. # # @return [Integer] the number of paths passed as arguments # # @api public # def self.unlink(*paths) @impl.unlink(*(paths.map {|p| assert_path(p) })) end # @return [File::Stat] object for the named file. # # @api public # def self.stat(path) @impl.stat(assert_path(path)) end # @return [Integer] the size of the file # # @api public # def self.size(path) @impl.size(assert_path(path)) end # @return [File::Stat] Same as stat, but does not follow the last symbolic # link. Instead, reports on the link itself. # # @api public # def self.lstat(path) @impl.lstat(assert_path(path)) end # Compares the contents of this file against the contents of a stream. # # @param stream [IO] The stream to compare the contents against # @return [Boolean] Whether the contents were the same # # @api public # def self.compare_stream(path, stream) @impl.compare_stream(assert_path(path), stream) end # Produces an opaque pathname "handle" object representing the given path. # Different implementations of the underlying file system may use different runtime # objects. The produced "handle" should be used in all other operations # that take a "path". No operation should be directly invoked on the returned opaque object # # @param path [String] The string representation of the path # @return [Object] An opaque path handle on which no operations should be directly performed # # @api public # def self.pathname(path) @impl.pathname(path) end # Asserts that the given path is of the expected type produced by #pathname # # @raise [ArgumentError] when path is not of the expected type # # @api public # def self.assert_path(path) @impl.assert_path(path) end # Produces a string representation of the opaque path handle. # # @param path [Object] a path handle produced by {#pathname} # @return [String] a string representation of the path # def self.path_string(path) @impl.path_string(path) end # Create and open a file for write only if it doesn't exist. # # @see Puppet::FileSystem::open # # @raise [Errno::EEXIST] path already exists. # # @api public # def self.exclusive_create(path, mode, &block) @impl.exclusive_create(assert_path(path), mode, &block) end + + # Changes permission bits on the named path to the bit pattern represented + # by mode. + # + # @param mode [Integer] The mode to apply to the file if it is created + # @param path [String] The path to the file, can also accept [PathName] + # + # @raise [Errno::ENOENT]: path doesn't exist + # + # @api public + # + def self.chmod(mode, path) + @impl.chmod(mode, path) + end end diff --git a/lib/puppet/file_system/file19windows.rb b/lib/puppet/file_system/file19windows.rb index 1b33bcad1..bf114bc97 100644 --- a/lib/puppet/file_system/file19windows.rb +++ b/lib/puppet/file_system/file19windows.rb @@ -1,107 +1,111 @@ require 'puppet/file_system/file19' require 'puppet/util/windows' class Puppet::FileSystem::File19Windows < Puppet::FileSystem::File19 def exist?(path) if ! Puppet.features.manages_symlinks? return ::File.exist?(path) end path = path.to_str if path.respond_to?(:to_str) # support WatchedFile path = path.to_s # support String and Pathname begin if Puppet::Util::Windows::File.symlink?(path) path = Puppet::Util::Windows::File.readlink(path) end ! Puppet::Util::Windows::File.stat(path).nil? rescue # generally INVALID_HANDLE_VALUE which means 'file not found' false end end def symlink(path, dest, options = {}) raise_if_symlinks_unsupported dest_exists = exist?(dest) # returns false on dangling symlink dest_stat = Puppet::Util::Windows::File.stat(dest) if dest_exists dest_symlink = Puppet::Util::Windows::File.symlink?(dest) # silent fail to preserve semantics of original FileUtils return 0 if dest_exists && dest_stat.ftype == 'directory' if dest_exists && dest_stat.ftype == 'file' && options[:force] != true raise(Errno::EEXIST, "#{dest} already exists and the :force option was not specified") end if options[:noop] != true ::File.delete(dest) if dest_exists # can only be file Puppet::Util::Windows::File.symlink(path, dest) end 0 end def symlink?(path) return false if ! Puppet.features.manages_symlinks? Puppet::Util::Windows::File.symlink?(path) end def readlink(path) raise_if_symlinks_unsupported Puppet::Util::Windows::File.readlink(path) end def unlink(*file_names) if ! Puppet.features.manages_symlinks? return ::File.unlink(*file_names) end file_names.each do |file_name| file_name = file_name.to_s # handle PathName stat = Puppet::Util::Windows::File.stat(file_name) rescue nil # sigh, Ruby + Windows :( if stat && stat.ftype == 'directory' if Puppet::Util::Windows::File.symlink?(file_name) Dir.rmdir(file_name) else raise Errno::EPERM.new(file_name) end else ::File.unlink(file_name) end end file_names.length end def stat(path) if ! Puppet.features.manages_symlinks? return super end Puppet::Util::Windows::File.stat(path) end def lstat(path) if ! Puppet.features.manages_symlinks? return Puppet::Util::Windows::File.stat(path) end Puppet::Util::Windows::File.lstat(path) end + def chmod(mode, path) + Puppet::Util::Windows::Security.set_mode(mode, path.to_s) + end + private def raise_if_symlinks_unsupported if ! Puppet.features.manages_symlinks? msg = "This version of Windows does not support symlinks. Windows Vista / 2008 or higher is required." raise Puppet::Util::Windows::Error.new(msg) end if ! Puppet::Util::Windows::Process.process_privilege_symlink? Puppet.warning "The current user does not have the necessary permission to manage symlinks." end end end diff --git a/lib/puppet/file_system/file_impl.rb b/lib/puppet/file_system/file_impl.rb index bf8ff31e5..f682a1752 100644 --- a/lib/puppet/file_system/file_impl.rb +++ b/lib/puppet/file_system/file_impl.rb @@ -1,138 +1,141 @@ # Abstract implementation of the Puppet::FileSystem # class Puppet::FileSystem::FileImpl def pathname(path) path.is_a?(Pathname) ? path : Pathname.new(path) end def assert_path(path) return path if path.is_a?(Pathname) # Some paths are string, or (in the case of WatchedFile, it pretends to be one by implementing to_str. # (sigh). # unless path.is_a?(String) || path.respond_to?(:to_str) raise ArgumentError, "FileSystem implementation expected Pathname, got: '#{path.class}'" end # converts String and #to_str to Pathname Pathname.new(path) end def path_string(path) path.to_s end def open(path, mode, options, &block) ::File.open(path, options, mode, &block) end def dir(path) path.dirname end def basename(path) path.basename.to_s end def size(path) path.size end def exclusive_create(path, mode, &block) opt = File::CREAT | File::EXCL | File::WRONLY self.open(path, mode, opt, &block) end def exclusive_open(path, mode, options = 'r', timeout = 300, &block) wait = 0.001 + (Kernel.rand / 1000) written = false while !written ::File.open(path, options, mode) do |rf| if rf.flock(::File::LOCK_EX|::File::LOCK_NB) yield rf written = true else sleep wait timeout -= wait wait *= 2 if timeout < 0 raise Timeout::Error, "Timeout waiting for exclusive lock on #{@path}" end end end end end def each_line(path, &block) ::File.open(path) do |f| f.each_line do |line| yield line end end end def read(path) path.read end def binread(path) raise NotImplementedError end def exist?(path) ::File.exist?(path) end def directory?(path) ::File.directory?(path) end def executable?(path) ::File.executable?(path) end def writable?(path) path.writable? end def touch(path) ::FileUtils.touch(path) end def mkpath(path) path.mkpath end def children(path) path.children end def symlink(path, dest, options = {}) FileUtils.symlink(path, dest, options) end def symlink?(path) File.symlink?(path) end def readlink(path) File.readlink(path) end def unlink(*paths) File.unlink(*paths) end def stat(path) File.stat(path) end def lstat(path) File.lstat(path) end def compare_stream(path, stream) open(path, 0, 'rb') { |this| FileUtils.compare_stream(this, stream) } end + def chmod(mode, path) + FileUtils.chmod(mode, path) + end end diff --git a/lib/puppet/util/windows/file.rb b/lib/puppet/util/windows/file.rb index 6e124b812..15a6ef469 100644 --- a/lib/puppet/util/windows/file.rb +++ b/lib/puppet/util/windows/file.rb @@ -1,264 +1,279 @@ require 'puppet/util/windows' module Puppet::Util::Windows::File require 'ffi' require 'windows/api' def replace_file(target, source) target_encoded = Puppet::Util::Windows::String.wide_string(target.to_s) source_encoded = Puppet::Util::Windows::String.wide_string(source.to_s) flags = 0x1 backup_file = nil result = API.replace_file( target_encoded, source_encoded, backup_file, flags, 0, 0 ) return true if result raise Puppet::Util::Windows::Error.new("ReplaceFile(#{target}, #{source})") end module_function :replace_file MoveFileEx = Windows::API.new('MoveFileExW', 'PPL', 'B') def move_file_ex(source, target, flags = 0) result = MoveFileEx.call(Puppet::Util::Windows::String.wide_string(source.to_s), Puppet::Util::Windows::String.wide_string(target.to_s), flags) return true unless result == 0 raise Puppet::Util::Windows::Error. new("MoveFileEx(#{source}, #{target}, #{flags.to_s(8)})") end module_function :move_file_ex module API extend FFI::Library ffi_lib 'kernel32' ffi_convention :stdcall # http://msdn.microsoft.com/en-us/library/windows/desktop/aa365512(v=vs.85).aspx # BOOL WINAPI ReplaceFile( # _In_ LPCTSTR lpReplacedFileName, # _In_ LPCTSTR lpReplacementFileName, # _In_opt_ LPCTSTR lpBackupFileName, # _In_ DWORD dwReplaceFlags - 0x1 REPLACEFILE_WRITE_THROUGH, # 0x2 REPLACEFILE_IGNORE_MERGE_ERRORS, # 0x4 REPLACEFILE_IGNORE_ACL_ERRORS # _Reserved_ LPVOID lpExclude, # _Reserved_ LPVOID lpReserved # ); attach_function :replace_file, :ReplaceFileW, [:buffer_in, :buffer_in, :buffer_in, :uint, :uint, :uint], :bool # BOOLEAN WINAPI CreateSymbolicLink( # _In_ LPTSTR lpSymlinkFileName, - symbolic link to be created # _In_ LPTSTR lpTargetFileName, - name of target for symbolic link # _In_ DWORD dwFlags - 0x0 target is a file, 0x1 target is a directory # ); # rescue on Windows < 6.0 so that code doesn't explode begin attach_function :create_symbolic_link, :CreateSymbolicLinkW, [:buffer_in, :buffer_in, :uint], :bool rescue LoadError end # DWORD WINAPI GetFileAttributes( # _In_ LPCTSTR lpFileName # ); attach_function :get_file_attributes, :GetFileAttributesW, [:buffer_in], :uint # HANDLE WINAPI CreateFile( # _In_ LPCTSTR lpFileName, # _In_ DWORD dwDesiredAccess, # _In_ DWORD dwShareMode, # _In_opt_ LPSECURITY_ATTRIBUTES lpSecurityAttributes, # _In_ DWORD dwCreationDisposition, # _In_ DWORD dwFlagsAndAttributes, # _In_opt_ HANDLE hTemplateFile # ); attach_function :create_file, :CreateFileW, [:buffer_in, :uint, :uint, :pointer, :uint, :uint, :uint], :uint # http://msdn.microsoft.com/en-us/library/windows/desktop/aa363216(v=vs.85).aspx # BOOL WINAPI DeviceIoControl( # _In_ HANDLE hDevice, # _In_ DWORD dwIoControlCode, # _In_opt_ LPVOID lpInBuffer, # _In_ DWORD nInBufferSize, # _Out_opt_ LPVOID lpOutBuffer, # _In_ DWORD nOutBufferSize, # _Out_opt_ LPDWORD lpBytesReturned, # _Inout_opt_ LPOVERLAPPED lpOverlapped # ); attach_function :device_io_control, :DeviceIoControl, [:uint, :uint, :pointer, :uint, :pointer, :uint, :pointer, :pointer], :bool MAXIMUM_REPARSE_DATA_BUFFER_SIZE = 16384 # REPARSE_DATA_BUFFER # http://msdn.microsoft.com/en-us/library/cc232006.aspx # http://msdn.microsoft.com/en-us/library/windows/hardware/ff552012(v=vs.85).aspx # struct is always MAXIMUM_REPARSE_DATA_BUFFER_SIZE bytes class ReparseDataBuffer < FFI::Struct layout :reparse_tag, :uint, :reparse_data_length, :ushort, :reserved, :ushort, :substitute_name_offset, :ushort, :substitute_name_length, :ushort, :print_name_offset, :ushort, :print_name_length, :ushort, :flags, :uint, # max less above fields dword / uint 4 bytes, ushort 2 bytes :path_buffer, [:uchar, MAXIMUM_REPARSE_DATA_BUFFER_SIZE - 20] end # BOOL WINAPI CloseHandle( # _In_ HANDLE hObject # ); attach_function :close_handle, :CloseHandle, [:uint], :bool end def symlink(target, symlink) flags = File.directory?(target) ? 0x1 : 0x0 result = API.create_symbolic_link(Puppet::Util::Windows::String.wide_string(symlink.to_s), Puppet::Util::Windows::String.wide_string(target.to_s), flags) return true if result raise Puppet::Util::Windows::Error.new( "CreateSymbolicLink(#{symlink}, #{target}, #{flags.to_s(8)})") end module_function :symlink INVALID_FILE_ATTRIBUTES = 0xFFFFFFFF #define INVALID_FILE_ATTRIBUTES (DWORD (-1)) def self.get_file_attributes(file_name) result = API.get_file_attributes(Puppet::Util::Windows::String.wide_string(file_name.to_s)) return result unless result == INVALID_FILE_ATTRIBUTES raise Puppet::Util::Windows::Error.new("GetFileAttributes(#{file_name})") end INVALID_HANDLE_VALUE = -1 #define INVALID_HANDLE_VALUE ((HANDLE)(LONG_PTR)-1) def self.create_file(file_name, desired_access, share_mode, security_attributes, creation_disposition, flags_and_attributes, template_file_handle) result = API.create_file(Puppet::Util::Windows::String.wide_string(file_name.to_s), desired_access, share_mode, security_attributes, creation_disposition, flags_and_attributes, template_file_handle) return result unless result == INVALID_HANDLE_VALUE raise Puppet::Util::Windows::Error.new( "CreateFile(#{file_name}, #{desired_access.to_s(8)}, #{share_mode.to_s(8)}, " + "#{security_attributes}, #{creation_disposition.to_s(8)}, " + "#{flags_and_attributes.to_s(8)}, #{template_file_handle})") end def self.device_io_control(handle, io_control_code, in_buffer = nil, out_buffer = nil) if out_buffer.nil? raise Puppet::Util::Windows::Error.new("out_buffer is required") end result = API.device_io_control( handle, io_control_code, in_buffer, in_buffer.nil? ? 0 : in_buffer.size, out_buffer, out_buffer.size, FFI::MemoryPointer.new(:uint, 1), nil ) return out_buffer if result raise Puppet::Util::Windows::Error.new( "DeviceIoControl(#{handle}, #{io_control_code}, " + "#{in_buffer}, #{in_buffer ? in_buffer.size : ''}, " + "#{out_buffer}, #{out_buffer ? out_buffer.size : ''}") end FILE_ATTRIBUTE_REPARSE_POINT = 0x400 def symlink?(file_name) begin attributes = get_file_attributes(file_name) (attributes & FILE_ATTRIBUTE_REPARSE_POINT) == FILE_ATTRIBUTE_REPARSE_POINT rescue # raised INVALID_FILE_ATTRIBUTES is equivalent to file not found false end end module_function :symlink? GENERIC_READ = 0x80000000 FILE_SHARE_READ = 1 OPEN_EXISTING = 3 FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000 FILE_FLAG_BACKUP_SEMANTICS = 0x02000000 def self.open_symlink(link_name) begin yield handle = create_file( Puppet::Util::Windows::String.wide_string(link_name.to_s), GENERIC_READ, FILE_SHARE_READ, nil, # security_attributes OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, 0) # template_file ensure API.close_handle(handle) if handle end end def readlink(link_name) open_symlink(link_name) do |handle| resolve_symlink(handle) end end module_function :readlink def stat(file_name) file_name = file_name.to_s # accomodate PathName or String stat = File.stat(file_name) + singleton_class = class << stat; self; end + target_path = file_name + if symlink?(file_name) - link_ftype = File.stat(readlink(file_name)).ftype + target_path = readlink(file_name) + link_ftype = File.stat(target_path).ftype + # sigh, monkey patch instance method for instance, and close over link_ftype - singleton_class = class << stat; self; end singleton_class.send(:define_method, :ftype) do link_ftype end end + + singleton_class.send(:define_method, :mode) do + Puppet::Util::Windows::Security.get_mode(target_path) + end + stat end module_function :stat def lstat(file_name) file_name = file_name.to_s # accomodate PathName or String # monkey'ing around! stat = File.lstat(file_name) + + singleton_class = class << stat; self; end + singleton_class.send(:define_method, :mode) do + Puppet::Util::Windows::Security.get_mode(file_name) + end + if symlink?(file_name) def stat.ftype "link" end end stat end module_function :lstat private # http://msdn.microsoft.com/en-us/library/windows/desktop/aa364571(v=vs.85).aspx FSCTL_GET_REPARSE_POINT = 0x900a8 def self.resolve_symlink(handle) # must be multiple of 1024, min 10240 out_buffer = FFI::MemoryPointer.new(API::ReparseDataBuffer.size) device_io_control(handle, FSCTL_GET_REPARSE_POINT, nil, out_buffer) reparse_data = API::ReparseDataBuffer.new(out_buffer) offset = reparse_data[:print_name_offset] length = reparse_data[:print_name_length] result = reparse_data[:path_buffer].to_a[offset, length].pack('C*') result.force_encoding('UTF-16LE').encode(Encoding.default_external) end end diff --git a/spec/integration/configurer_spec.rb b/spec/integration/configurer_spec.rb index 5e65355c5..99eaeaefd 100755 --- a/spec/integration/configurer_spec.rb +++ b/spec/integration/configurer_spec.rb @@ -1,80 +1,81 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/configurer' describe Puppet::Configurer do include PuppetSpec::Files describe "when downloading plugins" do it "should use the :pluginsignore setting, split on whitespace, for ignoring remote files" do Puppet.settings.stubs(:use) resource = Puppet::Type.type(:notify).new :name => "yay" Puppet::Type.type(:file).expects(:new).at_most(2).with do |args| args[:ignore] == Puppet[:pluginsignore].split(/\s+/) end.returns resource configurer = Puppet::Configurer.new configurer.stubs(:download_plugins?).returns true configurer.download_plugins end end describe "when running" do before(:each) do @catalog = Puppet::Resource::Catalog.new @catalog.add_resource(Puppet::Type.type(:notify).new(:title => "testing")) # Make sure we don't try to persist the local state after the transaction ran, # because it will fail during test (the state file is in a not-existing directory) # and we need the transaction to be successful to be able to produce a summary report @catalog.host_config = false @configurer = Puppet::Configurer.new end it "should send a transaction report with valid data" do @configurer.stubs(:save_last_run_summary) Puppet::Transaction::Report.indirection.expects(:save).with do |report, x| report.time.class == Time and report.logs.length > 0 end Puppet[:report] = true @configurer.run :catalog => @catalog end it "should save a correct last run summary" do report = Puppet::Transaction::Report.new("apply") Puppet::Transaction::Report.indirection.stubs(:save) Puppet[:lastrunfile] = tmpfile("lastrunfile") Puppet.settings.setting(:lastrunfile).mode = 0666 Puppet[:report] = true # We only record integer seconds in the timestamp, and truncate # backwards, so don't use a more accurate timestamp in the test. # --daniel 2011-03-07 t1 = Time.now.tv_sec @configurer.run :catalog => @catalog, :report => report t2 = Time.now.tv_sec - file_mode = Puppet.features.microsoft_windows? ? '100644' : '100666' + # sticky bit only applies to directories in windows + file_mode = Puppet.features.microsoft_windows? ? '666' : '100666' Puppet::FileSystem.stat(Puppet[:lastrunfile]).mode.to_s(8).should == file_mode summary = nil File.open(Puppet[:lastrunfile], "r") do |fd| summary = YAML.load(fd.read) end summary.should be_a(Hash) %w{time changes events resources}.each do |key| summary.should be_key(key) end summary["time"].should be_key("notify") summary["time"]["last_run"].should be_between(t1, t2) end end end diff --git a/spec/integration/type/file_spec.rb b/spec/integration/type/file_spec.rb index cf494ba2f..f864fe06a 100755 --- a/spec/integration/type/file_spec.rb +++ b/spec/integration/type/file_spec.rb @@ -1,1373 +1,1375 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet_spec/files' if Puppet.features.microsoft_windows? require 'puppet/util/windows' class WindowsSecurity extend Puppet::Util::Windows::Security end end describe Puppet::Type.type(:file) do include PuppetSpec::Files let(:catalog) { Puppet::Resource::Catalog.new } let(:path) do # we create a directory first so backups of :path that are stored in # the same directory will also be removed after the tests parent = tmpdir('file_spec') File.join(parent, 'file_testing') end let(:dir) do # we create a directory first so backups of :path that are stored in # the same directory will also be removed after the tests parent = tmpdir('file_spec') File.join(parent, 'dir_testing') end if Puppet.features.posix? def set_mode(mode, file) File.chmod(mode, file) end def get_mode(file) Puppet::FileSystem.lstat(file).mode end def get_owner(file) Puppet::FileSystem.lstat(file).uid end def get_group(file) Puppet::FileSystem.lstat(file).gid end else class SecurityHelper extend Puppet::Util::Windows::Security end def set_mode(mode, file) SecurityHelper.set_mode(mode, file) end def get_mode(file) SecurityHelper.get_mode(file) end def get_owner(file) SecurityHelper.get_owner(file) end def get_group(file) SecurityHelper.get_group(file) end def get_aces_for_path_by_sid(path, sid) SecurityHelper.get_aces_for_path_by_sid(path, sid) end end before do # stub this to not try to create state.yaml Puppet::Util::Storage.stubs(:store) end it "should not attempt to manage files that do not exist if no means of creating the file is specified" do source = tmpfile('source') catalog.add_resource described_class.new :path => source, :mode => 0755 status = catalog.apply.report.resource_statuses["File[#{source}]"] status.should_not be_failed status.should_not be_changed Puppet::FileSystem.exist?(source).should be_false end describe "when ensure is absent" do it "should remove the file if present" do FileUtils.touch(path) catalog.add_resource(described_class.new(:path => path, :ensure => :absent, :backup => :false)) report = catalog.apply.report report.resource_statuses["File[#{path}]"].should_not be_failed Puppet::FileSystem.exist?(path).should be_false end it "should do nothing if file is not present" do catalog.add_resource(described_class.new(:path => path, :ensure => :absent, :backup => :false)) report = catalog.apply.report report.resource_statuses["File[#{path}]"].should_not be_failed Puppet::FileSystem.exist?(path).should be_false end # issue #14599 it "should not fail if parts of path aren't directories" do FileUtils.touch(path) catalog.add_resource(described_class.new(:path => File.join(path,'no_such_file'), :ensure => :absent, :backup => :false)) report = catalog.apply.report report.resource_statuses["File[#{File.join(path,'no_such_file')}]"].should_not be_failed end end describe "when setting permissions" do it "should set the owner" do target = tmpfile_with_contents('target', '') owner = get_owner(target) catalog.add_resource described_class.new( :name => target, :owner => owner ) catalog.apply get_owner(target).should == owner end it "should set the group" do target = tmpfile_with_contents('target', '') group = get_group(target) catalog.add_resource described_class.new( :name => target, :group => group ) catalog.apply get_group(target).should == group end describe "when setting mode" do describe "for directories" do let(:target) { tmpdir('dir_mode') } it "should set executable bits for newly created directories" do catalog.add_resource described_class.new(:path => target, :ensure => :directory, :mode => 0600) catalog.apply (get_mode(target) & 07777).should == 0700 end it "should set executable bits for existing readable directories" do set_mode(0600, target) catalog.add_resource described_class.new(:path => target, :ensure => :directory, :mode => 0644) catalog.apply (get_mode(target) & 07777).should == 0755 end it "should not set executable bits for unreadable directories" do begin catalog.add_resource described_class.new(:path => target, :ensure => :directory, :mode => 0300) catalog.apply (get_mode(target) & 07777).should == 0300 ensure # so we can cleanup set_mode(0700, target) end end it "should set user, group, and other executable bits" do catalog.add_resource described_class.new(:path => target, :ensure => :directory, :mode => 0664) catalog.apply (get_mode(target) & 07777).should == 0775 end it "should set executable bits when overwriting a non-executable file" do target_path = tmpfile_with_contents('executable', '') set_mode(0444, target_path) catalog.add_resource described_class.new(:path => target_path, :ensure => :directory, :mode => 0666, :backup => false) catalog.apply (get_mode(target_path) & 07777).should == 0777 File.should be_directory(target_path) end end describe "for files" do it "should not set executable bits" do catalog.add_resource described_class.new(:path => path, :ensure => :file, :mode => 0666) catalog.apply (get_mode(path) & 07777).should == 0666 end it "should not set executable bits when replacing an executable directory (#10365)" do pending("bug #10365") FileUtils.mkdir(path) set_mode(0777, path) catalog.add_resource described_class.new(:path => path, :ensure => :file, :mode => 0666, :backup => false, :force => true) catalog.apply (get_mode(path) & 07777).should == 0666 end end describe "for links", :if => described_class.defaultprovider.feature?(:manages_symlinks) do let(:link) { tmpfile('link_mode') } describe "when managing links" do let(:link_target) { tmpfile('target') } before :each do FileUtils.touch(link_target) File.chmod(0444, link_target) Puppet::FileSystem.symlink(link_target, link) end it "should not set the executable bit on the link nor the target" do catalog.add_resource described_class.new(:path => link, :ensure => :link, :mode => 0666, :target => link_target, :links => :manage) catalog.apply (Puppet::FileSystem.stat(link).mode & 07777) == 0666 (Puppet::FileSystem.lstat(link_target).mode & 07777) == 0444 end it "should ignore dangling symlinks (#6856)" do File.delete(link_target) catalog.add_resource described_class.new(:path => link, :ensure => :link, :mode => 0666, :target => link_target, :links => :manage) catalog.apply Puppet::FileSystem.exist?(link).should be_false end it "should create a link to the target if ensure is omitted" do FileUtils.touch(link_target) catalog.add_resource described_class.new(:path => link, :target => link_target) catalog.apply Puppet::FileSystem.exist?(link).should be_true Puppet::FileSystem.lstat(link).ftype.should == 'link' Puppet::FileSystem.readlink(link).should == link_target end end describe "when following links" do it "should ignore dangling symlinks (#6856)" do target = tmpfile('dangling') FileUtils.touch(target) Puppet::FileSystem.symlink(target, link) File.delete(target) catalog.add_resource described_class.new(:path => path, :source => link, :mode => 0600, :links => :follow) catalog.apply end describe "to a directory" do let(:link_target) { tmpdir('dir_target') } before :each do File.chmod(0600, link_target) Puppet::FileSystem.symlink(link_target, link) end after :each do File.chmod(0750, link_target) end describe "that is readable" do it "should set the executable bits when creating the destination (#10315)" do catalog.add_resource described_class.new(:path => path, :source => link, :mode => 0666, :links => :follow) catalog.apply File.should be_directory(path) (get_mode(path) & 07777).should == 0777 end it "should set the executable bits when overwriting the destination (#10315)" do FileUtils.touch(path) catalog.add_resource described_class.new(:path => path, :source => link, :mode => 0666, :links => :follow, :backup => false) catalog.apply File.should be_directory(path) (get_mode(path) & 07777).should == 0777 end end describe "that is not readable" do before :each do set_mode(0300, link_target) end # so we can cleanup after :each do set_mode(0700, link_target) end it "should set executable bits when creating the destination (#10315)" do catalog.add_resource described_class.new(:path => path, :source => link, :mode => 0666, :links => :follow) catalog.apply File.should be_directory(path) (get_mode(path) & 07777).should == 0777 end it "should set executable bits when overwriting the destination" do FileUtils.touch(path) catalog.add_resource described_class.new(:path => path, :source => link, :mode => 0666, :links => :follow, :backup => false) catalog.apply File.should be_directory(path) (get_mode(path) & 07777).should == 0777 end end end describe "to a file" do let(:link_target) { tmpfile('file_target') } before :each do FileUtils.touch(link_target) Puppet::FileSystem.symlink(link_target, link) end it "should create the file, not a symlink (#2817, #10315)" do catalog.add_resource described_class.new(:path => path, :source => link, :mode => 0600, :links => :follow) catalog.apply File.should be_file(path) (get_mode(path) & 07777).should == 0600 end it "should overwrite the file" do FileUtils.touch(path) catalog.add_resource described_class.new(:path => path, :source => link, :mode => 0600, :links => :follow) catalog.apply File.should be_file(path) (get_mode(path) & 07777).should == 0600 end end describe "to a link to a directory" do let(:real_target) { tmpdir('real_target') } let(:target) { tmpfile('target') } before :each do File.chmod(0666, real_target) # link -> target -> real_target Puppet::FileSystem.symlink(real_target, target) Puppet::FileSystem.symlink(target, link) end after :each do File.chmod(0750, real_target) end describe "when following all links" do it "should create the destination and apply executable bits (#10315)" do catalog.add_resource described_class.new(:path => path, :source => link, :mode => 0600, :links => :follow) catalog.apply File.should be_directory(path) (get_mode(path) & 07777).should == 0700 end it "should overwrite the destination and apply executable bits" do FileUtils.mkdir(path) catalog.add_resource described_class.new(:path => path, :source => link, :mode => 0600, :links => :follow) catalog.apply File.should be_directory(path) (get_mode(path) & 0111).should == 0100 end end end end end end end describe "when writing files" do it "should backup files to a filebucket when one is configured" do filebucket = Puppet::Type.type(:filebucket).new :path => tmpfile("filebucket"), :name => "mybucket" file = described_class.new :path => path, :backup => "mybucket", :content => "foo" catalog.add_resource file catalog.add_resource filebucket File.open(file[:path], "w") { |f| f.write("bar") } md5 = Digest::MD5.hexdigest("bar") catalog.apply filebucket.bucket.getfile(md5).should == "bar" end it "should backup files in the local directory when a backup string is provided" do file = described_class.new :path => path, :backup => ".bak", :content => "foo" catalog.add_resource file File.open(file[:path], "w") { |f| f.puts "bar" } catalog.apply backup = file[:path] + ".bak" Puppet::FileSystem.exist?(backup).should be_true File.read(backup).should == "bar\n" end it "should fail if no backup can be performed" do dir = tmpdir("backups") file = described_class.new :path => File.join(dir, "testfile"), :backup => ".bak", :content => "foo" catalog.add_resource file File.open(file[:path], 'w') { |f| f.puts "bar" } # Create a directory where the backup should be so that writing to it fails Dir.mkdir(File.join(dir, "testfile.bak")) Puppet::Util::Log.stubs(:newmessage) catalog.apply File.read(file[:path]).should == "bar\n" end it "should not backup symlinks", :if => described_class.defaultprovider.feature?(:manages_symlinks) do link = tmpfile("link") dest1 = tmpfile("dest1") dest2 = tmpfile("dest2") bucket = Puppet::Type.type(:filebucket).new :path => tmpfile("filebucket"), :name => "mybucket" file = described_class.new :path => link, :target => dest2, :ensure => :link, :backup => "mybucket" catalog.add_resource file catalog.add_resource bucket File.open(dest1, "w") { |f| f.puts "whatever" } Puppet::FileSystem.symlink(dest1, link) md5 = Digest::MD5.hexdigest(File.read(file[:path])) catalog.apply Puppet::FileSystem.readlink(link).should == dest2 Puppet::FileSystem.exist?(bucket[:path]).should be_false end it "should backup directories to the local filesystem by copying the whole directory" do file = described_class.new :path => path, :backup => ".bak", :content => "foo", :force => true catalog.add_resource file Dir.mkdir(path) otherfile = File.join(path, "foo") File.open(otherfile, "w") { |f| f.print "yay" } catalog.apply backup = "#{path}.bak" FileTest.should be_directory(backup) File.read(File.join(backup, "foo")).should == "yay" end it "should backup directories to filebuckets by backing up each file separately" do bucket = Puppet::Type.type(:filebucket).new :path => tmpfile("filebucket"), :name => "mybucket" file = described_class.new :path => tmpfile("bucket_backs"), :backup => "mybucket", :content => "foo", :force => true catalog.add_resource file catalog.add_resource bucket Dir.mkdir(file[:path]) foofile = File.join(file[:path], "foo") barfile = File.join(file[:path], "bar") File.open(foofile, "w") { |f| f.print "fooyay" } File.open(barfile, "w") { |f| f.print "baryay" } foomd5 = Digest::MD5.hexdigest(File.read(foofile)) barmd5 = Digest::MD5.hexdigest(File.read(barfile)) catalog.apply bucket.bucket.getfile(foomd5).should == "fooyay" bucket.bucket.getfile(barmd5).should == "baryay" end end describe "when recursing" do def build_path(dir) Dir.mkdir(dir) File.chmod(0750, dir) @dirs = [dir] @files = [] %w{one two}.each do |subdir| fdir = File.join(dir, subdir) Dir.mkdir(fdir) File.chmod(0750, fdir) @dirs << fdir %w{three}.each do |file| ffile = File.join(fdir, file) @files << ffile File.open(ffile, "w") { |f| f.puts "test #{file}" } File.chmod(0640, ffile) end end end it "should be able to recurse over a nonexistent file" do @file = described_class.new( :name => path, :mode => 0644, :recurse => true, :backup => false ) catalog.add_resource @file lambda { @file.eval_generate }.should_not raise_error end it "should be able to recursively set properties on existing files" do path = tmpfile("file_integration_tests") build_path(path) file = described_class.new( :name => path, :mode => 0644, :recurse => true, :backup => false ) catalog.add_resource file catalog.apply @dirs.should_not be_empty @dirs.each do |path| (get_mode(path) & 007777).should == 0755 end @files.should_not be_empty @files.each do |path| (get_mode(path) & 007777).should == 0644 end end it "should be able to recursively make links to other files", :if => described_class.defaultprovider.feature?(:manages_symlinks) do source = tmpfile("file_link_integration_source") build_path(source) dest = tmpfile("file_link_integration_dest") @file = described_class.new(:name => dest, :target => source, :recurse => true, :ensure => :link, :backup => false) catalog.add_resource @file catalog.apply @dirs.each do |path| link_path = path.sub(source, dest) Puppet::FileSystem.lstat(link_path).should be_directory end @files.each do |path| link_path = path.sub(source, dest) Puppet::FileSystem.lstat(link_path).ftype.should == "link" end end it "should be able to recursively copy files" do source = tmpfile("file_source_integration_source") build_path(source) dest = tmpfile("file_source_integration_dest") @file = described_class.new(:name => dest, :source => source, :recurse => true, :backup => false) catalog.add_resource @file catalog.apply @dirs.each do |path| newpath = path.sub(source, dest) Puppet::FileSystem.lstat(newpath).should be_directory end @files.each do |path| newpath = path.sub(source, dest) Puppet::FileSystem.lstat(newpath).ftype.should == "file" end end it "should not recursively manage files managed by a more specific explicit file" do dir = tmpfile("recursion_vs_explicit_1") subdir = File.join(dir, "subdir") file = File.join(subdir, "file") FileUtils.mkdir_p(subdir) File.open(file, "w") { |f| f.puts "" } base = described_class.new(:name => dir, :recurse => true, :backup => false, :mode => "755") sub = described_class.new(:name => subdir, :recurse => true, :backup => false, :mode => "644") catalog.add_resource base catalog.add_resource sub catalog.apply (get_mode(file) & 007777).should == 0644 end it "should recursively manage files even if there is an explicit file whose name is a prefix of the managed file" do managed = File.join(path, "file") generated = File.join(path, "file_with_a_name_starting_with_the_word_file") managed_mode = 0700 FileUtils.mkdir_p(path) FileUtils.touch(managed) FileUtils.touch(generated) catalog.add_resource described_class.new(:name => path, :recurse => true, :backup => false, :mode => managed_mode) catalog.add_resource described_class.new(:name => managed, :recurse => true, :backup => false, :mode => "644") catalog.apply (get_mode(generated) & 007777).should == managed_mode end describe "when recursing remote directories" do describe "when sourceselect first" do describe "for a directory" do it "should recursively copy the first directory that exists" do one = File.expand_path('thisdoesnotexist') two = tmpdir('two') FileUtils.mkdir_p(File.join(two, 'three')) FileUtils.touch(File.join(two, 'three', 'four')) catalog.add_resource Puppet::Type.newfile( :path => path, :ensure => :directory, :backup => false, :recurse => true, :sourceselect => :first, :source => [one, two] ) catalog.apply File.should be_directory(path) Puppet::FileSystem.exist?(File.join(path, 'one')).should be_false Puppet::FileSystem.exist?(File.join(path, 'three', 'four')).should be_true end it "should recursively copy an empty directory" do one = File.expand_path('thisdoesnotexist') two = tmpdir('two') three = tmpdir('three') file_in_dir_with_contents(three, 'a', '') catalog.add_resource Puppet::Type.newfile( :path => path, :ensure => :directory, :backup => false, :recurse => true, :sourceselect => :first, :source => [one, two, three] ) catalog.apply File.should be_directory(path) Puppet::FileSystem.exist?(File.join(path, 'a')).should be_false end it "should only recurse one level" do one = tmpdir('one') FileUtils.mkdir_p(File.join(one, 'a', 'b')) FileUtils.touch(File.join(one, 'a', 'b', 'c')) two = tmpdir('two') FileUtils.mkdir_p(File.join(two, 'z')) FileUtils.touch(File.join(two, 'z', 'y')) catalog.add_resource Puppet::Type.newfile( :path => path, :ensure => :directory, :backup => false, :recurse => true, :recurselimit => 1, :sourceselect => :first, :source => [one, two] ) catalog.apply Puppet::FileSystem.exist?(File.join(path, 'a')).should be_true Puppet::FileSystem.exist?(File.join(path, 'a', 'b')).should be_false Puppet::FileSystem.exist?(File.join(path, 'z')).should be_false end end describe "for a file" do it "should copy the first file that exists" do one = File.expand_path('thisdoesnotexist') two = tmpfile_with_contents('two', 'yay') three = tmpfile_with_contents('three', 'no') catalog.add_resource Puppet::Type.newfile( :path => path, :ensure => :file, :backup => false, :sourceselect => :first, :source => [one, two, three] ) catalog.apply File.read(path).should == 'yay' end it "should copy an empty file" do one = File.expand_path('thisdoesnotexist') two = tmpfile_with_contents('two', '') three = tmpfile_with_contents('three', 'no') catalog.add_resource Puppet::Type.newfile( :path => path, :ensure => :file, :backup => false, :sourceselect => :first, :source => [one, two, three] ) catalog.apply File.read(path).should == '' end end end describe "when sourceselect all" do describe "for a directory" do it "should recursively copy all sources from the first valid source" do dest = tmpdir('dest') one = tmpdir('one') two = tmpdir('two') three = tmpdir('three') four = tmpdir('four') file_in_dir_with_contents(one, 'a', one) file_in_dir_with_contents(two, 'a', two) file_in_dir_with_contents(two, 'b', two) file_in_dir_with_contents(three, 'a', three) file_in_dir_with_contents(three, 'c', three) obj = Puppet::Type.newfile( :path => dest, :ensure => :directory, :backup => false, :recurse => true, :sourceselect => :all, :source => [one, two, three, four] ) catalog.add_resource obj catalog.apply File.read(File.join(dest, 'a')).should == one File.read(File.join(dest, 'b')).should == two File.read(File.join(dest, 'c')).should == three end it "should only recurse one level from each valid source" do one = tmpdir('one') FileUtils.mkdir_p(File.join(one, 'a', 'b')) FileUtils.touch(File.join(one, 'a', 'b', 'c')) two = tmpdir('two') FileUtils.mkdir_p(File.join(two, 'z')) FileUtils.touch(File.join(two, 'z', 'y')) obj = Puppet::Type.newfile( :path => path, :ensure => :directory, :backup => false, :recurse => true, :recurselimit => 1, :sourceselect => :all, :source => [one, two] ) catalog.add_resource obj catalog.apply Puppet::FileSystem.exist?(File.join(path, 'a')).should be_true Puppet::FileSystem.exist?(File.join(path, 'a', 'b')).should be_false Puppet::FileSystem.exist?(File.join(path, 'z')).should be_true Puppet::FileSystem.exist?(File.join(path, 'z', 'y')).should be_false end end end end end describe "when generating resources" do before do source = tmpdir("generating_in_catalog_source") s1 = file_in_dir_with_contents(source, "one", "uno") s2 = file_in_dir_with_contents(source, "two", "dos") @file = described_class.new( :name => path, :source => source, :recurse => true, :backup => false ) catalog.add_resource @file end it "should add each generated resource to the catalog" do catalog.apply do |trans| catalog.resource(:file, File.join(path, "one")).must be_a(described_class) catalog.resource(:file, File.join(path, "two")).must be_a(described_class) end end it "should have an edge to each resource in the relationship graph" do catalog.apply do |trans| one = catalog.resource(:file, File.join(path, "one")) catalog.relationship_graph.should be_edge(@file, one) two = catalog.resource(:file, File.join(path, "two")) catalog.relationship_graph.should be_edge(@file, two) end end end describe "when copying files" do it "should be able to copy files with pound signs in their names (#285)" do source = tmpfile_with_contents("filewith#signs", "foo") dest = tmpfile("destwith#signs") catalog.add_resource described_class.new(:name => dest, :source => source) catalog.apply File.read(dest).should == "foo" end it "should be able to copy files with spaces in their names" do dest = tmpfile("destwith spaces") source = tmpfile_with_contents("filewith spaces", "foo") - File.chmod(0755, source) + + expected_mode = 0755 + Puppet::FileSystem.chmod(expected_mode, source) catalog.add_resource described_class.new(:path => dest, :source => source) catalog.apply - expected_mode = Puppet.features.microsoft_windows? ? 0644 : 0755 File.read(dest).should == "foo" (Puppet::FileSystem.stat(dest).mode & 007777).should == expected_mode end it "should be able to copy individual files even if recurse has been specified" do source = tmpfile_with_contents("source", "foo") dest = tmpfile("dest") catalog.add_resource described_class.new(:name => dest, :source => source, :recurse => true) catalog.apply File.read(dest).should == "foo" end end it "should create a file with content if ensure is omitted" do catalog.add_resource described_class.new( :path => path, :content => "this is some content, yo" ) catalog.apply File.read(path).should == "this is some content, yo" end it "should create files with content if both content and ensure are set" do file = described_class.new( :path => path, :ensure => "file", :content => "this is some content, yo" ) catalog.add_resource file catalog.apply File.read(path).should == "this is some content, yo" end it "should delete files with sources but that are set for deletion" do source = tmpfile_with_contents("source_source_with_ensure", "yay") dest = tmpfile_with_contents("source_source_with_ensure", "boo") file = described_class.new( :path => dest, :ensure => :absent, :source => source, :backup => false ) catalog.add_resource file catalog.apply Puppet::FileSystem.exist?(dest).should be_false end describe "when sourcing" do let(:source) { tmpfile_with_contents("source_default_values", "yay") } it "should apply the source metadata values" do set_mode(0770, source) file = described_class.new( :path => path, :ensure => :file, :source => source, :backup => false ) catalog.add_resource file catalog.apply get_owner(path).should == get_owner(source) get_group(path).should == get_group(source) (get_mode(path) & 07777).should == 0770 end it "should override the default metadata values" do set_mode(0770, source) file = described_class.new( :path => path, :ensure => :file, :source => source, :backup => false, :mode => 0440 ) catalog.add_resource file catalog.apply (get_mode(path) & 07777).should == 0440 end describe "on Windows systems", :if => Puppet.features.microsoft_windows? do def expects_sid_granted_full_access_explicitly(path, sid) inherited_ace = Windows::Security::INHERITED_ACE aces = get_aces_for_path_by_sid(path, sid) aces.should_not be_empty aces.each do |ace| ace.mask.should == Windows::File::FILE_ALL_ACCESS (ace.flags & inherited_ace).should_not == inherited_ace end end def expects_system_granted_full_access_explicitly(path) expects_sid_granted_full_access_explicitly(path, @sids[:system]) end def expects_at_least_one_inherited_ace_grants_full_access(path, sid) inherited_ace = Windows::Security::INHERITED_ACE aces = get_aces_for_path_by_sid(path, sid) aces.should_not be_empty aces.any? do |ace| ace.mask == Windows::File::FILE_ALL_ACCESS && (ace.flags & inherited_ace) == inherited_ace end.should be_true end def expects_at_least_one_inherited_system_ace_grants_full_access(path) expects_at_least_one_inherited_ace_grants_full_access(path, @sids[:system]) end it "should provide valid default values when ACLs are not supported" do + Puppet::Util::Windows::Security.stubs(:supports_acl?).returns(false) Puppet::Util::Windows::Security.stubs(:supports_acl?).with(source).returns false file = described_class.new( :path => path, :ensure => :file, :source => source, :backup => false ) catalog.add_resource file catalog.apply get_owner(path).should =~ /^S\-1\-5\-.*$/ get_group(path).should =~ /^S\-1\-0\-0.*$/ get_mode(path).should == 0644 end describe "when processing SYSTEM ACEs" do before do @sids = { :current_user => Puppet::Util::Windows::Security.name_to_sid(Sys::Admin.get_login), :system => Win32::Security::SID::LocalSystem, :admin => Puppet::Util::Windows::Security.name_to_sid("Administrator"), :guest => Puppet::Util::Windows::Security.name_to_sid("Guest"), :users => Win32::Security::SID::BuiltinUsers, :power_users => Win32::Security::SID::PowerUsers, :none => Win32::Security::SID::Nobody } end describe "on files" do before :each do @file = described_class.new( :path => path, :ensure => :file, :source => source, :backup => false ) catalog.add_resource @file end describe "when source permissions are ignored" do before :each do @file[:source_permissions] = :ignore end it "preserves the inherited SYSTEM ACE" do catalog.apply expects_at_least_one_inherited_system_ace_grants_full_access(path) end end describe "when permissions are insync?" do it "preserves the explicit SYSTEM ACE" do FileUtils.touch(path) sd = Puppet::Util::Windows::Security.get_security_descriptor(path) sd.protect = true sd.owner = @sids[:none] sd.group = @sids[:none] Puppet::Util::Windows::Security.set_security_descriptor(source, sd) Puppet::Util::Windows::Security.set_security_descriptor(path, sd) catalog.apply expects_system_granted_full_access_explicitly(path) end end describe "when permissions are not insync?" do before :each do @file[:owner] = 'None' @file[:group] = 'None' end it "replaces inherited SYSTEM ACEs with an uninherited one for an existing file" do FileUtils.touch(path) expects_at_least_one_inherited_system_ace_grants_full_access(path) catalog.apply expects_system_granted_full_access_explicitly(path) end it "replaces inherited SYSTEM ACEs for a new file with an uninherited one" do catalog.apply expects_system_granted_full_access_explicitly(path) end end describe "created with SYSTEM as the group" do before :each do @file[:owner] = @sids[:users] @file[:group] = @sids[:system] @file[:mode] = 0644 catalog.apply end it "should allow the user to explicitly set the mode to 4" do system_aces = get_aces_for_path_by_sid(path, @sids[:system]) system_aces.should_not be_empty system_aces.each do |ace| ace.mask.should == Windows::File::FILE_GENERIC_READ end end it "prepends SYSTEM ace when changing group from system to power users" do @file[:group] = @sids[:power_users] catalog.apply system_aces = get_aces_for_path_by_sid(path, @sids[:system]) system_aces.size.should == 1 end end describe "with :links set to :follow" do it "should not fail to apply" do # at minimal, we need an owner and/or group @file[:owner] = @sids[:users] @file[:links] = :follow catalog.apply do |transaction| if transaction.any_failed? pretty_transaction_error(transaction) end end end end end describe "on directories" do before :each do @directory = described_class.new( :path => dir, :ensure => :directory ) catalog.add_resource @directory end def grant_everyone_full_access(path) sd = Puppet::Util::Windows::Security.get_security_descriptor(path) sd.dacl.allow( 'S-1-1-0', #everyone Windows::File::FILE_ALL_ACCESS, Windows::File::OBJECT_INHERIT_ACE | Windows::File::CONTAINER_INHERIT_ACE) Puppet::Util::Windows::Security.set_security_descriptor(path, sd) end after :each do grant_everyone_full_access(dir) end describe "when source permissions are ignored" do before :each do @directory[:source_permissions] = :ignore end it "preserves the inherited SYSTEM ACE" do catalog.apply expects_at_least_one_inherited_system_ace_grants_full_access(dir) end end describe "when permissions are insync?" do it "preserves the explicit SYSTEM ACE" do Dir.mkdir(dir) source_dir = tmpdir('source_dir') @directory[:source] = source_dir sd = Puppet::Util::Windows::Security.get_security_descriptor(source_dir) sd.protect = true sd.owner = @sids[:none] sd.group = @sids[:none] Puppet::Util::Windows::Security.set_security_descriptor(source_dir, sd) Puppet::Util::Windows::Security.set_security_descriptor(dir, sd) catalog.apply expects_system_granted_full_access_explicitly(dir) end end describe "when permissions are not insync?" do before :each do @directory[:owner] = 'None' @directory[:group] = 'None' @directory[:mode] = 0444 end it "replaces inherited SYSTEM ACEs with an uninherited one for an existing directory" do FileUtils.mkdir(dir) expects_at_least_one_inherited_system_ace_grants_full_access(dir) catalog.apply expects_system_granted_full_access_explicitly(dir) end it "replaces inherited SYSTEM ACEs with an uninherited one for an existing directory" do catalog.apply expects_system_granted_full_access_explicitly(dir) end describe "created with SYSTEM as the group" do before :each do @directory[:owner] = @sids[:users] @directory[:group] = @sids[:system] @directory[:mode] = 0644 catalog.apply end it "should allow the user to explicitly set the mode to 4" do system_aces = get_aces_for_path_by_sid(dir, @sids[:system]) system_aces.should_not be_empty system_aces.each do |ace| # unlike files, Puppet sets execute bit on directories that are readable ace.mask.should == Windows::File::FILE_GENERIC_READ | Windows::File::FILE_GENERIC_EXECUTE end end it "prepends SYSTEM ace when changing group from system to power users" do @directory[:group] = @sids[:power_users] catalog.apply system_aces = get_aces_for_path_by_sid(dir, @sids[:system]) system_aces.size.should == 1 end end describe "with :links set to :follow" do it "should not fail to apply" do # at minimal, we need an owner and/or group @directory[:owner] = @sids[:users] @directory[:links] = :follow catalog.apply do |transaction| if transaction.any_failed? pretty_transaction_error(transaction) end end end end end end end end end describe "when purging files" do before do sourcedir = tmpdir("purge_source") destdir = tmpdir("purge_dest") sourcefile = File.join(sourcedir, "sourcefile") @copiedfile = File.join(destdir, "sourcefile") @localfile = File.join(destdir, "localfile") @purgee = File.join(destdir, "to_be_purged") File.open(@localfile, "w") { |f| f.print "oldtest" } File.open(sourcefile, "w") { |f| f.print "funtest" } # this file should get removed File.open(@purgee, "w") { |f| f.print "footest" } lfobj = Puppet::Type.newfile( :title => "localfile", :path => @localfile, :content => "rahtest", :ensure => :file, :backup => false ) destobj = Puppet::Type.newfile( :title => "destdir", :path => destdir, :source => sourcedir, :backup => false, :purge => true, :recurse => true ) catalog.add_resource lfobj, destobj catalog.apply end it "should still copy remote files" do File.read(@copiedfile).should == 'funtest' end it "should not purge managed, local files" do File.read(@localfile).should == 'rahtest' end it "should purge files that are neither remote nor otherwise managed" do Puppet::FileSystem.exist?(@purgee).should be_false end end describe "when using validate_cmd" do it "should fail the file resource if command fails" do catalog.add_resource(described_class.new(:path => path, :content => "foo", :validate_cmd => "/usr/bin/env false")) Puppet::Util::Execution.expects(:execute).with("/usr/bin/env false", {:combine => true, :failonfail => true}).raises(Puppet::ExecutionFailure, "Failed") report = catalog.apply.report report.resource_statuses["File[#{path}]"].should be_failed Puppet::FileSystem.exist?(path).should be_false end it "should succeed the file resource if command succeeds" do catalog.add_resource(described_class.new(:path => path, :content => "foo", :validate_cmd => "/usr/bin/env true")) Puppet::Util::Execution.expects(:execute).with("/usr/bin/env true", {:combine => true, :failonfail => true}).returns '' report = catalog.apply.report report.resource_statuses["File[#{path}]"].should_not be_failed Puppet::FileSystem.exist?(path).should be_true end end def tmpfile_with_contents(name, contents) file = tmpfile(name) File.open(file, "w") { |f| f.write contents } file end def file_in_dir_with_contents(dir, name, contents) full_name = File.join(dir, name) File.open(full_name, "w") { |f| f.write contents } full_name end def pretty_transaction_error(transaction) report = transaction.report status_failures = report.resource_statuses.values.select { |r| r.failed? } status_fail_msg = status_failures. collect(&:events). flatten. select { |event| event.status == 'failure' }. collect { |event| "#{event.resource}: #{event.message}" }.join("; ") raise "Got #{status_failures.length} failure(s) while applying: #{status_fail_msg}" end end diff --git a/spec/integration/type/nagios_spec.rb b/spec/integration/type/nagios_spec.rb index 2746fecb3..818b61649 100644 --- a/spec/integration/type/nagios_spec.rb +++ b/spec/integration/type/nagios_spec.rb @@ -1,78 +1,80 @@ #!/usr/bin/env ruby require 'spec_helper' require 'puppet/file_bucket/dipper' describe "Nagios file creation" do include PuppetSpec::Files before :each do FileUtils.touch(target_file) File.chmod(0600, target_file) Puppet::FileBucket::Dipper.any_instance.stubs(:backup) # Don't backup to filebucket end let :target_file do tmpfile('nagios_integration_specs') end # Copied from the crontab integration spec. # # @todo This should probably live in the PuppetSpec module instead then. def run_in_catalog(*resources) catalog = Puppet::Resource::Catalog.new catalog.host_config = false resources.each do |resource| resource.expects(:err).never catalog.add_resource(resource) end # the resources are not properly contained and generated resources # will end up with dangling edges without this stubbing: catalog.stubs(:container_of).returns resources[0] catalog.apply end # These three helpers are from file_spec.rb # # @todo Define those centrally as well? def get_mode(file) Puppet::FileSystem.stat(file).mode end context "when creating a nagios config file" do context "which is not managed" do it "should choose the file mode if requested" do resource = Puppet::Type.type(:nagios_host).new( :name => 'spechost', :use => 'spectemplate', :ensure => 'present', :target => target_file, :mode => '0640' ) run_in_catalog(resource) - ( "%o" % get_mode(target_file) ).should == "100640" + # sticky bit only applies to directories in Windows + mode = Puppet.features.microsoft_windows? ? "640" : "100640" + ( "%o" % get_mode(target_file) ).should == mode end end context "which is managed" do it "should not the mode" do file_res = Puppet::Type.type(:file).new( :name => target_file, :ensure => :present ) nag_res = Puppet::Type.type(:nagios_host).new( :name => 'spechost', :use => 'spectemplate', :ensure => :present, :target => target_file, :mode => '0640' ) run_in_catalog(file_res, nag_res) ( "%o" % get_mode(target_file) ).should_not == "100640" end end end end diff --git a/spec/integration/util/settings_spec.rb b/spec/integration/util/settings_spec.rb index 783f56032..a76ade637 100755 --- a/spec/integration/util/settings_spec.rb +++ b/spec/integration/util/settings_spec.rb @@ -1,89 +1,89 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet_spec/files' describe Puppet::Settings do include PuppetSpec::Files def minimal_default_settings { :noop => {:default => false, :desc => "noop"} } end def define_settings(section, settings_hash) settings.define_settings(section, minimal_default_settings.update(settings_hash)) end let(:settings) { Puppet::Settings.new } it "should be able to make needed directories" do define_settings(:main, :maindir => { :default => tmpfile("main"), :type => :directory, :desc => "a", } ) settings.use(:main) expect(File.directory?(settings[:maindir])).to be_true end it "should make its directories with the correct modes" do define_settings(:main, :maindir => { :default => tmpfile("main"), :type => :directory, :desc => "a", :mode => 0750 } ) settings.use(:main) - expect(Puppet::FileSystem.stat(settings[:maindir]).mode & 007777).to eq(Puppet.features.microsoft_windows? ? 0755 : 0750) + expect(Puppet::FileSystem.stat(settings[:maindir]).mode & 007777).to eq(0750) end it "reparses configuration if configuration file is touched", :if => !Puppet.features.microsoft_windows? do config = tmpfile("config") define_settings(:main, :config => { :type => :file, :default => config, :desc => "a" }, :environment => { :default => 'dingos', :desc => 'test', } ) Puppet[:filetimeout] = '1s' File.open(config, 'w') do |file| file.puts <<-EOF [main] environment=toast EOF end settings.initialize_global_settings expect(settings[:environment]).to eq('toast') # First reparse establishes WatchedFiles settings.reparse_config_files sleep 1 File.open(config, 'w') do |file| file.puts <<-EOF [main] environment=bacon EOF end # Second reparse if later than filetimeout, reparses if changed settings.reparse_config_files expect(settings[:environment]).to eq('bacon') end end diff --git a/spec/unit/type/file/mode_spec.rb b/spec/unit/type/file/mode_spec.rb index 3f2dd90bb..9936ebdbc 100755 --- a/spec/unit/type/file/mode_spec.rb +++ b/spec/unit/type/file/mode_spec.rb @@ -1,194 +1,195 @@ #! /usr/bin/env ruby require 'spec_helper' describe Puppet::Type.type(:file).attrclass(:mode) do include PuppetSpec::Files let(:path) { tmpfile('mode_spec') } let(:resource) { Puppet::Type.type(:file).new :path => path, :mode => 0644 } let(:mode) { resource.property(:mode) } describe "#validate" do it "should accept values specified as integers" do expect { mode.value = 0755 }.not_to raise_error end it "should accept values specified as octal numbers in strings" do expect { mode.value = '0755' }.not_to raise_error end it "should accept valid symbolic strings" do expect { mode.value = 'g+w,u-x' }.not_to raise_error end it "should not accept strings other than octal numbers" do expect do mode.value = 'readable please!' end.to raise_error(Puppet::Error, /The file mode specification is invalid/) end end describe "#munge" do # This is sort of a redundant test, but its spec is important. it "should return the value as a string" do mode.munge('0644').should be_a(String) end it "should accept strings as arguments" do mode.munge('0644').should == '644' end it "should accept symbolic strings as arguments and return them intact" do mode.munge('u=rw,go=r').should == 'u=rw,go=r' end it "should accept integers are arguments" do mode.munge(0644).should == '644' end end describe "#dirmask" do before :each do Dir.mkdir(path) end it "should add execute bits corresponding to read bits for directories" do mode.dirmask('0644').should == '755' end it "should not add an execute bit when there is no read bit" do mode.dirmask('0600').should == '700' end it "should not add execute bits for files that aren't directories" do resource[:path] = tmpfile('other_file') mode.dirmask('0644').should == '0644' end end describe "#insync?" do it "should return true if the mode is correct" do FileUtils.touch(path) mode.must be_insync('644') end it "should return false if the mode is incorrect" do FileUtils.touch(path) mode.must_not be_insync('755') end it "should return true if the file is a link and we are managing links", :if => Puppet.features.manages_symlinks? do Puppet::FileSystem.symlink('anything', path) mode.must be_insync('644') end describe "with a symbolic mode" do let(:resource_sym) { Puppet::Type.type(:file).new :path => path, :mode => 'u+w,g-w' } let(:mode_sym) { resource_sym.property(:mode) } it "should return true if the mode matches, regardless of other bits" do FileUtils.touch(path) mode_sym.must be_insync('644') end it "should return false if the mode requires 0's where there are 1's" do FileUtils.touch(path) mode_sym.must_not be_insync('624') end it "should return false if the mode requires 1's where there are 0's" do FileUtils.touch(path) mode_sym.must_not be_insync('044') end end end describe "#retrieve" do it "should return absent if the resource doesn't exist" do resource[:path] = File.expand_path("/does/not/exist") mode.retrieve.should == :absent end it "should retrieve the directory mode from the provider" do Dir.mkdir(path) mode.expects(:dirmask).with('644').returns '755' resource.provider.expects(:mode).returns '755' mode.retrieve.should == '755' end it "should retrieve the file mode from the provider" do FileUtils.touch(path) mode.expects(:dirmask).with('644').returns '644' resource.provider.expects(:mode).returns '644' mode.retrieve.should == '644' end end describe '#should_to_s' do describe 'with a 3-digit mode' do it 'returns a 4-digit mode with a leading zero' do mode.should_to_s('755').should == '0755' end end describe 'with a 4-digit mode' do it 'returns the 4-digit mode when the first digit is a zero' do mode.should_to_s('0755').should == '0755' end it 'returns the 4-digit mode when the first digit is not a zero' do mode.should_to_s('1755').should == '1755' end end end describe '#is_to_s' do describe 'with a 3-digit mode' do it 'returns a 4-digit mode with a leading zero' do mode.is_to_s('755').should == '0755' end end describe 'with a 4-digit mode' do it 'returns the 4-digit mode when the first digit is a zero' do mode.is_to_s('0755').should == '0755' end it 'returns the 4-digit mode when the first digit is not a zero' do mode.is_to_s('1755').should == '1755' end end describe 'when passed :absent' do it 'returns :absent' do mode.is_to_s(:absent).should == :absent end end end describe "#sync with a symbolic mode" do let(:resource_sym) { Puppet::Type.type(:file).new :path => path, :mode => 'u+w,g-w' } let(:mode_sym) { resource_sym.property(:mode) } before { FileUtils.touch(path) } it "changes only the requested bits" do # lower nibble must be set to 4 for the sake of passing on Windows - FileUtils.chmod 0464, path + Puppet::FileSystem.chmod(0464, path) + mode_sym.sync stat = Puppet::FileSystem.stat(path) (stat.mode & 0777).to_s(8).should == "644" end end end