diff --git a/lib/puppet/type/pfile.rb b/lib/puppet/type/pfile.rb index f86e1e273..7d928d959 100644 --- a/lib/puppet/type/pfile.rb +++ b/lib/puppet/type/pfile.rb @@ -1,1169 +1,1171 @@ require 'digest/md5' require 'cgi' require 'etc' require 'uri' require 'fileutils' require 'puppet/network/handler' require 'puppet/util/diff' module Puppet newtype(:file) do include Puppet::Util::MethodHelper @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 Reductive Labs and we can hopefully work with you to develop a native resource to support what you are doing." newparam(:path) do desc "The path to the file to manage. Must be fully qualified." isnamevar validate do |value| unless value =~ /^#{File::SEPARATOR}/ raise Puppet::Error, "File paths must be fully qualified" 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 } The ``puppetmasterd`` daemon creates a filebucket by default, so you can usually back up to your main server with this configuration. Once you've described the bucket in your configuration, you can use it in any file:: file { \"/my/file\": source => \"/path/in/nfs/or/something\", backup => main } This will back the file up to the central server. At this point, the benefits of using a filebucket are that you do not have backup files lying around on each of your machines, a given version of a file is only backed up once, and you can restore any given file manually, no matter how old. Eventually, transactional support will be able to automatically restore filebucketed files. " defaultto { "puppet" } munge do |value| # I don't really know how this is happening. if value.is_a?(Array) value = value.shift end case value when false, "false", :false: false when true, "true", ".puppet-bak", :true: ".puppet-bak" when /^\./ value when String: # We can't depend on looking this up right now, # we have to do it after all of the objects # have been instantiated. if bucketobj = Puppet::Type.type(:filebucket)[value] @resource.bucket = bucketobj.bucket bucketobj.title else # Set it to the string; finish() turns it into a # filebucket. @resource.bucket = value value end when Puppet::Network::Client.client(:Dipper): @resource.bucket = value value.name else self.fail "Invalid backup type %s" % value.inspect end end end newparam(:recurse) do desc "Whether and how deeply to do recursive management." newvalues(:true, :false, :inf, /^[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 Integer, Fixnum, Bignum: value when /^\d+$/: Integer(value) else raise ArgumentError, "Invalid recurse value %s" % 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., ``*/*``." defaultto false 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, :ignore) # :ignore and :manage behave equivalently on local files, # but don't copy remote links defaultto :ignore 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 attr_accessor :bucket # Autorequire any parent directories. autorequire(:file) do if self[:path] File.dirname(self[:path]) else Puppet.err "no path for %s, somehow; cannot setup autorequires" % self.ref nil end end # Autorequire the owner and group of the file. {:user => :owner, :group => :group}.each do |type, property| autorequire(type) do if @parameters.include?(property) # The user/group property automatically converts to IDs next unless should = @parameters[property].shouldorig val = should[0] if val.is_a?(Integer) or val =~ /^\d+$/ nil else val end end end end CREATORS = [:content, :source, :target] validate do count = 0 CREATORS.each do |param| count += 1 if self.should(param) end if count > 1 self.fail "You cannot specify more than one of %s" % CREATORS.collect { |p| p.to_s}.join(", ") end end def self.[](path) return nil unless path super(path.gsub(/\/+/, '/').sub(/\/$/, '')) end # List files, but only one level deep. def self.instances(base = "/") unless FileTest.directory?(base) return [] end files = [] Dir.entries(base).reject { |e| e == "." or e == ".." }.each do |name| path = File.join(base, name) if obj = self[path] obj[:check] = :all files << obj else files << self.create( :name => path, :check => :all ) end end files end @depthfirst = false def argument?(arg) @arghash.include?(arg) 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. if writeable asuser = self.should(:owner) end end return asuser end # We have to do some extra finishing, to retrieve our bucket if # there is one. def finish # Let's cache these values, since there should really only be # a couple of these buckets @@filebuckets ||= {} # Look up our bucket, if there is one if bucket = self.bucket case bucket when String: if obj = @@filebuckets[bucket] # This sets the @value on :backup, too self.bucket = obj elsif bucket == "puppet" obj = Puppet::Network::Client.client(:Dipper).new( :Path => Puppet[:clientbucketdir] ) self.bucket = obj @@filebuckets[bucket] = obj elsif obj = Puppet::Type.type(:filebucket).bucket(bucket) @@filebuckets[bucket] = obj self.bucket = obj else self.fail "Could not find filebucket %s" % bucket end when Puppet::Network::Client.client(:Dipper): # things are hunky-dorey else self.fail "Invalid bucket type %s" % bucket.class end end super end # Create any children via recursion or whatever. def eval_generate recurse() end # Deal with backups. def handlebackup(file = nil) # let the path be specified file ||= self[:path] # if they specifically don't want a backup, then just say # we're good unless FileTest.exists?(file) return true end unless self[:backup] return true end case File.stat(file).ftype when "directory": if self[:recurse] # we don't need to backup directories when recurse is on return true else backup = self.bucket || self[:backup] case backup when Puppet::Network::Client.client(:Dipper): notice "Recursively backing up to filebucket" require 'find' Find.find(self[:path]) do |f| if File.file?(f) sum = backup.backup(f) self.info "Filebucketed %s to %s with sum %s" % [f, backup.name, sum] end end return true when String: newfile = file + backup # Just move it, since it's a directory. if FileTest.exists?(newfile) remove_backup(newfile) end begin bfile = file + backup # Ruby 1.8.1 requires the 'preserve' addition, but # later versions do not appear to require it. FileUtils.cp_r(file, bfile, :preserve => true) return true rescue => detail # since they said they want a backup, let's error out # if we couldn't make one self.fail "Could not back %s up: %s" % [file, detail.message] end else self.err "Invalid backup type %s" % backup.inspect return false end end when "file": backup = self.bucket || self[:backup] case backup when Puppet::Network::Client.client(:Dipper): sum = backup.backup(file) self.info "Filebucketed to %s with sum %s" % [backup.name, sum] return true when String: newfile = file + backup if FileTest.exists?(newfile) remove_backup(newfile) end begin # FIXME Shouldn't this just use a Puppet object with # 'source' specified? bfile = file + backup # Ruby 1.8.1 requires the 'preserve' addition, but # later versions do not appear to require it. FileUtils.cp(file, bfile, :preserve => true) return true rescue => detail # since they said they want a backup, let's error out # if we couldn't make one self.fail "Could not back %s up: %s" % [file, detail.message] end else self.err "Invalid backup type %s" % backup.inspect return false end when "link": return true else self.notice "Cannot backup files of type %s" % File.stat(file).ftype return false end end def handleignore(children) return children unless self[:ignore] self[:ignore].each { |ignore| ignored = [] Dir.glob(File.join(self[:path],ignore), File::FNM_DOTMATCH) { |match| ignored.push(File.basename(match)) } children = children - ignored } return children end def initialize(hash) # Store a copy of the arguments for later. tmphash = hash.to_hash # Used for caching clients @clients = {} super # Get rid of any duplicate slashes, and remove any trailing slashes. - @title = @title.gsub(/\/+/, "/").sub(/\/$/, "") + @title = @title.gsub(/\/+/, "/") + + @title.sub!(/\/$/, "") unless @title == "/" # Clean out as many references to any file paths as possible. # This was the source of many, many bugs. @arghash = tmphash @arghash.delete(self.class.namevar) [:source, :parent].each do |param| if @arghash.include?(param) @arghash.delete(param) end end @stat = nil end # Build a recursive map of a link source def linkrecurse(recurse) target = @parameters[:target].should method = :lstat if self[:links] == :follow method = :stat end targetstat = nil unless FileTest.exist?(target) return end # Now stat our target targetstat = File.send(method, target) unless targetstat.ftype == "directory" return end # Now that we know our corresponding target is a directory, # change our type self[:ensure] = :directory unless FileTest.readable? target self.notice "Cannot manage %s: permission denied" % self.name return end children = Dir.entries(target).reject { |d| d =~ /^\.+$/ } # Get rid of ignored children if @parameters.include?(:ignore) children = handleignore(children) end added = [] children.each do |file| Dir.chdir(target) do longname = File.join(target, file) # Files know to create directories when recursion # is enabled and we're making links args = { :recurse => recurse, :ensure => longname } if child = self.newchild(file, true, args) added << child end end end added end # Build up a recursive map of what's around right now def localrecurse(recurse) unless FileTest.exist?(self[:path]) and self.stat.directory? #self.info "%s is not a directory; not recursing" % # self[:path] return end unless FileTest.readable? self[:path] self.notice "Cannot manage %s: permission denied" % self.name return end children = Dir.entries(self[:path]) #Get rid of ignored children if @parameters.include?(:ignore) children = handleignore(children) end added = [] children.each { |file| file = File.basename(file) next if file =~ /^\.\.?$/ # skip . and .. options = {:recurse => recurse} if child = self.newchild(file, true, options) added << child end } added end # Create a new file or directory object as a child to the current # object. def newchild(path, local, hash = {}) raise(Puppet::DevError, "File recursion cannot happen without a catalog") unless catalog # make local copy of arguments args = symbolize_options(@arghash) # There's probably a better way to do this, but we don't want # to pass this info on. if v = args[:ensure] v = symbolize(v) args.delete(:ensure) end if path =~ %r{^#{File::SEPARATOR}} self.devfail( "Must pass relative paths to PFile#newchild()" ) else path = File.join(self[:path], path) end args[:path] = path unless hash.include?(:recurse) if args.include?(:recurse) if args[:recurse].is_a?(Integer) args[:recurse] -= 1 # reduce the level of recursion end end end hash.each { |key,value| args[key] = value } child = nil # The child might already exist because 'localrecurse' runs # before 'sourcerecurse'. I could push the override stuff into # a separate method or something, but the work is the same other # than this last bit, so it doesn't really make sense. if child = catalog.resource(:file, path) unless child.parent.object_id == self.object_id self.debug "Not managing more explicit file %s" % path return nil end # This is only necessary for sourcerecurse, because we might have # created the object with different 'should' values than are # set remotely. unless local args.each { |var,value| next if var == :path next if var == :name # behave idempotently unless child.should(var) == value child[var] = value end } end return nil else # create it anew #notice "Creating new file with args %s" % args.inspect args[:parent] = self begin # This method is used by subclasses of :file, so use the class name rather than hard-coding # :file. return nil unless child = catalog.create_implicit_resource(self.class.name, args) rescue => detail puts detail.backtrace self.notice "Cannot manage: %s" % [detail] return nil end end # LAK:FIXME This shouldn't be necessary, but as long as we're # modeling the relationship graph specifically, it is. catalog.relationship_graph.add_edge! self, child return child 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 # Recurse into the directory. This basically just calls 'localrecurse' # and maybe 'sourcerecurse', returning the collection of generated # files. def recurse # are we at the end of the recursion? return unless self.recurse? recurse = self[:recurse] # we might have a string, rather than a number if recurse.is_a?(String) if recurse =~ /^[0-9]+$/ recurse = Integer(recurse) else # anything else is infinite recursion recurse = true end end if recurse.is_a?(Integer) recurse -= 1 end children = [] # We want to do link-recursing before normal recursion so that all # of the target stuff gets copied over correctly. if @parameters.include? :target and ret = self.linkrecurse(recurse) children += ret end if ret = self.localrecurse(recurse) children += ret end # These will be files pulled in by the file source sourced = false if @parameters.include?(:source) ret, sourced = self.sourcerecurse(recurse) if ret children += ret end end # The purge check needs to happen after all of the other recursion. if self.purge? children.each do |child| if (sourced and ! sourced.include?(child[:path])) or ! child.managed? child[:ensure] = :absent end end end children end # A simple method for determining whether we should be recursing. def recurse? return false unless @parameters.include?(:recurse) val = @parameters[:recurse].value if val and (val == true or val > 0) return true else return false end end # Remove the old backup. def remove_backup(newfile) if self.class.name == :file and self[:links] != :follow method = :lstat else method = :stat end old = File.send(method, newfile).ftype if old == "directory" raise Puppet::Error, "Will not remove directory backup %s; use a filebucket" % newfile end info "Removing old backup of type %s" % File.send(method, newfile).ftype begin File.unlink(newfile) rescue => detail if Puppet[:trace] puts detail.backtrace end self.err "Could not remove old backup: %s" % detail return false end end # Remove any existing data. This is only used when dealing with # links or directories. def remove_existing(should) return unless s = stat(true) unless handlebackup self.fail "Could not back up; will not replace" end unless should.to_s == "link" return if s.ftype.to_s == should.to_s end case s.ftype when "directory": if self[:force] == :true debug "Removing existing directory for replacement with %s" % should FileUtils.rmtree(self[:path]) else notice "Not removing directory; use 'force' to override" end when "link", "file": debug "Removing existing %s for replacement with %s" % [s.ftype, should] File.unlink(self[:path]) else self.fail "Could not back up files of type %s" % s.ftype end end # a wrapper method to make sure the file exists before doing anything def retrieve unless stat = self.stat(true) self.debug "File does not exist" # If the file doesn't exist but we have a source, then call # retrieve on that property propertyvalues = properties().inject({}) { |hash, property| hash[property] = :absent hash } if @parameters.include?(:source) propertyvalues[:source] = @parameters[:source].retrieve end return propertyvalues end return currentpropvalues() end # This recurses against the remote source and makes sure the local # and remote structures match. It's run after 'localrecurse'. This # method only does anything when its corresponding remote entry is # a directory; in that case, this method creates file objects that # correspond to any contained remote files. def sourcerecurse(recurse) # we'll set this manually as necessary if @arghash.include?(:ensure) @arghash.delete(:ensure) end r = false if recurse unless recurse == 0 r = 1 end end ignore = self[:ignore] result = [] found = [] # Keep track of all the files we found in the source, so we can purge # appropriately. sourced = [] @parameters[:source].should.each do |source| sourceobj, path = uri2obj(source) # okay, we've got our source object; now we need to # build up a local file structure to match the remote # one server = sourceobj.server desc = server.list(path, self[:links], r, ignore) if desc == "" next end # Now create a new child for every file returned in the list. result += desc.split("\n").collect { |line| file, type = line.split("\t") next if file == "/" # skip the listing object name = file.sub(/^\//, '') # This makes sure that the first source *always* wins # for conflicting files. next if found.include?(name) # For directories, keep all of the sources, so that # sourceselect still works as planned. if type == "directory" newsource = @parameters[:source].should.collect do |tmpsource| tmpsource + file end else newsource = source + file end args = {:source => newsource} if type == file args[:recurse] = nil end found << name sourced << File.join(self[:path], name) self.newchild(name, false, args) }.reject {|c| c.nil? } if self[:sourceselect] == :first return [result, sourced] end end return [result, sourced] 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 # 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). def stat(refresh = false) method = :stat # Files are the only types that support links if (self.class.name == :file and self[:links] != :follow) or self.class.name == :tidy method = :lstat end path = self[:path] # Just skip them when they don't exist at all. unless FileTest.exists?(path) or FileTest.symlink?(path) @stat = nil return @stat end if @stat.nil? or refresh == true begin @stat = File.send(method, self[:path]) rescue Errno::ENOENT => error @stat = nil rescue Errno::EACCES => error self.warning "Could not stat; permission denied" @stat = nil end end return @stat end # We have to hack this just a little bit, because otherwise we'll get # an error when the target and the contents are created as properties on # the far side. def to_trans(retrieve = true) obj = super if obj[:target] == :notlink obj.delete(:target) end obj end def localfileserver unless defined? @@localfileserver args = { :Local => true, :Mount => { "/" => "localhost" }, :Config => false } @@localfileserver = Puppet::Network::Handler.handler(:fileserver).new(args) end @@localfileserver end def uri2obj(source) sourceobj = FileSource.new path = nil unless source devfail "Got a nil source" end if source =~ /^\// source = "file://localhost/%s" % URI.escape(source) sourceobj.mount = "localhost" sourceobj.local = true end begin uri = URI.parse(URI.escape(source)) rescue => detail self.fail "Could not understand source %s: %s" % [source, detail.to_s] end case uri.scheme when "file": sourceobj.server = localfileserver path = "/localhost" + uri.path when "puppet": # FIXME: We should cache clients by uri.host + uri.port # not by the full source path unless @clients.include?(source) host = uri.host host ||= Puppet[:server] unless Puppet[:name] == "puppet" if host.nil? server = localfileserver else args = { :Server => host } if uri.port args[:Port] = uri.port end server = Puppet::Network::Client.file.new(args) end @clients[source] = server end sourceobj.server = @clients[source] tmp = uri.path if tmp =~ %r{^/(\w+)} sourceobj.mount = $1 path = tmp #path = tmp.sub(%r{^/\w+},'') || "/" else self.fail "Invalid source path %s" % tmp end else self.fail "Got other URL type '%s' from %s" % [uri.scheme, source] end return [sourceobj, path.sub(/\/\//, '/')] end # Write out the file. We open the file correctly, with all of the # uid and mode and such, and then yield the file handle for actual # writing. def write(property, usetmp = true) mode = self.should(:mode) remove_existing(:file) # The temporary file path = nil if usetmp path = self[:path] + ".puppettmp" else path = self[:path] end # As the correct user and group write_if_writable(File.dirname(path)) do f = nil # Open our file with the correct modes if mode Puppet::Util.withumask(000) do f = File.open(path, File::CREAT|File::WRONLY|File::TRUNC, mode) end else f = File.open(path, File::CREAT|File::WRONLY|File::TRUNC) end # Yield it yield f f.flush f.close end # And put our new file in place if usetmp begin File.rename(path, self[:path]) rescue => detail self.err "Could not rename tmp %s for replacing: %s" % [self[:path], detail] ensure # Make sure the created file gets removed if FileTest.exists?(path) File.unlink(path) end end end # make sure all of the modes are actually correct property_fix # And then update our checksum, so the next run doesn't find it. # FIXME This is extra work, because it's going to read the whole # file back in again. self.setchecksum end # Run the block as the specified user if the dir is writeable, else # run it as root (or the current user). def write_if_writable(dir) yield # We're getting different behaviors from different versions of ruby, so... # asroot = true # Puppet::Util::SUIDManager.asuser(asuser(), self.should(:group)) do # if FileTest.writable?(dir) # asroot = false # yield # end # end # # if asroot # yield # end end private # Override the parent method, because we don't want to generate changes # when the file is missing and there is no 'ensure' state. def propertychanges(currentvalues) unless self.stat found = false ([:ensure] + CREATORS).each do |prop| if @parameters.include?(prop) found = true break end end unless found return [] end end super 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].include?(thing.name) # Make sure we get a new stat objct self.stat(true) currentvalue = thing.retrieve unless thing.insync?(currentvalue) thing.sync end end end end # Puppet.type(:pfile) # the filesource class can't include the path, because the path # changes for every file instance class FileSource attr_accessor :mount, :root, :server, :local 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/pfile/checksum' require 'puppet/type/pfile/content' # can create the file require 'puppet/type/pfile/source' # can create the file require 'puppet/type/pfile/target' # creates a different type of file require 'puppet/type/pfile/ensure' # can create the file require 'puppet/type/pfile/owner' require 'puppet/type/pfile/group' require 'puppet/type/pfile/mode' require 'puppet/type/pfile/type' end diff --git a/test/ral/types/file.rb b/test/ral/types/file.rb index a3a0c579a..aa2e63a89 100755 --- a/test/ral/types/file.rb +++ b/test/ral/types/file.rb @@ -1,1821 +1,1826 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../lib/puppettest' require 'puppettest' require 'puppettest/support/utils' require 'fileutils' class TestFile < Test::Unit::TestCase include PuppetTest::Support::Utils include PuppetTest::FileTesting # hmmm # this is complicated, because we store references to the created # objects in a central store def mkfile(hash) file = nil assert_nothing_raised { file = Puppet.type(:file).create(hash) } return file end def mktestfile # because luke's home directory is on nfs, it can't be used for testing # as root tmpfile = tempfile() File.open(tmpfile, "w") { |f| f.puts rand(100) } @@tmpfiles.push tmpfile mkfile(:name => tmpfile) end def setup super @file = Puppet::Type.type(:file) $method = @method_name Puppet[:filetimeout] = -1 end def teardown Puppet::Util::Storage.clear system("rm -rf %s" % Puppet[:statefile]) super end def initstorage Puppet::Util::Storage.init Puppet::Util::Storage.load end def clearstorage Puppet::Util::Storage.store Puppet::Util::Storage.clear end def test_owner file = mktestfile() users = {} count = 0 # collect five users Etc.passwd { |passwd| if count > 5 break else count += 1 end users[passwd.uid] = passwd.name } fake = {} # find a fake user while true a = rand(1000) begin Etc.getpwuid(a) rescue fake[a] = "fakeuser" break end end uid, name = users.shift us = {} us[uid] = name users.each { |uid, name| assert_apply(file) assert_nothing_raised() { file[:owner] = name } assert_nothing_raised() { file.retrieve } assert_apply(file) } end def test_group file = mktestfile() [%x{groups}.chomp.split(/ /), Process.groups].flatten.each { |group| assert_nothing_raised() { file[:group] = group } assert(file.property(:group)) assert(file.property(:group).should) } end def test_groups_fails_when_invalid assert_raise(Puppet::Error, "did not fail when the group was empty") do Puppet::Type.type(:file).create :path => "/some/file", :group => "" end end if Puppet::Util::SUIDManager.uid == 0 def test_createasuser dir = tmpdir() user = nonrootuser() path = File.join(tmpdir, "createusertesting") @@tmpfiles << path file = nil assert_nothing_raised { file = Puppet.type(:file).create( :path => path, :owner => user.name, :ensure => "file", :mode => "755" ) } comp = mk_catalog("createusertest", file) assert_events([:file_created], comp) end def test_nofollowlinks basedir = tempfile() Dir.mkdir(basedir) file = File.join(basedir, "file") link = File.join(basedir, "link") File.open(file, "w", 0644) { |f| f.puts "yayness"; f.flush } File.symlink(file, link) # First test 'user' user = nonrootuser() inituser = File.lstat(link).uid File.lchown(inituser, nil, link) obj = nil assert_nothing_raised { obj = Puppet.type(:file).create( :title => link, :owner => user.name ) } obj.retrieve # Make sure it defaults to managing the link assert_events([:file_changed], obj) assert_equal(user.uid, File.lstat(link).uid) assert_equal(inituser, File.stat(file).uid) File.chown(inituser, nil, file) File.lchown(inituser, nil, link) # Try following obj[:links] = :follow assert_events([:file_changed], obj) assert_equal(user.uid, File.stat(file).uid) assert_equal(inituser, File.lstat(link).uid) # And then explicitly managing File.chown(inituser, nil, file) File.lchown(inituser, nil, link) obj[:links] = :manage assert_events([:file_changed], obj) assert_equal(user.uid, File.lstat(link).uid) assert_equal(inituser, File.stat(file).uid) obj.delete(:owner) obj[:links] = :ignore # And then test 'group' group = nonrootgroup initgroup = File.stat(file).gid obj[:group] = group.name assert_events([:file_changed], obj) assert_equal(initgroup, File.stat(file).gid) assert_equal(group.gid, File.lstat(link).gid) File.chown(nil, initgroup, file) File.lchown(nil, initgroup, link) obj[:links] = :follow assert_events([:file_changed], obj) assert_equal(group.gid, File.stat(file).gid) File.chown(nil, initgroup, file) File.lchown(nil, initgroup, link) obj[:links] = :manage assert_events([:file_changed], obj) assert_equal(group.gid, File.lstat(link).gid) assert_equal(initgroup, File.stat(file).gid) end def test_ownerasroot file = mktestfile() users = {} count = 0 # collect five users Etc.passwd { |passwd| if count > 5 break else count += 1 end next if passwd.uid < 0 users[passwd.uid] = passwd.name } fake = {} # find a fake user while true a = rand(1000) begin Etc.getpwuid(a) rescue fake[a] = "fakeuser" break end end users.each { |uid, name| assert_nothing_raised() { file[:owner] = name } changes = [] assert_nothing_raised() { changes << file.evaluate } assert(changes.length > 0) assert_apply(file) currentvalue = file.retrieve assert(file.insync?(currentvalue)) assert_nothing_raised() { file[:owner] = uid } assert_apply(file) currentvalue = file.retrieve # make sure changing to number doesn't cause a sync assert(file.insync?(currentvalue)) } # We no longer raise an error here, because we check at run time #fake.each { |uid, name| # assert_raise(Puppet::Error) { # file[:owner] = name # } # assert_raise(Puppet::Error) { # file[:owner] = uid # } #} end def test_groupasroot file = mktestfile() [%x{groups}.chomp.split(/ /), Process.groups].flatten.each { |group| next unless Puppet::Util.gid(group) # grr. assert_nothing_raised() { file[:group] = group } assert(file.property(:group)) assert(file.property(:group).should) assert_apply(file) currentvalue = file.retrieve assert(file.insync?(currentvalue)) assert_nothing_raised() { file.delete(:group) } } end if Facter.value(:operatingsystem) == "Darwin" def test_sillyowner file = tempfile() File.open(file, "w") { |f| f.puts "" } File.chown(-2, nil, file) assert(File.stat(file).uid > 120000, "eh?") user = nonrootuser obj = Puppet::Type.newfile( :path => file, :owner => user.name ) assert_apply(obj) assert_equal(user.uid, File.stat(file).uid) end end else $stderr.puts "Run as root for complete owner and group testing" end def test_create %w{a b c d}.collect { |name| tempfile() + name.to_s }.each { |path| file =nil assert_nothing_raised() { file = Puppet.type(:file).create( :name => path, :ensure => "file" ) } assert_events([:file_created], file) assert_events([], file) assert(FileTest.file?(path), "File does not exist") assert(file.insync?(file.retrieve)) @@tmpfiles.push path } end def test_create_dir basedir = tempfile() Dir.mkdir(basedir) %w{a b c d}.collect { |name| "#{basedir}/%s" % name }.each { |path| file = nil assert_nothing_raised() { file = Puppet.type(:file).create( :name => path, :ensure => "directory" ) } assert(! FileTest.directory?(path), "Directory %s already exists" % [path]) assert_events([:directory_created], file) assert_events([], file) assert(file.insync?(file.retrieve)) assert(FileTest.directory?(path)) @@tmpfiles.push path } end def test_modes file = mktestfile # Set it to something else initially File.chmod(0775, file.title) [0644,0755,0777,0641].each { |mode| assert_nothing_raised() { file[:mode] = mode } assert_events([:file_changed], file) assert_events([], file) assert(file.insync?(file.retrieve)) assert_nothing_raised() { file.delete(:mode) } } end def test_checksums types = %w{md5 md5lite timestamp time} exists = "/tmp/sumtest-exists" nonexists = "/tmp/sumtest-nonexists" @@tmpfiles << exists @@tmpfiles << nonexists # try it both with files that exist and ones that don't files = [exists, nonexists] initstorage File.open(exists,File::CREAT|File::TRUNC|File::WRONLY) { |of| of.puts "initial text" } types.each { |type| files.each { |path| if Puppet[:debug] Puppet.warning "Testing %s on %s" % [type,path] end file = nil events = nil # okay, we now know that we have a file... assert_nothing_raised() { file = Puppet.type(:file).create( :name => path, :ensure => "file", :checksum => type ) } trans = nil currentvalues = file.retrieve if file.title !~ /nonexists/ sum = file.property(:checksum) assert(sum.insync?(currentvalues[sum]), "file is not in sync") end events = assert_apply(file) assert(events) assert(! events.include?(:file_changed), "File incorrectly changed") assert_events([], file) # We have to sleep because the time resolution of the time-based # mechanisms is greater than one second sleep 1 if type =~ /time/ assert_nothing_raised() { File.open(path,File::CREAT|File::TRUNC|File::WRONLY) { |of| of.puts "some more text, yo" } } Puppet.type(:file).clear # now recreate the file assert_nothing_raised() { file = Puppet.type(:file).create( :name => path, :checksum => type ) } trans = nil assert_events([:file_changed], file) # Run it a few times to make sure we aren't getting # spurious changes. sum = nil assert_nothing_raised do sum = file.property(:checksum).retrieve end assert(file.property(:checksum).insync?(sum), "checksum is not in sync") sleep 1.1 if type =~ /time/ assert_nothing_raised() { File.unlink(path) File.open(path,File::CREAT|File::TRUNC|File::WRONLY) { |of| # We have to put a certain amount of text in here or # the md5-lite test fails 2.times { of.puts rand(100) } of.flush } } assert_events([:file_changed], file) # verify that we're actually getting notified when a file changes assert_nothing_raised() { Puppet.type(:file).clear } if path =~ /nonexists/ File.unlink(path) end } } end def cyclefile(path) # i had problems with using :name instead of :path [:name,:path].each { |param| file = nil changes = nil comp = nil trans = nil initstorage assert_nothing_raised { file = Puppet.type(:file).create( param => path, :recurse => true, :checksum => "md5" ) } comp = Puppet.type(:component).create( :name => "component" ) comp.push file assert_nothing_raised { trans = comp.evaluate } assert_nothing_raised { trans.evaluate } clearstorage Puppet::Type.allclear } end def test_localrecurse # Create a test directory path = tempfile() dir = @file.create :path => path, :mode => 0755, :recurse => true config = mk_catalog(dir) Dir.mkdir(path) # Make sure we return nothing when there are no children ret = nil assert_nothing_raised() { ret = dir.localrecurse(true) } assert_equal([], ret, "empty dir returned children") # Now make a file and make sure we get it test = File.join(path, "file") File.open(test, "w") { |f| f.puts "yay" } assert_nothing_raised() { ret = dir.localrecurse(true) } fileobj = @file[test] assert(fileobj, "child object was not created") assert_equal([fileobj], ret, "child object was not returned") # And that it inherited our recurse setting assert_equal(true, fileobj[:recurse], "file did not inherit recurse") # Make sure it's not returned again assert_nothing_raised() { ret = dir.localrecurse(true) } assert_equal([], ret, "child object was returned twice") # Now just for completion, make sure we will return many files files = [] 10.times do |i| f = File.join(path, i.to_s) files << f File.open(f, "w") do |o| o.puts "" end end assert_nothing_raised() { ret = dir.localrecurse(true) } assert_equal(files.sort, ret.collect { |f| f.title }.sort, "child object was returned twice") # Clean everything up and start over files << test files.each do |f| File.unlink(f) end # Now make sure we correctly ignore things dir[:ignore] = "*.out" bad = File.join(path, "test.out") good = File.join(path, "yayness") [good, bad].each do |f| File.open(f, "w") { |o| o.puts "" } end assert_nothing_raised() { ret = dir.localrecurse(true) } assert_equal([good], ret.collect { |f| f.title }, "ignore failed") # Now make sure purging works dir[:purge] = true dir[:ignore] = "svn" assert_nothing_raised() { ret = dir.localrecurse(true) } assert_equal([bad], ret.collect { |f| f.title }, "purge failed") badobj = @file[bad] assert(badobj, "did not create bad object") end def test_recurse basedir = tempfile() FileUtils.mkdir_p(basedir) # Create our file dir = nil assert_nothing_raised { dir = Puppet.type(:file).create( :path => basedir, :check => %w{owner mode group} ) } return_nil = false # and monkey-patch it [:localrecurse, :linkrecurse].each do |m| dir.meta_def(m) do |recurse| if return_nil # for testing nil return, of course return nil else return [recurse] end end end # We have to special-case this, because it returns a list of # found files. dir.meta_def(:sourcerecurse) do |recurse| if return_nil # for testing nil return, of course return nil else return [recurse], [] end end # First try it with recurse set to false dir[:recurse] = false assert_nothing_raised do assert_nil(dir.recurse) end # Now try it with the different valid positive values [true, "true", "inf", 50].each do |value| assert_nothing_raised { dir[:recurse] = value} # Now make sure the methods are called appropriately ret = nil assert_nothing_raised do ret = dir.recurse end # We should only call the localrecurse method, so make sure # that's the case if value == 50 # Make sure our counter got decremented assert_equal([49], ret, "did not call localrecurse") else assert_equal([true], ret, "did not call localrecurse") end end # Make sure it doesn't recurse when we've set recurse to false [false, "false"].each do |value| assert_nothing_raised { dir[:recurse] = value } ret = nil assert_nothing_raised() { ret = dir.recurse } assert_nil(ret) end dir[:recurse] = true # Now add a target, so we do the linking thing dir[:target] = tempfile() ret = nil assert_nothing_raised { ret = dir.recurse } assert_equal([true, true], ret, "did not call linkrecurse") # And add a source, and make sure we call that dir[:source] = tempfile() assert_nothing_raised { ret = dir.recurse } assert_equal([true, true, true], ret, "did not call linkrecurse") # Lastly, make sure we correctly handle returning nil return_nil = true assert_nothing_raised { ret = dir.recurse } end def test_recurse? file = Puppet::Type.type(:file).create :path => tempfile # Make sure we default to false assert(! file.recurse?, "Recurse defaulted to true") [true, "true", 10, "inf"].each do |value| file[:recurse] = value assert(file.recurse?, "%s did not cause recursion" % value) end [false, "false", 0].each do |value| file[:recurse] = value assert(! file.recurse?, "%s caused recursion" % value) end end def test_recursion basedir = tempfile() subdir = File.join(basedir, "subdir") tmpfile = File.join(basedir,"testing") FileUtils.mkdir_p(subdir) dir = nil [true, "true", "inf", 50].each do |value| assert_nothing_raised { dir = Puppet.type(:file).create( :path => basedir, :recurse => value, :check => %w{owner mode group} ) } config = mk_catalog dir children = nil assert_nothing_raised { children = dir.eval_generate } assert_equal([subdir], children.collect {|c| c.title }, "Incorrect generated children") # Remove our subdir resource, subdir_resource = config.resource(:file, subdir) config.remove_resource(subdir_resource) # Create the test file File.open(tmpfile, "w") { |f| f.puts "yayness" } assert_nothing_raised { children = dir.eval_generate } # And make sure we get both resources back. assert_equal([subdir, tmpfile].sort, children.collect {|c| c.title }.sort, "Incorrect generated children when recurse == %s" % value.inspect) File.unlink(tmpfile) Puppet.type(:file).clear end end def test_filetype_retrieval file = nil # Verify it retrieves files of type directory assert_nothing_raised { file = Puppet.type(:file).create( :name => tmpdir(), :check => :type ) } assert_nothing_raised { file.evaluate } assert_equal("directory", file.property(:type).retrieve) # And then check files assert_nothing_raised { file = Puppet.type(:file).create( :name => tempfile(), :ensure => "file" ) } assert_apply(file) file[:check] = "type" assert_apply(file) assert_equal("file", file.property(:type).retrieve) file[:type] = "directory" currentvalues = {} assert_nothing_raised { currentvalues = file.retrieve } # The 'retrieve' method sets @should to @is, so they're never # out of sync. It's a read-only class. assert(file.insync?(currentvalues)) end def test_remove basedir = tempfile() subdir = File.join(basedir, "this") FileUtils.mkdir_p(subdir) dir = nil assert_nothing_raised { dir = Puppet.type(:file).create( :path => basedir, :recurse => true, :check => %w{owner mode group} ) } mk_catalog dir assert_nothing_raised { dir.eval_generate } obj = nil assert_nothing_raised { obj = Puppet.type(:file)[subdir] } assert(obj, "Could not retrieve subdir object") assert_nothing_raised { obj.remove(true) } assert_nothing_raised { obj = Puppet.type(:file)[subdir] } assert_nil(obj, "Retrieved removed object") end def test_path dir = tempfile() path = File.join(dir, "subdir") assert_nothing_raised("Could not make file") { FileUtils.mkdir_p(File.dirname(path)) File.open(path, "w") { |f| f.puts "yayness" } } file = nil dirobj = nil assert_nothing_raised("Could not make file object") { dirobj = Puppet.type(:file).create( :path => dir, :recurse => true, :check => %w{mode owner group} ) } mk_catalog dirobj assert_nothing_raised { dirobj.eval_generate } assert_nothing_raised { file = dirobj.class[path] } assert(file, "Could not retrieve file object") assert_equal("/%s" % file.ref, file.path) end def test_autorequire basedir = tempfile() subfile = File.join(basedir, "subfile") baseobj = Puppet.type(:file).create( :name => basedir, :ensure => "directory" ) subobj = Puppet.type(:file).create( :name => subfile, :ensure => "file" ) edge = nil assert_nothing_raised do edge = subobj.autorequire.shift end assert_equal(baseobj, edge.source, "file did not require its parent dir") assert_equal(subobj, edge.target, "file did not require its parent dir") end def test_content file = tempfile() str = "This is some content" obj = nil assert_nothing_raised { obj = Puppet.type(:file).create( :name => file, :content => str ) } assert(!obj.insync?(obj.retrieve), "Object is incorrectly in sync") assert_events([:file_created], obj) currentvalues = obj.retrieve assert(obj.insync?(currentvalues), "Object is not in sync") text = File.read(file) assert_equal(str, text, "Content did not copy correctly") newstr = "Another string, yo" obj[:content] = newstr assert(!obj.insync?(obj.retrieve), "Object is incorrectly in sync") assert_events([:file_changed], obj) text = File.read(file) assert_equal(newstr, text, "Content did not copy correctly") currentvalues = obj.retrieve assert(obj.insync?(currentvalues), "Object is not in sync") end # Unfortunately, I know this fails def disabled_test_recursivemkdir path = tempfile() subpath = File.join(path, "this", "is", "a", "dir") file = nil assert_nothing_raised { file = Puppet.type(:file).create( :name => subpath, :ensure => "directory", :recurse => true ) } comp = mk_catalog("yay", file) comp.finalize assert_apply(comp) #assert_events([:directory_created], comp) assert(FileTest.directory?(subpath), "Did not create directory") end # Make sure that content updates the checksum on the same run def test_checksumchange_for_content dest = tempfile() File.open(dest, "w") { |f| f.puts "yayness" } file = nil assert_nothing_raised { file = Puppet.type(:file).create( :name => dest, :checksum => "md5", :content => "This is some content" ) } file.retrieve assert_events([:file_changed], file) file.retrieve assert_events([], file) end # Make sure that content updates the checksum on the same run def test_checksumchange_for_ensure dest = tempfile() file = nil assert_nothing_raised { file = Puppet.type(:file).create( :name => dest, :checksum => "md5", :ensure => "file" ) } file.retrieve assert_events([:file_created], file) file.retrieve assert_events([], file) end # Make sure that content gets used before ensure def test_contentbeatsensure dest = tempfile() file = nil assert_nothing_raised { file = Puppet.type(:file).create( :name => dest, :ensure => "file", :content => "this is some content, yo" ) } currentvalues = file.retrieve assert_events([:file_created], file) file.retrieve assert_events([], file) assert_events([], file) end # Make sure that content gets used before ensure def test_deletion_beats_source dest = tempfile() source = tempfile() File.open(source, "w") { |f| f.puts "yay" } file = nil assert_nothing_raised { file = Puppet.type(:file).create( :name => dest, :ensure => :absent, :source => source ) } file.retrieve assert_events([], file) assert(! FileTest.exists?(dest), "file was copied during deletion") # Now create the dest, and make sure it gets deleted File.open(dest, "w") { |f| f.puts "boo" } assert_events([:file_removed], file) assert(! FileTest.exists?(dest), "file was not deleted during deletion") end def test_nameandpath path = tempfile() file = nil assert_nothing_raised { file = Puppet.type(:file).create( :title => "fileness", :path => path, :content => "this is some content" ) } assert_apply(file) assert(FileTest.exists?(path)) end # Make sure that a missing group isn't fatal at object instantiation time. def test_missinggroup file = nil assert_nothing_raised { file = Puppet.type(:file).create( :path => tempfile(), :group => "fakegroup" ) } assert(file.property(:group), "Group property failed") end def test_modecreation path = tempfile() file = Puppet.type(:file).create( :path => path, :ensure => "file", :mode => "0777" ) assert_equal(0777, file.should(:mode), "Mode did not get set correctly") assert_apply(file) assert_equal(0777, File.stat(path).mode & 007777, "file mode is incorrect") File.unlink(path) file[:ensure] = "directory" assert_apply(file) assert_equal(0777, File.stat(path).mode & 007777, "directory mode is incorrect") end def test_followlinks File.umask(0022) basedir = tempfile() Dir.mkdir(basedir) file = File.join(basedir, "file") link = File.join(basedir, "link") File.open(file, "w", 0644) { |f| f.puts "yayness"; f.flush } File.symlink(file, link) obj = nil assert_nothing_raised { obj = Puppet.type(:file).create( :path => link, :mode => "755" ) } obj.retrieve assert_events([], obj) # Assert that we default to not following links assert_equal("%o" % 0644, "%o" % (File.stat(file).mode & 007777)) # Assert that we can manage the link directly, but modes still don't change obj[:links] = :manage assert_events([], obj) assert_equal("%o" % 0644, "%o" % (File.stat(file).mode & 007777)) obj[:links] = :follow assert_events([:file_changed], obj) assert_equal("%o" % 0755, "%o" % (File.stat(file).mode & 007777)) # Now verify that content and checksum don't update, either obj.delete(:mode) obj[:checksum] = "md5" obj[:links] = :ignore assert_events([], obj) File.open(file, "w") { |f| f.puts "more text" } assert_events([], obj) obj[:links] = :follow assert_events([], obj) File.open(file, "w") { |f| f.puts "even more text" } assert_events([:file_changed], obj) obj.delete(:checksum) obj[:content] = "this is some content" obj[:links] = :ignore assert_events([], obj) File.open(file, "w") { |f| f.puts "more text" } assert_events([], obj) obj[:links] = :follow assert_events([:file_changed], obj) end # If both 'ensure' and 'content' are used, make sure that all of the other # properties are handled correctly. def test_contentwithmode path = tempfile() file = nil assert_nothing_raised { file = Puppet.type(:file).create( :path => path, :ensure => "file", :content => "some text\n", :mode => 0755 ) } assert_apply(file) assert_equal("%o" % 0755, "%o" % (File.stat(path).mode & 007777)) end def test_backupmodes File.umask(0022) file = tempfile() newfile = tempfile() File.open(file, "w", 0411) { |f| f.puts "yayness" } obj = nil assert_nothing_raised { obj = Puppet::Type.type(:file).create( :path => file, :content => "rahness\n", :backup => ".puppet-bak" ) } assert_apply(obj) backupfile = file + obj[:backup] @@tmpfiles << backupfile assert(FileTest.exists?(backupfile), "Backup file %s does not exist" % backupfile) assert_equal(0411, filemode(backupfile), "File mode is wrong for backupfile") bucket = "bucket" bpath = tempfile() Dir.mkdir(bpath) Puppet::Type.type(:filebucket).create( :title => bucket, :path => bpath ) obj[:backup] = bucket obj[:content] = "New content" assert_apply(obj) md5 = "18cc17fa3047fcc691fdf49c0a7f539a" dir, file, pathfile = Puppet::Network::Handler.filebucket.paths(bpath, md5) assert_equal(0440, filemode(file)) end def test_largefilechanges source = tempfile() dest = tempfile() # Now make a large file File.open(source, "w") { |f| 500.times { |i| f.puts "line %s" % i } } obj = Puppet::Type.type(:file).create( :title => dest, :source => source ) assert_events([:file_created], obj) File.open(source, File::APPEND|File::WRONLY) { |f| f.puts "another line" } assert_events([:file_changed], obj) # Now modify the dest file File.open(dest, File::APPEND|File::WRONLY) { |f| f.puts "one more line" } assert_events([:file_changed, :file_changed], obj) end def test_replacefilewithlink path = tempfile() link = tempfile() File.open(path, "w") { |f| f.puts "yay" } File.open(link, "w") { |f| f.puts "a file" } file = nil assert_nothing_raised { file = Puppet.type(:file).create( :ensure => path, :path => link ) } assert_events([:link_created], file) assert(FileTest.symlink?(link), "Link was not created") assert_equal(path, File.readlink(link), "Link was created incorrectly") end def test_file_with_spaces dir = tempfile() Dir.mkdir(dir) source = File.join(dir, "file spaces") dest = File.join(dir, "another space") File.open(source, "w") { |f| f.puts :yay } obj = Puppet::Type.type(:file).create( :path => dest, :source => source ) assert(obj, "Did not create file") assert_apply(obj) assert(FileTest.exists?(dest), "File did not get created") end def test_present_matches_anything path = tempfile() file = Puppet::Type.newfile(:path => path, :ensure => :present) currentvalues = file.retrieve assert(! file.insync?(currentvalues), "File incorrectly in sync") # Now make a file File.open(path, "w") { |f| f.puts "yay" } currentvalues = file.retrieve assert(file.insync?(currentvalues), "File not in sync") # Now make a directory File.unlink(path) Dir.mkdir(path) currentvalues = file.retrieve assert(file.insync?(currentvalues), "Directory not considered 'present'") Dir.rmdir(path) # Now make a link file[:links] = :manage otherfile = tempfile() File.symlink(otherfile, path) currentvalues = file.retrieve assert(file.insync?(currentvalues), "Symlink not considered 'present'") File.unlink(path) # Now set some content, and make sure it works file[:content] = "yayness" assert_apply(file) assert_equal("yayness", File.read(path), "Content did not get set correctly") end # Make sure unmanaged files are purged. def test_purge sourcedir = tempfile() destdir = tempfile() Dir.mkdir(sourcedir) Dir.mkdir(destdir) sourcefile = File.join(sourcedir, "sourcefile") dsourcefile = File.join(destdir, "sourcefile") localfile = File.join(destdir, "localfile") purgee = File.join(destdir, "to_be_purged") File.open(sourcefile, "w") { |f| f.puts "funtest" } # this file should get removed File.open(purgee, "w") { |f| f.puts "footest" } lfobj = Puppet::Type.newfile( :title => "localfile", :path => localfile, :content => "rahtest", :backup => false ) destobj = Puppet::Type.newfile(:title => "destdir", :path => destdir, :source => sourcedir, :backup => false, :recurse => true) config = mk_catalog(lfobj, destobj) config.apply assert(FileTest.exists?(dsourcefile), "File did not get copied") assert(FileTest.exists?(localfile), "File did not get created") assert(FileTest.exists?(purgee), "File got prematurely purged") assert_nothing_raised { destobj[:purge] = true } config.apply assert(FileTest.exists?(localfile), "Local file got purged") assert(FileTest.exists?(dsourcefile), "Source file got purged") assert(! FileTest.exists?(purgee), "File did not get purged") end # Testing #274. Make sure target can be used without 'ensure'. def test_target_without_ensure source = tempfile() dest = tempfile() File.open(source, "w") { |f| f.puts "funtest" } obj = nil assert_nothing_raised { obj = Puppet::Type.newfile(:path => dest, :target => source) } assert_apply(obj) end def test_autorequire_owner_and_group file = tempfile() comp = nil user = nil group =nil home = nil ogroup = nil assert_nothing_raised { user = Puppet.type(:user).create( :name => "pptestu", :home => file, :gid => "pptestg" ) home = Puppet.type(:file).create( :path => file, :owner => "pptestu", :group => "pptestg", :ensure => "directory" ) group = Puppet.type(:group).create( :name => "pptestg" ) comp = mk_catalog(user, group, home) } # Now make sure we get a relationship for each of these rels = nil assert_nothing_raised { rels = home.autorequire } assert(rels.detect { |e| e.source == user }, "owner was not autorequired") assert(rels.detect { |e| e.source == group }, "group was not autorequired") end # Testing #309 -- //my/file => /my/file def test_slash_deduplication ["/my/////file/for//testing", "//my/file/for/testing///", "/my/file/for/testing"].each do |path| file = nil assert_nothing_raised do file = Puppet::Type.newfile(:path => path) end assert_equal("/my/file/for/testing", file.title) assert_equal(file, Puppet::Type.type(:file)["/my/file/for/testing"]) Puppet::Type.type(:file).clear end end # Testing #304 def test_links_to_directories link = tempfile() file = tempfile() dir = tempfile() Dir.mkdir(dir) bucket = Puppet::Type.newfilebucket :name => "main" File.symlink(dir, link) File.open(file, "w") { |f| f.puts "" } assert_equal(dir, File.readlink(link)) obj = Puppet::Type.newfile :path => link, :ensure => :link, :target => file, :recurse => false, :backup => "main" assert_apply(obj) assert_equal(file, File.readlink(link)) end # Testing #303 def test_nobackups_with_links link = tempfile() new = tempfile() File.open(link, "w") { |f| f.puts "old" } File.open(new, "w") { |f| f.puts "new" } obj = Puppet::Type.newfile :path => link, :ensure => :link, :target => new, :recurse => true, :backup => false assert_nothing_raised do obj.handlebackup end bfile = [link, "puppet-bak"].join(".") assert(! FileTest.exists?(bfile), "Backed up when told not to") assert_apply(obj) assert(! FileTest.exists?(bfile), "Backed up when told not to") end # Make sure we consistently handle backups for all cases. def test_ensure_with_backups # We've got three file types, so make sure we can replace any type # with the other type and that backups are done correctly. types = [:file, :directory, :link] dir = tempfile() path = File.join(dir, "test") linkdest = tempfile() creators = { :file => proc { File.open(path, "w") { |f| f.puts "initial" } }, :directory => proc { Dir.mkdir(path) }, :link => proc { File.symlink(linkdest, path) } } bucket = Puppet::Type.newfilebucket :name => "main", :path => tempfile() obj = Puppet::Type.newfile :path => path, :force => true, :links => :manage Puppet[:trace] = true ["main", false].each do |backup| obj[:backup] = backup obj.finish types.each do |should| types.each do |is| # It makes no sense to replace a directory with a directory # next if should == :directory and is == :directory Dir.mkdir(dir) # Make the thing creators[is].call obj[:ensure] = should if should == :link obj[:target] = linkdest else if obj.property(:target) obj.delete(:target) end end # First try just removing the initial data assert_nothing_raised do obj.remove_existing(should) end unless is == should # Make sure the original is gone assert(! FileTest.exists?(obj[:path]), "remove_existing did not work: " + "did not remove %s with %s" % [is, should]) end FileUtils.rmtree(obj[:path]) # Now make it again creators[is].call property = obj.property(:ensure) currentvalue = property.retrieve unless property.insync?(currentvalue) assert_nothing_raised do property.sync end end FileUtils.rmtree(dir) end end end end if Process.uid == 0 # Testing #364. def test_writing_in_directories_with_no_write_access # Make a directory that our user does not have access to dir = tempfile() Dir.mkdir(dir) # Get a fake user user = nonrootuser # and group group = nonrootgroup # First try putting a file in there path = File.join(dir, "file") file = Puppet::Type.newfile :path => path, :owner => user.name, :group => group.name, :content => "testing" # Make sure we can create it assert_apply(file) assert(FileTest.exists?(path), "File did not get created") # And that it's owned correctly assert_equal(user.uid, File.stat(path).uid, "File has the wrong owner") assert_equal(group.gid, File.stat(path).gid, "File has the wrong group") assert_equal("testing", File.read(path), "file has the wrong content") # Now make a dir subpath = File.join(dir, "subdir") subdir = Puppet::Type.newfile :path => subpath, :owner => user.name, :group => group.name, :ensure => :directory # Make sure we can create it assert_apply(subdir) assert(FileTest.directory?(subpath), "File did not get created") # And that it's owned correctly assert_equal(user.uid, File.stat(subpath).uid, "File has the wrong owner") assert_equal(group.gid, File.stat(subpath).gid, "File has the wrong group") assert_equal("testing", File.read(path), "file has the wrong content") end end # #366 def test_replace_aliases file = Puppet::Type.newfile :path => tempfile() file[:replace] = :yes assert_equal(:true, file[:replace], ":replace did not alias :true to :yes") file[:replace] = :no assert_equal(:false, file[:replace], ":replace did not alias :false to :no") end # #365 -- make sure generated files also use filebuckets. def test_recursive_filebuckets source = tempfile() dest = tempfile() s1 = File.join(source, "1") sdir = File.join(source, "dir") s2 = File.join(sdir, "2") Dir.mkdir(source) Dir.mkdir(sdir) [s1, s2].each { |file| File.open(file, "w") { |f| f.puts "yay: %s" % File.basename(file) } } sums = {} [s1, s2].each do |f| sums[File.basename(f)] = Digest::MD5.hexdigest(File.read(f)) end dfiles = [File.join(dest, "1"), File.join(dest, "dir", "2")] bpath = tempfile bucket = Puppet::Type.type(:filebucket).create :name => "rtest", :path => bpath dipper = bucket.bucket dipper = Puppet::Network::Handler.filebucket.new( :Path => bpath ) assert(dipper, "did not receive bucket client") file = Puppet::Type.newfile :path => dest, :source => source, :recurse => true, :backup => "rtest" assert_apply(file) dfiles.each do |f| assert(FileTest.exists?(f), "destfile %s was not created" % f) end # Now modify the source files to make sure things get backed up correctly [s1, s2].each { |sf| File.open(sf, "w") { |f| f.puts "boo: %s" % File.basename(sf) } } assert_apply(file) dfiles.each do |f| assert_equal("boo: %s\n" % File.basename(f), File.read(f), "file was not copied correctly") end # Make sure we didn't just copy the files over to backup locations dfiles.each do |f| assert(! FileTest.exists?(f + "rtest"), "file %s was copied for backup instead of bucketed" % File.basename(f)) end # Now make sure we can get the source sums from the bucket sums.each do |f, sum| result = nil assert_nothing_raised do result = dipper.getfile(sum) end assert(result, "file %s was not backed to filebucket" % f) assert_equal("yay: %s\n" % f, result, "file backup was not correct") end end def test_backup path = tempfile() file = Puppet::Type.newfile :path => path, :content => "yay" [false, :false, "false"].each do |val| assert_nothing_raised do file[:backup] = val end assert_equal(false, file[:backup], "%s did not translate" % val.inspect) end [true, :true, "true", ".puppet-bak"].each do |val| assert_nothing_raised do file[:backup] = val end assert_equal(".puppet-bak", file[:backup], "%s did not translate" % val.inspect) end # Now try a non-bucket string assert_nothing_raised do file[:backup] = ".bak" end assert_equal(".bak", file[:backup], ".bak did not translate") # Now try a non-existent bucket assert_nothing_raised do file[:backup] = "main" end assert_equal("main", file[:backup], "bucket name was not retained") assert_equal("main", file.bucket, "file's bucket was not set") # And then an existing bucket obj = Puppet::Type.type(:filebucket).create :name => "testing" bucket = obj.bucket assert_nothing_raised do file[:backup] = "testing" end assert_equal("testing", file[:backup], "backup value was reset") assert_equal(obj.bucket, file.bucket, "file's bucket was not set") end def test_pathbuilder dir = tempfile() Dir.mkdir(dir) file = File.join(dir, "file") File.open(file, "w") { |f| f.puts "" } obj = Puppet::Type.newfile :path => dir, :recurse => true, :mode => 0755 mk_catalog obj assert_equal("/%s" % obj.ref, obj.path) list = obj.eval_generate fileobj = obj.class[file] assert(fileobj, "did not generate file object") assert_equal("/%s" % fileobj.ref, fileobj.path, "did not generate correct subfile path") end # Testing #403 def test_removal_with_content_set path = tempfile() File.open(path, "w") { |f| f.puts "yay" } file = Puppet::Type.newfile(:name => path, :ensure => :absent, :content => "foo") assert_apply(file) assert(! FileTest.exists?(path), "File was not removed") end # Testing #434 def test_stripping_extra_slashes_during_lookup file = Puppet::Type.newfile(:path => "/one/two") %w{/one/two/ /one/two /one//two //one//two//}.each do |path| assert(Puppet::Type.type(:file)[path], "could not look up file via path %s" % path) end end # Testing #438 def test_creating_properties_conflict file = tempfile() first = tempfile() second = tempfile() params = [:content, :source, :target] params.each do |param| assert_nothing_raised("%s conflicted with ensure" % [param]) do Puppet::Type.newfile(:path => file, param => first, :ensure => :file) end Puppet::Type.type(:file).clear params.each do |other| next if other == param assert_raise(Puppet::Error, "%s and %s did not conflict" % [param, other]) do Puppet::Type.newfile(:path => file, other => first, param => second) end end end end # Testing #508 if Process.uid == 0 def test_files_replace_with_right_attrs source = tempfile() File.open(source, "w") { |f| f.puts "some text" } File.chmod(0755, source) user = nonrootuser group = nonrootgroup path = tempfile() good = {:uid => user.uid, :gid => group.gid, :mode => 0640} run = Proc.new do |obj, msg| assert_apply(obj) stat = File.stat(obj[:path]) good.each do |should, sval| if should == :mode current = filemode(obj[:path]) else current = stat.send(should) end assert_equal(sval, current, "Attr %s was not correct %s" % [should, msg]) end end file = Puppet::Type.newfile(:path => path, :owner => user.name, :group => group.name, :mode => 0640, :backup => false) {:source => source, :content => "some content"}.each do |attr, value| file[attr] = value # First create the file run.call(file, "upon creation with %s" % attr) # Now change something so that we replace the file case attr when :source: File.open(source, "w") { |f| f.puts "some different text" } when :content: file[:content] = "something completely different" else raise "invalid attr %s" % attr end # Run it again run.call(file, "after modification with %s" % attr) # Now remove the file and the attr file.delete(attr) File.unlink(path) end end end # #505 def test_numeric_recurse dir = tempfile() subdir = File.join(dir, "subdir") other = File.join(subdir, "deeper") file = File.join(other, "file") [dir, subdir, other].each { |d| Dir.mkdir(d) } File.open(file, "w") { |f| f.puts "yay" } File.chmod(0644, file) obj = Puppet::Type.newfile(:path => dir, :mode => 0750, :recurse => "2") config = mk_catalog(obj) children = nil assert_nothing_raised("Failure when recursing") do children = obj.eval_generate end assert(obj.class[subdir], "did not create subdir object") children.each do |c| assert_nothing_raised("Failure when recursing on %s" % c) do c.catalog = config others = c.eval_generate end end oobj = obj.class[other] assert(oobj, "did not create other object") assert_nothing_raised do assert_nil(oobj.eval_generate, "recursed too far") end end # Make sure we default to the "puppet" filebucket, rather than a string def test_backup_defaults_to_bucket path = tempfile file = Puppet::Type.newfile(:path => path, :content => 'some content') file.finish assert_instance_of(Puppet::Network::Client::Dipper, file.bucket, "did not default to a filebucket for backups") end # #515 - make sure 'ensure' other than "link" is deleted during recursion def test_ensure_deleted_during_recursion dir = tempfile() Dir.mkdir(dir) file = File.join(dir, "file") File.open(file, "w") { |f| f.puts "asdfasdf" } obj = Puppet::Type.newfile(:path => dir, :ensure => :directory, :recurse => true) config = mk_catalog(obj) children = nil assert_nothing_raised do children = obj.eval_generate end fobj = obj.class[file] assert(fobj, "did not create file object") assert(fobj.should(:ensure) != :directory, "ensure was passed to child") end # #567 def test_missing_files_are_in_sync file = tempfile obj = Puppet::Type.newfile(:path => file, :mode => 0755) changes = obj.evaluate assert(changes.empty?, "Missing file with no ensure resulted in changes") end + + def test_root_dir_is_named_correctly + obj = Puppet::Type.newfile(:path => '/', :mode => 0755) + assert_equal("/", obj.title, "/ directory was changed to empty string") + end end