diff --git a/lib/puppet/metatype/container.rb b/lib/puppet/metatype/container.rb index 9ed587a4c..d7c509699 100644 --- a/lib/puppet/metatype/container.rb +++ b/lib/puppet/metatype/container.rb @@ -1,96 +1,93 @@ class Puppet::Type attr_accessor :children # this is a retarded hack method to get around the difference between # component children and file children def self.depthfirst? if defined? @depthfirst return @depthfirst else return false end end def parent=(parent) if self.parentof?(parent) devfail "%s[%s] is already the parent of %s[%s]" % [self.class.name, self.title, parent.class.name, parent.title] end @parent = parent end # Add a hook for testing for recursion. def parentof?(child) if (self == child) debug "parent is equal to child" return true elsif defined? @parent and @parent.parentof?(child) debug "My parent is parent of child" return true elsif @children.include?(child) debug "child is already in children array" return true else return false end end def push(*childs) unless defined? @children @children = [] end childs.each { |child| # Make sure we don't have any loops here. if parentof?(child) devfail "Already the parent of %s[%s]" % [child.class.name, child.title] end unless child.is_a?(Puppet::Element) self.debug "Got object of type %s" % child.class self.devfail( "Containers can only contain Puppet::Elements, not %s" % child.class ) end @children.push(child) child.parent = self } end # Remove an object. The argument determines whether the object's # subscriptions get eliminated, too. def remove(rmdeps = true) # Our children remove themselves from our @children array (else the object # we called this on at the top would not be removed), so we duplicate the # array and iterate over that. If we don't do this, only half of the # objects get removed. @children.dup.each { |child| child.remove(rmdeps) } @children.clear # This is hackish (mmm, cut and paste), but it works for now, and it's # better than warnings. [@states, @parameters, @metaparams].each do |hash| hash.each do |name, obj| obj.remove end hash.clear end self.class.delete(self) - if defined? @parent and @parent - @parent.delete(self) - @parent = nil - end + @parent = nil # Remove the reference to the provider. if self.provider @provider.clear @provider = nil end end end # $Id$ diff --git a/lib/puppet/pgraph.rb b/lib/puppet/pgraph.rb index 292e25073..58bee8605 100644 --- a/lib/puppet/pgraph.rb +++ b/lib/puppet/pgraph.rb @@ -1,109 +1,112 @@ #!/usr/bin/env ruby # # Created by Luke A. Kanies on 2006-11-24. # Copyright (c) 2006. All rights reserved. require 'puppet/gratr/digraph' require 'puppet/gratr/import' require 'puppet/gratr/dot' require 'puppet/relationship' # This class subclasses a graph class in order to handle relationships # among resources. class Puppet::PGraph < GRATR::Digraph + # This is the type used for splicing. + attr_accessor :container_type + # The dependencies for a given resource. def dependencies(resource) tree_from_vertex(resource, :dfs).keys end # Override this method to use our class instead. def edge_class() Puppet::Relationship end # Determine all of the leaf nodes below a given vertex. def leaves(vertex, type = :dfs) tree = tree_from_vertex(vertex, type) leaves = tree.keys.find_all { |c| adjacent(c, :direction => :out).empty? } return leaves end # Collect all of the edges that the passed events match. Returns # an array of edges. def matching_edges(events) events.collect do |event| source = event.source unless vertex?(source) Puppet.warning "Got an event from invalid vertex %s" % source.ref next end # Get all of the edges that this vertex should forward events # to, which is the same thing as saying all edges directly below # This vertex in the graph. adjacent(source, :direction => :out, :type => :edges).find_all do |edge| edge.match?(event.event) end.each { |edge| target = edge.target if target.respond_to?(:ref) source.info "Scheduling %s of %s" % [edge.callback, target.ref] end } end.flatten end # Take container information from another graph and use it # to replace any container vertices with their respective leaves. # This creates direct relationships where there were previously # indirect relationships through the containers. def splice!(other, type) vertices.each do |vertex| # Go through each vertex and replace the edges with edges # to the leaves instead next unless vertex.is_a?(type) leaves = other.leaves(vertex) next if leaves.empty? # First create new edges for each of the :in edges adjacent(vertex, :direction => :in, :type => :edges).each do |edge| leaves.each do |leaf| add_edge!(edge.source, leaf, edge.label) if cyclic? raise ArgumentError, "%s => %s results in a loop" % [up, leaf] end end end # Then for each of the out edges adjacent(vertex, :direction => :out, :type => :edges).each do |edge| leaves.each do |leaf| add_edge!(leaf, edge.target, edge.label) if cyclic? raise ArgumentError, "%s => %s results in a loop" % [leaf, down] end end end # And finally, remove the vertex entirely. remove_vertex!(vertex) end end # For some reason, unconnected vertices do not show up in # this graph. def to_jpg(name) gv = vertices() Dir.chdir("/Users/luke/Desktop/pics") do induced_subgraph(gv).write_to_graphic_file('jpg', name) end end end # $Id$ diff --git a/lib/puppet/server/authstore.rb b/lib/puppet/server/authstore.rb index 3e7881162..b0f63b68a 100755 --- a/lib/puppet/server/authstore.rb +++ b/lib/puppet/server/authstore.rb @@ -1,226 +1,229 @@ # standard module for determining whether a given hostname or IP has access to # the requested resource require 'ipaddr' module Puppet class Server class AuthStoreError < Puppet::Error; end class AuthorizationError < Puppet::Error; end class AuthStore # This has to be an array, not a hash, else it loses its ordering. ORDER = [ [:ip, [:ip]], [:name, [:hostname, :domain]] ] Puppet::Util.logmethods(self, true) def allow(pattern) # a simple way to allow anyone at all to connect if pattern == "*" @globalallow = true else store(pattern, @allow) end end def allowed?(name, ip) if name or ip + # This is probably unnecessary, and can cause some weirdnesses in + # cases where we're operating over localhost but don't have a real + # IP defined. unless name and ip raise Puppet::DevError, "Name and IP must be passed to 'allowed?'" end # else, we're networked and such else # we're local return true end # yay insecure overrides if @globalallow return true end value = nil ORDER.each { |nametype, array| if nametype == :ip value = IPAddr.new(ip) else value = name.split(".").reverse end array.each { |type| [[@deny, false], [@allow, true]].each { |ary| hash, retval = ary if hash.include?(type) hash[type].each { |pattern| if match?(nametype, value, pattern) return retval end } end } } } self.info "defaulting to no access for %s" % name # default to false return false end def deny(pattern) store(pattern, @deny) end def initialize @globalallow = nil @allow = Hash.new { |hash, key| hash[key] = [] } @deny = Hash.new { |hash, key| hash[key] = [] } end private def match?(nametype, value, pattern) if value == pattern # simplest shortcut return true end case nametype when :ip: matchip?(value, pattern) when :name: matchname?(value, pattern) else raise Puppet::DevError, "Invalid match type %s" % nametype end end def matchip?(value, pattern) # we're just using builtin stuff for this, thankfully if pattern.include?(value) return true else return false end end def matchname?(value, pattern) # yay, horribly inefficient if pattern[-1] != '*' # the pattern has no metachars and is not equal # thus, no match #Puppet.info "%s is not equal with no * in %s" % [value, pattern] return false else # we know the last field of the pattern is '*' # if everything up to that doesn't match, we're definitely false if pattern[0..-2] != value[0..pattern.length-2] #Puppet.notice "subpatterns didn't match; %s vs %s" % # [pattern[0..-2], value[0..pattern.length-2]] return false end case value.length <=> pattern.length when -1: # value is shorter than pattern if pattern.length - value.length == 1 # only ever allowed when the value is the domain of a # splatted pattern #Puppet.info "allowing splatted domain %s" % [value] return true else return false end when 0: # value is the same length as pattern if pattern[-1] == "*" #Puppet.notice "same length with *" return true else return false end when 1: # value is longer than pattern # at this point we've already verified that everything up to # the '*' in the pattern matches, so we are true return true end end end def store(pattern, hash) type, value = type(pattern) if type and value # this won't work once we get beyond simple stuff... hash[type] << value else raise AuthStoreError, "Invalid pattern %s" % pattern end end def type(pattern) type = value = nil case pattern when /^(\d+\.){3}\d+$/: type = :ip begin value = IPAddr.new(pattern) rescue ArgumentError => detail raise AuthStoreError, "Invalid IP address pattern %s" % pattern end when /^(\d+\.){3}\d+\/(\d+)$/: mask = Integer($2) if mask < 1 or mask > 32 raise AuthStoreError, "Invalid IP mask %s" % mask end type = :ip begin value = IPAddr.new(pattern) rescue ArgumentError => detail raise AuthStoreError, "Invalid IP address pattern %s" % pattern end when /^(\d+\.){1,3}\*$/: # an ip address with a '*' at the end type = :ip match = $1 match.sub!(".", '') ary = pattern.split(".") mask = case ary.index(match) when 0: 8 when 1: 16 when 2: 24 else raise AuthStoreError, "Invalid IP pattern %s" % pattern end ary.pop while ary.length < 4 ary.push("0") end begin value = IPAddr.new(ary.join(".") + "/" + mask.to_s) rescue ArgumentError => detail raise AuthStoreError, "Invalid IP address pattern %s" % pattern end when /^[\d.]+$/: # necessary so incomplete IP addresses can't look # like hostnames raise AuthStoreError, "Invalid IP address pattern %s" % pattern when /^([a-zA-Z][-\w]*\.)+[-\w]+$/: # a full hostname type = :hostname value = pattern.split(".").reverse when /^\*(\.([a-zA-Z][-\w]*)){1,}$/: type = :domain value = pattern.split(".").reverse else raise AuthStoreError, "Invalid pattern %s" % pattern end return [type, value] end end end end # # $Id$ diff --git a/lib/puppet/server/fileserver.rb b/lib/puppet/server/fileserver.rb index 8033fac5b..53c60cdbe 100755 --- a/lib/puppet/server/fileserver.rb +++ b/lib/puppet/server/fileserver.rb @@ -1,598 +1,599 @@ require 'puppet' require 'webrick/httpstatus' require 'cgi' require 'delegate' module Puppet class FileServerError < Puppet::Error; end class Server class FileServer < Handler attr_accessor :local Puppet.setdefaults("fileserver", :fileserverconfig => ["$confdir/fileserver.conf", "Where the fileserver configuration is stored."]) CHECKPARAMS = [:mode, :type, :owner, :group, :checksum] @interface = XMLRPC::Service::Interface.new("fileserver") { |iface| iface.add_method("string describe(string, string)") iface.add_method("string list(string, string, boolean, array)") iface.add_method("string retrieve(string, string)") } # Describe a given file. This returns all of the manageable aspects # of that file. def describe(url, links = :ignore, client = nil, clientip = nil) links = links.intern if links.is_a? String if links == :manage raise Puppet::FileServerError, "Cannot currently copy links" end mount, path = convert(url, client, clientip) if client mount.debug "Describing %s for %s" % [url, client] end obj = nil unless obj = mount.check(path, links) return "" end desc = [] CHECKPARAMS.each { |check| if state = obj.state(check) unless state.is mount.debug "Manually retrieving info for %s" % check state.retrieve end desc << state.is else if check == "checksum" and obj.state(:type).is == "file" mount.notice "File %s does not have data for %s" % [obj.name, check] end desc << nil end } return desc.join("\t") end # Create a new fileserving module. def initialize(hash = {}) @mounts = {} @files = {} if hash[:Local] @local = hash[:Local] else @local = false end if hash[:Config] == false @noreadconfig = true else @config = Puppet::LoadedFile.new( hash[:Config] || Puppet[:fileserverconfig] ) @noreadconfig = false end if hash.include?(:Mount) @passedconfig = true unless hash[:Mount].is_a?(Hash) raise Puppet::DevError, "Invalid mount hash %s" % hash[:Mount].inspect end hash[:Mount].each { |dir, name| if FileTest.exists?(dir) self.mount(dir, name) end } else @passedconfig = false readconfig(false) # don't check the file the first time. end end # List a specific directory's contents. def list(url, links = :ignore, recurse = false, ignore = false, client = nil, clientip = nil) mount, path = convert(url, client, clientip) if client mount.debug "Listing %s for %s" % [url, client] end obj = nil unless FileTest.exists?(path) return "" end # We pass two paths here, but reclist internally changes one # of the arguments when called internally. desc = reclist(mount, path, path, recurse, ignore) if desc.length == 0 mount.notice "Got no information on //%s/%s" % [mount, path] return "" end desc.collect { |sub| sub.join("\t") }.join("\n") end + + def local? + self.local + end # Mount a new directory with a name. def mount(path, name) if @mounts.include?(name) if @mounts[name] != path raise FileServerError, "%s is already mounted at %s" % [@mounts[name].path, name] else # it's already mounted; no problem return end end # Let the mounts do their own error-checking. @mounts[name] = Mount.new(name, path) @mounts[name].info "Mounted %s" % path return @mounts[name] end # Retrieve a file from the local disk and pass it to the remote # client. def retrieve(url, links = :ignore, client = nil, clientip = nil) links = links.intern if links.is_a? String mount, path = convert(url, client, clientip) if client mount.info "Sending %s to %s" % [url, client] end unless FileTest.exists?(path) return "" end links = links.intern if links.is_a? String if links == :ignore and FileTest.symlink?(path) return "" end str = nil if links == :manage raise Puppet::Error, "Cannot copy links yet." else str = File.read(path) end if @local return str else return CGI.escape(str) end end def umount(name) @mounts.delete(name) if @mounts.include? name end private def authcheck(file, mount, client, clientip) + # If we're local, don't bother passing in information. + if local? + client = nil + clientip = nil + end unless mount.allowed?(client, clientip) mount.warning "%s cannot access %s" % [client, file] raise Puppet::Server::AuthorizationError, "Cannot access %s" % mount end end def convert(url, client, clientip) readconfig url = URI.unescape(url) mount, stub = splitpath(url, client) authcheck(url, mount, client, clientip) path = nil unless path = mount.subdir(stub, client) mount.notice "Could not find subdirectory %s" % "//%s/%s" % [mount, stub] return "" end return mount, path end # Deal with ignore parameters. def handleignore(children, path, ignore) ignore.each { |ignore| Dir.glob(File.join(path,ignore), File::FNM_DOTMATCH) { |match| children.delete(File.basename(match)) } } return children end # Read the configuration file. def readconfig(check = true) return if @noreadconfig if check and ! @config.changed? return end newmounts = {} begin File.open(@config.file) { |f| mount = nil count = 1 f.each { |line| case line when /^\s*#/: next # skip comments when /^\s*$/: next # skip blank lines when /\[(\w+)\]/: name = $1 if newmounts.include?(name) raise FileServerError, "%s is already mounted at %s" % [newmounts[name], name], count, @config.file end mount = Mount.new(name) newmounts[name] = mount when /^\s*(\w+)\s+(.+)$/: var = $1 value = $2 case var when "path": begin mount.path = value rescue FileServerError => detail Puppet.err "Removing mount %s: %s" % [mount.name, detail] newmounts.delete(mount.name) end when "allow": value.split(/\s*,\s*/).each { |val| begin mount.info "allowing %s access" % val mount.allow(val) rescue AuthStoreError => detail raise FileServerError.new(detail.to_s, count, @config.file) end } when "deny": value.split(/\s*,\s*/).each { |val| begin mount.info "denying %s access" % val mount.deny(val) rescue AuthStoreError => detail raise FileServerError.new(detail.to_s, count, @config.file) end } else raise FileServerError.new("Invalid argument '%s'" % var, count, @config.file) end else raise FileServerError.new("Invalid line '%s'" % line.chomp, count, @config.file) end count += 1 } } rescue Errno::EACCES => detail Puppet.err "FileServer error: Cannot read %s; cannot serve" % @config #raise Puppet::Error, "Cannot read %s" % @config rescue Errno::ENOENT => detail Puppet.err "FileServer error: '%s' does not exist; cannot serve" % @config #raise Puppet::Error, "%s does not exit" % @config #rescue FileServerError => detail # Puppet.err "FileServer error: %s" % detail end # Verify each of the mounts are valid. # We let the check raise an error, so that it can raise an error # pointing to the specific problem. newmounts.each { |name, mount| unless mount.valid? raise FileServerError, "No path specified for mount %s" % name end } @mounts = newmounts end # Recursively list the directory. FIXME This should be using # puppet objects, not directly listing. def reclist(mount, root, path, recurse, ignore) # Take out the root of the path. name = path.sub(root, '') if name == "" name = "/" end if name == path raise FileServerError, "Could not match %s in %s" % [root, path] end desc = [name] ftype = File.stat(path).ftype desc << ftype if recurse.is_a?(Integer) recurse -= 1 end ary = [desc] if recurse == true or (recurse.is_a?(Integer) and recurse > -1) if ftype == "directory" children = Dir.entries(path) if ignore children = handleignore(children, path, ignore) end children.each { |child| next if child =~ /^\.\.?$/ reclist(mount, root, File.join(path, child), recurse, ignore).each { |cobj| ary << cobj } } end end return ary.reject { |c| c.nil? } end # Split the path into the separate mount point and path. def splitpath(dir, client) # the dir is based on one of the mounts # so first retrieve the mount path mount = nil path = nil if dir =~ %r{/(\w+)/?} mount = $1 path = dir.sub(%r{/#{mount}/?}, '') unless @mounts.include?(mount) raise FileServerError, "Fileserver module '%s' not mounted" % mount end unless @mounts[mount].valid? raise FileServerError, "Fileserver error: Mount '%s' does not have a path set" % mount end # And now replace the name with the actual object. mount = @mounts[mount] else raise FileServerError, "Fileserver error: Invalid path '%s'" % dir end if path == "" path = nil else # Remove any double slashes that might have occurred path = URI.unescape(path.gsub(/\/\//, "/")) end return mount, path end def to_s "fileserver" end # A simple class for wrapping mount points. Instances of this class # don't know about the enclosing object; they're mainly just used for # authorization. class Mount < AuthStore attr_reader :name Puppet::Util.logmethods(self, true) # Run 'retrieve' on a file. This gets the actual parameters, so # we can pass them to the client. def check(dir, links) unless FileTest.exists?(dir) self.notice "File source %s does not exist" % dir return nil end obj = fileobj(dir, links) # FIXME we should really have a timeout here -- we don't # want to actually check on every connection, maybe no more # than every 60 seconds or something. It'd be nice if we # could use the builtin scheduling to do this. # Retrieval is enough here, because we don't want to cache # any information in the state file, and we don't want to generate # any state changes or anything. We don't even need to sync # the checksum, because we're always going to hit the disk # directly. obj.retrieve return obj end # Create a map for a specific client. def clientmap(client) { "h" => client.sub(/\..*$/, ""), "H" => client, "d" => client.sub(/[^.]+\./, "") # domain name } end # Replace % patterns as appropriate. def expand(path, client = nil) # This map should probably be moved into a method. map = nil if client map = clientmap(client) else Puppet.notice "No client; expanding '%s' with local host" % path # Else, use the local information map = localmap() end path.gsub(/%(.)/) do |v| key = $1 if key == "%" "%" else map[key] || v end end end # Do we have any patterns in our path, yo? def expandable? if defined? @expandable @expandable else false end end # Create out object. It must have a name. def initialize(name, path = nil) unless name =~ %r{^\w+$} raise FileServerError, "Invalid name format '%s'" % name end @name = name if path self.path = path else @path = nil end - @comp = Puppet.type(:component).create( - :name => "mount[#{name}]" - ) - #@comp.type = "mount" - #@comp.name = name - super() end def fileobj(path, links) obj = nil if obj = Puppet.type(:file)[path] # This can only happen in local fileserving, but it's an # important one. It'd be nice if we didn't just set # the check params every time, but I'm not sure it's worth # the effort. obj[:check] = CHECKPARAMS else obj = Puppet.type(:file).create( :name => path, :check => CHECKPARAMS ) - - @comp.push(obj) end if links == :manage links = :follow end # This, ah, might be completely redundant unless obj[:links] == links obj[:links] = links end return obj end # Cache this manufactured map, since if it's used it's likely # to get used a lot. def localmap unless defined? @@localmap @@localmap = { "h" => Facter.value("hostname"), "H" => [Facter.value("hostname"), Facter.value("domain")].join("."), "d" => Facter.value("domain") } end @@localmap end # Return the path as appropriate, expanding as necessary. def path(client = nil) if expandable? return expand(@path, client) else return @path end end # Set the path. def path=(path) # FIXME: For now, just don't validate paths with replacement # patterns in them. if path =~ /%./ # Mark that we're expandable. @expandable = true else unless FileTest.exists?(path) raise FileServerError, "%s does not exist" % path end unless FileTest.directory?(path) raise FileServerError, "%s is not a directory" % path end unless FileTest.readable?(path) raise FileServerError, "%s is not readable" % path end @expandable = false end @path = path end # Retrieve a specific directory relative to a mount point. # If they pass in a client, then expand as necessary. def subdir(dir = nil, client = nil) basedir = self.path(client) dirname = if dir File.join(basedir, dir.split("/").join(File::SEPARATOR)) else basedir end dirname end def to_s "mount[#{@name}]" end # Verify our configuration is valid. This should really check to # make sure at least someone will be allowed, but, eh. def valid? return false unless @path return true end end end end end # $Id$ diff --git a/lib/puppet/transaction.rb b/lib/puppet/transaction.rb index 6835b1b19..5bd5afd2f 100644 --- a/lib/puppet/transaction.rb +++ b/lib/puppet/transaction.rb @@ -1,479 +1,561 @@ # the class that actually walks our resource/state tree, collects the changes, # and performs them require 'puppet' require 'puppet/statechange' module Puppet class Transaction - attr_accessor :component, :resources, :tags, :ignoreschedules, :ignoretags - attr_accessor :relgraph + attr_accessor :component, :resources, :ignoreschedules, :ignoretags + attr_accessor :relgraph, :sorted_resources + + attr_writer :tags include Puppet::Util Puppet.config.setdefaults(:transaction, :tags => ["", "Tags to use to find resources. If this is set, then only resources tagged with the specified tags will be applied. Values must be comma-separated."] ) # Add some additional times for reporting def addtimes(hash) hash.each do |name, num| @timemetrics[name] = num end end - # Apply all changes for a child, returning a list of the events + # Apply all changes for a resource, returning a list of the events # generated. - def apply(child) - child.info "applying" + def apply(resource) # First make sure there are no failed dependencies. To do this, # we check for failures in any of the vertexes above us. It's not # enough to check the immediate dependencies, which is why we use # a tree from the reversed graph. - p @relgraph.vertices.collect { |v| v.ref } - @relgraph.reversal.tree_from_vertex(child, :dfs).keys.each do |dep| + @relgraph.reversal.tree_from_vertex(resource, :dfs).keys.each do |dep| skip = false if fails = failed?(dep) - child.notice "Dependency %s[%s] has %s failures" % + resource.notice "Dependency %s[%s] has %s failures" % [dep.class.name, dep.name, @failures[dep]] skip = true end if skip - child.warning "Skipping because of failed dependencies" + resource.warning "Skipping because of failed dependencies" @resourcemetrics[:skipped] += 1 return [] end end + + # If the resource needs to generate new objects at eval time, do it now. + eval_generate(resource) begin - changes = child.evaluate + changes = resource.evaluate rescue => detail if Puppet[:trace] puts detail.backtrace end - child.err "Failed to retrieve current state: %s" % detail + resource.err "Failed to retrieve current state: %s" % detail # Mark that it failed - @failures[child] += 1 + @failures[resource] += 1 # And then return return [] end unless changes.is_a? Array changes = [changes] end if changes.length > 0 @resourcemetrics[:out_of_sync] += 1 end - childevents = changes.collect { |change| + resourceevents = changes.collect { |change| @changes << change @count += 1 change.transaction = self events = nil begin # use an array, so that changes can return more than one # event if they want events = [change.forward].flatten.reject { |e| e.nil? } rescue => detail if Puppet[:trace] puts detail.backtrace end change.state.err "change from %s to %s failed: %s" % [change.state.is_to_s, change.state.should_to_s, detail] - @failures[child] += 1 + @failures[resource] += 1 next # FIXME this should support using onerror to determine # behaviour; or more likely, the client calling us # should do so end # Mark that our change happened, so it can be reversed # if we ever get to that point unless events.nil? or (events.is_a?(Array) and events.empty?) change.changed = true @resourcemetrics[:applied] += 1 end events }.flatten.reject { |e| e.nil? } unless changes.empty? # Record when we last synced - child.cache(:synced, Time.now) + resource.cache(:synced, Time.now) # Flush, if appropriate - if child.respond_to?(:flush) - child.flush + if resource.respond_to?(:flush) + resource.flush end end - childevents + resourceevents end # Find all of the changed resources. def changed? @changes.find_all { |change| change.changed }.collect { |change| change.state.parent }.uniq end + + # Do any necessary cleanup. Basically just removes any generated + # resources. + def cleanup + @generated.each do |resource| + resource.remove + end + end + + # See if the resource generates new resources at evaluation time. + def eval_generate(resource) + if resource.respond_to?(:eval_generate) + if children = resource.eval_generate + dependents = @relgraph.adjacent(resource, :direction => :out, :type => :edges) + targets = @relgraph.adjacent(resource, :direction => :in, :type => :edges) + children.each do |gen_child| + gen_child.info "generated" + @relgraph.add_edge!(resource, gen_child) + dependents.each do |edge| + @relgraph.add_edge!(gen_child, edge.target, edge.label) + end + targets.each do |edge| + @relgraph.add_edge!(edge.source, gen_child, edge.label) + end + @sorted_resources.insert(@sorted_resources.index(resource) + 1, gen_child) + @generated << gen_child + end + end + end + end + + # Evaluate a single resource. + def eval_resource(resource) + events = [] + + unless tagged?(resource) + resource.debug "Not tagged with %s" % tags.join(", ") + return events + end + + unless scheduled?(resource) + resource.debug "Not scheduled" + return events + end + + @resourcemetrics[:scheduled] += 1 + + # Perform the actual changes + seconds = thinmark do + events = apply(resource) + end + + # Keep track of how long we spend in each type of resource + @timemetrics[resource.class.name] += seconds + + # Check to see if there are any events for this resource + if triggedevents = trigger(resource) + events += triggedevents + end + + # Collect the targets of any subscriptions to those events + @relgraph.matching_edges(events).each do |edge| + @targets[edge.target] << edge + end + + # And return the events for collection + events + end # This method does all the actual work of running a transaction. It # collects all of the changes, executes them, and responds to any # necessary events. def evaluate @count = 0 - - # Allow the tags to be overriden - tags = self.tags || Puppet[:tags] - if tags.nil? or tags == "" - tags = nil - else - tags = [tags] unless tags.is_a? Array - tags = tags.collect do |tag| - tag.split(/\s*,\s*/) - end.flatten - end - + # Start logging. Puppet::Log.newdestination(@report) - - prefetch() - - # Now add any dynamically generated resources - generate() - - # Create a relationship graph from our resource graph - @relgraph = relationship_graph - @relgraph.to_jpg("relations") + prepare() begin - allevents = @relgraph.topsort.collect { |child| - events = [] - if (self.ignoretags or tags.nil? or child.tagged?(tags)) - if self.ignoreschedules or child.scheduled? - @resourcemetrics[:scheduled] += 1 - # Perform the actual changes - - seconds = thinmark do - events = apply(child) - end - - # Keep track of how long we spend in each type of resource - @timemetrics[child.class.name] += seconds - else - child.debug "Not scheduled" - end - else - child.debug "Not tagged with %s" % tags.join(", ") - end - - # Check to see if there are any events for this child - if triggedevents = trigger(child) - events += triggedevents - end - - # Collect the targets of any subscriptions to those events - @relgraph.matching_edges(events).each do |edge| - @targets[edge.target] << edge - end - - # And return the events for collection - events + allevents = @sorted_resources.collect { |resource| + eval_resource(resource) }.flatten.reject { |e| e.nil? } ensure # And then close the transaction log. Puppet::Log.close(@report) end + + cleanup() Puppet.debug "Finishing transaction %s with %s changes" % [self.object_id, @count] allevents end # Determine whether a given resource has failed. def failed?(obj) if @failures[obj] > 0 return @failures[obj] else return false end end # Collect any dynamically generated resources. def generate list = @resources.vertices + + # Store a list of all generated resources, so that we can clean them up + # after the transaction closes. + @generated = [] + newlist = [] while ! list.empty? list.each do |resource| if resource.respond_to?(:generate) made = resource.generate + next unless made unless made.is_a?(Array) made = [made] end + made.uniq! made.each do |res| @resources.add_vertex!(res) newlist << res + @generated << res end end end list.clear list = newlist newlist = [] end end # this should only be called by a Puppet::Type::Component resource now # and it should only receive an array def initialize(resources) @resources = resources.to_graph @resourcemetrics = { :total => @resources.vertices.length, :out_of_sync => 0, # The number of resources that had changes :applied => 0, # The number of resources fixed :skipped => 0, # The number of resources skipped :restarted => 0, # The number of resources triggered :failed_restarts => 0, # The number of resources that fail a trigger :scheduled => 0 # The number of resources scheduled } # Metrics for distributing times across the different types. @timemetrics = Hash.new(0) # The number of resources that were triggered in this run @triggered = Hash.new { |hash, key| hash[key] = Hash.new(0) } # Targets of being triggered. @targets = Hash.new do |hash, key| hash[key] = [] end # The changes we're performing @changes = [] # The resources that have failed and the number of failures each. This # is used for skipping resources because of failed dependencies. @failures = Hash.new do |h, key| h[key] = 0 end @report = Report.new end # Prefetch any providers that support it. We don't support prefetching # types, just providers. def prefetch @resources.collect { |obj| if pro = obj.provider pro.class else nil end }.reject { |o| o.nil? }.uniq.each do |klass| # XXX We need to do something special here in case of failure. if klass.respond_to?(:prefetch) klass.prefetch end end end + # Prepare to evaluate the elements in a transaction. + def prepare + prefetch() + + # Now add any dynamically generated resources + generate() + + # Create a relationship graph from our resource graph + @relgraph = relationship_graph + + @sorted_resources = @relgraph.topsort + end + # Create a graph of all of the relationships in our resource graph. def relationship_graph graph = Puppet::PGraph.new # First create the dependency graph @resources.vertices.each do |vertex| graph.add_vertex!(vertex) vertex.builddepends.each do |edge| graph.add_edge!(edge) end end # Then splice in the container information graph.splice!(@resources, Puppet::Type::Component) # Lastly, add in any autorequires graph.vertices.each do |vertex| vertex.autorequire.each do |edge| unless graph.edge?(edge) graph.add_edge!(edge) end end end return graph end # Generate a transaction report. def report @resourcemetrics[:failed] = @failures.find_all do |name, num| num > 0 end.length # Get the total time spent @timemetrics[:total] = @timemetrics.inject(0) do |total, vals| total += vals[1] total end # Unfortunately, RRD does not deal well with changing lists of values, # so we have to pick a list of values and stick with it. In this case, # that means we record the total time, the config time, and that's about # it. We should probably send each type's time as a separate metric. @timemetrics.dup.each do |name, value| if Puppet::Type.type(name) @timemetrics.delete(name) end end # Add all of the metrics related to resource count and status @report.newmetric(:resources, @resourcemetrics) # Record the relative time spent in each resource. @report.newmetric(:time, @timemetrics) # Then all of the change-related metrics @report.newmetric(:changes, :total => @changes.length ) @report.time = Time.now return @report end # Roll all completed changes back. def rollback @targets.clear @triggered.clear allevents = @changes.reverse.collect { |change| # skip changes that were never actually run unless change.changed Puppet.debug "%s was not changed" % change.to_s next end begin events = change.backward rescue => detail Puppet.err("%s rollback failed: %s" % [change,detail]) if Puppet[:trace] puts detail.backtrace end next # at this point, we would normally do error handling # but i haven't decided what to do for that yet # so just record that a sync failed for a given resource #@@failures[change.state.parent] += 1 # this still could get hairy; what if file contents changed, # but a chmod failed? how would i handle that error? dern end @relgraph.matching_edges(events).each do |edge| @targets[edge.target] << edge end # Now check to see if there are any events for this child. # Kind of hackish, since going backwards goes a change at a # time, not a child at a time. trigger(change.state.parent) # And return the events for collection events }.flatten.reject { |e| e.nil? } end + + # Is the resource currently scheduled? + def scheduled?(resource) + self.ignoreschedules or resource.scheduled? + end + + # The tags we should be checking. + def tags + # Allow the tags to be overridden + unless defined? @tags + @tags = Puppet[:tags] + end + + unless defined? @processed_tags + if @tags.nil? or @tags == "" + @tags = [] + else + @tags = [@tags] unless @tags.is_a? Array + @tags = @tags.collect do |tag| + tag.split(/\s*,\s*/) + end.flatten + end + @processed_tags = true + end + + @tags + end + + # Is this resource tagged appropriately? + def tagged?(resource) + self.ignoretags or tags.empty? or resource.tagged?(tags) + end + + # Are there any edges that target this resource? + def targeted?(resource) + @targets[resource] + end # Trigger any subscriptions to a child. This does an upwardly recursive # search -- it triggers the passed resource, but also the resource's parent # and so on up the tree. def trigger(child) obj = child callbacks = Hash.new { |hash, key| hash[key] = [] } sources = Hash.new { |hash, key| hash[key] = [] } trigged = [] while obj if @targets.include?(obj) callbacks.clear sources.clear @targets[obj].each do |edge| # Some edges don't have callbacks next unless edge.callback # Collect all of the subs for each callback callbacks[edge.callback] << edge # And collect the sources for logging sources[edge.source] << edge.callback end sources.each do |source, callbacklist| obj.debug "%s[%s] results in triggering %s" % [source.class.name, source.name, callbacklist.join(", ")] end callbacks.each do |callback, subs| message = "Triggering '%s' from %s dependencies" % [callback, subs.length] obj.notice message # At this point, just log failures, don't try to react # to them in any way. begin obj.send(callback) @resourcemetrics[:restarted] += 1 rescue => detail obj.err "Failed to call %s on %s: %s" % [callback, obj, detail] @resourcemetrics[:failed_restarts] += 1 if Puppet[:trace] puts detail.backtrace end end # And then add an event for it. trigged << Puppet::Event.new( :event => :triggered, :transaction => self, :source => obj, :message => message ) triggered(obj, callback) end end obj = obj.parent end if trigged.empty? return nil else return trigged end end def triggered(resource, method) @triggered[resource][method] += 1 end def triggered?(resource, method) @triggered[resource][method] end end end require 'puppet/transaction/report' # $Id$ diff --git a/lib/puppet/type/pfile.rb b/lib/puppet/type/pfile.rb index 15367a0c2..b3bbba3b9 100644 --- a/lib/puppet/type/pfile.rb +++ b/lib/puppet/type/pfile.rb @@ -1,1043 +1,1033 @@ require 'digest/md5' require 'cgi' require 'etc' require 'uri' require 'fileutils' require 'puppet/type/state' require 'puppet/server/fileserver' module Puppet newtype(:file) do @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`` element will be used less and less to manage content, and instead native elements will be used to do so. If you find that you are often copying files in from a central location, rather than using native elements, please contact Reductive Labs and we can hopefully work with you to develop a native element 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. If a filebucket is specified, files will be backed up there; else, they will be backed up in the same directory with a ``.puppet-bak`` extension,, and no backups will be made if backup is ``false``. To use filebuckets, you must first create a filebucket 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 only benefits to doing so 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. " attr_reader :bucket defaultto ".puppet-bak" munge do |value| case value when false, "false", :false: false when true, "true", ".puppet-bak", :true: ".puppet-bak" 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. @bucket = value value else self.fail "Invalid backup type %s" % value.inspect end end # Provide a straight-through hook for setting the bucket. def bucket=(bucket) @value = bucket @bucket = bucket end end newparam(:linkmaker) do desc "An internal parameter used by the *symlink* type to do recursive link creation." end newparam(:recurse) do desc "Whether and how deeply to do recursive management." newvalues(:true, :false, :inf, /^[0-9]+$/) munge do |value| newval = super(value) case newval when :true, :inf: true when :false: false else newval end end end newparam(:replace) 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) defaultto :true end newparam(:force) 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) 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." defaultto :false newvalues(:true, :false) end # Autorequire any parent directories. autorequire(:file) do - cur = [] - pary = self[:path].split(File::SEPARATOR) - pary.shift # remove the initial nil - pary.pop # remove us - - pary.inject([""]) do |ary, dir| - ary << dir - cur << ary.join(File::SEPARATOR) - ary - end - - cur + File.dirname(self[:path]) end # Autorequire the owner and group of the file. {:user => :owner, :group => :group}.each do |type, state| autorequire(type) do if @states.include?(state) # The user/group states automatically converts to IDs next unless should = @states[state].shouldorig val = should[0] if val.is_a?(Integer) or val =~ /^\d+$/ nil else val end end end end validate do if self[:content] and self[:source] self.fail "You cannot specify both content and a source" end end # List files, but only one level deep. def self.list(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::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' state 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 @parameters.include?(:backup) and bucket = @parameters[:backup].bucket case bucket when String: if obj = @@filebuckets[bucket] # This sets the @value on :backup, too @parameters[:backup].bucket = obj elsif obj = Puppet.type(:filebucket).bucket(bucket) @@filebuckets[bucket] = obj @parameters[:backup].bucket = obj else self.fail "Could not find filebucket %s" % bucket end when Puppet::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[:backup] case backup when Puppet::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[:backup] case backup when Puppet::Client::Dipper: sum = backup.backup(file) self.info "Filebucketed to %s with sum %s" % [backup.name, sum] return true when String: newfile = file + backup if FileTest.exists?(newfile) remove_backup(newfile) end begin # FIXME Shouldn't this just use a Puppet object with # 'source' specified? bfile = file + backup # Ruby 1.8.1 requires the 'preserve' addition, but # later versions do not appear to require it. FileUtils.cp(file, bfile, :preserve => true) return true rescue => detail # since they said they want a backup, let's error out # if we couldn't make one self.fail "Could not back %s up: %s" % [file, detail.message] end else self.err "Invalid backup type %s" % backup.inspect return false end when "link": return true else self.notice "Cannot backup files of type %s" % File.stat(file).ftype return false end end def handleignore(children) return children unless self[:ignore] self[:ignore].each { |ignore| ignored = [] Dir.glob(File.join(self[:path],ignore), File::FNM_DOTMATCH) { |match| ignored.push(File.basename(match)) } children = children - ignored } return children end def initialize(hash) # Store a copy of the arguments for later. tmphash = hash.to_hash # Used for caching clients @clients = {} super # Get rid of any duplicate slashes, and remove any trailing slashes. @title = @title.gsub(/\/+/, "/").sub(/\/$/, "") # 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) - if @arghash.include?(:source) - @arghash.delete(:source) + [:source, :parent].each do |param| + if @arghash.include?(param) + @arghash.delete(param) + end end - if @arghash.include?(:parent) - @arghash.delete(:parent) + if @arghash[:target] + warning "%s vs %s" % [@arghash[:ensure], @arghash[:target]] end @stat = nil end # Build a recursive map of a link source def linkrecurse(recurse) target = @states[:target].should method = :lstat if self[:links] == :follow method = :stat end targetstat = nil unless FileTest.exist?(target) - #self.info "%s does not exist; not recursing" % - # target return end # Now stat our target targetstat = File.send(method, target) unless targetstat.ftype == "directory" - #self.info "%s is not a directory; not recursing" % - # target 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 + # Get rid of ignored children if @parameters.include?(:ignore) children = handleignore(children) - end + 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) - unless @children.include?(child) - self.push child - added.push file - end + 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 + end added = [] children.each { |file| file = File.basename(file) next if file =~ /^\.\.?$/ # skip . and .. options = {:recurse => recurse} if child = self.newchild(file, true, options) # Mark any unmanaged files for removal if purge is set. # Use the array rather than [] because tidy uses this method, too. - if @parameters.include?(:purge) and self[:purge] == :true and child.implicit? + if @parameters.include?(:purge) and self.purge? + info "purging %s" % child.ref child[:ensure] = :absent + else + child[:require] = self end - - unless @children.include?(child) - self.push child - added.push file - end + added << child end } + + added end # Create a new file or directory object as a child to the current # object. def newchild(path, local, hash = {}) # make local copy of arguments args = @arghash.dup 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 klass = nil # We specifically look in @parameters here, because 'linkmaker' isn't # a valid attribute for subclasses, so using 'self[:linkmaker]' throws # an error. if @parameters.include?(:linkmaker) and args.include?(:source) and ! FileTest.directory?(args[:source]) klass = Puppet.type(:symlink) # clean up the args a lot for links old = args.dup args = { :ensure => old[:source], :path => path } else klass = self.class end - + # 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 = klass[path] - unless @children.include?(child) + 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 child = klass.implicitcreate(args) # implicit creation can return nil if child.nil? return nil end - @children << child rescue Puppet::Error => detail self.notice( "Cannot manage: %s" % [detail.message] ) self.debug args.inspect child = nil rescue => detail self.notice( "Cannot manage: %s" % [detail] ) self.debug args.inspect child = nil end end 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 if defined? @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 ppath = @parent.path.sub(/\/?file=.+/, '') tmp = [] if ppath != "/" and ppath != "" tmp << ppath end tmp << self.class.name.to_s + "=" + self.name return tmp else return super end else # The top-level name is always puppet[top], so we don't # bother with that. And we don't add the hostname # here, it gets added in the log server thingy. if self.name == "puppet[top]" return ["/"] else # We assume that if we don't have a parent that we # should not cache the path return [self.class.name.to_s + "=" + self.name] end 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'. + # and maybe 'sourcerecurse', returning the collection of generated + # files. def recurse + # are we at the end of the recursion? + unless self.recurse? + return + end + recurse = self[:recurse] # we might have a string, rather than a number if recurse.is_a?(String) if recurse =~ /^[0-9]+$/ recurse = Integer(recurse) - #elsif recurse =~ /^inf/ # infinite recursion else # anything else is infinite recursion recurse = true end end - # are we at the end of the recursion? - #if recurse == 0 - unless self.recurse? - return - end - if recurse.is_a?(Integer) recurse -= 1 end - - self.localrecurse(recurse) - if @states.include? :target - self.linkrecurse(recurse) + + children = [] + + # We want to do link-recursing before normal recursion so that all + # of the target stuff gets copied over correctly. + if @states.include? :target and ret = self.linkrecurse(recurse) + children += ret end - if @states.include?(:source) - self.sourcerecurse(recurse) + if ret = self.localrecurse(recurse) + children += ret end + if @states.include?(:source) and ret = self.sourcerecurse(recurse) + children += ret + end + + children end + # A simple method for determining whether we should be recursing. def recurse? return false unless @parameters.include?(:recurse) val = @parameters[:recurse].value if val and (val == true or val > 0) return true else return false end end # Remove the old backup. def remove_backup(newfile) if self.class.name == :file and self[:links] != :follow method = :lstat else method = :stat end old = File.send(method, newfile).ftype if old == "directory" raise Puppet::Error, "Will not remove directory backup %s; use a filebucket" % newfile end info "Removing old backup of type %s" % File.send(method, newfile).ftype begin File.unlink(newfile) rescue => detail if Puppet[:trace] puts detail.backtrace end self.err "Could not remove old backup: %s" % detail return false end end # Remove any existing data. This is only used when dealing with # links or directories. def remove_existing(should) return unless s = stat(true) unless handlebackup self.fail "Could not back up; will not replace" end unless should.to_s == "link" return if s.ftype.to_s == should.to_s end case s.ftype when "directory": if self[:force] == :true debug "Removing existing directory for replacement with %s" % should FileUtils.rmtree(self[:path]) else notice "Not replacing 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 - if @states.include?(:source) - # This probably isn't the best place for it, but we need - # to make sure that we have a corresponding checksum state. - unless @states.include?(:checksum) - self[:checksum] = "md5" - end - - # We have to retrieve the source info before the recursion happens, - # although I'm not exactly clear on why. - @states[:source].retrieve - end - - if @parameters.include?(:recurse) - self.recurse - end - unless stat = self.stat(true) self.debug "File does not exist" @states.each { |name,state| # We've already retrieved the source, and we don't # want to overwrite whatever it did. This is a bit # of a hack, but oh well, source is definitely special. - next if name == :source + # next if name == :source state.is = :absent } + + # If the file doesn't exist but we have a source, then call + # retrieve on that state + if @states.include?(:source) + @states[:source].retrieve + end return end states().each { |state| - # We don't want to call 'describe()' twice, so only do a local - # retrieve on the source. - if state.name == :source - state.retrieve(false) - else - state.retrieve - end + state.retrieve } end # This recurses against the remote source and makes sure the local - # and remote structures match. It's run after 'localrecurse'. + # 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) - # FIXME sourcerecurse should support purging non-remote files - source = @states[:source].source - - unless ! source.nil? and source !~ /^\s*$/ - self.notice "source %s does not exist" % @states[:source].should - return nil - end - - sourceobj, path = uri2obj(source) - # we'll set this manually as necessary if @arghash.include?(:ensure) @arghash.delete(:ensure) end - - # 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 - sum = "md5" - if state = self.state(:checksum) - sum = state.should - end + r = false if recurse unless recurse == 0 r = 1 end end - + ignore = self[:ignore] - desc = server.list(path, self[:links], r, ignore) + @states[: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 - # Now create a new child for every file returned in the list. - desc.split("\n").each { |line| - file, type = line.split("\t") - next if file == "/" # skip the listing object - name = file.sub(/^\//, '') - args = {:source => source + file} - if type == file - args[:recurse] = nil + 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. + return desc.split("\n").collect { |line| + file, type = line.split("\t") + next if file == "/" # skip the listing object + name = file.sub(/^\//, '') + args = {:source => source + file} + if type == file + args[:recurse] = nil + end - self.newchild(name, false, args) - } + self.newchild(name, false, args) + }.reject {|c| c.nil? }.each do |f| f.info "sourced" end + end + return [] end # Set the checksum, from another state. There are multiple states 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 @states.include? :checksum if sum @states[:checksum].checksum = sum else # If they didn't pass in a sum, then tell checksum to # figure it out. @states[:checksum].retrieve @states[:checksum].checksum = @states[:checksum].is end end end # Stat our file. Depending on the value of the 'links' attribute, we use # either 'stat' or 'lstat', and we expect the states 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 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 def uri2obj(source) sourceobj = FileSource.new path = nil unless source devfail "Got a nil source" end if source =~ /^\// source = "file://localhost/%s" % URI.escape(source) sourceobj.mount = "localhost" sourceobj.local = true end begin uri = URI.parse(URI.escape(source)) rescue => detail self.fail "Could not understand source %s: %s" % [source, detail.to_s] end case uri.scheme when "file": unless defined? @@localfileserver @@localfileserver = Puppet::Server::FileServer.new( :Local => true, :Mount => { "/" => "localhost" }, :Config => false ) #@@localfileserver.mount("/", "localhost") end sourceobj.server = @@localfileserver path = "/localhost" + uri.path when "puppet": args = { :Server => uri.host } if uri.port args[:Port] = uri.port end # FIXME We should cache a copy of this server #sourceobj.server = Puppet::NetworkClient.new(args) unless @clients.include?(source) @clients[source] = Puppet::Client::FileClient.new(args) 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 recursive file proto %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(usetmp = true) mode = self.should(:mode) remove_existing(:file) # The temporary file path = nil if usetmp path = self[:path] + ".puppettmp" else path = self[:path] end # As the correct user and group Puppet::SUIDManager.asuser(asuser(), self.should(:group)) do f = nil # Open our file with the correct modes if mode Puppet::Util.withumask(000) do f = File.open(path, File::CREAT|File::WRONLY|File::TRUNC, mode) end else f = File.open(path, File::CREAT|File::WRONLY|File::TRUNC) end # Yield it yield f f.flush f.close end # And put our new file in place if usetmp begin File.rename(path, self[:path]) rescue => detail self.err "Could not rename tmp %s for replacing: %s" % [self[:path], detail] ensure # Make sure the created file gets removed if FileTest.exists?(path) File.unlink(path) end end end # And then update our checksum, so the next run doesn't find it. # FIXME This is extra work, because it's going to read the whole # file back in again. self.setchecksum end end # Puppet.type(:pfile) # the filesource class can't include the path, because the path # changes for every file instance class FileSource attr_accessor :mount, :root, :server, :local end # We put all of the states 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 state list. require 'puppet/type/pfile/checksum' require 'puppet/type/pfile/content' # can create the file require 'puppet/type/pfile/source' # can create the file require 'puppet/type/pfile/target' require 'puppet/type/pfile/ensure' # can create the file require 'puppet/type/pfile/uid' require 'puppet/type/pfile/group' require 'puppet/type/pfile/mode' require 'puppet/type/pfile/type' end # $Id$ diff --git a/lib/puppet/type/pfile/ensure.rb b/lib/puppet/type/pfile/ensure.rb index 6f7b15d49..c998e0f7f 100755 --- a/lib/puppet/type/pfile/ensure.rb +++ b/lib/puppet/type/pfile/ensure.rb @@ -1,180 +1,181 @@ module Puppet Puppet.type(:file).ensurable do require 'etc' desc "Whether to create files that don't currently exist. Possible values are *absent*, *present* (equivalent to ``exists`` in most file tests -- 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' states have a default, but with files we, um, don't. nodefault newvalue(:absent) do File.unlink(@parent[:path]) end aliasvalue(:false, :absent) newvalue(:file) do # Make sure we're not managing the content some other way if state = @parent.state(:content) or state = @parent.state(:source) state.sync else @parent.write(false) { |f| f.flush } mode = @parent.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 + p @is mode = @parent.should(:mode) parent = File.dirname(@parent[:path]) unless FileTest.exists? parent raise Puppet::Error, "Cannot create %s; parent directory %s does not exist" % [@parent[:path], parent] end Puppet::SUIDManager.asuser(@parent.asuser()) { if mode Puppet::Util.withumask(000) do Dir.mkdir(@parent[:path],mode) end else Dir.mkdir(@parent[:path]) end } @parent.setchecksum return :directory_created end newvalue(:link) do if state = @parent.state(:target) state.retrieve if state.linkmaker self.set_directory return :directory_created else return state.mklink end 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 @parent[:target] = value return :link end # Check that we can actually create anything def check basedir = File.dirname(@parent[:path]) if ! FileTest.exists?(basedir) raise Puppet::Error, "Can not create %s; parent directory does not exist" % @parent.title elsif ! FileTest.directory?(basedir) raise Puppet::Error, "Can not create %s; %s is not a directory" % [@parent.title, dirname] end end # We have to treat :present specially, because it works with any # type of file. def insync? if self.should == :present if @is.nil? or @is == :absent return false else return true end else return super end end def retrieve if stat = @parent.stat(false) @is = stat.ftype.intern else if self.should == :false @is = :false else @is = :absent end end end def sync unless self.should == :absent @parent.remove_existing(self.should) end event = super # There are some cases where all of the work does not get done on # file creation, so we have to do some extra checking. @parent.each do |thing| next unless thing.is_a? Puppet::State next if thing == self thing.retrieve unless thing.insync? thing.sync end end return event end end end # $Id$ diff --git a/lib/puppet/type/pfile/source.rb b/lib/puppet/type/pfile/source.rb index 62d3dbbc4..8ac60422c 100755 --- a/lib/puppet/type/pfile/source.rb +++ b/lib/puppet/type/pfile/source.rb @@ -1,290 +1,253 @@ require 'puppet/server/fileserver' module Puppet - # Copy files from a local or remote source. + # 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).newstate(:source) do PINPARAMS = Puppet::Server::FileServer::CHECKPARAMS 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\" } } See the [fileserver docs][] 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. [fileserver docs]: ../installing/fsconfigref.html " uncheckable + + validate do |source| + unless @parent.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 checksum + if defined?(@stats) + @stats[:checksum] + else + nil + end + end # Ask the file server to describe our file. def describe(source) sourceobj, path = @parent.uri2obj(source) server = sourceobj.server begin desc = server.describe(path, @parent[:links]) rescue NetworkClientError => 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::SUIDManager.uid == 0 args.delete(:owner) end - if args.empty? + if args.empty? or (args[:type] == "link" and @parent[: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? + unless described? + info "No specified sources exist" + return true + end + + if @is == :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 + # Now, we just check to see if the checksums are the same + return @parent.is(:checksum) == @stats[:checksum] + 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 - - unless defined? @shouldorig - raise Puppet::DevError, "No sources defined for %s" % - @parent.title - end - - @source = nil unless defined? @source + @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 - @source = nil # Find the first source that exists. @shouldorig contains # the sources as specified by the user. - @shouldorig.each { |source| + @should.each { |source| if @stats = self.describe(source) @source = source break end } end if @stats.nil? or @stats[:type].nil? @is = :notdescribed - @source = nil return nil end - - # If we're a normal file, then set things up to copy the file down. + case @stats[:type] - when "file": - if sum = @parent.state(:checksum) - if sum.is - if sum.is == :absent - sum.retrieve(true) - end - @is = sum.is - else - @is = :absent - end - else - self.info "File does not have checksum" - @is = :absent - end - # if replace => false then fake the checksum so that the file - # is not overwritten. - unless @is == :absent - if @parent[:replace] == :false - info "Not replacing existing file" - @is = @stats[:checksum] - end - end - @should = [@stats[:checksum]] - # If we're a directory, then do not copy anything, and instead just - # create the directory using the 'create' state. - when "directory": - if state = @parent.state(:ensure) - unless state.should == "directory" - state.should = "directory" - end - else - @parent[:ensure] = "directory" - @parent.state(:ensure).retrieve - end - # we'll let the :ensure state do our work - @should.clear - @is = true - when "link": - case @parent[:links] - when :ignore - @is = :nocopy - @should = [:nocopy] - self.info "Ignoring link %s" % @source - return - when :follow - @stats = self.describe(source, :follow) - if @stats.empty? - raise Puppet::Error, "Could not follow link %s" % @source - end - when :copy - raise Puppet::Error, "Cannot copy links yet" - end + when "directory", "file": + @parent[:ensure] = @stats[:type] else self.info @stats.inspect self.err "Cannot use files of type %s as sources" % @stats[:type] - @should = [:nocopy] @is = :nocopy + return 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? unless @parent.argument?(stat) - if state = @parent.state(stat) - state.should = value - else - @parent[stat] = value - end - #else - # @parent.info "Already specified %s" % stat + @parent[stat] = value end } + + @is = @stats[:checksum] end - - # The special thing here is that we need to make sure that 'should' - # is only set for files, not directories. The processing we're doing - # here doesn't really matter, because the @should values will be - # overridden when we 'retrieve'. - munge do |source| - if source.is_a? Symbol - return source - end - - # Remove any trailing slashes - source.sub!(/\/$/, '') - unless @parent.uri2obj(source) - raise Puppet::Error, "Invalid source %s" % source + + def should + @should + end + + # Make sure we're also checking the checksum + def should=(value) + super + + # @parent[:check] = [:checksum, :ensure] + unless @parent.state(:checksum) + @parent[:checksum] = :md5 end - - if ! defined? @stats or @stats.nil? - # stupid hack for now; it'll get overriden - return source - else - if @stats[:type] == "directory" - @is = true - return nil - else - return source - end + + unless @parent.state(:ensure) + @parent[:check] = :ensure end end def sync - if @is == :notdescribed - self.retrieve # try again - if @is == :notdescribed - @parent.log "Could not retrieve information on %s" % - @parent.title - return nil - end - if @is == @should - return nil - end - end - - case @stats[:type] - when "link": - end unless @stats[:type] == "file" #if @stats[:type] == "directory" #[@parent.name, @is.inspect, @should.inspect] #end raise Puppet::DevError, "Got told to copy non-file %s" % @parent[:path] end - unless defined? @source - raise Puppet::DevError, "Somehow source is still undefined" - end - sourceobj, path = @parent.uri2obj(@source) begin contents = sourceobj.server.retrieve(path, @parent[:links]) rescue NetworkClientError => detail self.err "Could not retrieve %s: %s" % [path, detail] return nil end # FIXME It's stupid that this isn't taken care of in the # protocol. unless sourceobj.server.local contents = CGI.unescape(contents) end if contents == "" self.notice "Could not retrieve contents for %s" % @source end exists = File.exists?(@parent[:path]) @parent.write { |f| f.print contents } if exists return :file_changed else return :file_created end end end end # $Id$ diff --git a/lib/puppet/type/pfile/target.rb b/lib/puppet/type/pfile/target.rb index 8c22b10b9..17d9bebbc 100644 --- a/lib/puppet/type/pfile/target.rb +++ b/lib/puppet/type/pfile/target.rb @@ -1,83 +1,81 @@ module Puppet Puppet.type(:file).newstate(:target) do attr_accessor :linkmaker desc "The target for creating a link. Currently, symlinks are the only type supported." newvalue(:notlink) do # We do nothing if the value is absent return :nochange end # Anything else, basically newvalue(/./) do if ! @parent.should(:ensure) @parent[:ensure] = :link elsif @parent.should(:ensure) != :link raise Puppet::Error, "You cannot specify a target unless 'ensure' is set to 'link'" end if @parent.state(:ensure).insync? mklink() end end # Create our link. def mklink target = self.should # Clean up any existing objects. @parent.remove_existing(target) Dir.chdir(File.dirname(@parent[:path])) do Puppet::SUIDManager.asuser(@parent.asuser()) do mode = @parent.should(:mode) if mode Puppet::Util.withumask(000) do File.symlink(target, @parent[:path]) end else File.symlink(target, @parent[:path]) end end :link_created end end def retrieve if @parent.state(:ensure).should == :directory @is = self.should @linkmaker = true else if stat = @parent.stat # If we're just checking the value if (should = self.should) and (should != :notlink) and File.exists?(should) and (tstat = File.lstat(should)) and (tstat.ftype == "directory") and @parent.recurse? - warning "Changing ensure to directory; recurse is %s but %s" % - [@parent[:recurse].inspect, @parent.recurse?] @parent[:ensure] = :directory @is = should @linkmaker = true else if stat.ftype == "link" @is = File.readlink(@parent[:path]) @linkmaker = false else @is = :notlink end end else @is = :absent end end end end end # $Id$ diff --git a/lib/puppet/type/state.rb b/lib/puppet/type/state.rb index 9b96b909f..6dd5f1567 100644 --- a/lib/puppet/type/state.rb +++ b/lib/puppet/type/state.rb @@ -1,545 +1,545 @@ # The virtual base class for states, which are the self-contained building # blocks for actually doing work on the system. require 'puppet' require 'puppet/element' require 'puppet/statechange' require 'puppet/parameter' module Puppet class State < Puppet::Parameter attr_accessor :is # Because 'should' uses an array, we have a special method for handling # it. We also want to keep copies of the original values, so that # they can be retrieved and compared later when merging. attr_reader :shouldorig class << self attr_accessor :unmanaged attr_reader :name def checkable @checkable = true end def uncheckable @checkable = false end def checkable? if defined? @checkable return @checkable else return true end end end # Look up a value's name, so we can find options and such. def self.value_name(value) name = symbolize(value) if @parametervalues[name] return name elsif ary = self.match?(value) return ary[0] else return nil end end # Retrieve an option set when a value was defined. def self.value_option(name, option) if option.is_a?(String) option = symbolize(option) end if hash = @parameteroptions[name] hash[option] else nil end end # Create the value management variables. def self.initvars @parametervalues = {} @aliasvalues = {} @parameterregexes = {} @parameteroptions = {} end # Define a new valid value for a state. You must provide the value itself, # usually as a symbol, or a regex to match the value. # # The first argument to the method is either the value itself or a regex. # The second argument is an option hash; valid options are: # * :event: The event that should be returned when this value is set. # * :call: When to call any associated block. The default value # is ``instead``, which means to call the value instead of calling the # provider. You can also specify ``before`` or ``after``, which will # call both the block and the provider, according to the order you specify # (the ``first`` refers to when the block is called, not the provider). def self.newvalue(name, options = {}, &block) name = name.intern if name.is_a? String @parameteroptions[name] = {} paramopts = @parameteroptions[name] # Symbolize everything options.each do |opt, val| paramopts[symbolize(opt)] = symbolize(val) end # By default, call the block instead of the provider. if block_given? paramopts[:call] ||= :instead else paramopts[:call] ||= :none end # If there was no block given, we still want to store the information # for validation, but we won't be defining a method block ||= true case name when Symbol if @parametervalues.include?(name) Puppet.warning "%s reassigning value %s" % [self.name, name] end @parametervalues[name] = block if block_given? method = "set_" + name.to_s settor = paramopts[:settor] || (self.name.to_s + "=") define_method(method, &block) paramopts[:method] = method end when Regexp # The regexes are handled in parameter.rb. This value is used # for validation. @parameterregexes[name] = block # This is used for looking up the block for execution. if block_given? paramopts[:block] = block end else raise ArgumentError, "Invalid value %s of type %s" % [name, name.class] end end # Call the provider method. def call_provider(value) begin provider.send(self.class.name.to_s + "=", value) rescue NoMethodError self.fail "The %s provider can not handle attribute %s" % [provider.class.name, self.class.name] end end # Call the dynamically-created method associated with our value, if # there is one. def call_valuemethod(name, value) event = nil if method = self.class.value_option(name, :method) and self.respond_to?(method) self.debug "setting %s (currently %s)" % [value, self.is] begin event = self.send(method) rescue Puppet::Error raise rescue => detail if Puppet[:trace] puts detail.backtrace end error = Puppet::Error.new("Could not set %s on %s: %s" % [value, self.class.name, detail], @parent.line, @parent.file) error.set_backtrace detail.backtrace raise error end elsif block = self.class.value_option(name, :block) # FIXME It'd be better here to define a method, so that # the blocks could return values. # If the regex was defined with no associated block, then just pass # through and the correct event will be passed back. event = self.instance_eval(&block) end return event, name end # How should a state change be printed as a string? def change_to_s begin if @is == :absent return "defined '%s' as '%s'" % [self.name, self.should_to_s] elsif self.should == :absent or self.should == [:absent] return "undefined %s from '%s'" % [self.name, self.is_to_s] else return "%s changed '%s' to '%s'" % [self.name, self.is_to_s, self.should_to_s] 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 # Figure out which event to return. def event(name, event = nil) if value_event = self.class.value_option(name, :event) return value_event else if event and event.is_a?(Symbol) if event == :nochange return nil else return event end else event = case self.should when :present: (@parent.class.name.to_s + "_created").intern when :absent: (@parent.class.name.to_s + "_removed").intern else (@parent.class.name.to_s + "_changed").intern end end end return event end # initialize our state def initialize(hash) super() @is = nil unless hash.include?(:parent) self.devfail "State %s was not passed a parent" % self end @parent = hash[:parent] if hash.include?(:should) self.should = hash[:should] end if hash.include?(:is) self.is = hash[:is] end end def inspect str = "State('%s', " % self.name if self.is str += "@is = '%s', " % [self.is] else str += "@is = nil, " end if defined? @should and @should str += "@should = '%s')" % @should.join(", ") else str += "@should = nil)" end end # Determine whether the state is in-sync or not. If @should is # not defined or is set to a non-true value, then we do not have # a valid value for it and thus consider the state to be in-sync # since we cannot fix it. Otherwise, we expect our should value # to be an array, and if @is matches any of those values, then # we consider it to be in-sync. def insync? #debug "%s value is '%s', should be '%s'" % # [self,self.is.inspect,self.should.inspect] unless defined? @should and @should return true end unless @should.is_a?(Array) self.devfail "%s's should is not array" % self.class.name end # an empty array is analogous to no should values if @should.empty? return true end # Look for a matching value @should.each { |val| if @is == val or @is == val.to_s return true end } # otherwise, return false return false end # because the @should and @is vars might be in weird formats, # we need to set up a mechanism for pretty printing of the values # default to just the values, but this way individual states can # override these methods def is_to_s @is end # Send a log message. def log(msg) unless @parent[:loglevel] self.devfail "Parent %s has no loglevel" % @parent.name end Puppet::Log.create( :level => @parent[:loglevel], :message => msg, :source => self ) end # each state class must define the name() method, and state instances # do not change that name # this implicitly means that a given object can only have one state # instance of a given state class def name return self.class.name end # for testing whether we should actually do anything def noop unless defined? @noop @noop = false end tmp = @noop || self.parent.noop || Puppet[:noop] || false #debug "noop is %s" % tmp return tmp end # return the full path to us, for logging and rollback; not currently # used def pathbuilder if defined? @parent and @parent return [@parent.path, self.name] else return [self.name] end end # Retrieve the parent's provider. Some types don't have providers, in which # case we return the parent object itself. def provider @parent.provider || @parent end # By default, call the method associated with the state name on our # provider. In other words, if the state name is 'gid', we'll call # 'provider.gid' to retrieve the current value. def retrieve @is = provider.send(self.class.name) end # Set our value, using the provider, an associated block, or both. def set(value) # Set a name for looking up associated options like the event. name = self.class.value_name(value) call = self.class.value_option(name, :call) # If we're supposed to call the block first or instead, call it now if call == :before or call == :instead event, tmp = call_valuemethod(name, value) end unless call == :instead if @parent.provider call_provider(value) else # They haven't provided a block, and our parent does not have # a provider, so we have no idea how to handle this. self.fail "%s cannot handle values of type %s" % [self.class.name, value.inspect] end end if call == :after event, tmp = call_valuemethod(name, value) end return event(name, event) end # Only return the first value def should if defined? @should unless @should.is_a?(Array) self.devfail "should for %s on %s is not an array" % [self.class.name, @parent.name] end return @should[0] else return nil end end # Set the should value. def should=(values) unless values.is_a?(Array) values = [values] end @shouldorig = values if self.respond_to?(:validate) values.each { |val| validate(val) } end if self.respond_to?(:munge) @should = values.collect { |val| self.munge(val) } else @should = values end end def should_to_s if defined? @should @should.join(" ") else return nil end end # The default 'sync' method only selects among a list of registered # values. def sync if self.insync? self.info "already in sync" return nil end unless self.class.values self.devfail "No values defined for %s" % self.class.name end if value = self.should set(value) else self.devfail "Got a nil value for should" end end # The states need to return tags so that logs correctly collect them. def tags unless defined? @tags @tags = [] # This might not be true in testing if @parent.respond_to? :tags @tags = @parent.tags end @tags << self.name end @tags end def to_s return "%s(%s)" % [@parent.name,self.name] end # This state will get automatically added to any type that responds # to the methods 'exists?', 'create', and 'destroy'. class Ensure < Puppet::State @name = :ensure def self.defaultvalues newvalue(:present) do if @parent.provider and @parent.provider.respond_to?(:create) @parent.provider.create else @parent.create end end newvalue(:absent) do if @parent.provider and @parent.provider.respond_to?(:destroy) @parent.provider.destroy else @parent.destroy end end defaultto do if @parent.managed? :present else nil end end # This doc will probably get overridden @doc ||= "The basic state that the object should be in." end def self.inherited(sub) # Add in the two states that everyone will have. sub.class_eval do end end def change_to_s begin - if @is == :absent + if @is == :absent or @is.nil? return "created" elsif self.should == :absent return "removed" else return "%s changed '%s' to '%s'" % [self.name, self.is_to_s, self.should_to_s] 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 retrieve # XXX This is a problem -- whether the object exists or not often # depends on the results of other states, yet we're the first state # to get checked, which means that those other states do not have # @is values set. This seems to be the source of quite a few bugs, # although they're mostly logging bugs, not functional ones. if prov = @parent.provider and prov.respond_to?(:exists?) result = prov.exists? elsif @parent.respond_to?(:exists?) result = @parent.exists? else raise Puppet::DevError, "No ability to determine if %s exists" % @parent.class.name end if result @is = :present else @is = :absent end end # If they're talking about the thing at all, they generally want to # say it should exist. #defaultto :present defaultto do if @parent.managed? :present else nil end end end end end # $Id$ diff --git a/test/other/transactions.rb b/test/other/transactions.rb index c143b3a0c..7342b57ec 100755 --- a/test/other/transactions.rb +++ b/test/other/transactions.rb @@ -1,577 +1,767 @@ #!/usr/bin/env ruby $:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'puppet' require 'puppettest' require 'puppettest/support/resources' # $Id$ class TestTransactions < Test::Unit::TestCase include PuppetTest::FileTesting include PuppetTest::Support::Resources + + def mkgenerator(&block) + # Create a bogus type that generates new instances with shorter + type = Puppet::Type.newtype(:generator) do + newparam(:name, :namevar => true) + end + if block + type.class_eval(&block) + end + cleanup do + Puppet::Type.rmtype(:generator) + end + + return type + end def test_reports path1 = tempfile() path2 = tempfile() objects = [] objects << Puppet::Type.newfile( :path => path1, :content => "yayness" ) objects << Puppet::Type.newfile( :path => path2, :content => "booness" ) trans = assert_events([:file_created, :file_created], *objects) report = nil assert_nothing_raised { report = trans.report } # First test the report logs assert(report.logs.length > 0, "Did not get any report logs") report.logs.each do |obj| assert_instance_of(Puppet::Log, obj) end # Then test the metrics metrics = report.metrics assert(metrics, "Did not get any metrics") assert(metrics.length > 0, "Did not get any metrics") assert(metrics.has_key?("resources"), "Did not get object metrics") assert(metrics.has_key?("changes"), "Did not get change metrics") metrics.each do |name, metric| assert_instance_of(Puppet::Metric, metric) end end def test_prefetch # Create a type just for testing prefetch name = :prefetchtesting $prefetched = false type = Puppet::Type.newtype(name) do newparam(:name) {} end cleanup do Puppet::Type.rmtype(name) end # Now create a provider type.provide(:prefetch) do def self.prefetch $prefetched = true end end # Now create an instance inst = type.create :name => "yay" # Create a transaction trans = Puppet::Transaction.new(newcomp(inst)) # Make sure prefetch works assert_nothing_raised do trans.prefetch end assert_equal(true, $prefetched, "type prefetch was not called") # Now make sure it gets called from within evaluate() $prefetched = false assert_nothing_raised do trans.evaluate end assert_equal(true, $prefetched, "evaluate did not call prefetch") end def test_refreshes_generate_events path = tempfile() firstpath = tempfile() secondpath = tempfile() file = Puppet::Type.newfile(:title => "file", :path => path, :content => "yayness") first = Puppet::Type.newexec(:title => "first", :command => "/bin/echo first > #{firstpath}", :subscribe => [:file, path], :refreshonly => true ) second = Puppet::Type.newexec(:title => "second", :command => "/bin/echo second > #{secondpath}", :subscribe => [:exec, "first"], :refreshonly => true ) assert_apply(file, first, second) assert(FileTest.exists?(secondpath), "Refresh did not generate an event") end unless %x{groups}.chomp.split(/ /).length > 1 $stderr.puts "You must be a member of more than one group to test transactions" else def ingroup(gid) require 'etc' begin group = Etc.getgrgid(gid) rescue => detail puts "Could not retrieve info for group %s: %s" % [gid, detail] return nil end return @groups.include?(group.name) end def setup super @groups = %x{groups}.chomp.split(/ /) unless @groups.length > 1 p @groups raise "You must be a member of more than one group to test this" end end def newfile(hash = {}) tmpfile = tempfile() File.open(tmpfile, "w") { |f| f.puts rand(100) } # XXX now, because os x apparently somehow allows me to make a file # owned by a group i'm not a member of, i have to verify that # the file i just created is owned by one of my groups # grrr unless ingroup(File.stat(tmpfile).gid) Puppet.info "Somehow created file in non-member group %s; fixing" % File.stat(tmpfile).gid require 'etc' firstgr = @groups[0] unless firstgr.is_a?(Integer) str = Etc.getgrnam(firstgr) firstgr = str.gid end File.chown(nil, firstgr, tmpfile) end hash[:name] = tmpfile assert_nothing_raised() { return Puppet.type(:file).create(hash) } end def newservice assert_nothing_raised() { return Puppet.type(:service).create( :name => "sleeper", :type => "init", :path => exampledir("root/etc/init.d"), :hasstatus => true, :check => [:ensure] ) } end def newexec(file) assert_nothing_raised() { return Puppet.type(:exec).create( :name => "touch %s" % file, :path => "/bin:/usr/bin:/sbin:/usr/sbin", :returns => 0 ) } end # modify a file and then roll the modifications back def test_filerollback transaction = nil file = newfile() states = {} check = [:group,:mode] file[:check] = check assert_nothing_raised() { file.retrieve } assert_nothing_raised() { check.each { |state| assert(file[state]) states[state] = file[state] } } component = newcomp("file",file) require 'etc' groupname = Etc.getgrgid(File.stat(file.name).gid).name assert_nothing_raised() { # Find a group that it's not set to group = @groups.find { |group| group != groupname } unless group raise "Could not find suitable group" end file[:group] = group file[:mode] = "755" } trans = assert_events([:file_changed, :file_changed], component) file.retrieve assert_rollback_events(trans, [:file_changed, :file_changed], "file") assert_nothing_raised() { file.retrieve } states.each { |state,value| assert_equal( value,file.is(state), "File %s remained %s" % [state, file.is(state)] ) } end # start a service, and then roll the modification back # Disabled, because it wasn't really worth the effort. def disabled_test_servicetrans transaction = nil service = newservice() component = newcomp("service",service) assert_nothing_raised() { service[:ensure] = 1 } service.retrieve assert(service.insync?, "Service did not start") system("ps -ef | grep ruby") trans = assert_events([:service_started], component) service.retrieve assert_rollback_events(trans, [:service_stopped], "service") end # test that services are correctly restarted and that work is done # in the right order def test_refreshing transaction = nil file = newfile() execfile = File.join(tmpdir(), "exectestingness") exec = newexec(execfile) states = {} check = [:group,:mode] file[:check] = check file[:group] = @groups[0] assert_apply(file) @@tmpfiles << execfile component = newcomp("both",file,exec) # 'subscribe' expects an array of arrays exec[:subscribe] = [[file.class.name,file.name]] exec[:refreshonly] = true assert_nothing_raised() { file.retrieve exec.retrieve } check.each { |state| states[state] = file[state] } assert_nothing_raised() { file[:mode] = "755" } trans = assert_events([:file_changed, :triggered], component) assert(FileTest.exists?(execfile), "Execfile does not exist") File.unlink(execfile) assert_nothing_raised() { file[:group] = @groups[1] } trans = assert_events([:file_changed, :triggered], component) assert(FileTest.exists?(execfile), "Execfile does not exist") end # Verify that one component requiring another causes the contained # resources in the requiring component to get refreshed. def test_refresh_across_two_components transaction = nil file = newfile() execfile = File.join(tmpdir(), "exectestingness2") @@tmpfiles << execfile exec = newexec(execfile) states = {} check = [:group,:mode] file[:check] = check file[:group] = @groups[0] assert_apply(file) fcomp = newcomp("file",file) ecomp = newcomp("exec",exec) component = newcomp("both",fcomp,ecomp) # 'subscribe' expects an array of arrays #component[:require] = [[file.class.name,file.name]] ecomp[:subscribe] = fcomp exec[:refreshonly] = true trans = assert_events([], component) assert_nothing_raised() { file[:group] = @groups[1] file[:mode] = "755" } trans = assert_events([:file_changed, :file_changed, :triggered], component) end # Make sure that multiple subscriptions get triggered. def test_multisubs path = tempfile() file1 = tempfile() file2 = tempfile() file = Puppet.type(:file).create( :path => path, :ensure => "file" ) exec1 = Puppet.type(:exec).create( :path => ENV["PATH"], :command => "touch %s" % file1, :refreshonly => true, :subscribe => [:file, path] ) exec2 = Puppet.type(:exec).create( :path => ENV["PATH"], :command => "touch %s" % file2, :refreshonly => true, :subscribe => [:file, path] ) assert_apply(file, exec1, exec2) assert(FileTest.exists?(file1), "File 1 did not get created") assert(FileTest.exists?(file2), "File 2 did not get created") end # Make sure that a failed trigger doesn't result in other events not # getting triggered. def test_failedrefreshes path = tempfile() newfile = tempfile() file = Puppet.type(:file).create( :path => path, :ensure => "file" ) svc = Puppet.type(:service).create( :name => "thisservicedoesnotexist", :subscribe => [:file, path] ) exec = Puppet.type(:exec).create( :path => ENV["PATH"], :command => "touch %s" % newfile, :logoutput => true, :refreshonly => true, :subscribe => [:file, path] ) assert_apply(file, svc, exec) assert(FileTest.exists?(path), "File did not get created") assert(FileTest.exists?(newfile), "Refresh file did not get created") end # Make sure that unscheduled and untagged objects still respond to events def test_unscheduled_and_untagged_response Puppet::Type.type(:schedule).mkdefaultschedules Puppet[:ignoreschedules] = false file = Puppet.type(:file).create( :name => tempfile(), :ensure => "file" ) fname = tempfile() exec = Puppet.type(:exec).create( :name => "touch %s" % fname, :path => "/usr/bin:/bin", :schedule => "monthly", :subscribe => ["file", file.name] ) comp = newcomp(file,exec) comp.finalize # Run it once assert_apply(comp) assert(FileTest.exists?(fname), "File did not get created") assert(!exec.scheduled?, "Exec is somehow scheduled") # Now remove it, so it can get created again File.unlink(fname) file[:content] = "some content" assert_events([:file_changed, :triggered], comp) assert(FileTest.exists?(fname), "File did not get recreated") # Now remove it, so it can get created again File.unlink(fname) # And tag our exec exec.tag("testrun") # And our file, so it runs file.tag("norun") Puppet[:tags] = "norun" file[:content] = "totally different content" assert(! file.insync?, "Uh, file is in sync?") assert_events([:file_changed, :triggered], comp) assert(FileTest.exists?(fname), "File did not get recreated") end def test_failed_reqs_mean_no_run exec = Puppet::Type.type(:exec).create( :command => "/bin/mkdir /this/path/cannot/possibly/exit", :title => "mkdir" ) file1 = Puppet::Type.type(:file).create( :title => "file1", :path => tempfile(), :require => exec, :ensure => :file ) file2 = Puppet::Type.type(:file).create( :title => "file2", :path => tempfile(), :require => file1, :ensure => :file ) comp = newcomp(exec, file1, file2) comp.finalize assert_apply(comp) assert(! FileTest.exists?(file1[:path]), "File got created even tho its dependency failed") assert(! FileTest.exists?(file2[:path]), "File got created even tho its deep dependency failed") end end def f(n) Puppet::Type.type(:file)["/tmp/#{n.to_s}"] end def test_relationship_graph one, two, middle, top = mktree {one => two, "f" => "c", "h" => middle}.each do |source, target| if source.is_a?(String) source = f(source) end if target.is_a?(String) target = f(target) end target[:require] = source end trans = Puppet::Transaction.new(top) graph = nil assert_nothing_raised do graph = trans.relationship_graph end assert_instance_of(Puppet::PGraph, graph, "Did not get relationship graph") # Make sure all of the components are gone comps = graph.vertices.find_all { |v| v.is_a?(Puppet::Type::Component)} assert(comps.empty?, "Deps graph still contains components") # It must be reversed because of how topsort works sorted = graph.topsort.reverse # Now make sure the appropriate edges are there and are in the right order assert(graph.dependencies(f(:f)).include?(f(:c)), "c not marked a dep of f") assert(sorted.index(f(:c)) < sorted.index(f(:f)), "c is not before f") one.each do |o| two.each do |t| assert(graph.dependencies(o).include?(t), "%s not marked a dep of %s" % [t.ref, o.ref]) assert(sorted.index(t) < sorted.index(o), "%s is not before %s" % [t.ref, o.ref]) end end trans.resources.leaves(middle).each do |child| assert(graph.dependencies(f(:h)).include?(child), "%s not marked a dep of h" % [child.ref]) assert(sorted.index(child) < sorted.index(f(:h)), "%s is not before h" % child.ref) end # Lastly, make sure our 'g' vertex made it into the relationship # graph, since it's not involved in any relationships. assert(graph.vertex?(f(:g)), "Lost vertexes with no relations") graph.to_jpg("normal_relations") end + # Test pre-evaluation generation def test_generate - # Create a bogus type that generates new instances with shorter - Puppet::Type.newtype(:generator) do - newparam(:name, :namevar => true) - + mkgenerator() do def generate ret = [] if title.length > 1 ret << self.class.create(:title => title[0..-2]) + else + return nil end ret end end - cleanup do - Puppet::Type.rmtype(:generator) - end yay = Puppet::Type.newgenerator :title => "yay" rah = Puppet::Type.newgenerator :title => "rah" comp = newcomp(yay, rah) trans = comp.evaluate assert_nothing_raised do trans.generate end %w{ya ra y r}.each do |name| assert(trans.resources.vertex?(Puppet::Type.type(:generator)[name]), "Generated %s was not a vertex" % name) end + + # Now make sure that cleanup gets rid of those generated types. + assert_nothing_raised do + trans.cleanup + end + + %w{ya ra y r}.each do |name| + assert(!trans.resources.vertex?(Puppet::Type.type(:generator)[name]), + "Generated vertex %s was not removed from graph" % name) + assert_nil(Puppet::Type.type(:generator)[name], + "Generated vertex %s was not removed from class" % name) + end + end + + # Test mid-evaluation generation. + def test_eval_generate + $evaluated = {} + type = mkgenerator() do + def eval_generate + ret = [] + if title.length > 1 + ret << self.class.create(:title => title[0..-2]) + else + return nil + end + ret + end + + def evaluate + $evaluated[self.title] = true + return [] + end + end + + yay = Puppet::Type.newgenerator :title => "yay" + rah = Puppet::Type.newgenerator :title => "rah", :subscribe => yay + comp = newcomp(yay, rah) + trans = comp.evaluate + + trans.prepare + + # Now apply the resources, and make sure they appropriately generate + # things. + assert_nothing_raised("failed to apply yay") do + trans.apply(yay) + end + ya = type["ya"] + assert(ya, "Did not generate ya") + assert(trans.relgraph.vertex?(ya), + "Did not add ya to rel_graph") + + # Now make sure the appropriate relationships were added + assert(trans.relgraph.edge?(yay, ya), + "parent was not required by child") + assert(trans.relgraph.edge?(ya, rah), + "rah was not subscribed to ya") + + # And make sure the relationship is a subscription with a callback, + # not just a require. + assert_equal({:callback => :refresh, :event => :ALL_EVENTS}, + trans.relgraph[Puppet::Relationship.new(ya, rah)], + "The label was not retained") + + # Now make sure it in turn eval_generates appropriately + assert_nothing_raised("failed to apply yay") do + trans.apply(type["ya"]) + end + + %w{y}.each do |name| + res = type[name] + assert(res, "Did not generate %s" % name) + assert(trans.relgraph.vertex?(res), + "Did not add %s to rel_graph" % name) + end + + assert_nothing_raised("failed to eval_generate with nil response") do + trans.apply(type["y"]) + end + + assert_equal(%w{yay ya y rah}, trans.sorted_resources.collect { |r| r.title }, + "Did not eval_generate correctly") + + assert_nothing_raised("failed to apply rah") do + trans.apply(rah) + end + + ra = type["ra"] + assert(ra, "Did not generate ra") + assert(trans.relgraph.vertex?(ra), + "Did not add ra to rel_graph" % name) + + # Now make sure this generated resource has the same relationships as the generating + # resource + assert(trans.relgraph.edge?(yay, ra), + "yay is not required by ra") + assert(trans.relgraph.edge?(ya, ra), + "ra is not subscribed to ya") + + # And make sure the relationship is a subscription with a callback, + # not just a require. + assert_equal({:callback => :refresh, :event => :ALL_EVENTS}, + trans.relgraph[Puppet::Relationship.new(ya, ra)], + "The label was not retained") + + # Now make sure that cleanup gets rid of those generated types. + assert_nothing_raised do + trans.cleanup + end + + %w{ya ra y r}.each do |name| + assert(!trans.relgraph.vertex?(type[name]), + "Generated vertex %s was not removed from graph" % name) + assert_nil(type[name], + "Generated vertex %s was not removed from class" % name) + end + + # Now, start over and make sure that everything gets evaluated. + trans = comp.evaluate + assert_nothing_raised do + trans.evaluate + end + + assert_equal(%w{yay ya y rah ra r}.sort, $evaluated.keys.sort, + "Not all resources were evaluated") + end + + def test_tags + res = Puppet::Type.newfile :path => tempfile() + comp = newcomp(res) + + # Make sure they default to none + assert_equal([], comp.evaluate.tags) + + # Make sure we get the main tags + Puppet[:tags] = %w{this is some tags} + assert_equal(%w{this is some tags}, comp.evaluate.tags) + + # And make sure they get processed correctly + Puppet[:tags] = ["one", "two,three", "four"] + assert_equal(%w{one two three four}, comp.evaluate.tags) + + # lastly, make sure we can override them + trans = comp.evaluate + trans.tags = ["one", "two,three", "four"] + assert_equal(%w{one two three four}, comp.evaluate.tags) + end + + def test_tagged? + res = Puppet::Type.newfile :path => tempfile() + comp = newcomp(res) + trans = comp.evaluate + + assert(trans.tagged?(res), "tagged? defaulted to false") + + # Now set some tags + trans.tags = %w{some tags} + + # And make sure it's false + assert(! trans.tagged?(res), "matched invalid tags") + + # Set ignoretags and make sure it sticks + trans.ignoretags = true + assert(trans.tagged?(res), "tags were not ignored") + + # Now make sure we actually correctly match tags + res[:tag] = "mytag" + trans.ignoretags = false + trans.tags = %w{notag} + + assert(! trans.tagged?(res), "tags incorrectly matched") + + trans.tags = %w{mytag yaytag} + assert(trans.tagged?(res), "tags should have matched") + + end + + # Make sure events propagate down the relationship graph appropriately. + def test_trigger end end # $Id$ \ No newline at end of file diff --git a/test/server/fileserver.rb b/test/server/fileserver.rb index 00d235cb2..5e9c60ddc 100755 --- a/test/server/fileserver.rb +++ b/test/server/fileserver.rb @@ -1,1024 +1,1025 @@ #!/usr/bin/env ruby $:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'puppet' require 'puppet/server/fileserver' require 'puppettest' class TestFileServer < Test::Unit::TestCase include PuppetTest def mkmount(path = nil) mount = nil name = "yaytest" base = path || tempfile() unless FileTest.exists?(base) Dir.mkdir(base) end # Create a test file File.open(File.join(base, "file"), "w") { |f| f.puts "bazoo" } assert_nothing_raised { mount = Puppet::Server::FileServer::Mount.new(name, base) } return mount end # make a simple file source def mktestdir testdir = File.join(tmpdir(), "remotefilecopytesting") @@tmpfiles << testdir # create a tmpfile pattern = "tmpfile" tmpfile = File.join(testdir, pattern) assert_nothing_raised { Dir.mkdir(testdir) File.open(tmpfile, "w") { |f| 3.times { f.puts rand(100) } } } return [testdir, %r{#{pattern}}, tmpfile] end # make a bunch of random test files def mktestfiles(testdir) @@tmpfiles << testdir assert_nothing_raised { files = %w{a b c d e}.collect { |l| name = File.join(testdir, "file%s" % l) File.open(name, "w") { |f| f.puts rand(100) } name } return files } end def assert_describe(base, file, server) file = File.basename(file) assert_nothing_raised { desc = server.describe(base + file) assert(desc, "Got no description for %s" % file) assert(desc != "", "Got no description for %s" % file) assert_match(/^\d+/, desc, "Got invalid description %s" % desc) } end # test for invalid names def test_namefailures server = nil assert_nothing_raised { server = Puppet::Server::FileServer.new( :Local => true, :Config => false ) } assert_raise(Puppet::FileServerError) { server.mount("/tmp", "invalid+name") } assert_raise(Puppet::FileServerError) { server.mount("/tmp", "invalid-name") } assert_raise(Puppet::FileServerError) { server.mount("/tmp", "invalid name") } assert_raise(Puppet::FileServerError) { server.mount("/tmp", "") } end # verify that listing the root behaves as expected def test_listroot server = nil testdir, pattern, tmpfile = mktestdir() file = nil checks = Puppet::Server::FileServer::CHECKPARAMS # and make our fileserver assert_nothing_raised { server = Puppet::Server::FileServer.new( :Local => true, :Config => false ) } # mount the testdir assert_nothing_raised { server.mount(testdir, "test") } # and verify different iterations of 'root' return the same value list = nil assert_nothing_raised { list = server.list("/test/", :ignore, true, false) } assert(list =~ pattern) assert_nothing_raised { list = server.list("/test", :ignore, true, false) } assert(list =~ pattern) end # test listing individual files def test_getfilelist server = nil testdir, pattern, tmpfile = mktestdir() file = nil assert_nothing_raised { server = Puppet::Server::FileServer.new( :Local => true, :Config => false ) } assert_nothing_raised { server.mount(testdir, "test") } # get our listing list = nil sfile = "/test/tmpfile" assert_nothing_raised { list = server.list(sfile, :ignore, true, false) } assert_nothing_raised { file = Puppet.type(:file)[tmpfile] } output = "/\tfile" # verify it got listed as a file assert_equal(output, list) # verify we got all fields assert(list !~ /\t\t/) # verify that we didn't get the directory itself list.split("\n").each { |line| assert(line !~ %r{remotefile}) } # and then verify that the contents match contents = File.read(tmpfile) ret = nil assert_nothing_raised { ret = server.retrieve(sfile) } assert_equal(contents, ret) end # check that the fileserver is seeing newly created files def test_seenewfiles server = nil testdir, pattern, tmpfile = mktestdir() newfile = File.join(testdir, "newfile") # go through the whole schtick again... file = nil checks = Puppet::Server::FileServer::CHECKPARAMS assert_nothing_raised { server = Puppet::Server::FileServer.new( :Local => true, :Config => false ) } assert_nothing_raised { server.mount(testdir, "test") } list = nil sfile = "/test/" assert_nothing_raised { list = server.list(sfile, :ignore, true, false) } # create the new file File.open(newfile, "w") { |f| 3.times { f.puts rand(100) } } newlist = nil assert_nothing_raised { newlist = server.list(sfile, :ignore, true, false) } # verify the list has changed assert(list != newlist) # and verify that we are specifically seeing the new file assert(newlist =~ /newfile/) end # verify we can mount /, which is what local file servers will # normally do def test_zmountroot server = nil assert_nothing_raised { server = Puppet::Server::FileServer.new( :Local => true, :Config => false ) } assert_nothing_raised { server.mount("/", "root") } testdir, pattern, tmpfile = mktestdir() list = nil assert_nothing_raised { list = server.list("/root/" + testdir, :ignore, true, false) } assert(list =~ pattern) assert_nothing_raised { list = server.list("/root" + testdir, :ignore, true, false) } assert(list =~ pattern) end # verify that we're correctly recursing the right number of levels def test_recursionlevels server = nil assert_nothing_raised { server = Puppet::Server::FileServer.new( :Local => true, :Config => false ) } # make our deep recursion basedir = File.join(tmpdir(), "recurseremotetesting") testdir = "%s/with/some/sub/directories/for/the/purposes/of/testing" % basedir oldfile = File.join(testdir, "oldfile") assert_nothing_raised { system("mkdir -p %s" % testdir) File.open(oldfile, "w") { |f| 3.times { f.puts rand(100) } } @@tmpfiles << basedir } assert_nothing_raised { server.mount(basedir, "test") } # get our list list = nil assert_nothing_raised { list = server.list("/test/with", :ignore, false, false) } # make sure we only got one line, since we're not recursing assert(list !~ /\n/) # for each level of recursion, make sure we get the right list [0, 1, 2].each { |num| assert_nothing_raised { list = server.list("/test/with", :ignore, num, false) } count = 0 while list =~ /\n/ list.sub!(/\n/, '') count += 1 end assert_equal(num, count) } end # verify that we're not seeing the dir we ask for; i.e., that our # list is relative to that dir, not it's parent dir def test_listedpath server = nil assert_nothing_raised { server = Puppet::Server::FileServer.new( :Local => true, :Config => false ) } # create a deep dir basedir = tempfile() testdir = "%s/with/some/sub/directories/for/testing" % basedir oldfile = File.join(testdir, "oldfile") assert_nothing_raised { system("mkdir -p %s" % testdir) File.open(oldfile, "w") { |f| 3.times { f.puts rand(100) } } @@tmpfiles << basedir } # mounty mounty assert_nothing_raised { server.mount(basedir, "localhost") } list = nil # and then check a few dirs assert_nothing_raised { list = server.list("/localhost/with", :ignore, false, false) } assert(list !~ /with/) assert_nothing_raised { list = server.list("/localhost/with/some/sub", :ignore, true, false) } assert(list !~ /sub/) end # test many dirs, not necessarily very deep def test_widelists server = nil assert_nothing_raised { server = Puppet::Server::FileServer.new( :Local => true, :Config => false ) } basedir = tempfile() dirs = %w{a set of directories} assert_nothing_raised { Dir.mkdir(basedir) dirs.each { |dir| Dir.mkdir(File.join(basedir, dir)) } @@tmpfiles << basedir } assert_nothing_raised { server.mount(basedir, "localhost") } list = nil assert_nothing_raised { list = server.list("/localhost/", :ignore, 1, false) } assert_instance_of(String, list, "Server returned %s instead of string") list = list.split("\n") assert_equal(dirs.length + 1, list.length) end # verify that 'describe' works as advertised def test_describe server = nil testdir = tstdir() files = mktestfiles(testdir) file = nil checks = Puppet::Server::FileServer::CHECKPARAMS assert_nothing_raised { server = Puppet::Server::FileServer.new( :Local => true, :Config => false ) } assert_nothing_raised { server.mount(testdir, "test") } # get our list list = nil sfile = "/test/" assert_nothing_raised { list = server.list(sfile, :ignore, true, false) } # and describe each file in the list assert_nothing_raised { list.split("\n").each { |line| file, type = line.split("\t") desc = server.describe(sfile + file) } } # and then make sure we can describe everything that we know is there files.each { |file| assert_describe(sfile, file, server) } # And then describe some files that we know aren't there retval = nil assert_nothing_raised("Describing non-existent files raised an error") { retval = server.describe(sfile + "noexisties") } assert_equal("", retval, "Description of non-existent files returned a value") # Now try to describe some sources that don't even exist retval = nil assert_raise(Puppet::FileServerError, "Describing non-existent mount did not raise an error") { retval = server.describe("/notmounted/" + "noexisties") } assert_nil(retval, "Description of non-existent mounts returned a value") end # test that our config file is parsing and working as planned def test_configfile server = nil basedir = File.join(tmpdir, "fileserverconfigfiletesting") @@tmpfiles << basedir # make some dirs for mounting Dir.mkdir(basedir) mounts = {} %w{thing thus these those}.each { |dir| path = File.join(basedir, dir) Dir.mkdir(path) mounts[dir] = mktestfiles(path) } # create an example file with each of them conffile = tempfile @@tmpfiles << conffile File.open(conffile, "w") { |f| f.print "# a test config file [thing] path #{basedir}/thing allow 192.168.0.* [thus] path #{basedir}/thus allow *.madstop.com, *.kanies.com deny *.sub.madstop.com [these] path #{basedir}/these [those] path #{basedir}/those " } # create a server with the file assert_nothing_raised { server = Puppet::Server::FileServer.new( - :Local => true, + :Local => false, :Config => conffile ) } list = nil # run through once with no host/ip info, to verify everything is working mounts.each { |mount, files| mount = "/#{mount}/" assert_nothing_raised { list = server.list(mount, :ignore, true, false) } assert_nothing_raised { list.split("\n").each { |line| file, type = line.split("\t") desc = server.describe(mount + file) } } files.each { |f| assert_describe(mount, f, server) } } # now let's check that things are being correctly forbidden # this is just a map of names and expected results { "thing" => { :deny => [ ["hostname.com", "192.168.1.0"], ["hostname.com", "192.158.0.0"] ], :allow => [ ["hostname.com", "192.168.0.0"], ["hostname.com", "192.168.0.245"], ] }, "thus" => { :deny => [ ["hostname.com", "192.168.1.0"], ["name.sub.madstop.com", "192.158.0.0"] ], :allow => [ ["luke.kanies.com", "192.168.0.0"], ["luke.madstop.com", "192.168.0.245"], ] } }.each { |mount, hash| mount = "/#{mount}/" # run through the map hash.each { |type, ary| ary.each { |sub| host, ip = sub case type when :deny: assert_raise(Puppet::Server::AuthorizationError, "Host %s, ip %s, allowed %s" % [host, ip, mount]) { list = server.list(mount, :ignore, true, false, host, ip) } when :allow: assert_nothing_raised("Host %s, ip %s, denied %s" % [host, ip, mount]) { list = server.list(mount, :ignore, true, false, host, ip) } end } } } end # Test that we smoothly handle invalid config files def test_configfailures # create an example file with each of them conffile = tempfile() invalidmounts = { "noexist" => "[noexist] path /this/path/does/not/exist allow 192.168.0.* " } invalidconfigs = [ "[not valid] path /this/path/does/not/exist allow 192.168.0.* ", "[valid] invalidstatement path /etc allow 192.168.0.* ", "[valid] allow 192.168.0.* " ] invalidmounts.each { |mount, text| File.open(conffile, "w") { |f| f.print text } # create a server with the file server = nil assert_nothing_raised { server = Puppet::Server::FileServer.new( :Local => true, :Config => conffile ) } assert_raise(Puppet::FileServerError, "Invalid mount was mounted") { server.list(mount, :ignore) } } invalidconfigs.each_with_index { |text, i| File.open(conffile, "w") { |f| f.print text } # create a server with the file server = nil assert_raise(Puppet::FileServerError, "Invalid config %s did not raise error" % i) { server = Puppet::Server::FileServer.new( :Local => true, :Config => conffile ) } } end # verify we reread the config file when it changes def test_filereread server = nil conffile = tempfile() dir = tstdir() files = mktestfiles(dir) File.open(conffile, "w") { |f| f.print "# a test config file [thing] path #{dir} allow test1.domain.com " } # Reset the timeout, so we reload faster Puppet[:filetimeout] = 0.5 # start our server with a fast timeout assert_nothing_raised { server = Puppet::Server::FileServer.new( - :Local => true, + :Local => false, :Config => conffile ) } list = nil assert_nothing_raised { list = server.list("/thing/", :ignore, false, false, "test1.domain.com", "127.0.0.1") } assert(list != "", "List returned nothing in rereard test") assert_raise(Puppet::Server::AuthorizationError, "List allowed invalid host") { list = server.list("/thing/", :ignore, false, false, "test2.domain.com", "127.0.0.1") } sleep 1 File.open(conffile, "w") { |f| f.print "# a test config file [thing] path #{dir} allow test2.domain.com " } assert_raise(Puppet::Server::AuthorizationError, "List allowed invalid host") { list = server.list("/thing/", :ignore, false, false, "test1.domain.com", "127.0.0.1") } assert_nothing_raised { list = server.list("/thing/", :ignore, false, false, "test2.domain.com", "127.0.0.1") } assert(list != "", "List returned nothing in rereard test") list = nil end # Verify that we get converted to the right kind of string def test_mountstring mount = nil name = "yaytest" path = tmpdir() assert_nothing_raised { mount = Puppet::Server::FileServer::Mount.new(name, path) } assert_equal("mount[#{name}]", mount.to_s) end def test_servinglinks server = nil source = tempfile() file = File.join(source, "file") link = File.join(source, "link") Dir.mkdir(source) File.open(file, "w") { |f| f.puts "yay" } File.symlink(file, link) assert_nothing_raised { server = Puppet::Server::FileServer.new( :Local => true, :Config => false ) } assert_nothing_raised { server.mount(source, "mount") } # First describe the link when following results = {} assert_nothing_raised { server.describe("/mount/link", :follow).split("\t").zip( Puppet::Server::FileServer::CHECKPARAMS ).each { |v,p| results[p] = v } } assert_equal("file", results[:type]) # Then not results = {} assert_nothing_raised { server.describe("/mount/link", :ignore).split("\t").zip( Puppet::Server::FileServer::CHECKPARAMS ).each { |v,p| results[p] = v } } assert_equal("link", results[:type]) results.each { |p,v| assert(v, "%s has no value" % p) assert(v != "", "%s has no value" % p) } end # Test that substitution patterns in the path are exapanded # properly. Disabled, because it was testing too much of the process # and in a non-portable way. This is a thorough enough test that it should # be kept, but it should be done in a way that is clearly portable (e.g., # no md5 sums of file paths). def test_host_specific client1 = "client1.example.com" client2 = "client2.example.com" ip = "127.0.0.1" # Setup a directory hierarchy for the tests fsdir = File.join(tmpdir(), "host-specific") @@tmpfiles << fsdir hostdir = File.join(fsdir, "host") fqdndir = File.join(fsdir, "fqdn") client1_hostdir = File.join(hostdir, "client1") client2_fqdndir = File.join(fqdndir, client2) contents = { client1_hostdir => "client1\n", client2_fqdndir => client2 + "\n" } [fsdir, hostdir, fqdndir, client1_hostdir, client2_fqdndir].each { |d| Dir.mkdir(d) } [client1_hostdir, client2_fqdndir].each do |d| File.open(File.join(d, "file.txt"), "w") do |f| f.print contents[d] end end conffile = tempfile() File.open(conffile, "w") do |f| f.print(" [host] path #{hostdir}/%h allow * [fqdn] path #{fqdndir}/%H allow * ") end server = nil assert_nothing_raised { server = Puppet::Server::FileServer.new( :Local => true, :Config => conffile ) } # check that list returns the correct thing for the two clients list = nil sfile = "/host/file.txt" assert_nothing_raised { list = server.list(sfile, :ignore, true, false, client1, ip) } assert_equal("/\tfile", list) assert_nothing_raised { list = server.list(sfile, :ignore, true, false, client2, ip) } assert_equal("", list) sfile = "/fqdn/file.txt" assert_nothing_raised { list = server.list(sfile, :ignore, true, false, client1, ip) } assert_equal("", list) assert_nothing_raised { list = server.list(sfile, :ignore, true, false, client2, ip) } assert_equal("/\tfile", list) # check describe sfile = "/host/file.txt" assert_nothing_raised { list = server.describe(sfile, :ignore, client1, ip).split("\t") } assert_equal(5, list.size) assert_equal("file", list[1]) md5 = Digest::MD5.hexdigest(contents[client1_hostdir]) assert_equal("{md5}#{md5}", list[4]) assert_nothing_raised { list = server.describe(sfile, :ignore, client2, ip).split("\t") } assert_equal([], list) sfile = "/fqdn/file.txt" assert_nothing_raised { list = server.describe(sfile, :ignore, client1, ip).split("\t") } assert_equal([], list) assert_nothing_raised { list = server.describe(sfile, :ignore, client2, ip).split("\t") } assert_equal(5, list.size) assert_equal("file", list[1]) md5 = Digest::MD5.hexdigest(contents[client2_fqdndir]) assert_equal("{md5}#{md5}", list[4]) # Check retrieve sfile = "/host/file.txt" assert_nothing_raised { list = server.retrieve(sfile, :ignore, client1, ip).chomp } assert_equal(contents[client1_hostdir].chomp, list) assert_nothing_raised { list = server.retrieve(sfile, :ignore, client2, ip).chomp } assert_equal("", list) sfile = "/fqdn/file.txt" assert_nothing_raised { list = server.retrieve(sfile, :ignore, client1, ip).chomp } assert_equal("", list) assert_nothing_raised { list = server.retrieve(sfile, :ignore, client2, ip).chomp } assert_equal(contents[client2_fqdndir].chomp, list) end # Make sure the 'subdir' method in Mount works. def test_mount_subdir mount = nil base = tempfile() Dir.mkdir(base) subdir = File.join(base, "subdir") Dir.mkdir(subdir) [base, subdir].each do |d| File.open(File.join(d, "file"), "w") { |f| f.puts "bazoo" } end mount = mkmount(base) assert_equal(base, mount.subdir(), "Did not default to base path") assert_equal(subdir, mount.subdir("subdir"), "Did not default to base path") end # Make sure mounts get correctly marked expandable or not, depending on # the path. def test_expandable name = "yaytest" dir = tempfile() Dir.mkdir(dir) mount = mkmount() assert_nothing_raised { mount.path = dir } assert(! mount.expandable?, "Mount incorrectly called expandable") assert_nothing_raised { mount.path = "/dir/a%a" } assert(mount.expandable?, "Mount not called expandable") # This isn't a valid replacement pattern, so it should throw an error # because the dir doesn't exist assert_raise(Puppet::FileServerError) { mount.path = "/dir/a%" } # Now send it back to a normal path assert_nothing_raised { mount.path = dir } # Make sure it got reverted assert(! mount.expandable?, "Mount incorrectly called expandable") end def test_mount_expand mount = mkmount() check = proc do |client, pattern, repl| path = "/my/#{pattern}/file" assert_equal("/my/#{repl}/file", mount.expand(path, client)) end # Do a round of checks with a fake client client = "host.domain.com" {"%h" => "host", # Short name "%H" => client, # Full name "%d" => "domain.com", # domain "%%" => "%", # escape "%o" => "%o" # other }.each do |pat, repl| result = check.call(client, pat, repl) end # Now, check that they use Facter info Puppet.notice "The following messages are normal" client = nil local = Facter["hostname"].value domain = Facter["domain"].value fqdn = [local, domain].join(".") {"%h" => local, # Short name "%H" => fqdn, # Full name "%d" => domain, # domain "%%" => "%", # escape "%o" => "%o" # other }.each do |pat, repl| check.call(client, pat, repl) end end + # Test that the fileserver expands the %h and %d things. def test_fileserver_expansion server = nil assert_nothing_raised { server = Puppet::Server::FileServer.new( :Local => true, :Config => false ) } dir = tempfile() ip = Facter.value(:ipaddress) Dir.mkdir(dir) host = "host.domain.com" { "%H" => "host.domain.com", "%h" => "host", "%d" => "domain.com" }.each do |pattern, string| file = File.join(dir, string) mount = File.join(dir, pattern) File.open(file, "w") do |f| f.puts "yayness: %s" % string end name = "name" obj = nil assert_nothing_raised { obj = server.mount(mount, name) } obj.allow "*" ret = nil assert_nothing_raised do ret = server.list("/name", :ignore, false, false, host, ip) end assert_equal("/\tfile", ret) assert_nothing_raised do ret = server.describe("/name", :ignore, host, ip) end assert(ret =~ /\tfile\t/, "Did not get valid a description") assert_nothing_raised do ret = server.retrieve("/name", :ignore, host, ip) end assert_equal(ret, File.read(file)) server.umount(name) File.unlink(file) end end end # $Id$ diff --git a/test/types/file.rb b/test/types/file.rb index 28cb6c754..d5b493788 100755 --- a/test/types/file.rb +++ b/test/types/file.rb @@ -1,1616 +1,1795 @@ #!/usr/bin/env ruby $:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'puppet' require 'fileutils' require 'puppettest' class TestFile < Test::Unit::TestCase 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) begin initstorage rescue system("rm -rf %s" % Puppet[:statefile]) end end def teardown Puppet::Storage.clear system("rm -rf %s" % Puppet[:statefile]) super end def initstorage Puppet::Storage.init Puppet::Storage.load end def clearstorage Puppet::Storage.store Puppet::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.state(:group)) assert(file.state(:group).should) } end if Puppet::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 = newcomp("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) file.retrieve assert(file.insync?()) assert_nothing_raised() { file[:owner] = uid } assert_apply(file) file.retrieve # make sure changing to number doesn't cause a sync assert(file.insync?()) } # 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| assert_nothing_raised() { file[:group] = group } assert(file.state(:group)) assert(file.state(:group).should) assert_apply(file) file.retrieve assert(file.insync?()) 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?()) @@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?()) 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?()) 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 file.retrieve if file.title !~ /nonexists/ sum = file.state(:checksum) assert(sum.insync?, "file is not in sync") end events = assert_apply(file) 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. assert_nothing_raised do file.state(:checksum).retrieve end assert(file.state(:checksum).insync?, "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 + + 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") + + # check that the file lists us as a dependency + assert_equal([[:file, dir.title]], fileobj[:require], "dependency was not set up") + + # 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 }, "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") + assert_equal(:absent, badobj.should(:ensure), "ensure was not set to absent on 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, :sourcerecurse, :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 + + # 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, "this", "is", "sub", "dir") - tmpfile = File.join(subdir,"testing") + 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} ) } + + children = nil assert_nothing_raised { - dir.evaluate - } - - subobj = nil - assert_nothing_raised { - subobj = Puppet.type(:file)[subdir] + children = dir.eval_generate } - - assert(subobj, "Could not retrieve %s object" % subdir) + + assert_equal([subdir], children.collect {|c| c.title }, + "Incorrect generated children") + + dir.class[subdir].remove File.open(tmpfile, "w") { |f| f.puts "yayness" } - - dir.evaluate - - file = nil + assert_nothing_raised { - file = Puppet.type(:file)[tmpfile] + children = dir.eval_generate } - assert(file, "Could not retrieve %s object" % tmpfile) + assert_equal([subdir, tmpfile].sort, children.collect {|c| c.title }.sort, + "Incorrect generated children") + + File.unlink(tmpfile) #system("rm -rf %s" % basedir) Puppet.type(:file).clear end end -=begin - def test_ignore - - end -=end - - # XXX disabled until i change how dependencies work - def disabled_test_recursionwithcreation - path = "/tmp/this/directory/structure/does/not/exist" - @@tmpfiles.push "/tmp/this" - - file = nil - assert_nothing_raised { - file = mkfile( - :name => path, - :recurse => true, - :ensure => "file" - ) - } - - trans = nil - comp = newcomp("recursewithfiles", file) - assert_nothing_raised { - trans = comp.evaluate - } - - events = nil - assert_nothing_raised { - events = trans.evaluate.collect { |e| e.event.to_s } - } - - puts "events are %s" % events.join(", ") - 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.state(:type).is) # 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.state(:type).is) file[:type] = "directory" assert_nothing_raised { 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?) 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} ) } assert_nothing_raised { - dir.retrieve + 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, "and", "a", "sub", "dir") + 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} ) } assert_nothing_raised { - dirobj.evaluate + dirobj.generate } assert_nothing_raised { file = dirobj.class[path] } assert(file, "Could not retrieve file object") assert_equal("file=%s" % file.title, 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" ) comp = newcomp(baseobj, subobj) comp.finalize assert(subobj.requires?(baseobj), "File did not require basedir") assert(!subobj.requires?(subobj), "File required itself") assert_events([:directory_created, :file_created], comp) 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?, "Object is incorrectly in sync") assert_events([:file_created], obj) obj.retrieve assert(obj.insync?, "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?, "Object is incorrectly in sync") assert_events([:file_changed], obj) text = File.read(file) assert_equal(newstr, text, "Content did not copy correctly") obj.retrieve assert(obj.insync?, "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 = newcomp("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" ) } file.retrieve assert_events([:file_created], file) file.retrieve assert_events([], file) assert_events([], file) 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.state(:group), "Group state failed") end def test_modecreation path = tempfile() file = Puppet.type(:file).create( :path => path, :ensure => "file", :mode => "0777" ) assert_apply(file) assert_equal(0777, File.stat(path).mode & 007777) File.unlink(path) file[:ensure] = "directory" assert_apply(file) assert_equal(0777, File.stat(path).mode & 007777) end def test_followlinks basedir = tempfile() Dir.mkdir(basedir) file = File.join(basedir, "file") link = File.join(basedir, "link") File.open(file, "w", 0644) { |f| f.puts "yayness"; f.flush } File.symlink(file, link) obj = nil assert_nothing_raised { obj = Puppet.type(:file).create( :path => link, :mode => "755" ) } obj.retrieve assert_events([], obj) # Assert that we default to not following links assert_equal("%o" % 0644, "%o" % (File.stat(file).mode & 007777)) # Assert that we can manage the link directly, but modes still don't change obj[:links] = :manage assert_events([], obj) assert_equal("%o" % 0644, "%o" % (File.stat(file).mode & 007777)) obj[:links] = :follow assert_events([:file_changed], obj) assert_equal("%o" % 0755, "%o" % (File.stat(file).mode & 007777)) # Now verify that content and checksum don't update, either obj.delete(:mode) obj[:checksum] = "md5" obj[:links] = :ignore assert_events([], obj) File.open(file, "w") { |f| f.puts "more text" } assert_events([], obj) obj[:links] = :follow assert_events([], obj) File.open(file, "w") { |f| f.puts "even more text" } assert_events([:file_changed], obj) obj.delete(:checksum) obj[:content] = "this is some content" obj[:links] = :ignore assert_events([], obj) File.open(file, "w") { |f| f.puts "more text" } assert_events([], obj) obj[:links] = :follow assert_events([:file_changed], obj) end # If both 'ensure' and 'content' are used, make sure that all of the other # states 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 # Make sure we can create symlinks def test_symlinks path = tempfile() link = tempfile() File.open(path, "w") { |f| f.puts "yay" } file = nil assert_nothing_raised { file = Puppet.type(:file).create( :title => "somethingelse", :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") # Make sure running it again works assert_events([], file) assert_events([], file) assert_events([], file) end + + def test_linkrecurse + dest = tempfile() + link = @file.create :path => tempfile(), :recurse => true, :ensure => dest + + ret = nil + + # Start with nothing, just to make sure we get nothing back + assert_nothing_raised { ret = link.linkrecurse(true) } + assert_nil(ret, "got a return when the dest doesn't exist") + + # then with a directory with only one file + Dir.mkdir(dest) + one = File.join(dest, "one") + File.open(one, "w") { |f| f.puts "" } + link[:ensure] = dest + assert_nothing_raised { ret = link.linkrecurse(true) } + + assert_equal(:directory, link.should(:ensure), "ensure was not set to directory") + assert_equal([File.join(link.title, "one")], ret.collect { |f| f.title }, + "Did not get linked file") + oneobj = @file[File.join(link.title, "one")] + assert_equal(one, oneobj.should(:target), "target was not set correctly") + + oneobj.remove + File.unlink(one) + + # Then make sure we get multiple files + returns = [] + 5.times do |i| + path = File.join(dest, i.to_s) + returns << File.join(link.title, i.to_s) + File.open(path, "w") { |f| f.puts "" } + end + assert_nothing_raised { ret = link.linkrecurse(true) } + + assert_equal(returns.sort, ret.collect { |f| f.title }, + "Did not get links back") + + returns.each do |path| + obj = @file[path] + assert(path, "did not get obj for %s" % path) + sdest = File.join(dest, File.basename(path)) + assert_equal(sdest, obj.should(:target), + "target was not set correctly for %s" % path) + end + end def test_simplerecursivelinking source = tempfile() - dest = tempfile() + path = tempfile() subdir = File.join(source, "subdir") file = File.join(subdir, "file") system("mkdir -p %s" % subdir) system("touch %s" % file) link = nil assert_nothing_raised { link = Puppet.type(:file).create( :ensure => source, - :path => dest, + :path => path, :recurse => true ) } assert_apply(link) - subdest = File.join(dest, "subdir") - linkpath = File.join(subdest, "file") - assert(File.directory?(dest), "dest is not a dir") - assert(File.directory?(subdest), "subdest is not a dir") + sublink = File.join(path, "subdir") + linkpath = File.join(sublink, "file") + assert(File.directory?(path), "dest is not a dir") + assert(File.directory?(sublink), "subdest is not a dir") assert(File.symlink?(linkpath), "path is not a link") assert_equal(file, File.readlink(linkpath)) + assert_nil(@file[sublink], "objects were not removed") assert_events([], link) end def test_recursivelinking source = tempfile() dest = tempfile() files = [] dirs = [] # Make a bunch of files and dirs Dir.mkdir(source) Dir.chdir(source) do system("mkdir -p %s" % "some/path/of/dirs") system("mkdir -p %s" % "other/path/of/dirs") system("touch %s" % "file") system("touch %s" % "other/file") system("touch %s" % "some/path/of/file") system("touch %s" % "some/path/of/dirs/file") system("touch %s" % "other/path/of/file") files = %x{find . -type f}.chomp.split(/\n/) dirs = %x{find . -type d}.chomp.split(/\n/).reject{|d| d =~ /^\.+$/ } end link = nil assert_nothing_raised { link = Puppet.type(:file).create( :ensure => source, :path => dest, :recurse => true ) } assert_apply(link) files.each do |f| f.sub!(/^\.#{File::SEPARATOR}/, '') path = File.join(dest, f) assert(FileTest.exists?(path), "Link %s was not created" % path) assert(FileTest.symlink?(path), "%s is not a link" % f) target = File.readlink(path) assert_equal(File.join(source, f), target) end dirs.each do |d| d.sub!(/^\.#{File::SEPARATOR}/, '') path = File.join(dest, d) assert(FileTest.exists?(path), "Dir %s was not created" % path) assert(FileTest.directory?(path), "%s is not a directory" % d) end end def test_localrelativelinks dir = tempfile() Dir.mkdir(dir) source = File.join(dir, "source") File.open(source, "w") { |f| f.puts "yay" } dest = File.join(dir, "link") link = nil assert_nothing_raised { link = Puppet.type(:file).create( :path => dest, :ensure => "source" ) } assert_events([:link_created], link) assert(FileTest.symlink?(dest), "Did not create link") assert_equal("source", File.readlink(dest)) assert_equal("yay\n", File.read(dest)) end def test_recursivelinkingmissingtarget source = tempfile() dest = tempfile() objects = [] objects << Puppet.type(:exec).create( :command => "mkdir %s; touch %s/file" % [source, source], + :title => "yay", :path => ENV["PATH"] ) objects << Puppet.type(:file).create( :ensure => source, :path => dest, - :recurse => true + :recurse => true, + :require => objects[0] ) assert_apply(*objects) link = File.join(dest, "file") assert(FileTest.symlink?(link), "Did not make link") assert_equal(File.join(source, "file"), File.readlink(link)) end def test_backupmodes 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" ) } # user = group = nil # if Process.uid == 0 # user = nonrootuser # group = nonrootgroup # obj[:owner] = user.name # obj[:group] = group.name # File.chown(user.uid, group.gid, file) # end 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") # if Process.uid == 0 # assert_equal(user.uid, File.stat(backupfile).uid) # assert_equal(group.gid, File.stat(backupfile).gid) # end 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) bucketedpath = File.join(bpath, "18cc17fa3047fcc691fdf49c0a7f539a", "contents") assert_equal(0440, filemode(bucketedpath)) 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_replacedirwithlink path = tempfile() link = tempfile() File.open(path, "w") { |f| f.puts "yay" } Dir.mkdir(link) File.open(File.join(link, "yay"), "w") do |f| f.puts "boo" end file = nil assert_nothing_raised { file = Puppet.type(:file).create( :ensure => path, :path => link, :backup => false ) } # First run through without :force assert_events([], file) assert(FileTest.directory?(link), "Link replaced dir without force") assert_nothing_raised { file[:force] = true } 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_replace_links_with_files base = tempfile() Dir.mkdir(base) file = File.join(base, "file") link = File.join(base, "link") File.open(file, "w") { |f| f.puts "yayness" } File.symlink(file, link) obj = Puppet::Type.type(:file).create( :path => link, :ensure => "file" ) assert_apply(obj) assert_equal("yayness\n", File.read(file), "Original file got changed") assert_equal("file", File.lstat(link).ftype, "File is still a link") end def test_no_erase_linkedto_files base = tempfile() Dir.mkdir(base) dirs = {} %w{other source target}.each do |d| dirs[d] = File.join(base, d) Dir.mkdir(dirs[d]) end file = File.join(dirs["other"], "file") sourcefile = File.join(dirs["source"], "sourcefile") link = File.join(dirs["target"], "link") File.open(file, "w") { |f| f.puts "other" } File.open(sourcefile, "w") { |f| f.puts "source" } File.symlink(file, link) obj = Puppet::Type.type(:file).create( :path => dirs["target"], :ensure => "file", :source => dirs["source"], :recurse => true ) trans = assert_events([:file_created, :file_created], obj) newfile = File.join(dirs["target"], "sourcefile") assert(File.exists?(newfile), "File did not get copied") assert_equal(File.read(sourcefile), File.read(newfile), "File did not get copied correctly.") assert_equal("other\n", File.read(file), "Original file got changed") assert_equal("file", File.lstat(link).ftype, "File is still a link") end def test_replace_links dest = tempfile() otherdest = tempfile() link = tempfile() File.open(dest, "w") { |f| f.puts "boo" } File.open(otherdest, "w") { |f| f.puts "yay" } obj = Puppet::Type.type(:file).create( :path => link, :ensure => otherdest ) assert_apply(obj) assert_equal(otherdest, File.readlink(link), "Link did not get created") obj[:ensure] = dest assert_apply(obj) assert_equal(dest, File.readlink(link), "Link did not get changed") 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) file.retrieve assert(! file.insync?, "File incorrectly in sync") # Now make a file File.open(path, "w") { |f| f.puts "yay" } file.retrieve assert(file.insync?, "File not in sync") # Now make a directory File.unlink(path) Dir.mkdir(path) file.retrieve assert(file.insync?, "Directory not considered 'present'") Dir.rmdir(path) # Now make a link file[:links] = :manage otherfile = tempfile() File.symlink(otherfile, path) file.retrieve assert(file.insync?, "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 can be purged. + # Make sure unmanaged files are be 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") randfile = File.join(destdir, "random") File.open(sourcefile, "w") { |f| f.puts "funtest" } # this file should get removed File.open(randfile, "w") { |f| f.puts "footest" } lfobj = Puppet::Type.newfile(:path => localfile, :content => "rahtest") + destobj = Puppet::Type.newfile(:path => destdir, :source => sourcedir, :recurse => true) - assert_apply(lfobj, destobj) assert(FileTest.exists?(dsourcefile), "File did not get copied") assert(FileTest.exists?(localfile), "File did not get created") assert(FileTest.exists?(randfile), "File got prematurely purged") assert_nothing_raised { destobj[:purge] = true } assert_apply(lfobj, destobj) assert(FileTest.exists?(dsourcefile), "File got purged") assert(FileTest.exists?(localfile), "File got purged") assert(! FileTest.exists?(randfile), "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 = newcomp(user, group, home) } comp.finalize comp.retrieve assert(home.requires?(user), "File did not require owner") assert(home.requires?(group), "File did not require group") 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.state(: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 state = obj.state(:ensure) state.retrieve unless state.insync? assert_nothing_raised do state.sync end end FileUtils.rmtree(dir) end end end end def test_check_checksums dir = tempfile() Dir.mkdir(dir) subdir = File.join(dir, "sub") Dir.mkdir(subdir) file = File.join(dir, "file") File.open(file, "w") { |f| f.puts "yay" } obj = Puppet::Type.type(:file).create( :path => dir, :check => :checksum, :recurse => true ) assert_apply(obj) File.open(file, "w") { |f| f.puts "rah" } sleep 1 system("touch %s" % subdir) Puppet::Storage.store Puppet::Storage.load assert_apply(obj) [file, subdir].each do |path| sub = Puppet::Type.type(:file)[path] assert(sub, "did not find obj for %s" % path) sub.retrieve assert_nothing_raised do sub.state(:checksum).sync end end end end # $Id$ diff --git a/test/types/filesources.rb b/test/types/filesources.rb index 4c1139a0b..bae4c7d5f 100755 --- a/test/types/filesources.rb +++ b/test/types/filesources.rb @@ -1,675 +1,939 @@ #!/usr/bin/env ruby $:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'puppet' require 'cgi' require 'fileutils' require 'puppettest' class TestFileSources < Test::Unit::TestCase include PuppetTest::FileTesting def setup super - begin - initstorage - rescue - system("rm -rf %s" % Puppet[:statefile]) - end if defined? @port @port += 1 else @port = 8800 end + @file = Puppet::Type.type(:file) + end + + def use_storage + begin + initstorage + rescue + system("rm -rf %s" % Puppet[:statefile]) + end end def initstorage Puppet::Storage.init Puppet::Storage.load end - - def clearstorage - Puppet::Storage.store - Puppet::Storage.clear + + # Make a simple recursive tree. + def mk_sourcetree + source = tempfile() + sourcefile = File.join(source, "file") + Dir.mkdir source + File.open(sourcefile, "w") { |f| f.puts "yay" } + + dest = tempfile() + destfile = File.join(dest, "file") + return source, dest, sourcefile, destfile end def test_newchild path = tempfile() @@tmpfiles.push path FileUtils.mkdir_p path File.open(File.join(path,"childtest"), "w") { |of| of.puts "yayness" } file = nil comp = nil trans = nil assert_nothing_raised { file = Puppet.type(:file).create( :name => path ) } child = nil assert_nothing_raised { child = file.newchild("childtest", true) } assert(child) assert_raise(Puppet::DevError) { file.newchild(File.join(path,"childtest"), true) } end + + def test_describe + source = tempfile() + dest = tempfile() + + file = Puppet::Type.newfile :path => dest, :source => source, + :title => "copier" + + state = file.state(:source) + + # First try describing with a normal source + result = nil + assert_nothing_raised do + result = state.describe(source) + end + assert_nil(result, "Got a result back when source is missing") + + # Now make a remote directory + Dir.mkdir(source) + assert_nothing_raised do + result = state.describe(source) + end + assert_equal("directory", result[:type]) + + # And as a file + Dir.rmdir(source) + File.open(source, "w") { |f| f.puts "yay" } + assert_nothing_raised do + result = state.describe(source) + end + assert_equal("file", result[:type]) + assert(result[:checksum], "did not get value for checksum") + if Puppet::SUIDManager.uid == 0 + assert(result.has_key?("owner"), "Lost owner in describe") + else + assert(! result.has_key?("owner"), + "Kept owner in describe even tho not root") + end + + # Now let's do the various link things + File.unlink(source) + target = tempfile() + File.open(target, "w") { |f| f.puts "yay" } + File.symlink(target, source) + + file[:links] = :ignore + assert_nil(state.describe(source), + "Links were not ignored") + + file[:links] = :manage + # We can't manage links at this point + assert_raise(Puppet::FileServerError) do + state.describe(source) + end + + # And then make sure links get followed, otherwise + file[:links] = :follow + assert_equal("file", state.describe(source)[:type]) + end + + def test_source_retrieve + source = tempfile() + dest = tempfile() + + file = Puppet::Type.newfile :path => dest, :source => source, + :title => "copier" + + assert(file.state(:checksum), "source state did not create checksum state") + state = file.state(:source) + assert(state, "did not get source state") + + # Make sure the munge didn't actually change the source + assert_equal(source, state.should, "munging changed the source") + + # First try it with a missing source + assert_nothing_raised do + state.retrieve + end + + # And make sure the state considers itself in sync, since there's nothing + # to do + assert(state.insync?, "source thinks there's work to do with no file or dest") + + # Now make the dest a directory, and make sure the object sets :ensure up to + # create a directory + Dir.mkdir(source) + assert_nothing_raised do + state.retrieve + end + assert_equal(:directory, file.should(:ensure), + "Did not set to create directory") + + # And make sure the source state won't try to do anything with a remote dir + assert(state.insync?, "Source was out of sync even tho remote is dir") + + # Now remove the source, and make sure :ensure was not modified + Dir.rmdir(source) + assert_nothing_raised do + state.retrieve + end + assert_equal(:directory, file.should(:ensure), + "Did not keep :ensure setting") + + # Now have a remote file and make sure things work correctly + File.open(source, "w") { |f| f.puts "yay" } + File.chmod(0755, source) + + assert_nothing_raised do + state.retrieve + end + assert_equal(:file, file.should(:ensure), + "Did not make correct :ensure setting") + assert_equal(0755, file.should(:mode), + "Mode was not copied over") + + # Now let's make sure that we get the first found source + fake = tempfile() + state.should = [fake, source] + assert_nothing_raised do + state.retrieve + end + assert_equal(Digest::MD5.hexdigest(File.read(source)), state.checksum.sub(/^\{\w+\}/, ''), + "Did not catch later source") + end + + def test_insync + source = tempfile() + dest = tempfile() + + file = Puppet::Type.newfile :path => dest, :source => source, + :title => "copier" + + state = file.state(:source) + assert(state, "did not get source state") + + # Try it with no source at all + file.retrieve + assert(state.insync?, "source state not in sync with missing source") + + # with a directory + Dir.mkdir(source) + file.retrieve + assert(state.insync?, "source state not in sync with directory as source") + Dir.rmdir(source) + + # with a file + File.open(source, "w") { |f| f.puts "yay" } + file.retrieve + assert(!state.insync?, "source state was in sync when file was missing") + + # With a different file + File.open(dest, "w") { |f| f.puts "foo" } + file.retrieve + assert(!state.insync?, "source state was in sync with different file") + + # with matching files + File.open(dest, "w") { |f| f.puts "yay" } + file.retrieve + assert(state.insync?, "source state was not in sync with matching file") + end + + def test_source_sync + source = tempfile() + dest = tempfile() + + file = Puppet::Type.newfile :path => dest, :source => source, + :title => "copier" + state = file.state(:source) + + File.open(source, "w") { |f| f.puts "yay" } + + file.retrieve + assert(! state.insync?, "source thinks it's in sync") + + event = nil + assert_nothing_raised do + event = state.sync + end + assert_equal(:file_created, event) + assert_equal(File.read(source), File.read(dest), + "File was not copied correctly") + + # Now write something different + File.open(source, "w") { |f| f.puts "rah" } + file.retrieve + assert(! state.insync?, "source should be out of sync") + assert_nothing_raised do + event = state.sync + end + assert_equal(:file_changed, event) + assert_equal(File.read(source), File.read(dest), + "File was not copied correctly") + end + + # XXX This test doesn't cover everything. Specifically, + # it doesn't handle 'ignore' and 'links'. + def test_sourcerecurse + source, dest, sourcefile, destfile = mk_sourcetree + + # The sourcerecurse method will only ever get called when we're + # recursing, so we go ahead and set it. + obj = Puppet::Type.newfile :source => source, :path => dest, :recurse => true + + result = nil + assert_nothing_raised do + result = obj.sourcerecurse(true) + end + dfileobj = @file[destfile] + assert(dfileobj, "Did not create destfile object") + assert_equal([dfileobj], result) + + # Clean this up so it can be recreated + dfileobj.remove + + # Make sure we correctly iterate over the sources + nosource = tempfile() + obj[:source] = [nosource, source] + + result = nil + assert_nothing_raised do + result = obj.sourcerecurse(true) + end + dfileobj = @file[destfile] + assert(dfileobj, "Did not create destfile object with a missing source") + assert_equal([dfileobj], result) + dfileobj.remove + + # Lastly, make sure we return an empty array when no sources are there + obj[:source] = [nosource, tempfile()] + + assert_nothing_raised do + result = obj.sourcerecurse(true) + end + assert_equal([], result, "Sourcerecurse failed when all sources are missing") + end def test_simplelocalsource path = tempfile() - @@tmpfiles.push path FileUtils.mkdir_p path frompath = File.join(path,"source") topath = File.join(path,"dest") fromfile = nil tofile = nil trans = nil File.open(frompath, File::WRONLY|File::CREAT|File::APPEND) { |of| of.puts "yayness" } assert_nothing_raised { tofile = Puppet.type(:file).create( :name => topath, :source => frompath ) } assert_apply(tofile) assert(FileTest.exists?(topath), "File #{topath} is missing") from = File.open(frompath) { |o| o.read } to = File.open(topath) { |o| o.read } assert_equal(from,to) - @@tmpfiles.push path + end + + # Make sure a simple recursive copy works + def test_simple_recursive_source + source, dest, sourcefile, destfile = mk_sourcetree + + file = Puppet::Type.newfile :path => dest, :source => source, :recurse => true + + assert_events([:directory_created, :file_created], file) + + assert(FileTest.directory?(dest), "Dest dir was not created") + assert(FileTest.file?(destfile), "dest file was not created") + assert_equal("yay\n", File.read(destfile), "dest file was not copied correctly") end def recursive_source_test(fromdir, todir) Puppet::Type.allclear initstorage tofile = nil trans = nil assert_nothing_raised { tofile = Puppet.type(:file).create( :path => todir, :recurse => true, :backup => false, :source => fromdir ) } assert_apply(tofile) assert(FileTest.exists?(todir), "Created dir %s does not exist" % todir) Puppet::Type.allclear end def run_complex_sources(networked = false) path = tempfile() - @@tmpfiles.push path # first create the source directory FileUtils.mkdir_p path - # okay, let's create a directory structure fromdir = File.join(path,"fromdir") Dir.mkdir(fromdir) FileUtils.cd(fromdir) { - mkranddirsandfiles() + File.open("one", "w") { |f| f.puts "onefile"} + File.open("two", "w") { |f| f.puts "twofile"} } todir = File.join(path, "todir") source = fromdir if networked source = "puppet://localhost/%s%s" % [networked, fromdir] end recursive_source_test(source, todir) - return [fromdir,todir] + return [fromdir,todir, File.join(todir, "one"), File.join(todir, "two")] end def test_complex_sources_twice - fromdir, todir = run_complex_sources + fromdir, todir, one, two = run_complex_sources + assert_trees_equal(fromdir,todir) + recursive_source_test(fromdir, todir) assert_trees_equal(fromdir,todir) + # Now remove the whole tree and try it again. + [one, two].each do |f| File.unlink(f) end + Dir.rmdir(todir) recursive_source_test(fromdir, todir) assert_trees_equal(fromdir,todir) end def test_sources_with_deleted_destfiles - fromdir, todir = run_complex_sources - # then delete some files + fromdir, todir, one, two = run_complex_sources assert(FileTest.exists?(todir)) - missing_files = delete_random_files(todir) + + # We shouldn't have a 'two' file object in memory + assert_nil(@file[two], "object for 'two' is still in memory") + # then delete a file + File.unlink(two) + + puts "yay" # and run recursive_source_test(fromdir, todir) - missing_files.each { |file| - assert(FileTest.exists?(file), "Deleted file %s is still missing" % file) - } + assert(FileTest.exists?(two), "Deleted file was not recopied") # and make sure they're still equal assert_trees_equal(fromdir,todir) end def test_sources_with_readonly_destfiles fromdir, todir = run_complex_sources assert(FileTest.exists?(todir)) readonly_random_files(todir) recursive_source_test(fromdir, todir) # and make sure they're still equal assert_trees_equal(fromdir,todir) end def test_sources_with_modified_dest_files fromdir, todir = run_complex_sources assert(FileTest.exists?(todir)) # then modify some files modify_random_files(todir) recursive_source_test(fromdir, todir) # and make sure they're still equal assert_trees_equal(fromdir,todir) end def test_sources_with_added_destfiles fromdir, todir = run_complex_sources assert(FileTest.exists?(todir)) # and finally, add some new files add_random_files(todir) recursive_source_test(fromdir, todir) fromtree = file_list(fromdir) totree = file_list(todir) assert(fromtree != totree, "Trees are incorrectly equal") # then remove our new files FileUtils.cd(todir) { %x{find . 2>/dev/null}.chomp.split(/\n/).each { |file| if file =~ /file[0-9]+/ File.unlink(file) end } } # and make sure they're still equal assert_trees_equal(fromdir,todir) end def test_RecursionWithAddedFiles basedir = tempfile() Dir.mkdir(basedir) @@tmpfiles << basedir file1 = File.join(basedir, "file1") file2 = File.join(basedir, "file2") subdir1 = File.join(basedir, "subdir1") file3 = File.join(subdir1, "file") File.open(file1, "w") { |f| 3.times { f.print rand(100) } } rootobj = nil assert_nothing_raised { rootobj = Puppet.type(:file).create( :name => basedir, :recurse => true, :check => %w{type owner} ) rootobj.evaluate } klass = Puppet.type(:file) assert(klass[basedir]) assert(klass[file1]) assert_nil(klass[file2]) File.open(file2, "w") { |f| 3.times { f.print rand(100) } } assert_nothing_raised { rootobj.evaluate } assert(klass[file2]) Dir.mkdir(subdir1) File.open(file3, "w") { |f| 3.times { f.print rand(100) } } assert_nothing_raised { rootobj.evaluate } assert(klass[file3]) end def mkfileserverconf(mounts) file = tempfile() File.open(file, "w") { |f| mounts.each { |path, name| f.puts "[#{name}]\n\tpath #{path}\n\tallow *\n" } } @@tmpfiles << file return file end def test_NetworkSources server = nil mounts = { "/" => "root" } fileserverconf = mkfileserverconf(mounts) Puppet[:autosign] = true Puppet[:masterport] = 8762 serverpid = nil assert_nothing_raised() { server = Puppet::Server.new( :Handlers => { :CA => {}, # so that certs autogenerate :FileServer => { :Config => fileserverconf } } ) } serverpid = fork { assert_nothing_raised() { #trap(:INT) { server.shutdown; Kernel.exit! } trap(:INT) { server.shutdown } server.start } } @@tmppids << serverpid sleep(1) fromdir, todir = run_complex_sources("root") assert_trees_equal(fromdir,todir) recursive_source_test(fromdir, todir) assert_trees_equal(fromdir,todir) assert_nothing_raised { system("kill -INT %s" % serverpid) } end def test_networkSourcesWithoutService server = nil Puppet[:autosign] = true Puppet[:masterport] = 8765 serverpid = nil assert_nothing_raised() { server = Puppet::Server.new( :Handlers => { :CA => {}, # so that certs autogenerate } ) } serverpid = fork { assert_nothing_raised() { #trap(:INT) { server.shutdown; Kernel.exit! } trap(:INT) { server.shutdown } server.start } } @@tmppids << serverpid sleep(1) name = File.join(tmpdir(), "nosourcefile") file = Puppet.type(:file).create( :source => "puppet://localhost/dist/file", :name => name ) assert_nothing_raised { file.retrieve } comp = newcomp("nosource", file) assert_nothing_raised { comp.evaluate } assert(!FileTest.exists?(name), "File with no source exists anyway") end def test_unmountedNetworkSources server = nil mounts = { "/" => "root", "/noexistokay" => "noexist" } fileserverconf = mkfileserverconf(mounts) Puppet[:autosign] = true Puppet[:masterport] = @port serverpid = nil assert_nothing_raised() { server = Puppet::Server.new( :Port => @port, :Handlers => { :CA => {}, # so that certs autogenerate :FileServer => { :Config => fileserverconf } } ) } serverpid = fork { assert_nothing_raised() { #trap(:INT) { server.shutdown; Kernel.exit! } trap(:INT) { server.shutdown } server.start } } @@tmppids << serverpid sleep(1) name = File.join(tmpdir(), "nosourcefile") file = Puppet.type(:file).create( :source => "puppet://localhost/noexist/file", :name => name ) assert_nothing_raised { file.retrieve } comp = newcomp("nosource", file) assert_nothing_raised { comp.evaluate } assert(!FileTest.exists?(name), "File with no source exists anyway") end def test_alwayschecksum from = tempfile() to = tempfile() File.open(from, "w") { |f| f.puts "yayness" } File.open(to, "w") { |f| f.puts "yayness" } file = nil # Now the files should be exactly the same, so we should not see attempts # at copying assert_nothing_raised { file = Puppet.type(:file).create( :path => to, :source => from ) } file.retrieve assert(file.is(:checksum), "File does not have a checksum state") assert_equal(0, file.evaluate.length, "File produced changes") end def test_sourcepaths files = [] 3.times { files << tempfile() } to = tempfile() File.open(files[-1], "w") { |f| f.puts "yee-haw" } file = nil assert_nothing_raised { file = Puppet.type(:file).create( :name => to, :source => files ) } comp = newcomp(file) assert_events([:file_created], comp) assert(File.exists?(to), "File does not exist") txt = nil File.open(to) { |f| txt = f.read.chomp } assert_equal("yee-haw", txt, "Contents do not match") end # Make sure that source-copying updates the checksum on the same run def test_checksumchange source = tempfile() dest = tempfile() File.open(dest, "w") { |f| f.puts "boo" } File.open(source, "w") { |f| f.puts "yay" } file = nil assert_nothing_raised { file = Puppet.type(:file).create( :name => dest, :source => source ) } file.retrieve assert_events([:file_changed], file) file.retrieve assert_events([], file) end # Make sure that source-copying updates the checksum on the same run def test_sourcebeatsensure source = tempfile() dest = tempfile() File.open(source, "w") { |f| f.puts "yay" } file = nil assert_nothing_raised { file = Puppet.type(:file).create( :name => dest, :ensure => "file", :source => source ) } file.retrieve assert_events([:file_created], file) file.retrieve assert_events([], file) assert_events([], file) end def test_sourcewithlinks source = tempfile() link = tempfile() dest = tempfile() File.open(source, "w") { |f| f.puts "yay" } File.symlink(source, link) file = nil assert_nothing_raised { file = Puppet.type(:file).create( :name => dest, :source => link ) } # Default to skipping links assert_events([], file) assert(! FileTest.exists?(dest), "Created link") # Now follow the links file[:links] = :follow assert_events([:file_created], file) assert(FileTest.file?(dest), "Destination is not a file") # Now copy the links #assert_raise(Puppet::FileServerError) { trans = nil assert_nothing_raised { file[:links] = :manage comp = newcomp(file) trans = comp.evaluate trans.evaluate } assert(trans.failed?(file), "Object did not fail to copy links") end def test_changes source = tempfile() dest = tempfile() File.open(source, "w") { |f| f.puts "yay" } obj = nil assert_nothing_raised { obj = Puppet.type(:file).create( :name => dest, :source => source ) } assert_events([:file_created], obj) assert_equal(File.read(source), File.read(dest), "Files are not equal") assert_events([], obj) File.open(source, "w") { |f| f.puts "boo" } assert_events([:file_changed], obj) assert_equal(File.read(source), File.read(dest), "Files are not equal") assert_events([], obj) File.open(dest, "w") { |f| f.puts "kaboom" } # There are two changes, because first the checksum is noticed, and # then the source causes a change assert_events([:file_changed, :file_changed], obj) assert_equal(File.read(source), File.read(dest), "Files are not equal") assert_events([], obj) end def test_file_source_with_space dir = tempfile() source = File.join(dir, "file with spaces") Dir.mkdir(dir) File.open(source, "w") { |f| f.puts "yayness" } newdir = tempfile() newpath = File.join(newdir, "file with spaces") file = Puppet::Type.newfile( :path => newdir, :source => dir, :recurse => true ) assert_apply(file) assert(FileTest.exists?(newpath), "Did not create file") assert_equal("yayness\n", File.read(newpath)) end # Make sure files aren't replaced when replace is false, but otherwise # are. def test_replace source = tempfile() File.open(source, "w") { |f| f.puts "yayness" } dest = tempfile() file = Puppet::Type.newfile( :path => dest, :source => source, :recurse => true ) assert_apply(file) assert(FileTest.exists?(dest), "Did not create file") assert_equal("yayness\n", File.read(dest)) # Now set :replace assert_nothing_raised { file[:replace] = false } File.open(source, "w") { |f| f.puts "funtest" } assert_apply(file) # Make sure it doesn't change. assert_equal("yayness\n", File.read(dest)) # Now set it to true and make sure it does change. assert_nothing_raised { file[:replace] = true } assert_apply(file) # Make sure it doesn't change. assert_equal("funtest\n", File.read(dest)) end # Testing #285. This just makes sure that URI parsing works correctly. def test_fileswithpoundsigns dir = tstdir() subdir = File.join(dir, "#dir") Dir.mkdir(subdir) file = File.join(subdir, "file") File.open(file, "w") { |f| f.puts "yayness" } dest = tempfile() source = "file://localhost#{dir}" obj = Puppet::Type.newfile( :path => dest, :source => source, :recurse => true ) newfile = File.join(dest, "#dir", "file") poundsource = "file://localhost#{subdir}" sourceobj = path = nil assert_nothing_raised { sourceobj, path = obj.uri2obj(poundsource) } assert_equal("/localhost" + URI.escape(subdir), path) assert_apply(obj) assert(FileTest.exists?(newfile), "File did not get created") assert_equal("yayness\n", File.read(newfile)) end end # $Id$