diff --git a/lib/puppet/type/file.rb b/lib/puppet/type/file.rb index 67bba5c0a..e94761503 100644 --- a/lib/puppet/type/file.rb +++ b/lib/puppet/type/file.rb @@ -1,807 +1,818 @@ require 'digest/md5' require 'cgi' require 'etc' require 'uri' require 'fileutils' 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." newparam(:path) do desc "The path to the file to manage. Must be fully qualified." isnamevar validate do |value| unless value =~ /^#{File::SEPARATOR}/ 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| path, name = File.split(value) { :index => Puppet::FileCollection.collection.index(path), :name => name } end # and the reverse unmunge do |value| File.join( Puppet::FileCollection.collection.path(value[:index]), value[:name] ) end to_canonicalize do |s| # Get rid of any duplicate slashes, and remove any trailing slashes unless # the title is just a slash, in which case leave it. s.gsub(/\/+/, "/").sub(/(.)\/$/,'\1') 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 } The ``puppetmasterd`` 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:: 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 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, and you can restore any given file manually, no matter how old. 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 %s" % value.inspect end end end newparam(:recurse) do desc "Whether and how deeply to do recursive management." 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 any parent directories. autorequire(:file) do basedir = File.dirname(self[:path]) if basedir != self[:path] basedir else nil 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] validate do count = 0 CREATORS.each do |param| count += 1 if self.should(param) end if @parameters.include?(:source) count += 1 end if count > 1 self.fail "You cannot specify more than one of %s" % CREATORS.collect { |p| p.to_s}.join(", ") end if !self[:source] and self[:recurse] == :remote self.fail "You cannot specify a remote recursion without a source" end if !self[:recurse] and self[:recurselimit] self.warning "Possible error: recurselimit is set but not recurse, no recursion will happen" end end def self.[](path) return nil unless path super(path.gsub(/\/+/, '/').sub(/\/$/, '')) end # List files, but only one level deep. def self.instances(base = "/") unless FileTest.directory?(base) return [] end files = [] Dir.entries(base).reject { |e| e == "." or e == ".." }.each do |name| path = File.join(base, name) if obj = self[path] obj[:check] = :all files << obj else files << self.new( :name => path, :check => :all ) end end files end @depthfirst = false # 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. if writeable asuser = self.should(:owner) end end return asuser end def bucket return @bucket if defined?(@bucket) and @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 %s specified in backup" % backup end return default_bucket unless filebucket @bucket = filebucket.bucket return @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 = nil 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 @title = self.class.canonicalize_ref(@title) @stat = nil 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 return 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 = {} if self[:recurse] != :remote children = recurse_local end 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? return false unless @parameters.include?(:recurse) val = @parameters[:recurse].value if val and (val == true or val == :remote) return true else return false end 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 = "%s/%s" % [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.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 %s" % should FileUtils.rmtree(self[:path]) else notice "Not removing directory; use 'force' to override" end when "link", "file" debug "Removing existing %s for replacement with %s" % [s.ftype, should] File.unlink(self[:path]) else self.fail "Could not back up files of type %s" % s.ftype end expire 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 true if s.ftype == "file" return 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" return 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). cached_attr(:stat) do 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 path = self[:path] begin File.send(method, self[:path]) rescue Errno::ENOENT => error return nil rescue Errno::EACCES => error warning "Could not stat; permission denied" return 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 if obj[:target] == :notlink obj.delete(:target) end obj end - # Write out the file. Requires the content to be written, - # the property name for logging, and the checksum for validation. - def write(content, property, checksum = nil) + # 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 = (content.length != 0) + use_temporary_file = write_temporary_file? if use_temporary_file path = "#{self[:path]}.puppettmp_#{rand(10000)}" while File.exists?(path) or File.symlink?(path) path = "#{self[:path]}.puppettmp_#{rand(10000)}" - end - else + end + else path = self[:path] - end + end mode = self.should(:mode) # might be nil umask = mode ? 000 : 022 - Puppet::Util.withumask(umask) do - File.open(path, File::CREAT|File::WRONLY|File::TRUNC, mode) { |f| f.print content } - end + content_checksum = Puppet::Util.withumask(umask) { File.open(path, 'w', mode) { |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) if validate_checksum? + fail_if_checksum_is_wrong(path, content_checksum) if validate_checksum? File.rename(path, self[:path]) rescue => detail - fail "Could not rename temporary file %s to %s : %s" % [path, self[:path], detail] + fail "Could not rename temporary file %s to %s: %s" % [path, 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? - false + self[:checksum] !~ /time/ end # Make sure the file we wrote out is what we think it is. - def fail_if_checksum_is_wrong(path, checksum) - # Use the appropriate checksum type -- md5, md5lite, etc. - checksum = parameter(:checksum).sum(content) - + def fail_if_checksum_is_wrong(path, content_checksum) newsum = parameter(:checksum).sum_file(path) - return if [:absent, nil, checksum].include?(newsum) + return if [:absent, nil, content_checksum].include?(newsum) + + self.fail "File written to disk did not match checksum; discarding changes (%s vs %s)" % [content_checksum, 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 - self.fail "File written to disk did not match checksum; discarding changes (%s vs %s)" % [checksum, newsum] + 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 expire currentvalue = thing.retrieve unless thing.insync?(currentvalue) thing.sync end 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 diff --git a/lib/puppet/type/file/checksum.rb b/lib/puppet/type/file/checksum.rb index d4c24886c..3e2fdbf09 100755 --- a/lib/puppet/type/file/checksum.rb +++ b/lib/puppet/type/file/checksum.rb @@ -1,26 +1,33 @@ require 'puppet/util/checksums' # Specify which checksum algorithm to use when checksumming # files. Puppet::Type.type(:file).newparam(:checksum) do include Puppet::Util::Checksums desc "The checksum type to use when checksumming a file. The default checksum parameter, if checksums are enabled, is md5." newvalues "md5", "md5lite", "timestamp", "mtime", "time", "none" defaultto :md5 def sum(content) type = value || :md5 # because this might be called before defaults are set "{#{type}}" + send(type, content) end def sum_file(path) type = value || :md5 # because this might be called before defaults are set method = type.to_s + "_file" "{#{type}}" + send(method, path).to_s end + + def sum_stream(&block) + type = value || :md5 # same comment as above + method = type.to_s + "_stream" + checksum = send(method, &block) + "{#{type}}#{checksum}" + end end diff --git a/lib/puppet/type/file/content.rb b/lib/puppet/type/file/content.rb index cc2494b00..1817d5646 100755 --- a/lib/puppet/type/file/content.rb +++ b/lib/puppet/type/file/content.rb @@ -1,140 +1,175 @@ +require 'net/http' +require 'uri' + require 'puppet/util/checksums' +require 'puppet/network/http/api/v1' module Puppet Puppet::Type.type(:file).newproperty(:content) do include Puppet::Util::Diff include Puppet::Util::Checksums + include Puppet::Network::HTTP::API::V1 + + attr_reader :actual_content desc "Specify the contents of a file as a string. Newlines, tabs, and spaces can be specified using the escaped syntax (e.g., \\n for a newline). The primary purpose of this parameter is to provide a kind of limited templating:: define resolve(nameserver1, nameserver2, domain, search) { $str = \"search $search domain $domain nameserver $nameserver1 nameserver $nameserver2 \" file { \"/etc/resolv.conf\": content => $str } } This attribute is especially useful when used with `PuppetTemplating templating`:trac:." # Store a checksum as the value, rather than the actual content. # Simplifies everything. munge do |value| if value == :absent value elsif checksum?(value) # XXX This is potentially dangerous because it means users can't write a file whose # entire contents are a plain checksum value else @actual_content = value resource.parameter(:checksum).sum(value) end end # Checksums need to invert how changes are printed. def change_to_s(currentvalue, newvalue) # Our "new" checksum value is provided by the source. if source = resource.parameter(:source) and tmp = source.checksum newvalue = tmp end if currentvalue == :absent return "defined content as '%s'" % [newvalue] elsif newvalue == :absent return "undefined content from '%s'" % [currentvalue] else return "content changed '%s' to '%s'" % [currentvalue, newvalue] end end def checksum_type if source = resource.parameter(:source) result = source.checksum else checksum = resource.parameter(:checksum) result = resource[:checksum] end if result =~ /^\{(\w+)\}.+/ return $1.to_sym else return result end end - # If content was specified, return that; else try to return the source content; - # else, return nil. - def actual_content - if defined?(@actual_content) and @actual_content - return @actual_content - end - - if s = resource.parameter(:source) - return s.content - end - fail "Could not find actual content from checksum" + def length + (actual_content and actual_content.length) || 0 end def content - self.should || (s = resource.parameter(:source) and s.content) + self.should end # Override this method to provide diffs if asked for. # Also, fix #872: when content is used, and replace is true, the file # should be insync when it exists def insync?(is) if resource.should_be_file? return false if is == :absent else return true end return true if ! @resource.replace? return true unless self.should result = super if ! result and Puppet[:show_diff] string_file_diff(@resource[:path], actual_content) end return result end def retrieve return :absent unless stat = @resource.stat ftype = stat.ftype # Don't even try to manage the content on directories or links return nil if ["directory","link"].include?(ftype) begin resource.parameter(:checksum).sum_file(resource[:path]) rescue => detail raise Puppet::Error, "Could not read #{ftype} #{@resource.title}: #{detail}" end end # Make sure we're also managing the checksum property. def should=(value) @resource.newattr(:checksum) unless @resource.parameter(:checksum) super end # Just write our content out to disk. def sync return_event = @resource.stat ? :file_changed : :file_created # We're safe not testing for the 'source' if there's no 'should' # because we wouldn't have gotten this far if there weren't at least # one valid value somewhere. - @resource.write(actual_content, :content) + @resource.write(:content) return return_event end + + def write(file) + self.fail "Writing content that wasn't provided" unless actual_content || resource.parameter(:source) + resource.parameter(:checksum).sum_stream { |sum| + each_chunk_from(actual_content || resource.parameter(:source)) { |chunk| + sum << chunk + file.print chunk + } + } + end + + def each_chunk_from(source_or_content) + if source_or_content.is_a?(String) + yield source_or_content + elsif source_or_content.nil? + nil + elsif source_or_content.local? + File.open(source_or_content.full_path, "r") do |src| + while chunk = src.read(8192) + yield chunk + end + end + else + request = Puppet::Indirector::Request.new(:file_content, :find, source_or_content.full_path) + connection = Puppet::Network::HttpPool.http_instance(source_or_content.server, source_or_content.port) + connection.request_get(indirection2uri(request), {"Accept" => "raw"}) do |response| + case response.code + when "404"; nil + when /^2/; response.read_body { |chunk| yield chunk } + else + # Raise the http error if we didn't get a 'success' of some kind. + message = "Error %s on SERVER: %s" % [response.code, (response.body||'').empty? ? response.message : response.body] + raise Net::HTTPError.new(message, response) + end + end + end + end end end diff --git a/lib/puppet/type/file/ensure.rb b/lib/puppet/type/file/ensure.rb index b2dd39459..83f3d3e6a 100755 --- a/lib/puppet/type/file/ensure.rb +++ b/lib/puppet/type/file/ensure.rb @@ -1,177 +1,177 @@ module Puppet Puppet::Type.type(:file).ensurable do require 'etc' desc "Whether to create files that don't currently exist. Possible values are *absent*, *present*, *file*, and *directory*. Specifying ``present`` will match any form of file existence, and if the file is missing will create an empty file. Specifying ``absent`` will delete the file (and directory if recurse => true). Anything other than those values will be considered to be a symlink. For instance, the following text creates a link:: # Useful on solaris file { \"/etc/inetd.conf\": ensure => \"/etc/inet/inetd.conf\" } You can make relative links:: # Useful on solaris file { \"/etc/inetd.conf\": ensure => \"inet/inetd.conf\" } If you need to make a relative link to a file named the same as one of the valid values, you must prefix it with ``./`` or something similar. You can also make recursive symlinks, which will create a directory structure that maps to the target directory, with directories corresponding to each directory and links corresponding to each file." # Most 'ensure' properties have a default, but with files we, um, don't. nodefault newvalue(:absent) do File.unlink(@resource[:path]) end aliasvalue(:false, :absent) newvalue(:file, :event => :file_created) do # Make sure we're not managing the content some other way if property = @resource.property(:content) property.sync else - @resource.write("", :ensure) + @resource.write(:ensure) mode = @resource.should(:mode) end end #aliasvalue(:present, :file) newvalue(:present, :event => :file_created) do # Make a file if they want something, but this will match almost # anything. set_file end newvalue(:directory, :event => :directory_created) do mode = @resource.should(:mode) parent = File.dirname(@resource[:path]) unless FileTest.exists? parent raise Puppet::Error, "Cannot create %s; parent directory %s does not exist" % [@resource[:path], parent] end if mode Puppet::Util.withumask(000) do Dir.mkdir(@resource[:path],mode) end else Dir.mkdir(@resource[:path]) end @resource.send(:property_fix) return :directory_created end newvalue(:link, :event => :link_created) do fail "Cannot create a symlink without a target" unless property = resource.property(:target) property.retrieve property.mklink end # Symlinks. newvalue(/./) do # This code never gets executed. We need the regex to support # specifying it, but the work is done in the 'symlink' code block. end munge do |value| value = super(value) value,resource[:target] = :link,value unless value.is_a? Symbol resource[:links] = :manage if value == :link and resource[:links] != :follow value end def change_to_s(currentvalue, newvalue) return super unless newvalue.to_s == "file" return super unless property = @resource.property(:content) # We know that content is out of sync if we're here, because # it's essentially equivalent to 'ensure' in the transaction. if source = @resource.parameter(:source) should = source.checksum else should = property.should end if should == :absent is = property.retrieve else is = :absent end return property.change_to_s(is, should) end # Check that we can actually create anything def check basedir = File.dirname(@resource[:path]) if ! FileTest.exists?(basedir) raise Puppet::Error, "Can not create %s; parent directory does not exist" % @resource.title elsif ! FileTest.directory?(basedir) raise Puppet::Error, "Can not create %s; %s is not a directory" % [@resource.title, dirname] end end # We have to treat :present specially, because it works with any # type of file. def insync?(currentvalue) unless currentvalue == :absent or resource.replace? return true end if self.should == :present if currentvalue.nil? or currentvalue == :absent return false else return true end else return super(currentvalue) end end def retrieve if stat = @resource.stat(false) return stat.ftype.intern else if self.should == :false return :false else return :absent end end end def sync @resource.remove_existing(self.should) if self.should == :absent return :file_removed end event = super return event end end end diff --git a/lib/puppet/type/file/source.rb b/lib/puppet/type/file/source.rb index b3d9b3ec2..1eb418e90 100755 --- a/lib/puppet/type/file/source.rb +++ b/lib/puppet/type/file/source.rb @@ -1,179 +1,193 @@ 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 ``puppetd`` will fill in the name of its configuration server and ``puppet`` 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``) and the client demon (``puppetd``) differs slightly: ``puppet`` will look such a file up on the module path on the local host, whereas ``puppetd`` will connect to the puppet server that it received the manifest from. See the `FileServingConfiguration fileserver configuration documentation`:trac: 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| begin uri = URI.parse(URI.escape(source)) rescue => detail self.fail "Could not understand source %s: %s" % [source, detail.to_s] end unless uri.scheme.nil? or %w{file puppet}.include?(uri.scheme) self.fail "Cannot use URLs of type '%s' as source for fileserving" % [uri.scheme] end end end munge do |sources| sources = [sources] unless sources.is_a?(Array) sources.collect { |source| source.sub(/\/$/, '') } end def change_to_s(currentvalue, newvalue) # newvalue = "{md5}" + @metadata.checksum if @resource.property(:ensure).retrieve == :absent return "creating from source %s with contents %s" % [metadata.source, metadata.checksum] else return "replacing from source %s with contents %s" % [metadata.source, metadata.checksum] end end def checksum metadata && metadata.checksum end - # Look up (if necessary) and return remote content. - cached_attr(:content) do - raise Puppet::DevError, "No source for content was stored with the metadata" unless metadata.source - - unless tmp = Puppet::FileServing::Content.find(metadata.source) - fail "Could not find any content at %s" % metadata.source - end - 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::Util::SUIDManager.uid != 0 next if metadata_method == :checksum and metadata.ftype == "directory" if resource[param_name].nil? or resource[param_name] == :absent v = metadata.send(metadata_method) resource.info "Setting #{param_name} to #{v}" 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 pinparams [:mode, :type, :owner, :group, :content] end def found? ! (metadata.nil? or metadata.ftype.nil?) end # 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. cached_attr(:metadata) do return nil unless value result = nil value.each do |source| begin if data = Puppet::FileServing::Metadata.find(source) result = data result.source = source break end rescue => detail fail detail, "Could not retrieve file metadata for %s: %s" % [source, detail] end end fail "Could not retrieve information from source(s) %s" % value.join(", ") unless result result end # Make sure we're also checking the checksum def value=(value) super checks = (pinparams + [:ensure]) checks.delete(:checksum) resource[:check] = checks resource[:checksum] = :md5 unless resource.property(:checksum) end + + def local? + found? and uri and (uri.scheme || "file") == "file" + end + + def full_path + if found? and uri + return URI.unescape(uri.path) + end + 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 + @uri ||= URI.parse(URI.escape(metadata.source)) + end end end diff --git a/lib/puppet/util/checksums.rb b/lib/puppet/util/checksums.rb index f68f77624..e534fb0fb 100644 --- a/lib/puppet/util/checksums.rb +++ b/lib/puppet/util/checksums.rb @@ -1,95 +1,120 @@ # A stand-alone module for calculating checksums # in a generic way. module Puppet::Util::Checksums # Is the provided string a checksum? def checksum?(string) string =~ /^\{(\w{3,5})\}\S+/ end # Strip the checksum type from an existing checksum def sumtype(checksum) if checksum =~ /^\{(\w+)\}/ return $1 else return nil end end # Calculate a checksum using Digest::MD5. def md5(content) require 'digest/md5' Digest::MD5.hexdigest(content) end # Calculate a checksum of the first 500 chars of the content using Digest::MD5. def md5lite(content) md5(content[0..511]) end # Calculate a checksum of a file's content using Digest::MD5. def md5_file(filename, lite = false) require 'digest/md5' digest = Digest::MD5.new() return checksum_file(digest, filename, lite) end # Calculate a checksum of the first 500 chars of a file's content using Digest::MD5. def md5lite_file(filename) md5_file(filename, true) end + def md5_stream(&block) + require 'digest/md5' + digest = Digest::MD5.new() + yield digest + return digest.hexdigest + end + + alias :md5lite_stream :md5_stream + # Return the :mtime timestamp of a file. def mtime_file(filename) File.stat(filename).send(:mtime) end + # by definition this doesn't exist + def mtime_stream + nil + end + + alias :ctime_stream :mtime_stream + # Calculate a checksum using Digest::SHA1. def sha1(content) require 'digest/sha1' Digest::SHA1.hexdigest(content) end # Calculate a checksum of the first 500 chars of the content using Digest::SHA1. def sha1lite(content) sha1(content[0..511]) end # Calculate a checksum of a file's content using Digest::SHA1. def sha1_file(filename, lite = false) require 'digest/sha1' digest = Digest::SHA1.new() return checksum_file(digest, filename, lite) end # Calculate a checksum of the first 500 chars of a file's content using Digest::SHA1. def sha1lite_file(filename) sha1_file(filename, true) end + def sha1_stream + require 'digest/sha1' + digest = Digest::SHA1.new() + yield digest + return digest.hexdigest + end + + alias :sha1lite_stream :sha1_stream + # Return the :ctime of a file. def ctime_file(filename) File.stat(filename).send(:ctime) end # Return a "no checksum" def none_file(filename) "" end private # Perform an incremental checksum on a file. def checksum_file(digest, filename, lite = false) buffer = lite ? 512 : 4096 File.open(filename, 'r') do |file| while content = file.read(buffer) digest << content break if lite end end return digest.hexdigest end end diff --git a/spec/integration/type/file.rb b/spec/integration/type/file.rb index 6222f0a89..0724467cb 100755 --- a/spec/integration/type/file.rb +++ b/spec/integration/type/file.rb @@ -1,483 +1,483 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet_spec/files' describe Puppet::Type.type(:file) do include PuppetSpec::Files 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 => "/my/file", :mode => "755" catalog = Puppet::Resource::Catalog.new catalog.add_resource file file.parameter(:mode).expects(:retrieve).never transaction = Puppet::Transaction.new(catalog) transaction.resource_harness.evaluate(file).should_not be_failed end describe "when writing files" do it "should backup files to a filebucket when one is configured" 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 catalog.add_resource file, bucket 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" 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 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 catalog.add_resource file File.open(file[:path], "w") { |f| f.puts "bar" } File.chmod(0111, dir) # make it non-writeable Puppet::Util::Log.stubs(:newmessage) catalog.apply File.read(file[:path]).should == "bar\n" end it "should not backup symlinks" 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 catalog.add_resource file, 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 catalog.add_resource file Dir.mkdir(file[:path]) otherfile = File.join(file[:path], "foo") File.open(otherfile, "w") { |f| f.print "yay" } catalog.apply backup = file[:path] + ".bak" FileTest.should be_directory(backup) File.read(File.join(backup, "foo")).should == "yay" end it "should backup directories to filebuckets by backing up each file separately" do bucket = Puppet::Type.type(:filebucket).new :path => tmpfile("filebucket"), :name => "mybucket" file = Puppet::Type.type(:file).new :path => tmpfile("bucket_backs"), :backup => "mybucket", :content => "foo", :force => true catalog = Puppet::Resource::Catalog.new catalog.add_resource file, 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" catalog = Puppet::Resource::Catalog.new catalog.add_resource file File.open(file[:path], "w") { |f| f.print "bar" } File.expects(:rename).raises ArgumentError - lambda { file.write("something", :content) }.should raise_error(Puppet::Error) + lambda { file.write(:content) }.should raise_error(Puppet::Error) File.read(file[: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 %s" % file } File.chmod(0640, ffile) end end end it "should be able to recurse over a nonexistent file" do @path = tmpfile("file_integration_tests") @file = Puppet::Type::File.new(:name => @path, :mode => 0644, :recurse => true, :backup => false) @catalog = Puppet::Resource::Catalog.new @catalog.add_resource @file lambda { @file.eval_generate }.should_not raise_error end it "should be able to recursively set properties on existing files" do @path = tmpfile("file_integration_tests") build_path(@path) @file = Puppet::Type::File.new(:name => @path, :mode => 0644, :recurse => true, :backup => false) @catalog = Puppet::Resource::Catalog.new @catalog.add_resource @file @catalog.apply @dirs.each do |path| (File.stat(path).mode & 007777).should == 0755 end @files.each do |path| (File.stat(path).mode & 007777).should == 0644 end end it "should be able to recursively make links to other files" 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) @catalog = Puppet::Resource::Catalog.new @catalog.add_resource @file @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" 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) @catalog = Puppet::Resource::Catalog.new @catalog.add_resource @file @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") @catalog = Puppet::Resource::Catalog.new @catalog.add_resource base @catalog.add_resource sub @catalog.apply (File.stat(file).mode & 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(dir, "file") generated = File.join(dir, "file_with_a_name_starting_with_the_word_file") FileUtils.mkdir_p(dir) File.open(managed, "w") { |f| f.puts "" } File.open(generated, "w") { |f| f.puts "" } @catalog = Puppet::Resource::Catalog.new @catalog.add_resource Puppet::Type::File.new(:name => dir, :recurse => true, :backup => false, :mode => "755") @catalog.add_resource Puppet::Type::File.new(:name => managed, :recurse => true, :backup => false, :mode => "644") @catalog.apply (File.stat(generated).mode & 007777).should == 0755 end end describe "when generating resources" do before do @source = tmpfile("generating_in_catalog_source") @dest = tmpfile("generating_in_catalog_dest") Dir.mkdir(@source) 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) @catalog = Puppet::Resource::Catalog.new @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) 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.should be_edge(@file, one) two = @catalog.resource(:file, File.join(@dest, "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) 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) catalog = Puppet::Resource::Catalog.new catalog.add_resource file catalog.apply File.read(dest).should == "foo" (File.stat(dest).mode & 007777).should == 0755 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) 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, :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" 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, :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" 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 = Puppet::Type.type(:file).new( :name => dest, :ensure => :absent, :source => source, :backup => false ) catalog = Puppet::Resource::Catalog.new catalog.add_resource file catalog.apply File.should_not be_exist(dest) 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" } # this file should get removed File.open(@purgee, "w") { |f| f.puts "footest" } @lfobj = Puppet::Type.newfile( :title => "localfile", :path => @localfile, :content => "rahtest\n", :ensure => :file, :backup => false ) @destobj = Puppet::Type.newfile(:title => "destdir", :path => @destdir, :source => @sourcedir, :backup => false, :purge => true, :recurse => true) @catalog = Puppet::Resource::Catalog.new @catalog.add_resource @lfobj, @destobj end it "should still copy remote files" do @catalog.apply FileTest.should be_exist(@copiedfile) end it "should not purge managed, local files" do @catalog.apply FileTest.should be_exist(@localfile) 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/unit/type/file.rb b/spec/unit/type/file.rb index f9c691b11..4213753a4 100755 --- a/spec/unit/type/file.rb +++ b/spec/unit/type/file.rb @@ -1,861 +1,874 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' describe Puppet::Type.type(:file) do before do Puppet.settings.stubs(:use) @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 end end [false, "false", 0].each do |value| it "should consider #{value} to disable recursion" do @file[:recurse] = value @file.should_not be_recurse 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") - lambda { file.write("something", :content) }.should raise_error(Puppet::Error) + file.stubs(:validate_checksum?).returns(false) + + property = stub('content_property', :actual_content => "something", :length => "something".length) + file.stubs(:property).with(:content).returns(property) + + lambda { file.write(:content) }.should raise_error(Puppet::Error) + 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) + + property.expects(:write).with(filehandle) + + file.write(:content) end describe "when validating the checksum" do before { @file.stubs(:validate_checksum?).returns(true) } - it "should fail if the checksum property and content checksums do not match" do - property = stub('checksum_property', :checktype => :md5, :md5 => 'checksum_a', :getsum => 'checksum_b') - @file.stubs(:property).with(:checksum).returns(property) + it "should fail if the checksum parameter and content checksums do not match" do + checksum = stub('checksum_parameter', :sum => 'checksum_b') + @file.stubs(:parameter).with(:checksum).returns(checksum) - @file.stubs(:validate_checksum?).returns(true) - lambda { @file.write "something", :NOTUSED }.should raise_error(Puppet::Error) + 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 fail if the checksum property and content checksums do not match" do - property = stub('checksum_property', :checktype => :md5, :md5 => 'checksum_a', :getsum => 'checksum_b') - @file.stubs(:property).with(:checksum).returns(property) + checksum = stub('checksum_parameter', :sum => 'checksum_b') + @file.stubs(:parameter).with(:checksum).returns(checksum) + + property = stub('content_property', :actual_content => "something", :length => "something".length, :write => 'checksum_a') + @file.stubs(:property).with(:content).returns(property) - lambda { @file.write "something", :NOTUSED }.should_not raise_error(Puppet::Error) + lambda { @file.write :NOTUSED }.should_not raise_error(Puppet::Error) end end end it "should have a method for determining if the file is present" do @file.must respond_to(:exist?) end 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 be considered nonexistent if it can not be stat'ed" do @file.expects(:stat).returns nil @file.must_not be_exist end 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 be a file if :ensure is set to :file" do @file[:ensure] = :file @file.must be_should_be_file 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 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 be a file if :ensure is not set and :content is" do @file[:content] = "foo" @file.must be_should_be_file 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 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 it "should autorequire its parent directory" do catalog = Puppet::Resource::Catalog.new file = Puppet::Type::File.new(:name => "/foo/bar") dir = Puppet::Type::File.new(:name => "/foo") catalog.add_resource file catalog.add_resource dir reqs = file.autorequire reqs[0].source.must == dir reqs[0].target.must == file end it "should not autorequire its parent dir if its parent dir is itself" do catalog = Puppet::Resource::Catalog.new file = Puppet::Type::File.new(:name => "/") catalog.add_resource file file.autorequire.should be_empty 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 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 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 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 have its 'path' attribute set as its namevar" do Puppet::Type.type(:file).namevar.should == :path end end describe "when managing links" do require 'puppettest/support/assertions' include PuppetTest require 'tempfile' before do @basedir = tempfile Dir.mkdir(@basedir) @file = File.join(@basedir, "file") @link = File.join(@basedir, "link") File.open(@file, "w", 0644) { |f| f.puts "yayness"; f.flush } File.symlink(@file, @link) @resource = Puppet::Type.type(:file).new( :path => @link, :mode => "755" ) @catalog.add_resource @resource end after do remove_tmp_files 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. ("%o" % (File.stat(@file).mode & 007777)).should == "%o" % 0644 end it "should be able to follow links" do @resource[:links] = :follow @catalog.apply ("%o" % (File.stat(@file).mode & 007777)).should == "%o" % 0755 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) 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 it "should use :stat if it is following links" do @resource[:links] = :follow File.expects(:stat) @resource.stat end it "should use :lstat if is it not following links" do @resource[:links] = :manage File.expects(:lstat) @resource.stat end it "should stat the path of the file" do File.expects(:lstat).with("/foo/bar") @resource.stat 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 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 return nil if the file cannot be stat'ed" do File.expects(:lstat).raises(Errno::EACCES) @resource.stat.should be_nil end it "should return the stat instance" do File.expects(:lstat).returns "mystat" @resource.stat.should == "mystat" end it "should cache the stat instance if it has a catalog and is applying" do stat = mock 'stat' File.expects(:lstat).returns stat catalog = Puppet::Resource::Catalog.new @resource.catalog = catalog catalog.stubs(:applying?).returns true @resource.stat.should equal(@resource.stat) 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 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" end end it "should have a method for performing recursion" do @file.must respond_to(:perform_recursion) end describe "when executing a recursive search" do it "should use Metadata to do its recursion" do Puppet::FileServing::Metadata.expects(:search) @file.perform_recursion(@file[:path]) end it "should use the provided path as the key to the search" do Puppet::FileServing::Metadata.expects(:search).with { |key, options| key == "/foo" } @file.perform_recursion("/foo") end it "should return the results of the metadata search" do Puppet::FileServing::Metadata.expects(:search).returns "foobar" @file.perform_recursion(@file[:path]).should == "foobar" end it "should pass its recursion value to the search" do @file[:recurse] = true Puppet::FileServing::Metadata.expects(:search).with { |key, options| options[:recurse] == true } @file.perform_recursion(@file[:path]) end it "should pass true if recursion is remote" do @file[:recurse] = :remote Puppet::FileServing::Metadata.expects(:search).with { |key, options| options[:recurse] == true } @file.perform_recursion(@file[:path]) end it "should pass its recursion limit value to the search" do @file[:recurselimit] = 10 Puppet::FileServing::Metadata.expects(:search).with { |key, options| options[:recurselimit] == 10 } @file.perform_recursion(@file[:path]) end it "should configure the search to ignore or manage links" do @file[:links] = :manage Puppet::FileServing::Metadata.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.expects(:search).with { |key, options| options[:ignore] == %w{.svn CVS} } @file.perform_recursion(@file[:path]) 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 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.expects(:perform_recursion).returns [@metadata] @file.expects(:newchild).never @file.recurse_local 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 it "should set checksum_type to none if this file checksum is none" do @file[:checksum] = :none Puppet::FileServing::Metadata.expects(:search).with { |path,params| params[:checksum_type] == :none }.returns [@metadata] @file.expects(:newchild).with("my/file").returns "fiebar" @file.recurse_local end end it "should have a method for performing link recursion" do @file.must respond_to(:recurse_link) end describe "when doing link recursion" 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({}) 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({}) 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) 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) 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) 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) 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} end end it "should have a method for performing remote recursion" do @file.must respond_to(:recurse_remote) end describe "when doing remote recursion" do before do @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({}) 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({}) 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({}) 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} 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) end it "should set the source of each resource to the source of the metadata" do @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) 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] @resource.stubs(:[]=) @resource.expects(:[]=).with(:checksum, :md5) @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] @resource.expects(:parameter).with(:source).returns @parameter @parameter.expects(:metadata=).with(@first) @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.expects(:newchild).never @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.parameter(:source).expects(:metadata=).with @first @file.recurse_remote("first" => @resource) end describe "and multiple sources are provided" do 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({}) end end describe "and :sourceselect is set to :all" do before do @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 one = [klass.new("/one", :relative_path => "a")] @file.expects(:perform_recursion).with("/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 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("/four").returns [] @file.recurse_remote({}) end end end end describe "when returning resources with :eval_generate" do before do @graph = stub 'graph', :add_edge => nil @catalog.stubs(:relationship_graph).returns @graph @file.catalog = @catalog @file[:recurse] = true 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] end it "should not recurse if recursion is disabled" do @file.expects(:recurse?).returns false @file.expects(:recurse).never @file.eval_generate.should == [] end it "should return each resource found through recursion" do foo = stub 'foo', :[] => "/foo" bar = stub 'bar', :[] => "/bar" bar2 = stub 'bar2', :[] => "/bar" @file.expects(:recurse).returns [foo, bar] @file.eval_generate.should == [foo, bar] end end describe "when recursing" do before do @file[:recurse] = true @metadata = Puppet::FileServing::Metadata end describe "and a source is set" do before { @file[:source] = "/my/source" } 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 end describe "and a target is set" do before { @file[:target] = "/link/target" } it "should use recurse_link" do @file.stubs(:recurse_local).returns(:foo => "bar") @file.expects(:recurse_link).with(:foo => "bar").returns [] @file.recurse end end it "should use recurse_local if recurse is not remote" do @file.expects(:recurse_local).returns({}) @file.recurse end it "should not use recurse_local if recurse remote" do @file[:recurse] = :remote @file.expects(:recurse_local).never @file.recurse 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] end describe "and purging is enabled" do before do @file[:purge] = true 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) @file.recurse end it "should not remove files that exist in the remote repository" do @file["source"] = "/my/file" @file.expects(:recurse_local).returns({}) remote = stub 'remote' remote.stubs(:[]).with(:source).returns "/whatever" # Thus, a remote file remote.stubs(:[]).with(:path).returns "foo" @file.expects(:recurse_remote).with { |hash| hash["remote"] = remote } remote.expects(:[]=).with(:ensure, :absent).never @file.recurse end 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 {: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.class.expects(:new).with { |params| params[param].nil? } @file.newchild("sub/file") end 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 it "should not copy default values to the new child" do @file.class.expects(:new).with { |params| params[:backup].nil? } @file.newchild("my/path") end 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 @file.parameter(:source).copy_source_values @file.class.expects(:new).with { |params| params[:group].nil? } @file.newchild("my/path") 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" end it "should allow setting backup to 'false'" do Puppet::Type::File.new(:name => "/my/file", :backup => false)[:backup].should be_false 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" end it "should support any other backup extension" do Puppet::Type::File.new(:name => "/my/file", :backup => ".bak")[:backup].should == ".bak" end 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 = Puppet::Type::File.new(:name => "/my/file") catalog.add_resource file file[:backup] = "foo" file.bucket.should == bucket_resource.bucket end it "should find filebuckets added to the catalog after the file resource was created" do catalog = Puppet::Resource::Catalog.new file = Puppet::Type::File.new(:name => "/my/file", :backup => "foo") catalog.add_resource file bucket_resource = Puppet::Type.type(:filebucket).new :name => "foo", :path => "/my/file/bucket" catalog.add_resource bucket_resource file.bucket.should == bucket_resource.bucket 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 file = Puppet::Type::File.new(:name => "/my/file", :backup => false) catalog.add_resource file file.bucket.should be_nil end 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 file = Puppet::Type::File.new(:name => "/my/file", :backup => ".foo") catalog.add_resource file file.bucket.should be_nil end it "should fail if there's no catalog and backup is not false" do file = Puppet::Type::File.new(:name => "/my/file", :backup => "foo") lambda { file.bucket }.should raise_error(Puppet::Error) 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 lambda { file.bucket }.should raise_error(Puppet::Error) 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) 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 end end - describe "when writing the file" 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") - - lambda { file.write("something", :content) }.should raise_error(Puppet::Error) - 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 end end end diff --git a/spec/unit/type/file/checksum.rb b/spec/unit/type/file/checksum.rb index 8f0f84290..e6a853042 100644 --- a/spec/unit/type/file/checksum.rb +++ b/spec/unit/type/file/checksum.rb @@ -1,59 +1,73 @@ #!/usr/bin/env ruby Dir.chdir(File.dirname(__FILE__)) { (s = lambda { |f| File.exist?(f) ? require(f) : Dir.chdir("..") { s.call(f) } }).call("spec/spec_helper.rb") } checksum = Puppet::Type.type(:file).attrclass(:checksum) describe checksum do before do @resource = Puppet::Type.type(:file).new :path => "/foo/bar" @checksum = @resource.parameter(:checksum) end it "should be a parameter" do checksum.superclass.must == Puppet::Parameter end it "should use its current value when asked to sum content" do @checksum.value = :md5lite @checksum.expects(:md5lite).with("foobar").returns "yay" @checksum.sum("foobar") end it "should use :md5 to sum when no value is set" do @checksum.expects(:md5).with("foobar").returns "yay" @checksum.sum("foobar") end it "should return the summed contents with a checksum label" do sum = Digest::MD5.hexdigest("foobar") @resource[:checksum] = :md5 @checksum.sum("foobar").should == "{md5}#{sum}" end it "should use :md5 as its default type" do @checksum.default.should == :md5 end it "should use its current value when asked to sum a file's content" do @checksum.value = :md5lite @checksum.expects(:md5lite_file).with("/foo/bar").returns "yay" @checksum.sum_file("/foo/bar") end it "should use :md5 to sum a file when no value is set" do @checksum.expects(:md5_file).with("/foo/bar").returns "yay" @checksum.sum_file("/foo/bar") end it "should convert all sums to strings when summing files" do @checksum.value = :mtime @checksum.expects(:mtime_file).with("/foo/bar").returns Time.now lambda { @checksum.sum_file("/foo/bar") }.should_not raise_error end it "should return the summed contents of a file with a checksum label" do @resource[:checksum] = :md5 @checksum.expects(:md5_file).returns "mysum" @checksum.sum_file("/foo/bar").should == "{md5}mysum" end + + it "should return the summed contents of a stream with a checksum label" do + @resource[:checksum] = :md5 + @checksum.expects(:md5_stream).returns "mysum" + @checksum.sum_stream.should == "{md5}mysum" + end + + it "should yield the sum_stream block to the underlying checksum" do + @resource[:checksum] = :md5 + @checksum.expects(:md5_stream).yields("something").returns("mysum") + @checksum.sum_stream do |sum| + sum.should == "something" + end + end end diff --git a/spec/unit/type/file/content.rb b/spec/unit/type/file/content.rb index 5a3a8598d..96f037f7d 100755 --- a/spec/unit/type/file/content.rb +++ b/spec/unit/type/file/content.rb @@ -1,256 +1,386 @@ #!/usr/bin/env ruby Dir.chdir(File.dirname(__FILE__)) { (s = lambda { |f| File.exist?(f) ? require(f) : Dir.chdir("..") { s.call(f) } }).call("spec/spec_helper.rb") } content = Puppet::Type.type(:file).attrclass(:content) describe content do before do @resource = Puppet::Type.type(:file).new :path => "/foo/bar" end it "should be a subclass of Property" do content.superclass.must == Puppet::Property end describe "when determining the checksum type" do before do @resource = Puppet::Type.type(:file).new :path => "/foo/bar" end it "should use the type specified in the source checksum if a source is set" do @resource[:source] = "/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 before do @resource = Puppet::Type.type(:file).new :path => "/foo/bar" end 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 use the content from the source if the source is set" do + it "should not use the content from the source if the source is set" do source = mock 'source' - source.expects(:content).returns "scont" - @resource.expects(:parameter).with(:source).returns source + @resource.expects(:parameter).never.with(:source).returns source @content = content.new(:resource => @resource) - @content.actual_content.should == "scont" - end - - it "should fail if no source is available and no content is set" do - @content = content.new(:resource => @resource) - lambda { @content.actual_content }.should raise_error(Puppet::Error) + @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}%s" % 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.must be_insync("whatever") end it "should return false if the current content is :absent" do @content.should_not be_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_not be_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_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_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(:string_file_diff).once @content.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(:string_file_diff).never @content.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_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_insync("something") end it "should not be insync if the file does not exist" do @content.should_not be_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("some content", :content) + @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) + @fh = stub_everything + end + + it "should fail if no actual content nor source exists" do + lambda { @content.write(@fh) }.should raise_error + 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 local source" do + before(:each) do + @content.stubs(:actual_content).returns(nil) + @source = stub_everything 'source', :local? => true, :full_path => "/path/to/source" + @resource.stubs(:parameter).with(:source).returns @source + + @sum = stub_everything 'sum' + @resource.stubs(:parameter).with(:checksum).returns(@sum) + + @digest = stub_everything 'digest' + @sum.stubs(:sum_stream).yields(@digest) + + @file = stub_everything 'file' + File.stubs(:open).yields(@file) + @file.stubs(:read).with(8192).returns("chunk1").then.returns("chunk2").then.returns(nil) + end + + it "should open the local file" do + File.expects(:open).with("/path/to/source", "r") + @content.write(@fh) + end + + it "should read the local file by chunks" do + @file.expects(:read).with(8192).returns("chunk1").then.returns(nil) + @content.write(@fh) + end + + it "should write each chunk to the file" do + @fh.expects(:print).with("chunk1").then.with("chunk2") + @content.write(@fh) + end + + it "should pass each chunk to the current sum stream" do + @digest.expects(:<<).with("chunk1").then.with("chunk2") + @content.write(@fh) + end + + it "should return the checksum computed" do + @sum.stubs(:sum_stream).yields(@digest).returns("checksum") + @content.write(@fh).should == "checksum" + end + end + + describe "from remote source" do + before(:each) do + @response = stub_everything 'mock response', :code => "404" + @conn = stub_everything 'connection' + @conn.stubs(:request_get).yields(@response) + Puppet::Network::HttpPool.stubs(:http_instance).returns @conn + + @content.stubs(:actual_content).returns(nil) + @source = stub_everything 'source', :local? => false, :full_path => "/path/to/source", :server => "server", :port => 1234 + @resource.stubs(:parameter).with(:source).returns @source + + @sum = stub_everything 'sum' + @resource.stubs(:parameter).with(:checksum).returns(@sum) + + @digest = stub_everything 'digest' + @sum.stubs(:sum_stream).yields(@digest) + end + + it "should open a network connection to source server and port" do + Puppet::Network::HttpPool.expects(:http_instance).with("server", 1234).returns @conn + @content.write(@fh) + end + + it "should send the correct indirection uri" do + @conn.expects(:request_get).with { |uri,headers| uri == "/production/file_content//path/to/source" }.yields(@response) + @content.write(@fh) + end + + it "should return nil if source is not found" do + @response.expects(:code).returns("404") + @content.write(@fh).should == nil + end + + it "should not write anything if source is not found" do + @response.expects(:code).returns("404") + @fh.expects(:print).never + @content.write(@fh).should == nil + end + + it "should raise an HTTP error in case of server error" do + @response.expects(:code).returns("500") + lambda { @content.write(@fh) }.should raise_error + end + + it "should write content by chunks" do + @response.expects(:code).returns("200") + @response.expects(:read_body).multiple_yields("chunk1","chunk2") + @fh.expects(:print).with("chunk1").then.with("chunk2") + @content.write(@fh) + end + + it "should pass each chunk to the current sum stream" do + @response.expects(:code).returns("200") + @response.expects(:read_body).multiple_yields("chunk1","chunk2") + @digest.expects(:<<).with("chunk1").then.with("chunk2") + @content.write(@fh) + end + + it "should return the checksum computed" do + @response.expects(:code).returns("200") + @response.expects(:read_body).multiple_yields("chunk1","chunk2") + @sum.expects(:sum_stream).yields(@digest).returns("checksum") + @content.write(@fh).should == "checksum" + end + end + end end diff --git a/spec/unit/type/file/source.rb b/spec/unit/type/file/source.rb index e4e9ad9d1..b9bb22240 100755 --- a/spec/unit/type/file/source.rb +++ b/spec/unit/type/file/source.rb @@ -1,248 +1,272 @@ #!/usr/bin/env ruby Dir.chdir(File.dirname(__FILE__)) { (s = lambda { |f| File.exist?(f) ? require(f) : Dir.chdir("..") { s.call(f) } }).call("spec/spec_helper.rb") } source = Puppet::Type.type(:file).attrclass(:source) describe Puppet::Type.type(:file).attrclass(:source) do before do # Wow that's a messy interface to the resource. @resource = stub 'resource', :[]= => nil, :property => nil, :catalog => stub("catalog", :dependent_data_expired? => false) end it "should be a subclass of Parameter" do source.superclass.must == Puppet::Parameter end describe "when initializing" do 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) end it "should fail if the URI is not a local file, file URI, or puppet URI" do s = source.new(:resource => @resource) lambda { s.value = %w{http://foo/bar} }.must raise_error(Puppet::Error) end end it "should have a method for retrieving its metadata" do source.new(:resource => @resource).must respond_to(:metadata) end it "should have a method for setting its metadata" do source.new(:resource => @resource).must respond_to(:metadata=) end describe "when returning the metadata" 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 => "/foo/bar") Puppet::FileServing::Metadata.expects(:find).with("/foo/bar").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 => ["/foo/bar", "/fee/booz"]) Puppet::FileServing::Metadata.expects(:find).with("/foo/bar").returns nil Puppet::FileServing::Metadata.expects(:find).with("/fee/booz").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 => "/foo/bar") Puppet::FileServing::Metadata.expects(:find).with("/foo/bar").returns metadata metadata.expects(:source=).with("/foo/bar") @source.metadata end it "should fail intelligently if an exception is encountered while querying for metadata" do @source = source.new(:resource => @resource, :value => "/foo/bar") Puppet::FileServing::Metadata.expects(:find).with("/foo/bar").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 => "/foo/bar") Puppet::FileServing::Metadata.expects(:find).with("/foo/bar").returns nil @source.expects(:fail).raises RuntimeError lambda { @source.metadata }.should raise_error(RuntimeError) end it "should expire the metadata appropriately" do expirer = stub 'expired', :dependent_data_expired? => true metadata = stub 'metadata', :source= => nil Puppet::FileServing::Metadata.expects(:find).with("/fee/booz").returns metadata @source = source.new(:resource => @resource, :value => ["/fee/booz"]) @source.metadata = "foo" @source.stubs(:expirer).returns expirer @source.metadata.should_not == "foo" 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 => "/foo/bar" @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::Util::SUIDManager.expects(:uid).returns 0 @source.copy_source_values @resource[:owner].must == 100 @resource[:group].must == 200 @resource[:mode].must == 123 # 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::Util::SUIDManager.expects(:uid).returns 100 @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" @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 method for returning the content" do - source.new(:resource => @resource).must respond_to(:content) + it "should have a local? method" do + source.new(:resource => @resource).must be_respond_to(:local?) end - describe "when looking up the content" do - before do + context "when accessing source properties" do + before(:each) do @source = source.new(:resource => @resource) - @metadata = stub 'metadata', :source => "/my/source" - @source.stubs(:metadata).returns @metadata - - @content = stub 'content', :content => "foobar" + @metadata = stub_everything + @source.stubs(:metadata).returns(@metadata) end - it "should fail if the metadata does not have a source set" do - @metadata.stubs(:source).returns nil - lambda { @source.content }.should raise_error(Puppet::DevError) - end + describe "for local sources" do + before(:each) do + @metadata.stubs(:ftype).returns "file" + @metadata.stubs(:source).returns("file:///path/to/source") + end - it "should look the content up from the Content class using the metadata source if no content is set" do - Puppet::FileServing::Content.expects(:find).with("/my/source").returns @content - @source.content.should == "foobar" - end + it "should be local" do + @source.must be_local + end - it "should return previously found content" do - Puppet::FileServing::Content.expects(:find).with("/my/source").returns @content - @source.content.should == "foobar" - @source.content.should == "foobar" - end + it "should be local if there is no scheme" do + @metadata.stubs(:source).returns("/path/to/source") + @source.must be_local + end - it "should fail if no content can be retrieved" do - Puppet::FileServing::Content.expects(:find).with("/my/source").returns nil - @source.expects(:fail).raises RuntimeError - lambda { @source.content }.should raise_error(RuntimeError) + it "should be able to return the metadata source full path" do + @source.full_path.should == "/path/to/source" + end end - it "should expire the content appropriately" do - expirer = stub 'expired', :dependent_data_expired? => true + describe "for remote sources" do + before(:each) do + @metadata.stubs(:ftype).returns "file" + @metadata.stubs(:source).returns("puppet://server:8192/path/to/source") + end - content2 = stub 'content', :content => "secondrun" - Puppet::FileServing::Content.expects(:find).with("/my/source").times(2).returns(@content).then.returns(content2) - @source.content.should == "foobar" + it "should not be local" do + @source.should_not be_local + end - @source.stubs(:expirer).returns expirer + it "should be able to return the metadata source full path" do + @source.full_path.should == "/path/to/source" + end + + it "should be able to return the source server" do + @source.server.should == "server" + end + + it "should be able to return the source port" do + @source.port.should == 8192 + end - @source.content.should == "secondrun" + describe "which don't specify server or port" do + before(:each) do + @metadata.stubs(:source).returns("puppet:///path/to/source") + end + + it "should return the default source server" do + Puppet.settings.expects(:[]).with(:server).returns("myserver") + @source.server.should == "myserver" + end + + it "should return the default source port" do + Puppet.settings.expects(:[]).with(:masterport).returns(1234) + @source.port.should == 1234 + end + end end end + end diff --git a/spec/unit/util/checksums.rb b/spec/unit/util/checksums.rb index e0c990962..eba564352 100755 --- a/spec/unit/util/checksums.rb +++ b/spec/unit/util/checksums.rb @@ -1,129 +1,149 @@ #!/usr/bin/env ruby # # Created by Luke Kanies on 2007-9-22. # Copyright (c) 2007. All rights reserved. require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet/util/checksums' describe Puppet::Util::Checksums do before do @summer = Object.new @summer.extend(Puppet::Util::Checksums) end content_sums = [:md5, :md5lite, :sha1, :sha1lite] file_only = [:ctime, :mtime, :none] content_sums.each do |sumtype| it "should be able to calculate %s sums from strings" % sumtype do @summer.should be_respond_to(sumtype) end end [content_sums, file_only].flatten.each do |sumtype| it "should be able to calculate %s sums from files" % sumtype do @summer.should be_respond_to(sumtype.to_s + "_file") end end + [content_sums, file_only].flatten.each do |sumtype| + it "should be able to calculate %s sums from stream" % sumtype do + @summer.should be_respond_to(sumtype.to_s + "_stream") + end + end + it "should have a method for determining whether a given string is a checksum" do @summer.should respond_to(:checksum?) end %w{{md5}asdfasdf {sha1}asdfasdf {ctime}asdasdf {mtime}asdfasdf}.each do |sum| it "should consider #{sum} to be a checksum" do @summer.should be_checksum(sum) end end %w{{nosuchsum}asdfasdf {a}asdfasdf {ctime}}.each do |sum| it "should not consider #{sum} to be a checksum" do @summer.should_not be_checksum(sum) end end it "should have a method for stripping a sum type from an existing checksum" do @summer.sumtype("{md5}asdfasdfa").should == "md5" end it "should return a nil sumtype if the checksum does not mention a checksum type" do @summer.sumtype("asdfasdfa").should be_nil end {:md5 => Digest::MD5, :sha1 => Digest::SHA1}.each do |sum, klass| describe("when using %s" % sum) do it "should use #{klass} to calculate string checksums" do klass.expects(:hexdigest).with("mycontent").returns "whatever" @summer.send(sum, "mycontent").should == "whatever" end it "should use incremental #{klass} sums to calculate file checksums" do digest = mock 'digest' klass.expects(:new).returns digest file = "/path/to/my/file" fh = mock 'filehandle' fh.expects(:read).with(4096).times(3).returns("firstline").then.returns("secondline").then.returns(nil) #fh.expects(:read).with(512).returns("secondline") #fh.expects(:read).with(512).returns(nil) File.expects(:open).with(file, "r").yields(fh) digest.expects(:<<).with "firstline" digest.expects(:<<).with "secondline" digest.expects(:hexdigest).returns :mydigest @summer.send(sum.to_s + "_file", file).should == :mydigest end + + it "should yield #{klass} to the given block to calculate stream checksums" do + digest = mock 'digest' + klass.expects(:new).returns digest + digest.expects(:hexdigest).returns :mydigest + + @summer.send(sum.to_s + "_stream") do |sum| + sum.should == digest + end.should == :mydigest + end end end {:md5lite => Digest::MD5, :sha1lite => Digest::SHA1}.each do |sum, klass| describe("when using %s" % sum) do it "should use #{klass} to calculate string checksums from the first 512 characters of the string" do content = "this is a test" * 100 klass.expects(:hexdigest).with(content[0..511]).returns "whatever" @summer.send(sum, content).should == "whatever" end it "should use #{klass} to calculate a sum from the first 512 characters in the file" do digest = mock 'digest' klass.expects(:new).returns digest file = "/path/to/my/file" fh = mock 'filehandle' fh.expects(:read).with(512).returns('my content') File.expects(:open).with(file, "r").yields(fh) digest.expects(:<<).with "my content" digest.expects(:hexdigest).returns :mydigest @summer.send(sum.to_s + "_file", file).should == :mydigest end end end [:ctime, :mtime].each do |sum| describe("when using %s" % sum) do it "should use the '#{sum}' on the file to determine the ctime" do file = "/my/file" stat = mock 'stat', sum => "mysum" File.expects(:stat).with(file).returns(stat) @summer.send(sum.to_s + "_file", file).should == "mysum" end + + it "should return nil for streams" do + @summer.send(sum.to_s + "_stream").should be_nil + end end end describe "when using the none checksum" do it "should return an empty string" do @summer.none_file("/my/file").should == "" end end end