diff --git a/lib/puppet/type/file.rb b/lib/puppet/type/file.rb index d3c66bc02..c30c19af8 100644 --- a/lib/puppet/type/file.rb +++ b/lib/puppet/type/file.rb @@ -1,821 +1,823 @@ 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. **Autorequires:** If Puppet is managing the user or group that owns a file, the file resource will autorequire them. If Puppet is managing any parent directories of a file, the file resource will autorequire them." def self.title_patterns [ [ /^(.*?)\/*\Z/m, [ [ :path, lambda{|x| x} ] ] ] ] end newparam(:path) do desc "The path to the file to manage. Must be fully qualified." isnamevar validate do |value| # accept various path syntaxes: lone slash, posix, win32, unc unless (Puppet.features.posix? and value =~ /^\//) or (value =~ /^[A-Za-z]:\// or value =~ /^\/\/[^\/]+\/[^\/]+/) fail Puppet::Error, "File paths must be fully qualified, not '#{value}'" end end # convert the current path in an index into the collection and the last # path name. The aim is to use less storage for all common paths in a hierarchy munge do |value| # We need to save off, and remove the volume designator in the # path if it is there, since File.split does not handle paths # with volume designators properly, except when run on Windows. # Since we are potentially compiling a catalog for a Windows # machine on a non-Windows master, we need to handle this # ourselves. optional_volume_designator = value.match(/^([a-z]:)[\/\\].*/i) value_without_designator = value.sub(/^(?:[a-z]:)?(.*)/i, '\1') path, name = ::File.split(value_without_designator.gsub(/\/+/,'/')) if optional_volume_designator path = optional_volume_designator[1] + path end { :index => Puppet::FileCollection.collection.index(path), :name => name } end # and the reverse unmunge do |value| basedir = Puppet::FileCollection.collection.path(value[:index]) # a lone slash as :name indicates a root dir on windows if value[:name] == '/' basedir else ::File.join( basedir, value[:name] ) end end end newparam(:backup) do desc "Whether files should be backed up before being replaced. The preferred method of backing files up is via a `filebucket`, which stores files by their MD5 sums and allows easy retrieval without littering directories with backups. You can specify a local filebucket or a network-accessible server-based filebucket by setting `backup => bucket-name`. Alternatively, if you specify any value that begins with a `.` (e.g., `.puppet-bak`), then Puppet will use copy the file in the same directory with that value as the extension of the backup. Setting `backup => false` disables all backups of the file in question. Puppet automatically creates a local filebucket named `puppet` and defaults to backing up there. To use a server-based filebucket, you must specify one in your configuration. filebucket { main: server => puppet, path => false, # The path => false line works around a known issue with the filebucket type. } The `puppet master` daemon creates a filebucket by default, so you can usually back up to your main server with this configuration. Once you've described the bucket in your configuration, you can use it in any file's backup attribute: file { \"/my/file\": source => \"/path/in/nfs/or/something\", backup => main } This will back the file up to the central server. At this point, the benefits of using a central filebucket are that you do not have backup files lying around on each of your machines, a given version of a file is only backed up once, you can restore any given file manually (no matter how old), and you can use Puppet Dashboard to view file contents. Eventually, transactional support will be able to automatically restore filebucketed files. " defaultto "puppet" munge do |value| # I don't really know how this is happening. value = value.shift if value.is_a?(Array) case value when false, "false", :false false when true, "true", ".puppet-bak", :true ".puppet-bak" when String value else self.fail "Invalid backup type #{value.inspect}" end end end newparam(:recurse) do desc "Whether and how deeply to do recursive management. Options are: * `inf,true` --- Regular style recursion on both remote and local directory structure. * `remote` --- Descends recursively into the remote directory but not the local directory. Allows copying of a few files into a directory containing many unmanaged files without scanning all the local files. * `false` --- Default of no recursion. * `[0-9]+` --- Same as true, but limit recursion. Warning: this syntax has been deprecated in favor of the `recurselimit` attribute. " newvalues(:true, :false, :inf, :remote, /^[0-9]+$/) # Replace the validation so that we allow numbers in # addition to string representations of them. validate { |arg| } munge do |value| newval = super(value) case newval when :true, :inf; true when :false; false when :remote; :remote when Integer, Fixnum, Bignum self.warning "Setting recursion depth with the recurse parameter is now deprecated, please use recurselimit" # recurse == 0 means no recursion return false if value == 0 resource[:recurselimit] = value true when /^\d+$/ self.warning "Setting recursion depth with the recurse parameter is now deprecated, please use recurselimit" value = Integer(value) # recurse == 0 means no recursion return false if value == 0 resource[:recurselimit] = value true else self.fail "Invalid recurse value #{value.inspect}" end end end newparam(:recurselimit) do desc "How deeply to do recursive management." newvalues(/^[0-9]+$/) munge do |value| newval = super(value) case newval when Integer, Fixnum, Bignum; value when /^\d+$/; Integer(value) else self.fail "Invalid recurselimit value #{value.inspect}" end end end newparam(:replace, :boolean => true) do desc "Whether or not to replace a file that is sourced but exists. This is useful for using file sources purely for initialization." newvalues(:true, :false) aliasvalue(:yes, :true) aliasvalue(:no, :false) defaultto :true end newparam(:force, :boolean => true) do desc "Force the file operation. Currently only used when replacing directories with links." newvalues(:true, :false) defaultto false end newparam(:ignore) do desc "A parameter which omits action on files matching specified patterns during recursion. Uses Ruby's builtin globbing engine, so shell metacharacters are fully supported, e.g. `[a-z]*`. Matches that would descend into the directory structure are ignored, e.g., `*/*`." validate do |value| unless value.is_a?(Array) or value.is_a?(String) or value == false self.devfail "Ignore must be a string or an Array" end end end newparam(:links) do desc "How to handle links during file actions. During file copying, `follow` will copy the target file instead of the link, `manage` will copy the link itself, and `ignore` will just pass it by. When not copying, `manage` and `ignore` behave equivalently (because you cannot really ignore links entirely during local recursion), and `follow` will manage the file to which the link points." newvalues(:follow, :manage) defaultto :manage end newparam(:purge, :boolean => true) do desc "Whether unmanaged files should be purged. If you have a filebucket configured the purged files will be uploaded, but if you do not, this will destroy data. Only use this option for generated files unless you really know what you are doing. This option only makes sense when recursively managing directories. Note that when using `purge` with `source`, Puppet will purge any files that are not on the remote system." defaultto :false newvalues(:true, :false) end newparam(:sourceselect) do desc "Whether to copy all valid sources, or just the first one. This parameter is only used in recursive copies; by default, the first valid source is the only one used as a recursive source, but if this parameter is set to `all`, then all valid sources will have all of their contents copied to the local host, and for sources that have the same file, the source earlier in the list will be used." defaultto :first newvalues(:first, :all) end # Autorequire the nearest ancestor directory found in the catalog. autorequire(:file) do basedir = ::File.dirname(self[:path]) if basedir != self[:path] parents = [] until basedir == parents.last parents << basedir basedir = ::File.dirname(basedir) end # The filename of the first ancestor found, or nil parents.find { |dir| catalog.resource(:file, dir) } else nil end end # Autorequire the owner and group of the file. {:user => :owner, :group => :group}.each do |type, property| autorequire(type) do if @parameters.include?(property) # The user/group property automatically converts to IDs next unless should = @parameters[property].shouldorig val = should[0] if val.is_a?(Integer) or val =~ /^\d+$/ nil else val end end end end CREATORS = [:content, :source, :target] SOURCE_ONLY_CHECKSUMS = [:none, :ctime, :mtime] validate do creator_count = 0 CREATORS.each do |param| creator_count += 1 if self.should(param) end creator_count += 1 if @parameters.include?(:source) self.fail "You cannot specify more than one of #{CREATORS.collect { |p| p.to_s}.join(", ")}" if creator_count > 1 self.fail "You cannot specify a remote recursion without a source" if !self[:source] and self[:recurse] == :remote self.fail "You cannot specify source when using checksum 'none'" if self[:checksum] == :none && !self[:source].nil? SOURCE_ONLY_CHECKSUMS.each do |checksum_type| self.fail "You cannot specify content when using checksum '#{checksum_type}'" if self[:checksum] == checksum_type && !self[:content].nil? end self.warning "Possible error: recurselimit is set but not recurse, no recursion will happen" if !self[:recurse] and self[:recurselimit] end def self.[](path) return nil unless path super(path.gsub(/\/+/, '/').sub(/\/$/, '')) end def self.instances return [] end # Determine the user to write files as. def asuser if self.should(:owner) and ! self.should(:owner).is_a?(Symbol) writeable = Puppet::Util::SUIDManager.asuser(self.should(:owner)) { FileTest.writable?(::File.dirname(self[:path])) } # If the parent directory is writeable, then we execute # as the user in question. Otherwise we'll rely on # the 'owner' property to do things. asuser = self.should(:owner) if writeable end asuser end def bucket return @bucket if @bucket backup = self[:backup] return nil unless backup return nil if backup =~ /^\./ unless catalog or backup == "puppet" fail "Can not find filebucket for backups without a catalog" end unless catalog and filebucket = catalog.resource(:filebucket, backup) or backup == "puppet" fail "Could not find filebucket #{backup} specified in backup" end return default_bucket unless filebucket @bucket = filebucket.bucket @bucket end def default_bucket Puppet::Type.type(:filebucket).mkdefaultbucket.bucket end # Does the file currently exist? Just checks for whether # we have a stat def exist? stat ? true : false end # We have to do some extra finishing, to retrieve our bucket if # there is one. def finish # Look up our bucket, if there is one bucket super end # Create any children via recursion or whatever. def eval_generate return [] unless self.recurse? recurse #recurse.reject do |resource| # catalog.resource(:file, resource[:path]) #end.each do |child| # catalog.add_resource child # catalog.relationship_graph.add_edge self, child #end end def flush # We want to make sure we retrieve metadata anew on each transaction. @parameters.each do |name, param| param.flush if param.respond_to?(:flush) end @stat = :needs_stat end def initialize(hash) # Used for caching clients @clients = {} super # If they've specified a source, we get our 'should' values # from it. unless self[:ensure] if self[:target] self[:ensure] = :symlink elsif self[:content] self[:ensure] = :file end end @stat = :needs_stat end # Configure discovered resources to be purged. def mark_children_for_purging(children) children.each do |name, child| next if child[:source] child[:ensure] = :absent end end # Create a new file or directory object as a child to the current # object. def newchild(path) full_path = ::File.join(self[:path], path) # Add some new values to our original arguments -- these are the ones # set at initialization. We specifically want to exclude any param # values set by the :source property or any default values. # LAK:NOTE This is kind of silly, because the whole point here is that # the values set at initialization should live as long as the resource # but values set by default or by :source should only live for the transaction # or so. Unfortunately, we don't have a straightforward way to manage # the different lifetimes of this data, so we kludge it like this. # The right-side hash wins in the merge. options = @original_parameters.merge(:path => full_path).reject { |param, value| value.nil? } # These should never be passed to our children. [:parent, :ensure, :recurse, :recurselimit, :target, :alias, :source].each do |param| options.delete(param) if options.include?(param) end self.class.new(options) end # Files handle paths specially, because they just lengthen their # path names, rather than including the full parent's title each # time. def pathbuilder # We specifically need to call the method here, so it looks # up our parent in the catalog graph. if parent = parent() # We only need to behave specially when our parent is also # a file if parent.is_a?(self.class) # Remove the parent file name list = parent.pathbuilder list.pop # remove the parent's path info return list << self.ref else return super end else return [self.ref] end end # Should we be purging? def purge? @parameters.include?(:purge) and (self[:purge] == :true or self[:purge] == "true") end # Recursively generate a list of file resources, which will # be used to copy remote files, manage local files, and/or make links # to map to another directory. def recurse children = (self[:recurse] == :remote) ? {} : recurse_local if self[:target] recurse_link(children) elsif self[:source] recurse_remote(children) end # If we're purging resources, then delete any resource that isn't on the # remote system. mark_children_for_purging(children) if self.purge? result = children.values.sort { |a, b| a[:path] <=> b[:path] } remove_less_specific_files(result) end # This is to fix bug #2296, where two files recurse over the same # set of files. It's a rare case, and when it does happen you're # not likely to have many actual conflicts, which is good, because # this is a pretty inefficient implementation. def remove_less_specific_files(files) mypath = self[:path].split(::File::Separator) other_paths = catalog.vertices. select { |r| r.is_a?(self.class) and r[:path] != self[:path] }. collect { |r| r[:path].split(::File::Separator) }. select { |p| p[0,mypath.length] == mypath } return files if other_paths.empty? files.reject { |file| path = file[:path].split(::File::Separator) other_paths.any? { |p| path[0,p.length] == p } } end # A simple method for determining whether we should be recursing. def recurse? self[:recurse] == true or self[:recurse] == :remote end # Recurse the target of the link. def recurse_link(children) perform_recursion(self[:target]).each do |meta| if meta.relative_path == "." self[:ensure] = :directory next end children[meta.relative_path] ||= newchild(meta.relative_path) if meta.ftype == "directory" children[meta.relative_path][:ensure] = :directory else children[meta.relative_path][:ensure] = :link children[meta.relative_path][:target] = meta.full_path end end children end # Recurse the file itself, returning a Metadata instance for every found file. def recurse_local result = perform_recursion(self[:path]) return {} unless result result.inject({}) do |hash, meta| next hash if meta.relative_path == "." hash[meta.relative_path] = newchild(meta.relative_path) hash end end # Recurse against our remote file. def recurse_remote(children) sourceselect = self[:sourceselect] total = self[:source].collect do |source| next unless result = perform_recursion(source) return if top = result.find { |r| r.relative_path == "." } and top.ftype != "directory" result.each { |data| data.source = "#{source}/#{data.relative_path}" } break result if result and ! result.empty? and sourceselect == :first result end.flatten # This only happens if we have sourceselect == :all unless sourceselect == :first found = [] total.reject! do |data| result = found.include?(data.relative_path) found << data.relative_path unless found.include?(data.relative_path) result end end total.each do |meta| if meta.relative_path == "." parameter(:source).metadata = meta next end children[meta.relative_path] ||= newchild(meta.relative_path) children[meta.relative_path][:source] = meta.source children[meta.relative_path][:checksum] = :md5 if meta.ftype == "file" children[meta.relative_path].parameter(:source).metadata = meta end children end def perform_recursion(path) Puppet::FileServing::Metadata.indirection.search( path, :links => self[:links], :recurse => (self[:recurse] == :remote ? true : self[:recurse]), :recurselimit => self[:recurselimit], :ignore => self[:ignore], :checksum_type => (self[:source] || self[:content]) ? self[:checksum] : :none ) end # Remove any existing data. This is only used when dealing with # links or directories. def remove_existing(should) return unless s = stat self.fail "Could not back up; will not replace" unless perform_backup unless should.to_s == "link" return if s.ftype.to_s == should.to_s end case s.ftype when "directory" if self[:force] == :true debug "Removing existing directory for replacement with #{should}" FileUtils.rmtree(self[:path]) else notice "Not removing directory; use 'force' to override" + return end when "link", "file" debug "Removing existing #{s.ftype} for replacement with #{should}" ::File.unlink(self[:path]) else self.fail "Could not back up files of type #{s.ftype}" end @stat = :needs_stat + true end def retrieve if source = parameter(:source) source.copy_source_values end super end # Set the checksum, from another property. There are multiple # properties that modify the contents of a file, and they need the # ability to make sure that the checksum value is in sync. def setchecksum(sum = nil) if @parameters.include? :checksum if sum @parameters[:checksum].checksum = sum else # If they didn't pass in a sum, then tell checksum to # figure it out. currentvalue = @parameters[:checksum].retrieve @parameters[:checksum].checksum = currentvalue end end end # Should this thing be a normal file? This is a relatively complex # way of determining whether we're trying to create a normal file, # and it's here so that the logic isn't visible in the content property. def should_be_file? return true if self[:ensure] == :file # I.e., it's set to something like "directory" return false if e = self[:ensure] and e != :present # The user doesn't really care, apparently if self[:ensure] == :present return true unless s = stat return(s.ftype == "file" ? true : false) end # If we've gotten here, then :ensure isn't set return true if self[:content] return true if stat and stat.ftype == "file" false end # Stat our file. Depending on the value of the 'links' attribute, we # use either 'stat' or 'lstat', and we expect the properties to use the # resulting stat object accordingly (mostly by testing the 'ftype' # value). # # We use the initial value :needs_stat to ensure we only stat the file once, # but can also keep track of a failed stat (@stat == nil). This also allows # us to re-stat on demand by setting @stat = :needs_stat. def stat return @stat unless @stat == :needs_stat method = :stat # Files are the only types that support links if (self.class.name == :file and self[:links] != :follow) or self.class.name == :tidy method = :lstat end @stat = begin ::File.send(method, self[:path]) rescue Errno::ENOENT => error nil rescue Errno::EACCES => error warning "Could not stat; permission denied" nil end end # We have to hack this just a little bit, because otherwise we'll get # an error when the target and the contents are created as properties on # the far side. def to_trans(retrieve = true) obj = super obj.delete(:target) if obj[:target] == :notlink obj end # Write out the file. Requires the property name for logging. # Write will be done by the content property, along with checksum computation def write(property) remove_existing(:file) use_temporary_file = write_temporary_file? if use_temporary_file path = "#{self[:path]}.puppettmp_#{rand(10000)}" path = "#{self[:path]}.puppettmp_#{rand(10000)}" while ::File.exists?(path) or ::File.symlink?(path) else path = self[:path] end mode = self.should(:mode) # might be nil umask = mode ? 000 : 022 mode_int = mode ? mode.to_i(8) : nil content_checksum = Puppet::Util.withumask(umask) { ::File.open(path, 'w', mode_int ) { |f| write_content(f) } } # And put our new file in place if use_temporary_file # This is only not true when our file is empty. begin fail_if_checksum_is_wrong(path, content_checksum) if validate_checksum? ::File.rename(path, self[:path]) rescue => detail fail "Could not rename temporary file #{path} to #{self[:path]}: #{detail}" ensure # Make sure the created file gets removed ::File.unlink(path) if FileTest.exists?(path) end end # make sure all of the modes are actually correct property_fix end private # Should we validate the checksum of the file we're writing? def validate_checksum? self[:checksum] !~ /time/ end # Make sure the file we wrote out is what we think it is. def fail_if_checksum_is_wrong(path, content_checksum) newsum = parameter(:checksum).sum_file(path) return if [:absent, nil, content_checksum].include?(newsum) self.fail "File written to disk did not match checksum; discarding changes (#{content_checksum} vs #{newsum})" end # write the current content. Note that if there is no content property # simply opening the file with 'w' as done in write is enough to truncate # or write an empty length file. def write_content(file) (content = property(:content)) && content.write(file) end private def write_temporary_file? # unfortunately we don't know the source file size before fetching it # so let's assume the file won't be empty (c = property(:content) and c.length) || (s = @parameters[:source] and 1) end # There are some cases where all of the work does not get done on # file creation/modification, so we have to do some extra checking. def property_fix properties.each do |thing| next unless [:mode, :owner, :group, :seluser, :selrole, :seltype, :selrange].include?(thing.name) # Make sure we get a new stat objct @stat = :needs_stat currentvalue = thing.retrieve thing.sync unless thing.safe_insync?(currentvalue) end end end # We put all of the properties in separate files, because there are so many # of them. The order these are loaded is important, because it determines # the order they are in the property lit. require 'puppet/type/file/checksum' require 'puppet/type/file/content' # can create the file require 'puppet/type/file/source' # can create the file require 'puppet/type/file/target' # creates a different type of file require 'puppet/type/file/ensure' # can create the file require 'puppet/type/file/owner' require 'puppet/type/file/group' require 'puppet/type/file/mode' require 'puppet/type/file/type' require 'puppet/type/file/selcontext' # SELinux file context require 'puppet/type/file/ctime' require 'puppet/type/file/mtime' diff --git a/spec/unit/type/file_spec.rb b/spec/unit/type/file_spec.rb index 0041ce9f2..332fc1420 100755 --- a/spec/unit/type/file_spec.rb +++ b/spec/unit/type/file_spec.rb @@ -1,1194 +1,1466 @@ #!/usr/bin/env rspec require 'spec_helper' describe Puppet::Type.type(:file) do + include PuppetSpec::Files + + let(:path) { tmpfile('file_testing') } + let(:file) { described_class.new(:path => path, :catalog => catalog) } + let(:provider) { file.provider } + let(:catalog) { Puppet::Resource::Catalog.new } + before do - Puppet.settings.stubs(:use) @real_posix = Puppet.features.posix? Puppet.features.stubs("posix?").returns(true) - - @path = Tempfile.new("puppetspec") - pathname = @path.path - @path.close!() - @path = pathname - @file = Puppet::Type::File.new(:name => @path) - - @catalog = Puppet::Resource::Catalog.new - @file.catalog = @catalog end - describe "when determining if recursion is enabled" do - it "should default to recursion being disabled" do - @file.should_not be_recurse - end - [true, "true", 10, "inf", "remote"].each do |value| - it "should consider #{value} to enable recursion" do - @file[:recurse] = value - @file.must be_recurse + describe "the path parameter" do + describe "on POSIX systems", :if => Puppet.features.posix? do + it "should remove trailing slashes" do + file[:path] = "/foo/bar/baz/" + file[:path].should == "/foo/bar/baz" end - end - [false, "false", 0].each do |value| - it "should consider #{value} to disable recursion" do - @file[:recurse] = value - @file.should_not be_recurse + it "should remove double slashes" do + file[:path] = "/foo/bar//baz" + file[:path].should == "/foo/bar/baz" end - end - end - - describe "#write" do - it "should propagate failures encountered when renaming the temporary file" do - File.stubs(:open) - File.expects(:rename).raises ArgumentError - file = Puppet::Type::File.new(:name => "/my/file", :backup => "puppet") + it "should remove trailing double slashes" do + file[:path] = "/foo/bar/baz//" + file[:path].should == "/foo/bar/baz" + end - file.stubs(:validate_checksum?).returns(false) + it "should leave a single slash alone" do + file[:path] = "/" + file[:path].should == "/" + end - property = stub('content_property', :actual_content => "something", :length => "something".length) - file.stubs(:property).with(:content).returns(property) + it "should accept a double-slash at the start of the path" do + expect { + file[:path] = "//tmp/xxx" + # REVISIT: This should be wrong, later. See the next test. + # --daniel 2011-01-31 + file[:path].should == '/tmp/xxx' + }.should_not raise_error + end - lambda { file.write(:content) }.should raise_error(Puppet::Error) + # REVISIT: This is pending, because I don't want to try and audit the + # entire codebase to make sure we get this right. POSIX treats two (and + # exactly two) '/' characters at the start of the path specially. + # + # See sections 3.2 and 4.11, which allow DomainOS to be all special like + # and still have the POSIX branding and all. --daniel 2011-01-31 + it "should preserve the double-slash at the start of the path" end - it "should delegate writing to the content property" do - filehandle = stub_everything 'fh' - File.stubs(:open).yields(filehandle) - File.stubs(:rename) - property = stub('content_property', :actual_content => "something", :length => "something".length) - file = Puppet::Type::File.new(:name => "/my/file", :backup => "puppet") - file.stubs(:validate_checksum?).returns(false) - file.stubs(:property).with(:content).returns(property) + describe "on Windows systems", :if => Puppet.features.microsoft_windows? do + it "should remove trailing slashes" do + file[:path] = "X:/foo/bar/baz/" + file[:path].should == "X:/foo/bar/baz" + end - property.expects(:write).with(filehandle) + it "should remove double slashes" do + file[:path] = "X:/foo/bar//baz" + file[:path].should == "X:/foo/bar/baz" + end - file.write(:content) - end + it "should remove trailing double slashes" do + file[:path] = "X:/foo/bar/baz//" + file[:path].should == "X:/foo/bar/baz" + end - describe "when validating the checksum" do - before { @file.stubs(:validate_checksum?).returns(true) } + it "should leave a drive letter with a slash alone", :'fails_on_ruby_1.9.2' => true do + file[:path] = "X:/" + file[:path].should == "X:/" + end - it "should fail if the checksum parameter and content checksums do not match" do - checksum = stub('checksum_parameter', :sum => 'checksum_b', :sum_file => 'checksum_b') - @file.stubs(:parameter).with(:checksum).returns(checksum) + it "should not accept a drive letter without a slash", :'fails_on_ruby_1.9.2' => true do + lambda { file[:path] = "X:" }.should raise_error(/File paths must be fully qualified/) + end - property = stub('content_property', :actual_content => "something", :length => "something".length, :write => 'checksum_a') - @file.stubs(:property).with(:content).returns(property) + describe "when using UNC filenames", :if => Puppet.features.microsoft_windows?, :'fails_on_ruby_1.9.2' => true do + before :each do + pending("UNC file paths not yet supported") + end - lambda { @file.write :NOTUSED }.should raise_error(Puppet::Error) - end - end + it "should remove trailing slashes" do + file[:path] = "//server/foo/bar/baz/" + file[:path].should == "//server/foo/bar/baz" + end - describe "when not validating the checksum" do - before { @file.stubs(:validate_checksum?).returns(false) } + it "should remove double slashes" do + file[:path] = "//server/foo/bar//baz" + file[:path].should == "//server/foo/bar/baz" + end - it "should not fail if the checksum property and content checksums do not match" do - checksum = stub('checksum_parameter', :sum => 'checksum_b') - @file.stubs(:parameter).with(:checksum).returns(checksum) + it "should remove trailing double slashes" do + file[:path] = "//server/foo/bar/baz//" + file[:path].should == "//server/foo/bar/baz" + end - property = stub('content_property', :actual_content => "something", :length => "something".length, :write => 'checksum_a') - @file.stubs(:property).with(:content).returns(property) + it "should remove a trailing slash from a sharename" do + file[:path] = "//server/foo/" + file[:path].should == "//server/foo" + end - lambda { @file.write :NOTUSED }.should_not raise_error(Puppet::Error) + it "should not modify a sharename" do + file[:path] = "//server/foo" + file[:path].should == "//server/foo" + end end end end - it "should have a method for determining if the file is present" do - @file.must respond_to(:exist?) - end + describe "the backup parameter" do + [false, 'false', :false].each do |value| + it "should disable backup if the value is #{value.inspect}" do + file[:backup] = value + file[:backup].should == false + end + end - it "should be considered existent if it can be stat'ed" do - @file.expects(:stat).returns mock('stat') - @file.must be_exist - end + [true, 'true', '.puppet-bak'].each do |value| + it "should use .puppet-bak if the value is #{value.inspect}" do + file[:backup] = value + file[:backup].should == '.puppet-bak' + end + end - it "should be considered nonexistent if it can not be stat'ed" do - @file.expects(:stat).returns nil - @file.must_not be_exist - end + it "should use the provided value if it's any other string" do + file[:backup] = "over there" + file[:backup].should == "over there" + end - it "should have a method for determining if the file should be a normal file" do - @file.must respond_to(:should_be_file?) + it "should fail if backup is set to anything else" do + expect do + file[:backup] = 97 + end.to raise_error(Puppet::Error, /Invalid backup type 97/) + end end - it "should be a file if :ensure is set to :file" do - @file[:ensure] = :file - @file.must be_should_be_file - end + describe "the recurse parameter" do + it "should default to recursion being disabled" do + file[:recurse].should be_false + end - it "should be a file if :ensure is set to :present and the file exists as a normal file" do - @file.stubs(:stat).returns(mock('stat', :ftype => "file")) - @file[:ensure] = :present - @file.must be_should_be_file - end + [true, "true", 10, "inf", "remote"].each do |value| + it "should consider #{value} to enable recursion" do + file[:recurse] = value + file[:recurse].should be_true + end + end - it "should not be a file if :ensure is set to something other than :file" do - @file[:ensure] = :directory - @file.must_not be_should_be_file - end + [false, "false", 0].each do |value| + it "should consider #{value} to disable recursion" do + file[:recurse] = value + file[:recurse].should be_false + end + end - it "should not be a file if :ensure is set to :present and the file exists but is not a normal file" do - @file.stubs(:stat).returns(mock('stat', :ftype => "directory")) - @file[:ensure] = :present - @file.must_not be_should_be_file + it "should warn if recurse is specified as a number" do + file[:recurse] = 3 + message = /Setting recursion depth with the recurse parameter is now deprecated, please use recurselimit/ + @logs.find { |log| log.level == :warning and log.message =~ message}.should_not be_nil + end end - it "should be a file if :ensure is not set and :content is" do - @file[:content] = "foo" - @file.must be_should_be_file - end + describe "the recurselimit parameter" do + it "should accept integers" do + file[:recurselimit] = 12 + file[:recurselimit].should == 12 + end - it "should be a file if neither :ensure nor :content is set but the file exists as a normal file" do - @file.stubs(:stat).returns(mock("stat", :ftype => "file")) - @file.must be_should_be_file - end + it "should munge string numbers to number numbers" do + file[:recurselimit] = '12' + file[:recurselimit].should == 12 + end - it "should not be a file if neither :ensure nor :content is set but the file exists but not as a normal file" do - @file.stubs(:stat).returns(mock("stat", :ftype => "directory")) - @file.must_not be_should_be_file + it "should fail if given a non-number" do + expect do + file[:recurselimit] = 'twelve' + end.to raise_error(Puppet::Error, /Invalid value "twelve"/) + end end - describe "when using POSIX filenames" do - it "should autorequire its parent directory" do - file = Puppet::Type::File.new(:path => "/foo/bar") - dir = Puppet::Type::File.new(:path => "/foo") - @catalog.add_resource file - @catalog.add_resource dir - reqs = file.autorequire - reqs[0].source.must == dir - reqs[0].target.must == file + describe "the replace parameter" do + [true, :true, :yes].each do |value| + it "should consider #{value} to be true" do + file[:replace] = value + file[:replace].should == :true + end end - it "should autorequire its nearest ancestor directory" do - file = Puppet::Type::File.new(:path => "/foo/bar/baz") - dir = Puppet::Type::File.new(:path => "/foo") - root = Puppet::Type::File.new(:path => "/") - @catalog.add_resource file - @catalog.add_resource dir - @catalog.add_resource root - reqs = file.autorequire - reqs.length.must == 1 - reqs[0].source.must == dir - reqs[0].target.must == file + [false, :false, :no].each do |value| + it "should consider #{value} to be false" do + file[:replace] = value + file[:replace].should == :false + end end + end - it "should not autorequire anything when there is no nearest ancestor directory" do - file = Puppet::Type::File.new(:path => "/foo/bar/baz") - @catalog.add_resource file - file.autorequire.should be_empty + describe "#[]" do + it "should raise an exception" do + expect do + described_class['anything'] + end.to raise_error("Global resource access is deprecated") end + end - it "should not autorequire its parent dir if its parent dir is itself" do - file = Puppet::Type::File.new(:path => "/") - @catalog.add_resource file - file.autorequire.should be_empty + describe ".instances" do + it "should return an empty array" do + described_class.instances.should == [] end + end - it "should remove trailing slashes" do - file = Puppet::Type::File.new(:path => "/foo/bar/baz/") - file[:path].should == "/foo/bar/baz" + describe "#asuser" do + before :each do + # Mocha won't let me just stub SUIDManager.asuser to yield and return, + # but it will do exactly that if we're not root. + Puppet.features.stubs(:root?).returns false end - it "should remove double slashes" do - file = Puppet::Type::File.new(:path => "/foo/bar//baz") - file[:path].should == "/foo/bar/baz" - end + it "should return the desired owner if they can write to the parent directory" do + file[:owner] = 1001 + FileTest.stubs(:writable?).with(File.dirname file[:path]).returns true - it "should remove trailing double slashes" do - file = Puppet::Type::File.new(:path => "/foo/bar/baz//") - file[:path].should == "/foo/bar/baz" + file.asuser.should == 1001 end - it "should leave a single slash alone" do - file = Puppet::Type::File.new(:path => "/") - file[:path].should == "/" - end + it "should return nil if the desired owner can't write to the parent directory" do + file[:owner] = 1001 + FileTest.stubs(:writable?).with(File.dirname file[:path]).returns false - it "should accept a double-slash at the start of the path" do - expect { - file = Puppet::Type::File.new(:path => "//tmp/xxx") - # REVISIT: This should be wrong, later. See the next test. - # --daniel 2011-01-31 - file[:path].should == '/tmp/xxx' - }.should_not raise_error + file.asuser.should == nil end - # REVISIT: This is pending, because I don't want to try and audit the - # entire codebase to make sure we get this right. POSIX treats two (and - # exactly two) '/' characters at the start of the path specially. - # - # See sections 3.2 and 4.11, which allow DomainOS to be all special like - # and still have the POSIX branding and all. --daniel 2011-01-31 - it "should preserve the double-slash at the start of the path" + it "should return nil if not managing owner" do + file.asuser.should == nil + end end - describe "when using Microsoft Windows filenames" do - it "should autorequire its parent directory" do - file = Puppet::Type::File.new(:path => "X:/foo/bar") - dir = Puppet::Type::File.new(:path => "X:/foo") - @catalog.add_resource file - @catalog.add_resource dir - reqs = file.autorequire - reqs[0].source.must == dir - reqs[0].target.must == file + describe "#bucket" do + it "should return nil if backup is off" do + file[:backup] = false + file.bucket.should == nil end - it "should autorequire its nearest ancestor directory" do - file = Puppet::Type::File.new(:path => "X:/foo/bar/baz") - dir = Puppet::Type::File.new(:path => "X:/foo") - root = Puppet::Type::File.new(:path => "X:/") - @catalog.add_resource file - @catalog.add_resource dir - @catalog.add_resource root - reqs = file.autorequire - reqs.length.must == 1 - reqs[0].source.must == dir - reqs[0].target.must == file - end + it "should not return a bucket if using a file extension for backup" do + file[:backup] = '.backup' - it "should not autorequire anything when there is no nearest ancestor directory" do - file = Puppet::Type::File.new(:path => "X:/foo/bar/baz") - @catalog.add_resource file - file.autorequire.should be_empty + file.bucket.should == nil end - it "should not autorequire its parent dir if its parent dir is itself" do - file = Puppet::Type::File.new(:path => "X:/") - @catalog.add_resource file - file.autorequire.should be_empty - end + it "should return the default filebucket if using the 'puppet' filebucket" do + file[:backup] = 'puppet' + bucket = stub('bucket') + file.stubs(:default_bucket).returns bucket - it "should remove trailing slashes" do - file = Puppet::Type::File.new(:path => "X:/foo/bar/baz/") - file[:path].should == "X:/foo/bar/baz" + file.bucket.should == bucket end - it "should remove double slashes" do - file = Puppet::Type::File.new(:path => "X:/foo/bar//baz") - file[:path].should == "X:/foo/bar/baz" - end + it "should fail if using a remote filebucket and no catalog exists" do + file.catalog = nil + file[:backup] = 'my_bucket' - it "should remove trailing double slashes" do - file = Puppet::Type::File.new(:path => "X:/foo/bar/baz//") - file[:path].should == "X:/foo/bar/baz" + expect { file.bucket }.to raise_error(Puppet::Error, "Can not find filebucket for backups without a catalog") end - it "should leave a drive letter with a slash alone", :'fails_on_ruby_1.9.2' => true do - file = Puppet::Type::File.new(:path => "X:/") - file[:path].should == "X:/" + it "should fail if the specified filebucket isn't in the catalog" do + file[:backup] = 'my_bucket' + + expect { file.bucket }.to raise_error(Puppet::Error, "Could not find filebucket my_bucket specified in backup") end - it "should not accept a drive letter without a slash", :'fails_on_ruby_1.9.2' => true do - lambda { Puppet::Type::File.new(:path => "X:") }.should raise_error(/File paths must be fully qualified/) + it "should use the specified filebucket if it is in the catalog" do + file[:backup] = 'my_bucket' + filebucket = Puppet::Type.type(:filebucket).new(:name => 'my_bucket') + catalog.add_resource(filebucket) + + file.bucket.should == filebucket.bucket end end - describe "when using UNC filenames", :'fails_on_ruby_1.9.2' => true do + describe "#asuser" do before :each do - pending("UNC file paths not yet supported") + # Mocha won't let me just stub SUIDManager.asuser to yield and return, + # but it will do exactly that if we're not root. + Puppet.features.stubs(:root?).returns false end - it "should autorequire its parent directory" do - file = Puppet::Type::File.new(:path => "//server/foo/bar") - dir = Puppet::Type::File.new(:path => "//server/foo") - @catalog.add_resource file - @catalog.add_resource dir - reqs = file.autorequire - reqs[0].source.must == dir - reqs[0].target.must == file + it "should return the desired owner if they can write to the parent directory" do + file[:owner] = 1001 + FileTest.stubs(:writable?).with(File.dirname file[:path]).returns true + + file.asuser.should == 1001 end - it "should autorequire its nearest ancestor directory" do - file = Puppet::Type::File.new(:path => "//server/foo/bar/baz/qux") - dir = Puppet::Type::File.new(:path => "//server/foo/bar") - root = Puppet::Type::File.new(:path => "//server/foo") - @catalog.add_resource file - @catalog.add_resource dir - @catalog.add_resource root - reqs = file.autorequire - reqs.length.must == 1 - reqs[0].source.must == dir - reqs[0].target.must == file + it "should return nil if the desired owner can't write to the parent directory" do + file[:owner] = 1001 + FileTest.stubs(:writable?).with(File.dirname file[:path]).returns false + + file.asuser.should == nil end - it "should not autorequire anything when there is no nearest ancestor directory" do - file = Puppet::Type::File.new(:path => "//server/foo/bar/baz/qux") - @catalog.add_resource file - file.autorequire.should be_empty + it "should return nil if not managing owner" do + file.asuser.should == nil end + end - it "should not autorequire its parent dir if its parent dir is itself" do - file = Puppet::Type::File.new(:path => "//server/foo") - @catalog.add_resource file - puts file.autorequire - file.autorequire.should be_empty + describe "#bucket" do + it "should return nil if backup is off" do + file[:backup] = false + file.bucket.should == nil end - it "should remove trailing slashes" do - file = Puppet::Type::File.new(:path => "//server/foo/bar/baz/") - file[:path].should == "//server/foo/bar/baz" + it "should return nil if using a file extension for backup" do + file[:backup] = '.backup' + + file.bucket.should == nil end - it "should remove double slashes" do - file = Puppet::Type::File.new(:path => "//server/foo/bar//baz") - file[:path].should == "//server/foo/bar/baz" + it "should return the default filebucket if using the 'puppet' filebucket" do + file[:backup] = 'puppet' + bucket = stub('bucket') + file.stubs(:default_bucket).returns bucket + + file.bucket.should == bucket end - it "should remove trailing double slashes" do - file = Puppet::Type::File.new(:path => "//server/foo/bar/baz//") - file[:path].should == "//server/foo/bar/baz" + it "should fail if using a remote filebucket and no catalog exists" do + file.catalog = nil + file[:backup] = 'my_bucket' + + expect { file.bucket }.to raise_error(Puppet::Error, "Can not find filebucket for backups without a catalog") end - it "should remove a trailing slash from a sharename" do - file = Puppet::Type::File.new(:path => "//server/foo/") - file[:path].should == "//server/foo" + it "should fail if the specified filebucket isn't in the catalog" do + file[:backup] = 'my_bucket' + + expect { file.bucket }.to raise_error(Puppet::Error, "Could not find filebucket my_bucket specified in backup") end - it "should not modify a sharename" do - file = Puppet::Type::File.new(:path => "//server/foo") - file[:path].should == "//server/foo" + it "should use the specified filebucket if it is in the catalog" do + file[:backup] = 'my_bucket' + filebucket = Puppet::Type.type(:filebucket).new(:name => 'my_bucket') + catalog.add_resource(filebucket) + + file.bucket.should == filebucket.bucket end end - describe "when initializing" do - it "should set a desired 'ensure' value if none is set and 'content' is set" do - file = Puppet::Type::File.new(:name => "/my/file", :content => "/foo/bar") - file[:ensure].should == :file + describe "#exist?" do + it "should be considered existent if it can be stat'ed" do + file.expects(:stat).returns mock('stat') + file.must be_exist end - it "should set a desired 'ensure' value if none is set and 'target' is set" do - file = Puppet::Type::File.new(:name => "/my/file", :target => "/foo/bar") - file[:ensure].should == :symlink + it "should be considered nonexistent if it can not be stat'ed" do + file.expects(:stat).returns nil + file.must_not be_exist end end - describe "when validating attributes" do - %w{path checksum backup recurse recurselimit source replace force ignore links purge sourceselect}.each do |attr| - it "should have a '#{attr}' parameter" do - Puppet::Type.type(:file).attrtype(attr.intern).should == :param - end + describe "#eval_generate" do + before do + @graph = stub 'graph', :add_edge => nil + catalog.stubs(:relationship_graph).returns @graph end - %w{content target ensure owner group mode type}.each do |attr| - it "should have a '#{attr}' property" do - Puppet::Type.type(:file).attrtype(attr.intern).should == :property - end - end + it "should recurse if recursion is enabled" do + resource = stub('resource', :[] => 'resource') + file.expects(:recurse).returns [resource] - it "should have its 'path' attribute set as its namevar" do - Puppet::Type.type(:file).key_attributes.should == [:path] + file[:recurse] = true + + file.eval_generate.should == [resource] end - end - describe "when managing links" do - require 'tempfile' + it "should not recurse if recursion is disabled" do + file.expects(:recurse).never - if @real_posix - describe "on POSIX systems" do - before do - @basedir = tempfile - Dir.mkdir(@basedir) - @file = File.join(@basedir, "file") - @link = File.join(@basedir, "link") + file[:recurse] = false - File.open(@file, "w", 0644) { |f| f.puts "yayness"; f.flush } - File.symlink(@file, @link) + file.eval_generate.should == [] + end + end - @resource = Puppet::Type.type(:file).new(:path => @link, :mode => "755") - @catalog.add_resource @resource - end + describe "#flush" do + it "should flush all properties that respond to :flush" do + file[:source] = File.expand_path(__FILE__) + file.parameter(:source).expects(:flush) + file.flush + end - after do - remove_tmp_files - end + it "should reset its stat reference" do + FileUtils.touch(path) + stat1 = file.stat - it "should default to managing the link" do - @catalog.apply - # I convert them to strings so they display correctly if there's an error. - ("%o" % (File.stat(@file).mode & 007777)).should == "%o" % 0644 - end + file.stat.should equal(stat1) - it "should be able to follow links" do - @resource[:links] = :follow - @catalog.apply + file.flush - ("%o" % (File.stat(@file).mode & 007777)).should == "%o" % 0755 - end - end - else # @real_posix - # should recode tests using expectations instead of using the filesystem + file.stat.should_not equal(stat1) end + end - describe "on Microsoft Windows systems" do - before do - Puppet.features.stubs(:posix?).returns(false) - Puppet.features.stubs(:microsoft_windows?).returns(true) - end + describe "#initialize" do + it "should remove a trailing slash from the title to create the path" do + title = File.expand_path("/abc/\n\tdef/") + file = described_class.new(:title => title) + file[:path].should == title + end - it "should refuse to work with links" + it "should set a desired 'ensure' value if none is set and 'content' is set" do + file = described_class.new(:path => path, :content => "/foo/bar") + file[:ensure].should == :file end - end - it "should be able to retrieve a stat instance for the file it is managing" do - Puppet::Type.type(:file).new(:path => "/foo/bar", :source => "/bar/foo").should respond_to(:stat) + it "should set a desired 'ensure' value if none is set and 'target' is set" do + file = described_class.new(:path => path, :target => File.expand_path(__FILE__)) + file[:ensure].should == :symlink + end end - describe "when stat'ing its file" do - before do - @resource = Puppet::Type.type(:file).new(:path => "/foo/bar") - @resource[:links] = :manage # so we always use :lstat - end + describe "#mark_children_for_purging" do + it "should set each child's ensure to absent" do + paths = %w[foo bar baz] + children = paths.inject({}) do |children,child| + children.merge child => described_class.new(:path => File.join(path, child), :ensure => :present) + end - it "should use :stat if it is following links" do - @resource[:links] = :follow - File.expects(:stat) + file.mark_children_for_purging(children) - @resource.stat + children.length.should == 3 + children.values.each do |child| + child[:ensure].should == :absent + end end - it "should use :lstat if is it not following links" do - @resource[:links] = :manage - File.expects(:lstat) - - @resource.stat - end + it "should skip children which have a source" do + child = described_class.new(:path => path, :ensure => :present, :source => File.expand_path(__FILE__)) - it "should stat the path of the file" do - File.expects(:lstat).with("/foo/bar") + file.mark_children_for_purging('foo' => child) - @resource.stat + child[:ensure].should == :present end + end - # This only happens in testing. - it "should return nil if the stat does not exist" do - File.expects(:lstat).returns nil - - @resource.stat.should be_nil + describe "#newchild" do + it "should create a new resource relative to the parent" do + child = file.newchild('bar') + + child.should be_a(described_class) + child[:path].should == File.join(file[:path], 'bar') + end + + { + :ensure => :present, + :recurse => true, + :recurselimit => 5, + :target => "some_target", + :source => "some_source", + }.each do |param, value| + it "should omit the #{param} parameter" do + # Make a new file, because we have to set the param at initialization + # or it wouldn't be copied regardless. + file = described_class.new(:path => path, param => value) + child = file.newchild('bar') + child[param].should_not == value + end end - it "should return nil if the file does not exist" do - File.expects(:lstat).raises(Errno::ENOENT) - - @resource.stat.should be_nil - end + it "should copy all of the parent resource's 'should' values that were set at initialization" do + parent = described_class.new(:path => path, :owner => 'root', :group => 'wheel') - it "should return nil if the file cannot be stat'ed" do - File.expects(:lstat).raises(Errno::EACCES) + child = parent.newchild("my/path") - @resource.stat.should be_nil + child[:owner].should == 'root' + child[:group].should == 'wheel' end - it "should return the stat instance" do - File.expects(:lstat).returns "mystat" - - @resource.stat.should == "mystat" + it "should not copy default values to the new child" do + child = file.newchild("my/path") + child.original_parameters.should_not include(:backup) end - it "should cache the stat instance if it has a catalog and is applying" do - stat = mock 'stat' - File.expects(:lstat).returns stat + it "should not copy values to the child which were set by the source" do + file[:source] = File.expand_path(__FILE__) + metadata = stub 'metadata', :owner => "root", :group => "root", :mode => 0755, :ftype => "file", :checksum => "{md5}whatever" + file.parameter(:source).stubs(:metadata).returns metadata - catalog = Puppet::Resource::Catalog.new - @resource.catalog = catalog + file.parameter(:source).copy_source_values - catalog.stubs(:applying?).returns true - - @resource.stat.should equal(@resource.stat) + file.class.expects(:new).with { |params| params[:group].nil? } + file.newchild("my/path") end end - describe "when flushing" do - it "should flush all properties that respond to :flush" do - @resource = Puppet::Type.type(:file).new(:path => "/foo/bar", :source => "/bar/foo") - @resource.parameter(:source).expects(:flush) - @resource.flush + describe "#purge?" do + it "should return false if purge is not set" do + file.must_not be_purge end - it "should reset its stat reference" do - @resource = Puppet::Type.type(:file).new(:path => "/foo/bar") - File.expects(:lstat).times(2).returns("stat1").then.returns("stat2") - @resource.stat.should == "stat1" - @resource.flush - @resource.stat.should == "stat2" + it "should return true if purge is set to true" do + file[:purge] = true + + file.must be_purge end - end - it "should have a method for performing recursion" do - @file.must respond_to(:perform_recursion) - end + it "should return false if purge is set to false" do + file[:purge] = false - describe "when executing a recursive search" do - it "should use Metadata to do its recursion" do - Puppet::FileServing::Metadata.indirection.expects(:search) - @file.perform_recursion(@file[:path]) + file.must_not be_purge end + end - it "should use the provided path as the key to the search" do - Puppet::FileServing::Metadata.indirection.expects(:search).with { |key, options| key == "/foo" } - @file.perform_recursion("/foo") + describe "#recurse" do + before do + file[:recurse] = true + @metadata = Puppet::FileServing::Metadata end - it "should return the results of the metadata search" do - Puppet::FileServing::Metadata.indirection.expects(:search).returns "foobar" - @file.perform_recursion(@file[:path]).should == "foobar" + describe "and a source is set" do + it "should pass the already-discovered resources to recurse_remote" do + file[:source] = File.expand_path(__FILE__) + file.stubs(:recurse_local).returns(:foo => "bar") + file.expects(:recurse_remote).with(:foo => "bar").returns [] + file.recurse + end end - it "should pass its recursion value to the search" do - @file[:recurse] = true - Puppet::FileServing::Metadata.indirection.expects(:search).with { |key, options| options[:recurse] == true } - @file.perform_recursion(@file[:path]) + describe "and a target is set" do + it "should use recurse_link" do + file[:target] = File.expand_path(__FILE__) + file.stubs(:recurse_local).returns(:foo => "bar") + file.expects(:recurse_link).with(:foo => "bar").returns [] + file.recurse + end end - it "should pass true if recursion is remote" do - @file[:recurse] = :remote - Puppet::FileServing::Metadata.indirection.expects(:search).with { |key, options| options[:recurse] == true } - @file.perform_recursion(@file[:path]) + it "should use recurse_local if recurse is not remote" do + file.expects(:recurse_local).returns({}) + file.recurse end - it "should pass its recursion limit value to the search" do - @file[:recurselimit] = 10 - Puppet::FileServing::Metadata.indirection.expects(:search).with { |key, options| options[:recurselimit] == 10 } - @file.perform_recursion(@file[:path]) + it "should not use recurse_local if recurse is remote" do + file[:recurse] = :remote + file.expects(:recurse_local).never + file.recurse end - it "should configure the search to ignore or manage links" do - @file[:links] = :manage - Puppet::FileServing::Metadata.indirection.expects(:search).with { |key, options| options[:links] == :manage } - @file.perform_recursion(@file[:path]) + it "should return the generated resources as an array sorted by file path" do + one = stub 'one', :[] => "/one" + two = stub 'two', :[] => "/one/two" + three = stub 'three', :[] => "/three" + file.expects(:recurse_local).returns(:one => one, :two => two, :three => three) + file.recurse.should == [one, two, three] end - it "should pass its 'ignore' setting to the search if it has one" do - @file[:ignore] = %w{.svn CVS} - Puppet::FileServing::Metadata.indirection.expects(:search).with { |key, options| options[:ignore] == %w{.svn CVS} } - @file.perform_recursion(@file[:path]) + describe "and purging is enabled" do + before do + file[:purge] = true + end + + it "should mark each file for removal" do + local = described_class.new(:path => path, :ensure => :present) + file.expects(:recurse_local).returns("local" => local) + + file.recurse + local[:ensure].should == :absent + end + + it "should not remove files that exist in the remote repository" do + file[:source] = File.expand_path(__FILE__) + file.expects(:recurse_local).returns({}) + + remote = described_class.new(:path => path, :source => File.expand_path(__FILE__), :ensure => :present) + + file.expects(:recurse_remote).with { |hash| hash["remote"] = remote } + + file.recurse + + remote[:ensure].should_not == :absent + end end - end - it "should have a method for performing local recursion" do - @file.must respond_to(:recurse_local) end - describe "when doing local recursion" do - before do - @metadata = stub 'metadata', :relative_path => "my/file" - end + describe "#remove_less_specific_files" do + it "should remove any nested files that are already in the catalog" do + foo = described_class.new :path => File.join(file[:path], 'foo') + bar = described_class.new :path => File.join(file[:path], 'bar') + baz = described_class.new :path => File.join(file[:path], 'baz') - it "should pass its path to the :perform_recursion method" do - @file.expects(:perform_recursion).with(@file[:path]).returns [@metadata] - @file.stubs(:newchild) - @file.recurse_local - end + catalog.add_resource(foo) + catalog.add_resource(bar) - it "should return an empty hash if the recursion returns nothing" do - @file.expects(:perform_recursion).returns nil - @file.recurse_local.should == {} + file.remove_less_specific_files([foo, bar, baz]).should == [baz] end + end - it "should create a new child resource with each generated metadata instance's relative path" do - @file.expects(:perform_recursion).returns [@metadata] - @file.expects(:newchild).with(@metadata.relative_path).returns "fiebar" - @file.recurse_local - end + describe "#remove_less_specific_files" do + it "should remove any nested files that are already in the catalog" do + foo = described_class.new :path => File.join(file[:path], 'foo') + bar = described_class.new :path => File.join(file[:path], 'bar') + baz = described_class.new :path => File.join(file[:path], 'baz') - it "should not create a new child resource for the '.' directory" do - @metadata.stubs(:relative_path).returns "." + catalog.add_resource(foo) + catalog.add_resource(bar) - @file.expects(:perform_recursion).returns [@metadata] - @file.expects(:newchild).never - @file.recurse_local + file.remove_less_specific_files([foo, bar, baz]).should == [baz] end - it "should return a hash of the created resources with the relative paths as the hash keys" do - @file.expects(:perform_recursion).returns [@metadata] - @file.expects(:newchild).with("my/file").returns "fiebar" - @file.recurse_local.should == {"my/file" => "fiebar"} + end + + describe "#recurse?" do + it "should be true if recurse is true" do + file[:recurse] = true + file.must be_recurse end - it "should set checksum_type to none if this file checksum is none" do - @file[:checksum] = :none - Puppet::FileServing::Metadata.indirection.expects(:search).with { |path,params| params[:checksum_type] == :none }.returns [@metadata] - @file.expects(:newchild).with("my/file").returns "fiebar" - @file.recurse_local + it "should be true if recurse is remote" do + file[:recurse] = :remote + file.must be_recurse end - end - it "should have a method for performing link recursion" do - @file.must respond_to(:recurse_link) + it "should be false if recurse is false" do + file[:recurse] = false + file.must_not be_recurse + end end - describe "when doing link recursion" do + describe "#recurse_link" do before do @first = stub 'first', :relative_path => "first", :full_path => "/my/first", :ftype => "directory" @second = stub 'second', :relative_path => "second", :full_path => "/my/second", :ftype => "file" @resource = stub 'file', :[]= => nil end it "should pass its target to the :perform_recursion method" do - @file[:target] = "mylinks" - @file.expects(:perform_recursion).with("mylinks").returns [@first] - @file.stubs(:newchild).returns @resource - @file.recurse_link({}) + file[:target] = "mylinks" + file.expects(:perform_recursion).with("mylinks").returns [@first] + file.stubs(:newchild).returns @resource + file.recurse_link({}) end it "should ignore the recursively-found '.' file and configure the top-level file to create a directory" do @first.stubs(:relative_path).returns "." - @file[:target] = "mylinks" - @file.expects(:perform_recursion).with("mylinks").returns [@first] - @file.stubs(:newchild).never - @file.expects(:[]=).with(:ensure, :directory) - @file.recurse_link({}) + file[:target] = "mylinks" + file.expects(:perform_recursion).with("mylinks").returns [@first] + file.stubs(:newchild).never + file.expects(:[]=).with(:ensure, :directory) + file.recurse_link({}) end it "should create a new child resource for each generated metadata instance's relative path that doesn't already exist in the children hash" do - @file.expects(:perform_recursion).returns [@first, @second] - @file.expects(:newchild).with(@first.relative_path).returns @resource - @file.recurse_link("second" => @resource) + file.expects(:perform_recursion).returns [@first, @second] + file.expects(:newchild).with(@first.relative_path).returns @resource + file.recurse_link("second" => @resource) end it "should not create a new child resource for paths that already exist in the children hash" do - @file.expects(:perform_recursion).returns [@first] - @file.expects(:newchild).never - @file.recurse_link("first" => @resource) + file.expects(:perform_recursion).returns [@first] + file.expects(:newchild).never + file.recurse_link("first" => @resource) end it "should set the target to the full path of discovered file and set :ensure to :link if the file is not a directory" do - file = stub 'file' - file.expects(:[]=).with(:target, "/my/second") - file.expects(:[]=).with(:ensure, :link) + file.stubs(:perform_recursion).returns [@first, @second] + file.recurse_link("first" => @resource, "second" => file) - @file.stubs(:perform_recursion).returns [@first, @second] - @file.recurse_link("first" => @resource, "second" => file) + file[:ensure].should == :link + file[:target].should == "/my/second" end it "should :ensure to :directory if the file is a directory" do - file = stub 'file' - file.expects(:[]=).with(:ensure, :directory) + file.stubs(:perform_recursion).returns [@first, @second] + file.recurse_link("first" => file, "second" => @resource) - @file.stubs(:perform_recursion).returns [@first, @second] - @file.recurse_link("first" => file, "second" => @resource) + file[:ensure].should == :directory end it "should return a hash with both created and existing resources with the relative paths as the hash keys" do - file = 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} + 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) + describe "#recurse_local" do + before do + @metadata = stub 'metadata', :relative_path => "my/file" + end + + it "should pass its path to the :perform_recursion method" do + file.expects(:perform_recursion).with(file[:path]).returns [@metadata] + file.stubs(:newchild) + file.recurse_local + end + + it "should return an empty hash if the recursion returns nothing" do + file.expects(:perform_recursion).returns nil + file.recurse_local.should == {} + end + + it "should create a new child resource with each generated metadata instance's relative path" do + file.expects(:perform_recursion).returns [@metadata] + file.expects(:newchild).with(@metadata.relative_path).returns "fiebar" + file.recurse_local + end + + it "should not create a new child resource for the '.' directory" do + @metadata.stubs(:relative_path).returns "." + + file.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.indirection.expects(:search).with { |path,params| params[:checksum_type] == :none }.returns [@metadata] + file.expects(:newchild).with("my/file").returns "fiebar" + file.recurse_local + end end - describe "when doing remote recursion" do + describe "#recurse_remote" do before do - @file[:source] = "puppet://foo/bar" + file[:source] = "puppet://foo/bar" @first = Puppet::FileServing::Metadata.new("/my", :relative_path => "first") @second = Puppet::FileServing::Metadata.new("/my", :relative_path => "second") @first.stubs(:ftype).returns "directory" @second.stubs(:ftype).returns "directory" @parameter = stub 'property', :metadata= => nil @resource = stub 'file', :[]= => nil, :parameter => @parameter end it "should pass its source to the :perform_recursion method" do data = Puppet::FileServing::Metadata.new("/whatever", :relative_path => "foobar") - @file.expects(:perform_recursion).with("puppet://foo/bar").returns [data] - @file.stubs(:newchild).returns @resource - @file.recurse_remote({}) + file.expects(:perform_recursion).with("puppet://foo/bar").returns [data] + file.stubs(:newchild).returns @resource + file.recurse_remote({}) end it "should not recurse when the remote file is not a directory" do data = Puppet::FileServing::Metadata.new("/whatever", :relative_path => ".") data.stubs(:ftype).returns "file" - @file.expects(:perform_recursion).with("puppet://foo/bar").returns [data] - @file.expects(:newchild).never - @file.recurse_remote({}) + file.expects(:perform_recursion).with("puppet://foo/bar").returns [data] + file.expects(:newchild).never + file.recurse_remote({}) end it "should set the source of each returned file to the searched-for URI plus the found relative path" do @first.expects(:source=).with File.join("puppet://foo/bar", @first.relative_path) - @file.expects(:perform_recursion).returns [@first] - @file.stubs(:newchild).returns @resource - @file.recurse_remote({}) + file.expects(:perform_recursion).returns [@first] + file.stubs(:newchild).returns @resource + file.recurse_remote({}) end it "should create a new resource for any relative file paths that do not already have a resource" do - @file.stubs(:perform_recursion).returns [@first] - @file.expects(:newchild).with("first").returns @resource - @file.recurse_remote({}).should == {"first" => @resource} + file.stubs(:perform_recursion).returns [@first] + file.expects(:newchild).with("first").returns @resource + file.recurse_remote({}).should == {"first" => @resource} end it "should not create a new resource for any relative file paths that do already have a resource" do - @file.stubs(:perform_recursion).returns [@first] - @file.expects(:newchild).never - @file.recurse_remote("first" => @resource) + file.stubs(:perform_recursion).returns [@first] + file.expects(:newchild).never + file.recurse_remote("first" => @resource) end it "should set the source of each resource to the source of the metadata" do - @file.stubs(:perform_recursion).returns [@first] + file.stubs(:perform_recursion).returns [@first] @resource.stubs(:[]=) @resource.expects(:[]=).with(:source, File.join("puppet://foo/bar", @first.relative_path)) - @file.recurse_remote("first" => @resource) + file.recurse_remote("first" => @resource) end # LAK:FIXME This is a bug, but I can't think of a fix for it. Fortunately it's already # filed, and when it's fixed, we'll just fix the whole flow. it "should set the checksum type to :md5 if the remote file is a file" do @first.stubs(:ftype).returns "file" - @file.stubs(:perform_recursion).returns [@first] + file.stubs(:perform_recursion).returns [@first] @resource.stubs(:[]=) @resource.expects(:[]=).with(:checksum, :md5) - @file.recurse_remote("first" => @resource) + file.recurse_remote("first" => @resource) end it "should store the metadata in the source property for each resource so the source does not have to requery the metadata" do - @file.stubs(:perform_recursion).returns [@first] + file.stubs(:perform_recursion).returns [@first] @resource.expects(:parameter).with(:source).returns @parameter @parameter.expects(:metadata=).with(@first) - @file.recurse_remote("first" => @resource) + file.recurse_remote("first" => @resource) end it "should not create a new resource for the '.' file" do @first.stubs(:relative_path).returns "." - @file.stubs(:perform_recursion).returns [@first] + file.stubs(:perform_recursion).returns [@first] - @file.expects(:newchild).never + file.expects(:newchild).never - @file.recurse_remote({}) + file.recurse_remote({}) end it "should store the metadata in the main file's source property if the relative path is '.'" do @first.stubs(:relative_path).returns "." - @file.stubs(:perform_recursion).returns [@first] + file.stubs(:perform_recursion).returns [@first] - @file.parameter(:source).expects(:metadata=).with @first + file.parameter(:source).expects(:metadata=).with @first - @file.recurse_remote("first" => @resource) + file.recurse_remote("first" => @resource) end describe "and multiple sources are provided" do describe "and :sourceselect is set to :first" do it "should create file instances for the results for the first source to return any values" do data = Puppet::FileServing::Metadata.new("/whatever", :relative_path => "foobar") - @file[:source] = %w{/one /two /three /four} - @file.expects(:perform_recursion).with("/one").returns nil - @file.expects(:perform_recursion).with("/two").returns [] - @file.expects(:perform_recursion).with("/three").returns [data] - @file.expects(:perform_recursion).with("/four").never - @file.expects(:newchild).with("foobar").returns @resource - @file.recurse_remote({}) + file[:source] = %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 + file[:sourceselect] = :all end it "should return every found file that is not in a previous source" do klass = Puppet::FileServing::Metadata - @file[:source] = %w{/one /two /three /four} - @file.stubs(:newchild).returns @resource + file[:source] = %w{/one /two /three /four} + file.stubs(:newchild).returns @resource one = [klass.new("/one", :relative_path => "a")] - @file.expects(:perform_recursion).with("/one").returns one - @file.expects(:newchild).with("a").returns @resource + file.expects(:perform_recursion).with("/one").returns one + file.expects(:newchild).with("a").returns @resource two = [klass.new("/two", :relative_path => "a"), klass.new("/two", :relative_path => "b")] - @file.expects(:perform_recursion).with("/two").returns two - @file.expects(:newchild).with("b").returns @resource + file.expects(:perform_recursion).with("/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("/three").returns three + file.expects(:newchild).with("c").returns @resource - @file.expects(:perform_recursion).with("/four").returns [] + file.expects(:perform_recursion).with("/four").returns [] - @file.recurse_remote({}) + file.recurse_remote({}) end end end end - describe "when specifying both source, and content properties" do - before do - @file[:source] = '/one' - @file[:content] = 'file contents' + describe "#perform_recursion" do + it "should use Metadata to do its recursion" do + Puppet::FileServing::Metadata.indirection.expects(:search) + file.perform_recursion(file[:path]) end - it "should raise an exception" do - lambda {@file.validate }.should raise_error(/You cannot specify more than one of/) + it "should use the provided path as the key to the search" do + Puppet::FileServing::Metadata.indirection.expects(:search).with { |key, options| key == "/foo" } + file.perform_recursion("/foo") end - end - describe "when using source" do - before do - @file[:source] = '/one' + it "should return the results of the metadata search" do + Puppet::FileServing::Metadata.indirection.expects(:search).returns "foobar" + file.perform_recursion(file[:path]).should == "foobar" end - Puppet::Type::File::ParameterChecksum.value_collection.values.reject {|v| v == :none}.each do |checksum_type| - describe "with checksum '#{checksum_type}'" do - before do - @file[:checksum] = checksum_type - end - it 'should validate' do + it "should pass its recursion value to the search" do + file[:recurse] = true + Puppet::FileServing::Metadata.indirection.expects(:search).with { |key, options| options[:recurse] == true } + file.perform_recursion(file[:path]) + end - lambda { @file.validate }.should_not raise_error - end - end + it "should pass true if recursion is remote" do + file[:recurse] = :remote + Puppet::FileServing::Metadata.indirection.expects(:search).with { |key, options| options[:recurse] == true } + file.perform_recursion(file[:path]) end - describe "with checksum 'none'" do - before do - @file[:checksum] = :none - end + it "should pass its recursion limit value to the search" do + file[:recurselimit] = 10 + Puppet::FileServing::Metadata.indirection.expects(:search).with { |key, options| options[:recurselimit] == 10 } + file.perform_recursion(file[:path]) + end - it 'should raise an exception when validating' do - lambda { @file.validate }.should raise_error(/You cannot specify source when using checksum 'none'/) - end + it "should configure the search to ignore or manage links" do + file[:links] = :manage + Puppet::FileServing::Metadata.indirection.expects(:search).with { |key, options| options[:links] == :manage } + file.perform_recursion(file[:path]) + end + + it "should pass its 'ignore' setting to the search if it has one" do + file[:ignore] = %w{.svn CVS} + Puppet::FileServing::Metadata.indirection.expects(:search).with { |key, options| options[:ignore] == %w{.svn CVS} } + file.perform_recursion(file[:path]) end end - describe "when using content" do - before do - @file[:content] = 'file contents' + describe "#remove_existing" do + it "should do nothing if the file doesn't exist" do + file.remove_existing(:file).should == nil end - (Puppet::Type::File::ParameterChecksum.value_collection.values - SOURCE_ONLY_CHECKSUMS).each do |checksum_type| - describe "with checksum '#{checksum_type}'" do - before do - @file[:checksum] = checksum_type - end + it "should fail if it can't backup the file" do + file.stubs(:stat).returns stub('stat') + file.stubs(:perform_backup).returns false - it 'should validate' do - lambda { @file.validate }.should_not raise_error - end - end + expect { file.remove_existing(:file) }.to raise_error(Puppet::Error, /Could not back up; will not replace/) end - SOURCE_ONLY_CHECKSUMS.each do |checksum_type| - describe "with checksum '#{checksum_type}'" do - it 'should raise an exception when validating' do - @file[:checksum] = checksum_type + it "should not do anything if the file is already the right type and not a link" do + file.stubs(:stat).returns stub('stat', :ftype => 'file') - lambda { @file.validate }.should raise_error(/You cannot specify content when using checksum '#{checksum_type}'/) - end - end + file.remove_existing(:file).should == nil + end + + it "should not remove directories and should not invalidate the stat unless force is set" do + # Actually call stat to set @needs_stat to nil + file.stat + file.stubs(:stat).returns stub('stat', :ftype => 'directory') + + file.remove_existing(:file) + + file.instance_variable_get(:@stat).should == nil + @logs.should be_any {|log| log.level == :notice and log.message =~ /Not removing directory; use 'force' to override/} + end + + it "should remove a directory if force is set" do + file[:force] = true + file.stubs(:stat).returns stub('stat', :ftype => 'directory') + + FileUtils.expects(:rmtree).with(file[:path]) + + file.remove_existing(:file).should == true + end + + it "should remove an existing file" do + file.stubs(:perform_backup).returns true + FileUtils.touch(path) + + file.remove_existing(:directory).should == true + + File.exists?(file[:path]).should == false + end + + it "should remove an existing link", :unless => Puppet.features.microsoft_windows? do + file.stubs(:perform_backup).returns true + + target = tmpfile('link_target') + FileUtils.touch(target) + FileUtils.symlink(target, path) + file[:target] = target + + file.remove_existing(:directory).should == true + + File.exists?(file[:path]).should == false + end + + it "should fail if the file is not a file, link, or directory" do + file.stubs(:stat).returns stub('stat', :ftype => 'socket') + + expect { file.remove_existing(:file) }.to raise_error(Puppet::Error, /Could not back up files of type socket/) + end + + it "should invalidate the existing stat of the file" do + # Actually call stat to set @needs_stat to nil + file.stat + file.stubs(:stat).returns stub('stat', :ftype => 'file') + + File.stubs(:unlink) + + file.remove_existing(:directory).should == true + file.instance_variable_get(:@stat).should == :needs_stat end end - describe "when returning resources with :eval_generate" do - before do - @graph = stub 'graph', :add_edge => nil - @catalog.stubs(:relationship_graph).returns @graph + describe "#retrieve" do + it "should copy the source values if the 'source' parameter is set" do + file[:source] = '/foo/bar' + file.parameter(:source).expects(:copy_source_values) + file.retrieve + end + end - @file.catalog = @catalog - @file[:recurse] = true + describe "#should_be_file?" do + it "should have a method for determining if the file should be a normal file" do + file.must respond_to(:should_be_file?) end - it "should recurse if recursion is enabled" do - resource = stub('resource', :[] => "resource") - @file.expects(:recurse?).returns true - @file.expects(:recurse).returns [resource] - @file.eval_generate.should == [resource] + it "should be a file if :ensure is set to :file" do + file[:ensure] = :file + file.must be_should_be_file end - it "should not recurse if recursion is disabled" do - @file.expects(:recurse?).returns false - @file.expects(:recurse).never - @file.eval_generate.should == [] + it "should be a file if :ensure is set to :present and the file exists as a normal file" do + file.stubs(:stat).returns(mock('stat', :ftype => "file")) + file[:ensure] = :present + file.must be_should_be_file + end + + it "should not be a file if :ensure is set to something other than :file" do + file[:ensure] = :directory + file.must_not be_should_be_file end - it "should return each resource found through recursion" do - foo = stub 'foo', :[] => "/foo" - bar = stub 'bar', :[] => "/bar" - bar2 = stub 'bar2', :[] => "/bar" + 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 - @file.expects(:recurse).returns [foo, bar] + it "should be a file if neither :ensure nor :content is set but the file exists as a normal file" do + file.stubs(:stat).returns(mock("stat", :ftype => "file")) + file.must be_should_be_file + end - @file.eval_generate.should == [foo, bar] + it "should not be a file if neither :ensure nor :content is set but the file exists but not as a normal file" do + file.stubs(:stat).returns(mock("stat", :ftype => "directory")) + file.must_not be_should_be_file end end - describe "when recursing" do + describe "#stat", :unless => Puppet.features.microsoft_windows? do before do - @file[:recurse] = true - @metadata = Puppet::FileServing::Metadata + target = tmpfile('link_target') + FileUtils.touch(target) + FileUtils.symlink(target, path) + + file[:target] = target + file[:links] = :manage # so we always use :lstat end - describe "and a source is set" do - before { @file[:source] = "/my/source" } + it "should stat the target if it is following links" do + file[:links] = :follow - it "should pass the already-discovered resources to recurse_remote" do - @file.stubs(:recurse_local).returns(:foo => "bar") - @file.expects(:recurse_remote).with(:foo => "bar").returns [] - @file.recurse - end + file.stat.ftype.should == 'file' end - describe "and a target is set" do - before { @file[:target] = "/link/target" } + it "should stat the link if is it not following links" do + file[:links] = :manage - it "should use recurse_link" do - @file.stubs(:recurse_local).returns(:foo => "bar") - @file.expects(:recurse_link).with(:foo => "bar").returns [] - @file.recurse - end + file.stat.ftype.should == 'link' end - it "should use recurse_local if recurse is not remote" do - @file.expects(:recurse_local).returns({}) - @file.recurse - end + it "should return nil if the file does not exist" do + file[:path] = '/foo/bar/baz/non-existent' - it "should not use recurse_local if recurse remote" do - @file[:recurse] = :remote - @file.expects(:recurse_local).never - @file.recurse + file.stat.should be_nil end - it "should return the generated resources as an array sorted by file path" do - one = stub 'one', :[] => "/one" - two = stub 'two', :[] => "/one/two" - three = stub 'three', :[] => "/three" - @file.expects(:recurse_local).returns(:one => one, :two => two, :three => three) - @file.recurse.should == [one, two, three] + it "should return nil if the file cannot be stat'ed" do + dir = tmpfile('link_test_dir') + child = File.join(dir, 'some_file') + Dir.mkdir(dir) + File.chmod(0, dir) + + file[:path] = child + + file.stat.should be_nil + + # chmod it back so we can clean it up + File.chmod(0777, dir) end - describe "and purging is enabled" do - before do - @file[:purge] = true - end + it "should return the stat instance" do + file.stat.should be_a(File::Stat) + end - it "should configure each file to be removed" do - local = stub 'local' - local.stubs(:[]).with(:source).returns nil # Thus, a local file - local.stubs(:[]).with(:path).returns "foo" - @file.expects(:recurse_local).returns("local" => local) - local.expects(:[]=).with(:ensure, :absent) + it "should cache the stat instance" do + file.stat.should equal(file.stat) + end + end - @file.recurse - end + describe "#write" do + it "should propagate failures encountered when renaming the temporary file" do + File.stubs(:open) + File.expects(:rename).raises ArgumentError - it "should not remove files that exist in the remote repository" do - @file["source"] = "/my/file" - @file.expects(:recurse_local).returns({}) + file[:backup] = 'puppet' - remote = stub 'remote' - remote.stubs(:[]).with(:source).returns "/whatever" # Thus, a remote file - remote.stubs(:[]).with(:path).returns "foo" + file.stubs(:validate_checksum?).returns(false) - @file.expects(:recurse_remote).with { |hash| hash["remote"] = remote } - remote.expects(:[]=).with(:ensure, :absent).never + property = stub('content_property', :actual_content => "something", :length => "something".length) + file.stubs(:property).with(:content).returns(property) - @file.recurse - end + lambda { file.write(:content) }.should raise_error(Puppet::Error) end - describe "and making a new child resource" do - it "should not copy the parent resource's parent" do - Puppet::Type.type(:file).expects(:new).with { |options| ! options.include?(:parent) } - @file.newchild("my/path") - end + it "should delegate writing to the content property" do + filehandle = stub_everything 'fh' + File.stubs(:open).yields(filehandle) + File.stubs(:rename) + property = stub('content_property', :actual_content => "something", :length => "something".length) + file[:backup] = 'puppet' + + file.stubs(:validate_checksum?).returns(false) + file.stubs(:property).with(:content).returns(property) - {: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) + property.expects(:write).with(filehandle) - @file.class.expects(:new).with { |params| params[param].nil? } + file.write(:content) + end - @file.newchild("sub/file") - end - end + describe "when validating the checksum" do + before { file.stubs(:validate_checksum?).returns(true) } - 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 fail if the checksum parameter and content checksums do not match" do + checksum = stub('checksum_parameter', :sum => 'checksum_b', :sum_file => 'checksum_b') + file.stubs(:parameter).with(:checksum).returns(checksum) + + property = stub('content_property', :actual_content => "something", :length => "something".length, :write => 'checksum_a') + file.stubs(:property).with(:content).returns(property) - it "should not copy default values to the new child" do - @file.class.expects(:new).with { |params| params[:backup].nil? } - @file.newchild("my/path") + lambda { file.write :NOTUSED }.should raise_error(Puppet::Error) end + end + + describe "when not validating the checksum" do + before { file.stubs(:validate_checksum?).returns(false) } - it "should not copy values to the child which were set by the source" do - @file[:source] = "/foo/bar" - metadata = stub 'metadata', :owner => "root", :group => "root", :mode => 0755, :ftype => "file", :checksum => "{md5}whatever" - @file.parameter(:source).stubs(:metadata).returns metadata + it "should not fail if the checksum property and content checksums do not match" do + checksum = stub('checksum_parameter', :sum => 'checksum_b') + file.stubs(:parameter).with(:checksum).returns(checksum) - @file.parameter(:source).copy_source_values + property = stub('content_property', :actual_content => "something", :length => "something".length, :write => 'checksum_a') + file.stubs(:property).with(:content).returns(property) - @file.class.expects(:new).with { |params| params[:group].nil? } - @file.newchild("my/path") + lambda { file.write :NOTUSED }.should_not raise_error(Puppet::Error) end end end - describe "when setting the backup" do - it "should default to 'puppet'" do - Puppet::Type::File.new(:name => "/my/file")[:backup].should == "puppet" + describe "#fail_if_checksum_is_wrong" do + it "should fail if the checksum of the file doesn't match the expected one" do + expect do + file.instance_eval do + parameter(:checksum).stubs(:sum_file).returns('wrong!!') + fail_if_checksum_is_wrong(self[:path], 'anything!') + end + end.to raise_error(Puppet::Error, /File written to disk did not match checksum/) end - it "should allow setting backup to 'false'" do - (!Puppet::Type::File.new(:name => "/my/file", :backup => false)[:backup]).should be_true + it "should not fail if the checksum is correct" do + file.instance_eval do + parameter(:checksum).stubs(:sum_file).returns('anything!') + fail_if_checksum_is_wrong(self[:path], 'anything!').should == nil + end end - it "should set the backup to '.puppet-bak' if it is set to true" do - Puppet::Type::File.new(:name => "/my/file", :backup => true)[:backup].should == ".puppet-bak" + it "should not fail if the checksum is absent" do + file.instance_eval do + parameter(:checksum).stubs(:sum_file).returns(nil) + fail_if_checksum_is_wrong(self[:path], 'anything!').should == nil + end end + end - it "should support any other backup extension" do - Puppet::Type::File.new(:name => "/my/file", :backup => ".bak")[:backup].should == ".bak" - end + describe "#write_content" do + it "should delegate writing the file to the content property" do + io = stub('io') + file[:content] = "some content here" + file.property(:content).expects(:write).with(io) - it "should set the filebucket when backup is set to a string matching the name of a filebucket in the catalog" do - catalog = Puppet::Resource::Catalog.new - bucket_resource = Puppet::Type.type(:filebucket).new :name => "foo", :path => "/my/file/bucket" - catalog.add_resource bucket_resource + file.send(:write_content, io) + end + end - file = Puppet::Type::File.new(:name => "/my/file") - catalog.add_resource file + describe "#write_temporary_file?" do + it "should be true if the file has specified content" do + file[:content] = 'some content' - file[:backup] = "foo" - file.bucket.should == bucket_resource.bucket + file.send(:write_temporary_file?).should be_true end - it "should find filebuckets added to the catalog after the file resource was created" do - catalog = Puppet::Resource::Catalog.new + it "should be true if the file has specified source" do + file[:source] = '/tmp/foo' - file = Puppet::Type::File.new(:name => "/my/file", :backup => "foo") - catalog.add_resource file + file.send(:write_temporary_file?).should be_true + end - bucket_resource = Puppet::Type.type(:filebucket).new :name => "foo", :path => "/my/file/bucket" - catalog.add_resource bucket_resource + it "should be false if the file has neither content nor source" do + file.send(:write_temporary_file?).should be_false + end + end - file.bucket.should == bucket_resource.bucket + describe "#property_fix" do + { + :mode => 0777, + :owner => 'joeuser', + :group => 'joeusers', + :seluser => 'seluser', + :selrole => 'selrole', + :seltype => 'seltype', + :selrange => 'selrange' + }.each do |name,value| + it "should sync the #{name} property if it's not in sync" do + file[name] = value + + prop = file.property(name) + prop.expects(:retrieve) + prop.expects(:safe_insync?).returns false + prop.expects(:sync) + + file.send(:property_fix) + end end + end + + describe "when autorequiring" do + describe "directories" do + it "should autorequire its parent directory" do + dir = described_class.new(:path => File.dirname(path)) + catalog.add_resource file + catalog.add_resource dir + reqs = file.autorequire + reqs[0].source.must == dir + reqs[0].target.must == file + end + + it "should autorequire its nearest ancestor directory" do + dir = described_class.new(:path => File.dirname(path)) + grandparent = described_class.new(:path => File.dirname(File.dirname(path))) + catalog.add_resource file + catalog.add_resource dir + catalog.add_resource grandparent + reqs = file.autorequire + reqs.length.must == 1 + reqs[0].source.must == dir + reqs[0].target.must == file + end - it "should 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 + it "should not autorequire anything when there is no nearest ancestor directory" do + catalog.add_resource file + file.autorequire.should be_empty + end - file = Puppet::Type::File.new(:name => "/my/file", :backup => false) - catalog.add_resource file + it "should not autorequire its parent dir if its parent dir is itself" do + file[:path] = '/' + catalog.add_resource file + file.autorequire.should be_empty + end - file.bucket.should be_nil + describe "on Windows systems", :if => Puppet.features.microsoft_windows? do + describe "when using UNC filenames" do + it "should autorequire its parent directory" do + file[:path] = '//server/foo/bar/baz' + dir = described_class.new(:path => "//server/foo/bar") + catalog.add_resource file + catalog.add_resource dir + reqs = file.autorequire + reqs[0].source.must == dir + reqs[0].target.must == file + end + + it "should autorequire its nearest ancestor directory" do + file = described_class.new(:path => "//server/foo/bar/baz/qux") + dir = described_class.new(:path => "//server/foo/bar/baz") + grandparent = described_class.new(:path => "//server/foo/bar") + catalog.add_resource file + catalog.add_resource dir + catalog.add_resource grandparent + reqs = file.autorequire + reqs.length.must == 1 + reqs[0].source.must == dir + reqs[0].target.must == file + end + + it "should not autorequire anything when there is no nearest ancestor directory" do + file = described_class.new(:path => "//server/foo/bar/baz/qux") + catalog.add_resource file + file.autorequire.should be_empty + end + + it "should not autorequire its parent dir if its parent dir is itself" do + file = described_class.new(:path => "//server/foo") + catalog.add_resource file + puts file.autorequire + file.autorequire.should be_empty + end + end + end end + end - 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 + describe "when managing links" do + require 'tempfile' + + if @real_posix + describe "on POSIX systems" do + before do + Dir.mkdir(path) + @target = File.join(path, "target") + @link = File.join(path, "link") - file = Puppet::Type::File.new(:name => "/my/file", :backup => ".foo") - catalog.add_resource file + File.open(@target, "w", 0644) { |f| f.puts "yayness" } + File.symlink(@target, @link) - file.bucket.should be_nil - end + file[:path] = @link + file[:mode] = 0755 - it "should fail if there's no catalog and backup is not false" do - file = Puppet::Type::File.new(:name => "/my/file", :backup => "foo") + catalog.add_resource file + end - lambda { file.bucket }.should raise_error(Puppet::Error) + it "should default to managing the link" do + catalog.apply + # I convert them to strings so they display correctly if there's an error. + (File.stat(@target).mode & 007777).to_s(8).should == '644' + end + + it "should be able to follow links" do + file[:links] = :follow + catalog.apply + + (File.stat(@target).mode & 007777).to_s(8).should == '755' + end + end + else # @real_posix + # should recode tests using expectations instead of using the filesystem end - it "should fail if a non-existent catalog is specified" do - file = Puppet::Type::File.new(:name => "/my/file", :backup => "foo") - catalog = Puppet::Resource::Catalog.new - catalog.add_resource file + describe "on Microsoft Windows systems" do + before do + Puppet.features.stubs(:posix?).returns(false) + Puppet.features.stubs(:microsoft_windows?).returns(true) + end - lambda { file.bucket }.should raise_error(Puppet::Error) + it "should refuse to work with links" end + end - it "should be able to use the default filebucket without a catalog" do - file = Puppet::Type::File.new(:name => "/my/file", :backup => "puppet") - file.bucket.should be_instance_of(Puppet::FileBucket::Dipper) + describe "when using source" do + before do + file[:source] = '/one' end + Puppet::Type::File::ParameterChecksum.value_collection.values.reject {|v| v == :none}.each do |checksum_type| + describe "with checksum '#{checksum_type}'" do + before do + file[:checksum] = checksum_type + end + + it 'should validate' do - it "should look up the filebucket during finish()" do - file = Puppet::Type::File.new(:name => "/my/file", :backup => ".foo") - file.expects(:bucket) - file.finish + lambda { file.validate }.should_not raise_error + end + end end - end - describe "when retrieving the current file state" do - it "should copy the source values if the 'source' parameter is set" do - file = Puppet::Type::File.new(:name => "/my/file", :source => "/foo/bar") - file.parameter(:source).expects(:copy_source_values) - file.retrieve + describe "with checksum 'none'" do + before do + file[:checksum] = :none + end + + it 'should raise an exception when validating' do + lambda { file.validate }.should raise_error(/You cannot specify source when using checksum 'none'/) + end end end - describe ".title_patterns" do + describe "when using content" do before do - @type_class = Puppet::Type.type(:file) + file[:content] = 'file contents' end - it "should have a regexp that captures the entire string, except for a terminating slash" do - patterns = @type_class.title_patterns - string = "abc/\n\tdef/" - patterns[0][0] =~ string - $1.should == "abc/\n\tdef" + (Puppet::Type::File::ParameterChecksum.value_collection.values - SOURCE_ONLY_CHECKSUMS).each do |checksum_type| + describe "with checksum '#{checksum_type}'" do + before do + file[:checksum] = checksum_type + end + + it 'should validate' do + lambda { file.validate }.should_not raise_error + end + end + end + + SOURCE_ONLY_CHECKSUMS.each do |checksum_type| + describe "with checksum '#{checksum_type}'" do + it 'should raise an exception when validating' do + file[:checksum] = checksum_type + + lambda { file.validate }.should raise_error(/You cannot specify content when using checksum '#{checksum_type}'/) + end + end end end describe "when auditing" do + before :each do + # to prevent the catalog from trying to write state.yaml + Puppet::Util::Storage.stubs(:store) + end + it "should not fail if creating a new file if group is not set" do - File.exists?(@path).should == false - file = Puppet::Type::File.new(:name => @path, :audit => "all", :content => "content") - catalog = Puppet::Resource::Catalog.new + file = described_class.new(:path => path, :audit => 'all', :content => 'content') catalog.add_resource(file) - Puppet::Util::Storage.stubs(:store) # to prevent the catalog from trying to write state.yaml - transaction = catalog.apply + report = catalog.apply.report - transaction.report.resource_statuses["File[#{@path}]"].failed.should == false - File.exists?(@path).should == true + report.resource_statuses["File[#{path}]"].should_not be_failed + File.read(path).should == 'content' end it "should not log errors if creating a new file with ensure present and no content" do - File.exists?(@path).should == false - file = Puppet::Type::File.new(:name => @path, :audit => "content", :ensure => "present") - catalog = Puppet::Resource::Catalog.new + file[:audit] = 'content' + file[:ensure] = 'present' catalog.add_resource(file) - Puppet::Util::Storage.stubs(:store) # to prevent the catalog from trying to write state.yaml - catalog.apply - @logs.reject {|l| l.level == :notice }.should be_empty + + File.should be_exist(path) + @logs.should_not be_any {|l| l.level != :notice } end end describe "when specifying both source and checksum" do it 'should use the specified checksum when source is first' do - @file[:source] = '/foo' - @file[:checksum] = :md5lite + file[:source] = '/foo' + file[:checksum] = :md5lite - @file[:checksum].should be :md5lite + file[:checksum].should == :md5lite end + it 'should use the specified checksum when source is last' do - @file[:checksum] = :md5lite - @file[:source] = '/foo' + file[:checksum] = :md5lite + file[:source] = '/foo' - @file[:checksum].should be :md5lite + file[:checksum].should == :md5lite end end - describe ".instances" do - it 'should return an empty array' do - Puppet::Type::File.instances.should == [] + describe "when validating" do + [[:source, :target], [:source, :content], [:target, :content]].each do |prop1,prop2| + it "should fail if both #{prop1} and #{prop2} are specified" do + file[prop1] = "prop1 value" + file[prop2] = "prop2 value" + expect do + file.validate + end.to raise_error(Puppet::Error, /You cannot specify more than one of/) + end end end + end