diff --git a/lib/puppet/type/file.rb b/lib/puppet/type/file.rb index 0ec54d907..f093543d7 100644 --- a/lib/puppet/type/file.rb +++ b/lib/puppet/type/file.rb @@ -1,1167 +1,1156 @@ require 'digest/md5' require 'cgi' require 'etc' require 'uri' require 'fileutils' require 'puppet/network/handler' require 'puppet/util/diff' +require 'puppet/util/checksums' module Puppet newtype(:file) do include Puppet::Util::MethodHelper + include Puppet::Util::Checksums @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(/\/+/, "/") @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 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 puts detail.backtrace if Puppet[:trace] 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 = Puppet::Type::File::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) + # Write out the file. Requires the content to be written, + # the property name for logging, and the checksum for validation. + def write(content, property, checksum = nil) + if validate = validate_checksum? + # Use the appropriate checksum type -- md5, md5lite, etc. + sumtype = property(:checksum).checktype + checksum ||= "{#{sumtype}}" + property(:checksum).send(sumtype, content) + end 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 + use_temporary_file = (content.length != 0) + path = self[:path] + path += ".puppettmp" if use_temporary_file - # Yield it - yield f + mode = self.should(:mode) # might be nil + umask = mode ? 000 : 022 - f.flush - f.close + Puppet::Util.withumask(umask) do + File.open(path, File::CREAT|File::WRONLY|File::TRUNC, mode) { |f| f.print content } end # And put our new file in place - if usetmp + if use_temporary_file # This is only not true when our file is empty. begin + fail_if_checksum_is_wrong(path, checksum) if validate File.rename(path, self[:path]) rescue => detail - self.err "Could not rename tmp %s for replacing: %s" % - [self[:path], 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 + File.unlink(path) if FileTest.exists?(path) 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 - + self.setchecksum(checksum) 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 + + # Should we validate the checksum of the file we're writing? + def validate_checksum? + if sumparam = @parameters[:checksum] + return sumparam.checktype.to_s !~ /time/ + else + return false + end end private + # Make sure the file we wrote out is what we think it is. + def fail_if_checksum_is_wrong(path, checksum) + if checksum =~ /^\{(\w+)\}.+/ + sumtype = $1 + else + # This shouldn't happen, but if it happens to, it's nicer + # to just use a default sumtype than fail. + sumtype = "md5" + end + newsum = property(:checksum).getsum(sumtype, path) + return if newsum == checksum + + self.fail "File written to disk did not match checksum; discarding changes (%s vs %s)" % [checksum, newsum] + end + # 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 ::Puppet::Type::File::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/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' end diff --git a/lib/puppet/type/file/checksum.rb b/lib/puppet/type/file/checksum.rb index 08f48ea21..debb5a7db 100755 --- a/lib/puppet/type/file/checksum.rb +++ b/lib/puppet/type/file/checksum.rb @@ -1,326 +1,271 @@ -# Keep a copy of the file checksums, and notify when they change. +require 'puppet/util/checksums' -# This state never actually modifies the system, it only notices when the system +# Keep a copy of the file checksums, and notify when they change. This +# property never actually modifies the system, it only notices when the system # changes on its own. -module Puppet - Puppet.type(:file).newproperty(:checksum) do - desc "How to check whether a file has changed. This state is used internally - for file copying, but it can also be used to monitor files somewhat - like Tripwire without managing the file contents in any way. You can - specify that a file's checksum should be monitored and then subscribe to - the file from another object and receive events to signify - checksum changes, for instance." +Puppet::Type.type(:file).newproperty(:checksum) do + include Puppet::Util::Checksums - @event = :file_changed + desc "How to check whether a file has changed. This state is used internally + for file copying, but it can also be used to monitor files somewhat + like Tripwire without managing the file contents in any way. You can + specify that a file's checksum should be monitored and then subscribe to + the file from another object and receive events to signify + checksum changes, for instance." - @unmanaged = true + @event = :file_changed - @validtypes = %w{md5 md5lite timestamp mtime time} + @unmanaged = true - def self.validtype?(type) - @validtypes.include?(type) - end + @validtypes = %w{md5 md5lite timestamp mtime time} - @validtypes.each do |ctype| - newvalue(ctype) do - handlesum() - end - end - - str = @validtypes.join("|") + def self.validtype?(type) + @validtypes.include?(type) + end - # This is here because Puppet sets this internally, using - # {md5}...... - newvalue(/^\{#{str}\}/) do + @validtypes.each do |ctype| + newvalue(ctype) do handlesum() end + end - newvalue(:nosum) do - # nothing - :nochange - end + str = @validtypes.join("|") - # If they pass us a sum type, behave normally, but if they pass - # us a sum type + sum, stick the sum in the cache. - munge do |value| - if value =~ /^\{(\w+)\}(.+)$/ - type = symbolize($1) - sum = $2 - cache(type, sum) - return type - else - if FileTest.directory?(@resource[:path]) - return :time - else - return symbolize(value) - end - end - end + # This is here because Puppet sets this internally, using + # {md5}...... + newvalue(/^\{#{str}\}/) do + handlesum() + end - # Store the checksum in the data cache, or retrieve it if only the - # sum type is provided. - def cache(type, sum = nil) - unless type - raise ArgumentError, "A type must be specified to cache a checksum" - end - type = symbolize(type) - unless state = @resource.cached(:checksums) - self.debug "Initializing checksum hash" - state = {} - @resource.cache(:checksums, state) - end + newvalue(:nosum) do + # nothing + :nochange + end - if sum - unless sum =~ /\{\w+\}/ - sum = "{%s}%s" % [type, sum] - end - state[type] = sum + # If they pass us a sum type, behave normally, but if they pass + # us a sum type + sum, stick the sum in the cache. + munge do |value| + if value =~ /^\{(\w+)\}(.+)$/ + type = symbolize($1) + sum = $2 + cache(type, sum) + return type + else + if FileTest.directory?(@resource[:path]) + return :time else - return state[type] + return symbolize(value) end end + end - # Because source and content and whomever else need to set the checksum - # and do the updating, we provide a simple mechanism for doing so. - def checksum=(value) - munge(@should) - self.updatesum(value) + # Store the checksum in the data cache, or retrieve it if only the + # sum type is provided. + def cache(type, sum = nil) + unless type + raise ArgumentError, "A type must be specified to cache a checksum" + end + type = symbolize(type) + unless state = @resource.cached(:checksums) + self.debug "Initializing checksum hash" + state = {} + @resource.cache(:checksums, state) end - def checktype - self.should || :md5 + if sum + unless sum =~ /\{\w+\}/ + sum = "{%s}%s" % [type, sum] + end + state[type] = sum + else + return state[type] end + end - # Checksums need to invert how changes are printed. - def change_to_s(currentvalue, newvalue) - begin - if currentvalue == :absent - return "defined '%s' as '%s'" % - [self.name, self.currentsum] - elsif newvalue == :absent - return "undefined %s from '%s'" % - [self.name, self.is_to_s(currentvalue)] + # Because source and content and whomever else need to set the checksum + # and do the updating, we provide a simple mechanism for doing so. + def checksum=(value) + munge(@should) + self.updatesum(value) + end + + def checktype + self.should || :md5 + end + + # Checksums need to invert how changes are printed. + def change_to_s(currentvalue, newvalue) + begin + if currentvalue == :absent + return "defined '%s' as '%s'" % + [self.name, self.currentsum] + elsif newvalue == :absent + return "undefined %s from '%s'" % + [self.name, self.is_to_s(currentvalue)] + else + if defined? @cached and @cached + return "%s changed '%s' to '%s'" % + [self.name, @cached, self.is_to_s(currentvalue)] else - if defined? @cached and @cached - return "%s changed '%s' to '%s'" % - [self.name, @cached, self.is_to_s(currentvalue)] - else - return "%s changed '%s' to '%s'" % - [self.name, self.currentsum, self.is_to_s(currentvalue)] - end + return "%s changed '%s' to '%s'" % + [self.name, self.currentsum, self.is_to_s(currentvalue)] end - rescue Puppet::Error, Puppet::DevError - raise - rescue => detail - raise Puppet::DevError, "Could not convert change %s to string: %s" % - [self.name, detail] end + rescue Puppet::Error, Puppet::DevError + raise + rescue => detail + raise Puppet::DevError, "Could not convert change %s to string: %s" % + [self.name, detail] end + end - def currentsum - #"{%s}%s" % [self.should, cache(self.should)] - cache(checktype()) - end + def currentsum + cache(checktype()) + end - # Retrieve the cached sum - def getcachedsum - hash = nil - unless hash = @resource.cached(:checksums) - hash = {} - @resource.cache(:checksums, hash) - end + # Retrieve the cached sum + def getcachedsum + hash = nil + unless hash = @resource.cached(:checksums) + hash = {} + @resource.cache(:checksums, hash) + end - sumtype = self.should + sumtype = self.should - if hash.include?(sumtype) - #self.notice "Found checksum %s for %s" % - # [hash[sumtype] ,@resource[:path]] - sum = hash[sumtype] + if hash.include?(sumtype) + #self.notice "Found checksum %s for %s" % + # [hash[sumtype] ,@resource[:path]] + sum = hash[sumtype] - unless sum =~ /^\{\w+\}/ - sum = "{%s}%s" % [sumtype, sum] - end - return sum - elsif hash.empty? - #self.notice "Could not find sum of type %s" % sumtype - return :nosum - else - #self.notice "Found checksum for %s but not of type %s" % - # [@resource[:path],sumtype] - return :nosum + unless sum =~ /^\{\w+\}/ + sum = "{%s}%s" % [sumtype, sum] end + return sum + elsif hash.empty? + #self.notice "Could not find sum of type %s" % sumtype + return :nosum + else + #self.notice "Found checksum for %s but not of type %s" % + # [@resource[:path],sumtype] + return :nosum end + end - # Calculate the sum from disk. - def getsum(checktype) - sum = "" - - checktype = checktype.intern if checktype.is_a? String - case checktype - when :md5, :md5lite: - if ! FileTest.file?(@resource[:path]) - @resource.debug "Cannot MD5 sum %s; using mtime" % - [@resource.stat.ftype] - sum = @resource.stat.mtime.to_s - else - begin - File.open(@resource[:path]) { |file| - hashfunc = Digest::MD5.new - while (!file.eof) - readBuf = file.read(512) - hashfunc.update(readBuf) - if checktype == :md5lite then - break - end - end - sum = hashfunc.hexdigest - } - rescue Errno::EACCES => detail - self.notice "Cannot checksum %s: permission denied" % - @resource[:path] - @resource.delete(self.class.name) - rescue => detail - self.notice "Cannot checksum: %s" % - detail - @resource.delete(self.class.name) - end - end - when :timestamp, :mtime: - sum = @resource.stat.mtime.to_s - #sum = File.stat(@resource[:path]).mtime.to_s - when :time: - sum = @resource.stat.ctime.to_s - #sum = File.stat(@resource[:path]).ctime.to_s - else - raise Puppet::Error, "Invalid sum type %s" % checktype - end + # Calculate the sum from disk. + def getsum(checktype, file = nil) + sum = "" + + checktype = :mtime if checktype == :timestamp + checktype = :ctime if checktype == :time + + file ||= @resource[:path] + + return nil unless FileTest.exist?(file) - return "{#{checktype}}" + sum.to_s + if ! FileTest.file?(file) + checktype = :mtime end + method = checktype.to_s + "_file" - # At this point, we don't actually modify the system, we modify - # the stored state to reflect the current state, and then kick - # off an event to mark any changes. - def handlesum - currentvalue = self.retrieve - if currentvalue.nil? - raise Puppet::Error, "Checksum state for %s is somehow nil" % - @resource.title - end + self.fail("Invalid checksum type %s" % checktype) unless respond_to?(method) - if self.insync?(currentvalue) - self.debug "Checksum is already in sync" - return nil - end - # @resource.debug "%s(%s): after refresh, is '%s'" % - # [self.class.name,@resource.name,@is] + return "{%s}%s" % [checktype, send(method, file)] + end - # If we still can't retrieve a checksum, it means that - # the file still doesn't exist - if currentvalue == :absent - # if they're copying, then we won't worry about the file - # not existing yet - unless @resource.property(:source) - self.warning("File %s does not exist -- cannot checksum" % - @resource[:path] - ) - end - return nil - end - - # If the sums are different, then return an event. - if self.updatesum(currentvalue) - return :file_changed - else - return nil - end + # At this point, we don't actually modify the system, we modify + # the stored state to reflect the current state, and then kick + # off an event to mark any changes. + def handlesum + currentvalue = self.retrieve + if currentvalue.nil? + raise Puppet::Error, "Checksum state for %s is somehow nil" % + @resource.title end - def insync?(currentvalue) - @should = [checktype()] - if cache(checktype()) - return currentvalue == currentsum() - else - # If there's no cached sum, then we don't want to generate - # an event. - return true + if self.insync?(currentvalue) + self.debug "Checksum is already in sync" + return nil + end + # If we still can't retrieve a checksum, it means that + # the file still doesn't exist + if currentvalue == :absent + # if they're copying, then we won't worry about the file + # not existing yet + unless @resource.property(:source) + self.warning("File %s does not exist -- cannot checksum" % @resource[:path]) end + return nil end - # Even though they can specify multiple checksums, the insync? - # mechanism can really only test against one, so we'll just retrieve - # the first specified sum type. - def retrieve(usecache = false) - # When the 'source' is retrieving, it passes "true" here so - # that we aren't reading the file twice in quick succession, yo. - currentvalue = currentsum() - if usecache and currentvalue - return currentvalue - end - - stat = nil - unless stat = @resource.stat - return :absent - end - - if stat.ftype == "link" and @resource[:links] != :follow - self.debug "Not checksumming symlink" - # @resource.delete(:checksum) - return currentvalue - end - - # Just use the first allowed check type - currentvalue = getsum(checktype()) + # If the sums are different, then return an event. + if self.updatesum(currentvalue) + return :file_changed + else + return nil + end + end - # If there is no sum defined, then store the current value - # into the cache, so that we're not marked as being - # out of sync. We don't want to generate an event the first - # time we get a sum. - unless cache(checktype()) - # FIXME we should support an updatechecksums-like mechanism - self.updatesum(currentvalue) - end - - # @resource.debug "checksum state is %s" % self.is + def insync?(currentvalue) + @should = [checktype()] + if cache(checktype()) + return currentvalue == currentsum() + else + # If there's no cached sum, then we don't want to generate + # an event. + return true + end + end + + # Even though they can specify multiple checksums, the insync? + # mechanism can really only test against one, so we'll just retrieve + # the first specified sum type. + def retrieve(usecache = false) + # When the 'source' is retrieving, it passes "true" here so + # that we aren't reading the file twice in quick succession, yo. + currentvalue = currentsum() + return currentvalue if usecache and currentvalue + + stat = nil + return :absent unless stat = @resource.stat + + if stat.ftype == "link" and @resource[:links] != :follow + self.debug "Not checksumming symlink" + # @resource.delete(:checksum) return currentvalue end - # Store the new sum to the state db. - def updatesum(newvalue) - result = false + # Just use the first allowed check type + currentvalue = getsum(checktype()) - if newvalue.is_a?(Symbol) - raise Puppet::Error, "%s has invalid checksum" % @resource.title - end + # If there is no sum defined, then store the current value + # into the cache, so that we're not marked as being + # out of sync. We don't want to generate an event the first + # time we get a sum. + self.updatesum(currentvalue) unless cache(checktype()) + + # @resource.debug "checksum state is %s" % self.is + return currentvalue + end - # if we're replacing, vs. updating - if sum = cache(checktype()) - # unless defined? @should - # raise Puppet::Error.new( - # ("@should is not initialized for %s, even though we " + - # "found a checksum") % @resource[:path] - # ) - # end - - if newvalue == sum - return false - end + # Store the new sum to the state db. + def updatesum(newvalue) + result = false - self.debug "Replacing %s checksum %s with %s" % - [@resource.title, sum, newvalue] - # @resource.debug "currentvalue: %s; @should: %s" % - # [newvalue,@should] - result = true - else - @resource.debug "Creating checksum %s" % newvalue - result = false - end + # if we're replacing, vs. updating + if sum = cache(checktype()) + return false if newvalue == sum - # Cache the sum so the log message can be right if possible. - @cached = sum - cache(checktype(), newvalue) - return result + self.debug "Replacing %s checksum %s with %s" % [@resource.title, sum, newvalue] + result = true + else + @resource.debug "Creating checksum %s" % newvalue + result = false end + + # Cache the sum so the log message can be right if possible. + @cached = sum + cache(checktype(), newvalue) + return result end end - diff --git a/lib/puppet/type/file/content.rb b/lib/puppet/type/file/content.rb index 6dcda0aa6..687a83f14 100755 --- a/lib/puppet/type/file/content.rb +++ b/lib/puppet/type/file/content.rb @@ -1,87 +1,92 @@ module Puppet Puppet.type(:file).newproperty(:content) do include Puppet::Util::Diff desc "Specify the contents of a file as a string. Newlines, tabs, and spaces can be specified using the escaped syntax (e.g., \\n for a newline). The primary purpose of this parameter is to provide a kind of limited templating:: define resolve(nameserver1, nameserver2, domain, search) { $str = \"search $search domain $domain nameserver $nameserver1 nameserver $nameserver2 \" file { \"/etc/resolv.conf\": content => $str } } This attribute is especially useful when used with `PuppetTemplating templating`:trac:." def change_to_s(currentvalue, newvalue) newvalue = "{md5}" + Digest::MD5.hexdigest(newvalue) if currentvalue == :absent return "created file with contents %s" % newvalue else currentvalue = "{md5}" + Digest::MD5.hexdigest(currentvalue) return "changed file contents from %s to %s" % [currentvalue, newvalue] end end # Override this method to provide diffs if asked for. # Also, fix #872: when content is used, and replace is true, the file # should be insync when it exists def insync?(is) if ! @resource.replace? and File.exists?(@resource[:path]) return true end result = super if ! result and Puppet[:show_diff] and File.exists?(@resource[:path]) string_file_diff(@resource[:path], self.should) end return result end # We should probably take advantage of existing md5 sums if they're there, # but I really don't feel like dealing with the complexity right now. def retrieve stat = nil unless stat = @resource.stat return :absent end if stat.ftype == "link" and @resource[:links] == :ignore return self.should end # Don't even try to manage the content on directories if stat.ftype == "directory" and @resource[:links] == :ignore @resource.delete(:content) return nil end begin currentvalue = File.read(@resource[:path]) return currentvalue rescue => detail raise Puppet::Error, "Could not read %s: %s" % [@resource.title, detail] end end + # Make sure we're also managing the checksum property. + def should=(value) + super + @resource.newattr(:checksum) unless @resource.property(:checksum) + end # Just write our content out to disk. def sync return_event = @resource.stat ? :file_changed : :file_created - @resource.write(:content) { |f| f.print self.should } + @resource.write(self.should, :content) return return_event end end end diff --git a/lib/puppet/type/file/ensure.rb b/lib/puppet/type/file/ensure.rb index 3aa918f65..028a7083d 100755 --- a/lib/puppet/type/file/ensure.rb +++ b/lib/puppet/type/file/ensure.rb @@ -1,179 +1,177 @@ module Puppet Puppet.type(:file).ensurable do require 'etc' desc "Whether to create files that don't currently exist. Possible values are *absent*, *present* (will match any form of file existence, and if the file is missing will create an empty file), *file*, and *directory*. Specifying ``absent`` will delete the file, although currently this will not recursively delete directories. Anything other than those values will be considered to be a symlink. For instance, the following text creates a link:: # Useful on solaris file { \"/etc/inetd.conf\": ensure => \"/etc/inet/inetd.conf\" } You can make relative links:: # Useful on solaris file { \"/etc/inetd.conf\": ensure => \"inet/inetd.conf\" } If you need to make a relative link to a file named the same as one of the valid values, you must prefix it with ``./`` or something similar. You can also make recursive symlinks, which will create a directory structure that maps to the target directory, with directories corresponding to each directory and links corresponding to each file." # Most 'ensure' properties have a default, but with files we, um, don't. nodefault newvalue(:absent) do File.unlink(@resource[:path]) end aliasvalue(:false, :absent) newvalue(:file) do # Make sure we're not managing the content some other way if property = (@resource.property(:content) || @resource.property(:source)) property.sync else - @resource.write(false) { |f| f.flush } + @resource.write("", :ensure) mode = @resource.should(:mode) end return :file_created end #aliasvalue(:present, :file) newvalue(:present) do # Make a file if they want something, but this will match almost # anything. set_file end newvalue(:directory) do mode = @resource.should(:mode) parent = File.dirname(@resource[:path]) unless FileTest.exists? parent raise Puppet::Error, "Cannot create %s; parent directory %s does not exist" % [@resource[:path], parent] end - @resource.write_if_writable(parent) do - if mode - Puppet::Util.withumask(000) do - Dir.mkdir(@resource[:path],mode) - end - else - Dir.mkdir(@resource[:path]) + if mode + Puppet::Util.withumask(000) do + Dir.mkdir(@resource[:path],mode) end + else + Dir.mkdir(@resource[:path]) end @resource.send(:property_fix) @resource.setchecksum return :directory_created end newvalue(:link) do if property = @resource.property(:target) property.retrieve return property.mklink else self.fail "Cannot create a symlink without a target" end end # Symlinks. newvalue(/./) do # This code never gets executed. We need the regex to support # specifying it, but the work is done in the 'symlink' code block. end munge do |value| value = super(value) return value if value.is_a? Symbol @resource[:target] = value return :link end def change_to_s(currentvalue, newvalue) if property = (@resource.property(:content) || @resource.property(:source)) and ! property.insync?(currentvalue) currentvalue = property.retrieve return property.change_to_s(property.retrieve, property.should) else super(currentvalue, newvalue) end end # Check that we can actually create anything def check basedir = File.dirname(@resource[:path]) if ! FileTest.exists?(basedir) raise Puppet::Error, "Can not create %s; parent directory does not exist" % @resource.title elsif ! FileTest.directory?(basedir) raise Puppet::Error, "Can not create %s; %s is not a directory" % [@resource.title, dirname] end end # We have to treat :present specially, because it works with any # type of file. def insync?(currentvalue) if property = @resource.property(:source) and ! property.described? warning "No specified sources exist" return true end if self.should == :present if currentvalue.nil? or currentvalue == :absent return false else return true end else return super(currentvalue) end end def retrieve if stat = @resource.stat(false) return stat.ftype.intern else if self.should == :false return :false else return :absent end end end def sync @resource.remove_existing(self.should) if self.should == :absent return :file_removed end event = super return event end end end diff --git a/lib/puppet/type/file/source.rb b/lib/puppet/type/file/source.rb index a3e533c31..7fa5eb1a9 100755 --- a/lib/puppet/type/file/source.rb +++ b/lib/puppet/type/file/source.rb @@ -1,279 +1,277 @@ module Puppet # Copy files from a local or remote source. This state *only* does any work # when the remote file is an actual file; in that case, this state copies # the file down. If the remote file is a dir or a link or whatever, then # this state, during retrieval, modifies the appropriate other states # so that things get taken care of appropriately. Puppet.type(:file).newproperty(:source) do include Puppet::Util::Diff attr_accessor :source, :local desc "Copy a file over the current file. Uses ``checksum`` to determine when a file should be copied. Valid values are either fully qualified paths to files, or URIs. Currently supported URI types are *puppet* and *file*. This is one of the primary mechanisms for getting content into applications that Puppet does not directly support and is very useful for those configuration files that don't change much across sytems. For instance:: class sendmail { file { \"/etc/mail/sendmail.cf\": source => \"puppet://server/module/sendmail.cf\" } } You can also leave out the server name, in which case ``puppetd`` will fill in the name of its configuration server and ``puppet`` will use the local filesystem. This makes it easy to use the same configuration in both local and centralized forms. Currently, only the ``puppet`` scheme is supported for source URL's. Puppet will connect to the file server running on ``server`` to retrieve the contents of the file. If the ``server`` part is empty, the behavior of the command-line interpreter (``puppet``) and the client demon (``puppetd``) differs slightly: ``puppet`` will look such a file up on the module path on the local host, whereas ``puppetd`` will connect to the puppet server that it received the manifest from. See the `FileServingConfiguration fileserver configuration documentation`:trac: for information on how to configure and use file services within Puppet. If you specify multiple file sources for a file, then the first source that exists will be used. This allows you to specify what amount to search paths for files:: file { \"/path/to/my/file\": source => [ \"/nfs/files/file.$host\", \"/nfs/files/file.$operatingsystem\", \"/nfs/files/file\" ] } This will use the first found file as the source. You cannot currently copy links using this mechanism; set ``links`` to ``follow`` if any remote sources are links. " uncheckable validate do |source| unless @resource.uri2obj(source) raise Puppet::Error, "Invalid source %s" % source end end munge do |source| # if source.is_a? Symbol # return source # end # Remove any trailing slashes source.sub(/\/$/, '') end def change_to_s(currentvalue, newvalue) # newvalue = "{md5}" + @stats[:checksum] if @resource.property(:ensure).retrieve == :absent return "creating from source %s with contents %s" % [@source, @stats[:checksum]] else return "replacing from source %s with contents %s" % [@source, @stats[:checksum]] end end def checksum if defined?(@stats) @stats[:checksum] else nil end end # Ask the file server to describe our file. def describe(source) sourceobj, path = @resource.uri2obj(source) server = sourceobj.server begin desc = server.describe(path, @resource[:links]) rescue Puppet::Network::XMLRPCClientError => detail self.err "Could not describe %s: %s" % [path, detail] return nil end args = {} pinparams.zip( desc.split("\t") ).each { |param, value| if value =~ /^[0-9]+$/ value = value.to_i end unless value.nil? args[param] = value end } # we can't manage ownership as root, so don't even try unless Puppet::Util::SUIDManager.uid == 0 args.delete(:owner) end if args.empty? or (args[:type] == "link" and @resource[:links] == :ignore) return nil else return args end end # Have we successfully described the remote source? def described? ! @stats.nil? and ! @stats[:type].nil? #and @is != :notdescribed end # Use the info we get from describe() to check if we're in sync. def insync?(currentvalue) unless described? warning "No specified sources exist" return true end if currentvalue == :nocopy return true end # the only thing this actual state can do is copy files around. Therefore, # only pay attention if the remote is a file. unless @stats[:type] == "file" return true end #FIXARB: Inefficient? Needed to call retrieve on parent's ensure and checksum parentensure = @resource.property(:ensure).retrieve if parentensure != :absent and ! @resource.replace? return true end # Now, we just check to see if the checksums are the same parentchecksum = @resource.property(:checksum).retrieve result = (!parentchecksum.nil? and (parentchecksum == @stats[:checksum])) # Diff the contents if they ask it. This is quite annoying -- we need to do this in # 'insync?' because they might be in noop mode, but we don't want to do the file # retrieval twice, so we cache the value. if ! result and Puppet[:show_diff] and File.exists?(@resource[:path]) and ! @stats[:_diffed] @stats[:_remote_content] = get_remote_content string_file_diff(@resource[:path], @stats[:_remote_content]) @stats[:_diffed] = true end return result end def pinparams Puppet::Network::Handler.handler(:fileserver).params end # This basically calls describe() on our file, and then sets all # of the local states appropriately. If the remote file is a normal # file then we set it to copy; if it's a directory, then we just mark # that the local directory should be created. def retrieve(remote = true) sum = nil @source = nil # This is set to false by the File#retrieve function on the second # retrieve, so that we do not do two describes. if remote # Find the first source that exists. @shouldorig contains # the sources as specified by the user. @should.each { |source| if @stats = self.describe(source) @source = source break end } end if @stats.nil? or @stats[:type].nil? return nil # :notdescribed end case @stats[:type] when "directory", "file": @resource[:ensure] = @stats[:type] unless @resource.deleting? else self.info @stats.inspect self.err "Cannot use files of type %s as sources" % @stats[:type] return :nocopy end # Take each of the stats and set them as states on the local file # if a value has not already been provided. @stats.each { |stat, value| next if stat == :checksum next if stat == :type # was the stat already specified, or should the value # be inherited from the source? @resource[stat] = value unless @resource.argument?(stat) } return @stats[:checksum] end def should @should end # Make sure we're also checking the checksum def should=(value) super checks = (pinparams + [:ensure]) checks.delete(:checksum) @resource[:check] = checks - unless @resource.property(:checksum) - @resource[:checksum] = :md5 - end + @resource[:checksum] = :md5 unless @resource.property(:checksum) end def sync contents = @stats[:_remote_content] || get_remote_content() exists = File.exists?(@resource[:path]) - @resource.write(:source) { |f| f.print contents } + @resource.write(contents, :source, @stats[:checksum]) if exists return :file_changed else return :file_created end end private def get_remote_content raise Puppet::DevError, "Got told to copy non-file %s" % @resource[:path] unless @stats[:type] == "file" sourceobj, path = @resource.uri2obj(@source) begin contents = sourceobj.server.retrieve(path, @resource[:links]) rescue => detail self.fail "Could not retrieve %s: %s" % [path, detail] end contents = CGI.unescape(contents) unless sourceobj.server.local if contents == "" self.notice "Could not retrieve contents for %s" % @source end return contents end end end diff --git a/lib/puppet/util/checksums.rb b/lib/puppet/util/checksums.rb index 598b3adfa..15d2eadd1 100644 --- a/lib/puppet/util/checksums.rb +++ b/lib/puppet/util/checksums.rb @@ -1,75 +1,75 @@ # A stand-alone module for calculating checksums # in a generic way. module Puppet::Util::Checksums # Calculate a checksum using Digest::MD5. def md5(content) require 'digest/md5' Digest::MD5.hexdigest(content) end # Calculate a checksum of the first 500 chars of the content using Digest::MD5. def md5lite(content) md5(content[0..511]) end # Calculate a checksum of a file's content using Digest::MD5. def md5_file(filename, lite = false) require 'digest/md5' digest = Digest::MD5.new() return checksum_file(digest, filename, lite) end # Calculate a checksum of the first 500 chars of a file's content using Digest::MD5. def md5lite_file(filename) md5_file(filename, true) end # Return the :mtime timestamp of a file. def mtime_file(filename) File.stat(filename).send(:mtime) end # Calculate a checksum using Digest::SHA1. def sha1(content) require 'digest/sha1' Digest::SHA1.hexdigest(content) end # Calculate a checksum of the first 500 chars of the content using Digest::SHA1. def sha1lite(content) sha1(content[0..511]) end # Calculate a checksum of a file's content using Digest::SHA1. def sha1_file(filename, lite = false) require 'digest/sha1' digest = Digest::SHA1.new() return checksum_file(digest, filename, lite) end # Calculate a checksum of the first 500 chars of a file's content using Digest::SHA1. def sha1lite_file(filename) sha1_file(filename, true) end # Return the :ctime of a file. - def timestamp_file(filename) + def ctime_file(filename) File.stat(filename).send(:ctime) end private # Perform an incremental checksum on a file. def checksum_file(digest, filename, lite = false) File.open(filename, 'r') do |file| while content = file.read(512) digest << content break if lite end end return digest.hexdigest end end diff --git a/spec/unit/ral/types/file/checksum.rb b/spec/unit/ral/types/file/checksum.rb new file mode 100755 index 000000000..3ce95362c --- /dev/null +++ b/spec/unit/ral/types/file/checksum.rb @@ -0,0 +1,119 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../../../spec_helper' + +require 'puppet/type/file' + +describe Puppet::Type::File, " when used with replace=>false and content" do + before do + @path = Tempfile.new("puppetspec") + @path.close!() + @path = @path.path + @file = Puppet::Type::File.create( { :name => @path, :content => "foo", :replace => :false } ) + end + + %w{md5 md5lite timestamp time}.each do |type| + 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 +end diff --git a/spec/unit/util/checksums.rb b/spec/unit/util/checksums.rb index 31cf24f5b..0e0d06c0d 100755 --- a/spec/unit/util/checksums.rb +++ b/spec/unit/util/checksums.rb @@ -1,99 +1,99 @@ #!/usr/bin/env ruby # # Created by Luke Kanies on 2007-9-22. # Copyright (c) 2007. All rights reserved. require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet/util/checksums' describe Puppet::Util::Checksums do before do @summer = Object.new @summer.extend(Puppet::Util::Checksums) end content_sums = [:md5, :md5lite, :sha1, :sha1lite] - file_only = [:timestamp, :mtime] + file_only = [:ctime, :mtime] content_sums.each do |sumtype| it "should be able to calculate %s sums from strings" % sumtype do @summer.should be_respond_to(sumtype) end end [content_sums, file_only].flatten.each do |sumtype| it "should be able to calculate %s sums from files" % sumtype do @summer.should be_respond_to(sumtype.to_s + "_file") end end {:md5 => Digest::MD5, :sha1 => Digest::SHA1}.each do |sum, klass| describe("when using %s" % sum) do it "should use #{klass} to calculate string checksums" do klass.expects(:hexdigest).with("mycontent").returns "whatever" @summer.send(sum, "mycontent").should == "whatever" end it "should use incremental #{klass} sums to calculate file checksums" do digest = mock 'digest' klass.expects(:new).returns digest file = "/path/to/my/file" fh = mock 'filehandle' fh.expects(:read).with(512).times(3).returns("firstline").then.returns("secondline").then.returns(nil) #fh.expects(:read).with(512).returns("secondline") #fh.expects(:read).with(512).returns(nil) File.expects(:open).with(file, "r").yields(fh) digest.expects(:<<).with "firstline" digest.expects(:<<).with "secondline" digest.expects(:hexdigest).returns :mydigest @summer.send(sum.to_s + "_file", file).should == :mydigest end end end {:md5lite => Digest::MD5, :sha1lite => Digest::SHA1}.each do |sum, klass| describe("when using %s" % sum) do it "should use #{klass} to calculate string checksums from the first 512 characters of the string" do content = "this is a test" * 100 klass.expects(:hexdigest).with(content[0..511]).returns "whatever" @summer.send(sum, content).should == "whatever" end it "should use #{klass} to calculate a sum from the first 512 characters in the file" do digest = mock 'digest' klass.expects(:new).returns digest file = "/path/to/my/file" fh = mock 'filehandle' fh.expects(:read).with(512).returns('my content') File.expects(:open).with(file, "r").yields(fh) digest.expects(:<<).with "my content" digest.expects(:hexdigest).returns :mydigest @summer.send(sum.to_s + "_file", file).should == :mydigest end end end - {:timestamp => :ctime, :mtime => :mtime}.each do |sum, method| + [:ctime, :mtime].each do |sum| describe("when using %s" % sum) do - it "should use the '#{method}' on the file to determine the timestamp" do + it "should use the '#{sum}' on the file to determine the ctime" do file = "/my/file" - stat = mock 'stat', method => "mysum" + stat = mock 'stat', sum => "mysum" File.expects(:stat).with(file).returns(stat) @summer.send(sum.to_s + "_file", file).should == "mysum" end end end end diff --git a/test/ral/types/file.rb b/test/ral/types/file.rb index aa2e63a89..c7872ccea 100755 --- a/test/ral/types/file.rb +++ b/test/ral/types/file.rb @@ -1,1826 +1,1843 @@ #!/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.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) + assert_events([:file_changed, :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", + :ensure => :file, :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(FileTest.exists?(localfile), "Local file did not get created") + assert(FileTest.exists?(purgee), "Purge target 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 + # #1010 and #1037 -- write should fail if the written checksum does not + # match the file we thought we were writing. + def test_write_validates_checksum + file = tempfile + inst = Puppet::Type.newfile(:path => file, :content => "something") + + tmpfile = file + ".puppettmp" + + wh = mock 'writehandle', :print => nil + rh = mock 'readhandle' + rh.expects(:read).with(512).times(2).returns("other").then.returns(nil) + File.expects(:open).with { |*args| args[0] == tmpfile and args[1] != "r" }.yields(wh) + File.expects(:open).with { |*args| args[0] == tmpfile and args[1] == "r" }.yields(rh) + + File.stubs(:rename) + FileTest.stubs(:exist?).returns(true) + FileTest.stubs(:file?).returns(true) + + inst.expects(:fail) + inst.write("something", :whatever) + end +end