diff --git a/lib/puppet/file_serving/fileset.rb b/lib/puppet/file_serving/fileset.rb index 8bc5e256d..f2ebf5a9d 100644 --- a/lib/puppet/file_serving/fileset.rb +++ b/lib/puppet/file_serving/fileset.rb @@ -1,173 +1,173 @@ require 'find' require 'puppet/file_serving' require 'puppet/file_serving/metadata' # Operate recursively on a path, returning a set of file paths. class Puppet::FileServing::Fileset attr_reader :path, :ignore, :links attr_accessor :recurse, :recurselimit, :checksum_type # Produce a hash of files, with merged so that earlier files # with the same postfix win. E.g., /dir1/subfile beats /dir2/subfile. # It's a hash because we need to know the relative path of each file, # and the base directory. # This will probably only ever be used for searching for plugins. def self.merge(*filesets) result = {} filesets.each do |fileset| fileset.files.each do |file| result[file] ||= fileset.path end end result end # Return a list of all files in our fileset. This is different from the # normal definition of find in that we support specific levels # of recursion, which means we need to know when we're going another # level deep, which Find doesn't do. def files files = perform_recursion # Now strip off the leading path, so each file becomes relative, and remove # any slashes that might end up at the beginning of the path. result = files.collect { |file| file.sub(%r{^#{Regexp.escape(@path)}/*}, '') } # And add the path itself. result.unshift(".") result end # Should we ignore this path? def ignore?(path) return false if @ignore == [nil] # 'detect' normally returns the found result, whereas we just want true/false. ! @ignore.detect { |pattern| File.fnmatch?(pattern, path) }.nil? end def ignore=(values) values = [values] unless values.is_a?(Array) @ignore = values end def initialize(path, options = {}) if Puppet.features.microsoft_windows? # REMIND: UNC path path = path.chomp(File::SEPARATOR) unless path =~ /^[A-Za-z]:\/$/ else path = path.chomp(File::SEPARATOR) unless path == File::SEPARATOR end - raise ArgumentError.new("Fileset paths must be fully qualified: #{path}") unless File.expand_path(path) == path + raise ArgumentError.new("Fileset paths must be fully qualified: #{path}") unless Puppet::Util.absolute_path?(path) @path = path # Set our defaults. @ignore = [] @links = :manage @recurse = false @recurselimit = :infinite if options.is_a?(Puppet::Indirector::Request) initialize_from_request(options) else initialize_from_hash(options) end raise ArgumentError.new("Fileset paths must exist") unless stat = stat(path) raise ArgumentError.new("Fileset recurse parameter must not be a number anymore, please use recurselimit") if @recurse.is_a?(Integer) end def links=(links) links = links.to_sym raise(ArgumentError, "Invalid :links value '#{links}'") unless [:manage, :follow].include?(links) @links = links @stat_method = links == :manage ? :lstat : :stat end # Should we recurse further? This is basically a single # place for all of the logic around recursion. def recurse?(depth) # recurse if told to, and infinite recursion or current depth not at the limit self.recurse and (self.recurselimit == :infinite or depth <= self.recurselimit) end def initialize_from_hash(options) options.each do |option, value| method = option.to_s + "=" begin send(method, value) rescue NoMethodError raise ArgumentError, "Invalid option '#{option}'" end end end def initialize_from_request(request) [:links, :ignore, :recurse, :recurselimit, :checksum_type].each do |param| if request.options.include?(param) # use 'include?' so the values can be false value = request.options[param] elsif request.options.include?(param.to_s) value = request.options[param.to_s] end next if value.nil? value = true if value == "true" value = false if value == "false" value = Integer(value) if value.is_a?(String) and value =~ /^\d+$/ send(param.to_s + "=", value) end end private # Pull the recursion logic into one place. It's moderately hairy, and this # allows us to keep the hairiness apart from what we do with the files. def perform_recursion # Start out with just our base directory. current_dirs = [@path] next_dirs = [] depth = 1 result = [] return result unless recurse?(depth) while dir_path = current_dirs.shift or ((depth += 1) and recurse?(depth) and current_dirs = next_dirs and next_dirs = [] and dir_path = current_dirs.shift) next unless stat = stat(dir_path) next unless stat.directory? Dir.entries(dir_path).each do |file_path| next if [".", ".."].include?(file_path) # Note that this also causes matching directories not # to be recursed into. next if ignore?(file_path) # Add it to our list of files to return result << File.join(dir_path, file_path) # And to our list of files/directories to iterate over. next_dirs << File.join(dir_path, file_path) end end result end public # Stat a given file, using the links-appropriate method. def stat(path) @stat_method ||= self.links == :manage ? :lstat : :stat begin return File.send(@stat_method, path) rescue # If this happens, it is almost surely because we're # trying to manage a link to a file that does not exist. return nil end end end diff --git a/lib/puppet/file_serving/indirection_hooks.rb b/lib/puppet/file_serving/indirection_hooks.rb index bdcc8865e..4aafcb9fd 100644 --- a/lib/puppet/file_serving/indirection_hooks.rb +++ b/lib/puppet/file_serving/indirection_hooks.rb @@ -1,31 +1,31 @@ require 'uri' require 'puppet/file_serving' +require 'puppet/util' # This module is used to pick the appropriate terminus # in file-serving indirections. This is necessary because # the terminus varies based on the URI asked for. module Puppet::FileServing::IndirectionHooks PROTOCOL_MAP = {"puppet" => :rest, "file" => :file} # Pick an appropriate terminus based on the protocol. def select_terminus(request) # We rely on the request's parsing of the URI. # Short-circuit to :file if it's a fully-qualified path or specifies a 'file' protocol. - return PROTOCOL_MAP["file"] if request.key =~ /^#{::File::SEPARATOR}/ - return PROTOCOL_MAP["file"] if request.key =~ /^[a-z]:[\/\\]/i + return PROTOCOL_MAP["file"] if Puppet::Util.absolute_path?(request.key) return PROTOCOL_MAP["file"] if request.protocol == "file" # We're heading over the wire the protocol is 'puppet' and we've got a server name or we're not named 'apply' or 'puppet' if request.protocol == "puppet" and (request.server or !["puppet","apply"].include?(Puppet.settings[:name])) return PROTOCOL_MAP["puppet"] end if request.protocol and PROTOCOL_MAP[request.protocol].nil? raise(ArgumentError, "URI protocol '#{request.protocol}' is not currently supported for file serving") end # If we're still here, we're using the file_server or modules. :file_server end end diff --git a/lib/puppet/file_serving/metadata.rb b/lib/puppet/file_serving/metadata.rb index 382ac9c96..4c863ee89 100644 --- a/lib/puppet/file_serving/metadata.rb +++ b/lib/puppet/file_serving/metadata.rb @@ -1,114 +1,153 @@ require 'puppet' require 'puppet/indirector' require 'puppet/file_serving' require 'puppet/file_serving/base' require 'puppet/util/checksums' require 'puppet/file_serving/indirection_hooks' # A class that handles retrieving file metadata. class Puppet::FileServing::Metadata < Puppet::FileServing::Base include Puppet::Util::Checksums extend Puppet::Indirector indirects :file_metadata, :extend => Puppet::FileServing::IndirectionHooks attr_reader :path, :owner, :group, :mode, :checksum_type, :checksum, :ftype, :destination PARAM_ORDER = [:mode, :ftype, :owner, :group] def attributes_with_tabs raise(ArgumentError, "Cannot manage files of type #{ftype}") unless ['file','directory','link'].include? ftype desc = [] PARAM_ORDER.each { |check| check = :ftype if check == :type desc << send(check) } desc << checksum desc << @destination rescue nil if ftype == 'link' desc.join("\t") end def checksum_type=(type) raise(ArgumentError, "Unsupported checksum type #{type}") unless respond_to?("#{type}_file") @checksum_type = type end + class MetaStat + extend Forwardable + + def initialize(stat) + @stat = stat + end + + def_delegator :@stat, :uid, :owner + def_delegator :@stat, :gid, :group + def_delegators :@stat, :mode, :ftype + end + + class WindowsStat < MetaStat + if Puppet.features.microsoft_windows? + require 'puppet/util/windows/security' + end + + def initialize(stat, path) + super(stat) + @path = path + end + + [:owner, :group, :mode].each do |method| + define_method method do + Puppet::Util::Windows::Security.send("get_#{method}", @path) + end + end + end + + def collect_stat(path) + stat = stat() + + if Puppet.features.microsoft_windows? + WindowsStat.new(stat, path) + else + MetaStat.new(stat) + end + end + # Retrieve the attributes for this file, relative to a base directory. # Note that File.stat raises Errno::ENOENT if the file is absent and this # method does not catch that exception. def collect real_path = full_path - stat = stat() - @owner = stat.uid - @group = stat.gid - @ftype = stat.ftype + stat = collect_stat(real_path) + @owner = stat.owner + @group = stat.group + @ftype = stat.ftype # We have to mask the mode, yay. @mode = stat.mode & 007777 case stat.ftype when "file" @checksum = ("{#{@checksum_type}}") + send("#{@checksum_type}_file", real_path).to_s when "directory" # Always just timestamp the directory. @checksum_type = "ctime" @checksum = ("{#{@checksum_type}}") + send("#{@checksum_type}_file", path).to_s when "link" @destination = File.readlink(real_path) @checksum = ("{#{@checksum_type}}") + send("#{@checksum_type}_file", real_path).to_s rescue nil else raise ArgumentError, "Cannot manage files of type #{stat.ftype}" end end def initialize(path,data={}) @owner = data.delete('owner') @group = data.delete('group') @mode = data.delete('mode') if checksum = data.delete('checksum') @checksum_type = checksum['type'] @checksum = checksum['value'] end @checksum_type ||= "md5" @ftype = data.delete('type') @destination = data.delete('destination') super(path,data) end PSON.register_document_type('FileMetadata',self) def to_pson_data_hash { 'document_type' => 'FileMetadata', 'data' => super['data'].update( { 'owner' => owner, 'group' => group, 'mode' => mode, 'checksum' => { 'type' => checksum_type, 'value' => checksum }, 'type' => ftype, 'destination' => destination, }), 'metadata' => { 'api_version' => 1 } } end def to_pson(*args) to_pson_data_hash.to_pson(*args) end def self.from_pson(data) new(data.delete('path'), data) end end diff --git a/lib/puppet/indirector/request.rb b/lib/puppet/indirector/request.rb index 0388bd31a..b6b69ea82 100644 --- a/lib/puppet/indirector/request.rb +++ b/lib/puppet/indirector/request.rb @@ -1,201 +1,200 @@ require 'cgi' require 'uri' require 'puppet/indirector' # This class encapsulates all of the information you need to make an # Indirection call, and as a a result also handles REST calls. It's somewhat # analogous to an HTTP Request object, except tuned for our Indirector. class Puppet::Indirector::Request attr_accessor :key, :method, :options, :instance, :node, :ip, :authenticated, :ignore_cache, :ignore_terminus attr_accessor :server, :port, :uri, :protocol attr_reader :indirection_name OPTION_ATTRIBUTES = [:ip, :node, :authenticated, :ignore_terminus, :ignore_cache, :instance, :environment] # Is this an authenticated request? def authenticated? # Double negative, so we just get true or false ! ! authenticated end def environment @environment ||= Puppet::Node::Environment.new end def environment=(env) @environment = if env.is_a?(Puppet::Node::Environment) env else Puppet::Node::Environment.new(env) end end def escaped_key URI.escape(key) end # LAK:NOTE This is a messy interface to the cache, and it's only # used by the Configurer class. I decided it was better to implement # it now and refactor later, when we have a better design, than # to spend another month coming up with a design now that might # not be any better. def ignore_cache? ignore_cache end def ignore_terminus? ignore_terminus end def initialize(indirection_name, method, key_or_instance, options_or_instance = {}) if options_or_instance.is_a? Hash options = options_or_instance @instance = nil else options = {} @instance = options_or_instance end self.indirection_name = indirection_name self.method = method set_attributes(options) @options = options.inject({}) { |hash, ary| hash[ary[0].to_sym] = ary[1]; hash } if key_or_instance.is_a?(String) || key_or_instance.is_a?(Symbol) key = key_or_instance else @instance ||= key_or_instance end if key # If the request key is a URI, then we need to treat it specially, # because it rewrites the key. We could otherwise strip server/port/etc # info out in the REST class, but it seemed bad design for the REST # class to rewrite the key. - if key.to_s =~ /^[a-z]:[\/\\]/i # It's an absolute path for Windows. - @key = key - elsif key.to_s =~ /^\w+:\/\// # it's a URI + + if key.to_s =~ /^\w+:\// and not Puppet::Util.absolute_path?(key.to_s) # it's a URI set_uri_key(key) else @key = key end end @key = @instance.name if ! @key and @instance end # Look up the indirection based on the name provided. def indirection Puppet::Indirector::Indirection.instance(indirection_name) end def indirection_name=(name) @indirection_name = name.to_sym end def model raise ArgumentError, "Could not find indirection '#{indirection_name}'" unless i = indirection i.model end # Should we allow use of the cached object? def use_cache? if defined?(@use_cache) ! ! use_cache else true end end # Are we trying to interact with multiple resources, or just one? def plural? method == :search end # Create the query string, if options are present. def query_string return "" unless options and ! options.empty? "?" + options.collect do |key, value| case value when nil; next when true, false; value = value.to_s when Fixnum, Bignum, Float; value = value # nothing when String; value = CGI.escape(value) when Symbol; value = CGI.escape(value.to_s) when Array; value = CGI.escape(YAML.dump(value)) else raise ArgumentError, "HTTP REST queries cannot handle values of type '#{value.class}'" end "#{key}=#{value}" end.join("&") end def to_hash result = options.dup OPTION_ATTRIBUTES.each do |attribute| if value = send(attribute) result[attribute] = value end end result end def to_s return(uri ? uri : "/#{indirection_name}/#{key}") end private def set_attributes(options) OPTION_ATTRIBUTES.each do |attribute| if options.include?(attribute) send(attribute.to_s + "=", options[attribute]) options.delete(attribute) end end end # Parse the key as a URI, setting attributes appropriately. def set_uri_key(key) @uri = key begin uri = URI.parse(URI.escape(key)) rescue => detail raise ArgumentError, "Could not understand URL #{key}: #{detail}" end # Just short-circuit these to full paths if uri.scheme == "file" - @key = URI.unescape(uri.path) + @key = Puppet::Util.uri_to_path(uri) return end @server = uri.host if uri.host # If the URI class can look up the scheme, it will provide a port, # otherwise it will default to '0'. if uri.port.to_i == 0 and uri.scheme == "puppet" @port = Puppet.settings[:masterport].to_i else @port = uri.port.to_i end @protocol = uri.scheme if uri.scheme == 'puppet' @key = URI.unescape(uri.path.sub(/^\//, '')) return end env, indirector, @key = URI.unescape(uri.path.sub(/^\//, '')).split('/',3) @key ||= '' self.environment = env unless env == '' end end diff --git a/lib/puppet/provider/file/posix.rb b/lib/puppet/provider/file/posix.rb index 7b7336b9d..a3c75eef2 100644 --- a/lib/puppet/provider/file/posix.rb +++ b/lib/puppet/provider/file/posix.rb @@ -1,97 +1,135 @@ Puppet::Type.type(:file).provide :posix do desc "Uses POSIX functionality to manage file's users and rights." confine :feature => :posix include Puppet::Util::POSIX include Puppet::Util::Warnings require 'etc' - def id2name(id) - return id.to_s if id.is_a?(Symbol) + def uid2name(id) + return id.to_s if id.is_a?(Symbol) or id.is_a?(String) return nil if id > Puppet[:maximum_uid].to_i begin user = Etc.getpwuid(id) - rescue TypeError - return nil - rescue ArgumentError + rescue TypeError, ArgumentError return nil end if user.uid == "" return nil else return user.name end end - def is_owner_insync?(current, should) - should.each do |value| - if value =~ /^\d+$/ - uid = Integer(value) - elsif value.is_a?(String) - fail "Could not find user #{value}" unless uid = uid(value) - else - uid = value - end - - return true if uid == current - end + # Determine if the user is valid, and if so, return the UID + def name2uid(value) + Integer(value) rescue uid(value) || false + end - unless Puppet.features.root? - warnonce "Cannot manage ownership unless running as root" - return true + def gid2name(id) + return id.to_s if id.is_a?(Symbol) or id.is_a?(String) + return nil if id > Puppet[:maximum_uid].to_i + + begin + group = Etc.getgrgid(id) + rescue TypeError, ArgumentError + return nil end - false + if group.gid == "" + return nil + else + return group.name + end end - # Determine if the user is valid, and if so, return the UID - def validuser?(value) - Integer(value) rescue uid(value) || false + def name2gid(value) + Integer(value) rescue gid(value) || false end - def retrieve(resource) + def owner unless stat = resource.stat return :absent end currentvalue = stat.uid # On OS X, files that are owned by -2 get returned as really # large UIDs instead of negative ones. This isn't a Ruby bug, # it's an OS X bug, since it shows up in perl, too. if currentvalue > Puppet[:maximum_uid].to_i self.warning "Apparently using negative UID (#{currentvalue}) on a platform that does not consistently handle them" currentvalue = :silly end currentvalue end - def sync(path, links, should) + def owner=(should) # Set our method appropriately, depending on links. - if links == :manage + if resource[:links] == :manage method = :lchown else method = :chown end - uid = nil - should.each do |user| - break if uid = validuser?(user) + begin + File.send(method, should, nil, resource[:path]) + rescue => detail + raise Puppet::Error, "Failed to set owner to '#{should}': #{detail}" + end + end + + def group + return :absent unless stat = resource.stat + + currentvalue = stat.gid + + # On OS X, files that are owned by -2 get returned as really + # large GIDs instead of negative ones. This isn't a Ruby bug, + # it's an OS X bug, since it shows up in perl, too. + if currentvalue > Puppet[:maximum_uid].to_i + self.warning "Apparently using negative GID (#{currentvalue}) on a platform that does not consistently handle them" + currentvalue = :silly end - raise Puppet::Error, "Could not find user(s) #{should.join(",")}" unless uid + currentvalue + end + + def group=(should) + # Set our method appropriately, depending on links. + if resource[:links] == :manage + method = :lchgrp + else + method = :chgrp + end begin - File.send(method, uid, nil, path) + File.send(method, nil, should, resource[:path]) rescue => detail - raise Puppet::Error, "Failed to set owner to '#{uid}': #{detail}" + raise Puppet::Error, "Failed to set group to '#{should}': #{detail}" + end + end + + def mode + if stat = resource.stat + return (stat.mode & 007777).to_s(8) + else + return :absent end + end - :file_changed + def mode=(value) + begin + File.chmod(value.to_i(8), resource[:path]) + rescue => detail + error = Puppet::Error.new("failed to set mode #{mode} on #{resource[:path]}: #{detail.message}") + error.set_backtrace detail.backtrace + raise error + end end end diff --git a/lib/puppet/provider/file/win32.rb b/lib/puppet/provider/file/win32.rb deleted file mode 100644 index 9423e8f00..000000000 --- a/lib/puppet/provider/file/win32.rb +++ /dev/null @@ -1,72 +0,0 @@ -Puppet::Type.type(:file).provide :microsoft_windows do - desc "Uses Microsoft Windows functionality to manage file's users and rights." - - confine :feature => :microsoft_windows - - include Puppet::Util::Warnings - - require 'sys/admin' if Puppet.features.microsoft_windows? - - def id2name(id) - return id.to_s if id.is_a?(Symbol) - return nil if id > Puppet[:maximum_uid].to_i - # should translate ID numbers to usernames - id - end - - def is_owner_insync?(current, should) - should.each do |value| - if value =~ /^\d+$/ - uid = Integer(value) - elsif value.is_a?(String) - fail "Could not find user #{value}" unless uid = uid(value) - else - uid = value - end - - return true if uid == current - end - - unless Puppet.features.root? - warnonce "Cannot manage ownership unless running as root" - return true - end - - false - end - - # Determine if the user is valid, and if so, return the UID - def validuser?(value) - info "Is '#{value}' a valid user?" - return 0 - begin - number = Integer(value) - return number - rescue ArgumentError - number = nil - end - (number = uid(value)) && number - end - - def retrieve(resource) - unless stat = resource.stat - return :absent - end - - currentvalue = stat.uid - - # On OS X, files that are owned by -2 get returned as really - # large UIDs instead of negative ones. This isn't a Ruby bug, - # it's an OS X bug, since it shows up in perl, too. - if currentvalue > Puppet[:maximum_uid].to_i - self.warning "Apparently using negative UID (#{currentvalue}) on a platform that does not consistently handle them" - currentvalue = :silly - end - - currentvalue - end - - def sync(path, links, should) - info("should set '%s'%%owner to '%s'" % [path, should]) - end -end diff --git a/lib/puppet/provider/file/windows.rb b/lib/puppet/provider/file/windows.rb new file mode 100644 index 000000000..d71e7d43c --- /dev/null +++ b/lib/puppet/provider/file/windows.rb @@ -0,0 +1,100 @@ +Puppet::Type.type(:file).provide :windows do + desc "Uses Microsoft Windows functionality to manage file's users and rights." + + confine :feature => :microsoft_windows + + include Puppet::Util::Warnings + + if Puppet.features.microsoft_windows? + require 'puppet/util/windows' + require 'puppet/util/adsi' + include Puppet::Util::Windows::Security + end + + ERROR_INVALID_SID_STRUCTURE = 1337 + + def id2name(id) + # If it's a valid sid, get the name. Otherwise, it's already a name, so + # just return it. + begin + if string_to_sid_ptr(id) + name = nil + Puppet::Util::ADSI.execquery( + "SELECT Name FROM Win32_Account WHERE SID = '#{id}' + AND LocalAccount = true" + ).each { |a| name ||= a.name } + return name + end + rescue Puppet::Util::Windows::Error => e + raise unless e.code == ERROR_INVALID_SID_STRUCTURE + end + + id + end + + # Determine if the account is valid, and if so, return the UID + def name2id(value) + # If it's a valid sid, then return it. Else, it's a name we need to convert + # to sid. + begin + return value if string_to_sid_ptr(value) + rescue Puppet::Util::Windows::Error => e + raise unless e.code == ERROR_INVALID_SID_STRUCTURE + end + + Puppet::Util::ADSI.sid_for_account(value) rescue nil + end + + # We use users and groups interchangeably, so use the same methods for both + # (the type expects different methods, so we have to oblige). + alias :uid2name :id2name + alias :gid2name :id2name + + alias :name2gid :name2id + alias :name2uid :name2id + + def owner + return :absent unless resource.exist? + get_owner(resource[:path]) + end + + def owner=(should) + begin + set_owner(should, resource[:path]) + rescue => detail + raise Puppet::Error, "Failed to set owner to '#{should}': #{detail}" + end + end + + def group + return :absent unless resource.exist? + get_group(resource[:path]) + end + + def group=(should) + begin + set_group(should, resource[:path]) + rescue => detail + raise Puppet::Error, "Failed to set group to '#{should}': #{detail}" + end + end + + def mode + if resource.exist? + get_mode(resource[:path]).to_s(8) + else + :absent + end + end + + def mode=(value) + begin + set_mode(value.to_i(8), resource[:path]) + rescue => detail + error = Puppet::Error.new("failed to set mode #{mode} on #{resource[:path]}: #{detail.message}") + error.set_backtrace detail.backtrace + raise error + end + :file_changed + end +end diff --git a/lib/puppet/type/file.rb b/lib/puppet/type/file.rb index d3c66bc02..bfe61144d 100644 --- a/lib/puppet/type/file.rb +++ b/lib/puppet/type/file.rb @@ -1,821 +1,803 @@ require 'digest/md5' require 'cgi' require 'etc' require 'uri' require 'fileutils' +require 'enumerator' +require 'pathname' require 'puppet/network/handler' require 'puppet/util/diff' require 'puppet/util/checksums' require 'puppet/network/client' require 'puppet/util/backups' Puppet::Type.newtype(:file) do include Puppet::Util::MethodHelper include Puppet::Util::Checksums include Puppet::Util::Backups @doc = "Manages local files, including setting ownership and permissions, creation of both files and directories, and retrieving entire files from remote servers. As Puppet matures, it expected that the `file` resource will be used less and less to manage content, and instead native resources will be used to do so. If you find that you are often copying files in from a central location, rather than using native resources, please contact Puppet Labs and we can hopefully work with you to develop a native resource to support what you are doing. **Autorequires:** If Puppet is managing the user or group that owns a file, the file resource will autorequire them. If Puppet is managing any parent directories of a file, the file resource will autorequire them." def self.title_patterns [ [ /^(.*?)\/*\Z/m, [ [ :path, lambda{|x| x} ] ] ] ] end newparam(:path) do desc "The path to the file to manage. Must be fully qualified." isnamevar validate do |value| - # accept various path syntaxes: lone slash, posix, win32, unc - unless (Puppet.features.posix? and value =~ /^\//) or (value =~ /^[A-Za-z]:\// or value =~ /^\/\/[^\/]+\/[^\/]+/) + unless Puppet::Util.absolute_path?(value) fail Puppet::Error, "File paths must be fully qualified, not '#{value}'" end end # convert the current path in an index into the collection and the last # path name. The aim is to use less storage for all common paths in a hierarchy munge do |value| - # We need to save off, and remove the volume designator in the - # path if it is there, since File.split does not handle paths - # with volume designators properly, except when run on Windows. - # Since we are potentially compiling a catalog for a Windows - # machine on a non-Windows master, we need to handle this - # ourselves. - optional_volume_designator = value.match(/^([a-z]:)[\/\\].*/i) - value_without_designator = value.sub(/^(?:[a-z]:)?(.*)/i, '\1') - - path, name = ::File.split(value_without_designator.gsub(/\/+/,'/')) - - if optional_volume_designator - path = optional_volume_designator[1] + path - end + # We know the value is absolute, so expanding it will just standardize it. + path, name = ::File.split(::File.expand_path value) { :index => Puppet::FileCollection.collection.index(path), :name => name } end # and the reverse unmunge do |value| basedir = Puppet::FileCollection.collection.path(value[:index]) - # a lone slash as :name indicates a root dir on windows - if value[:name] == '/' - basedir - else - ::File.join( basedir, value[:name] ) - end + + ::File.expand_path ::File.join( basedir, value[:name] ) end end newparam(:backup) do desc "Whether files should be backed up before being replaced. The preferred method of backing files up is via a `filebucket`, which stores files by their MD5 sums and allows easy retrieval without littering directories with backups. You can specify a local filebucket or a network-accessible server-based filebucket by setting `backup => bucket-name`. Alternatively, if you specify any value that begins with a `.` (e.g., `.puppet-bak`), then Puppet will use copy the file in the same directory with that value as the extension of the backup. Setting `backup => false` disables all backups of the file in question. Puppet automatically creates a local filebucket named `puppet` and defaults to backing up there. To use a server-based filebucket, you must specify one in your configuration. filebucket { main: server => puppet, path => false, # The path => false line works around a known issue with the filebucket type. } The `puppet master` daemon creates a filebucket by default, so you can usually back up to your main server with this configuration. Once you've described the bucket in your configuration, you can use it in any file's backup attribute: file { \"/my/file\": source => \"/path/in/nfs/or/something\", backup => main } This will back the file up to the central server. At this point, the benefits of using a central filebucket are that you do not have backup files lying around on each of your machines, a given version of a file is only backed up once, you can restore any given file manually (no matter how old), and you can use Puppet Dashboard to view file contents. Eventually, transactional support will be able to automatically restore filebucketed files. " defaultto "puppet" munge do |value| # I don't really know how this is happening. value = value.shift if value.is_a?(Array) case value when false, "false", :false false when true, "true", ".puppet-bak", :true ".puppet-bak" when String value else self.fail "Invalid backup type #{value.inspect}" end end end newparam(:recurse) do desc "Whether and how deeply to do recursive management. Options are: * `inf,true` --- Regular style recursion on both remote and local directory structure. * `remote` --- Descends recursively into the remote directory but not the local directory. Allows copying of a few files into a directory containing many unmanaged files without scanning all the local files. * `false` --- Default of no recursion. * `[0-9]+` --- Same as true, but limit recursion. Warning: this syntax has been deprecated in favor of the `recurselimit` attribute. " newvalues(:true, :false, :inf, :remote, /^[0-9]+$/) # Replace the validation so that we allow numbers in # addition to string representations of them. validate { |arg| } munge do |value| newval = super(value) case newval when :true, :inf; true when :false; false when :remote; :remote when Integer, Fixnum, Bignum self.warning "Setting recursion depth with the recurse parameter is now deprecated, please use recurselimit" # recurse == 0 means no recursion return false if value == 0 resource[:recurselimit] = value true when /^\d+$/ self.warning "Setting recursion depth with the recurse parameter is now deprecated, please use recurselimit" value = Integer(value) # recurse == 0 means no recursion return false if value == 0 resource[:recurselimit] = value true else self.fail "Invalid recurse value #{value.inspect}" end end end newparam(:recurselimit) do desc "How deeply to do recursive management." newvalues(/^[0-9]+$/) munge do |value| newval = super(value) case newval when Integer, Fixnum, Bignum; value when /^\d+$/; Integer(value) else self.fail "Invalid recurselimit value #{value.inspect}" end end end newparam(:replace, :boolean => true) do desc "Whether or not to replace a file that is sourced but exists. This is useful for using file sources purely for initialization." newvalues(:true, :false) aliasvalue(:yes, :true) aliasvalue(:no, :false) defaultto :true end newparam(:force, :boolean => true) do desc "Force the file operation. Currently only used when replacing directories with links." newvalues(:true, :false) defaultto false end newparam(:ignore) do desc "A parameter which omits action on files matching specified patterns during recursion. Uses Ruby's builtin globbing engine, so shell metacharacters are fully supported, e.g. `[a-z]*`. Matches that would descend into the directory structure are ignored, e.g., `*/*`." validate do |value| unless value.is_a?(Array) or value.is_a?(String) or value == false self.devfail "Ignore must be a string or an Array" end end end newparam(:links) do desc "How to handle links during file actions. During file copying, `follow` will copy the target file instead of the link, `manage` will copy the link itself, and `ignore` will just pass it by. When not copying, `manage` and `ignore` behave equivalently (because you cannot really ignore links entirely during local recursion), and `follow` will manage the file to which the link points." newvalues(:follow, :manage) defaultto :manage end newparam(:purge, :boolean => true) do desc "Whether unmanaged files should be purged. If you have a filebucket configured the purged files will be uploaded, but if you do not, this will destroy data. Only use this option for generated files unless you really know what you are doing. This option only makes sense when recursively managing directories. Note that when using `purge` with `source`, Puppet will purge any files that are not on the remote system." defaultto :false newvalues(:true, :false) end newparam(:sourceselect) do desc "Whether to copy all valid sources, or just the first one. This parameter is only used in recursive copies; by default, the first valid source is the only one used as a recursive source, but if this parameter is set to `all`, then all valid sources will have all of their contents copied to the local host, and for sources that have the same file, the source earlier in the list will be used." defaultto :first newvalues(:first, :all) end # Autorequire the nearest ancestor directory found in the catalog. autorequire(:file) do - basedir = ::File.dirname(self[:path]) - if basedir != self[:path] - parents = [] - until basedir == parents.last - parents << basedir - basedir = ::File.dirname(basedir) - end - # The filename of the first ancestor found, or nil - parents.find { |dir| catalog.resource(:file, dir) } - else - nil + path = Pathname(self[:path]) + if !path.root? + # Start at our parent, to avoid autorequiring ourself + parents = path.parent.enum_for(:ascend) + found = parents.find { |p| catalog.resource(:file, p.to_s) } + found and found.to_s end end # Autorequire the owner and group of the file. {:user => :owner, :group => :group}.each do |type, property| autorequire(type) do if @parameters.include?(property) # The user/group property automatically converts to IDs next unless should = @parameters[property].shouldorig val = should[0] if val.is_a?(Integer) or val =~ /^\d+$/ nil else val end end end end CREATORS = [:content, :source, :target] SOURCE_ONLY_CHECKSUMS = [:none, :ctime, :mtime] validate do creator_count = 0 CREATORS.each do |param| creator_count += 1 if self.should(param) end creator_count += 1 if @parameters.include?(:source) self.fail "You cannot specify more than one of #{CREATORS.collect { |p| p.to_s}.join(", ")}" if creator_count > 1 self.fail "You cannot specify a remote recursion without a source" if !self[:source] and self[:recurse] == :remote self.fail "You cannot specify source when using checksum 'none'" if self[:checksum] == :none && !self[:source].nil? SOURCE_ONLY_CHECKSUMS.each do |checksum_type| self.fail "You cannot specify content when using checksum '#{checksum_type}'" if self[:checksum] == checksum_type && !self[:content].nil? end self.warning "Possible error: recurselimit is set but not recurse, no recursion will happen" if !self[:recurse] and self[:recurselimit] end def self.[](path) return nil unless path super(path.gsub(/\/+/, '/').sub(/\/$/, '')) end def self.instances return [] end # Determine the user to write files as. def asuser if self.should(:owner) and ! self.should(:owner).is_a?(Symbol) writeable = Puppet::Util::SUIDManager.asuser(self.should(:owner)) { FileTest.writable?(::File.dirname(self[:path])) } # If the parent directory is writeable, then we execute # as the user in question. Otherwise we'll rely on # the 'owner' property to do things. asuser = self.should(:owner) if writeable end asuser end def bucket return @bucket if @bucket backup = self[:backup] return nil unless backup return nil if backup =~ /^\./ unless catalog or backup == "puppet" fail "Can not find filebucket for backups without a catalog" end unless catalog and filebucket = catalog.resource(:filebucket, backup) or backup == "puppet" fail "Could not find filebucket #{backup} specified in backup" end return default_bucket unless filebucket @bucket = filebucket.bucket @bucket end def default_bucket Puppet::Type.type(:filebucket).mkdefaultbucket.bucket end # Does the file currently exist? Just checks for whether # we have a stat def exist? stat ? true : false end # We have to do some extra finishing, to retrieve our bucket if # there is one. def finish # Look up our bucket, if there is one bucket super end # Create any children via recursion or whatever. def eval_generate return [] unless self.recurse? recurse #recurse.reject do |resource| # catalog.resource(:file, resource[:path]) #end.each do |child| # catalog.add_resource child # catalog.relationship_graph.add_edge self, child #end end def flush # We want to make sure we retrieve metadata anew on each transaction. @parameters.each do |name, param| param.flush if param.respond_to?(:flush) end @stat = :needs_stat end def initialize(hash) # Used for caching clients @clients = {} super # If they've specified a source, we get our 'should' values # from it. unless self[:ensure] if self[:target] self[:ensure] = :symlink elsif self[:content] self[:ensure] = :file end end @stat = :needs_stat end # Configure discovered resources to be purged. def mark_children_for_purging(children) children.each do |name, child| next if child[:source] child[:ensure] = :absent end end # Create a new file or directory object as a child to the current # object. def newchild(path) full_path = ::File.join(self[:path], path) # Add some new values to our original arguments -- these are the ones # set at initialization. We specifically want to exclude any param # values set by the :source property or any default values. # LAK:NOTE This is kind of silly, because the whole point here is that # the values set at initialization should live as long as the resource # but values set by default or by :source should only live for the transaction # or so. Unfortunately, we don't have a straightforward way to manage # the different lifetimes of this data, so we kludge it like this. # The right-side hash wins in the merge. options = @original_parameters.merge(:path => full_path).reject { |param, value| value.nil? } # These should never be passed to our children. [:parent, :ensure, :recurse, :recurselimit, :target, :alias, :source].each do |param| options.delete(param) if options.include?(param) end self.class.new(options) end # Files handle paths specially, because they just lengthen their # path names, rather than including the full parent's title each # time. def pathbuilder # We specifically need to call the method here, so it looks # up our parent in the catalog graph. if parent = parent() # We only need to behave specially when our parent is also # a file if parent.is_a?(self.class) # Remove the parent file name list = parent.pathbuilder list.pop # remove the parent's path info return list << self.ref else return super end else return [self.ref] end end # Should we be purging? def purge? @parameters.include?(:purge) and (self[:purge] == :true or self[:purge] == "true") end # Recursively generate a list of file resources, which will # be used to copy remote files, manage local files, and/or make links # to map to another directory. def recurse children = (self[:recurse] == :remote) ? {} : recurse_local if self[:target] recurse_link(children) elsif self[:source] recurse_remote(children) end # If we're purging resources, then delete any resource that isn't on the # remote system. mark_children_for_purging(children) if self.purge? result = children.values.sort { |a, b| a[:path] <=> b[:path] } remove_less_specific_files(result) end # This is to fix bug #2296, where two files recurse over the same # set of files. It's a rare case, and when it does happen you're # not likely to have many actual conflicts, which is good, because # this is a pretty inefficient implementation. def remove_less_specific_files(files) mypath = self[:path].split(::File::Separator) other_paths = catalog.vertices. select { |r| r.is_a?(self.class) and r[:path] != self[:path] }. collect { |r| r[:path].split(::File::Separator) }. select { |p| p[0,mypath.length] == mypath } return files if other_paths.empty? files.reject { |file| path = file[:path].split(::File::Separator) other_paths.any? { |p| path[0,p.length] == p } } end # A simple method for determining whether we should be recursing. def recurse? self[:recurse] == true or self[:recurse] == :remote end # Recurse the target of the link. def recurse_link(children) perform_recursion(self[:target]).each do |meta| if meta.relative_path == "." self[:ensure] = :directory next end children[meta.relative_path] ||= newchild(meta.relative_path) if meta.ftype == "directory" children[meta.relative_path][:ensure] = :directory else children[meta.relative_path][:ensure] = :link children[meta.relative_path][:target] = meta.full_path end end children end # Recurse the file itself, returning a Metadata instance for every found file. def recurse_local result = perform_recursion(self[:path]) return {} unless result result.inject({}) do |hash, meta| next hash if meta.relative_path == "." hash[meta.relative_path] = newchild(meta.relative_path) hash end end # Recurse against our remote file. def recurse_remote(children) sourceselect = self[:sourceselect] total = self[:source].collect do |source| next unless result = perform_recursion(source) return if top = result.find { |r| r.relative_path == "." } and top.ftype != "directory" result.each { |data| data.source = "#{source}/#{data.relative_path}" } break result if result and ! result.empty? and sourceselect == :first result end.flatten # This only happens if we have sourceselect == :all unless sourceselect == :first found = [] total.reject! do |data| result = found.include?(data.relative_path) found << data.relative_path unless found.include?(data.relative_path) result end end total.each do |meta| if meta.relative_path == "." parameter(:source).metadata = meta next end children[meta.relative_path] ||= newchild(meta.relative_path) children[meta.relative_path][:source] = meta.source children[meta.relative_path][:checksum] = :md5 if meta.ftype == "file" children[meta.relative_path].parameter(:source).metadata = meta end children end def perform_recursion(path) Puppet::FileServing::Metadata.indirection.search( path, :links => self[:links], :recurse => (self[:recurse] == :remote ? true : self[:recurse]), :recurselimit => self[:recurselimit], :ignore => self[:ignore], :checksum_type => (self[:source] || self[:content]) ? self[:checksum] : :none ) end # Remove any existing data. This is only used when dealing with # links or directories. def remove_existing(should) return unless s = stat self.fail "Could not back up; will not replace" unless perform_backup unless should.to_s == "link" return if s.ftype.to_s == should.to_s end case s.ftype when "directory" if self[:force] == :true debug "Removing existing directory for replacement with #{should}" FileUtils.rmtree(self[:path]) else notice "Not removing directory; use 'force' to override" + return end when "link", "file" debug "Removing existing #{s.ftype} for replacement with #{should}" ::File.unlink(self[:path]) else self.fail "Could not back up files of type #{s.ftype}" end @stat = :needs_stat + true end def retrieve if source = parameter(:source) source.copy_source_values end super end # Set the checksum, from another property. There are multiple # properties that modify the contents of a file, and they need the # ability to make sure that the checksum value is in sync. def setchecksum(sum = nil) if @parameters.include? :checksum if sum @parameters[:checksum].checksum = sum else # If they didn't pass in a sum, then tell checksum to # figure it out. currentvalue = @parameters[:checksum].retrieve @parameters[:checksum].checksum = currentvalue end end end # Should this thing be a normal file? This is a relatively complex # way of determining whether we're trying to create a normal file, # and it's here so that the logic isn't visible in the content property. def should_be_file? return true if self[:ensure] == :file # I.e., it's set to something like "directory" return false if e = self[:ensure] and e != :present # The user doesn't really care, apparently if self[:ensure] == :present return true unless s = stat return(s.ftype == "file" ? true : false) end # If we've gotten here, then :ensure isn't set return true if self[:content] return true if stat and stat.ftype == "file" false end # Stat our file. Depending on the value of the 'links' attribute, we # use either 'stat' or 'lstat', and we expect the properties to use the # resulting stat object accordingly (mostly by testing the 'ftype' # value). # # We use the initial value :needs_stat to ensure we only stat the file once, # but can also keep track of a failed stat (@stat == nil). This also allows # us to re-stat on demand by setting @stat = :needs_stat. def stat return @stat unless @stat == :needs_stat method = :stat # Files are the only types that support links if (self.class.name == :file and self[:links] != :follow) or self.class.name == :tidy method = :lstat end @stat = begin ::File.send(method, self[:path]) rescue Errno::ENOENT => error nil rescue Errno::EACCES => error warning "Could not stat; permission denied" nil end end # We have to hack this just a little bit, because otherwise we'll get # an error when the target and the contents are created as properties on # the far side. def to_trans(retrieve = true) obj = super obj.delete(:target) if obj[:target] == :notlink obj end # Write out the file. Requires the property name for logging. # Write will be done by the content property, along with checksum computation def write(property) remove_existing(:file) use_temporary_file = write_temporary_file? if use_temporary_file path = "#{self[:path]}.puppettmp_#{rand(10000)}" path = "#{self[:path]}.puppettmp_#{rand(10000)}" while ::File.exists?(path) or ::File.symlink?(path) else path = self[:path] end mode = self.should(:mode) # might be nil umask = mode ? 000 : 022 mode_int = mode ? mode.to_i(8) : nil content_checksum = Puppet::Util.withumask(umask) { ::File.open(path, 'w', mode_int ) { |f| write_content(f) } } # And put our new file in place if use_temporary_file # This is only not true when our file is empty. begin fail_if_checksum_is_wrong(path, content_checksum) if validate_checksum? ::File.rename(path, self[:path]) rescue => detail fail "Could not rename temporary file #{path} to #{self[:path]}: #{detail}" ensure # Make sure the created file gets removed ::File.unlink(path) if FileTest.exists?(path) end end # make sure all of the modes are actually correct property_fix end private # Should we validate the checksum of the file we're writing? def validate_checksum? self[:checksum] !~ /time/ end # Make sure the file we wrote out is what we think it is. def fail_if_checksum_is_wrong(path, content_checksum) newsum = parameter(:checksum).sum_file(path) return if [:absent, nil, content_checksum].include?(newsum) self.fail "File written to disk did not match checksum; discarding changes (#{content_checksum} vs #{newsum})" end # write the current content. Note that if there is no content property # simply opening the file with 'w' as done in write is enough to truncate # or write an empty length file. def write_content(file) (content = property(:content)) && content.write(file) end private def write_temporary_file? # unfortunately we don't know the source file size before fetching it # so let's assume the file won't be empty (c = property(:content) and c.length) || (s = @parameters[:source] and 1) end # There are some cases where all of the work does not get done on # file creation/modification, so we have to do some extra checking. def property_fix properties.each do |thing| next unless [:mode, :owner, :group, :seluser, :selrole, :seltype, :selrange].include?(thing.name) # Make sure we get a new stat objct @stat = :needs_stat currentvalue = thing.retrieve thing.sync unless thing.safe_insync?(currentvalue) end end end # We put all of the properties in separate files, because there are so many # of them. The order these are loaded is important, because it determines # the order they are in the property lit. require 'puppet/type/file/checksum' require 'puppet/type/file/content' # can create the file require 'puppet/type/file/source' # can create the file require 'puppet/type/file/target' # creates a different type of file require 'puppet/type/file/ensure' # can create the file require 'puppet/type/file/owner' require 'puppet/type/file/group' require 'puppet/type/file/mode' require 'puppet/type/file/type' require 'puppet/type/file/selcontext' # SELinux file context require 'puppet/type/file/ctime' require 'puppet/type/file/mtime' diff --git a/lib/puppet/type/file/group.rb b/lib/puppet/type/file/group.rb index 4d1f2f4e6..4310a106d 100755 --- a/lib/puppet/type/file/group.rb +++ b/lib/puppet/type/file/group.rb @@ -1,113 +1,33 @@ require 'puppet/util/posix' # Manage file group ownership. module Puppet Puppet::Type.type(:file).newproperty(:group) do - include Puppet::Util::POSIX - - require 'etc' desc "Which group should own the file. Argument can be either group name or group ID." - @event = :file_changed validate do |group| raise(Puppet::Error, "Invalid group name '#{group.inspect}'") unless group and group != "" end - def id2name(id) - return id.to_s if id.is_a?(Symbol) - return nil if id > Puppet[:maximum_uid].to_i - begin - group = Etc.getgrgid(id) - rescue ArgumentError - return nil - end - if group.gid == "" - return nil - else - return group.name - end - end - - # We want to print names, not numbers - def is_to_s(currentvalue) - if currentvalue.is_a? Integer - id2name(currentvalue) || currentvalue - else - return currentvalue.to_s - end - end - - def should_to_s(newvalue = @should) - if newvalue.is_a? Integer - id2name(newvalue) || newvalue - else - return newvalue.to_s - end - end - def insync?(current) - @should.each do |value| - if value =~ /^\d+$/ - gid = Integer(value) - elsif value.is_a?(String) - fail "Could not find group #{value}" unless gid = gid(value) - else - gid = value - end - - return true if gid == current + # We don't want to validate/munge groups until we actually start to + # evaluate this property, because they might be added during the catalog + # apply. + @should.map! do |val| + provider.name2gid(val) or raise "Could not find group #{val}" end - false - end - - def retrieve - return :absent unless stat = resource.stat - currentvalue = stat.gid - - # On OS X, files that are owned by -2 get returned as really - # large GIDs instead of negative ones. This isn't a Ruby bug, - # it's an OS X bug, since it shows up in perl, too. - if currentvalue > Puppet[:maximum_uid].to_i - self.warning "Apparently using negative GID (#{currentvalue}) on a platform that does not consistently handle them" - currentvalue = :silly - end - - currentvalue + @should.include?(current) end - # Determine if the group is valid, and if so, return the GID - def validgroup?(value) - Integer(value) rescue gid(value) || false + # We want to print names, not numbers + def is_to_s(currentvalue) + provider.gid2name(currentvalue) || currentvalue end - # Normal users will only be able to manage certain groups. Right now, - # we'll just let it fail, but we should probably set things up so - # that users get warned if they try to change to an unacceptable group. - def sync - # Set our method appropriately, depending on links. - if resource[:links] == :manage - method = :lchown - else - method = :chown - end - - gid = nil - @should.each do |group| - break if gid = validgroup?(group) - end - - raise Puppet::Error, "Could not find group(s) #{@should.join(",")}" unless gid - - begin - # set owner to nil so it's ignored - File.send(method, nil, gid, resource[:path]) - rescue => detail - error = Puppet::Error.new( "failed to chgrp #{resource[:path]} to #{gid}: #{detail.message}") - raise error - end - :file_changed + def should_to_s(newvalue) + provider.gid2name(newvalue) || newvalue end end end diff --git a/lib/puppet/type/file/mode.rb b/lib/puppet/type/file/mode.rb index 9f58e6fb0..7dd2174c8 100755 --- a/lib/puppet/type/file/mode.rb +++ b/lib/puppet/type/file/mode.rb @@ -1,90 +1,60 @@ # Manage file modes. This state should support different formats # for specification (e.g., u+rwx, or -0011), but for now only supports # specifying the full mode. module Puppet Puppet::Type.type(:file).newproperty(:mode) do - require 'etc' desc "Mode the file should be. Currently relatively limited: you must specify the exact mode the file should be. Note that when you set the mode of a directory, Puppet always sets the search/traverse (1) bit anywhere the read (4) bit is set. This is almost always what you want: read allows you to list the entries in a directory, and search/traverse allows you to access (read/write/execute) those entries.) Because of this feature, you can recursively make a directory and all of the files in it world-readable by setting e.g.: file { '/some/dir': mode => 644, recurse => true, } In this case all of the files underneath `/some/dir` will have mode 644, and all of the directories will have mode 755." - @event = :file_changed + validate do |value| + if value.is_a?(String) and value !~ /^[0-7]+$/ + raise Puppet::Error, "File modes can only be octal numbers, not #{should.inspect}" + end + end munge do |should| - if should.is_a?(String) - unless should =~ /^[0-7]+$/ - raise Puppet::Error, "File modes can only be octal numbers, not #{should.inspect}" - end - should.to_i(8).to_s(8) - else - should.to_s(8) - end + dirmask(should) end # If we're a directory, we need to be executable for all cases # that are readable. This should probably be selectable, but eh. def dirmask(value) - if FileTest.directory?(@resource[:path]) - value = value.to_i(8) + value = value.to_i(8) unless value.is_a? Integer + if FileTest.directory?(resource[:path]) value |= 0100 if value & 0400 != 0 value |= 010 if value & 040 != 0 value |= 01 if value & 04 != 0 - value = value.to_s(8) end - value + value.to_s(8) end + # If we're not following links and we're a link, then we just turn + # off mode management entirely. def insync?(currentvalue) if stat = @resource.stat and stat.ftype == "link" and @resource[:links] != :follow self.debug "Not managing symlink mode" return true else return super(currentvalue) end end - - def retrieve - # If we're not following links and we're a link, then we just turn - # off mode management entirely. - - if stat = @resource.stat - unless defined?(@fixed) - @should &&= @should.collect { |s| self.dirmask(s) } - end - return (stat.mode & 007777).to_s(8) - else - return :absent - end - end - - def sync - mode = self.should - - begin - File.chmod(mode.to_i(8), @resource[:path]) - rescue => detail - error = Puppet::Error.new("failed to chmod #{@resource[:path]}: #{detail.message}") - error.set_backtrace detail.backtrace - raise error - end - :file_changed - end end end diff --git a/lib/puppet/type/file/owner.rb b/lib/puppet/type/file/owner.rb index 483cc7fce..2eda3c406 100755 --- a/lib/puppet/type/file/owner.rb +++ b/lib/puppet/type/file/owner.rb @@ -1,52 +1,36 @@ module Puppet Puppet::Type.type(:file).newproperty(:owner) do + include Puppet::Util::Warnings desc "To whom the file should belong. Argument can be user name or user ID." - @event = :file_changed def insync?(current) - provider.is_owner_insync?(current, @should) - end + # We don't want to validate/munge users until we actually start to + # evaluate this property, because they might be added during the catalog + # apply. + @should.map! do |val| + provider.name2uid(val) or raise "Could not find user #{val}" + end - # We want to print names, not numbers - def is_to_s(currentvalue) - provider.id2name(currentvalue) || currentvalue - end + return true if @should.include?(current) - def should_to_s(newvalue = @should) - case newvalue - when Symbol - newvalue.to_s - when Integer - provider.id2name(newvalue) || newvalue - when String - newvalue - else - raise Puppet::DevError, "Invalid uid type #{newvalue.class}(#{newvalue})" + unless Puppet.features.root? + warnonce "Cannot manage ownership unless running as root" + return true end + + false end - def retrieve - if self.should - @should = @should.collect do |val| - unless val.is_a?(Integer) - if tmp = provider.validuser?(val) - val = tmp - else - raise "Could not find user #{val}" - end - else - val - end - end - end - provider.retrieve(@resource) + # We want to print names, not numbers + def is_to_s(currentvalue) + provider.uid2name(currentvalue) || currentvalue end - def sync - provider.sync(resource[:path], resource[:links], @should) + def should_to_s(newvalue) + provider.uid2name(newvalue) || newvalue end end end diff --git a/lib/puppet/type/file/source.rb b/lib/puppet/type/file/source.rb index 8653a8f7a..2080b9ee1 100755 --- a/lib/puppet/type/file/source.rb +++ b/lib/puppet/type/file/source.rb @@ -1,188 +1,203 @@ require 'puppet/file_serving/content' require 'puppet/file_serving/metadata' module Puppet # Copy files from a local or remote source. This state *only* does any work # when the remote file is an actual file; in that case, this state copies # the file down. If the remote file is a dir or a link or whatever, then # this state, during retrieval, modifies the appropriate other states # so that things get taken care of appropriately. Puppet::Type.type(:file).newparam(:source) do include Puppet::Util::Diff attr_accessor :source, :local desc "Copy a file over the current file. Uses `checksum` to determine when a file should be copied. Valid values are either fully qualified paths to files, or URIs. Currently supported URI types are *puppet* and *file*. This is one of the primary mechanisms for getting content into applications that Puppet does not directly support and is very useful for those configuration files that don't change much across sytems. For instance: class sendmail { file { \"/etc/mail/sendmail.cf\": source => \"puppet://server/modules/module_name/sendmail.cf\" } } You can also leave out the server name, in which case `puppet agent` will fill in the name of its configuration server and `puppet apply` will use the local filesystem. This makes it easy to use the same configuration in both local and centralized forms. Currently, only the `puppet` scheme is supported for source URL's. Puppet will connect to the file server running on `server` to retrieve the contents of the file. If the `server` part is empty, the behavior of the command-line interpreter (`puppet apply`) and the client demon (`puppet agent`) differs slightly: `apply` will look such a file up on the module path on the local host, whereas `agent` will connect to the puppet server that it received the manifest from. See the [fileserver configuration documentation](http://docs.puppetlabs.com/guides/file_serving.html) for information on how to configure and use file services within Puppet. If you specify multiple file sources for a file, then the first source that exists will be used. This allows you to specify what amount to search paths for files: file { \"/path/to/my/file\": source => [ \"/modules/nfs/files/file.$host\", \"/modules/nfs/files/file.$operatingsystem\", \"/modules/nfs/files/file\" ] } This will use the first found file as the source. You cannot currently copy links using this mechanism; set `links` to `follow` if any remote sources are links. " validate do |sources| sources = [sources] unless sources.is_a?(Array) sources.each do |source| + next if Puppet::Util.absolute_path?(source) + begin uri = URI.parse(URI.escape(source)) rescue => detail self.fail "Could not understand source #{source}: #{detail}" end - self.fail "Cannot use URLs of type '#{uri.scheme}' as source for fileserving" unless uri.scheme.nil? or %w{file puppet}.include?(uri.scheme) or (Puppet.features.microsoft_windows? and uri.scheme =~ /^[a-z]$/i) + self.fail "Cannot use relative URLs '#{source}'" unless uri.absolute? + self.fail "Cannot use opaque URLs '#{source}'" unless uri.hierarchical? + self.fail "Cannot use URLs of type '#{uri.scheme}' as source for fileserving" unless %w{file puppet}.include?(uri.scheme) end end + SEPARATOR_REGEX = [Regexp.escape(File::SEPARATOR.to_s), Regexp.escape(File::ALT_SEPARATOR.to_s)].join + munge do |sources| sources = [sources] unless sources.is_a?(Array) - sources.collect { |source| source.sub(/\/$/, '') } + sources.map do |source| + source = source.sub(/[#{SEPARATOR_REGEX}]+$/, '') + + if Puppet::Util.absolute_path?(source) + URI.unescape(Puppet::Util.path_to_uri(source).to_s) + else + source + end + end end def change_to_s(currentvalue, newvalue) # newvalue = "{md5}#{@metadata.checksum}" if @resource.property(:ensure).retrieve == :absent return "creating from source #{metadata.source} with contents #{metadata.checksum}" else return "replacing from source #{metadata.source} with contents #{metadata.checksum}" end end def checksum metadata && metadata.checksum end # Look up (if necessary) and return remote content. def content return @content if @content raise Puppet::DevError, "No source for content was stored with the metadata" unless metadata.source unless tmp = Puppet::FileServing::Content.indirection.find(metadata.source) fail "Could not find any content at %s" % metadata.source end @content = tmp.content end # Copy the values from the source to the resource. Yay. def copy_source_values devfail "Somehow got asked to copy source values without any metadata" unless metadata # Take each of the stats and set them as states on the local file # if a value has not already been provided. [:owner, :mode, :group, :checksum].each do |metadata_method| param_name = (metadata_method == :checksum) ? :content : metadata_method next if metadata_method == :owner and !Puppet.features.root? next if metadata_method == :checksum and metadata.ftype == "directory" next if metadata_method == :checksum and metadata.ftype == "link" and metadata.links == :manage if resource[param_name].nil? or resource[param_name] == :absent resource[param_name] = metadata.send(metadata_method) end end if resource[:ensure] == :absent # We know all we need to elsif metadata.ftype != "link" resource[:ensure] = metadata.ftype elsif @resource[:links] == :follow resource[:ensure] = :present else resource[:ensure] = "link" resource[:target] = metadata.destination end end def found? ! (metadata.nil? or metadata.ftype.nil?) end attr_writer :metadata # Provide, and retrieve if necessary, the metadata for this file. Fail # if we can't find data about this host, and fail if there are any # problems in our query. def metadata return @metadata if @metadata return nil unless value value.each do |source| begin if data = Puppet::FileServing::Metadata.indirection.find(source) @metadata = data @metadata.source = source break end rescue => detail fail detail, "Could not retrieve file metadata for #{source}: #{detail}" end end fail "Could not retrieve information from environment #{Puppet[:environment]} source(s) #{value.join(", ")}" unless @metadata @metadata end def local? - found? and uri and (uri.scheme || "file") == "file" + found? and scheme == "file" end def full_path - URI.unescape(uri.path) if found? and uri + Puppet::Util.uri_to_path(uri) if found? end def server (uri and uri.host) or Puppet.settings[:server] end def port (uri and uri.port) or Puppet.settings[:masterport] end - private - def uri - return nil if metadata.source =~ /^[a-z]:[\/\\]/i # Abspath for Windows + def scheme + (uri and uri.scheme) + end + def uri @uri ||= URI.parse(URI.escape(metadata.source)) end end end diff --git a/lib/puppet/util.rb b/lib/puppet/util.rb index e10272846..b115a160d 100644 --- a/lib/puppet/util.rb +++ b/lib/puppet/util.rb @@ -1,453 +1,499 @@ # A module to collect utility functions. require 'English' require 'puppet/util/monkey_patches' require 'sync' require 'tempfile' require 'puppet/external/lock' require 'monitor' require 'puppet/util/execution_stub' +require 'uri' module Puppet # A command failed to execute. require 'puppet/error' class ExecutionFailure < Puppet::Error end module Util require 'benchmark' # These are all for backward compatibility -- these are methods that used # to be in Puppet::Util but have been moved into external modules. require 'puppet/util/posix' extend Puppet::Util::POSIX @@sync_objects = {}.extend MonitorMixin def self.activerecord_version if (defined?(::ActiveRecord) and defined?(::ActiveRecord::VERSION) and defined?(::ActiveRecord::VERSION::MAJOR) and defined?(::ActiveRecord::VERSION::MINOR)) ([::ActiveRecord::VERSION::MAJOR, ::ActiveRecord::VERSION::MINOR].join('.').to_f) else 0 end end def self.synchronize_on(x,type) sync_object,users = 0,1 begin @@sync_objects.synchronize { (@@sync_objects[x] ||= [Sync.new,0])[users] += 1 } @@sync_objects[x][sync_object].synchronize(type) { yield } ensure @@sync_objects.synchronize { @@sync_objects.delete(x) unless (@@sync_objects[x][users] -= 1) > 0 } end end # Change the process to a different user def self.chuser if group = Puppet[:group] begin Puppet::Util::SUIDManager.change_group(group, true) rescue => detail Puppet.warning "could not change to group #{group.inspect}: #{detail}" $stderr.puts "could not change to group #{group.inspect}" # Don't exit on failed group changes, since it's # not fatal #exit(74) end end if user = Puppet[:user] begin Puppet::Util::SUIDManager.change_user(user, true) rescue => detail $stderr.puts "Could not change to user #{user}: #{detail}" exit(74) end end end # Create instance methods for each of the log levels. This allows # the messages to be a little richer. Most classes will be calling this # method. def self.logmethods(klass, useself = true) Puppet::Util::Log.eachlevel { |level| klass.send(:define_method, level, proc { |args| args = args.join(" ") if args.is_a?(Array) if useself Puppet::Util::Log.create( :level => level, :source => self, :message => args ) else Puppet::Util::Log.create( :level => level, :message => args ) end }) } end # Proxy a bunch of methods to another object. def self.classproxy(klass, objmethod, *methods) classobj = class << klass; self; end methods.each do |method| classobj.send(:define_method, method) do |*args| obj = self.send(objmethod) obj.send(method, *args) end end end # Proxy a bunch of methods to another object. def self.proxy(klass, objmethod, *methods) methods.each do |method| klass.send(:define_method, method) do |*args| obj = self.send(objmethod) obj.send(method, *args) end end end # XXX this should all be done using puppet objects, not using # normal mkdir def self.recmkdir(dir,mode = 0755) if FileTest.exist?(dir) return false else tmp = dir.sub(/^\//,'') path = [File::SEPARATOR] tmp.split(File::SEPARATOR).each { |dir| path.push dir if ! FileTest.exist?(File.join(path)) Dir.mkdir(File.join(path), mode) elsif FileTest.directory?(File.join(path)) next else FileTest.exist?(File.join(path)) raise "Cannot create #{dir}: basedir #{File.join(path)} is a file" end } return true end end # Execute a given chunk of code with a new umask. def self.withumask(mask) cur = File.umask(mask) begin yield ensure File.umask(cur) end end def benchmark(*args) msg = args.pop level = args.pop object = nil if args.empty? if respond_to?(level) object = self else object = Puppet end else object = args.pop end raise Puppet::DevError, "Failed to provide level to :benchmark" unless level unless level == :none or object.respond_to? level raise Puppet::DevError, "Benchmarked object does not respond to #{level}" end # Only benchmark if our log level is high enough if level != :none and Puppet::Util::Log.sendlevel?(level) result = nil seconds = Benchmark.realtime { yield } object.send(level, msg + (" in %0.2f seconds" % seconds)) return seconds else yield end end def which(bin) if absolute_path?(bin) return bin if FileTest.file? bin and FileTest.executable? bin else ENV['PATH'].split(File::PATH_SEPARATOR).each do |dir| dest=File.join(dir, bin) return dest if FileTest.file? dest and FileTest.executable? dest end end nil end module_function :which # Determine in a platform-specific way whether a path is absolute. This # defaults to the local platform if none is specified. def absolute_path?(path, platform=nil) # Escape once for the string literal, and once for the regex. slash = '[\\\\/]' name = '[^\\\\/]+' regexes = { - :windows => %r!^([A-Z]:#{slash})|(#{slash}#{slash}#{name}#{slash}#{name})|(#{slash}#{slash}\?#{slash}#{name})!i, + :windows => %r!^(([A-Z]:#{slash})|(#{slash}#{slash}#{name}#{slash}#{name})|(#{slash}#{slash}\?#{slash}#{name}))!i, :posix => %r!^/!, } require 'puppet' platform ||= Puppet.features.microsoft_windows? ? :windows : :posix !! (path =~ regexes[platform]) end module_function :absolute_path? + # Convert a path to a file URI + def path_to_uri(path) + return unless path + + params = { :scheme => 'file' } + + if Puppet.features.microsoft_windows? + path = path.gsub(/\\/, '/') + + if unc = /^\/\/([^\/]+)(\/[^\/]+)/.match(path) + params[:host] = unc[1] + path = unc[2] + elsif path =~ /^[a-z]:\//i + path = '/' + path + end + end + + params[:path] = URI.escape(path) + + begin + URI::Generic.build(params) + rescue => detail + raise Puppet::Error, "Failed to convert '#{path}' to URI: #{detail}" + end + end + module_function :path_to_uri + + # Get the path component of a URI + def uri_to_path(uri) + return unless uri.is_a?(URI) + + path = URI.unescape(uri.path) + + if Puppet.features.microsoft_windows? and uri.scheme == 'file' + if uri.host + path = "//#{uri.host}" + path # UNC + else + path.sub!(/^\//, '') + end + end + + path + end + module_function :uri_to_path + # Execute the provided command in a pipe, yielding the pipe object. def execpipe(command, failonfail = true) if respond_to? :debug debug "Executing '#{command}'" else Puppet.debug "Executing '#{command}'" end command_str = command.respond_to?(:join) ? command.join('') : command output = open("| #{command_str} 2>&1") do |pipe| yield pipe end if failonfail unless $CHILD_STATUS == 0 raise ExecutionFailure, output end end output end def execfail(command, exception) output = execute(command) return output rescue ExecutionFailure raise exception, output end def execute_posix(command, arguments, stdin, stdout, stderr) child_pid = Kernel.fork do command = Array(command) Process.setsid begin $stdin.reopen(stdin) $stdout.reopen(stdout) $stderr.reopen(stderr) 3.upto(256){|fd| IO::new(fd).close rescue nil} Puppet::Util::SUIDManager.change_group(arguments[:gid], true) if arguments[:gid] Puppet::Util::SUIDManager.change_user(arguments[:uid], true) if arguments[:uid] ENV['LANG'] = ENV['LC_ALL'] = ENV['LC_MESSAGES'] = ENV['LANGUAGE'] = 'C' Kernel.exec(*command) rescue => detail puts detail.to_s exit!(1) end end child_pid end module_function :execute_posix def execute_windows(command, arguments, stdin, stdout, stderr) command = command.map do |part| part.include?(' ') ? %Q["#{part.gsub(/"/, '\"')}"] : part end.join(" ") if command.is_a?(Array) process_info = Process.create( :command_line => command, :startup_info => {:stdin => stdin, :stdout => stdout, :stderr => stderr} ) process_info.process_id end module_function :execute_windows # Execute the desired command, and return the status and output. # def execute(command, failonfail = true, uid = nil, gid = nil) # :combine sets whether or not to combine stdout/stderr in the output # :stdinfile sets a file that can be used for stdin. Passing a string # for stdin is not currently supported. def execute(command, arguments = {:failonfail => true, :combine => true}) if command.is_a?(Array) command = command.flatten.map(&:to_s) str = command.join(" ") elsif command.is_a?(String) str = command end if respond_to? :debug debug "Executing '#{str}'" else Puppet.debug "Executing '#{str}'" end null_file = Puppet.features.microsoft_windows? ? 'NUL' : '/dev/null' stdin = File.open(arguments[:stdinfile] || null_file, 'r') stdout = arguments[:squelch] ? File.open(null_file, 'w') : Tempfile.new('puppet') stderr = arguments[:combine] ? stdout : File.open(null_file, 'w') exec_args = [command, arguments, stdin, stdout, stderr] if execution_stub = Puppet::Util::ExecutionStub.current_value return execution_stub.call(*exec_args) elsif Puppet.features.posix? child_pid = execute_posix(*exec_args) child_status = Process.waitpid2(child_pid).last.exitstatus elsif Puppet.features.microsoft_windows? child_pid = execute_windows(*exec_args) child_status = Process.waitpid2(child_pid).last end [stdin, stdout, stderr].each {|io| io.close rescue nil} # read output in if required unless arguments[:squelch] output = wait_for_output(stdout) Puppet.warning "Could not get output" unless output end if arguments[:failonfail] and child_status != 0 raise ExecutionFailure, "Execution of '#{str}' returned #{child_status}: #{output}" end output end module_function :execute def wait_for_output(stdout) # Make sure the file's actually been written. This is basically a race # condition, and is probably a horrible way to handle it, but, well, oh # well. 2.times do |try| if File.exists?(stdout.path) output = stdout.open.read stdout.close(true) return output else time_to_sleep = try / 2.0 Puppet.warning "Waiting for output; will sleep #{time_to_sleep} seconds" sleep(time_to_sleep) end end nil end module_function :wait_for_output # Create an exclusive lock. def threadlock(resource, type = Sync::EX) Puppet::Util.synchronize_on(resource,type) { yield } end # Because some modules provide their own version of this method. alias util_execute execute module_function :benchmark def memory unless defined?(@pmap) @pmap = which('pmap') end if @pmap %x{#{@pmap} #{Process.pid}| grep total}.chomp.sub(/^\s*total\s+/, '').sub(/K$/, '').to_i else 0 end end def symbolize(value) if value.respond_to? :intern value.intern else value end end def symbolizehash(hash) newhash = {} hash.each do |name, val| if name.is_a? String newhash[name.intern] = val else newhash[name] = val end end end def symbolizehash!(hash) hash.each do |name, val| if name.is_a? String hash[name.intern] = val hash.delete(name) end end hash end module_function :symbolize, :symbolizehash, :symbolizehash! # Just benchmark, with no logging. def thinmark seconds = Benchmark.realtime { yield } seconds end module_function :memory, :thinmark def secure_open(file,must_be_w,&block) raise Puppet::DevError,"secure_open only works with mode 'w'" unless must_be_w == 'w' raise Puppet::DevError,"secure_open only requires a block" unless block_given? Puppet.warning "#{file} was a symlink to #{File.readlink(file)}" if File.symlink?(file) if File.exists?(file) or File.symlink?(file) wait = File.symlink?(file) ? 5.0 : 0.1 File.delete(file) sleep wait # give it a chance to reappear, just in case someone is actively trying something. end begin File.open(file,File::CREAT|File::EXCL|File::TRUNC|File::WRONLY,&block) rescue Errno::EEXIST desc = File.symlink?(file) ? "symlink to #{File.readlink(file)}" : File.stat(file).ftype puts "Warning: #{file} was apparently created by another process (as" puts "a #{desc}) as soon as it was deleted by this process. Someone may be trying" puts "to do something objectionable (such as tricking you into overwriting system" puts "files if you are running as root)." raise end end module_function :secure_open end end require 'puppet/util/errors' require 'puppet/util/methodhelper' require 'puppet/util/metaid' require 'puppet/util/classgen' require 'puppet/util/docs' require 'puppet/util/execution' require 'puppet/util/logging' require 'puppet/util/package' require 'puppet/util/warnings' diff --git a/lib/puppet/util/adsi.rb b/lib/puppet/util/adsi.rb index 504057903..cd0de7064 100644 --- a/lib/puppet/util/adsi.rb +++ b/lib/puppet/util/adsi.rb @@ -1,288 +1,289 @@ module Puppet::Util::ADSI class << self def connectable?(uri) begin !! connect(uri) rescue false end end def connect(uri) begin WIN32OLE.connect(uri) rescue Exception => e raise Puppet::Error.new( "ADSI connection error: #{e}" ) end end def create(name, resource_type) Puppet::Util::ADSI.connect(computer_uri).Create(resource_type, name) end def delete(name, resource_type) Puppet::Util::ADSI.connect(computer_uri).Delete(resource_type, name) end def computer_name unless @computer_name buf = " " * 128 Win32API.new('kernel32', 'GetComputerName', ['P','P'], 'I').call(buf, buf.length.to_s) @computer_name = buf.unpack("A*") end @computer_name end def computer_uri "WinNT://#{computer_name}" end def wmi_resource_uri( host = '.' ) "winmgmts:{impersonationLevel=impersonate}!//#{host}/root/cimv2" end def uri(resource_name, resource_type) "#{computer_uri}/#{resource_name},#{resource_type}" end def execquery(query) connect(wmi_resource_uri).execquery(query) end def sid_for_account(name) sid = nil - execquery( "SELECT Sid from Win32_Account WHERE Name = '#{name}' - AND LocalAccount = true").each { |u| - sid ||= u.Sid - } + + execquery( + "SELECT Sid from Win32_Account + WHERE Name = '#{name}' AND LocalAccount = true" + ).each {|u| sid ||= u.Sid} + sid end - end class User extend Enumerable attr_accessor :native_user attr_reader :name def initialize(name, native_user = nil) @name = name @native_user = native_user end def native_user @native_user ||= Puppet::Util::ADSI.connect(uri) end def self.uri(name) Puppet::Util::ADSI.uri(name, 'user') end def uri self.class.uri(name) end def self.logon(name, password) fLOGON32_LOGON_NETWORK = 3 fLOGON32_PROVIDER_DEFAULT = 0 logon_user = Win32API.new("advapi32", "LogonUser", ['P', 'P', 'P', 'L', 'L', 'P'], 'L') close_handle = Win32API.new("kernel32", "CloseHandle", ['P'], 'V') token = ' ' * 4 if logon_user.call(name, "", password, fLOGON32_LOGON_NETWORK, fLOGON32_PROVIDER_DEFAULT, token) != 0 close_handle.call(token.unpack('L')[0]) true else false end end def [](attribute) native_user.Get(attribute) end def []=(attribute, value) native_user.Put(attribute, value) end def commit begin native_user.SetInfo unless native_user.nil? rescue Exception => e raise Puppet::Error.new( "User update failed: #{e}" ) end self end def password_is?(password) self.class.logon(name, password) end def add_flag(flag_name, value) flag = native_user.Get(flag_name) rescue 0 native_user.Put(flag_name, flag | value) commit end def password=(password) native_user.SetPassword(password) commit fADS_UF_DONT_EXPIRE_PASSWD = 0x10000 add_flag("UserFlags", fADS_UF_DONT_EXPIRE_PASSWD) end def groups # WIN32OLE objects aren't enumerable, so no map groups = [] native_user.Groups.each {|g| groups << g.Name} groups end def add_to_groups(*group_names) group_names.each do |group_name| Puppet::Util::ADSI::Group.new(group_name).add_member(@name) end end alias add_to_group add_to_groups def remove_from_groups(*group_names) group_names.each do |group_name| Puppet::Util::ADSI::Group.new(group_name).remove_member(@name) end end alias remove_from_group remove_from_groups def set_groups(desired_groups, minimum = true) return if desired_groups.nil? or desired_groups.empty? desired_groups = desired_groups.split(',').map(&:strip) current_groups = self.groups # First we add the user to all the groups it should be in but isn't groups_to_add = desired_groups - current_groups add_to_groups(*groups_to_add) # Then we remove the user from all groups it is in but shouldn't be, if # that's been requested groups_to_remove = current_groups - desired_groups remove_from_groups(*groups_to_remove) unless minimum end def self.create(name) new(name, Puppet::Util::ADSI.create(name, 'user')) end def self.exists?(name) Puppet::Util::ADSI::connectable?(User.uri(name)) end def self.delete(name) Puppet::Util::ADSI.delete(name, 'user') end def self.each(&block) wql = Puppet::Util::ADSI.execquery("select * from win32_useraccount") users = [] wql.each do |u| users << new(u.name, u) end users.each(&block) end end class Group extend Enumerable attr_accessor :native_group attr_reader :name def initialize(name, native_group = nil) @name = name @native_group = native_group end def uri self.class.uri(name) end def self.uri(name) Puppet::Util::ADSI.uri(name, 'group') end def native_group @native_group ||= Puppet::Util::ADSI.connect(uri) end def commit begin native_group.SetInfo unless native_group.nil? rescue Exception => e raise Puppet::Error.new( "Group update failed: #{e}" ) end self end def add_members(*names) names.each do |name| native_group.Add(Puppet::Util::ADSI::User.uri(name)) end end alias add_member add_members def remove_members(*names) names.each do |name| native_group.Remove(Puppet::Util::ADSI::User.uri(name)) end end alias remove_member remove_members def members # WIN32OLE objects aren't enumerable, so no map members = [] native_group.Members.each {|m| members << m.Name} members end def set_members(desired_members) return if desired_members.nil? or desired_members.empty? current_members = self.members # First we add all missing members members_to_add = desired_members - current_members add_members(*members_to_add) # Then we remove all extra members members_to_remove = current_members - desired_members remove_members(*members_to_remove) end def self.create(name) new(name, Puppet::Util::ADSI.create(name, 'group')) end def self.exists?(name) Puppet::Util::ADSI.connectable?(Group.uri(name)) end def self.delete(name) Puppet::Util::ADSI.delete(name, 'group') end def self.each(&block) wql = Puppet::Util::ADSI.execquery( "select * from win32_group" ) groups = [] wql.each do |g| groups << new(g.name, g) end groups.each(&block) end end end diff --git a/lib/puppet/util/windows/security.rb b/lib/puppet/util/windows/security.rb index e136f645c..2c94f4d47 100644 --- a/lib/puppet/util/windows/security.rb +++ b/lib/puppet/util/windows/security.rb @@ -1,587 +1,593 @@ # This class maps POSIX owner, group, and modes to the Windows # security model, and back. # # The primary goal of this mapping is to ensure that owner, group, and # modes can be round-tripped in a consistent and deterministic # way. Otherwise, Puppet might think file resources are out-of-sync # every time it runs. A secondary goal is to provide equivalent # permissions for common use-cases. For example, setting the owner to # "Administrators", group to "Users", and mode to 750 (which also # denies access to everyone else. # # There are some well-known problems mapping windows and POSIX # permissions due to differences between the two security # models. Search for "POSIX permission mapping leak". In POSIX, access # to a file is determined solely based on the most specific class # (user, group, other). So a mode of 460 would deny write access to # the owner even if they are a member of the group. But in Windows, # the entire access control list is walked until the user is # explicitly denied or allowed (denied take precedence, and if neither # occurs they are denied). As a result, a user could be allowed access # based on their group membership. To solve this problem, other people # have used deny access control entries to more closely model POSIX, # but this introduces a lot of complexity. # # In general, this implementation only supports "typical" permissions, # where group permissions are a subset of user, and other permissions # are a subset of group, e.g. 754, but not 467. However, there are # some Windows quirks to be aware of. # # * The owner can be either a user or group SID, and most system files # are owned by the Administrators group. # * The group can be either a user or group SID. # * Unexpected results can occur if the owner and group are the # same, but the user and group classes are different, e.g. 750. In # this case, it is not possible to allow write access to the owner, # but not the group. As a result, the actual permissions set on the # file would be 770. # * In general, only privileged users can set the owner, group, or # change the mode for files they do not own. In 2003, the user must # be a member of the Administrators group. In Vista/2008, the user # must be running with elevated privileges. # * A file/dir can be deleted by anyone with the DELETE access right # OR by anyone that has the FILE_DELETE_CHILD access right for the # parent. See http://support.microsoft.com/kb/238018. But on Unix, # the user must have write access to the file/dir AND execute access # to all of the parent path components. # * Many access control entries are inherited from parent directories, # and it is common for file/dirs to have more than 3 entries, # e.g. Users, Power Users, Administrators, SYSTEM, etc, which cannot # be mapped into the 3 class POSIX model. The get_mode method will # set the S_IEXTRA bit flag indicating that an access control entry # was found whose SID is neither the owner, group, or other. This # enables Puppet to detect when file/dirs are out-of-sync, # especially those that Puppet did not create, but is attempting # to manage. # * On Unix, the owner and group can be modified without changing the # mode. But on Windows, an access control entry specifies which SID # it applies to. As a result, the set_owner and set_group methods # automatically rebuild the access control list based on the new # (and different) owner or group. require 'puppet/util/windows' require 'win32/security' require 'windows/file' require 'windows/handle' require 'windows/security' require 'windows/process' require 'windows/memory' module Puppet::Util::Windows::Security include Windows::File include Windows::Handle include Windows::Security include Windows::Process include Windows::Memory include Windows::MSVCRT::Buffer + extend Puppet::Util::Windows::Security + # file modes S_IRUSR = 0000400 S_IRGRP = 0000040 S_IROTH = 0000004 S_IWUSR = 0000200 S_IWGRP = 0000020 S_IWOTH = 0000002 S_IXUSR = 0000100 S_IXGRP = 0000010 S_IXOTH = 0000001 S_IRWXU = 0000700 S_IRWXG = 0000070 S_IRWXO = 0000007 S_IEXTRA = 02000000 # represents an extra ace # constants that are missing from Windows::Security PROTECTED_DACL_SECURITY_INFORMATION = 0x80000000 UNPROTECTED_DACL_SECURITY_INFORMATION = 0x20000000 NO_INHERITANCE = 0x0 # Set the owner of the object referenced by +path+ to the specified # +owner_sid+. The owner sid should be of the form "S-1-5-32-544" # and can either be a user or group. Only a user with the # SE_RESTORE_NAME privilege in their process token can overwrite the # object's owner to something other than the current user. def set_owner(owner_sid, path) old_sid = get_owner(path) change_sid(old_sid, owner_sid, OWNER_SECURITY_INFORMATION, path) end # Get the owner of the object referenced by +path+. The returned # value is a SID string, e.g. "S-1-5-32-544". Any user with read # access to an object can get the owner. Only a user with the # SE_BACKUP_NAME privilege in their process token can get the owner # for objects they do not have read access to. def get_owner(path) get_sid(OWNER_SECURITY_INFORMATION, path) end # Set the owner of the object referenced by +path+ to the specified # +group_sid+. The group sid should be of the form "S-1-5-32-544" # and can either be a user or group. Any user with WRITE_OWNER # access to the object can change the group (regardless of whether # the current user belongs to that group or not). def set_group(group_sid, path) old_sid = get_group(path) change_sid(old_sid, group_sid, GROUP_SECURITY_INFORMATION, path) end # Get the group of the object referenced by +path+. The returned # value is a SID string, e.g. "S-1-5-32-544". Any user with read # access to an object can get the group. Only a user with the # SE_BACKUP_NAME privilege in their process token can get the group # for objects they do not have read access to. def get_group(path) get_sid(GROUP_SECURITY_INFORMATION, path) end def change_sid(old_sid, new_sid, info, path) if old_sid != new_sid mode = get_mode(path) string_to_sid_ptr(new_sid) do |psid| with_privilege(SE_RESTORE_NAME) do open_file(path, WRITE_OWNER) do |handle| set_security_info(handle, info, psid) end end end # rebuild dacl now that sid has changed set_mode(mode, path) end end def get_sid(info, path) with_privilege(SE_BACKUP_NAME) do open_file(path, READ_CONTROL) do |handle| get_security_info(handle, info) end end end def get_attributes(path) attributes = GetFileAttributes(path) raise Puppet::Util::Windows::Error.new("Failed to get file attributes") if attributes == INVALID_FILE_ATTRIBUTES attributes end def add_attributes(path, flags) set_attributes(path, get_attributes(path) | flags) end def remove_attributes(path, flags) set_attributes(path, get_attributes(path) & ~flags) end def set_attributes(path, flags) raise Puppet::Util::Windows::Error.new("Failed to set file attributes") if SetFileAttributes(path, flags) == 0 end MASK_TO_MODE = { FILE_GENERIC_READ => S_IROTH, FILE_GENERIC_WRITE => S_IWOTH, (FILE_GENERIC_EXECUTE & ~FILE_READ_ATTRIBUTES) => S_IXOTH } # Get the mode of the object referenced by +path+. The returned # integer value represents the POSIX-style read, write, and execute # modes for the user, group, and other classes, e.g. 0640. Other # modes, e.g. S_ISVTX, are not supported. Any user with read access # to an object can get the mode. Only a user with the SE_BACKUP_NAME # privilege in their process token can get the mode for objects they # do not have read access to. def get_mode(path) owner_sid = get_owner(path) group_sid = get_group(path) well_known_world_sid = Win32::Security::SID::Everyone with_privilege(SE_BACKUP_NAME) do open_file(path, READ_CONTROL) do |handle| mode = 0 get_dacl(handle).each do |ace| case ace[:sid] when owner_sid MASK_TO_MODE.each_pair do |k,v| if (ace[:mask] & k) == k mode |= (v << 6) end end when group_sid MASK_TO_MODE.each_pair do |k,v| if (ace[:mask] & k) == k mode |= (v << 3) end end when well_known_world_sid MASK_TO_MODE.each_pair do |k,v| if (ace[:mask] & k) == k mode |= (v << 6) | (v << 3) | v end end else #puts "Warning, unable to map SID into POSIX mode: #{ace[:sid]}" mode |= S_IEXTRA end # if owner and group the same, then user and group modes are the OR of both if owner_sid == group_sid mode |= ((mode & S_IRWXG) << 3) | ((mode & S_IRWXU) >> 3) #puts "owner: #{group_sid}, 0x#{ace[:mask].to_s(16)}, #{mode.to_s(8)}" end end #puts "get_mode: #{mode.to_s(8)}" mode end end end MODE_TO_MASK = { S_IROTH => FILE_GENERIC_READ, S_IWOTH => FILE_GENERIC_WRITE, S_IXOTH => (FILE_GENERIC_EXECUTE & ~FILE_READ_ATTRIBUTES), - (S_IWOTH | S_IXUSR) => FILE_DELETE_CHILD, + (S_IWOTH | S_IXOTH) => FILE_DELETE_CHILD, } # Set the mode of the object referenced by +path+ to the specified # +mode+. The mode should be specified as POSIX-stye read, write, # and execute modes for the user, group, and other classes, # e.g. 0640. Other modes, e.g. S_ISVTX, are not supported. By # default, the DACL is set to protected, meaning it does not inherit # access control entries from parent objects. This can be changed by # setting +protected+ to false. The owner of the object (with # READ_CONTROL and WRITE_DACL access) can always change the # mode. Only a user with the SE_BACKUP_NAME and SE_RESTORE_NAME # privileges in their process token can change the mode for objects # that they do not have read and write access to. def set_mode(mode, path, protected = true) owner_sid = get_owner(path) group_sid = get_group(path) well_known_world_sid = Win32::Security::SID::Everyone owner_allow = STANDARD_RIGHTS_ALL | FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES group_allow = STANDARD_RIGHTS_READ | FILE_READ_ATTRIBUTES | SYNCHRONIZE other_allow = STANDARD_RIGHTS_READ | FILE_READ_ATTRIBUTES | SYNCHRONIZE MODE_TO_MASK.each do |k,v| if ((mode >> 6) & k) == k owner_allow |= v end if ((mode >> 3) & k) == k group_allow |= v end if (mode & k) == k other_allow |= v end end # if owner and group the same, then map group permissions to the one owner ACE isownergroup = owner_sid == group_sid if isownergroup owner_allow |= group_allow end set_acl(path, protected) do |acl| #puts "ace: owner #{owner_sid}, mask 0x#{owner_allow.to_s(16)}" add_access_allowed_ace(acl, owner_allow, owner_sid) unless isownergroup #puts "ace: group #{group_sid}, mask 0x#{group_allow.to_s(16)}" add_access_allowed_ace(acl, group_allow, group_sid) end #puts "ace: other #{well_known_world_sid}, mask 0x#{other_allow.to_s(16)}" add_access_allowed_ace(acl, other_allow, well_known_world_sid) # add inheritable aces for child dirs and files that are created within the dir if File.directory?(path) inherit = INHERIT_ONLY_ACE | OBJECT_INHERIT_ACE | CONTAINER_INHERIT_ACE add_access_allowed_ace(acl, owner_allow, Win32::Security::SID::CreatorOwner, inherit) add_access_allowed_ace(acl, group_allow, Win32::Security::SID::CreatorGroup, inherit) add_access_allowed_ace(acl, other_allow, well_known_world_sid, inherit) end end # if any ACE allows write, then clear readonly bit if ((owner_allow | group_allow | other_allow ) & FILE_WRITE_DATA) == FILE_WRITE_DATA remove_attributes(path, FILE_ATTRIBUTE_READONLY) end nil end # setting DACL requires both READ_CONTROL and WRITE_DACL access rights, # and their respective privileges, SE_BACKUP_NAME and SE_RESTORE_NAME. def set_acl(path, protected = true) with_privilege(SE_BACKUP_NAME) do with_privilege(SE_RESTORE_NAME) do open_file(path, READ_CONTROL | WRITE_DAC) do |handle| acl = 0.chr * 1024 # This can be increased later as needed unless InitializeAcl(acl, acl.size, ACL_REVISION) raise Puppet::Util::Windows::Error.new("Failed to initialize ACL") end raise Puppet::Util::Windows::Error.new("Invalid DACL") if IsValidAcl(acl) == 0 yield acl # protected means the object does not inherit aces from its parent info = DACL_SECURITY_INFORMATION info |= protected ? PROTECTED_DACL_SECURITY_INFORMATION : UNPROTECTED_DACL_SECURITY_INFORMATION # set the DACL set_security_info(handle, info, acl) end end end end def add_access_allowed_ace(acl, mask, sid, inherit = NO_INHERITANCE) string_to_sid_ptr(sid) do |sid_ptr| raise Puppet::Util::Windows::Error.new("Invalid SID") if IsValidSid(sid_ptr) == 0 if AddAccessAllowedAceEx(acl, ACL_REVISION, inherit, mask, sid_ptr) == 0 raise Puppet::Util::Windows::Error.new("Failed to add access control entry") end end end def add_access_denied_ace(acl, mask, sid) string_to_sid_ptr(sid) do |sid_ptr| raise Puppet::Util::Windows::Error.new("Invalid SID") if IsValidSid(sid_ptr) == 0 if AddAccessDeniedAce(acl, ACL_REVISION, mask, sid_ptr) == 0 raise Puppet::Util::Windows::Error.new("Failed to add access control entry") end end end def get_dacl(handle) get_dacl_ptr(handle) do |dacl_ptr| # REMIND: need to handle NULL DACL raise Puppet::Util::Windows::Error.new("Invalid DACL") if IsValidAcl(dacl_ptr) == 0 # ACL structure, size and count are the important parts. The # size includes both the ACL structure and all the ACEs. # # BYTE AclRevision # BYTE Padding1 # WORD AclSize # WORD AceCount # WORD Padding2 acl_buf = 0.chr * 8 memcpy(acl_buf, dacl_ptr, acl_buf.size) ace_count = acl_buf.unpack('CCSSS')[3] dacl = [] # deny all return dacl if ace_count == 0 0.upto(ace_count - 1) do |i| ace_ptr = [0].pack('L') next if GetAce(dacl_ptr, i, ace_ptr) == 0 # ACE structures vary depending on the type. All structures # begin with an ACE header, which specifies the type, flags # and size of what follows. We are only concerned with # ACCESS_ALLOWED_ACE and ACCESS_DENIED_ACEs, which have the # same structure: # # BYTE C AceType # BYTE C AceFlags # WORD S AceSize # DWORD L ACCESS_MASK # DWORD L Sid # .. ... # DWORD L Sid ace_buf = 0.chr * 8 memcpy(ace_buf, ace_ptr.unpack('L')[0], ace_buf.size) ace_type, ace_flags, size, mask = ace_buf.unpack('CCSL') # skip aces that only serve to propagate inheritance next if (ace_flags & INHERIT_ONLY_ACE).nonzero? case ace_type when ACCESS_ALLOWED_ACE_TYPE sid_ptr = ace_ptr.unpack('L')[0] + 8 # address of ace_ptr->SidStart raise Puppet::Util::Windows::Error.new("Failed to read DACL, invalid SID") unless IsValidSid(sid_ptr) sid = sid_ptr_to_string(sid_ptr) dacl << {:sid => sid, :type => ace_type, :mask => mask} else Puppet.warning "Unsupported access control entry type: 0x#{ace_type.to_s(16)}" end end dacl end end def get_dacl_ptr(handle) dacl = [0].pack('L') sd = [0].pack('L') rv = GetSecurityInfo( handle, SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, nil, nil, dacl, #dacl nil, #sacl sd) #sec desc raise Puppet::Util::Windows::Error.new("Failed to get DACL") unless rv == ERROR_SUCCESS begin yield dacl.unpack('L')[0] ensure LocalFree(sd.unpack('L')[0]) end end # Set the security info on the specified handle. def set_security_info(handle, info, ptr) rv = SetSecurityInfo( handle, SE_FILE_OBJECT, info, (info & OWNER_SECURITY_INFORMATION) == OWNER_SECURITY_INFORMATION ? ptr : nil, (info & GROUP_SECURITY_INFORMATION) == GROUP_SECURITY_INFORMATION ? ptr : nil, (info & DACL_SECURITY_INFORMATION) == DACL_SECURITY_INFORMATION ? ptr : nil, nil) raise Puppet::Util::Windows::Error.new("Failed to set security information") unless rv == ERROR_SUCCESS end # Get the SID string, e.g. "S-1-5-32-544", for the specified handle # and type of information (owner, group). def get_security_info(handle, info) sid = [0].pack('L') sd = [0].pack('L') rv = GetSecurityInfo( handle, SE_FILE_OBJECT, info, # security info info == OWNER_SECURITY_INFORMATION ? sid : nil, info == GROUP_SECURITY_INFORMATION ? sid : nil, nil, #dacl nil, #sacl sd) #sec desc raise Puppet::Util::Windows::Error.new("Failed to get security information") unless rv == ERROR_SUCCESS begin return sid_ptr_to_string(sid.unpack('L')[0]) ensure LocalFree(sd.unpack('L')[0]) end end # Convert a SID pointer to a string, e.g. "S-1-5-32-544". def sid_ptr_to_string(psid) sid_buf = 0.chr * 256 str_ptr = 0.chr * 4 raise Puppet::Util::Windows::Error.new("Invalid SID") if IsValidSid(psid) == 0 raise Puppet::Util::Windows::Error.new("Failed to convert binary SID") if ConvertSidToStringSid(psid, str_ptr) == 0 begin strncpy(sid_buf, str_ptr.unpack('L')[0], sid_buf.size - 1) sid_buf[sid_buf.size - 1] = 0.chr return sid_buf.strip ensure LocalFree(str_ptr.unpack('L')[0]) end end # Convert a SID string, e.g. "S-1-5-32-544" to a pointer (containing the # address of the binary SID structure). The returned value can be used in # Win32 APIs that expect a PSID, e.g. IsValidSid. def string_to_sid_ptr(string) sid_buf = 0.chr * 80 string_addr = [string].pack('p*').unpack('L')[0] raise Puppet::Util::Windows::Error.new("Failed to convert string SID: #{string}") unless ConvertStringSidToSid(string_addr, sid_buf) sid_ptr = sid_buf.unpack('L')[0] begin - yield sid_ptr + if block_given? + yield sid_ptr + else + true + end ensure LocalFree(sid_ptr) end end # Open an existing file with the specified access mode, and execute a # block with the opened file HANDLE. def open_file(path, access) handle = CreateFile( path, access, FILE_SHARE_READ | FILE_SHARE_WRITE, 0, # security_attributes OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, 0) # template raise Puppet::Util::Windows::Error.new("Failed to open '#{path}'") if handle == INVALID_HANDLE_VALUE begin yield handle ensure CloseHandle(handle) end end # Execute a block with the specified privilege enabled def with_privilege(privilege) set_privilege(privilege, true) yield ensure set_privilege(privilege, false) end # Enable or disable a privilege. Note this doesn't add any privileges the # user doesn't already has, it just enables privileges that are disabled. def set_privilege(privilege, enable) return unless Puppet.features.root? with_process_token(TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY) do |token| tmpLuid = 0.chr * 8 # Get the LUID for specified privilege. if LookupPrivilegeValue("", privilege, tmpLuid) == 0 raise Puppet::Util::Windows::Error.new("Failed to lookup privilege") end # DWORD + [LUID + DWORD] tkp = [1].pack('L') + tmpLuid + [enable ? SE_PRIVILEGE_ENABLED : 0].pack('L') if AdjustTokenPrivileges(token, 0, tkp, tkp.length , nil, nil) == 0 raise Puppet::Util::Windows::Error.new("Failed to adjust process privileges") end end end # Execute a block with the current process token def with_process_token(access) token = 0.chr * 4 if OpenProcessToken(GetCurrentProcess(), access, token) == 0 raise Puppet::Util::Windows::Error.new("Failed to open process token") end begin token = token.unpack('L')[0] yield token ensure CloseHandle(token) end end end diff --git a/spec/integration/type/file_spec.rb b/spec/integration/type/file_spec.rb index 9814c4539..cb04a652b 100755 --- a/spec/integration/type/file_spec.rb +++ b/spec/integration/type/file_spec.rb @@ -1,511 +1,695 @@ #!/usr/bin/env rspec 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) { tmpfile('file_testing') } + + if Puppet.features.posix? + def get_mode(file) + File.lstat(file).mode + end + else + class SecurityHelper + extend Puppet::Util::Windows::Security + end + + def get_mode(file) + SecurityHelper.get_mode(file) + 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 - file = Puppet::Type.type(:file).new :path => make_absolute("/my/file"), :mode => "755" - catalog = Puppet::Resource::Catalog.new + file = described_class.new :path => path, :mode => 0755 catalog.add_resource file file.parameter(:mode).expects(:retrieve).never - transaction = Puppet::Transaction.new(catalog) - transaction.resource_harness.evaluate(file).should_not be_failed + report = catalog.apply.report + report.resource_statuses["File[#{path}]"].should_not be_failed + File.should_not be_exist(path) end describe "when writing files" do - it "should backup files to a filebucket when one is configured", :fails_on_windows => true do - bucket = Puppet::Type.type(:filebucket).new :path => tmpfile("filebucket"), :name => "mybucket" - file = Puppet::Type.type(:file).new :path => tmpfile("bucket_backs"), :backup => "mybucket", :content => "foo" - catalog = Puppet::Resource::Catalog.new + 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 bucket + catalog.add_resource filebucket File.open(file[:path], "w") { |f| f.puts "bar" } md5 = Digest::MD5.hexdigest(File.read(file[:path])) catalog.apply - bucket.bucket.getfile(md5).should == "bar\n" + filebucket.bucket.getfile(md5).should == "bar\n" end it "should backup files in the local directory when a backup string is provided" do - file = Puppet::Type.type(:file).new :path => tmpfile("bucket_backs"), :backup => ".bak", :content => "foo" - catalog = Puppet::Resource::Catalog.new + 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" FileTest.should be_exist(backup) File.read(backup).should == "bar\n" end it "should fail if no backup can be performed" do - dir = tmpfile("backups") - Dir.mkdir(dir) - path = File.join(dir, "testfile") - file = Puppet::Type.type(:file).new :path => path, :backup => ".bak", :content => "foo" - catalog = Puppet::Resource::Catalog.new + file = described_class.new :path => path, :backup => ".bak", :content => "foo" catalog.add_resource file - File.open(file[:path], "w") { |f| f.puts "bar" } + File.open(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) + Dir.mkdir("#{path}.bak") catalog.apply - File.read(file[:path]).should == "bar\n" + File.read(path).should == "bar\n" end it "should not backup symlinks", :unless => Puppet.features.microsoft_windows? do link = tmpfile("link") dest1 = tmpfile("dest1") dest2 = tmpfile("dest2") bucket = Puppet::Type.type(:filebucket).new :path => tmpfile("filebucket"), :name => "mybucket" - file = Puppet::Type.type(:file).new :path => link, :target => dest2, :ensure => :link, :backup => "mybucket" - catalog = Puppet::Resource::Catalog.new + 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" } File.symlink(dest1, link) md5 = Digest::MD5.hexdigest(File.read(file[:path])) catalog.apply File.readlink(link).should == dest2 Find.find(bucket[:path]) { |f| File.file?(f) }.should be_nil end it "should backup directories to the local filesystem by copying the whole directory" do - file = Puppet::Type.type(:file).new :path => tmpfile("bucket_backs"), :backup => ".bak", :content => "foo", :force => true - catalog = Puppet::Resource::Catalog.new + file = described_class.new :path => path, :backup => ".bak", :content => "foo", :force => true catalog.add_resource file - Dir.mkdir(file[:path]) - otherfile = File.join(file[:path], "foo") + Dir.mkdir(path) + + otherfile = File.join(path, "foo") File.open(otherfile, "w") { |f| f.print "yay" } catalog.apply - backup = file[:path] + ".bak" + 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", :fails_on_windows => true do + 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 = Puppet::Type.type(:file).new :path => tmpfile("bucket_backs"), :backup => "mybucket", :content => "foo", :force => true - catalog = Puppet::Resource::Catalog.new + 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 it "should propagate failures encountered when renaming the temporary file" do - file = Puppet::Type.type(:file).new :path => tmpfile("fail_rename"), :content => "foo" - file.stubs(:remove_existing) # because it tries to make a backup + file = described_class.new :path => path, :content => "foo" + file.stubs(:perform_backup).returns(true) - catalog = Puppet::Resource::Catalog.new catalog.add_resource file - File.open(file[:path], "w") { |f| f.print "bar" } + File.open(path, "w") { |f| f.print "bar" } File.expects(:rename).raises ArgumentError - lambda { file.write(:content) }.should raise_error(Puppet::Error) - File.read(file[:path]).should == "bar" + expect { file.write(:content) }.to raise_error(Puppet::Error, /Could not rename temporary file/) + File.read(path).should == "bar" 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", :fails_on_windows => true do - @path = tmpfile("file_integration_tests") - - @file = Puppet::Type::File.new( - :name => @path, + it "should be able to recurse over a nonexistent file" do + @file = described_class.new( + :name => path, :mode => 0644, :recurse => true, :backup => false ) - @catalog = Puppet::Resource::Catalog.new - @catalog.add_resource @file + 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") + path = tmpfile("file_integration_tests") - build_path(@path) + build_path(path) - @file = Puppet::Type::File.new( - :name => @path, + file = described_class.new( + :name => path, :mode => 0644, :recurse => true, :backup => false ) - @catalog = Puppet::Resource::Catalog.new - @catalog.add_resource @file + catalog.add_resource file - @catalog.apply + catalog.apply + @dirs.should_not be_empty @dirs.each do |path| - (File.stat(path).mode & 007777).should == 0755 + (get_mode(path) & 007777).should == 0755 end + @files.should_not be_empty @files.each do |path| - (File.stat(path).mode & 007777).should == 0644 + (get_mode(path) & 007777).should == 0644 end end it "should be able to recursively make links to other files", :unless => Puppet.features.microsoft_windows? do source = tmpfile("file_link_integration_source") build_path(source) dest = tmpfile("file_link_integration_dest") - @file = Puppet::Type::File.new(:name => dest, :target => source, :recurse => true, :ensure => :link, :backup => false) + @file = described_class.new(:name => dest, :target => source, :recurse => true, :ensure => :link, :backup => false) - @catalog = Puppet::Resource::Catalog.new - @catalog.add_resource @file + catalog.add_resource @file - @catalog.apply + catalog.apply @dirs.each do |path| link_path = path.sub(source, dest) File.lstat(link_path).should be_directory end @files.each do |path| link_path = path.sub(source, dest) File.lstat(link_path).ftype.should == "link" end end - it "should be able to recursively copy files", :fails_on_windows => true do + 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 = Puppet::Type::File.new(:name => dest, :source => source, :recurse => true, :backup => false) + @file = described_class.new(:name => dest, :source => source, :recurse => true, :backup => false) - @catalog = Puppet::Resource::Catalog.new - @catalog.add_resource @file + catalog.add_resource @file - @catalog.apply + catalog.apply @dirs.each do |path| newpath = path.sub(source, dest) File.lstat(newpath).should be_directory end @files.each do |path| newpath = path.sub(source, dest) File.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 = Puppet::Type::File.new(:name => dir, :recurse => true, :backup => false, :mode => "755") - sub = Puppet::Type::File.new(:name => subdir, :recurse => true, :backup => false, :mode => "644") + 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 = Puppet::Resource::Catalog.new - @catalog.add_resource base - @catalog.add_resource sub + catalog.add_resource base + catalog.add_resource sub - @catalog.apply + catalog.apply - (File.stat(file).mode & 007777).should == 0644 + (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 - dir = tmpfile("recursion_vs_explicit_2") + managed = File.join(path, "file") + generated = File.join(path, "file_with_a_name_starting_with_the_word_file") + managed_mode = 0700 - managed = File.join(dir, "file") - generated = File.join(dir, "file_with_a_name_starting_with_the_word_file") - managed_mode = Puppet.features.microsoft_windows? ? 0444 : 0700 + FileUtils.mkdir_p(path) + FileUtils.touch(managed) + FileUtils.touch(generated) - FileUtils.mkdir_p(dir) - File.open(managed, "w") { |f| f.puts "" } - File.open(generated, "w") { |f| f.puts "" } + 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 = Puppet::Resource::Catalog.new - @catalog.add_resource Puppet::Type::File.new(:name => dir, :recurse => true, :backup => false, :mode => managed_mode) - @catalog.add_resource Puppet::Type::File.new(:name => managed, :recurse => true, :backup => false, :mode => "644") + catalog.apply - @catalog.apply + (get_mode(generated) & 007777).should == managed_mode + end - (File.stat(generated).mode & 007777).should == managed_mode + 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')) + + obj = Puppet::Type.newfile( + :path => path, + :ensure => :directory, + :backup => false, + :recurse => true, + :sourceselect => :first, + :source => [one, two] + ) + + catalog.add_resource obj + catalog.apply + + File.should be_directory(path) + File.should_not be_exist(File.join(path, 'one')) + File.should be_exist(File.join(path, 'three', 'four')) + end + + it "should recursively copy an empty directory" do + one = File.expand_path('thisdoesnotexist') + two = tmpdir('two') + three = tmpdir('three') + + FileUtils.mkdir_p(two) + FileUtils.mkdir_p(three) + FileUtils.touch(File.join(three, 'a')) + + obj = Puppet::Type.newfile( + :path => path, + :ensure => :directory, + :backup => false, + :recurse => true, + :sourceselect => :first, + :source => [one, two, three] + ) + + catalog.add_resource obj + catalog.apply + + File.should be_directory(path) + File.should_not be_exist(File.join(path, 'a')) + 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')) + + obj = Puppet::Type.newfile( + :path => path, + :ensure => :directory, + :backup => false, + :recurse => true, + :recurselimit => 1, + :sourceselect => :first, + :source => [one, two] + ) + + catalog.add_resource obj + catalog.apply + + File.should be_exist(File.join(path, 'a')) + File.should_not be_exist(File.join(path, 'a', 'b')) + File.should_not be_exist(File.join(path, 'z')) + end + end + + describe "for a file" do + it "should copy the first file that exists" do + one = File.expand_path('thisdoesnotexist') + two = tmpfile('two') + File.open(two, "w") { |f| f.print 'yay' } + three = tmpfile('three') + File.open(three, "w") { |f| f.print 'no' } + + obj = Puppet::Type.newfile( + :path => path, + :ensure => :file, + :backup => false, + :sourceselect => :first, + :source => [one, two, three] + ) + + catalog.add_resource obj + catalog.apply + + File.read(path).should == 'yay' + end + + it "should copy an empty file" do + one = File.expand_path('thisdoesnotexist') + two = tmpfile('two') + FileUtils.touch(two) + three = tmpfile('three') + File.open(three, "w") { |f| f.print 'no' } + + obj = Puppet::Type.newfile( + :path => path, + :ensure => :file, + :backup => false, + :sourceselect => :first, + :source => [one, two, three] + ) + + catalog.add_resource obj + 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 + one = tmpdir('one') + two = tmpdir('two') + three = tmpdir('three') + four = tmpdir('four') + + [one, two, three, four].each {|dir| FileUtils.mkdir_p(dir)} + + File.open(File.join(one, 'a'), "w") { |f| f.print one } + File.open(File.join(two, 'a'), "w") { |f| f.print two } + File.open(File.join(two, 'b'), "w") { |f| f.print two } + File.open(File.join(three, 'a'), "w") { |f| f.print three } + File.open(File.join(three, 'c'), "w") { |f| f.print three } + + obj = Puppet::Type.newfile( + :path => path, + :ensure => :directory, + :backup => false, + :recurse => true, + :sourceselect => :all, + :source => [one, two, three, four] + ) + + catalog.add_resource obj + catalog.apply + + File.read(File.join(path, 'a')).should == one + File.read(File.join(path, 'b')).should == two + File.read(File.join(path, '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 + + File.should be_exist(File.join(path, 'a')) + File.should_not be_exist(File.join(path, 'a', 'b')) + File.should be_exist(File.join(path, 'z')) + File.should_not be_exist(File.join(path, 'z', 'y')) + end + end + end end end - describe "when generating resources", :fails_on_windows => true do + describe "when generating resources" do before do - @source = tmpfile("generating_in_catalog_source") - - @dest = tmpfile("generating_in_catalog_dest") + source = tmpfile("generating_in_catalog_source") - Dir.mkdir(@source) + Dir.mkdir(source) - s1 = File.join(@source, "one") - s2 = File.join(@source, "two") + s1 = File.join(source, "one") + s2 = File.join(source, "two") File.open(s1, "w") { |f| f.puts "uno" } File.open(s2, "w") { |f| f.puts "dos" } - @file = Puppet::Type::File.new(:name => @dest, :source => @source, :recurse => true, :backup => false) + @file = described_class.new( + :name => path, + :source => source, + :recurse => true, + :backup => false + ) - @catalog = Puppet::Resource::Catalog.new - @catalog.add_resource @file + catalog.add_resource @file end it "should add each generated resource to the catalog" do - @catalog.apply do |trans| - @catalog.resource(:file, File.join(@dest, "one")).should be_instance_of(@file.class) - @catalog.resource(:file, File.join(@dest, "two")).should be_instance_of(@file.class) + catalog.apply do |trans| + catalog.resource(:file, File.join(path, "one")).should be_a(described_class) + catalog.resource(:file, File.join(path, "two")).should 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(@dest, "one")) - @catalog.relationship_graph.edge?(@file, one).should be + 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(@dest, "two")) - @catalog.relationship_graph.edge?(@file, two).should be + two = catalog.resource(:file, File.join(path, "two")) + catalog.relationship_graph.should be_edge(@file, two) end end end describe "when copying files" do # Ticket #285. it "should be able to copy files with pound signs in their names" do source = tmpfile("filewith#signs") dest = tmpfile("destwith#signs") File.open(source, "w") { |f| f.print "foo" } - file = Puppet::Type::File.new(:name => dest, :source => source) + file = described_class.new(:name => dest, :source => source) - catalog = Puppet::Resource::Catalog.new catalog.add_resource file catalog.apply File.read(dest).should == "foo" end it "should be able to copy files with spaces in their names" do source = tmpfile("filewith spaces") dest = tmpfile("destwith spaces") File.open(source, "w") { |f| f.print "foo" } File.chmod(0755, source) - file = Puppet::Type::File.new(:path => dest, :source => source) + file = described_class.new(:path => dest, :source => source) - catalog = Puppet::Resource::Catalog.new catalog.add_resource file catalog.apply expected_mode = Puppet.features.microsoft_windows? ? 0644 : 0755 File.read(dest).should == "foo" (File.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("source") dest = tmpfile("dest") File.open(source, "w") { |f| f.print "foo" } - file = Puppet::Type::File.new(:name => dest, :source => source, :recurse => true) + file = described_class.new(:name => dest, :source => source, :recurse => true) - catalog = Puppet::Resource::Catalog.new catalog.add_resource file catalog.apply File.read(dest).should == "foo" end end - it "should be able to create files when 'content' is specified but 'ensure' is not" do - dest = tmpfile("files_with_content") - - - file = Puppet::Type.type(:file).new( - :name => dest, + it "should create a file with content if ensure is omitted" do + file = described_class.new( + :path => path, :content => "this is some content, yo" ) - catalog = Puppet::Resource::Catalog.new catalog.add_resource file catalog.apply - File.read(dest).should == "this is some content, yo" + File.read(path).should == "this is some content, yo" end - it "should create files with content if both 'content' and 'ensure' are set" do - dest = tmpfile("files_with_content") - - - file = Puppet::Type.type(:file).new( - :name => dest, + 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 = Puppet::Resource::Catalog.new catalog.add_resource file catalog.apply - File.read(dest).should == "this is some content, yo" + File.read(path).should == "this is some content, yo" end it "should delete files with sources but that are set for deletion" do - dest = tmpfile("dest_source_with_ensure") source = tmpfile("source_source_with_ensure") + File.open(source, "w") { |f| f.puts "yay" } - File.open(dest, "w") { |f| f.puts "boo" } + File.open(path, "w") { |f| f.puts "boo" } - file = Puppet::Type.type(:file).new( - :name => make_absolute(dest), + file = described_class.new( + :path => path, :ensure => :absent, :source => source, :backup => false ) - catalog = Puppet::Resource::Catalog.new catalog.add_resource file catalog.apply - File.should_not be_exist(dest) + File.should_not be_exist(path) end describe "when purging files" do before do - @sourcedir = tmpfile("purge_source") - @destdir = tmpfile("purge_dest") - Dir.mkdir(@sourcedir) - Dir.mkdir(@destdir) - @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.puts "rahtest" } - File.open(@sourcefile, "w") { |f| f.puts "funtest" } + sourcedir = tmpfile("purge_source") + destdir = tmpfile("purge_dest") + Dir.mkdir(sourcedir) + Dir.mkdir(destdir) + 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.puts "footest" } + File.open(@purgee, "w") { |f| f.print "footest" } - @lfobj = Puppet::Type.newfile( + lfobj = Puppet::Type.newfile( :title => "localfile", :path => @localfile, - :content => "rahtest\n", + :content => "rahtest", :ensure => :file, :backup => false ) - @destobj = Puppet::Type.newfile( + destobj = Puppet::Type.newfile( :title => "destdir", - :path => @destdir, - :source => @sourcedir, + :path => destdir, + :source => sourcedir, :backup => false, :purge => true, :recurse => true ) - @catalog = Puppet::Resource::Catalog.new - @catalog.add_resource @lfobj, @destobj + catalog.add_resource lfobj, destobj + catalog.apply end it "should still copy remote files" do - @catalog.apply - FileTest.should be_exist(@copiedfile) + File.read(@copiedfile).should == 'funtest' end it "should not purge managed, local files" do - @catalog.apply - FileTest.should be_exist(@localfile) + File.read(@localfile).should == 'rahtest' end it "should purge files that are neither remote nor otherwise managed" do - @catalog.apply FileTest.should_not be_exist(@purgee) end end end diff --git a/spec/integration/util/windows/security_spec.rb b/spec/integration/util/windows/security_spec.rb index fcc14778f..ba4adc2a1 100755 --- a/spec/integration/util/windows/security_spec.rb +++ b/spec/integration/util/windows/security_spec.rb @@ -1,457 +1,468 @@ #!/usr/bin/env ruby require 'spec_helper' require 'puppet/util/adsi' if Puppet.features.microsoft_windows? class WindowsSecurityTester require 'puppet/util/windows/security' include Puppet::Util::Windows::Security end end describe "Puppet::Util::Windows::Security", :if => Puppet.features.microsoft_windows? do include PuppetSpec::Files before :all do - sid = nil - - wql = Puppet::Util::ADSI.execquery("select Sid from win32_account where name='#{Sys::Admin.get_login}'") - wql.each do |u| - sid = u.Sid - break - end - @sids = { - :current_user => sid, - :admin => Sys::Admin.get_user("Administrator").sid, - :guest => Sys::Admin.get_user("Guest").sid, - + :current_user => Puppet::Util::ADSI.sid_for_account(Sys::Admin.get_login), + :admin => Puppet::Util::ADSI.sid_for_account("Administrator"), + :guest => Puppet::Util::ADSI.sid_for_account("Guest"), :users => Win32::Security::SID::BuiltinUsers, :power_users => Win32::Security::SID::PowerUsers, } end let (:sids) { @sids } let (:winsec) { WindowsSecurityTester.new } shared_examples_for "a securable object" do describe "for a normal user" do before :each do Puppet.features.stubs(:root?).returns(false) end after :each do winsec.set_mode(WindowsSecurityTester::S_IRWXU, path) end - describe "when setting the owner sid" do + describe "#owner=" do it "should allow setting to the current user" do winsec.set_owner(sids[:current_user], path) end it "should raise an exception when setting to a different user" do lambda { winsec.set_owner(sids[:guest], path) }.should raise_error(Puppet::Error, /This security ID may not be assigned as the owner of this object./) end end - describe "when getting the owner sid" do + describe "#owner" do it "it should not be empty" do winsec.get_owner(path).should_not be_empty end it "should raise an exception if an invalid path is provided" do lambda { winsec.get_owner("c:\\doesnotexist.txt") }.should raise_error(Puppet::Error, /The system cannot find the file specified./) end end - describe "when setting the group sid" do + describe "#group=" do it "should allow setting to a group the current owner is a member of" do winsec.set_group(sids[:users], path) end # Unlike unix, if the user has permission to WRITE_OWNER, which the file owner has by default, # then they can set the primary group to a group that the user does not belong to. it "should allow setting to a group the current owner is not a member of" do winsec.set_group(sids[:power_users], path) end end - describe "when getting the group sid" do + describe "#group" do it "should not be empty" do winsec.get_group(path).should_not be_empty end it "should raise an exception if an invalid path is provided" do lambda { winsec.get_group("c:\\doesnotexist.txt") }.should raise_error(Puppet::Error, /The system cannot find the file specified./) end end - describe "setting the mode" do + describe "#mode=" do [0000, 0100, 0200, 0300, 0400, 0500, 0600, 0700].each do |mode| it "should enforce mode #{mode.to_s(8)}" do winsec.set_mode(mode, path) check_access(mode, path) end end it "should round-trip all 64 modes that do not require deny ACEs" do 0.upto(7).each do |u| 0.upto(u).each do |g| 0.upto(g).each do |o| # if user is superset of group, and group superset of other, then # no deny ace is required, and mode can be converted to win32 # access mask, and back to mode without loss of information # (provided the owner and group are not the same) next if ((u & g) != g) or ((g & o) != o) mode = (u << 6 | g << 3 | o << 0) winsec.set_mode(mode, path) winsec.get_mode(path).to_s(8).should == mode.to_s(8) end end end end describe "for modes that require deny aces" do it "should map everyone to group and owner" do winsec.set_mode(0426, path) winsec.get_mode(path).to_s(8).should == "666" end it "should combine user and group modes when owner and group sids are equal" do winsec.set_group(winsec.get_owner(path), path) winsec.set_mode(0410, path) winsec.get_mode(path).to_s(8).should == "550" end end describe "for read-only objects" do before :each do winsec.add_attributes(path, WindowsSecurityTester::FILE_ATTRIBUTE_READONLY) (winsec.get_attributes(path) & WindowsSecurityTester::FILE_ATTRIBUTE_READONLY).should be_nonzero end it "should make them writable if any sid has write permission" do winsec.set_mode(WindowsSecurityTester::S_IWUSR, path) (winsec.get_attributes(path) & WindowsSecurityTester::FILE_ATTRIBUTE_READONLY).should == 0 end it "should leave them read-only if no sid has write permission" do winsec.set_mode(WindowsSecurityTester::S_IRUSR | WindowsSecurityTester::S_IXGRP, path) (winsec.get_attributes(path) & WindowsSecurityTester::FILE_ATTRIBUTE_READONLY).should be_nonzero end end it "should raise an exception if an invalid path is provided" do lambda { winsec.set_mode(sids[:guest], "c:\\doesnotexist.txt") }.should raise_error(Puppet::Error, /The system cannot find the file specified./) end end - describe "getting the mode" do + describe "#mode" do it "should report when extra aces are encounted" do winsec.set_acl(path, true) do |acl| [ 544, 545, 546, 547 ].each do |rid| winsec.add_access_allowed_ace(acl, WindowsSecurityTester::STANDARD_RIGHTS_ALL, "S-1-5-32-#{rid}") end end mode = winsec.get_mode(path) (mode & WindowsSecurityTester::S_IEXTRA).should_not == 0 end it "should warn if a deny ace is encountered" do winsec.set_acl(path) do |acl| winsec.add_access_denied_ace(acl, WindowsSecurityTester::FILE_GENERIC_WRITE, sids[:guest]) winsec.add_access_allowed_ace(acl, WindowsSecurityTester::STANDARD_RIGHTS_ALL | WindowsSecurityTester::SPECIFIC_RIGHTS_ALL, sids[:current_user]) end Puppet.expects(:warning).with("Unsupported access control entry type: 0x1") winsec.get_mode(path) end it "should skip inherit-only ace" do winsec.set_acl(path) do |acl| winsec.add_access_allowed_ace(acl, WindowsSecurityTester::STANDARD_RIGHTS_ALL | WindowsSecurityTester::SPECIFIC_RIGHTS_ALL, sids[:current_user]) winsec.add_access_allowed_ace(acl, WindowsSecurityTester::FILE_GENERIC_READ, Win32::Security::SID::Everyone, WindowsSecurityTester::INHERIT_ONLY_ACE | WindowsSecurityTester::OBJECT_INHERIT_ACE) end (winsec.get_mode(path) & WindowsSecurityTester::S_IRWXO).should == 0 end it "should raise an exception if an invalid path is provided" do lambda { winsec.get_mode("c:\\doesnotexist.txt") }.should raise_error(Puppet::Error, /The system cannot find the file specified./) end end describe "inherited access control entries" do it "should be absent when the access control list is protected" do winsec.set_mode(WindowsSecurityTester::S_IRWXU, path) (winsec.get_mode(path) & WindowsSecurityTester::S_IEXTRA).should == 0 end it "should be present when the access control list is unprotected" do dir = tmpdir('win_sec_parent') # add a bunch of aces, make sure we can add to the directory allow = WindowsSecurityTester::STANDARD_RIGHTS_ALL | WindowsSecurityTester::SPECIFIC_RIGHTS_ALL inherit = WindowsSecurityTester::OBJECT_INHERIT_ACE | WindowsSecurityTester::CONTAINER_INHERIT_ACE winsec.set_acl(dir, true) do |acl| winsec.add_access_allowed_ace(acl, allow, "S-1-1-0", inherit) # everyone [ 544, 545, 546, 547 ].each do |rid| winsec.add_access_allowed_ace(acl, WindowsSecurityTester::STANDARD_RIGHTS_ALL, "S-1-5-32-#{rid}", inherit) end end # add a file child = File.join(dir, "child") File.new(child, "w").close # unprotect child, it should inherit from parent winsec.set_mode(WindowsSecurityTester::S_IRWXU, child, false) (winsec.get_mode(child) & WindowsSecurityTester::S_IEXTRA).should == WindowsSecurityTester::S_IEXTRA end end end describe "for an administrator", :if => Puppet.features.root? do before :each do winsec.set_owner(sids[:guest], path) winsec.set_group(sids[:guest], path) winsec.set_mode(WindowsSecurityTester::S_IRWXU | WindowsSecurityTester::S_IRWXG, path) lambda { File.open(path, 'r') }.should raise_error(Errno::EACCES) end after :each do winsec.set_owner(sids[:current_user], path) winsec.set_mode(WindowsSecurityTester::S_IRWXU, path) end - describe "when setting the owner sid" do + describe "#owner=" do it "should accept a user sid" do winsec.set_owner(sids[:admin], path) winsec.get_owner(path).should == sids[:admin] end it "should accept a group sid" do winsec.set_owner(sids[:power_users], path) winsec.get_owner(path).should == sids[:power_users] end it "should raise an exception if an invalid sid is provided" do lambda { winsec.set_owner("foobar", path) }.should raise_error(Puppet::Error, /Failed to convert string SID/) end it "should raise an exception if an invalid path is provided" do lambda { winsec.set_owner(sids[:guest], "c:\\doesnotexist.txt") }.should raise_error(Puppet::Error, /The system cannot find the file specified./) end end - describe "when setting the group sid" do + describe "#group=" do it "should accept a group sid" do winsec.set_group(sids[:power_users], path) winsec.get_group(path).should == sids[:power_users] end it "should accept a user sid" do winsec.set_group(sids[:admin], path) winsec.get_group(path).should == sids[:admin] end it "should allow owner and group to be the same sid" do winsec.set_owner(sids[:power_users], path) winsec.set_group(sids[:power_users], path) winsec.set_mode(0610, path) winsec.get_owner(path).should == sids[:power_users] winsec.get_group(path).should == sids[:power_users] # note group execute permission added to user ace, and then group rwx value # reflected to match winsec.get_mode(path).to_s(8).should == "770" end it "should raise an exception if an invalid sid is provided" do lambda { winsec.set_group("foobar", path) }.should raise_error(Puppet::Error, /Failed to convert string SID/) end it "should raise an exception if an invalid path is provided" do lambda { winsec.set_group(sids[:guest], "c:\\doesnotexist.txt") }.should raise_error(Puppet::Error, /The system cannot find the file specified./) end end describe "when the sid is NULL" do it "should retrieve an empty owner sid" it "should retrieve an empty group sid" end describe "when the sid refers to a deleted trustee" do it "should retrieve the user sid" do sid = nil user = Puppet::Util::ADSI::User.create("delete_me_user") user.commit begin sid = Sys::Admin::get_user(user.name).sid winsec.set_owner(sid, path) winsec.set_mode(WindowsSecurityTester::S_IRWXU, path) ensure Puppet::Util::ADSI::User.delete(user.name) end winsec.get_owner(path).should == sid winsec.get_mode(path).should == WindowsSecurityTester::S_IRWXU end it "should retrieve the group sid" do sid = nil group = Puppet::Util::ADSI::Group.create("delete_me_group") group.commit begin sid = Sys::Admin::get_group(group.name).sid winsec.set_group(sid, path) winsec.set_mode(WindowsSecurityTester::S_IRWXG, path) ensure Puppet::Util::ADSI::Group.delete(group.name) end winsec.get_group(path).should == sid winsec.get_mode(path).should == WindowsSecurityTester::S_IRWXG end end - describe "when getting the dacl" do + describe "#mode" do it "should deny all access when the DACL is empty" do winsec.set_acl(path, true) { |acl| } winsec.get_mode(path).should == 0 end # REMIND: ruby crashes when trying to set a NULL DACL # it "should allow all when it is nil" do # winsec.set_owner(sids[:current_user], path) # winsec.open_file(path, WindowsSecurityTester::READ_CONTROL | WindowsSecurityTester::WRITE_DAC) do |handle| # winsec.set_security_info(handle, WindowsSecurityTester::DACL_SECURITY_INFORMATION | WindowsSecurityTester::PROTECTED_DACL_SECURITY_INFORMATION, nil) # end # winsec.get_mode(path).to_s(8).should == "777" # end end + + describe "#string_to_sid_ptr" do + it "should raise an error if an invalid SID is specified" do + expect do + winsec.string_to_sid_ptr('foobar') + end.to raise_error(Puppet::Util::Windows::Error) { |error| error.code.should == 1337 } + end + + it "should yield if a block is given" do + yielded = nil + winsec.string_to_sid_ptr('S-1-1-0') do |sid| + yielded = sid + end + yielded.should_not be_nil + end + + it "should allow no block to be specified" do + winsec.string_to_sid_ptr('S-1-1-0').should be_true + end + end end end describe "file" do let :path do path = tmpfile('win_sec_test_file') File.new(path, "w").close path end it_behaves_like "a securable object" do def check_access(mode, path) if (mode & WindowsSecurityTester::S_IRUSR).nonzero? check_read(path) else lambda { check_read(path) }.should raise_error(Errno::EACCES) end if (mode & WindowsSecurityTester::S_IWUSR).nonzero? check_write(path) else lambda { check_write(path) }.should raise_error(Errno::EACCES) end if (mode & WindowsSecurityTester::S_IXUSR).nonzero? lambda { check_execute(path) }.should raise_error(Errno::ENOEXEC) else lambda { check_execute(path) }.should raise_error(Errno::EACCES) end end def check_read(path) File.open(path, 'r').close end def check_write(path) File.open(path, 'w').close end def check_execute(path) Kernel.exec(path) end end describe "locked files" do let (:explorer) { File.join(Dir::WINDOWS, "explorer.exe") } it "should get the owner" do winsec.get_owner(explorer).should match /^S-1-5-/ end it "should get the group" do winsec.get_group(explorer).should match /^S-1-5-/ end it "should get the mode" do winsec.get_mode(explorer).should == (WindowsSecurityTester::S_IRWXU | WindowsSecurityTester::S_IRWXG | WindowsSecurityTester::S_IEXTRA) end end end describe "directory" do let :path do tmpdir('win_sec_test_dir') end it_behaves_like "a securable object" do def check_access(mode, path) if (mode & WindowsSecurityTester::S_IRUSR).nonzero? check_read(path) else lambda { check_read(path) }.should raise_error(Errno::EACCES) end if (mode & WindowsSecurityTester::S_IWUSR).nonzero? check_write(path) else lambda { check_write(path) }.should raise_error(Errno::EACCES) end if (mode & WindowsSecurityTester::S_IXUSR).nonzero? check_execute(path) else lambda { check_execute(path) }.should raise_error(Errno::EACCES) end end def check_read(path) Dir.entries(path) end def check_write(path) Dir.mkdir(File.join(path, "subdir")) end def check_execute(path) Dir.chdir(path) {|dir| } end end describe "inheritable aces" do it "should be applied to child objects" do mode640 = WindowsSecurityTester::S_IRUSR | WindowsSecurityTester::S_IWUSR | WindowsSecurityTester::S_IRGRP winsec.set_mode(mode640, path) newfile = File.join(path, "newfile.txt") File.new(newfile, "w").close newdir = File.join(path, "newdir") Dir.mkdir(newdir) [newfile, newdir].each do |p| winsec.get_mode(p).to_s(8).should == mode640.to_s(8) end end end end end diff --git a/spec/shared_behaviours/file_serving.rb b/spec/shared_behaviours/file_serving.rb index f5a59f5cd..bc1d46ca5 100755 --- a/spec/shared_behaviours/file_serving.rb +++ b/spec/shared_behaviours/file_serving.rb @@ -1,67 +1,68 @@ #!/usr/bin/env rspec shared_examples_for "Puppet::FileServing::Files" do it "should use the rest terminus when the 'puppet' URI scheme is used and a host name is present" do uri = "puppet://myhost/fakemod/my/file" # It appears that the mocking somehow interferes with the caching subsystem. # This mock somehow causes another terminus to get generated. term = @indirection.terminus(:rest) @indirection.stubs(:terminus).with(:rest).returns term term.expects(:find) @indirection.find(uri) end it "should use the rest terminus when the 'puppet' URI scheme is used, no host name is present, and the process name is not 'puppet' or 'apply'" do uri = "puppet:///fakemod/my/file" Puppet.settings.stubs(:value).returns "foo" Puppet.settings.stubs(:value).with(:name).returns("puppetd") Puppet.settings.stubs(:value).with(:modulepath).returns("") @indirection.terminus(:rest).expects(:find) @indirection.find(uri) end it "should use the file_server terminus when the 'puppet' URI scheme is used, no host name is present, and the process name is 'puppet'" do uri = "puppet:///fakemod/my/file" Puppet::Node::Environment.stubs(:new).returns(stub("env", :name => "testing", :module => nil, :modulepath => [])) Puppet.settings.stubs(:value).returns "" Puppet.settings.stubs(:value).with(:name).returns("puppet") Puppet.settings.stubs(:value).with(:fileserverconfig).returns("/whatever") @indirection.terminus(:file_server).expects(:find) @indirection.terminus(:file_server).stubs(:authorized?).returns(true) @indirection.find(uri) end it "should use the file_server terminus when the 'puppet' URI scheme is used, no host name is present, and the process name is 'apply'" do uri = "puppet:///fakemod/my/file" Puppet::Node::Environment.stubs(:new).returns(stub("env", :name => "testing", :module => nil, :modulepath => [])) Puppet.settings.stubs(:value).returns "" Puppet.settings.stubs(:value).with(:name).returns("apply") Puppet.settings.stubs(:value).with(:fileserverconfig).returns("/whatever") @indirection.terminus(:file_server).expects(:find) @indirection.terminus(:file_server).stubs(:authorized?).returns(true) @indirection.find(uri) end it "should use the file terminus when the 'file' URI scheme is used" do - uri = "file:///fakemod/my/file" + uri = Puppet::Util.path_to_uri(File.expand_path('/fakemod/my/other file')) + uri.scheme.should == 'file' @indirection.terminus(:file).expects(:find) - @indirection.find(uri) + @indirection.find(uri.to_s) end it "should use the file terminus when a fully qualified path is provided" do - uri = "/fakemod/my/file" + uri = File.expand_path("/fakemod/my/file") @indirection.terminus(:file).expects(:find) @indirection.find(uri) end it "should use the configuration to test whether the request is allowed" do uri = "fakemod/my/file" mount = mock 'mount' config = stub 'configuration', :split_path => [mount, "eh"] @indirection.terminus(:file_server).stubs(:configuration).returns config @indirection.terminus(:file_server).expects(:find) mount.expects(:allowed?).returns(true) @indirection.find(uri, :node => "foo", :ip => "bar") end end diff --git a/spec/unit/configurer/downloader_spec.rb b/spec/unit/configurer/downloader_spec.rb index 8bb6a3dc6..5215fe5e3 100755 --- a/spec/unit/configurer/downloader_spec.rb +++ b/spec/unit/configurer/downloader_spec.rb @@ -1,200 +1,200 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/configurer/downloader' describe Puppet::Configurer::Downloader do require 'puppet_spec/files' include PuppetSpec::Files it "should require a name" do lambda { Puppet::Configurer::Downloader.new }.should raise_error(ArgumentError) end it "should require a path and a source at initialization" do lambda { Puppet::Configurer::Downloader.new("name") }.should raise_error(ArgumentError) end it "should set the name, path and source appropriately" do dler = Puppet::Configurer::Downloader.new("facts", "path", "source") dler.name.should == "facts" dler.path.should == "path" dler.source.should == "source" end it "should be able to provide a timeout value" do Puppet::Configurer::Downloader.should respond_to(:timeout) end it "should use the configtimeout, converted to an integer, as its timeout" do Puppet.settings.expects(:value).with(:configtimeout).returns "50" Puppet::Configurer::Downloader.timeout.should == 50 end describe "when creating the file that does the downloading" do before do @dler = Puppet::Configurer::Downloader.new("foo", "path", "source") end it "should create a file instance with the right path and source" do Puppet::Type.type(:file).expects(:new).with { |opts| opts[:path] == "path" and opts[:source] == "source" } @dler.file end it "should tag the file with the downloader name" do Puppet::Type.type(:file).expects(:new).with { |opts| opts[:tag] == "foo" } @dler.file end it "should always recurse" do Puppet::Type.type(:file).expects(:new).with { |opts| opts[:recurse] == true } @dler.file end it "should always purge" do Puppet::Type.type(:file).expects(:new).with { |opts| opts[:purge] == true } @dler.file end it "should never be in noop" do Puppet::Type.type(:file).expects(:new).with { |opts| opts[:noop] == false } @dler.file end it "should always set the owner to the current UID" do Process.expects(:uid).returns 51 Puppet::Type.type(:file).expects(:new).with { |opts| opts[:owner] == 51 } @dler.file end it "should always set the group to the current GID" do Process.expects(:gid).returns 61 Puppet::Type.type(:file).expects(:new).with { |opts| opts[:group] == 61 } @dler.file end it "should always force the download" do Puppet::Type.type(:file).expects(:new).with { |opts| opts[:force] == true } @dler.file end it "should never back up when downloading" do Puppet::Type.type(:file).expects(:new).with { |opts| opts[:backup] == false } @dler.file end it "should support providing an 'ignore' parameter" do Puppet::Type.type(:file).expects(:new).with { |opts| opts[:ignore] == [".svn"] } @dler = Puppet::Configurer::Downloader.new("foo", "path", "source", ".svn") @dler.file end it "should split the 'ignore' parameter on whitespace" do Puppet::Type.type(:file).expects(:new).with { |opts| opts[:ignore] == %w{.svn CVS} } @dler = Puppet::Configurer::Downloader.new("foo", "path", "source", ".svn CVS") @dler.file end end describe "when creating the catalog to do the downloading" do before do - @path = make_absolute("/download/path") - @dler = Puppet::Configurer::Downloader.new("foo", @path, "source") + @path = File.expand_path("/download/path") + @dler = Puppet::Configurer::Downloader.new("foo", @path, File.expand_path("source")) end it "should create a catalog and add the file to it" do catalog = @dler.catalog catalog.resources.size.should == 1 catalog.resources.first.class.should == Puppet::Type::File catalog.resources.first.name.should == @path end it "should specify that it is not managing a host catalog" do @dler.catalog.host_config.should == false end end describe "when downloading" do before do @dl_name = tmpfile("downloadpath") source_name = tmpfile("source") File.open(source_name, 'w') {|f| f.write('hola mundo') } @dler = Puppet::Configurer::Downloader.new("foo", @dl_name, source_name) end it "should not skip downloaded resources when filtering on tags", :fails_on_windows => true do Puppet[:tags] = 'maytag' @dler.evaluate File.exists?(@dl_name).should be_true end it "should log that it is downloading" do Puppet.expects(:info) Timeout.stubs(:timeout) @dler.evaluate end it "should set a timeout for the download" do Puppet::Configurer::Downloader.expects(:timeout).returns 50 Timeout.expects(:timeout).with(50) @dler.evaluate end it "should apply the catalog within the timeout block" do catalog = mock 'catalog' @dler.expects(:catalog).returns(catalog) Timeout.expects(:timeout).yields catalog.expects(:apply) @dler.evaluate end it "should return all changed file paths" do trans = mock 'transaction' catalog = mock 'catalog' @dler.expects(:catalog).returns(catalog) catalog.expects(:apply).yields(trans) Timeout.expects(:timeout).yields resource = mock 'resource' resource.expects(:[]).with(:path).returns "/changed/file" trans.expects(:changed?).returns([resource]) @dler.evaluate.should == %w{/changed/file} end it "should yield the resources if a block is given" do trans = mock 'transaction' catalog = mock 'catalog' @dler.expects(:catalog).returns(catalog) catalog.expects(:apply).yields(trans) Timeout.expects(:timeout).yields resource = mock 'resource' resource.expects(:[]).with(:path).returns "/changed/file" trans.expects(:changed?).returns([resource]) yielded = nil @dler.evaluate { |r| yielded = r } yielded.should == resource end it "should catch and log exceptions" do Puppet.expects(:err) Timeout.stubs(:timeout).raises(Puppet::Error, "testing") lambda { @dler.evaluate }.should_not raise_error end end end diff --git a/spec/unit/file_serving/fileset_spec.rb b/spec/unit/file_serving/fileset_spec.rb index aff4c91fa..4f9d3c542 100755 --- a/spec/unit/file_serving/fileset_spec.rb +++ b/spec/unit/file_serving/fileset_spec.rb @@ -1,378 +1,378 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/file_serving/fileset' describe Puppet::FileServing::Fileset, " when initializing" do include PuppetSpec::Files before :each do @somefile = make_absolute("/some/file") end it "should require a path" do proc { Puppet::FileServing::Fileset.new }.should raise_error(ArgumentError) end it "should fail if its path is not fully qualified" do proc { Puppet::FileServing::Fileset.new("some/file") }.should raise_error(ArgumentError) end it "should not fail if the path is fully qualified, with a trailing separator" do path_with_separator = "#{@somefile}#{File::SEPARATOR}" File.stubs(:lstat).with(@somefile).returns stub('stat') fileset = Puppet::FileServing::Fileset.new(path_with_separator) fileset.path.should == @somefile end it "should not fail if the path is just the file separator" do - path = make_absolute(File::SEPARATOR) + path = File.expand_path(File::SEPARATOR) File.stubs(:lstat).with(path).returns stub('stat') fileset = Puppet::FileServing::Fileset.new(path) fileset.path.should == path end it "should fail if its path does not exist" do File.expects(:lstat).with(@somefile).returns nil proc { Puppet::FileServing::Fileset.new(@somefile) }.should raise_error(ArgumentError) end it "should accept a 'recurse' option" do File.expects(:lstat).with(@somefile).returns stub("stat") set = Puppet::FileServing::Fileset.new(@somefile, :recurse => true) set.recurse.should be_true end it "should accept a 'recurselimit' option" do File.expects(:lstat).with(@somefile).returns stub("stat") set = Puppet::FileServing::Fileset.new(@somefile, :recurselimit => 3) set.recurselimit.should == 3 end it "should accept an 'ignore' option" do File.expects(:lstat).with(@somefile).returns stub("stat") set = Puppet::FileServing::Fileset.new(@somefile, :ignore => ".svn") set.ignore.should == [".svn"] end it "should accept a 'links' option" do File.expects(:lstat).with(@somefile).returns stub("stat") set = Puppet::FileServing::Fileset.new(@somefile, :links => :manage) set.links.should == :manage end it "should accept a 'checksum_type' option" do File.expects(:lstat).with(@somefile).returns stub("stat") set = Puppet::FileServing::Fileset.new(@somefile, :checksum_type => :test) set.checksum_type.should == :test end it "should fail if 'links' is set to anything other than :manage or :follow" do proc { Puppet::FileServing::Fileset.new(@somefile, :links => :whatever) }.should raise_error(ArgumentError) end it "should default to 'false' for recurse" do File.expects(:lstat).with(@somefile).returns stub("stat") Puppet::FileServing::Fileset.new(@somefile).recurse.should == false end it "should default to :infinite for recurselimit" do File.expects(:lstat).with(@somefile).returns stub("stat") Puppet::FileServing::Fileset.new(@somefile).recurselimit.should == :infinite end it "should default to an empty ignore list" do File.expects(:lstat).with(@somefile).returns stub("stat") Puppet::FileServing::Fileset.new(@somefile).ignore.should == [] end it "should default to :manage for links" do File.expects(:lstat).with(@somefile).returns stub("stat") Puppet::FileServing::Fileset.new(@somefile).links.should == :manage end it "should support using an Indirector Request for its options" do File.expects(:lstat).with(@somefile).returns stub("stat") request = Puppet::Indirector::Request.new(:file_serving, :find, "foo") lambda { Puppet::FileServing::Fileset.new(@somefile, request) }.should_not raise_error end describe "using an indirector request" do before do File.stubs(:lstat).returns stub("stat") @values = {:links => :manage, :ignore => %w{a b}, :recurse => true, :recurselimit => 1234} @request = Puppet::Indirector::Request.new(:file_serving, :find, "foo") @myfile = make_absolute("/my/file") end [:recurse, :recurselimit, :ignore, :links].each do |option| it "should pass :recurse, :recurselimit, :ignore, and :links settings on to the fileset if present" do @request.stubs(:options).returns(option => @values[option]) Puppet::FileServing::Fileset.new(@myfile, @request).send(option).should == @values[option] end it "should pass :recurse, :recurselimit, :ignore, and :links settings on to the fileset if present with the keys stored as strings" do @request.stubs(:options).returns(option.to_s => @values[option]) Puppet::FileServing::Fileset.new(@myfile, @request).send(option).should == @values[option] end end it "should convert the integer as a string to their integer counterpart when setting options" do @request.stubs(:options).returns(:recurselimit => "1234") Puppet::FileServing::Fileset.new(@myfile, @request).recurselimit.should == 1234 end it "should convert the string 'true' to the boolean true when setting options" do @request.stubs(:options).returns(:recurse => "true") Puppet::FileServing::Fileset.new(@myfile, @request).recurse.should == true end it "should convert the string 'false' to the boolean false when setting options" do @request.stubs(:options).returns(:recurse => "false") Puppet::FileServing::Fileset.new(@myfile, @request).recurse.should == false end end end describe Puppet::FileServing::Fileset, " when determining whether to recurse" do include PuppetSpec::Files before do @path = make_absolute("/my/path") File.expects(:lstat).with(@path).returns stub("stat") @fileset = Puppet::FileServing::Fileset.new(@path) end it "should always recurse if :recurse is set to 'true' and with infinite recursion" do @fileset.recurse = true @fileset.recurselimit = :infinite @fileset.recurse?(0).should be_true end it "should never recurse if :recurse is set to 'false'" do @fileset.recurse = false @fileset.recurse?(-1).should be_false end it "should recurse if :recurse is set to true, :recurselimit is set to an integer and the current depth is less than that integer" do @fileset.recurse = true @fileset.recurselimit = 1 @fileset.recurse?(0).should be_true end it "should recurse if :recurse is set to true, :recurselimit is set to an integer and the current depth is equal to that integer" do @fileset.recurse = true @fileset.recurselimit = 1 @fileset.recurse?(1).should be_true end it "should not recurse if :recurse is set to true, :recurselimit is set to an integer and the current depth is greater than that integer" do @fileset.recurse = true @fileset.recurselimit = 1 @fileset.recurse?(2).should be_false end end describe Puppet::FileServing::Fileset, " when recursing" do include PuppetSpec::Files before do @path = make_absolute("/my/path") File.expects(:lstat).with(@path).returns stub("stat", :directory? => true) @fileset = Puppet::FileServing::Fileset.new(@path) @dirstat = stub 'dirstat', :directory? => true @filestat = stub 'filestat', :directory? => false end def mock_dir_structure(path, stat_method = :lstat) File.stubs(stat_method).with(path).returns(@dirstat) Dir.stubs(:entries).with(path).returns(%w{one two .svn CVS}) # Keep track of the files we're stubbing. @files = %w{.} %w{one two .svn CVS}.each do |subdir| @files << subdir # relative path subpath = File.join(path, subdir) File.stubs(stat_method).with(subpath).returns(@dirstat) Dir.stubs(:entries).with(subpath).returns(%w{.svn CVS file1 file2}) %w{file1 file2 .svn CVS}.each do |file| @files << File.join(subdir, file) # relative path File.stubs(stat_method).with(File.join(subpath, file)).returns(@filestat) end end end it "should recurse through the whole file tree if :recurse is set to 'true'" do mock_dir_structure(@path) @fileset.stubs(:recurse?).returns(true) @fileset.files.sort.should == @files.sort end it "should not recurse if :recurse is set to 'false'" do mock_dir_structure(@path) @fileset.stubs(:recurse?).returns(false) @fileset.files.should == %w{.} end # It seems like I should stub :recurse? here, or that I shouldn't stub the # examples above, but... it "should recurse to the level set if :recurselimit is set to an integer" do mock_dir_structure(@path) @fileset.recurse = true @fileset.recurselimit = 1 @fileset.files.should == %w{. one two .svn CVS} end it "should ignore the '.' and '..' directories in subdirectories" do mock_dir_structure(@path) @fileset.recurse = true @fileset.files.sort.should == @files.sort end it "should function if the :ignore value provided is nil" do mock_dir_structure(@path) @fileset.recurse = true @fileset.ignore = nil lambda { @fileset.files }.should_not raise_error end it "should ignore files that match a single pattern in the ignore list" do mock_dir_structure(@path) @fileset.recurse = true @fileset.ignore = ".svn" @fileset.files.find { |file| file.include?(".svn") }.should be_nil end it "should ignore files that match any of multiple patterns in the ignore list" do mock_dir_structure(@path) @fileset.recurse = true @fileset.ignore = %w{.svn CVS} @fileset.files.find { |file| file.include?(".svn") or file.include?("CVS") }.should be_nil end it "should use File.stat if :links is set to :follow" do mock_dir_structure(@path, :stat) @fileset.recurse = true @fileset.links = :follow @fileset.files.sort.should == @files.sort end it "should use File.lstat if :links is set to :manage" do mock_dir_structure(@path, :lstat) @fileset.recurse = true @fileset.links = :manage @fileset.files.sort.should == @files.sort end it "should succeed when paths have regexp significant characters" do @path = make_absolute("/my/path/rV1x2DafFr0R6tGG+1bbk++++TM") File.expects(:lstat).with(@path).returns stub("stat", :directory? => true) @fileset = Puppet::FileServing::Fileset.new(@path) mock_dir_structure(@path) @fileset.recurse = true @fileset.files.sort.should == @files.sort end end describe Puppet::FileServing::Fileset, " when following links that point to missing files" do include PuppetSpec::Files before do @path = make_absolute("/my/path") File.expects(:lstat).with(@path).returns stub("stat", :directory? => true) @fileset = Puppet::FileServing::Fileset.new(@path) @fileset.links = :follow @fileset.recurse = true @stat = stub 'stat', :directory? => true File.expects(:stat).with(@path).returns(@stat) File.expects(:stat).with(File.join(@path, "mylink")).raises(Errno::ENOENT) Dir.stubs(:entries).with(@path).returns(["mylink"]) end it "should not fail" do proc { @fileset.files }.should_not raise_error end it "should still manage the link" do @fileset.files.sort.should == %w{. mylink}.sort end end describe Puppet::FileServing::Fileset, " when ignoring" do include PuppetSpec::Files before do @path = make_absolute("/my/path") File.expects(:lstat).with(@path).returns stub("stat", :directory? => true) @fileset = Puppet::FileServing::Fileset.new(@path) end it "should use ruby's globbing to determine what files should be ignored" do @fileset.ignore = ".svn" File.expects(:fnmatch?).with(".svn", "my_file") @fileset.ignore?("my_file") end it "should ignore files whose paths match a single provided ignore value" do @fileset.ignore = ".svn" File.stubs(:fnmatch?).with(".svn", "my_file").returns true @fileset.ignore?("my_file").should be_true end it "should ignore files whose paths match any of multiple provided ignore values" do @fileset.ignore = [".svn", "CVS"] File.stubs(:fnmatch?).with(".svn", "my_file").returns false File.stubs(:fnmatch?).with("CVS", "my_file").returns true @fileset.ignore?("my_file").should be_true end end describe Puppet::FileServing::Fileset, "when merging other filesets" do include PuppetSpec::Files before do @paths = [make_absolute("/first/path"), make_absolute("/second/path"), make_absolute("/third/path")] File.stubs(:lstat).returns stub("stat", :directory? => false) @filesets = @paths.collect do |path| File.stubs(:lstat).with(path).returns stub("stat", :directory? => true) Puppet::FileServing::Fileset.new(path, :recurse => true) end Dir.stubs(:entries).returns [] end it "should return a hash of all files in each fileset with the value being the base path" do Dir.expects(:entries).with(make_absolute("/first/path")).returns(%w{one uno}) Dir.expects(:entries).with(make_absolute("/second/path")).returns(%w{two dos}) Dir.expects(:entries).with(make_absolute("/third/path")).returns(%w{three tres}) Puppet::FileServing::Fileset.merge(*@filesets).should == { "." => make_absolute("/first/path"), "one" => make_absolute("/first/path"), "uno" => make_absolute("/first/path"), "two" => make_absolute("/second/path"), "dos" => make_absolute("/second/path"), "three" => make_absolute("/third/path"), "tres" => make_absolute("/third/path"), } end it "should include the base directory from the first fileset" do Dir.expects(:entries).with(make_absolute("/first/path")).returns(%w{one}) Dir.expects(:entries).with(make_absolute("/second/path")).returns(%w{two}) Puppet::FileServing::Fileset.merge(*@filesets)["."].should == make_absolute("/first/path") end it "should use the base path of the first found file when relative file paths conflict" do Dir.expects(:entries).with(make_absolute("/first/path")).returns(%w{one}) Dir.expects(:entries).with(make_absolute("/second/path")).returns(%w{one}) Puppet::FileServing::Fileset.merge(*@filesets)["one"].should == make_absolute("/first/path") end end diff --git a/spec/unit/file_serving/indirection_hooks_spec.rb b/spec/unit/file_serving/indirection_hooks_spec.rb index 526c49e0f..7bd713e78 100755 --- a/spec/unit/file_serving/indirection_hooks_spec.rb +++ b/spec/unit/file_serving/indirection_hooks_spec.rb @@ -1,59 +1,59 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/file_serving/indirection_hooks' describe Puppet::FileServing::IndirectionHooks do before do @object = Object.new @object.extend(Puppet::FileServing::IndirectionHooks) @request = stub 'request', :key => "mymod/myfile", :options => {:node => "whatever"}, :server => nil, :protocol => nil end describe "when being used to select termini" do it "should return :file if the request key is fully qualified" do - @request.expects(:key).returns "#{File::SEPARATOR}foo" + @request.expects(:key).returns File.expand_path('/foo') @object.select_terminus(@request).should == :file end it "should return :file if the URI protocol is set to 'file'" do @request.expects(:protocol).returns "file" @object.select_terminus(@request).should == :file end it "should fail when a protocol other than :puppet or :file is used" do @request.stubs(:protocol).returns "http" proc { @object.select_terminus(@request) }.should raise_error(ArgumentError) end describe "and the protocol is 'puppet'" do before do @request.stubs(:protocol).returns "puppet" end it "should choose :rest when a server is specified" do @request.stubs(:protocol).returns "puppet" @request.expects(:server).returns "foo" @object.select_terminus(@request).should == :rest end # This is so a given file location works when bootstrapping with no server. it "should choose :rest when the Settings name isn't 'puppet'" do @request.stubs(:protocol).returns "puppet" @request.stubs(:server).returns "foo" Puppet.settings.stubs(:value).with(:name).returns "foo" @object.select_terminus(@request).should == :rest end it "should choose :file_server when the settings name is 'puppet' and no server is specified" do modules = mock 'modules' @request.expects(:protocol).returns "puppet" @request.expects(:server).returns nil Puppet.settings.expects(:value).with(:name).returns "puppet" @object.select_terminus(@request).should == :file_server end end end end diff --git a/spec/unit/file_serving/metadata_spec.rb b/spec/unit/file_serving/metadata_spec.rb index 39f2a9548..7462fed5d 100755 --- a/spec/unit/file_serving/metadata_spec.rb +++ b/spec/unit/file_serving/metadata_spec.rb @@ -1,285 +1,329 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/file_serving/metadata' describe Puppet::FileServing::Metadata do it "should should be a subclass of Base" do Puppet::FileServing::Metadata.superclass.should equal(Puppet::FileServing::Base) end it "should indirect file_metadata" do Puppet::FileServing::Metadata.indirection.name.should == :file_metadata end it "should should include the IndirectionHooks module in its indirection" do Puppet::FileServing::Metadata.indirection.singleton_class.included_modules.should include(Puppet::FileServing::IndirectionHooks) end it "should have a method that triggers attribute collection" do Puppet::FileServing::Metadata.new("/foo/bar").should respond_to(:collect) end it "should support pson serialization" do Puppet::FileServing::Metadata.new("/foo/bar").should respond_to(:to_pson) end it "should support to_pson_data_hash" do Puppet::FileServing::Metadata.new("/foo/bar").should respond_to(:to_pson_data_hash) end it "should support pson deserialization" do Puppet::FileServing::Metadata.should respond_to(:from_pson) end describe "when serializing" do before do @metadata = Puppet::FileServing::Metadata.new("/foo/bar") end it "should perform pson serialization by calling to_pson on it's pson_data_hash" do pdh = mock "data hash" pdh_as_pson = mock "data as pson" @metadata.expects(:to_pson_data_hash).returns pdh pdh.expects(:to_pson).returns pdh_as_pson @metadata.to_pson.should == pdh_as_pson end it "should serialize as FileMetadata" do @metadata.to_pson_data_hash['document_type'].should == "FileMetadata" end it "the data should include the path, relative_path, links, owner, group, mode, checksum, type, and destination" do @metadata.to_pson_data_hash['data'].keys.sort.should == %w{ path relative_path links owner group mode checksum type destination }.sort end it "should pass the path in the hash verbatum" do @metadata.to_pson_data_hash['data']['path'] == @metadata.path end it "should pass the relative_path in the hash verbatum" do @metadata.to_pson_data_hash['data']['relative_path'] == @metadata.relative_path end it "should pass the links in the hash verbatum" do @metadata.to_pson_data_hash['data']['links'] == @metadata.links end it "should pass the path owner in the hash verbatum" do @metadata.to_pson_data_hash['data']['owner'] == @metadata.owner end it "should pass the group in the hash verbatum" do @metadata.to_pson_data_hash['data']['group'] == @metadata.group end it "should pass the mode in the hash verbatum" do @metadata.to_pson_data_hash['data']['mode'] == @metadata.mode end it "should pass the ftype in the hash verbatum as the 'type'" do @metadata.to_pson_data_hash['data']['type'] == @metadata.ftype end it "should pass the destination verbatum" do @metadata.to_pson_data_hash['data']['destination'] == @metadata.destination end it "should pass the checksum in the hash as a nested hash" do @metadata.to_pson_data_hash['data']['checksum'].should be_is_a(Hash) end it "should pass the checksum_type in the hash verbatum as the checksum's type" do @metadata.to_pson_data_hash['data']['checksum']['type'] == @metadata.checksum_type end it "should pass the checksum in the hash verbatum as the checksum's value" do @metadata.to_pson_data_hash['data']['checksum']['value'] == @metadata.checksum end end end -describe Puppet::FileServing::Metadata, " when finding the file to use for setting attributes" do - before do - @path = "/my/path" - @metadata = Puppet::FileServing::Metadata.new(@path) +describe Puppet::FileServing::Metadata do + include PuppetSpec::Files + + shared_examples_for "metadata collector" do + let(:metadata) do + data = described_class.new(path) + data.collect + data + end + + describe "when collecting attributes" do + describe "when managing files" do + let(:path) { tmpfile('file_serving_metadata') } + + before :each do + FileUtils.touch(path) + end + + it "should be able to produce xmlrpc-style attribute information" do + metadata.should respond_to(:attributes_with_tabs) + end + + it "should set the owner to the file's current owner" do + metadata.owner.should == owner + end + + it "should set the group to the file's current group" do + metadata.group.should == group + end + + it "should set the mode to the file's masked mode" do + set_mode(33261, path) + + metadata.mode.should == 0755 + end - # Use a link because it's easier to test -- no checksumming - @stat = stub "stat", :uid => 10, :gid => 20, :mode => 0755, :ftype => "link" + describe "#checksum" do + let(:checksum) { Digest::MD5.hexdigest("some content\n") } - # Not quite. We don't want to checksum links, but we must because they might be being followed. - @checksum = Digest::MD5.hexdigest("some content\n") # Remove these when :managed links are no longer checksumed. - @metadata.stubs(:md5_file).returns(@checksum) # - end + before :each do + File.open(path, "w") {|f| f.print("some content\n")} + end - it "should accept a base path path to which the file should be relative" do - File.expects(:lstat).with(@path).returns @stat - File.expects(:readlink).with(@path).returns "/what/ever" - @metadata.collect - end + it "should default to a checksum of type MD5 with the file's current checksum" do + metadata.checksum.should == "{md5}#{checksum}" + end - it "should use the set base path if one is not provided" do - File.expects(:lstat).with(@path).returns @stat - File.expects(:readlink).with(@path).returns "/what/ever" - @metadata.collect - end + it "should give a mtime checksum when checksum_type is set" do + time = Time.now + metadata.checksum_type = "mtime" + metadata.expects(:mtime_file).returns(@time) + metadata.collect + metadata.checksum.should == "{mtime}#{@time}" + end - it "should raise an exception if the file does not exist" do - File.expects(:lstat).with(@path).raises(Errno::ENOENT) - proc { @metadata.collect}.should raise_error(Errno::ENOENT) - end -end + it "should produce tab-separated mode, type, owner, group, and checksum for xmlrpc" do + set_mode(0755, path) -describe Puppet::FileServing::Metadata, " when collecting attributes" do - before do - @path = "/my/file" - # Use a real file mode, so we can validate the masking is done. - @stat = stub 'stat', :uid => 10, :gid => 20, :mode => 33261, :ftype => "file" - File.stubs(:lstat).returns(@stat) - @checksum = Digest::MD5.hexdigest("some content\n") - @metadata = Puppet::FileServing::Metadata.new("/my/file") - @metadata.stubs(:md5_file).returns(@checksum) - @metadata.collect - end + metadata.attributes_with_tabs.should == "#{0755.to_s}\tfile\t#{owner}\t#{group}\t{md5}#{checksum}" + end + end + end - it "should be able to produce xmlrpc-style attribute information" do - @metadata.should respond_to(:attributes_with_tabs) - end + describe "when managing directories" do + let(:path) { tmpdir('file_serving_metadata_dir') } + let(:time) { Time.now } - # LAK:FIXME This should actually change at some point - it "should set the owner by id" do - @metadata.owner.should be_instance_of(Fixnum) - end + before :each do + metadata.expects(:ctime_file).returns(time) + end - # LAK:FIXME This should actually change at some point - it "should set the group by id" do - @metadata.group.should be_instance_of(Fixnum) - end + it "should only use checksums of type 'ctime' for directories" do + metadata.collect + metadata.checksum.should == "{ctime}#{time}" + end - it "should set the owner to the file's current owner" do - @metadata.owner.should == 10 - end + it "should only use checksums of type 'ctime' for directories even if checksum_type set" do + metadata.checksum_type = "mtime" + metadata.expects(:mtime_file).never + metadata.collect + metadata.checksum.should == "{ctime}#{time}" + end - it "should set the group to the file's current group" do - @metadata.group.should == 20 - end + it "should produce tab-separated mode, type, owner, group, and checksum for xmlrpc" do + set_mode(0755, path) + metadata.collect - it "should set the mode to the file's masked mode" do - @metadata.mode.should == 0755 - end + metadata.attributes_with_tabs.should == "#{0755.to_s}\tdirectory\t#{owner}\t#{group}\t{ctime}#{time.to_s}" + end + end - it "should set the checksum to the file's current checksum" do - @metadata.checksum.should == "{md5}#{@checksum}" - end + describe "when managing links", :unless => Puppet.features.microsoft_windows? do + # 'path' is a link that points to 'target' + let(:path) { tmpfile('file_serving_metadata_link') } + let(:target) { tmpfile('file_serving_metadata_target') } + let(:checksum) { Digest::MD5.hexdigest("some content\n") } - describe "when managing files" do - it "should default to a checksum of type MD5" do - @metadata.checksum.should == "{md5}#{@checksum}" - end + before :each do + File.open(target, "w") {|f| f.print("some content\n")} + set_mode(0644, target) - it "should give a mtime checksum when checksum_type is set" do - time = Time.now - @metadata.checksum_type = "mtime" - @metadata.expects(:mtime_file).returns(@time) - @metadata.collect - @metadata.checksum.should == "{mtime}#{@time}" - end + FileUtils.symlink(target, path) + set_mode(0755, path) + end - it "should produce tab-separated mode, type, owner, group, and checksum for xmlrpc" do - @metadata.attributes_with_tabs.should == "#{0755.to_s}\tfile\t10\t20\t{md5}#{@checksum}" - end - end + it "should read links instead of returning their checksums" do + metadata.destination.should == target + end - describe "when managing directories" do - before do - @stat.stubs(:ftype).returns("directory") - @time = Time.now - @metadata.expects(:ctime_file).returns(@time) - end + pending "should produce tab-separated mode, type, owner, group, and destination for xmlrpc" do + # "We'd like this to be true, but we need to always collect the checksum because in the server/client/server round trip we lose the distintion between manage and follow." + metadata.attributes_with_tabs.should == "#{0755}\tlink\t#{owner}\t#{group}\t#{target}" + end - it "should only use checksums of type 'ctime' for directories" do - @metadata.collect - @metadata.checksum.should == "{ctime}#{@time}" + it "should produce tab-separated mode, type, owner, group, checksum, and destination for xmlrpc" do + metadata.attributes_with_tabs.should == "#{0755}\tlink\t#{owner}\t#{group}\t{md5}eb9c2bf0eb63f3a7bc0ea37ef18aeba5\t#{target}" + end + end end - it "should only use checksums of type 'ctime' for directories even if checksum_type set" do - @metadata.checksum_type = "mtime" - @metadata.expects(:mtime_file).never - @metadata.collect - @metadata.checksum.should == "{ctime}#{@time}" - end + describe Puppet::FileServing::Metadata, " when finding the file to use for setting attributes" do + let(:path) { tmpfile('file_serving_metadata_find_file') } + + before :each do + File.open(path, "w") {|f| f.print("some content\n")} + set_mode(0755, path) + end + + it "should accept a base path to which the file should be relative" do + dir = tmpdir('metadata_dir') + metadata = described_class.new(dir) + metadata.relative_path = 'relative_path' + + FileUtils.touch(metadata.full_path) + + metadata.collect + end + + it "should use the set base path if one is not provided" do + metadata.collect + end - it "should produce tab-separated mode, type, owner, group, and checksum for xmlrpc" do - @metadata.collect - @metadata.attributes_with_tabs.should == "#{0755.to_s}\tdirectory\t10\t20\t{ctime}#{@time.to_s}" + it "should raise an exception if the file does not exist" do + File.delete(path) + + proc { metadata.collect}.should raise_error(Errno::ENOENT) + end end end - describe "when managing links" do - before do - @stat.stubs(:ftype).returns("link") - File.expects(:readlink).with("/my/file").returns("/path/to/link") - @metadata.collect + describe "on POSIX systems", :if => Puppet.features.posix? do + let(:owner) {10} + let(:group) {20} - @checksum = Digest::MD5.hexdigest("some content\n") # Remove these when :managed links are no longer checksumed. - @file.stubs(:md5_file).returns(@checksum) # + before :each do + File::Stat.any_instance.stubs(:uid).returns owner + File::Stat.any_instance.stubs(:gid).returns group end - it "should read links instead of returning their checksums" do - @metadata.destination.should == "/path/to/link" + it_should_behave_like "metadata collector" + + def set_mode(mode, path) + File.chmod(mode, path) end + end + + describe "on Windows systems", :if => Puppet.features.microsoft_windows? do + let(:owner) {'S-1-1-50'} + let(:group) {'S-1-1-51'} - pending "should produce tab-separated mode, type, owner, group, and destination for xmlrpc" do - # "We'd like this to be true, but we need to always collect the checksum because in the server/client/server round trip we lose the distintion between manage and follow." - @metadata.attributes_with_tabs.should == "#{0755}\tlink\t10\t20\t/path/to/link" + before :each do + require 'puppet/util/windows/security' + Puppet::Util::Windows::Security.stubs(:get_owner).returns owner + Puppet::Util::Windows::Security.stubs(:get_group).returns group end - it "should produce tab-separated mode, type, owner, group, checksum, and destination for xmlrpc" do - @metadata.attributes_with_tabs.should == "#{0755}\tlink\t10\t20\t{md5}eb9c2bf0eb63f3a7bc0ea37ef18aeba5\t/path/to/link" + it_should_behave_like "metadata collector" + + def set_mode(mode, path) + Puppet::Util::Windows::Security.set_mode(mode, path) end end end -describe Puppet::FileServing::Metadata, " when pointing to a link" do + +describe Puppet::FileServing::Metadata, " when pointing to a link", :unless => Puppet.features.microsoft_windows? do describe "when links are managed" do before do @file = Puppet::FileServing::Metadata.new("/base/path/my/file", :links => :manage) File.expects(:lstat).with("/base/path/my/file").returns stub("stat", :uid => 1, :gid => 2, :ftype => "link", :mode => 0755) File.expects(:readlink).with("/base/path/my/file").returns "/some/other/path" @checksum = Digest::MD5.hexdigest("some content\n") # Remove these when :managed links are no longer checksumed. @file.stubs(:md5_file).returns(@checksum) # end it "should store the destination of the link in :destination if links are :manage" do @file.collect @file.destination.should == "/some/other/path" end pending "should not collect the checksum if links are :manage" do # We'd like this to be true, but we need to always collect the checksum because in the server/client/server round trip we lose the distintion between manage and follow. @file.collect @file.checksum.should be_nil end it "should collect the checksum if links are :manage" do # see pending note above @file.collect @file.checksum.should == "{md5}#{@checksum}" end end describe "when links are followed" do before do @file = Puppet::FileServing::Metadata.new("/base/path/my/file", :links => :follow) File.expects(:stat).with("/base/path/my/file").returns stub("stat", :uid => 1, :gid => 2, :ftype => "file", :mode => 0755) File.expects(:readlink).with("/base/path/my/file").never @checksum = Digest::MD5.hexdigest("some content\n") @file.stubs(:md5_file).returns(@checksum) end it "should not store the destination of the link in :destination if links are :follow" do @file.collect @file.destination.should be_nil end it "should collect the checksum if links are :follow" do @file.collect @file.checksum.should == "{md5}#{@checksum}" end end end diff --git a/spec/unit/indirector/direct_file_server_spec.rb b/spec/unit/indirector/direct_file_server_spec.rb index 87380438a..14c1fc5e9 100755 --- a/spec/unit/indirector/direct_file_server_spec.rb +++ b/spec/unit/indirector/direct_file_server_spec.rb @@ -1,79 +1,80 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/indirector/direct_file_server' describe Puppet::Indirector::DirectFileServer do before :all do Puppet::Indirector::Terminus.stubs(:register_terminus_class) @model = mock 'model' @indirection = stub 'indirection', :name => :mystuff, :register_terminus_type => nil, :model => @model Puppet::Indirector::Indirection.stubs(:instance).returns(@indirection) module Testing; end @direct_file_class = class Testing::Mytype < Puppet::Indirector::DirectFileServer self end @server = @direct_file_class.new - @uri = "file:///my/local" + @path = File.expand_path('/my/local') + @uri = Puppet::Util.path_to_uri(@path).to_s @request = Puppet::Indirector::Request.new(:mytype, :find, @uri) end describe Puppet::Indirector::DirectFileServer, "when finding a single file" do it "should return nil if the file does not exist" do - FileTest.expects(:exists?).with("/my/local").returns false + FileTest.expects(:exists?).with(@path).returns false @server.find(@request).should be_nil end it "should return a Content instance created with the full path to the file if the file exists" do - FileTest.expects(:exists?).with("/my/local").returns true + FileTest.expects(:exists?).with(@path).returns true @model.expects(:new).returns(:mycontent) @server.find(@request).should == :mycontent end end describe Puppet::Indirector::DirectFileServer, "when creating the instance for a single found file" do before do @data = mock 'content' @data.stubs(:collect) - FileTest.expects(:exists?).with("/my/local").returns true + FileTest.expects(:exists?).with(@path).returns true end it "should pass the full path to the instance" do - @model.expects(:new).with { |key, options| key == "/my/local" }.returns(@data) + @model.expects(:new).with { |key, options| key == @path }.returns(@data) @server.find(@request) end it "should pass the :links setting on to the created Content instance if the file exists and there is a value for :links" do @model.expects(:new).returns(@data) @data.expects(:links=).with(:manage) @request.stubs(:options).returns(:links => :manage) @server.find(@request) end end describe Puppet::Indirector::DirectFileServer, "when searching for multiple files" do it "should return nil if the file does not exist" do - FileTest.expects(:exists?).with("/my/local").returns false + FileTest.expects(:exists?).with(@path).returns false @server.find(@request).should be_nil end it "should use :path2instances from the terminus_helper to return instances if the file exists" do - FileTest.expects(:exists?).with("/my/local").returns true + FileTest.expects(:exists?).with(@path).returns true @server.expects(:path2instances) @server.search(@request) end it "should pass the original request to :path2instances" do - FileTest.expects(:exists?).with("/my/local").returns true - @server.expects(:path2instances).with(@request, "/my/local") + FileTest.expects(:exists?).with(@path).returns true + @server.expects(:path2instances).with(@request, @path) @server.search(@request) end end end diff --git a/spec/unit/indirector/file_metadata/file_spec.rb b/spec/unit/indirector/file_metadata/file_spec.rb index 9df53384b..b66c7e05f 100755 --- a/spec/unit/indirector/file_metadata/file_spec.rb +++ b/spec/unit/indirector/file_metadata/file_spec.rb @@ -1,48 +1,50 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/indirector/file_metadata/file' describe Puppet::Indirector::FileMetadata::File do it "should be registered with the file_metadata indirection" do Puppet::Indirector::Terminus.terminus_class(:file_metadata, :file).should equal(Puppet::Indirector::FileMetadata::File) end it "should be a subclass of the DirectFileServer terminus" do Puppet::Indirector::FileMetadata::File.superclass.should equal(Puppet::Indirector::DirectFileServer) end describe "when creating the instance for a single found file" do before do @metadata = Puppet::Indirector::FileMetadata::File.new - @uri = "file:///my/local" + @path = File.expand_path('/my/local') + @uri = Puppet::Util.path_to_uri(@path).to_s @data = mock 'metadata' @data.stubs(:collect) - FileTest.expects(:exists?).with("/my/local").returns true + FileTest.expects(:exists?).with(@path).returns true @request = Puppet::Indirector::Request.new(:file_metadata, :find, @uri) end it "should collect its attributes when a file is found" do @data.expects(:collect) Puppet::FileServing::Metadata.expects(:new).returns(@data) @metadata.find(@request).should == @data end end describe "when searching for multiple files" do before do @metadata = Puppet::Indirector::FileMetadata::File.new - @uri = "file:///my/local" + @path = File.expand_path('/my/local') + @uri = Puppet::Util.path_to_uri(@path).to_s @request = Puppet::Indirector::Request.new(:file_metadata, :find, @uri) end it "should collect the attributes of the instances returned" do - FileTest.expects(:exists?).with("/my/local").returns true + FileTest.expects(:exists?).with(@path).returns true @metadata.expects(:path2instances).returns( [mock("one", :collect => nil), mock("two", :collect => nil)] ) @metadata.search(@request) end end end diff --git a/spec/unit/indirector/request_spec.rb b/spec/unit/indirector/request_spec.rb index 87b9af438..d330248dd 100755 --- a/spec/unit/indirector/request_spec.rb +++ b/spec/unit/indirector/request_spec.rb @@ -1,304 +1,314 @@ #!/usr/bin/env rspec require 'spec_helper' require 'matchers/json' require 'puppet/indirector/request' describe Puppet::Indirector::Request do describe "when initializing" do it "should require an indirection name, a key, and a method" do lambda { Puppet::Indirector::Request.new }.should raise_error(ArgumentError) end it "should always convert the indirection name to a symbol" do Puppet::Indirector::Request.new("ind", :method, "mykey").indirection_name.should == :ind end it "should use provided value as the key if it is a string" do Puppet::Indirector::Request.new(:ind, :method, "mykey").key.should == "mykey" end it "should use provided value as the key if it is a symbol" do Puppet::Indirector::Request.new(:ind, :method, :mykey).key.should == :mykey end it "should use the name of the provided instance as its key if an instance is provided as the key instead of a string" do instance = mock 'instance', :name => "mykey" request = Puppet::Indirector::Request.new(:ind, :method, instance) request.key.should == "mykey" request.instance.should equal(instance) end it "should support options specified as a hash" do lambda { Puppet::Indirector::Request.new(:ind, :method, :key, :one => :two) }.should_not raise_error(ArgumentError) end it "should support nil options" do lambda { Puppet::Indirector::Request.new(:ind, :method, :key, nil) }.should_not raise_error(ArgumentError) end it "should support unspecified options" do lambda { Puppet::Indirector::Request.new(:ind, :method, :key) }.should_not raise_error(ArgumentError) end it "should use an empty options hash if nil was provided" do Puppet::Indirector::Request.new(:ind, :method, :key, nil).options.should == {} end it "should default to a nil node" do Puppet::Indirector::Request.new(:ind, :method, :key, nil).node.should be_nil end it "should set its node attribute if provided in the options" do Puppet::Indirector::Request.new(:ind, :method, :key, :node => "foo.com").node.should == "foo.com" end it "should default to a nil ip" do Puppet::Indirector::Request.new(:ind, :method, :key, nil).ip.should be_nil end it "should set its ip attribute if provided in the options" do Puppet::Indirector::Request.new(:ind, :method, :key, :ip => "192.168.0.1").ip.should == "192.168.0.1" end it "should default to being unauthenticated" do Puppet::Indirector::Request.new(:ind, :method, :key, nil).should_not be_authenticated end it "should set be marked authenticated if configured in the options" do Puppet::Indirector::Request.new(:ind, :method, :key, :authenticated => "eh").should be_authenticated end it "should keep its options as a hash even if a node is specified" do Puppet::Indirector::Request.new(:ind, :method, :key, :node => "eh").options.should be_instance_of(Hash) end it "should keep its options as a hash even if another option is specified" do Puppet::Indirector::Request.new(:ind, :method, :key, :foo => "bar").options.should be_instance_of(Hash) end it "should treat options other than :ip, :node, and :authenticated as options rather than attributes" do Puppet::Indirector::Request.new(:ind, :method, :key, :server => "bar").options[:server].should == "bar" end it "should normalize options to use symbols as keys" do Puppet::Indirector::Request.new(:ind, :method, :key, "foo" => "bar").options[:foo].should == "bar" end describe "and the request key is a URI" do + let(:file) { File.expand_path("/my/file with spaces") } + describe "and the URI is a 'file' URI" do before do - @request = Puppet::Indirector::Request.new(:ind, :method, "file:///my/file with spaces") + @request = Puppet::Indirector::Request.new(:ind, :method, "#{URI.unescape(Puppet::Util.path_to_uri(file).to_s)}") end it "should set the request key to the unescaped full file path" do - @request.key.should == "/my/file with spaces" + @request.key.should == file end it "should not set the protocol" do @request.protocol.should be_nil end it "should not set the port" do @request.port.should be_nil end it "should not set the server" do @request.server.should be_nil end end it "should set the protocol to the URI scheme" do Puppet::Indirector::Request.new(:ind, :method, "http://host/stuff").protocol.should == "http" end it "should set the server if a server is provided" do Puppet::Indirector::Request.new(:ind, :method, "http://host/stuff").server.should == "host" end it "should set the server and port if both are provided" do Puppet::Indirector::Request.new(:ind, :method, "http://host:543/stuff").port.should == 543 end it "should default to the masterport if the URI scheme is 'puppet'" do Puppet.settings.expects(:value).with(:masterport).returns "321" Puppet::Indirector::Request.new(:ind, :method, "puppet://host/stuff").port.should == 321 end it "should use the provided port if the URI scheme is not 'puppet'" do Puppet::Indirector::Request.new(:ind, :method, "http://host/stuff").port.should == 80 end it "should set the request key to the unescaped key part path from the URI" do Puppet::Indirector::Request.new(:ind, :method, "http://host/environment/terminus/stuff with spaces").key.should == "stuff with spaces" end it "should set the :uri attribute to the full URI" do - Puppet::Indirector::Request.new(:ind, :method, "http:///stuff").uri.should == "http:///stuff" + Puppet::Indirector::Request.new(:ind, :method, "http:///stu ff").uri.should == 'http:///stu ff' + end + + it "should not parse relative URI" do + Puppet::Indirector::Request.new(:ind, :method, "foo/bar").uri.should be_nil + end + + it "should not parse opaque URI" do + Puppet::Indirector::Request.new(:ind, :method, "mailto:joe").uri.should be_nil end end it "should allow indication that it should not read a cached instance" do Puppet::Indirector::Request.new(:ind, :method, :key, :ignore_cache => true).should be_ignore_cache end it "should default to not ignoring the cache" do Puppet::Indirector::Request.new(:ind, :method, :key).should_not be_ignore_cache end it "should allow indication that it should not not read an instance from the terminus" do Puppet::Indirector::Request.new(:ind, :method, :key, :ignore_terminus => true).should be_ignore_terminus end it "should default to not ignoring the terminus" do Puppet::Indirector::Request.new(:ind, :method, :key).should_not be_ignore_terminus end end it "should look use the Indirection class to return the appropriate indirection" do ind = mock 'indirection' Puppet::Indirector::Indirection.expects(:instance).with(:myind).returns ind request = Puppet::Indirector::Request.new(:myind, :method, :key) request.indirection.should equal(ind) end it "should use its indirection to look up the appropriate model" do ind = mock 'indirection' Puppet::Indirector::Indirection.expects(:instance).with(:myind).returns ind request = Puppet::Indirector::Request.new(:myind, :method, :key) ind.expects(:model).returns "mymodel" request.model.should == "mymodel" end it "should fail intelligently when asked to find a model but the indirection cannot be found" do Puppet::Indirector::Indirection.expects(:instance).with(:myind).returns nil request = Puppet::Indirector::Request.new(:myind, :method, :key) lambda { request.model }.should raise_error(ArgumentError) end it "should have a method for determining if the request is plural or singular" do Puppet::Indirector::Request.new(:myind, :method, :key).should respond_to(:plural?) end it "should be considered plural if the method is 'search'" do Puppet::Indirector::Request.new(:myind, :search, :key).should be_plural end it "should not be considered plural if the method is not 'search'" do Puppet::Indirector::Request.new(:myind, :find, :key).should_not be_plural end it "should use its uri, if it has one, as its string representation" do Puppet::Indirector::Request.new(:myind, :find, "foo://bar/baz").to_s.should == "foo://bar/baz" end it "should use its indirection name and key, if it has no uri, as its string representation" do Puppet::Indirector::Request.new(:myind, :find, "key") == "/myind/key" end it "should be able to return the URI-escaped key" do Puppet::Indirector::Request.new(:myind, :find, "my key").escaped_key.should == URI.escape("my key") end it "should have an environment accessor" do Puppet::Indirector::Request.new(:myind, :find, "my key", :environment => "foo").should respond_to(:environment) end it "should set its environment to an environment instance when a string is specified as its environment" do Puppet::Indirector::Request.new(:myind, :find, "my key", :environment => "foo").environment.should == Puppet::Node::Environment.new("foo") end it "should use any passed in environment instances as its environment" do env = Puppet::Node::Environment.new("foo") Puppet::Indirector::Request.new(:myind, :find, "my key", :environment => env).environment.should equal(env) end it "should use the default environment when none is provided" do Puppet::Indirector::Request.new(:myind, :find, "my key" ).environment.should equal(Puppet::Node::Environment.new) end it "should support converting its options to a hash" do Puppet::Indirector::Request.new(:myind, :find, "my key" ).should respond_to(:to_hash) end it "should include all of its attributes when its options are converted to a hash" do Puppet::Indirector::Request.new(:myind, :find, "my key", :node => 'foo').to_hash[:node].should == 'foo' end describe "when building a query string from its options" do before do @request = Puppet::Indirector::Request.new(:myind, :find, "my key") end it "should return an empty query string if there are no options" do @request.stubs(:options).returns nil @request.query_string.should == "" end it "should return an empty query string if the options are empty" do @request.stubs(:options).returns({}) @request.query_string.should == "" end it "should prefix the query string with '?'" do @request.stubs(:options).returns(:one => "two") @request.query_string.should =~ /^\?/ end it "should include all options in the query string, separated by '&'" do @request.stubs(:options).returns(:one => "two", :three => "four") @request.query_string.sub(/^\?/, '').split("&").sort.should == %w{one=two three=four}.sort end it "should ignore nil options" do @request.stubs(:options).returns(:one => "two", :three => nil) @request.query_string.should_not be_include("three") end it "should convert 'true' option values into strings" do @request.stubs(:options).returns(:one => true) @request.query_string.should == "?one=true" end it "should convert 'false' option values into strings" do @request.stubs(:options).returns(:one => false) @request.query_string.should == "?one=false" end it "should convert to a string all option values that are integers" do @request.stubs(:options).returns(:one => 50) @request.query_string.should == "?one=50" end it "should convert to a string all option values that are floating point numbers" do @request.stubs(:options).returns(:one => 1.2) @request.query_string.should == "?one=1.2" end it "should CGI-escape all option values that are strings" do escaping = CGI.escape("one two") @request.stubs(:options).returns(:one => "one two") @request.query_string.should == "?one=#{escaping}" end it "should YAML-dump and CGI-escape arrays" do escaping = CGI.escape(YAML.dump(%w{one two})) @request.stubs(:options).returns(:one => %w{one two}) @request.query_string.should == "?one=#{escaping}" end it "should convert to a string and CGI-escape all option values that are symbols" do escaping = CGI.escape("sym bol") @request.stubs(:options).returns(:one => :"sym bol") @request.query_string.should == "?one=#{escaping}" end it "should fail if options other than booleans or strings are provided" do @request.stubs(:options).returns(:one => {:one => :two}) lambda { @request.query_string }.should raise_error(ArgumentError) end end end diff --git a/spec/unit/provider/file/posix_spec.rb b/spec/unit/provider/file/posix_spec.rb new file mode 100644 index 000000000..546aab2b5 --- /dev/null +++ b/spec/unit/provider/file/posix_spec.rb @@ -0,0 +1,226 @@ +#!/usr/bin/env rspec + +require 'spec_helper' + +describe Puppet::Type.type(:file).provider(:posix), :if => Puppet.features.posix? do + include PuppetSpec::Files + + let(:path) { tmpfile('posix_file_spec') } + let(:resource) { Puppet::Type.type(:file).new :path => path, :mode => 0777, :provider => described_class.name } + let(:provider) { resource.provider } + + describe "#mode" do + it "should return a string with the higher-order bits stripped away" do + FileUtils.touch(path) + File.chmod(0644, path) + + provider.mode.should == '644' + end + + it "should return absent if the file doesn't exist" do + provider.mode.should == :absent + end + end + + describe "#mode=" do + it "should chmod the file to the specified value" do + FileUtils.touch(path) + File.chmod(0644, path) + + provider.mode = '0755' + + provider.mode.should == '755' + end + + it "should pass along any errors encountered" do + expect do + provider.mode = '644' + end.to raise_error(Puppet::Error, /failed to set mode/) + end + end + + describe "#uid2name" do + it "should return the name of the user identified by the id" do + Etc.stubs(:getpwuid).with(501).returns(Struct::Passwd.new('jilluser', nil, 501)) + + provider.uid2name(501).should == 'jilluser' + end + + it "should return the argument if it's already a name" do + provider.uid2name('jilluser').should == 'jilluser' + end + + it "should return nil if the argument is above the maximum uid" do + provider.uid2name(Puppet[:maximum_uid] + 1).should == nil + end + + it "should return nil if the user doesn't exist" do + Etc.expects(:getpwuid).raises(ArgumentError, "can't find user for 999") + + provider.uid2name(999).should == nil + end + end + + describe "#name2uid" do + it "should return the id of the user if it exists" do + passwd = Struct::Passwd.new('bobbo', nil, 502) + + Etc.stubs(:getpwnam).with('bobbo').returns(passwd) + Etc.stubs(:getpwuid).with(502).returns(passwd) + + provider.name2uid('bobbo').should == 502 + end + + it "should return the argument if it's already an id" do + provider.name2uid('503').should == 503 + end + + it "should return false if the user doesn't exist" do + Etc.stubs(:getpwnam).with('chuck').raises(ArgumentError, "can't find user for chuck") + + provider.name2uid('chuck').should == false + end + end + + describe "#owner" do + it "should return the uid of the file owner" do + FileUtils.touch(path) + owner = File.stat(path).uid + + provider.owner.should == owner + end + + it "should return absent if the file can't be statted" do + provider.owner.should == :absent + end + + it "should warn and return :silly if the value is beyond the maximum uid" do + stat = stub('stat', :uid => Puppet[:maximum_uid] + 1) + resource.stubs(:stat).returns(stat) + + provider.owner.should == :silly + @logs.should be_any {|log| log.level == :warning and log.message =~ /Apparently using negative UID/} + end + end + + describe "#owner=" do + it "should set the owner but not the group of the file" do + File.expects(:lchown).with(15, nil, resource[:path]) + + provider.owner = 15 + end + + it "should chown a link if managing links" do + resource[:links] = :manage + File.expects(:lchown).with(20, nil, resource[:path]) + + provider.owner = 20 + end + + it "should chown a link target if following links" do + resource[:links] = :follow + File.expects(:chown).with(20, nil, resource[:path]) + + provider.owner = 20 + end + + it "should pass along any error encountered setting the owner" do + File.expects(:lchown).raises(ArgumentError) + + expect { provider.owner = 25 }.to raise_error(Puppet::Error, /Failed to set owner to '25'/) + end + end + + describe "#gid2name" do + it "should return the name of the group identified by the id" do + Etc.stubs(:getgrgid).with(501).returns(Struct::Passwd.new('unicorns', nil, nil, 501)) + + provider.gid2name(501).should == 'unicorns' + end + + it "should return the argument if it's already a name" do + provider.gid2name('leprechauns').should == 'leprechauns' + end + + it "should return nil if the argument is above the maximum gid" do + provider.gid2name(Puppet[:maximum_uid] + 1).should == nil + end + + it "should return nil if the group doesn't exist" do + Etc.expects(:getgrgid).raises(ArgumentError, "can't find group for 999") + + provider.gid2name(999).should == nil + end + end + + describe "#name2gid" do + it "should return the id of the group if it exists" do + passwd = Struct::Passwd.new('penguins', nil, nil, 502) + + Etc.stubs(:getgrnam).with('penguins').returns(passwd) + Etc.stubs(:getgrgid).with(502).returns(passwd) + + provider.name2gid('penguins').should == 502 + end + + it "should return the argument if it's already an id" do + provider.name2gid('503').should == 503 + end + + it "should return false if the group doesn't exist" do + Etc.stubs(:getgrnam).with('wombats').raises(ArgumentError, "can't find group for wombats") + + provider.name2gid('wombats').should == false + end + + end + + describe "#group" do + it "should return the gid of the file group" do + FileUtils.touch(path) + group = File.stat(path).gid + + provider.group.should == group + end + + it "should return absent if the file can't be statted" do + provider.group.should == :absent + end + + it "should warn and return :silly if the value is beyond the maximum gid" do + stat = stub('stat', :gid => Puppet[:maximum_uid] + 1) + resource.stubs(:stat).returns(stat) + + provider.group.should == :silly + @logs.should be_any {|log| log.level == :warning and log.message =~ /Apparently using negative GID/} + end + end + + describe "#group=" do + it "should set the group but not the owner of the file" do + File.expects(:lchgrp).with(nil, 15, resource[:path]) + + provider.group = 15 + end + + it "should chgrp a link if managing links" do + resource[:links] = :manage + File.expects(:lchgrp).with(nil, 20, resource[:path]) + + provider.group = 20 + end + + it "should chgrp a link target if following links" do + resource[:links] = :follow + File.expects(:chgrp).with(nil, 20, resource[:path]) + + provider.group = 20 + end + + it "should pass along any error encountered setting the group" do + File.expects(:lchgrp).raises(ArgumentError) + + expect { provider.group = 25 }.to raise_error(Puppet::Error, /Failed to set group to '25'/) + end + end +end diff --git a/spec/unit/provider/file/windows_spec.rb b/spec/unit/provider/file/windows_spec.rb new file mode 100644 index 000000000..6cedeebf0 --- /dev/null +++ b/spec/unit/provider/file/windows_spec.rb @@ -0,0 +1,136 @@ +#!/usr/bin/env rspec + +require 'spec_helper' +if Puppet.features.microsoft_windows? + require 'puppet/util/windows' + class WindowsSecurity + extend Puppet::Util::Windows::Security + end +end + +describe Puppet::Type.type(:file).provider(:windows), :if => Puppet.features.microsoft_windows? do + include PuppetSpec::Files + + let(:path) { tmpfile('windows_file_spec') } + let(:resource) { Puppet::Type.type(:file).new :path => path, :mode => 0777, :provider => described_class.name } + let(:provider) { resource.provider } + + describe "#mode" do + it "should return a string with the higher-order bits stripped away" do + FileUtils.touch(path) + WindowsSecurity.set_mode(0644, path) + + provider.mode.should == '644' + end + + it "should return absent if the file doesn't exist" do + provider.mode.should == :absent + end + end + + describe "#mode=" do + it "should chmod the file to the specified value" do + FileUtils.touch(path) + WindowsSecurity.set_mode(0644, path) + + provider.mode = '0755' + + provider.mode.should == '755' + end + + it "should pass along any errors encountered" do + expect do + provider.mode = '644' + end.to raise_error(Puppet::Error, /failed to set mode/) + end + end + + describe "#id2name" do + it "should return the name of the user identified by the sid" do + result = [stub('user', :name => 'quinn')] + Puppet::Util::ADSI.stubs(:execquery).returns(result) + + provider.id2name('S-1-1-50').should == 'quinn' + end + + it "should return the argument if it's already a name" do + provider.id2name('flannigan').should == 'flannigan' + end + + it "should return nil if the user doesn't exist" do + Puppet::Util::ADSI.stubs(:execquery).returns [] + + provider.id2name('S-1-1-50').should == nil + end + end + + describe "#name2id" do + it "should return the sid of the user" do + Puppet::Util::ADSI.stubs(:execquery).returns [stub('account', :Sid => 'S-1-1-50')] + + provider.name2id('anybody').should == 'S-1-1-50' + end + + it "should return the argument if it's already a sid" do + provider.name2id('S-1-1-50').should == 'S-1-1-50' + end + + it "should return nil if the user doesn't exist" do + Puppet::Util::ADSI.stubs(:execquery).returns [] + + provider.name2id('someone').should == nil + end + end + + describe "#owner" do + it "should return the sid of the owner if the file does exist" do + FileUtils.touch(resource[:path]) + provider.stubs(:get_owner).with(resource[:path]).returns('S-1-1-50') + + provider.owner.should == 'S-1-1-50' + end + + it "should return absent if the file doesn't exist" do + provider.owner.should == :absent + end + end + + describe "#owner=" do + it "should set the owner to the specified value" do + provider.expects(:set_owner).with('S-1-1-50', resource[:path]) + provider.owner = 'S-1-1-50' + end + + it "should propagate any errors encountered when setting the owner" do + provider.stubs(:set_owner).raises(ArgumentError) + + expect { provider.owner = 'S-1-1-50' }.to raise_error(Puppet::Error, /Failed to set owner/) + end + end + + describe "#group" do + it "should return the sid of the group if the file does exist" do + FileUtils.touch(resource[:path]) + provider.stubs(:get_group).with(resource[:path]).returns('S-1-1-50') + + provider.group.should == 'S-1-1-50' + end + + it "should return absent if the file doesn't exist" do + provider.group.should == :absent + end + end + + describe "#group=" do + it "should set the group to the specified value" do + provider.expects(:set_group).with('S-1-1-50', resource[:path]) + provider.group = 'S-1-1-50' + end + + it "should propagate any errors encountered when setting the group" do + provider.stubs(:set_group).raises(ArgumentError) + + expect { provider.group = 'S-1-1-50' }.to raise_error(Puppet::Error, /Failed to set group/) + end + end +end diff --git a/spec/unit/type/file/content_spec.rb b/spec/unit/type/file/content_spec.rb index 04ec48555..b2082408a 100755 --- a/spec/unit/type/file/content_spec.rb +++ b/spec/unit/type/file/content_spec.rb @@ -1,436 +1,436 @@ #!/usr/bin/env rspec require 'spec_helper' content = Puppet::Type.type(:file).attrclass(:content) describe content do include PuppetSpec::Files before do @filename = tmpfile('testfile') @resource = Puppet::Type.type(:file).new :path => @filename File.open(@filename, 'w') {|f| f.write "initial file content"} content.stubs(:standalone?).returns(false) end describe "when determining the checksum type" do it "should use the type specified in the source checksum if a source is set" do - @resource[:source] = "/foo" + @resource[:source] = File.expand_path("/foo") @resource.parameter(:source).expects(:checksum).returns "{md5lite}eh" @content = content.new(:resource => @resource) @content.checksum_type.should == :md5lite end it "should use the type specified by the checksum parameter if no source is set" do @resource[:checksum] = :md5lite @content = content.new(:resource => @resource) @content.checksum_type.should == :md5lite end end describe "when determining the actual content to write" do it "should use the set content if available" do @content = content.new(:resource => @resource) @content.should = "ehness" @content.actual_content.should == "ehness" end it "should not use the content from the source if the source is set" do source = mock 'source' @resource.expects(:parameter).never.with(:source).returns source @content = content.new(:resource => @resource) @content.actual_content.should be_nil end end describe "when setting the desired content" do it "should make the actual content available via an attribute" do @content = content.new(:resource => @resource) @content.stubs(:checksum_type).returns "md5" @content.should = "this is some content" @content.actual_content.should == "this is some content" end it "should store the checksum as the desired content" do @content = content.new(:resource => @resource) digest = Digest::MD5.hexdigest("this is some content") @content.stubs(:checksum_type).returns "md5" @content.should = "this is some content" @content.should.must == "{md5}#{digest}" end it "should not checksum 'absent'" do @content = content.new(:resource => @resource) @content.should = :absent @content.should.must == :absent end it "should accept a checksum as the desired content" do @content = content.new(:resource => @resource) digest = Digest::MD5.hexdigest("this is some content") string = "{md5}#{digest}" @content.should = string @content.should.must == string end end describe "when retrieving the current content" do it "should return :absent if the file does not exist" do @content = content.new(:resource => @resource) @resource.expects(:stat).returns nil @content.retrieve.should == :absent end it "should not manage content on directories" do @content = content.new(:resource => @resource) stat = mock 'stat', :ftype => "directory" @resource.expects(:stat).returns stat @content.retrieve.should be_nil end it "should not manage content on links" do @content = content.new(:resource => @resource) stat = mock 'stat', :ftype => "link" @resource.expects(:stat).returns stat @content.retrieve.should be_nil end it "should always return the checksum as a string" do @content = content.new(:resource => @resource) @resource[:checksum] = :mtime stat = mock 'stat', :ftype => "file" @resource.expects(:stat).returns stat time = Time.now @resource.parameter(:checksum).expects(:mtime_file).with(@resource[:path]).returns time @content.retrieve.should == "{mtime}#{time}" end it "should return the checksum of the file if it exists and is a normal file" do @content = content.new(:resource => @resource) stat = mock 'stat', :ftype => "file" @resource.expects(:stat).returns stat @resource.parameter(:checksum).expects(:md5_file).with(@resource[:path]).returns "mysum" @content.retrieve.should == "{md5}mysum" end end describe "when testing whether the content is in sync" do before do @resource[:ensure] = :file @content = content.new(:resource => @resource) end it "should return true if the resource shouldn't be a regular file" do @resource.expects(:should_be_file?).returns false @content.should = "foo" @content.must be_safe_insync("whatever") end it "should return false if the current content is :absent" do @content.should = "foo" @content.should_not be_safe_insync(:absent) end it "should return false if the file should be a file but is not present" do @resource.expects(:should_be_file?).returns true @content.should = "foo" @content.should_not be_safe_insync(:absent) end describe "and the file exists" do before do @resource.stubs(:stat).returns mock("stat") end it "should return false if the current contents are different from the desired content" do @content.should = "some content" @content.should_not be_safe_insync("other content") end it "should return true if the sum for the current contents is the same as the sum for the desired content" do @content.should = "some content" @content.must be_safe_insync("{md5}" + Digest::MD5.hexdigest("some content")) end describe "and Puppet[:show_diff] is set" do before do Puppet[:show_diff] = true end it "should display a diff if the current contents are different from the desired content" do @content.should = "some content" @content.expects(:diff).returns("my diff").once @content.expects(:print).with("my diff").once @content.safe_insync?("other content") end it "should not display a diff if the sum for the current contents is the same as the sum for the desired content" do @content.should = "some content" @content.expects(:diff).never @content.safe_insync?("{md5}" + Digest::MD5.hexdigest("some content")) end end end describe "and :replace is false" do before do @resource.stubs(:replace?).returns false end it "should be insync if the file exists and the content is different" do @resource.stubs(:stat).returns mock('stat') @content.must be_safe_insync("whatever") end it "should be insync if the file exists and the content is right" do @resource.stubs(:stat).returns mock('stat') @content.must be_safe_insync("something") end it "should not be insync if the file does not exist" do @content.should = "foo" @content.should_not be_safe_insync(:absent) end end end describe "when changing the content" do before do @content = content.new(:resource => @resource) @content.should = "some content" @resource.stubs(:[]).with(:path).returns "/boo" @resource.stubs(:stat).returns "eh" end it "should use the file's :write method to write the content" do @resource.expects(:write).with(:content) @content.sync end it "should return :file_changed if the file already existed" do @resource.expects(:stat).returns "something" @resource.stubs(:write) @content.sync.should == :file_changed end it "should return :file_created if the file did not exist" do @resource.expects(:stat).returns nil @resource.stubs(:write) @content.sync.should == :file_created end end describe "when writing" do before do @content = content.new(:resource => @resource) end it "should attempt to read from the filebucket if no actual content nor source exists" do @fh = File.open(@filename, 'w') @content.should = "{md5}foo" @content.resource.bucket.class.any_instance.stubs(:getfile).returns "foo" @content.write(@fh) @fh.close end describe "from actual content" do before(:each) do @content.stubs(:actual_content).returns("this is content") end it "should write to the given file handle" do @fh.expects(:print).with("this is content") @content.write(@fh) end it "should return the current checksum value" do @resource.parameter(:checksum).expects(:sum_stream).returns "checksum" @content.write(@fh).should == "checksum" end end describe "from a file bucket" do it "should fail if a file bucket cannot be retrieved" do @content.should = "{md5}foo" @content.resource.expects(:bucket).returns nil lambda { @content.write(@fh) }.should raise_error(Puppet::Error) end it "should fail if the file bucket cannot find any content" do @content.should = "{md5}foo" bucket = stub 'bucket' @content.resource.expects(:bucket).returns bucket bucket.expects(:getfile).with("foo").raises "foobar" lambda { @content.write(@fh) }.should raise_error(Puppet::Error) end it "should write the returned content to the file" do @content.should = "{md5}foo" bucket = stub 'bucket' @content.resource.expects(:bucket).returns bucket bucket.expects(:getfile).with("foo").returns "mycontent" @fh.expects(:print).with("mycontent") @content.write(@fh) end end describe "from local source", :fails_on_windows => true do before(:each) do - @resource = Puppet::Type.type(:file).new :path => @filename, :backup => false @sourcename = tmpfile('source') + @resource = Puppet::Type.type(:file).new :path => @filename, :backup => false, :source => @sourcename + @source_content = "source file content"*10000 @sourcefile = File.open(@sourcename, 'w') {|f| f.write @source_content} @content = @resource.newattr(:content) - @source = @resource.newattr(:source) - @source.stubs(:metadata).returns stub_everything('metadata', :source => @sourcename, :ftype => 'file') + @source = @resource.parameter :source #newattr(:source) end it "should copy content from the source to the file" do @resource.write(@source) File.read(@filename).should == @source_content end it "should return the checksum computed" do File.open(@filename, 'w') do |file| @content.write(file).should == "{md5}#{Digest::MD5.hexdigest(@source_content)}" end end end describe "from remote source" do before(:each) do @resource = Puppet::Type.type(:file).new :path => @filename, :backup => false @response = stub_everything 'response', :code => "200" @source_content = "source file content"*10000 @response.stubs(:read_body).multiple_yields(*(["source file content"]*10000)) @conn = stub_everything 'connection' @conn.stubs(:request_get).yields(@response) Puppet::Network::HttpPool.stubs(:http_instance).returns @conn @content = @resource.newattr(:content) @sourcename = "puppet:///test/foo" @source = @resource.newattr(:source) @source.stubs(:metadata).returns stub_everything('metadata', :source => @sourcename, :ftype => 'file') end it "should write the contents to the file" do @resource.write(@source) File.read(@filename).should == @source_content end it "should not write anything if source is not found" do @response.stubs(:code).returns("404") lambda {@resource.write(@source)}.should raise_error(Net::HTTPError) { |e| e.message =~ /404/ } File.read(@filename).should == "initial file content" end it "should raise an HTTP error in case of server error" do @response.stubs(:code).returns("500") lambda { @content.write(@fh) }.should raise_error { |e| e.message.include? @source_content } end it "should return the checksum computed" do File.open(@filename, 'w') do |file| @content.write(file).should == "{md5}#{Digest::MD5.hexdigest(@source_content)}" end end end # These are testing the implementation rather than the desired behaviour; while that bites, there are a whole # pile of other methods in the File type that depend on intimate details of this implementation and vice-versa. # If these blow up, you are gonna have to review the callers to make sure they don't explode! --daniel 2011-02-01 describe "each_chunk_from should work" do before do @content = content.new(:resource => @resource) end it "when content is a string" do @content.each_chunk_from('i_am_a_string') { |chunk| chunk.should == 'i_am_a_string' } end # The following manifest is a case where source and content.should are both set # file { "/tmp/mydir" : # source => '/tmp/sourcedir', # recurse => true, # } it "when content checksum comes from source" do source_param = Puppet::Type.type(:file).attrclass(:source) source = source_param.new(:resource => @resource) @content.should = "{md5}123abcd" @content.expects(:chunk_file_from_source).returns('from_source') @content.each_chunk_from(source) { |chunk| chunk.should == 'from_source' } end it "when no content, source, but ensure present" do @resource[:ensure] = :present @content.each_chunk_from(nil) { |chunk| chunk.should == '' } end # you might do this if you were just auditing it "when no content, source, but ensure file" do @resource[:ensure] = :file @content.each_chunk_from(nil) { |chunk| chunk.should == '' } end it "when source_or_content is nil and content not a checksum" do @content.each_chunk_from(nil) { |chunk| chunk.should == '' } end # the content is munged so that if it's a checksum nil gets passed in it "when content is a checksum it should try to read from filebucket" do @content.should = "{md5}123abcd" @content.expects(:read_file_from_filebucket).once.returns('im_a_filebucket') @content.each_chunk_from(nil) { |chunk| chunk.should == 'im_a_filebucket' } end it "when running as puppet apply" do @content.class.expects(:standalone?).returns true source_or_content = stubs('source_or_content') source_or_content.expects(:content).once.returns :whoo @content.each_chunk_from(source_or_content) { |chunk| chunk.should == :whoo } end it "when running from source with a local file" do source_or_content = stubs('source_or_content') source_or_content.expects(:local?).returns true @content.expects(:chunk_file_from_disk).with(source_or_content).once.yields 'woot' @content.each_chunk_from(source_or_content) { |chunk| chunk.should == 'woot' } end it "when running from source with a remote file" do source_or_content = stubs('source_or_content') source_or_content.expects(:local?).returns false @content.expects(:chunk_file_from_source).with(source_or_content).once.yields 'woot' @content.each_chunk_from(source_or_content) { |chunk| chunk.should == 'woot' } end end end end diff --git a/spec/unit/type/file/group_spec.rb b/spec/unit/type/file/group_spec.rb index 707a37cd6..3817eb665 100755 --- a/spec/unit/type/file/group_spec.rb +++ b/spec/unit/type/file/group_spec.rb @@ -1,122 +1,60 @@ #!/usr/bin/env rspec -require 'spec_helper' - -property = Puppet::Type.type(:file).attrclass(:group) - -describe property do - before do - @resource = stub 'resource', :line => "foo", :file => "bar" - @resource.stubs(:[]).returns "foo" - @resource.stubs(:[]).with(:path).returns "/my/file" - @group = property.new :resource => @resource - end - - it "should have a method for testing whether a group is valid" do - @group.must respond_to(:validgroup?) - end - - it "should return the found gid if a group is valid" do - @group.expects(:gid).with("foo").returns 500 - @group.validgroup?("foo").should == 500 - end - - it "should return false if a group is not valid" do - @group.expects(:gid).with("foo").returns nil - @group.validgroup?("foo").should be_false - end - - describe "when retrieving the current value" do - it "should return :absent if the file cannot stat" do - @resource.expects(:stat).returns nil - - @group.retrieve.should == :absent - end - it "should get the gid from the stat instance from the file" do - stat = stub 'stat', :ftype => "foo" - @resource.expects(:stat).returns stat - stat.expects(:gid).returns 500 - - @group.retrieve.should == 500 - end +require 'spec_helper' - it "should warn and return :silly if the found value is higher than the maximum uid value" do - Puppet.settings.expects(:value).with(:maximum_uid).returns 500 +describe Puppet::Type.type(:file).attrclass(:group) do + include PuppetSpec::Files - stat = stub 'stat', :ftype => "foo" - @resource.expects(:stat).returns stat - stat.expects(:gid).returns 1000 + let(:path) { tmpfile('mode_spec') } + let(:resource) { Puppet::Type.type(:file).new :path => path, :group => 'users' } + let(:group) { resource.property(:group) } - @group.expects(:warning) - @group.retrieve.should == :silly - end + before :each do + # If the provider was already loaded without root, it won't have the + # feature, so we have to add it here to test. + Puppet::Type.type(:file).defaultprovider.has_feature :manages_ownership end - describe "when determining if the file is in sync" do - it "should directly compare the group values if the desired group is an integer" do - @group.should = [10] - @group.must be_safe_insync(10) - end + describe "#insync?" do + before :each do + resource[:group] = ['foos', 'bars'] - it "should treat numeric strings as integers" do - @group.should = ["10"] - @group.must be_safe_insync(10) + resource.provider.stubs(:name2gid).with('foos').returns 1001 + resource.provider.stubs(:name2gid).with('bars').returns 1002 end - it "should convert the group name to an integer if the desired group is a string" do - @group.expects(:gid).with("foo").returns 10 - @group.should = %w{foo} + it "should fail if an group's id can't be found by name" do + resource.provider.stubs(:name2gid).returns nil - @group.must be_safe_insync(10) + expect { group.insync?(5) }.to raise_error(/Could not find group foos/) end - it "should not validate that groups exist when a group is specified as an integer" do - @group.expects(:gid).never - @group.validgroup?(10) + it "should use the id for comparisons, not the name" do + group.insync?('foos').should be_false end - it "should fail if it cannot convert a group name to an integer" do - @group.expects(:gid).with("foo").returns nil - @group.should = %w{foo} - - lambda { @group.safe_insync?(10) }.should raise_error(Puppet::Error) + it "should return true if the current group is one of the desired group" do + group.insync?(1001).should be_true end - it "should return false if the groups are not equal" do - @group.should = [10] - @group.should_not be_safe_insync(20) + it "should return false if the current group is not one of the desired group" do + group.insync?(1003).should be_false end end - describe "when changing the group" do - before do - @group.should = %w{one} - @group.stubs(:gid).returns 500 - end - - it "should chown the file if :links is set to :follow" do - @resource.expects(:[]).with(:links).returns :follow - File.expects(:chown) - - @group.sync - end - - it "should lchown the file if :links is set to :manage" do - @resource.expects(:[]).with(:links).returns :manage - File.expects(:lchown) - - @group.sync - end + %w[is_to_s should_to_s].each do |prop_to_s| + describe "##{prop_to_s}" do + it "should use the name of the user if it can find it" do + resource.provider.stubs(:gid2name).with(1001).returns 'foos' - it "should use the first valid group in its 'should' list" do - @group.should = %w{one two three} - @group.expects(:validgroup?).with("one").returns nil - @group.expects(:validgroup?).with("two").returns 500 - @group.expects(:validgroup?).with("three").never + group.send(prop_to_s, 1001).should == 'foos' + end - File.expects(:chown).with(nil, 500, "/my/file") + it "should use the id of the user if it can't" do + resource.provider.stubs(:gid2name).with(1001).returns nil - @group.sync + group.send(prop_to_s, 1001).should == 1001 + end end end end diff --git a/spec/unit/type/file/mode_spec.rb b/spec/unit/type/file/mode_spec.rb new file mode 100755 index 000000000..021b6127e --- /dev/null +++ b/spec/unit/type/file/mode_spec.rb @@ -0,0 +1,88 @@ +#!/usr/bin/env rspec + +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 not accept strings other than octal numbers" do + expect do + mode.value = 'readable please!' + end.to raise_error(Puppet::Error, /File modes can only be octal numbers/) + end + end + + describe "#munge" do + it "should dirmask the value when munging" do + Dir.mkdir(path) + mode.value = 0644 + + mode.value.must == '755' + end + end + + describe "#dirmask" do + before :each do + Dir.mkdir(path) + end + + # This is sort of a redundant test, but its spec is important. + it "should return the value as a string" do + mode.dirmask('0644').should be_a(String) + end + + it "should accept strings as arguments" do + mode.dirmask('0644').should == '755' + end + + it "should accept integers are arguments" do + mode.dirmask(0644).should == '755' + 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 == '644' + 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", :unless => Puppet.features.microsoft_windows? do + File.symlink('anything', path) + + mode.must be_insync('644') + end + end +end diff --git a/spec/unit/type/file/owner_spec.rb b/spec/unit/type/file/owner_spec.rb index ed3bef1fe..a57e91091 100755 --- a/spec/unit/type/file/owner_spec.rb +++ b/spec/unit/type/file/owner_spec.rb @@ -1,149 +1,58 @@ #!/usr/bin/env rspec -require 'spec_helper' - -property = Puppet::Type.type(:file).attrclass(:owner) - -describe property do - before do - # FIXME: many of these tests exercise the provider rather than `owner` - # and should be moved into provider tests. ~JW - @provider = Puppet::Type.type(:file).provider(:posix).new - @provider.stubs(:uid).with("one").returns(1) - @resource = stub 'resource', :line => "foo", :file => "bar" - @resource.stubs(:[]).returns "foo" - @resource.stubs(:[]).with(:path).returns "/my/file" - @resource.stubs(:provider).returns @provider - - @owner = property.new :resource => @resource - end +require 'spec_helper' - it "should have a method for testing whether an owner is valid" do - @provider.must respond_to(:validuser?) - end +describe Puppet::Type.type(:file).attrclass(:owner) do + include PuppetSpec::Files - it "should return the found uid if an owner is valid" do - @provider.expects(:uid).with("foo").returns 500 - @provider.validuser?("foo").should == 500 - end + let(:path) { tmpfile('mode_spec') } + let(:resource) { Puppet::Type.type(:file).new :path => path, :owner => 'joeuser' } + let(:owner) { resource.property(:owner) } - it "should return false if an owner is not valid" do - @provider.expects(:uid).with("foo").returns nil - @provider.validuser?("foo").should be_false + before :each do + Puppet.features.stubs(:root?).returns(true) end - describe "when retrieving the current value" do - it "should return :absent if the file cannot stat" do - @resource.expects(:stat).returns nil - - @owner.retrieve.should == :absent - end - - it "should get the uid from the stat instance from the file" do - stat = stub 'stat', :ftype => "foo" - @resource.expects(:stat).returns stat - stat.expects(:uid).returns 500 - - @owner.retrieve.should == 500 - end - - it "should warn and return :silly if the found value is higher than the maximum uid value" do - Puppet.settings.expects(:value).with(:maximum_uid).returns 500 - - stat = stub 'stat', :ftype => "foo" - @resource.expects(:stat).returns stat - stat.expects(:uid).returns 1000 + describe "#insync?" do + before :each do + resource[:owner] = ['foo', 'bar'] - @provider.expects(:warning) - @owner.retrieve.should == :silly + resource.provider.stubs(:name2uid).with('foo').returns 1001 + resource.provider.stubs(:name2uid).with('bar').returns 1002 end - end - - describe "when determining if the file is in sync" do - describe "and not running as root" do - it "should warn once and return true" do - Puppet.features.expects(:root?).returns false - - @provider.expects(:warnonce) - @owner.should = [10] - @owner.must be_safe_insync(20) - end - end + it "should fail if an owner's id can't be found by name" do + resource.provider.stubs(:name2uid).returns nil - before do - Puppet.features.stubs(:root?).returns true + expect { owner.insync?(5) }.to raise_error(/Could not find user foo/) end - it "should be in sync if 'should' is not provided" do - @owner.must be_safe_insync(10) + it "should use the id for comparisons, not the name" do + owner.insync?('foo').should be_false end - it "should directly compare the owner values if the desired owner is an integer" do - @owner.should = [10] - @owner.must be_safe_insync(10) + it "should return true if the current owner is one of the desired owners" do + owner.insync?(1001).should be_true end - it "should treat numeric strings as integers" do - @owner.should = ["10"] - @owner.must be_safe_insync(10) - end - - it "should convert the owner name to an integer if the desired owner is a string" do - @provider.expects(:uid).with("foo").returns 10 - @owner.should = %w{foo} - - @owner.must be_safe_insync(10) - end - - it "should not validate that users exist when a user is specified as an integer" do - @provider.expects(:uid).never - @provider.validuser?(10) - end - - it "should fail if it cannot convert an owner name to an integer" do - @provider.expects(:uid).with("foo").returns nil - @owner.should = %w{foo} - - lambda { @owner.safe_insync?(10) }.should raise_error(Puppet::Error) - end - - it "should return false if the owners are not equal" do - @owner.should = [10] - @owner.should_not be_safe_insync(20) + it "should return false if the current owner is not one of the desired owners" do + owner.insync?(1003).should be_false end end - describe "when changing the owner" do - before do - @owner.should = %w{one} - @owner.stubs(:path).returns "path" - @owner.stubs(:uid).returns 500 - end - - it "should chown the file if :links is set to :follow" do - @resource.expects(:[]).with(:links).returns :follow - File.expects(:chown) - - @owner.sync - end - - it "should lchown the file if :links is set to :manage" do - @resource.expects(:[]).with(:links).returns :manage - File.expects(:lchown) + %w[is_to_s should_to_s].each do |prop_to_s| + describe "##{prop_to_s}" do + it "should use the name of the user if it can find it" do + resource.provider.stubs(:uid2name).with(1001).returns 'foo' - @owner.sync - end - - it "should use the first valid owner in its 'should' list" do - @owner.should = %w{one two three} - @provider.expects(:validuser?).with("one").returns nil - @provider.expects(:validuser?).with("two").returns 500 - @provider.expects(:validuser?).with("three").never + owner.send(prop_to_s, 1001).should == 'foo' + end - File.expects(:chown).with(500, nil, "/my/file") + it "should use the id of the user if it can't" do + resource.provider.stubs(:uid2name).with(1001).returns nil - @owner.sync + owner.send(prop_to_s, 1001).should == 1001 + end end end end diff --git a/spec/unit/type/file/source_spec.rb b/spec/unit/type/file/source_spec.rb index c696feaf8..0ade289e9 100755 --- a/spec/unit/type/file/source_spec.rb +++ b/spec/unit/type/file/source_spec.rb @@ -1,254 +1,333 @@ #!/usr/bin/env rspec require 'spec_helper' +require 'uri' source = Puppet::Type.type(:file).attrclass(:source) describe Puppet::Type.type(:file).attrclass(:source) do include PuppetSpec::Files before do # Wow that's a messy interface to the resource. @resource = stub 'resource', :[]= => nil, :property => nil, :catalog => stub("catalog", :dependent_data_expired? => false), :line => 0, :file => '' - @foobar = make_absolute("/foo/bar") - @feebooz = make_absolute("/fee/booz") + @foobar = make_absolute("/foo/bar baz") + @feebooz = make_absolute("/fee/booz baz") + + @foobar_uri = URI.unescape(Puppet::Util.path_to_uri(@foobar).to_s) + @feebooz_uri = URI.unescape(Puppet::Util.path_to_uri(@feebooz).to_s) end it "should be a subclass of Parameter" do source.superclass.must == Puppet::Parameter end - describe "when initializing" do + describe "#validate" do + let(:path) { tmpfile('file_source_validate') } + let(:resource) { Puppet::Type.type(:file).new(:path => path) } + it "should fail if the set values are not URLs" do - s = source.new(:resource => @resource) URI.expects(:parse).with('foo').raises RuntimeError - lambda { s.value = %w{foo} }.must raise_error(Puppet::Error) + lambda { resource[:source] = %w{foo} }.must raise_error(Puppet::Error) end it "should fail if the URI is not a local file, file URI, or puppet URI" do - s = source.new(:resource => @resource) + lambda { resource[:source] = %w{http://foo/bar} }.must raise_error(Puppet::Error, /Cannot use URLs of type 'http' as source for fileserving/) + end + + it "should strip trailing forward slashes", :unless => Puppet.features.microsoft_windows? do + resource[:source] = "/foo/bar\\//" + resource[:source].should == %w{file:/foo/bar\\} + end + + it "should strip trailing forward and backslashes", :if => Puppet.features.microsoft_windows? do + resource[:source] = "X:/foo/bar\\//" + resource[:source].should == %w{file:/X:/foo/bar} + end + + it "should accept an array of sources" do + resource[:source] = %w{file:///foo/bar puppet://host:8140/foo/bar} + resource[:source].should == %w{file:///foo/bar puppet://host:8140/foo/bar} + end - lambda { s.value = %w{http://foo/bar} }.must raise_error(Puppet::Error) + it "should accept file path characters that are not valid in URI" do + resource[:source] = 'file:///foo bar' + end + + it "should reject relative URI sources" do + lambda { resource[:source] = 'foo/bar' }.must raise_error(Puppet::Error) + end + + it "should reject opaque sources" do + lambda { resource[:source] = 'mailto:foo@com' }.must raise_error(Puppet::Error) + end + + it "should accept URI authority component" do + resource[:source] = 'file://host/foo' + resource[:source].should == %w{file://host/foo} + end + + it "should accept when URI authority is absent" do + resource[:source] = 'file:///foo/bar' + resource[:source].should == %w{file:///foo/bar} + end + end + + describe "#munge" do + let(:path) { tmpfile('file_source_munge') } + let(:resource) { Puppet::Type.type(:file).new(:path => path) } + + it "should prefix file scheme to absolute paths" do + resource[:source] = path + resource[:source].should == [URI.unescape(Puppet::Util.path_to_uri(path).to_s)] + end + + %w[file puppet].each do |scheme| + it "should not prefix valid #{scheme} URIs" do + resource[:source] = "#{scheme}:///foo bar" + resource[:source].should == ["#{scheme}:///foo bar"] + end end end describe "when returning the metadata", :fails_on_windows => true do before do @metadata = stub 'metadata', :source= => nil end it "should return already-available metadata" do @source = source.new(:resource => @resource) @source.metadata = "foo" @source.metadata.should == "foo" end it "should return nil if no @should value is set and no metadata is available" do @source = source.new(:resource => @resource) @source.metadata.should be_nil end it "should collect its metadata using the Metadata class if it is not already set" do @source = source.new(:resource => @resource, :value => @foobar) - Puppet::FileServing::Metadata.indirection.expects(:find).with(@foobar).returns @metadata + Puppet::FileServing::Metadata.indirection.expects(:find).with(@foobar_uri).returns @metadata @source.metadata end it "should use the metadata from the first found source" do metadata = stub 'metadata', :source= => nil @source = source.new(:resource => @resource, :value => [@foobar, @feebooz]) - Puppet::FileServing::Metadata.indirection.expects(:find).with(@foobar).returns nil - Puppet::FileServing::Metadata.indirection.expects(:find).with(@feebooz).returns metadata + Puppet::FileServing::Metadata.indirection.expects(:find).with(@foobar_uri).returns nil + Puppet::FileServing::Metadata.indirection.expects(:find).with(@feebooz_uri).returns metadata @source.metadata.should equal(metadata) end it "should store the found source as the metadata's source" do metadata = mock 'metadata' @source = source.new(:resource => @resource, :value => @foobar) - Puppet::FileServing::Metadata.indirection.expects(:find).with(@foobar).returns metadata + Puppet::FileServing::Metadata.indirection.expects(:find).with(@foobar_uri).returns metadata - metadata.expects(:source=).with(@foobar) + metadata.expects(:source=).with(@foobar_uri) @source.metadata end it "should fail intelligently if an exception is encountered while querying for metadata" do @source = source.new(:resource => @resource, :value => @foobar) - Puppet::FileServing::Metadata.indirection.expects(:find).with(@foobar).raises RuntimeError + Puppet::FileServing::Metadata.indirection.expects(:find).with(@foobar_uri).raises RuntimeError @source.expects(:fail).raises ArgumentError lambda { @source.metadata }.should raise_error(ArgumentError) end it "should fail if no specified sources can be found" do @source = source.new(:resource => @resource, :value => @foobar) - Puppet::FileServing::Metadata.indirection.expects(:find).with(@foobar).returns nil + Puppet::FileServing::Metadata.indirection.expects(:find).with(@foobar_uri).returns nil @source.expects(:fail).raises RuntimeError lambda { @source.metadata }.should raise_error(RuntimeError) end end it "should have a method for setting the desired values on the resource" do source.new(:resource => @resource).must respond_to(:copy_source_values) end describe "when copying the source values" do before do @resource = Puppet::Type.type(:file).new :path => @foobar @source = source.new(:resource => @resource) @metadata = stub 'metadata', :owner => 100, :group => 200, :mode => 123, :checksum => "{md5}asdfasdf", :ftype => "file" @source.stubs(:metadata).returns @metadata end it "should fail if there is no metadata" do @source.stubs(:metadata).returns nil @source.expects(:devfail).raises ArgumentError lambda { @source.copy_source_values }.should raise_error(ArgumentError) end it "should set :ensure to the file type" do @metadata.stubs(:ftype).returns "file" @source.copy_source_values @resource[:ensure].must == :file end it "should not set 'ensure' if it is already set to 'absent'" do @metadata.stubs(:ftype).returns "file" @resource[:ensure] = :absent @source.copy_source_values @resource[:ensure].must == :absent end describe "and the source is a file" do before do @metadata.stubs(:ftype).returns "file" end it "should copy the metadata's owner, group, checksum, and mode to the resource if they are not set on the resource" do Puppet.features.expects(:root?).returns true @source.copy_source_values @resource[:owner].must == 100 @resource[:group].must == 200 @resource[:mode].must == "173" # Metadata calls it checksum, we call it content. @resource[:content].must == @metadata.checksum end it "should not copy the metadata's owner to the resource if it is already set" do @resource[:owner] = 1 @resource[:group] = 2 @resource[:mode] = 3 @resource[:content] = "foobar" @source.copy_source_values @resource[:owner].must == 1 @resource[:group].must == 2 @resource[:mode].must == "3" @resource[:content].should_not == @metadata.checksum end describe "and puppet is not running as root" do it "should not try to set the owner" do Puppet.features.expects(:root?).returns false @source.copy_source_values @resource[:owner].should be_nil end end end describe "and the source is a link" do it "should set the target to the link destination" do @metadata.stubs(:ftype).returns "link" @metadata.stubs(:links).returns "manage" @resource.stubs(:[]) @resource.stubs(:[]=) @metadata.expects(:destination).returns "/path/to/symlink" @resource.expects(:[]=).with(:target, "/path/to/symlink") @source.copy_source_values end end end it "should have a local? method" do source.new(:resource => @resource).must be_respond_to(:local?) end context "when accessing source properties" do - before(:each) do - @source = source.new(:resource => @resource) - @metadata = stub_everything - @source.stubs(:metadata).returns(@metadata) - end + let(:path) { tmpfile('file_resource') } + let(:resource) { Puppet::Type.type(:file).new(:path => path) } + let(:sourcepath) { tmpfile('file_source') } describe "for local sources" do - before(:each) do - @metadata.stubs(:ftype).returns "file" - @metadata.stubs(:source).returns("file:///path/to/source") + before :each do + FileUtils.touch(sourcepath) end - it "should be local" do - @source.must be_local + describe "on POSIX systems", :if => Puppet.features.posix? do + ['', "file:", "file://"].each do |prefix| + it "with prefix '#{prefix}' should be local" do + resource[:source] = "#{prefix}#{sourcepath}" + resource.parameter(:source).must be_local + end + + it "should be able to return the metadata source full path" do + resource[:source] = "#{prefix}#{sourcepath}" + resource.parameter(:source).full_path.should == sourcepath + end + end end - it "should be local if there is no scheme" do - @metadata.stubs(:source).returns("/path/to/source") - @source.must be_local - end + describe "on Windows systems", :if => Puppet.features.microsoft_windows? do + ['', "file:/", "file:///"].each do |prefix| + it "should be local with prefix '#{prefix}'" do + resource[:source] = "#{prefix}#{sourcepath}" + resource.parameter(:source).must be_local + end + + it "should be able to return the metadata source full path" do + resource[:source] = "#{prefix}#{sourcepath}" + resource.parameter(:source).full_path.should == sourcepath + end + + it "should convert backslashes to forward slashes" do + resource[:source] = "#{prefix}#{sourcepath.gsub(/\\/, '/')}" + end + end - it "should be able to return the metadata source full path" do - @source.full_path.should == "/path/to/source" + it "should be UNC with two slashes" end end describe "for remote sources" do + let(:sourcepath) { "/path/to/source" } + let(:uri) { URI::Generic.build(:scheme => 'puppet', :host => 'server', :port => 8192, :path => sourcepath).to_s } + before(:each) do - @metadata.stubs(:ftype).returns "file" - @metadata.stubs(:source).returns("puppet://server:8192/path/to/source") + metadata = Puppet::FileServing::Metadata.new(path, :source => uri, 'type' => 'file') + #metadata = stub('remote', :ftype => "file", :source => uri) + Puppet::FileServing::Metadata.indirection.stubs(:find).with(uri).returns metadata + resource[:source] = uri end it "should not be local" do - @source.should_not be_local + resource.parameter(:source).should_not be_local end it "should be able to return the metadata source full path" do - @source.full_path.should == "/path/to/source" + resource.parameter(:source).full_path.should == "/path/to/source" end it "should be able to return the source server" do - @source.server.should == "server" + resource.parameter(:source).server.should == "server" end it "should be able to return the source port" do - @source.port.should == 8192 + resource.parameter(:source).port.should == 8192 end describe "which don't specify server or port" do - before(:each) do - @metadata.stubs(:source).returns("puppet:///path/to/source") - end + let(:uri) { "puppet:///path/to/source" } it "should return the default source server" do Puppet.settings.expects(:[]).with(:server).returns("myserver") - @source.server.should == "myserver" + resource.parameter(:source).server.should == "myserver" end it "should return the default source port" do Puppet.settings.expects(:[]).with(:masterport).returns(1234) - @source.port.should == 1234 + resource.parameter(:source).port.should == 1234 end end end end end diff --git a/spec/unit/type/file_spec.rb b/spec/unit/type/file_spec.rb index 0041ce9f2..3efd5a362 100755 --- a/spec/unit/type/file_spec.rb +++ b/spec/unit/type/file_spec.rb @@ -1,1194 +1,1474 @@ #!/usr/bin/env rspec require 'spec_helper' describe Puppet::Type.type(:file) do + include PuppetSpec::Files + + let(:path) { tmpfile('file_testing') } + let(:file) { described_class.new(:path => path, :catalog => catalog) } + let(:provider) { file.provider } + let(:catalog) { Puppet::Resource::Catalog.new } + before do - Puppet.settings.stubs(:use) @real_posix = Puppet.features.posix? Puppet.features.stubs("posix?").returns(true) - - @path = Tempfile.new("puppetspec") - pathname = @path.path - @path.close!() - @path = pathname - @file = Puppet::Type::File.new(:name => @path) - - @catalog = Puppet::Resource::Catalog.new - @file.catalog = @catalog end - describe "when determining if recursion is enabled" do - it "should default to recursion being disabled" do - @file.should_not be_recurse - end - [true, "true", 10, "inf", "remote"].each do |value| - it "should consider #{value} to enable recursion" do - @file[:recurse] = value - @file.must be_recurse + describe "the path parameter" do + describe "on POSIX systems", :if => Puppet.features.posix? do + it "should remove trailing slashes" do + file[:path] = "/foo/bar/baz/" + file[:path].should == "/foo/bar/baz" end - end - [false, "false", 0].each do |value| - it "should consider #{value} to disable recursion" do - @file[:recurse] = value - @file.should_not be_recurse + it "should remove double slashes" do + file[:path] = "/foo/bar//baz" + file[:path].should == "/foo/bar/baz" end - end - end - - describe "#write" do - it "should propagate failures encountered when renaming the temporary file" do - File.stubs(:open) - File.expects(:rename).raises ArgumentError - file = Puppet::Type::File.new(:name => "/my/file", :backup => "puppet") + it "should remove trailing double slashes" do + file[:path] = "/foo/bar/baz//" + file[:path].should == "/foo/bar/baz" + end - file.stubs(:validate_checksum?).returns(false) + it "should leave a single slash alone" do + file[:path] = "/" + file[:path].should == "/" + end - property = stub('content_property', :actual_content => "something", :length => "something".length) - file.stubs(:property).with(:content).returns(property) + it "should accept a double-slash at the start of the path" do + expect { + file[:path] = "//tmp/xxx" + # REVISIT: This should be wrong, later. See the next test. + # --daniel 2011-01-31 + file[:path].should == '/tmp/xxx' + }.should_not raise_error + end - lambda { file.write(:content) }.should raise_error(Puppet::Error) + # REVISIT: This is pending, because I don't want to try and audit the + # entire codebase to make sure we get this right. POSIX treats two (and + # exactly two) '/' characters at the start of the path specially. + # + # See sections 3.2 and 4.11, which allow DomainOS to be all special like + # and still have the POSIX branding and all. --daniel 2011-01-31 + it "should preserve the double-slash at the start of the path" end - it "should delegate writing to the content property" do - filehandle = stub_everything 'fh' - File.stubs(:open).yields(filehandle) - File.stubs(:rename) - property = stub('content_property', :actual_content => "something", :length => "something".length) - file = Puppet::Type::File.new(:name => "/my/file", :backup => "puppet") - file.stubs(:validate_checksum?).returns(false) - file.stubs(:property).with(:content).returns(property) + describe "on Windows systems", :if => Puppet.features.microsoft_windows? do + it "should remove trailing slashes" do + file[:path] = "X:/foo/bar/baz/" + file[:path].should == "X:/foo/bar/baz" + end - property.expects(:write).with(filehandle) + it "should remove double slashes" do + file[:path] = "X:/foo/bar//baz" + file[:path].should == "X:/foo/bar/baz" + end - file.write(:content) - end + it "should remove trailing double slashes" do + file[:path] = "X:/foo/bar/baz//" + file[:path].should == "X:/foo/bar/baz" + end - describe "when validating the checksum" do - before { @file.stubs(:validate_checksum?).returns(true) } + it "should leave a drive letter with a slash alone", :'fails_on_ruby_1.9.2' => true do + file[:path] = "X:/" + file[:path].should == "X:/" + end - it "should fail if the checksum parameter and content checksums do not match" do - checksum = stub('checksum_parameter', :sum => 'checksum_b', :sum_file => 'checksum_b') - @file.stubs(:parameter).with(:checksum).returns(checksum) + it "should not accept a drive letter without a slash", :'fails_on_ruby_1.9.2' => true do + lambda { file[:path] = "X:" }.should raise_error(/File paths must be fully qualified/) + end - property = stub('content_property', :actual_content => "something", :length => "something".length, :write => 'checksum_a') - @file.stubs(:property).with(:content).returns(property) + describe "when using UNC filenames", :if => Puppet.features.microsoft_windows?, :'fails_on_ruby_1.9.2' => true do + before :each do + pending("UNC file paths not yet supported") + end - lambda { @file.write :NOTUSED }.should raise_error(Puppet::Error) - end - end + it "should remove trailing slashes" do + file[:path] = "//server/foo/bar/baz/" + file[:path].should == "//server/foo/bar/baz" + end - describe "when not validating the checksum" do - before { @file.stubs(:validate_checksum?).returns(false) } + it "should remove double slashes" do + file[:path] = "//server/foo/bar//baz" + file[:path].should == "//server/foo/bar/baz" + end - it "should not fail if the checksum property and content checksums do not match" do - checksum = stub('checksum_parameter', :sum => 'checksum_b') - @file.stubs(:parameter).with(:checksum).returns(checksum) + it "should remove trailing double slashes" do + file[:path] = "//server/foo/bar/baz//" + file[:path].should == "//server/foo/bar/baz" + end - property = stub('content_property', :actual_content => "something", :length => "something".length, :write => 'checksum_a') - @file.stubs(:property).with(:content).returns(property) + it "should remove a trailing slash from a sharename" do + file[:path] = "//server/foo/" + file[:path].should == "//server/foo" + end - lambda { @file.write :NOTUSED }.should_not raise_error(Puppet::Error) + it "should not modify a sharename" do + file[:path] = "//server/foo" + file[:path].should == "//server/foo" + end end end end - it "should have a method for determining if the file is present" do - @file.must respond_to(:exist?) - end + describe "the backup parameter" do + [false, 'false', :false].each do |value| + it "should disable backup if the value is #{value.inspect}" do + file[:backup] = value + file[:backup].should == false + end + end - it "should be considered existent if it can be stat'ed" do - @file.expects(:stat).returns mock('stat') - @file.must be_exist - end + [true, 'true', '.puppet-bak'].each do |value| + it "should use .puppet-bak if the value is #{value.inspect}" do + file[:backup] = value + file[:backup].should == '.puppet-bak' + end + end - it "should be considered nonexistent if it can not be stat'ed" do - @file.expects(:stat).returns nil - @file.must_not be_exist - end + it "should use the provided value if it's any other string" do + file[:backup] = "over there" + file[:backup].should == "over there" + end - it "should have a method for determining if the file should be a normal file" do - @file.must respond_to(:should_be_file?) + it "should fail if backup is set to anything else" do + expect do + file[:backup] = 97 + end.to raise_error(Puppet::Error, /Invalid backup type 97/) + end end - it "should be a file if :ensure is set to :file" do - @file[:ensure] = :file - @file.must be_should_be_file - end + describe "the recurse parameter" do + it "should default to recursion being disabled" do + file[:recurse].should be_false + end - it "should be a file if :ensure is set to :present and the file exists as a normal file" do - @file.stubs(:stat).returns(mock('stat', :ftype => "file")) - @file[:ensure] = :present - @file.must be_should_be_file - end + [true, "true", 10, "inf", "remote"].each do |value| + it "should consider #{value} to enable recursion" do + file[:recurse] = value + file[:recurse].should be_true + end + end - it "should not be a file if :ensure is set to something other than :file" do - @file[:ensure] = :directory - @file.must_not be_should_be_file - end + [false, "false", 0].each do |value| + it "should consider #{value} to disable recursion" do + file[:recurse] = value + file[:recurse].should be_false + end + end - it "should not be a file if :ensure is set to :present and the file exists but is not a normal file" do - @file.stubs(:stat).returns(mock('stat', :ftype => "directory")) - @file[:ensure] = :present - @file.must_not be_should_be_file + it "should warn if recurse is specified as a number" do + file[:recurse] = 3 + message = /Setting recursion depth with the recurse parameter is now deprecated, please use recurselimit/ + @logs.find { |log| log.level == :warning and log.message =~ message}.should_not be_nil + end end - it "should be a file if :ensure is not set and :content is" do - @file[:content] = "foo" - @file.must be_should_be_file - end + describe "the recurselimit parameter" do + it "should accept integers" do + file[:recurselimit] = 12 + file[:recurselimit].should == 12 + end - it "should be a file if neither :ensure nor :content is set but the file exists as a normal file" do - @file.stubs(:stat).returns(mock("stat", :ftype => "file")) - @file.must be_should_be_file - end + it "should munge string numbers to number numbers" do + file[:recurselimit] = '12' + file[:recurselimit].should == 12 + end - it "should not be a file if neither :ensure nor :content is set but the file exists but not as a normal file" do - @file.stubs(:stat).returns(mock("stat", :ftype => "directory")) - @file.must_not be_should_be_file + it "should fail if given a non-number" do + expect do + file[:recurselimit] = 'twelve' + end.to raise_error(Puppet::Error, /Invalid value "twelve"/) + end end - describe "when using POSIX filenames" do - it "should autorequire its parent directory" do - file = Puppet::Type::File.new(:path => "/foo/bar") - dir = Puppet::Type::File.new(:path => "/foo") - @catalog.add_resource file - @catalog.add_resource dir - reqs = file.autorequire - reqs[0].source.must == dir - reqs[0].target.must == file + describe "the replace parameter" do + [true, :true, :yes].each do |value| + it "should consider #{value} to be true" do + file[:replace] = value + file[:replace].should == :true + end end - it "should autorequire its nearest ancestor directory" do - file = Puppet::Type::File.new(:path => "/foo/bar/baz") - dir = Puppet::Type::File.new(:path => "/foo") - root = Puppet::Type::File.new(:path => "/") - @catalog.add_resource file - @catalog.add_resource dir - @catalog.add_resource root - reqs = file.autorequire - reqs.length.must == 1 - reqs[0].source.must == dir - reqs[0].target.must == file + [false, :false, :no].each do |value| + it "should consider #{value} to be false" do + file[:replace] = value + file[:replace].should == :false + end end + end - it "should not autorequire anything when there is no nearest ancestor directory" do - file = Puppet::Type::File.new(:path => "/foo/bar/baz") - @catalog.add_resource file - file.autorequire.should be_empty + describe "#[]" do + it "should raise an exception" do + expect do + described_class['anything'] + end.to raise_error("Global resource access is deprecated") end + end - it "should not autorequire its parent dir if its parent dir is itself" do - file = Puppet::Type::File.new(:path => "/") - @catalog.add_resource file - file.autorequire.should be_empty + describe ".instances" do + it "should return an empty array" do + described_class.instances.should == [] end + end - it "should remove trailing slashes" do - file = Puppet::Type::File.new(:path => "/foo/bar/baz/") - file[:path].should == "/foo/bar/baz" + describe "#asuser" do + before :each do + # Mocha won't let me just stub SUIDManager.asuser to yield and return, + # but it will do exactly that if we're not root. + Puppet.features.stubs(:root?).returns false end - it "should remove double slashes" do - file = Puppet::Type::File.new(:path => "/foo/bar//baz") - file[:path].should == "/foo/bar/baz" - end + it "should return the desired owner if they can write to the parent directory" do + file[:owner] = 1001 + FileTest.stubs(:writable?).with(File.dirname file[:path]).returns true - it "should remove trailing double slashes" do - file = Puppet::Type::File.new(:path => "/foo/bar/baz//") - file[:path].should == "/foo/bar/baz" + file.asuser.should == 1001 end - it "should leave a single slash alone" do - file = Puppet::Type::File.new(:path => "/") - file[:path].should == "/" - end + it "should return nil if the desired owner can't write to the parent directory" do + file[:owner] = 1001 + FileTest.stubs(:writable?).with(File.dirname file[:path]).returns false - it "should accept a double-slash at the start of the path" do - expect { - file = Puppet::Type::File.new(:path => "//tmp/xxx") - # REVISIT: This should be wrong, later. See the next test. - # --daniel 2011-01-31 - file[:path].should == '/tmp/xxx' - }.should_not raise_error + file.asuser.should == nil end - # REVISIT: This is pending, because I don't want to try and audit the - # entire codebase to make sure we get this right. POSIX treats two (and - # exactly two) '/' characters at the start of the path specially. - # - # See sections 3.2 and 4.11, which allow DomainOS to be all special like - # and still have the POSIX branding and all. --daniel 2011-01-31 - it "should preserve the double-slash at the start of the path" + it "should return nil if not managing owner" do + file.asuser.should == nil + end end - describe "when using Microsoft Windows filenames" do - it "should autorequire its parent directory" do - file = Puppet::Type::File.new(:path => "X:/foo/bar") - dir = Puppet::Type::File.new(:path => "X:/foo") - @catalog.add_resource file - @catalog.add_resource dir - reqs = file.autorequire - reqs[0].source.must == dir - reqs[0].target.must == file + describe "#bucket" do + it "should return nil if backup is off" do + file[:backup] = false + file.bucket.should == nil end - it "should autorequire its nearest ancestor directory" do - file = Puppet::Type::File.new(:path => "X:/foo/bar/baz") - dir = Puppet::Type::File.new(:path => "X:/foo") - root = Puppet::Type::File.new(:path => "X:/") - @catalog.add_resource file - @catalog.add_resource dir - @catalog.add_resource root - reqs = file.autorequire - reqs.length.must == 1 - reqs[0].source.must == dir - reqs[0].target.must == file - end + it "should not return a bucket if using a file extension for backup" do + file[:backup] = '.backup' - it "should not autorequire anything when there is no nearest ancestor directory" do - file = Puppet::Type::File.new(:path => "X:/foo/bar/baz") - @catalog.add_resource file - file.autorequire.should be_empty + file.bucket.should == nil end - it "should not autorequire its parent dir if its parent dir is itself" do - file = Puppet::Type::File.new(:path => "X:/") - @catalog.add_resource file - file.autorequire.should be_empty - end + it "should return the default filebucket if using the 'puppet' filebucket" do + file[:backup] = 'puppet' + bucket = stub('bucket') + file.stubs(:default_bucket).returns bucket - it "should remove trailing slashes" do - file = Puppet::Type::File.new(:path => "X:/foo/bar/baz/") - file[:path].should == "X:/foo/bar/baz" + file.bucket.should == bucket end - it "should remove double slashes" do - file = Puppet::Type::File.new(:path => "X:/foo/bar//baz") - file[:path].should == "X:/foo/bar/baz" - end + it "should fail if using a remote filebucket and no catalog exists" do + file.catalog = nil + file[:backup] = 'my_bucket' - it "should remove trailing double slashes" do - file = Puppet::Type::File.new(:path => "X:/foo/bar/baz//") - file[:path].should == "X:/foo/bar/baz" + expect { file.bucket }.to raise_error(Puppet::Error, "Can not find filebucket for backups without a catalog") end - it "should leave a drive letter with a slash alone", :'fails_on_ruby_1.9.2' => true do - file = Puppet::Type::File.new(:path => "X:/") - file[:path].should == "X:/" + it "should fail if the specified filebucket isn't in the catalog" do + file[:backup] = 'my_bucket' + + expect { file.bucket }.to raise_error(Puppet::Error, "Could not find filebucket my_bucket specified in backup") end - it "should not accept a drive letter without a slash", :'fails_on_ruby_1.9.2' => true do - lambda { Puppet::Type::File.new(:path => "X:") }.should raise_error(/File paths must be fully qualified/) + it "should use the specified filebucket if it is in the catalog" do + file[:backup] = 'my_bucket' + filebucket = Puppet::Type.type(:filebucket).new(:name => 'my_bucket') + catalog.add_resource(filebucket) + + file.bucket.should == filebucket.bucket end end - describe "when using UNC filenames", :'fails_on_ruby_1.9.2' => true do + describe "#asuser" do before :each do - pending("UNC file paths not yet supported") + # Mocha won't let me just stub SUIDManager.asuser to yield and return, + # but it will do exactly that if we're not root. + Puppet.features.stubs(:root?).returns false end - it "should autorequire its parent directory" do - file = Puppet::Type::File.new(:path => "//server/foo/bar") - dir = Puppet::Type::File.new(:path => "//server/foo") - @catalog.add_resource file - @catalog.add_resource dir - reqs = file.autorequire - reqs[0].source.must == dir - reqs[0].target.must == file + it "should return the desired owner if they can write to the parent directory" do + file[:owner] = 1001 + FileTest.stubs(:writable?).with(File.dirname file[:path]).returns true + + file.asuser.should == 1001 end - it "should autorequire its nearest ancestor directory" do - file = Puppet::Type::File.new(:path => "//server/foo/bar/baz/qux") - dir = Puppet::Type::File.new(:path => "//server/foo/bar") - root = Puppet::Type::File.new(:path => "//server/foo") - @catalog.add_resource file - @catalog.add_resource dir - @catalog.add_resource root - reqs = file.autorequire - reqs.length.must == 1 - reqs[0].source.must == dir - reqs[0].target.must == file + it "should return nil if the desired owner can't write to the parent directory" do + file[:owner] = 1001 + FileTest.stubs(:writable?).with(File.dirname file[:path]).returns false + + file.asuser.should == nil end - it "should not autorequire anything when there is no nearest ancestor directory" do - file = Puppet::Type::File.new(:path => "//server/foo/bar/baz/qux") - @catalog.add_resource file - file.autorequire.should be_empty + it "should return nil if not managing owner" do + file.asuser.should == nil end + end - it "should not autorequire its parent dir if its parent dir is itself" do - file = Puppet::Type::File.new(:path => "//server/foo") - @catalog.add_resource file - puts file.autorequire - file.autorequire.should be_empty + describe "#bucket" do + it "should return nil if backup is off" do + file[:backup] = false + file.bucket.should == nil end - it "should remove trailing slashes" do - file = Puppet::Type::File.new(:path => "//server/foo/bar/baz/") - file[:path].should == "//server/foo/bar/baz" + it "should return nil if using a file extension for backup" do + file[:backup] = '.backup' + + file.bucket.should == nil end - it "should remove double slashes" do - file = Puppet::Type::File.new(:path => "//server/foo/bar//baz") - file[:path].should == "//server/foo/bar/baz" + it "should return the default filebucket if using the 'puppet' filebucket" do + file[:backup] = 'puppet' + bucket = stub('bucket') + file.stubs(:default_bucket).returns bucket + + file.bucket.should == bucket end - it "should remove trailing double slashes" do - file = Puppet::Type::File.new(:path => "//server/foo/bar/baz//") - file[:path].should == "//server/foo/bar/baz" + it "should fail if using a remote filebucket and no catalog exists" do + file.catalog = nil + file[:backup] = 'my_bucket' + + expect { file.bucket }.to raise_error(Puppet::Error, "Can not find filebucket for backups without a catalog") end - it "should remove a trailing slash from a sharename" do - file = Puppet::Type::File.new(:path => "//server/foo/") - file[:path].should == "//server/foo" + it "should fail if the specified filebucket isn't in the catalog" do + file[:backup] = 'my_bucket' + + expect { file.bucket }.to raise_error(Puppet::Error, "Could not find filebucket my_bucket specified in backup") end - it "should not modify a sharename" do - file = Puppet::Type::File.new(:path => "//server/foo") - file[:path].should == "//server/foo" + it "should use the specified filebucket if it is in the catalog" do + file[:backup] = 'my_bucket' + filebucket = Puppet::Type.type(:filebucket).new(:name => 'my_bucket') + catalog.add_resource(filebucket) + + file.bucket.should == filebucket.bucket end end - describe "when initializing" do - it "should set a desired 'ensure' value if none is set and 'content' is set" do - file = Puppet::Type::File.new(:name => "/my/file", :content => "/foo/bar") - file[:ensure].should == :file + describe "#exist?" do + it "should be considered existent if it can be stat'ed" do + file.expects(:stat).returns mock('stat') + file.must be_exist end - it "should set a desired 'ensure' value if none is set and 'target' is set" do - file = Puppet::Type::File.new(:name => "/my/file", :target => "/foo/bar") - file[:ensure].should == :symlink + it "should be considered nonexistent if it can not be stat'ed" do + file.expects(:stat).returns nil + file.must_not be_exist end end - describe "when validating attributes" do - %w{path checksum backup recurse recurselimit source replace force ignore links purge sourceselect}.each do |attr| - it "should have a '#{attr}' parameter" do - Puppet::Type.type(:file).attrtype(attr.intern).should == :param - end + describe "#eval_generate" do + before do + @graph = stub 'graph', :add_edge => nil + catalog.stubs(:relationship_graph).returns @graph end - %w{content target ensure owner group mode type}.each do |attr| - it "should have a '#{attr}' property" do - Puppet::Type.type(:file).attrtype(attr.intern).should == :property - end - end + it "should recurse if recursion is enabled" do + resource = stub('resource', :[] => 'resource') + file.expects(:recurse).returns [resource] + + file[:recurse] = true - it "should have its 'path' attribute set as its namevar" do - Puppet::Type.type(:file).key_attributes.should == [:path] + file.eval_generate.should == [resource] end - end - describe "when managing links" do - require 'tempfile' + it "should not recurse if recursion is disabled" do + file.expects(:recurse).never - if @real_posix - describe "on POSIX systems" do - before do - @basedir = tempfile - Dir.mkdir(@basedir) - @file = File.join(@basedir, "file") - @link = File.join(@basedir, "link") + file[:recurse] = false - File.open(@file, "w", 0644) { |f| f.puts "yayness"; f.flush } - File.symlink(@file, @link) + file.eval_generate.should == [] + end + end - @resource = Puppet::Type.type(:file).new(:path => @link, :mode => "755") - @catalog.add_resource @resource - end + describe "#flush" do + it "should flush all properties that respond to :flush" do + file[:source] = File.expand_path(__FILE__) + file.parameter(:source).expects(:flush) + file.flush + end - after do - remove_tmp_files - end + it "should reset its stat reference" do + FileUtils.touch(path) + stat1 = file.stat - it "should default to managing the link" do - @catalog.apply - # I convert them to strings so they display correctly if there's an error. - ("%o" % (File.stat(@file).mode & 007777)).should == "%o" % 0644 - end + file.stat.should equal(stat1) - it "should be able to follow links" do - @resource[:links] = :follow - @catalog.apply + file.flush - ("%o" % (File.stat(@file).mode & 007777)).should == "%o" % 0755 - end - end - else # @real_posix - # should recode tests using expectations instead of using the filesystem + file.stat.should_not equal(stat1) end + end - describe "on Microsoft Windows systems" do - before do - Puppet.features.stubs(:posix?).returns(false) - Puppet.features.stubs(:microsoft_windows?).returns(true) - end + describe "#initialize" do + it "should remove a trailing slash from the title to create the path" do + title = File.expand_path("/abc/\n\tdef/") + file = described_class.new(:title => title) + file[:path].should == title + end - it "should refuse to work with links" + it "should set a desired 'ensure' value if none is set and 'content' is set" do + file = described_class.new(:path => path, :content => "/foo/bar") + file[:ensure].should == :file end - end - it "should be able to retrieve a stat instance for the file it is managing" do - Puppet::Type.type(:file).new(:path => "/foo/bar", :source => "/bar/foo").should respond_to(:stat) + it "should set a desired 'ensure' value if none is set and 'target' is set" do + file = described_class.new(:path => path, :target => File.expand_path(__FILE__)) + file[:ensure].should == :symlink + end end - describe "when stat'ing its file" do - before do - @resource = Puppet::Type.type(:file).new(:path => "/foo/bar") - @resource[:links] = :manage # so we always use :lstat - end + describe "#mark_children_for_purging" do + it "should set each child's ensure to absent" do + paths = %w[foo bar baz] + children = paths.inject({}) do |children,child| + children.merge child => described_class.new(:path => File.join(path, child), :ensure => :present) + end - it "should use :stat if it is following links" do - @resource[:links] = :follow - File.expects(:stat) + file.mark_children_for_purging(children) - @resource.stat + children.length.should == 3 + children.values.each do |child| + child[:ensure].should == :absent + end end - it "should use :lstat if is it not following links" do - @resource[:links] = :manage - File.expects(:lstat) - - @resource.stat - end + it "should skip children which have a source" do + child = described_class.new(:path => path, :ensure => :present, :source => File.expand_path(__FILE__)) - it "should stat the path of the file" do - File.expects(:lstat).with("/foo/bar") + file.mark_children_for_purging('foo' => child) - @resource.stat + child[:ensure].should == :present end + end - # This only happens in testing. - it "should return nil if the stat does not exist" do - File.expects(:lstat).returns nil - - @resource.stat.should be_nil + describe "#newchild" do + it "should create a new resource relative to the parent" do + child = file.newchild('bar') + + child.should be_a(described_class) + child[:path].should == File.join(file[:path], 'bar') + end + + { + :ensure => :present, + :recurse => true, + :recurselimit => 5, + :target => "some_target", + :source => File.expand_path("some_source"), + }.each do |param, value| + it "should omit the #{param} parameter" do + # Make a new file, because we have to set the param at initialization + # or it wouldn't be copied regardless. + file = described_class.new(:path => path, param => value) + child = file.newchild('bar') + child[param].should_not == value + end end - it "should return nil if the file does not exist" do - File.expects(:lstat).raises(Errno::ENOENT) - - @resource.stat.should be_nil - end + it "should copy all of the parent resource's 'should' values that were set at initialization" do + parent = described_class.new(:path => path, :owner => 'root', :group => 'wheel') - it "should return nil if the file cannot be stat'ed" do - File.expects(:lstat).raises(Errno::EACCES) + child = parent.newchild("my/path") - @resource.stat.should be_nil + child[:owner].should == 'root' + child[:group].should == 'wheel' end - it "should return the stat instance" do - File.expects(:lstat).returns "mystat" - - @resource.stat.should == "mystat" + it "should not copy default values to the new child" do + child = file.newchild("my/path") + child.original_parameters.should_not include(:backup) end - it "should cache the stat instance if it has a catalog and is applying" do - stat = mock 'stat' - File.expects(:lstat).returns stat + it "should not copy values to the child which were set by the source" do + file[:source] = File.expand_path(__FILE__) + metadata = stub 'metadata', :owner => "root", :group => "root", :mode => 0755, :ftype => "file", :checksum => "{md5}whatever" + file.parameter(:source).stubs(:metadata).returns metadata - catalog = Puppet::Resource::Catalog.new - @resource.catalog = catalog + file.parameter(:source).copy_source_values - catalog.stubs(:applying?).returns true - - @resource.stat.should equal(@resource.stat) + file.class.expects(:new).with { |params| params[:group].nil? } + file.newchild("my/path") end end - describe "when flushing" do - it "should flush all properties that respond to :flush" do - @resource = Puppet::Type.type(:file).new(:path => "/foo/bar", :source => "/bar/foo") - @resource.parameter(:source).expects(:flush) - @resource.flush + describe "#purge?" do + it "should return false if purge is not set" do + file.must_not be_purge end - it "should reset its stat reference" do - @resource = Puppet::Type.type(:file).new(:path => "/foo/bar") - File.expects(:lstat).times(2).returns("stat1").then.returns("stat2") - @resource.stat.should == "stat1" - @resource.flush - @resource.stat.should == "stat2" + it "should return true if purge is set to true" do + file[:purge] = true + + file.must be_purge end - end - it "should have a method for performing recursion" do - @file.must respond_to(:perform_recursion) - end + it "should return false if purge is set to false" do + file[:purge] = false - describe "when executing a recursive search" do - it "should use Metadata to do its recursion" do - Puppet::FileServing::Metadata.indirection.expects(:search) - @file.perform_recursion(@file[:path]) + file.must_not be_purge end + end - it "should use the provided path as the key to the search" do - Puppet::FileServing::Metadata.indirection.expects(:search).with { |key, options| key == "/foo" } - @file.perform_recursion("/foo") + describe "#recurse" do + before do + file[:recurse] = true + @metadata = Puppet::FileServing::Metadata end - it "should return the results of the metadata search" do - Puppet::FileServing::Metadata.indirection.expects(:search).returns "foobar" - @file.perform_recursion(@file[:path]).should == "foobar" + describe "and a source is set" do + it "should pass the already-discovered resources to recurse_remote" do + file[:source] = File.expand_path(__FILE__) + file.stubs(:recurse_local).returns(:foo => "bar") + file.expects(:recurse_remote).with(:foo => "bar").returns [] + file.recurse + end end - it "should pass its recursion value to the search" do - @file[:recurse] = true - Puppet::FileServing::Metadata.indirection.expects(:search).with { |key, options| options[:recurse] == true } - @file.perform_recursion(@file[:path]) + describe "and a target is set" do + it "should use recurse_link" do + file[:target] = File.expand_path(__FILE__) + file.stubs(:recurse_local).returns(:foo => "bar") + file.expects(:recurse_link).with(:foo => "bar").returns [] + file.recurse + end end - it "should pass true if recursion is remote" do - @file[:recurse] = :remote - Puppet::FileServing::Metadata.indirection.expects(:search).with { |key, options| options[:recurse] == true } - @file.perform_recursion(@file[:path]) + it "should use recurse_local if recurse is not remote" do + file.expects(:recurse_local).returns({}) + file.recurse end - it "should pass its recursion limit value to the search" do - @file[:recurselimit] = 10 - Puppet::FileServing::Metadata.indirection.expects(:search).with { |key, options| options[:recurselimit] == 10 } - @file.perform_recursion(@file[:path]) + it "should not use recurse_local if recurse is remote" do + file[:recurse] = :remote + file.expects(:recurse_local).never + file.recurse end - it "should configure the search to ignore or manage links" do - @file[:links] = :manage - Puppet::FileServing::Metadata.indirection.expects(:search).with { |key, options| options[:links] == :manage } - @file.perform_recursion(@file[:path]) + it "should return the generated resources as an array sorted by file path" do + one = stub 'one', :[] => "/one" + two = stub 'two', :[] => "/one/two" + three = stub 'three', :[] => "/three" + file.expects(:recurse_local).returns(:one => one, :two => two, :three => three) + file.recurse.should == [one, two, three] end - it "should pass its 'ignore' setting to the search if it has one" do - @file[:ignore] = %w{.svn CVS} - Puppet::FileServing::Metadata.indirection.expects(:search).with { |key, options| options[:ignore] == %w{.svn CVS} } - @file.perform_recursion(@file[:path]) + describe "and purging is enabled" do + before do + file[:purge] = true + end + + it "should mark each file for removal" do + local = described_class.new(:path => path, :ensure => :present) + file.expects(:recurse_local).returns("local" => local) + + file.recurse + local[:ensure].should == :absent + end + + it "should not remove files that exist in the remote repository" do + file[:source] = File.expand_path(__FILE__) + file.expects(:recurse_local).returns({}) + + remote = described_class.new(:path => path, :source => File.expand_path(__FILE__), :ensure => :present) + + file.expects(:recurse_remote).with { |hash| hash["remote"] = remote } + + file.recurse + + remote[:ensure].should_not == :absent + end end - end - it "should have a method for performing local recursion" do - @file.must respond_to(:recurse_local) end - describe "when doing local recursion" do - before do - @metadata = stub 'metadata', :relative_path => "my/file" - end + describe "#remove_less_specific_files" do + it "should remove any nested files that are already in the catalog" do + foo = described_class.new :path => File.join(file[:path], 'foo') + bar = described_class.new :path => File.join(file[:path], 'bar') + baz = described_class.new :path => File.join(file[:path], 'baz') - it "should pass its path to the :perform_recursion method" do - @file.expects(:perform_recursion).with(@file[:path]).returns [@metadata] - @file.stubs(:newchild) - @file.recurse_local - end + catalog.add_resource(foo) + catalog.add_resource(bar) - it "should return an empty hash if the recursion returns nothing" do - @file.expects(:perform_recursion).returns nil - @file.recurse_local.should == {} + file.remove_less_specific_files([foo, bar, baz]).should == [baz] end + end - it "should create a new child resource with each generated metadata instance's relative path" do - @file.expects(:perform_recursion).returns [@metadata] - @file.expects(:newchild).with(@metadata.relative_path).returns "fiebar" - @file.recurse_local - end + describe "#remove_less_specific_files" do + it "should remove any nested files that are already in the catalog" do + foo = described_class.new :path => File.join(file[:path], 'foo') + bar = described_class.new :path => File.join(file[:path], 'bar') + baz = described_class.new :path => File.join(file[:path], 'baz') - it "should not create a new child resource for the '.' directory" do - @metadata.stubs(:relative_path).returns "." + catalog.add_resource(foo) + catalog.add_resource(bar) - @file.expects(:perform_recursion).returns [@metadata] - @file.expects(:newchild).never - @file.recurse_local + file.remove_less_specific_files([foo, bar, baz]).should == [baz] end - it "should return a hash of the created resources with the relative paths as the hash keys" do - @file.expects(:perform_recursion).returns [@metadata] - @file.expects(:newchild).with("my/file").returns "fiebar" - @file.recurse_local.should == {"my/file" => "fiebar"} + end + + describe "#recurse?" do + it "should be true if recurse is true" do + file[:recurse] = true + file.must be_recurse end - it "should set checksum_type to none if this file checksum is none" do - @file[:checksum] = :none - Puppet::FileServing::Metadata.indirection.expects(:search).with { |path,params| params[:checksum_type] == :none }.returns [@metadata] - @file.expects(:newchild).with("my/file").returns "fiebar" - @file.recurse_local + it "should be true if recurse is remote" do + file[:recurse] = :remote + file.must be_recurse end - end - it "should have a method for performing link recursion" do - @file.must respond_to(:recurse_link) + it "should be false if recurse is false" do + file[:recurse] = false + file.must_not be_recurse + end end - describe "when doing link recursion" do + describe "#recurse_link" do before do @first = stub 'first', :relative_path => "first", :full_path => "/my/first", :ftype => "directory" @second = stub 'second', :relative_path => "second", :full_path => "/my/second", :ftype => "file" @resource = stub 'file', :[]= => nil end it "should pass its target to the :perform_recursion method" do - @file[:target] = "mylinks" - @file.expects(:perform_recursion).with("mylinks").returns [@first] - @file.stubs(:newchild).returns @resource - @file.recurse_link({}) + file[:target] = "mylinks" + file.expects(:perform_recursion).with("mylinks").returns [@first] + file.stubs(:newchild).returns @resource + file.recurse_link({}) end it "should ignore the recursively-found '.' file and configure the top-level file to create a directory" do @first.stubs(:relative_path).returns "." - @file[:target] = "mylinks" - @file.expects(:perform_recursion).with("mylinks").returns [@first] - @file.stubs(:newchild).never - @file.expects(:[]=).with(:ensure, :directory) - @file.recurse_link({}) + file[:target] = "mylinks" + file.expects(:perform_recursion).with("mylinks").returns [@first] + file.stubs(:newchild).never + file.expects(:[]=).with(:ensure, :directory) + file.recurse_link({}) end it "should create a new child resource for each generated metadata instance's relative path that doesn't already exist in the children hash" do - @file.expects(:perform_recursion).returns [@first, @second] - @file.expects(:newchild).with(@first.relative_path).returns @resource - @file.recurse_link("second" => @resource) + file.expects(:perform_recursion).returns [@first, @second] + file.expects(:newchild).with(@first.relative_path).returns @resource + file.recurse_link("second" => @resource) end it "should not create a new child resource for paths that already exist in the children hash" do - @file.expects(:perform_recursion).returns [@first] - @file.expects(:newchild).never - @file.recurse_link("first" => @resource) + file.expects(:perform_recursion).returns [@first] + file.expects(:newchild).never + file.recurse_link("first" => @resource) end it "should set the target to the full path of discovered file and set :ensure to :link if the file is not a directory" do - file = stub 'file' - file.expects(:[]=).with(:target, "/my/second") - file.expects(:[]=).with(:ensure, :link) + file.stubs(:perform_recursion).returns [@first, @second] + file.recurse_link("first" => @resource, "second" => file) - @file.stubs(:perform_recursion).returns [@first, @second] - @file.recurse_link("first" => @resource, "second" => file) + file[:ensure].should == :link + file[:target].should == "/my/second" end it "should :ensure to :directory if the file is a directory" do - file = stub 'file' - file.expects(:[]=).with(:ensure, :directory) + file.stubs(:perform_recursion).returns [@first, @second] + file.recurse_link("first" => file, "second" => @resource) + + file[:ensure].should == :directory + end + + it "should return a hash with both created and existing resources with the relative paths as the hash keys" do + file.expects(:perform_recursion).returns [@first, @second] + file.stubs(:newchild).returns file + file.recurse_link("second" => @resource).should == {"second" => @resource, "first" => file} + end + end + + describe "#recurse_local" do + before do + @metadata = stub 'metadata', :relative_path => "my/file" + end + + it "should pass its path to the :perform_recursion method" do + file.expects(:perform_recursion).with(file[:path]).returns [@metadata] + file.stubs(:newchild) + file.recurse_local + end + + it "should return an empty hash if the recursion returns nothing" do + file.expects(:perform_recursion).returns nil + file.recurse_local.should == {} + end + + it "should create a new child resource with each generated metadata instance's relative path" do + file.expects(:perform_recursion).returns [@metadata] + file.expects(:newchild).with(@metadata.relative_path).returns "fiebar" + file.recurse_local + end + + it "should not create a new child resource for the '.' directory" do + @metadata.stubs(:relative_path).returns "." - @file.stubs(:perform_recursion).returns [@first, @second] - @file.recurse_link("first" => file, "second" => @resource) + file.expects(:perform_recursion).returns [@metadata] + file.expects(:newchild).never + file.recurse_local end - it "should return a hash with both created and existing resources with the relative paths as the hash keys" do - file = stub 'file', :[]= => nil - - @file.expects(:perform_recursion).returns [@first, @second] - @file.stubs(:newchild).returns file - @file.recurse_link("second" => @resource).should == {"second" => @resource, "first" => file} + it "should return a hash of the created resources with the relative paths as the hash keys" do + file.expects(:perform_recursion).returns [@metadata] + file.expects(:newchild).with("my/file").returns "fiebar" + file.recurse_local.should == {"my/file" => "fiebar"} end - end - it "should have a method for performing remote recursion" do - @file.must respond_to(:recurse_remote) + it "should set checksum_type to none if this file checksum is none" do + file[:checksum] = :none + Puppet::FileServing::Metadata.indirection.expects(:search).with { |path,params| params[:checksum_type] == :none }.returns [@metadata] + file.expects(:newchild).with("my/file").returns "fiebar" + file.recurse_local + end end - describe "when doing remote recursion" do + describe "#recurse_remote" do before do - @file[:source] = "puppet://foo/bar" + file[:source] = "puppet://foo/bar" @first = Puppet::FileServing::Metadata.new("/my", :relative_path => "first") @second = Puppet::FileServing::Metadata.new("/my", :relative_path => "second") @first.stubs(:ftype).returns "directory" @second.stubs(:ftype).returns "directory" @parameter = stub 'property', :metadata= => nil @resource = stub 'file', :[]= => nil, :parameter => @parameter end it "should pass its source to the :perform_recursion method" do data = Puppet::FileServing::Metadata.new("/whatever", :relative_path => "foobar") - @file.expects(:perform_recursion).with("puppet://foo/bar").returns [data] - @file.stubs(:newchild).returns @resource - @file.recurse_remote({}) + file.expects(:perform_recursion).with("puppet://foo/bar").returns [data] + file.stubs(:newchild).returns @resource + file.recurse_remote({}) end it "should not recurse when the remote file is not a directory" do data = Puppet::FileServing::Metadata.new("/whatever", :relative_path => ".") data.stubs(:ftype).returns "file" - @file.expects(:perform_recursion).with("puppet://foo/bar").returns [data] - @file.expects(:newchild).never - @file.recurse_remote({}) + file.expects(:perform_recursion).with("puppet://foo/bar").returns [data] + file.expects(:newchild).never + file.recurse_remote({}) end it "should set the source of each returned file to the searched-for URI plus the found relative path" do @first.expects(:source=).with File.join("puppet://foo/bar", @first.relative_path) - @file.expects(:perform_recursion).returns [@first] - @file.stubs(:newchild).returns @resource - @file.recurse_remote({}) + file.expects(:perform_recursion).returns [@first] + file.stubs(:newchild).returns @resource + file.recurse_remote({}) end it "should create a new resource for any relative file paths that do not already have a resource" do - @file.stubs(:perform_recursion).returns [@first] - @file.expects(:newchild).with("first").returns @resource - @file.recurse_remote({}).should == {"first" => @resource} + file.stubs(:perform_recursion).returns [@first] + file.expects(:newchild).with("first").returns @resource + file.recurse_remote({}).should == {"first" => @resource} end it "should not create a new resource for any relative file paths that do already have a resource" do - @file.stubs(:perform_recursion).returns [@first] - @file.expects(:newchild).never - @file.recurse_remote("first" => @resource) + file.stubs(:perform_recursion).returns [@first] + file.expects(:newchild).never + file.recurse_remote("first" => @resource) end it "should set the source of each resource to the source of the metadata" do - @file.stubs(:perform_recursion).returns [@first] + file.stubs(:perform_recursion).returns [@first] @resource.stubs(:[]=) @resource.expects(:[]=).with(:source, File.join("puppet://foo/bar", @first.relative_path)) - @file.recurse_remote("first" => @resource) + file.recurse_remote("first" => @resource) end # LAK:FIXME This is a bug, but I can't think of a fix for it. Fortunately it's already # filed, and when it's fixed, we'll just fix the whole flow. it "should set the checksum type to :md5 if the remote file is a file" do @first.stubs(:ftype).returns "file" - @file.stubs(:perform_recursion).returns [@first] + file.stubs(:perform_recursion).returns [@first] @resource.stubs(:[]=) @resource.expects(:[]=).with(:checksum, :md5) - @file.recurse_remote("first" => @resource) + file.recurse_remote("first" => @resource) end it "should store the metadata in the source property for each resource so the source does not have to requery the metadata" do - @file.stubs(:perform_recursion).returns [@first] + file.stubs(:perform_recursion).returns [@first] @resource.expects(:parameter).with(:source).returns @parameter @parameter.expects(:metadata=).with(@first) - @file.recurse_remote("first" => @resource) + file.recurse_remote("first" => @resource) end it "should not create a new resource for the '.' file" do @first.stubs(:relative_path).returns "." - @file.stubs(:perform_recursion).returns [@first] + file.stubs(:perform_recursion).returns [@first] - @file.expects(:newchild).never + file.expects(:newchild).never - @file.recurse_remote({}) + file.recurse_remote({}) end it "should store the metadata in the main file's source property if the relative path is '.'" do @first.stubs(:relative_path).returns "." - @file.stubs(:perform_recursion).returns [@first] + file.stubs(:perform_recursion).returns [@first] - @file.parameter(:source).expects(:metadata=).with @first + file.parameter(:source).expects(:metadata=).with @first - @file.recurse_remote("first" => @resource) + file.recurse_remote("first" => @resource) end describe "and multiple sources are provided" do + let(:sources) do + h = {} + %w{/one /two /three /four}.each do |key| + h[key] = URI.unescape(Puppet::Util.path_to_uri(File.expand_path(key)).to_s) + end + h + end + describe "and :sourceselect is set to :first" do it "should create file instances for the results for the first source to return any values" do data = Puppet::FileServing::Metadata.new("/whatever", :relative_path => "foobar") - @file[:source] = %w{/one /two /three /four} - @file.expects(:perform_recursion).with("/one").returns nil - @file.expects(:perform_recursion).with("/two").returns [] - @file.expects(:perform_recursion).with("/three").returns [data] - @file.expects(:perform_recursion).with("/four").never - @file.expects(:newchild).with("foobar").returns @resource - @file.recurse_remote({}) + file[:source] = sources.keys.map { |key| File.expand_path(key) } + file.expects(:perform_recursion).with(sources['/one']).returns nil + file.expects(:perform_recursion).with(sources['/two']).returns [] + file.expects(:perform_recursion).with(sources['/three']).returns [data] + file.expects(:perform_recursion).with(sources['/four']).never + file.expects(:newchild).with("foobar").returns @resource + file.recurse_remote({}) end end describe "and :sourceselect is set to :all" do before do - @file[:sourceselect] = :all + file[:sourceselect] = :all end it "should return every found file that is not in a previous source" do klass = Puppet::FileServing::Metadata - @file[:source] = %w{/one /two /three /four} - @file.stubs(:newchild).returns @resource + file[:source] = %w{/one /two /three /four}.map {|f| File.expand_path(f) } + file.stubs(:newchild).returns @resource one = [klass.new("/one", :relative_path => "a")] - @file.expects(:perform_recursion).with("/one").returns one - @file.expects(:newchild).with("a").returns @resource + file.expects(:perform_recursion).with(sources['/one']).returns one + file.expects(:newchild).with("a").returns @resource two = [klass.new("/two", :relative_path => "a"), klass.new("/two", :relative_path => "b")] - @file.expects(:perform_recursion).with("/two").returns two - @file.expects(:newchild).with("b").returns @resource + file.expects(:perform_recursion).with(sources['/two']).returns two + file.expects(:newchild).with("b").returns @resource three = [klass.new("/three", :relative_path => "a"), klass.new("/three", :relative_path => "c")] - @file.expects(:perform_recursion).with("/three").returns three - @file.expects(:newchild).with("c").returns @resource + file.expects(:perform_recursion).with(sources['/three']).returns three + file.expects(:newchild).with("c").returns @resource - @file.expects(:perform_recursion).with("/four").returns [] + file.expects(:perform_recursion).with(sources['/four']).returns [] - @file.recurse_remote({}) + file.recurse_remote({}) end end end end - describe "when specifying both source, and content properties" do - before do - @file[:source] = '/one' - @file[:content] = 'file contents' + describe "#perform_recursion" do + it "should use Metadata to do its recursion" do + Puppet::FileServing::Metadata.indirection.expects(:search) + file.perform_recursion(file[:path]) end - it "should raise an exception" do - lambda {@file.validate }.should raise_error(/You cannot specify more than one of/) + it "should use the provided path as the key to the search" do + Puppet::FileServing::Metadata.indirection.expects(:search).with { |key, options| key == "/foo" } + file.perform_recursion("/foo") end - end - describe "when using source" do - before do - @file[:source] = '/one' + it "should return the results of the metadata search" do + Puppet::FileServing::Metadata.indirection.expects(:search).returns "foobar" + file.perform_recursion(file[:path]).should == "foobar" end - Puppet::Type::File::ParameterChecksum.value_collection.values.reject {|v| v == :none}.each do |checksum_type| - describe "with checksum '#{checksum_type}'" do - before do - @file[:checksum] = checksum_type - end - it 'should validate' do + it "should pass its recursion value to the search" do + file[:recurse] = true + Puppet::FileServing::Metadata.indirection.expects(:search).with { |key, options| options[:recurse] == true } + file.perform_recursion(file[:path]) + end - lambda { @file.validate }.should_not raise_error - end - end + it "should pass true if recursion is remote" do + file[:recurse] = :remote + Puppet::FileServing::Metadata.indirection.expects(:search).with { |key, options| options[:recurse] == true } + file.perform_recursion(file[:path]) end - describe "with checksum 'none'" do - before do - @file[:checksum] = :none - end + it "should pass its recursion limit value to the search" do + file[:recurselimit] = 10 + Puppet::FileServing::Metadata.indirection.expects(:search).with { |key, options| options[:recurselimit] == 10 } + file.perform_recursion(file[:path]) + end - it 'should raise an exception when validating' do - lambda { @file.validate }.should raise_error(/You cannot specify source when using checksum 'none'/) - end + it "should configure the search to ignore or manage links" do + file[:links] = :manage + Puppet::FileServing::Metadata.indirection.expects(:search).with { |key, options| options[:links] == :manage } + file.perform_recursion(file[:path]) + end + + it "should pass its 'ignore' setting to the search if it has one" do + file[:ignore] = %w{.svn CVS} + Puppet::FileServing::Metadata.indirection.expects(:search).with { |key, options| options[:ignore] == %w{.svn CVS} } + file.perform_recursion(file[:path]) end end - describe "when using content" do - before do - @file[:content] = 'file contents' + describe "#remove_existing" do + it "should do nothing if the file doesn't exist" do + file.remove_existing(:file).should == nil end - (Puppet::Type::File::ParameterChecksum.value_collection.values - SOURCE_ONLY_CHECKSUMS).each do |checksum_type| - describe "with checksum '#{checksum_type}'" do - before do - @file[:checksum] = checksum_type - end + it "should fail if it can't backup the file" do + file.stubs(:stat).returns stub('stat') + file.stubs(:perform_backup).returns false - it 'should validate' do - lambda { @file.validate }.should_not raise_error - end - end + expect { file.remove_existing(:file) }.to raise_error(Puppet::Error, /Could not back up; will not replace/) end - SOURCE_ONLY_CHECKSUMS.each do |checksum_type| - describe "with checksum '#{checksum_type}'" do - it 'should raise an exception when validating' do - @file[:checksum] = checksum_type + it "should not do anything if the file is already the right type and not a link" do + file.stubs(:stat).returns stub('stat', :ftype => 'file') - lambda { @file.validate }.should raise_error(/You cannot specify content when using checksum '#{checksum_type}'/) - end - end + file.remove_existing(:file).should == nil + end + + it "should not remove directories and should not invalidate the stat unless force is set" do + # Actually call stat to set @needs_stat to nil + file.stat + file.stubs(:stat).returns stub('stat', :ftype => 'directory') + + file.remove_existing(:file) + + file.instance_variable_get(:@stat).should == nil + @logs.should be_any {|log| log.level == :notice and log.message =~ /Not removing directory; use 'force' to override/} + end + + it "should remove a directory if force is set" do + file[:force] = true + file.stubs(:stat).returns stub('stat', :ftype => 'directory') + + FileUtils.expects(:rmtree).with(file[:path]) + + file.remove_existing(:file).should == true + end + + it "should remove an existing file" do + file.stubs(:perform_backup).returns true + FileUtils.touch(path) + + file.remove_existing(:directory).should == true + + File.exists?(file[:path]).should == false + end + + it "should remove an existing link", :unless => Puppet.features.microsoft_windows? do + file.stubs(:perform_backup).returns true + + target = tmpfile('link_target') + FileUtils.touch(target) + FileUtils.symlink(target, path) + file[:target] = target + + file.remove_existing(:directory).should == true + + File.exists?(file[:path]).should == false + end + + it "should fail if the file is not a file, link, or directory" do + file.stubs(:stat).returns stub('stat', :ftype => 'socket') + + expect { file.remove_existing(:file) }.to raise_error(Puppet::Error, /Could not back up files of type socket/) + end + + it "should invalidate the existing stat of the file" do + # Actually call stat to set @needs_stat to nil + file.stat + file.stubs(:stat).returns stub('stat', :ftype => 'file') + + File.stubs(:unlink) + + file.remove_existing(:directory).should == true + file.instance_variable_get(:@stat).should == :needs_stat end end - describe "when returning resources with :eval_generate" do - before do - @graph = stub 'graph', :add_edge => nil - @catalog.stubs(:relationship_graph).returns @graph + describe "#retrieve" do + it "should copy the source values if the 'source' parameter is set" do + file[:source] = File.expand_path('/foo/bar') + file.parameter(:source).expects(:copy_source_values) + file.retrieve + end + end - @file.catalog = @catalog - @file[:recurse] = true + describe "#should_be_file?" do + it "should have a method for determining if the file should be a normal file" do + file.must respond_to(:should_be_file?) end - it "should recurse if recursion is enabled" do - resource = stub('resource', :[] => "resource") - @file.expects(:recurse?).returns true - @file.expects(:recurse).returns [resource] - @file.eval_generate.should == [resource] + it "should be a file if :ensure is set to :file" do + file[:ensure] = :file + file.must be_should_be_file end - it "should not recurse if recursion is disabled" do - @file.expects(:recurse?).returns false - @file.expects(:recurse).never - @file.eval_generate.should == [] + it "should be a file if :ensure is set to :present and the file exists as a normal file" do + file.stubs(:stat).returns(mock('stat', :ftype => "file")) + file[:ensure] = :present + file.must be_should_be_file + end + + it "should not be a file if :ensure is set to something other than :file" do + file[:ensure] = :directory + file.must_not be_should_be_file + end + + it "should not be a file if :ensure is set to :present and the file exists but is not a normal file" do + file.stubs(:stat).returns(mock('stat', :ftype => "directory")) + file[:ensure] = :present + file.must_not be_should_be_file end - it "should return each resource found through recursion" do - foo = stub 'foo', :[] => "/foo" - bar = stub 'bar', :[] => "/bar" - bar2 = stub 'bar2', :[] => "/bar" + it "should be a file if :ensure is not set and :content is" do + file[:content] = "foo" + file.must be_should_be_file + end - @file.expects(:recurse).returns [foo, bar] + it "should be a file if neither :ensure nor :content is set but the file exists as a normal file" do + file.stubs(:stat).returns(mock("stat", :ftype => "file")) + file.must be_should_be_file + end - @file.eval_generate.should == [foo, bar] + it "should not be a file if neither :ensure nor :content is set but the file exists but not as a normal file" do + file.stubs(:stat).returns(mock("stat", :ftype => "directory")) + file.must_not be_should_be_file end end - describe "when recursing" do + describe "#stat", :unless => Puppet.features.microsoft_windows? do before do - @file[:recurse] = true - @metadata = Puppet::FileServing::Metadata + target = tmpfile('link_target') + FileUtils.touch(target) + FileUtils.symlink(target, path) + + file[:target] = target + file[:links] = :manage # so we always use :lstat end - describe "and a source is set" do - before { @file[:source] = "/my/source" } + it "should stat the target if it is following links" do + file[:links] = :follow - it "should pass the already-discovered resources to recurse_remote" do - @file.stubs(:recurse_local).returns(:foo => "bar") - @file.expects(:recurse_remote).with(:foo => "bar").returns [] - @file.recurse - end + file.stat.ftype.should == 'file' end - describe "and a target is set" do - before { @file[:target] = "/link/target" } + it "should stat the link if is it not following links" do + file[:links] = :manage - it "should use recurse_link" do - @file.stubs(:recurse_local).returns(:foo => "bar") - @file.expects(:recurse_link).with(:foo => "bar").returns [] - @file.recurse - end + file.stat.ftype.should == 'link' end - it "should use recurse_local if recurse is not remote" do - @file.expects(:recurse_local).returns({}) - @file.recurse - end + it "should return nil if the file does not exist" do + file[:path] = '/foo/bar/baz/non-existent' - it "should not use recurse_local if recurse remote" do - @file[:recurse] = :remote - @file.expects(:recurse_local).never - @file.recurse + file.stat.should be_nil end - it "should return the generated resources as an array sorted by file path" do - one = stub 'one', :[] => "/one" - two = stub 'two', :[] => "/one/two" - three = stub 'three', :[] => "/three" - @file.expects(:recurse_local).returns(:one => one, :two => two, :three => three) - @file.recurse.should == [one, two, three] + it "should return nil if the file cannot be stat'ed" do + dir = tmpfile('link_test_dir') + child = File.join(dir, 'some_file') + Dir.mkdir(dir) + File.chmod(0, dir) + + file[:path] = child + + file.stat.should be_nil + + # chmod it back so we can clean it up + File.chmod(0777, dir) end - describe "and purging is enabled" do - before do - @file[:purge] = true - end + it "should return the stat instance" do + file.stat.should be_a(File::Stat) + end - it "should configure each file to be removed" do - local = stub 'local' - local.stubs(:[]).with(:source).returns nil # Thus, a local file - local.stubs(:[]).with(:path).returns "foo" - @file.expects(:recurse_local).returns("local" => local) - local.expects(:[]=).with(:ensure, :absent) + it "should cache the stat instance" do + file.stat.should equal(file.stat) + end + end - @file.recurse - end + describe "#write" do + it "should propagate failures encountered when renaming the temporary file" do + File.stubs(:open) + File.expects(:rename).raises ArgumentError - it "should not remove files that exist in the remote repository" do - @file["source"] = "/my/file" - @file.expects(:recurse_local).returns({}) + file[:backup] = 'puppet' - remote = stub 'remote' - remote.stubs(:[]).with(:source).returns "/whatever" # Thus, a remote file - remote.stubs(:[]).with(:path).returns "foo" + file.stubs(:validate_checksum?).returns(false) - @file.expects(:recurse_remote).with { |hash| hash["remote"] = remote } - remote.expects(:[]=).with(:ensure, :absent).never + property = stub('content_property', :actual_content => "something", :length => "something".length) + file.stubs(:property).with(:content).returns(property) - @file.recurse - end + lambda { file.write(:content) }.should raise_error(Puppet::Error) end - describe "and making a new child resource" do - it "should not copy the parent resource's parent" do - Puppet::Type.type(:file).expects(:new).with { |options| ! options.include?(:parent) } - @file.newchild("my/path") - end + it "should delegate writing to the content property" do + filehandle = stub_everything 'fh' + File.stubs(:open).yields(filehandle) + File.stubs(:rename) + property = stub('content_property', :actual_content => "something", :length => "something".length) + file[:backup] = 'puppet' - {:recurse => true, :target => "/foo/bar", :ensure => :present, :alias => "yay", :source => "/foo/bar"}.each do |param, value| - it "should not pass on #{param} to the sub resource" do - @file = Puppet::Type::File.new(:name => @path, param => value, :catalog => @catalog) + file.stubs(:validate_checksum?).returns(false) + file.stubs(:property).with(:content).returns(property) - @file.class.expects(:new).with { |params| params[param].nil? } + property.expects(:write).with(filehandle) - @file.newchild("sub/file") - end - end + file.write(:content) + end - it "should copy all of the parent resource's 'should' values that were set at initialization" do - file = @file.class.new(:path => "/foo/bar", :owner => "root", :group => "wheel") - @catalog.add_resource(file) - file.class.expects(:new).with { |options| options[:owner] == "root" and options[:group] == "wheel" } - file.newchild("my/path") - end + describe "when validating the checksum" do + before { file.stubs(:validate_checksum?).returns(true) } + + it "should fail if the checksum parameter and content checksums do not match" do + checksum = stub('checksum_parameter', :sum => 'checksum_b', :sum_file => 'checksum_b') + file.stubs(:parameter).with(:checksum).returns(checksum) - it "should not copy default values to the new child" do - @file.class.expects(:new).with { |params| params[:backup].nil? } - @file.newchild("my/path") + property = stub('content_property', :actual_content => "something", :length => "something".length, :write => 'checksum_a') + file.stubs(:property).with(:content).returns(property) + + lambda { file.write :NOTUSED }.should raise_error(Puppet::Error) end + end + + describe "when not validating the checksum" do + before { file.stubs(:validate_checksum?).returns(false) } - it "should not copy values to the child which were set by the source" do - @file[:source] = "/foo/bar" - metadata = stub 'metadata', :owner => "root", :group => "root", :mode => 0755, :ftype => "file", :checksum => "{md5}whatever" - @file.parameter(:source).stubs(:metadata).returns metadata + it "should not fail if the checksum property and content checksums do not match" do + checksum = stub('checksum_parameter', :sum => 'checksum_b') + file.stubs(:parameter).with(:checksum).returns(checksum) - @file.parameter(:source).copy_source_values + property = stub('content_property', :actual_content => "something", :length => "something".length, :write => 'checksum_a') + file.stubs(:property).with(:content).returns(property) - @file.class.expects(:new).with { |params| params[:group].nil? } - @file.newchild("my/path") + lambda { file.write :NOTUSED }.should_not raise_error(Puppet::Error) end end end - describe "when setting the backup" do - it "should default to 'puppet'" do - Puppet::Type::File.new(:name => "/my/file")[:backup].should == "puppet" + describe "#fail_if_checksum_is_wrong" do + it "should fail if the checksum of the file doesn't match the expected one" do + expect do + file.instance_eval do + parameter(:checksum).stubs(:sum_file).returns('wrong!!') + fail_if_checksum_is_wrong(self[:path], 'anything!') + end + end.to raise_error(Puppet::Error, /File written to disk did not match checksum/) end - it "should allow setting backup to 'false'" do - (!Puppet::Type::File.new(:name => "/my/file", :backup => false)[:backup]).should be_true + it "should not fail if the checksum is correct" do + file.instance_eval do + parameter(:checksum).stubs(:sum_file).returns('anything!') + fail_if_checksum_is_wrong(self[:path], 'anything!').should == nil + end end - it "should set the backup to '.puppet-bak' if it is set to true" do - Puppet::Type::File.new(:name => "/my/file", :backup => true)[:backup].should == ".puppet-bak" + it "should not fail if the checksum is absent" do + file.instance_eval do + parameter(:checksum).stubs(:sum_file).returns(nil) + fail_if_checksum_is_wrong(self[:path], 'anything!').should == nil + end end + end - it "should support any other backup extension" do - Puppet::Type::File.new(:name => "/my/file", :backup => ".bak")[:backup].should == ".bak" - end + describe "#write_content" do + it "should delegate writing the file to the content property" do + io = stub('io') + file[:content] = "some content here" + file.property(:content).expects(:write).with(io) - it "should set the filebucket when backup is set to a string matching the name of a filebucket in the catalog" do - catalog = Puppet::Resource::Catalog.new - bucket_resource = Puppet::Type.type(:filebucket).new :name => "foo", :path => "/my/file/bucket" - catalog.add_resource bucket_resource + file.send(:write_content, io) + end + end - file = Puppet::Type::File.new(:name => "/my/file") - catalog.add_resource file + describe "#write_temporary_file?" do + it "should be true if the file has specified content" do + file[:content] = 'some content' - file[:backup] = "foo" - file.bucket.should == bucket_resource.bucket + file.send(:write_temporary_file?).should be_true end - it "should find filebuckets added to the catalog after the file resource was created" do - catalog = Puppet::Resource::Catalog.new + it "should be true if the file has specified source" do + file[:source] = File.expand_path('/tmp/foo') - file = Puppet::Type::File.new(:name => "/my/file", :backup => "foo") - catalog.add_resource file + file.send(:write_temporary_file?).should be_true + end - bucket_resource = Puppet::Type.type(:filebucket).new :name => "foo", :path => "/my/file/bucket" - catalog.add_resource bucket_resource + it "should be false if the file has neither content nor source" do + file.send(:write_temporary_file?).should be_false + end + end - file.bucket.should == bucket_resource.bucket + describe "#property_fix" do + { + :mode => 0777, + :owner => 'joeuser', + :group => 'joeusers', + :seluser => 'seluser', + :selrole => 'selrole', + :seltype => 'seltype', + :selrange => 'selrange' + }.each do |name,value| + it "should sync the #{name} property if it's not in sync" do + file[name] = value + + prop = file.property(name) + prop.expects(:retrieve) + prop.expects(:safe_insync?).returns false + prop.expects(:sync) + + file.send(:property_fix) + end end + end - it "should have a nil filebucket if backup is false" do - catalog = Puppet::Resource::Catalog.new - bucket_resource = Puppet::Type.type(:filebucket).new :name => "foo", :path => "/my/file/bucket" - catalog.add_resource bucket_resource + describe "when autorequiring" do + describe "directories" do + it "should autorequire its parent directory" do + dir = described_class.new(:path => File.dirname(path)) + catalog.add_resource file + catalog.add_resource dir + reqs = file.autorequire + reqs[0].source.must == dir + reqs[0].target.must == file + end + + it "should autorequire its nearest ancestor directory" do + dir = described_class.new(:path => File.dirname(path)) + grandparent = described_class.new(:path => File.dirname(File.dirname(path))) + catalog.add_resource file + catalog.add_resource dir + catalog.add_resource grandparent + reqs = file.autorequire + reqs.length.must == 1 + reqs[0].source.must == dir + reqs[0].target.must == file + end + + it "should not autorequire anything when there is no nearest ancestor directory" do + catalog.add_resource file + file.autorequire.should be_empty + end - file = Puppet::Type::File.new(:name => "/my/file", :backup => false) - catalog.add_resource file + it "should not autorequire its parent dir if its parent dir is itself" do + file[:path] = File.expand_path('/') + catalog.add_resource file + file.autorequire.should be_empty + end - file.bucket.should be_nil + describe "on Windows systems", :if => Puppet.features.microsoft_windows? do + describe "when using UNC filenames" do + it "should autorequire its parent directory" do + file[:path] = '//server/foo/bar/baz' + dir = described_class.new(:path => "//server/foo/bar") + catalog.add_resource file + catalog.add_resource dir + reqs = file.autorequire + reqs[0].source.must == dir + reqs[0].target.must == file + end + + it "should autorequire its nearest ancestor directory" do + file = described_class.new(:path => "//server/foo/bar/baz/qux") + dir = described_class.new(:path => "//server/foo/bar/baz") + grandparent = described_class.new(:path => "//server/foo/bar") + catalog.add_resource file + catalog.add_resource dir + catalog.add_resource grandparent + reqs = file.autorequire + reqs.length.must == 1 + reqs[0].source.must == dir + reqs[0].target.must == file + end + + it "should not autorequire anything when there is no nearest ancestor directory" do + file = described_class.new(:path => "//server/foo/bar/baz/qux") + catalog.add_resource file + file.autorequire.should be_empty + end + + it "should not autorequire its parent dir if its parent dir is itself" do + file = described_class.new(:path => "//server/foo") + catalog.add_resource file + puts file.autorequire + file.autorequire.should be_empty + end + end + end end + end + + describe "when managing links" do + require 'tempfile' - it "should have a nil filebucket if backup is set to a string starting with '.'" do - catalog = Puppet::Resource::Catalog.new - bucket_resource = Puppet::Type.type(:filebucket).new :name => "foo", :path => "/my/file/bucket" - catalog.add_resource bucket_resource + if @real_posix + describe "on POSIX systems" do + before do + Dir.mkdir(path) + @target = File.join(path, "target") + @link = File.join(path, "link") - file = Puppet::Type::File.new(:name => "/my/file", :backup => ".foo") - catalog.add_resource file + File.open(@target, "w", 0644) { |f| f.puts "yayness" } + File.symlink(@target, @link) - file.bucket.should be_nil - end + file[:path] = @link + file[:mode] = 0755 - it "should fail if there's no catalog and backup is not false" do - file = Puppet::Type::File.new(:name => "/my/file", :backup => "foo") + catalog.add_resource file + end + + it "should default to managing the link" do + catalog.apply + # I convert them to strings so they display correctly if there's an error. + (File.stat(@target).mode & 007777).to_s(8).should == '644' + end + + it "should be able to follow links" do + file[:links] = :follow + catalog.apply - lambda { file.bucket }.should raise_error(Puppet::Error) + (File.stat(@target).mode & 007777).to_s(8).should == '755' + end + end + else # @real_posix + # should recode tests using expectations instead of using the filesystem end - it "should fail if a non-existent catalog is specified" do - file = Puppet::Type::File.new(:name => "/my/file", :backup => "foo") - catalog = Puppet::Resource::Catalog.new - catalog.add_resource file + describe "on Microsoft Windows systems" do + before do + Puppet.features.stubs(:posix?).returns(false) + Puppet.features.stubs(:microsoft_windows?).returns(true) + end - lambda { file.bucket }.should raise_error(Puppet::Error) + it "should refuse to work with links" end + end - it "should be able to use the default filebucket without a catalog" do - file = Puppet::Type::File.new(:name => "/my/file", :backup => "puppet") - file.bucket.should be_instance_of(Puppet::FileBucket::Dipper) + describe "when using source" do + before do + file[:source] = File.expand_path('/one') end + Puppet::Type::File::ParameterChecksum.value_collection.values.reject {|v| v == :none}.each do |checksum_type| + describe "with checksum '#{checksum_type}'" do + before do + file[:checksum] = checksum_type + end - it "should look up the filebucket during finish()" do - file = Puppet::Type::File.new(:name => "/my/file", :backup => ".foo") - file.expects(:bucket) - file.finish + it 'should validate' do + + lambda { file.validate }.should_not raise_error + end + end end - end - describe "when retrieving the current file state" do - it "should copy the source values if the 'source' parameter is set" do - file = Puppet::Type::File.new(:name => "/my/file", :source => "/foo/bar") - file.parameter(:source).expects(:copy_source_values) - file.retrieve + describe "with checksum 'none'" do + before do + file[:checksum] = :none + end + + it 'should raise an exception when validating' do + lambda { file.validate }.should raise_error(/You cannot specify source when using checksum 'none'/) + end end end - describe ".title_patterns" do + describe "when using content" do before do - @type_class = Puppet::Type.type(:file) + file[:content] = 'file contents' + end + + (Puppet::Type::File::ParameterChecksum.value_collection.values - SOURCE_ONLY_CHECKSUMS).each do |checksum_type| + describe "with checksum '#{checksum_type}'" do + before do + file[:checksum] = checksum_type + end + + it 'should validate' do + lambda { file.validate }.should_not raise_error + end + end end - it "should have a regexp that captures the entire string, except for a terminating slash" do - patterns = @type_class.title_patterns - string = "abc/\n\tdef/" - patterns[0][0] =~ string - $1.should == "abc/\n\tdef" + SOURCE_ONLY_CHECKSUMS.each do |checksum_type| + describe "with checksum '#{checksum_type}'" do + it 'should raise an exception when validating' do + file[:checksum] = checksum_type + + lambda { file.validate }.should raise_error(/You cannot specify content when using checksum '#{checksum_type}'/) + end + end end end describe "when auditing" do + before :each do + # to prevent the catalog from trying to write state.yaml + Puppet::Util::Storage.stubs(:store) + end + it "should not fail if creating a new file if group is not set" do - File.exists?(@path).should == false - file = Puppet::Type::File.new(:name => @path, :audit => "all", :content => "content") - catalog = Puppet::Resource::Catalog.new + file = described_class.new(:path => path, :audit => 'all', :content => 'content') catalog.add_resource(file) - Puppet::Util::Storage.stubs(:store) # to prevent the catalog from trying to write state.yaml - transaction = catalog.apply + report = catalog.apply.report - transaction.report.resource_statuses["File[#{@path}]"].failed.should == false - File.exists?(@path).should == true + report.resource_statuses["File[#{path}]"].should_not be_failed + File.read(path).should == 'content' end it "should not log errors if creating a new file with ensure present and no content" do - File.exists?(@path).should == false - file = Puppet::Type::File.new(:name => @path, :audit => "content", :ensure => "present") - catalog = Puppet::Resource::Catalog.new + file[:audit] = 'content' + file[:ensure] = 'present' catalog.add_resource(file) - Puppet::Util::Storage.stubs(:store) # to prevent the catalog from trying to write state.yaml - catalog.apply - @logs.reject {|l| l.level == :notice }.should be_empty + + File.should be_exist(path) + @logs.should_not be_any {|l| l.level != :notice } end end describe "when specifying both source and checksum" do it 'should use the specified checksum when source is first' do - @file[:source] = '/foo' - @file[:checksum] = :md5lite + file[:source] = File.expand_path('/foo') + file[:checksum] = :md5lite - @file[:checksum].should be :md5lite + file[:checksum].should == :md5lite end + it 'should use the specified checksum when source is last' do - @file[:checksum] = :md5lite - @file[:source] = '/foo' + file[:checksum] = :md5lite + file[:source] = File.expand_path('/foo') - @file[:checksum].should be :md5lite + file[:checksum].should == :md5lite end end - describe ".instances" do - it 'should return an empty array' do - Puppet::Type::File.instances.should == [] + describe "when validating" do + [[:source, :target], [:source, :content], [:target, :content]].each do |prop1,prop2| + it "should fail if both #{prop1} and #{prop2} are specified" do + file[prop1] = prop1 == :source ? File.expand_path("prop1 value") : "prop1 value" + file[prop2] = "prop2 value" + expect do + file.validate + end.to raise_error(Puppet::Error, /You cannot specify more than one of/) + end end end + end diff --git a/spec/unit/util/adsi_spec.rb b/spec/unit/util/adsi_spec.rb index 8db64184a..291bb64dc 100755 --- a/spec/unit/util/adsi_spec.rb +++ b/spec/unit/util/adsi_spec.rb @@ -1,211 +1,221 @@ #!/usr/bin/env ruby require 'spec_helper' require 'puppet/util/adsi' describe Puppet::Util::ADSI do let(:connection) { stub 'connection' } before(:each) do Puppet::Util::ADSI.instance_variable_set(:@computer_name, 'testcomputername') Puppet::Util::ADSI.stubs(:connect).returns connection end after(:each) do Puppet::Util::ADSI.instance_variable_set(:@computer_name, nil) end it "should generate the correct URI for a resource" do Puppet::Util::ADSI.uri('test', 'user').should == "WinNT://testcomputername/test,user" end it "should be able to get the name of the computer" do Puppet::Util::ADSI.computer_name.should == 'testcomputername' end it "should be able to provide the correct WinNT base URI for the computer" do Puppet::Util::ADSI.computer_uri.should == "WinNT://testcomputername" end - it "should return a SID for a passed user or group name" do - Puppet::Util::ADSI.expects(:execquery).returns([stub('acct_id', :Sid => 'S-1-5-32-547')]) - Puppet::Util::ADSI.sid_for_account('testers').should == 'S-1-5-32-547' + describe ".sid_for_account" do + it "should return the SID" do + result = [stub('account', :Sid => 'S-1-1-50')] + connection.expects(:execquery).returns(result) + + Puppet::Util::ADSI.sid_for_account('joe').should == 'S-1-1-50' + end + + it "should return nil if the account does not exist" do + connection.expects(:execquery).returns([]) + + Puppet::Util::ADSI.sid_for_account('foobar').should be_nil + end end describe Puppet::Util::ADSI::User do let(:username) { 'testuser' } it "should generate the correct URI" do Puppet::Util::ADSI::User.uri(username).should == "WinNT://testcomputername/#{username},user" end it "should be able to create a user" do adsi_user = stub('adsi') connection.expects(:Create).with('user', username).returns(adsi_user) user = Puppet::Util::ADSI::User.create(username) user.should be_a(Puppet::Util::ADSI::User) user.native_user.should == adsi_user end it "should be able to check the existence of a user" do Puppet::Util::ADSI.expects(:connect).with("WinNT://testcomputername/#{username},user").returns connection Puppet::Util::ADSI::User.exists?(username).should be_true end it "should be able to delete a user" do connection.expects(:Delete).with('user', username) Puppet::Util::ADSI::User.delete(username) end describe "an instance" do let(:adsi_user) { stub 'user' } let(:user) { Puppet::Util::ADSI::User.new(username, adsi_user) } it "should provide its groups as a list of names" do names = ["group1", "group2"] groups = names.map { |name| mock('group', :Name => name) } adsi_user.expects(:Groups).returns(groups) user.groups.should =~ names end it "should be able to test whether a given password is correct" do Puppet::Util::ADSI::User.expects(:logon).with(username, 'pwdwrong').returns(false) Puppet::Util::ADSI::User.expects(:logon).with(username, 'pwdright').returns(true) user.password_is?('pwdwrong').should be_false user.password_is?('pwdright').should be_true end it "should be able to set a password" do adsi_user.expects(:SetPassword).with('pwd') adsi_user.expects(:SetInfo).at_least_once flagname = "UserFlags" fADS_UF_DONT_EXPIRE_PASSWD = 0x10000 adsi_user.expects(:Get).with(flagname).returns(0) adsi_user.expects(:Put).with(flagname, fADS_UF_DONT_EXPIRE_PASSWD) user.password = 'pwd' end it "should generate the correct URI" do user.uri.should == "WinNT://testcomputername/#{username},user" end describe "when given a set of groups to which to add the user" do let(:groups_to_set) { 'group1,group2' } before(:each) do user.expects(:groups).returns ['group2', 'group3'] end describe "if membership is specified as inclusive" do it "should add the user to those groups, and remove it from groups not in the list" do group1 = stub 'group1' group1.expects(:Add).with("WinNT://testcomputername/#{username},user") group3 = stub 'group1' group3.expects(:Remove).with("WinNT://testcomputername/#{username},user") Puppet::Util::ADSI.expects(:connect).with('WinNT://testcomputername/group1,group').returns group1 Puppet::Util::ADSI.expects(:connect).with('WinNT://testcomputername/group3,group').returns group3 user.set_groups(groups_to_set, false) end end describe "if membership is specified as minimum" do it "should add the user to the specified groups without affecting its other memberships" do group1 = stub 'group1' group1.expects(:Add).with("WinNT://testcomputername/#{username},user") Puppet::Util::ADSI.expects(:connect).with('WinNT://testcomputername/group1,group').returns group1 user.set_groups(groups_to_set, true) end end end end end describe Puppet::Util::ADSI::Group do let(:groupname) { 'testgroup' } describe "an instance" do let(:adsi_group) { stub 'group' } let(:group) { Puppet::Util::ADSI::Group.new(groupname, adsi_group) } it "should be able to add a member" do adsi_group.expects(:Add).with("WinNT://testcomputername/someone,user") group.add_member('someone') end it "should be able to remove a member" do adsi_group.expects(:Remove).with("WinNT://testcomputername/someone,user") group.remove_member('someone') end it "should provide its groups as a list of names" do names = ['user1', 'user2'] users = names.map { |name| mock('user', :Name => name) } adsi_group.expects(:Members).returns(users) group.members.should =~ names end it "should be able to add a list of users to a group" do names = ['user1', 'user2'] adsi_group.expects(:Members).returns names.map{|n| stub(:Name => n)} adsi_group.expects(:Remove).with('WinNT://testcomputername/user1,user') adsi_group.expects(:Add).with('WinNT://testcomputername/user3,user') group.set_members(['user2', 'user3']) end it "should generate the correct URI" do group.uri.should == "WinNT://testcomputername/#{groupname},group" end end it "should generate the correct URI" do Puppet::Util::ADSI::Group.uri("people").should == "WinNT://testcomputername/people,group" end it "should be able to create a group" do adsi_group = stub("adsi") connection.expects(:Create).with('group', groupname).returns(adsi_group) group = Puppet::Util::ADSI::Group.create(groupname) group.should be_a(Puppet::Util::ADSI::Group) group.native_group.should == adsi_group end it "should be able to confirm the existence of a group" do Puppet::Util::ADSI.expects(:connect).with("WinNT://testcomputername/#{groupname},group").returns connection Puppet::Util::ADSI::Group.exists?(groupname).should be_true end it "should be able to delete a group" do connection.expects(:Delete).with('group', groupname) Puppet::Util::ADSI::Group.delete(groupname) end end end diff --git a/spec/unit/util_spec.rb b/spec/unit/util_spec.rb index 8644c9bf6..5bfb2f7bf 100755 --- a/spec/unit/util_spec.rb +++ b/spec/unit/util_spec.rb @@ -1,340 +1,442 @@ #!/usr/bin/env ruby require 'spec_helper' describe Puppet::Util do def process_status(exitstatus) return exitstatus if Puppet.features.microsoft_windows? stub('child_status', :exitstatus => exitstatus) end describe "#absolute_path?" do it "should default to the platform of the local system" do Puppet.features.stubs(:posix?).returns(true) Puppet.features.stubs(:microsoft_windows?).returns(false) Puppet::Util.should be_absolute_path('/foo') Puppet::Util.should_not be_absolute_path('C:/foo') Puppet.features.stubs(:posix?).returns(false) Puppet.features.stubs(:microsoft_windows?).returns(true) Puppet::Util.should be_absolute_path('C:/foo') Puppet::Util.should_not be_absolute_path('/foo') end describe "when using platform :posix" do - %w[/ /foo /foo/../bar //foo //Server/Foo/Bar //?/C:/foo/bar /\Server/Foo].each do |path| + %w[/ /foo /foo/../bar //foo //Server/Foo/Bar //?/C:/foo/bar /\Server/Foo /foo//bar/baz].each do |path| it "should return true for #{path}" do Puppet::Util.should be_absolute_path(path, :posix) end end - %w[. ./foo \foo C:/foo \\Server\Foo\Bar \\?\C:\foo\bar \/?/foo\bar \/Server/foo].each do |path| + %w[. ./foo \foo C:/foo \\Server\Foo\Bar \\?\C:\foo\bar \/?/foo\bar \/Server/foo foo//bar/baz].each do |path| it "should return false for #{path}" do Puppet::Util.should_not be_absolute_path(path, :posix) end end end describe "when using platform :windows" do - %w[C:/foo C:\foo \\\\Server\Foo\Bar \\\\?\C:\foo\bar //Server/Foo/Bar //?/C:/foo/bar /\?\C:/foo\bar \/Server\Foo/Bar].each do |path| + %w[C:/foo C:\foo \\\\Server\Foo\Bar \\\\?\C:\foo\bar //Server/Foo/Bar //?/C:/foo/bar /\?\C:/foo\bar \/Server\Foo/Bar c:/foo//bar//baz].each do |path| it "should return true for #{path}" do Puppet::Util.should be_absolute_path(path, :windows) end end - %w[/ . ./foo \foo /foo /foo/../bar //foo C:foo/bar].each do |path| + %w[/ . ./foo \foo /foo /foo/../bar //foo C:foo/bar foo//bar/baz].each do |path| it "should return false for #{path}" do Puppet::Util.should_not be_absolute_path(path, :windows) end end end end + describe "#path_to_uri" do + %w[. .. foo foo/bar foo/../bar].each do |path| + it "should reject relative path: #{path}" do + lambda { Puppet::Util.path_to_uri(path) }.should raise_error(Puppet::Error) + end + end + + it "should perform URI escaping" do + Puppet::Util.path_to_uri("/foo bar").path.should == "/foo%20bar" + end + + describe "when using platform :posix" do + before :each do + Puppet.features.stubs(:posix).returns true + Puppet.features.stubs(:microsoft_windows?).returns false + end + + %w[/ /foo /foo/../bar].each do |path| + it "should convert #{path} to URI" do + Puppet::Util.path_to_uri(path).path.should == path + end + end + end + + describe "when using platform :windows" do + before :each do + Puppet.features.stubs(:posix).returns false + Puppet.features.stubs(:microsoft_windows?).returns true + end + + it "should normalize backslashes" do + Puppet::Util.path_to_uri('c:\\foo\\bar\\baz').path.should == '/' + 'c:/foo/bar/baz' + end + + %w[C:/ C:/foo/bar].each do |path| + it "should convert #{path} to absolute URI" do + Puppet::Util.path_to_uri(path).path.should == '/' + path + end + end + + %w[share C$].each do |path| + it "should convert UNC #{path} to absolute URI" do + uri = Puppet::Util.path_to_uri("\\\\server\\#{path}") + uri.host.should == 'server' + uri.path.should == '/' + path + end + end + end + end + + describe ".uri_to_path" do + require 'uri' + + it "should strip host component" do + Puppet::Util.uri_to_path(URI.parse('http://foo/bar')).should == '/bar' + end + + it "should accept puppet URLs" do + Puppet::Util.uri_to_path(URI.parse('puppet:///modules/foo')).should == '/modules/foo' + end + + it "should return unencoded path" do + Puppet::Util.uri_to_path(URI.parse('http://foo/bar%20baz')).should == '/bar baz' + end + + it "should be nil-safe" do + Puppet::Util.uri_to_path(nil).should be_nil + end + + describe "when using platform :posix",:if => Puppet.features.posix? do + it "should accept root" do + Puppet::Util.uri_to_path(URI.parse('file:/')).should == '/' + end + + it "should accept single slash" do + Puppet::Util.uri_to_path(URI.parse('file:/foo/bar')).should == '/foo/bar' + end + + it "should accept triple slashes" do + Puppet::Util.uri_to_path(URI.parse('file:///foo/bar')).should == '/foo/bar' + end + end + + describe "when using platform :windows", :if => Puppet.features.microsoft_windows? do + it "should accept root" do + Puppet::Util.uri_to_path(URI.parse('file:/C:/')).should == 'C:/' + end + + it "should accept single slash" do + Puppet::Util.uri_to_path(URI.parse('file:/C:/foo/bar')).should == 'C:/foo/bar' + end + + it "should accept triple slashes" do + Puppet::Util.uri_to_path(URI.parse('file:///C:/foo/bar')).should == 'C:/foo/bar' + end + + it "should accept file scheme with double slashes as a UNC path" do + Puppet::Util.uri_to_path(URI.parse('file://host/share/file')).should == '//host/share/file' + end + end + end + describe "execution methods" do let(:pid) { 5501 } let(:null_file) { Puppet.features.microsoft_windows? ? 'NUL' : '/dev/null' } describe "#execute_posix" do before :each do # Most of the things this method does are bad to do during specs. :/ Kernel.stubs(:fork).returns(pid).yields Process.stubs(:setsid) Kernel.stubs(:exec) Puppet::Util::SUIDManager.stubs(:change_user) Puppet::Util::SUIDManager.stubs(:change_group) $stdin.stubs(:reopen) $stdout.stubs(:reopen) $stderr.stubs(:reopen) @stdin = File.open(null_file, 'r') @stdout = Tempfile.new('stdout') @stderr = File.open(null_file, 'w') end it "should fork a child process to execute the command" do Kernel.expects(:fork).returns(pid).yields Kernel.expects(:exec).with('test command') Puppet::Util.execute_posix('test command', {}, @stdin, @stdout, @stderr) end it "should start a new session group" do Process.expects(:setsid) Puppet::Util.execute_posix('test command', {}, @stdin, @stdout, @stderr) end it "should close all open file descriptors except stdin/stdout/stderr" do # This is ugly, but I can't really think of a better way to do it without # letting it actually close fds, which seems risky (0..2).each {|n| IO.expects(:new).with(n).never} (3..256).each {|n| IO.expects(:new).with(n).returns mock('io', :close) } Puppet::Util.execute_posix('test command', {}, @stdin, @stdout, @stderr) end it "should permanently change to the correct user and group if specified" do Puppet::Util::SUIDManager.expects(:change_group).with(55, true) Puppet::Util::SUIDManager.expects(:change_user).with(50, true) Puppet::Util.execute_posix('test command', {:uid => 50, :gid => 55}, @stdin, @stdout, @stderr) end it "should exit failure if there is a problem execing the command" do Kernel.expects(:exec).with('test command').raises("failed to execute!") Puppet::Util.stubs(:puts) Puppet::Util.expects(:exit!).with(1) Puppet::Util.execute_posix('test command', {}, @stdin, @stdout, @stderr) end it "should properly execute commands specified as arrays" do Kernel.expects(:exec).with('test command', 'with', 'arguments') Puppet::Util.execute_posix(['test command', 'with', 'arguments'], {:uid => 50, :gid => 55}, @stdin, @stdout, @stderr) end it "should return the pid of the child process" do Puppet::Util.execute_posix('test command', {}, @stdin, @stdout, @stderr).should == pid end end describe "#execute_windows" do let(:proc_info_stub) { stub 'processinfo', :process_id => pid } before :each do Process.stubs(:create).returns(proc_info_stub) Process.stubs(:waitpid2).with(pid).returns([pid, process_status(0)]) @stdin = File.open(null_file, 'r') @stdout = Tempfile.new('stdout') @stderr = File.open(null_file, 'w') end it "should create a new process for the command" do Process.expects(:create).with( :command_line => "test command", :startup_info => {:stdin => @stdin, :stdout => @stdout, :stderr => @stderr} ).returns(proc_info_stub) Puppet::Util.execute_windows('test command', {}, @stdin, @stdout, @stderr) end it "should return the pid of the child process" do Puppet::Util.execute_windows('test command', {}, @stdin, @stdout, @stderr).should == pid end it "should quote arguments containing spaces if command is specified as an array" do Process.expects(:create).with do |args| args[:command_line] == '"test command" with some "arguments \"with spaces"' end.returns(proc_info_stub) Puppet::Util.execute_windows(['test command', 'with', 'some', 'arguments "with spaces'], {}, @stdin, @stdout, @stderr) end end describe "#execute" do before :each do Process.stubs(:waitpid2).with(pid).returns([pid, process_status(0)]) end describe "when an execution stub is specified" do before :each do Puppet::Util::ExecutionStub.set do |command,args,stdin,stdout,stderr| "execution stub output" end end it "should call the block on the stub" do Puppet::Util.execute("/usr/bin/run_my_execute_stub").should == "execution stub output" end it "should not actually execute anything" do Puppet::Util.expects(:execute_posix).never Puppet::Util.expects(:execute_windows).never Puppet::Util.execute("/usr/bin/run_my_execute_stub") end end describe "when setting up input and output files" do include PuppetSpec::Files let(:executor) { Puppet.features.microsoft_windows? ? 'execute_windows' : 'execute_posix' } before :each do Puppet::Util.stubs(:wait_for_output) end it "should set stdin to the stdinfile if specified" do input = tmpfile('stdin') FileUtils.touch(input) Puppet::Util.expects(executor).with do |_,_,stdin,_,_| stdin.path == input end.returns(pid) Puppet::Util.execute('test command', :stdinfile => input) end it "should set stdin to the null file if not specified" do Puppet::Util.expects(executor).with do |_,_,stdin,_,_| stdin.path == null_file end.returns(pid) Puppet::Util.execute('test command') end describe "when squelch is set" do it "should set stdout and stderr to the null file" do Puppet::Util.expects(executor).with do |_,_,_,stdout,stderr| stdout.path == null_file and stderr.path == null_file end.returns(pid) Puppet::Util.execute('test command', :squelch => true) end end describe "when squelch is not set" do it "should set stdout to a temporary output file" do outfile = Tempfile.new('stdout') Tempfile.stubs(:new).returns(outfile) Puppet::Util.expects(executor).with do |_,_,_,stdout,_| stdout.path == outfile.path end.returns(pid) Puppet::Util.execute('test command', :squelch => false) end it "should set stderr to the same file as stdout if combine is true" do outfile = Tempfile.new('stdout') Tempfile.stubs(:new).returns(outfile) Puppet::Util.expects(executor).with do |_,_,_,stdout,stderr| stdout.path == outfile.path and stderr.path == outfile.path end.returns(pid) Puppet::Util.execute('test command', :squelch => false, :combine => true) end it "should set stderr to the null device if combine is false" do outfile = Tempfile.new('stdout') Tempfile.stubs(:new).returns(outfile) Puppet::Util.expects(executor).with do |_,_,_,stdout,stderr| stdout.path == outfile.path and stderr.path == null_file end.returns(pid) Puppet::Util.execute('test command', :squelch => false, :combine => false) end end end end describe "after execution" do let(:executor) { Puppet.features.microsoft_windows? ? 'execute_windows' : 'execute_posix' } before :each do Process.stubs(:waitpid2).with(pid).returns([pid, process_status(0)]) Puppet::Util.stubs(executor).returns(pid) end it "should wait for the child process to exit" do Puppet::Util.stubs(:wait_for_output) Process.expects(:waitpid2).with(pid).returns([pid, process_status(0)]) Puppet::Util.execute('test command') end it "should close the stdin/stdout/stderr files used by the child" do stdin = mock 'file', :close stdout = mock 'file', :close stderr = mock 'file', :close File.expects(:open). times(3). returns(stdin). then.returns(stdout). then.returns(stderr) Puppet::Util.execute('test command', :squelch => true) end it "should read and return the output if squelch is false" do stdout = Tempfile.new('test') Tempfile.stubs(:new).returns(stdout) stdout.write("My expected command output") Puppet::Util.execute('test command').should == "My expected command output" end it "should not read the output if squelch is true" do stdout = Tempfile.new('test') Tempfile.stubs(:new).returns(stdout) stdout.write("My expected command output") Puppet::Util.execute('test command', :squelch => true).should == nil end it "should delete the file used for output if squelch is false" do stdout = Tempfile.new('test') path = stdout.path Tempfile.stubs(:new).returns(stdout) Puppet::Util.execute('test command') File.should_not be_exist(path) end it "should raise an error if failonfail is true and the child failed" do Process.expects(:waitpid2).with(pid).returns([pid, process_status(1)]) expect { Puppet::Util.execute('fail command', :failonfail => true) }.to raise_error(Puppet::ExecutionFailure, /Execution of 'fail command' returned 1/) end it "should not raise an error if failonfail is false and the child failed" do Process.expects(:waitpid2).with(pid).returns([pid, process_status(1)]) expect { Puppet::Util.execute('fail command', :failonfail => false) }.not_to raise_error end it "should not raise an error if failonfail is true and the child succeeded" do Process.expects(:waitpid2).with(pid).returns([pid, process_status(0)]) expect { Puppet::Util.execute('fail command', :failonfail => true) }.not_to raise_error end end end end