diff --git a/lib/puppet/file_bucket/dipper.rb b/lib/puppet/file_bucket/dipper.rb index d6f6a3747..870c50eec 100644 --- a/lib/puppet/file_bucket/dipper.rb +++ b/lib/puppet/file_bucket/dipper.rb @@ -1,105 +1,106 @@ require 'puppet/file_bucket' require 'puppet/file_bucket/file' require 'puppet/indirector/request' class Puppet::FileBucket::Dipper # This is a transitional implementation that uses REST # to access remote filebucket files. attr_accessor :name # Create our bucket client def initialize(hash = {}) # Emulate the XMLRPC client server = hash[:Server] port = hash[:Port] || Puppet[:masterport] environment = Puppet[:environment] if hash.include?(:Path) @local_path = hash[:Path] @rest_path = nil else @local_path = nil @rest_path = "https://#{server}:#{port}/#{environment}/file_bucket_file/" end end def local? !! @local_path end # Back up a file to our bucket def backup(file) raise(ArgumentError, "File #{file} does not exist") unless ::File.exist?(file) contents = ::File.read(file) begin file_bucket_file = Puppet::FileBucket::File.new(contents, :bucket_path => @local_path) files_original_path = absolutize_path(file) - dest_path = "#{@rest_path}#{file_bucket_file.name}#{files_original_path}" + dest_path = "#{@rest_path}#{file_bucket_file.name}/#{files_original_path}" + file_bucket_path = "#{@rest_path}#{file_bucket_file.checksum_type}/#{file_bucket_file.checksum_data}/#{files_original_path}" # Make a HEAD request for the file so that we don't waste time # uploading it if it already exists in the bucket. - unless Puppet::FileBucket::File.indirection.head("#{@rest_path}#{file_bucket_file.checksum_type}/#{file_bucket_file.checksum_data}#{files_original_path}") + unless Puppet::FileBucket::File.indirection.head(file_bucket_path) Puppet::FileBucket::File.indirection.save(file_bucket_file, dest_path) end return file_bucket_file.checksum_data rescue => detail puts detail.backtrace if Puppet[:trace] raise Puppet::Error, "Could not back up #{file}: #{detail}" end end # Retrieve a file by sum. def getfile(sum) source_path = "#{@rest_path}md5/#{sum}" file_bucket_file = Puppet::FileBucket::File.indirection.find(source_path, :bucket_path => @local_path) raise Puppet::Error, "File not found" unless file_bucket_file file_bucket_file.to_s end # Restore the file def restore(file,sum) restore = true if FileTest.exists?(file) cursum = Digest::MD5.hexdigest(::File.read(file)) # if the checksum has changed... # this might be extra effort if cursum == sum restore = false end end if restore if newcontents = getfile(sum) tmp = "" newsum = Digest::MD5.hexdigest(newcontents) changed = nil if FileTest.exists?(file) and ! FileTest.writable?(file) changed = ::File.stat(file).mode ::File.chmod(changed | 0200, file) end ::File.open(file, ::File::WRONLY|::File::TRUNC|::File::CREAT) { |of| of.print(newcontents) } ::File.chmod(changed, file) if changed else Puppet.err "Could not find file with checksum #{sum}" return nil end return newsum else return nil end end private def absolutize_path( path ) require 'pathname' Pathname.new(path).realpath end end diff --git a/lib/puppet/file_bucket/file.rb b/lib/puppet/file_bucket/file.rb index 08c0329f1..2a0558fde 100644 --- a/lib/puppet/file_bucket/file.rb +++ b/lib/puppet/file_bucket/file.rb @@ -1,56 +1,56 @@ require 'puppet/file_bucket' require 'puppet/indirector' require 'puppet/util/checksums' require 'digest/md5' class Puppet::FileBucket::File # This class handles the abstract notion of a file in a filebucket. # There are mechanisms to save and load this file locally and remotely in puppet/indirector/filebucketfile/* # There is a compatibility class that emulates pre-indirector filebuckets in Puppet::FileBucket::Dipper extend Puppet::Indirector require 'puppet/file_bucket/file/indirection_hooks' indirects :file_bucket_file, :terminus_class => :file, :extend => Puppet::FileBucket::File::IndirectionHooks attr :contents attr :bucket_path def initialize( contents, options = {} ) - raise ArgumentError if !contents.is_a?(String) - @contents = contents + raise ArgumentError.new("contents must be a String, got a #{contents.class}") unless contents.is_a?(String) + @contents = contents @bucket_path = options.delete(:bucket_path) - raise ArgumentError if options != {} + raise ArgumentError.new("Unknown option(s): #{options.keys.join(', ')}") unless options.empty? end def checksum_type 'md5' end def checksum "{#{checksum_type}}#{checksum_data}" end def checksum_data @checksum_data ||= Digest::MD5.hexdigest(contents) end def to_s contents end def name "#{checksum_type}/#{checksum_data}" end def self.from_s( contents ) self.new( contents ) end def to_pson { "contents" => contents }.to_pson end def self.from_pson( pson ) self.new( pson["contents"] ) end end diff --git a/lib/puppet/file_serving/base.rb b/lib/puppet/file_serving/base.rb index 09cab97d9..706f67af9 100644 --- a/lib/puppet/file_serving/base.rb +++ b/lib/puppet/file_serving/base.rb @@ -1,88 +1,91 @@ # # Created by Luke Kanies on 2007-10-22. # Copyright (c) 2007. All rights reserved. require 'puppet/file_serving' # The base class for Content and Metadata; provides common # functionality like the behaviour around links. class Puppet::FileServing::Base # This is for external consumers to store the source that was used # to retrieve the metadata. attr_accessor :source # Does our file exist? def exist? stat return true rescue => detail return false end # Return the full path to our file. Fails if there's no path set. def full_path(dummy_argument=:work_arround_for_ruby_GC_bug) (if relative_path.nil? or relative_path == "" or relative_path == "." path else File.join(path, relative_path) end).gsub(%r{/+}, "/") end def initialize(path, options = {}) self.path = path @links = :manage options.each do |param, value| begin send param.to_s + "=", value rescue NoMethodError raise ArgumentError, "Invalid option #{param} for #{self.class}" end end end # Determine how we deal with links. attr_reader :links def links=(value) value = value.to_sym value = :manage if value == :ignore raise(ArgumentError, ":links can only be set to :manage or :follow") unless [:manage, :follow].include?(value) @links = value end # Set our base path. attr_reader :path def path=(path) - raise ArgumentError.new("Paths must be fully qualified") unless path =~ /^#{::File::SEPARATOR}/ + unless path =~ /^#{::File::SEPARATOR}/ or path =~ /^[a-z]:[\/\\]/i + raise ArgumentError.new("Paths must be fully qualified") + end + @path = path end # Set a relative path; this is used for recursion, and sets # the file's path relative to the initial recursion point. attr_reader :relative_path def relative_path=(path) raise ArgumentError.new("Relative paths must not be fully qualified") if path =~ /^#{::File::SEPARATOR}/ @relative_path = path end # Stat our file, using the appropriate link-sensitive method. def stat @stat_method ||= self.links == :manage ? :lstat : :stat File.send(@stat_method, full_path) end def to_pson_data_hash { # No 'document_type' since we don't send these bare 'data' => { 'path' => @path, 'relative_path' => @relative_path, 'links' => @links }, 'metadata' => { 'api_version' => 1 } } end end diff --git a/lib/puppet/file_serving/indirection_hooks.rb b/lib/puppet/file_serving/indirection_hooks.rb index a85e90ef1..2a0dc1792 100644 --- a/lib/puppet/file_serving/indirection_hooks.rb +++ b/lib/puppet/file_serving/indirection_hooks.rb @@ -1,34 +1,35 @@ # # Created by Luke Kanies on 2007-10-18. # Copyright (c) 2007. All rights reserved. require 'uri' require 'puppet/file_serving' # This module is used to pick the appropriate terminus # in file-serving indirections. This is necessary because # the terminus varies based on the URI asked for. module Puppet::FileServing::IndirectionHooks PROTOCOL_MAP = {"puppet" => :rest, "file" => :file} # Pick an appropriate terminus based on the protocol. def select_terminus(request) # We rely on the request's parsing of the URI. # Short-circuit to :file if it's a fully-qualified path or specifies a 'file' protocol. return PROTOCOL_MAP["file"] if request.key =~ /^#{::File::SEPARATOR}/ + return PROTOCOL_MAP["file"] if request.key =~ /^[a-z]:[\/\\]/i return PROTOCOL_MAP["file"] if request.protocol == "file" # We're heading over the wire the protocol is 'puppet' and we've got a server name or we're not named 'apply' or 'puppet' if request.protocol == "puppet" and (request.server or !["puppet","apply"].include?(Puppet.settings[:name])) return PROTOCOL_MAP["puppet"] end if request.protocol and PROTOCOL_MAP[request.protocol].nil? raise(ArgumentError, "URI protocol '#{request.protocol}' is not currently supported for file serving") end # If we're still here, we're using the file_server or modules. :file_server end end diff --git a/lib/puppet/indirector/request.rb b/lib/puppet/indirector/request.rb index 697d9df47..4dfbac9ab 100644 --- a/lib/puppet/indirector/request.rb +++ b/lib/puppet/indirector/request.rb @@ -1,250 +1,252 @@ require 'cgi' require 'uri' require 'puppet/indirector' require 'puppet/util/pson' # This class encapsulates all of the information you need to make an # Indirection call, and as a a result also handles REST calls. It's somewhat # analogous to an HTTP Request object, except tuned for our Indirector. class Puppet::Indirector::Request attr_accessor :key, :method, :options, :instance, :node, :ip, :authenticated, :ignore_cache, :ignore_terminus attr_accessor :server, :port, :uri, :protocol attr_reader :indirection_name OPTION_ATTRIBUTES = [:ip, :node, :authenticated, :ignore_terminus, :ignore_cache, :instance, :environment] # Load json before trying to register. Puppet.features.pson? and ::PSON.register_document_type('IndirectorRequest',self) def self.from_pson(json) raise ArgumentError, "No indirection name provided in json data" unless indirection_name = json['type'] raise ArgumentError, "No method name provided in json data" unless method = json['method'] raise ArgumentError, "No key provided in json data" unless key = json['key'] request = new(indirection_name, method, key, json['attributes']) if instance = json['instance'] klass = Puppet::Indirector::Indirection.instance(request.indirection_name).model if instance.is_a?(klass) request.instance = instance else request.instance = klass.from_pson(instance) end end request end def to_pson(*args) result = { 'document_type' => 'IndirectorRequest', 'data' => { 'type' => indirection_name, 'method' => method, 'key' => key } } data = result['data'] attributes = {} OPTION_ATTRIBUTES.each do |key| next unless value = send(key) attributes[key] = value end options.each do |opt, value| attributes[opt] = value end data['attributes'] = attributes unless attributes.empty? data['instance'] = instance if instance result.to_pson(*args) end # Is this an authenticated request? def authenticated? # Double negative, so we just get true or false ! ! authenticated end def environment @environment ||= Puppet::Node::Environment.new end def environment=(env) @environment = if env.is_a?(Puppet::Node::Environment) env else Puppet::Node::Environment.new(env) end end def escaped_key URI.escape(key) end # LAK:NOTE This is a messy interface to the cache, and it's only # used by the Configurer class. I decided it was better to implement # it now and refactor later, when we have a better design, than # to spend another month coming up with a design now that might # not be any better. def ignore_cache? ignore_cache end def ignore_terminus? ignore_terminus end def initialize(indirection_name, method, key_or_instance, options_or_instance = {}) if options_or_instance.is_a? Hash options = options_or_instance @instance = nil else options = {} @instance = options_or_instance end self.indirection_name = indirection_name self.method = method options = options.inject({}) { |hash, ary| hash[ary[0].to_sym] = ary[1]; hash } set_attributes(options) @options = options if key_or_instance.is_a?(String) || key_or_instance.is_a?(Symbol) key = key_or_instance else @instance ||= key_or_instance end if key # If the request key is a URI, then we need to treat it specially, # because it rewrites the key. We could otherwise strip server/port/etc # info out in the REST class, but it seemed bad design for the REST # class to rewrite the key. - if key.to_s =~ /^\w+:\/\// # it's a URI + if key.to_s =~ /^[a-z]:[\/\\]/i # It's an absolute path for Windows. + @key = key + elsif key.to_s =~ /^\w+:\/\// # it's a URI set_uri_key(key) else @key = key end end @key = @instance.name if ! @key and @instance end # Look up the indirection based on the name provided. def indirection Puppet::Indirector::Indirection.instance(indirection_name) end def indirection_name=(name) @indirection_name = name.to_sym end def model raise ArgumentError, "Could not find indirection '#{indirection_name}'" unless i = indirection i.model end # Should we allow use of the cached object? def use_cache? if defined?(@use_cache) ! ! use_cache else true end end # Are we trying to interact with multiple resources, or just one? def plural? method == :search end # Create the query string, if options are present. def query_string return "" unless options and ! options.empty? "?" + options.collect do |key, value| case value when nil; next when true, false; value = value.to_s when Fixnum, Bignum, Float; value = value # nothing when String; value = CGI.escape(value) when Symbol; value = CGI.escape(value.to_s) when Array; value = CGI.escape(YAML.dump(value)) else raise ArgumentError, "HTTP REST queries cannot handle values of type '#{value.class}'" end "#{key}=#{value}" end.join("&") end def to_hash result = options.dup OPTION_ATTRIBUTES.each do |attribute| if value = send(attribute) result[attribute] = value end end result end def to_s return(uri ? uri : "/#{indirection_name}/#{key}") end private def set_attributes(options) OPTION_ATTRIBUTES.each do |attribute| if options.include?(attribute.to_sym) send(attribute.to_s + "=", options[attribute]) options.delete(attribute) end end end # Parse the key as a URI, setting attributes appropriately. def set_uri_key(key) @uri = key begin uri = URI.parse(URI.escape(key)) rescue => detail raise ArgumentError, "Could not understand URL #{key}: #{detail}" end # Just short-circuit these to full paths if uri.scheme == "file" @key = URI.unescape(uri.path) return end @server = uri.host if uri.host # If the URI class can look up the scheme, it will provide a port, # otherwise it will default to '0'. if uri.port.to_i == 0 and uri.scheme == "puppet" @port = Puppet.settings[:masterport].to_i else @port = uri.port.to_i end @protocol = uri.scheme if uri.scheme == 'puppet' @key = URI.unescape(uri.path.sub(/^\//, '')) return end env, indirector, @key = URI.unescape(uri.path.sub(/^\//, '')).split('/',3) @key ||= '' self.environment = env unless env == '' end end diff --git a/lib/puppet/type/file.rb b/lib/puppet/type/file.rb index 988416ab5..07409a108 100644 --- a/lib/puppet/type/file.rb +++ b/lib/puppet/type/file.rb @@ -1,805 +1,819 @@ require 'digest/md5' require 'cgi' require 'etc' require 'uri' require 'fileutils' require 'puppet/network/handler' require 'puppet/util/diff' require 'puppet/util/checksums' require 'puppet/network/client' require 'puppet/util/backups' Puppet::Type.newtype(:file) do include Puppet::Util::MethodHelper include Puppet::Util::Checksums include Puppet::Util::Backups @doc = "Manages local files, including setting ownership and permissions, creation of both files and directories, and retrieving entire files from remote servers. As Puppet matures, it expected that the `file` resource will be used less and less to manage content, and instead native resources will be used to do so. If you find that you are often copying files in from a central location, rather than using native resources, please contact Puppet Labs and we can hopefully work with you to develop a native resource to support what you are doing. - + **Autorequires:** If Puppet is managing the user or group that owns a file, the file resource will autorequire them. If Puppet is managing any parent directories of a file, the file resource will autorequire them." def self.title_patterns [ [ /^(.*?)\/*\Z/m, [ [ :path, lambda{|x| x} ] ] ] ] end newparam(:path) do desc "The path to the file to manage. Must be fully qualified." isnamevar validate do |value| # accept various path syntaxes: lone slash, posix, win32, unc - unless (Puppet.features.posix? and value =~ /^\//) or (Puppet.features.microsoft_windows? and (value =~ /^[A-Za-z]:\// or value =~ /^\/\/[^\/]+\/[^\/]+/)) + unless (Puppet.features.posix? and value =~ /^\//) or (value =~ /^[A-Za-z]:\// or value =~ /^\/\/[^\/]+\/[^\/]+/) fail Puppet::Error, "File paths must be fully qualified, not '#{value}'" end end # convert the current path in an index into the collection and the last # path name. The aim is to use less storage for all common paths in a hierarchy munge do |value| - path, name = ::File.split(value.gsub(/\/+/,'/')) + # We need to save off, and remove the volume designator in the + # path if it is there, since File.split does not handle paths + # with volume designators properly, except when run on Windows. + # Since we are potentially compiling a catalog for a Windows + # machine on a non-Windows master, we need to handle this + # ourselves. + optional_volume_designator = value.match(/^([a-z]:)[\/\\].*/i) + value_without_designator = value.sub(/^(?:[a-z]:)?(.*)/i, '\1') + + path, name = ::File.split(value_without_designator.gsub(/\/+/,'/')) + + if optional_volume_designator + path = optional_volume_designator[1] + path + end + { :index => Puppet::FileCollection.collection.index(path), :name => name } end # and the reverse unmunge do |value| basedir = Puppet::FileCollection.collection.path(value[:index]) # a lone slash as :name indicates a root dir on windows if value[:name] == '/' basedir else ::File.join( basedir, value[:name] ) end end end newparam(:backup) do desc "Whether files should be backed up before being replaced. The preferred method of backing files up is via a `filebucket`, which stores files by their MD5 sums and allows easy retrieval without littering directories with backups. You can specify a local filebucket or a network-accessible server-based filebucket by setting `backup => bucket-name`. Alternatively, if you specify any value that begins with a `.` (e.g., `.puppet-bak`), then Puppet will use copy the file in the same directory with that value as the extension of the backup. Setting `backup => false` disables all backups of the file in question. Puppet automatically creates a local filebucket named `puppet` and defaults to backing up there. To use a server-based filebucket, you must specify one in your configuration filebucket { main: server => puppet } The `puppet master` daemon creates a filebucket by default, so you can usually back up to your main server with this configuration. Once you've described the bucket in your configuration, you can use it in any file file { \"/my/file\": source => \"/path/in/nfs/or/something\", backup => main } This will back the file up to the central server. At this point, the benefits of using a filebucket are that you do not have backup files lying around on each of your machines, a given version of a file is only backed up once, and you can restore any given file manually, no matter how old. Eventually, transactional support will be able to automatically restore filebucketed files. " defaultto "puppet" munge do |value| # I don't really know how this is happening. value = value.shift if value.is_a?(Array) case value when false, "false", :false false when true, "true", ".puppet-bak", :true ".puppet-bak" when String value else self.fail "Invalid backup type #{value.inspect}" end end end newparam(:recurse) do desc "Whether and how deeply to do recursive management. Options are: * `inf,true` --- Regular style recursion on both remote and local directory structure. * `remote` --- Descends recursively into the remote directory but not the local directory. Allows copying of a few files into a directory containing many unmanaged files without scanning all the local files. * `false` --- Default of no recursion. * `[0-9]+` --- Same as true, but limit recursion. Warning: this syntax has been deprecated in favor of the `recurselimit` attribute. " newvalues(:true, :false, :inf, :remote, /^[0-9]+$/) # Replace the validation so that we allow numbers in # addition to string representations of them. validate { |arg| } munge do |value| newval = super(value) case newval when :true, :inf; true when :false; false when :remote; :remote when Integer, Fixnum, Bignum self.warning "Setting recursion depth with the recurse parameter is now deprecated, please use recurselimit" # recurse == 0 means no recursion return false if value == 0 resource[:recurselimit] = value true when /^\d+$/ self.warning "Setting recursion depth with the recurse parameter is now deprecated, please use recurselimit" value = Integer(value) # recurse == 0 means no recursion return false if value == 0 resource[:recurselimit] = value true else self.fail "Invalid recurse value #{value.inspect}" end end end newparam(:recurselimit) do desc "How deeply to do recursive management." newvalues(/^[0-9]+$/) munge do |value| newval = super(value) case newval when Integer, Fixnum, Bignum; value when /^\d+$/; Integer(value) else self.fail "Invalid recurselimit value #{value.inspect}" end end end newparam(:replace, :boolean => true) do desc "Whether or not to replace a file that is sourced but exists. This is useful for using file sources purely for initialization." newvalues(:true, :false) aliasvalue(:yes, :true) aliasvalue(:no, :false) defaultto :true end newparam(:force, :boolean => true) do desc "Force the file operation. Currently only used when replacing directories with links." newvalues(:true, :false) defaultto false end newparam(:ignore) do desc "A parameter which omits action on files matching specified patterns during recursion. Uses Ruby's builtin globbing engine, so shell metacharacters are fully supported, e.g. `[a-z]*`. Matches that would descend into the directory structure are ignored, e.g., `*/*`." validate do |value| unless value.is_a?(Array) or value.is_a?(String) or value == false self.devfail "Ignore must be a string or an Array" end end end newparam(:links) do desc "How to handle links during file actions. During file copying, `follow` will copy the target file instead of the link, `manage` will copy the link itself, and `ignore` will just pass it by. When not copying, `manage` and `ignore` behave equivalently (because you cannot really ignore links entirely during local recursion), and `follow` will manage the file to which the link points." newvalues(:follow, :manage) defaultto :manage end newparam(:purge, :boolean => true) do desc "Whether unmanaged files should be purged. If you have a filebucket configured the purged files will be uploaded, but if you do not, this will destroy data. Only use this option for generated files unless you really know what you are doing. This option only makes sense when recursively managing directories. Note that when using `purge` with `source`, Puppet will purge any files that are not on the remote system." defaultto :false newvalues(:true, :false) end newparam(:sourceselect) do desc "Whether to copy all valid sources, or just the first one. This parameter is only used in recursive copies; by default, the first valid source is the only one used as a recursive source, but if this parameter is set to `all`, then all valid sources will have all of their contents copied to the local host, and for sources that have the same file, the source earlier in the list will be used." defaultto :first newvalues(:first, :all) end # Autorequire the nearest ancestor directory found in the catalog. autorequire(:file) do basedir = ::File.dirname(self[:path]) if basedir != self[:path] parents = [] until basedir == parents.last parents << basedir basedir = ::File.dirname(basedir) end # The filename of the first ancestor found, or nil parents.find { |dir| catalog.resource(:file, dir) } else nil end end # Autorequire the owner and group of the file. {:user => :owner, :group => :group}.each do |type, property| autorequire(type) do if @parameters.include?(property) # The user/group property automatically converts to IDs next unless should = @parameters[property].shouldorig val = should[0] if val.is_a?(Integer) or val =~ /^\d+$/ nil else val end end end end CREATORS = [:content, :source, :target] SOURCE_ONLY_CHECKSUMS = [:none, :ctime, :mtime] validate do creator_count = 0 CREATORS.each do |param| creator_count += 1 if self.should(param) end creator_count += 1 if @parameters.include?(:source) self.fail "You cannot specify more than one of #{CREATORS.collect { |p| p.to_s}.join(", ")}" if creator_count > 1 self.fail "You cannot specify a remote recursion without a source" if !self[:source] and self[:recurse] == :remote self.fail "You cannot specify source when using checksum 'none'" if self[:checksum] == :none && !self[:source].nil? SOURCE_ONLY_CHECKSUMS.each do |checksum_type| self.fail "You cannot specify content when using checksum '#{checksum_type}'" if self[:checksum] == checksum_type && !self[:content].nil? end self.warning "Possible error: recurselimit is set but not recurse, no recursion will happen" if !self[:recurse] and self[:recurselimit] end def self.[](path) return nil unless path super(path.gsub(/\/+/, '/').sub(/\/$/, '')) end def self.instances(base = '/') return self.new(:name => base, :recurse => true, :recurselimit => 1, :audit => :all).recurse_local.values end # Determine the user to write files as. def asuser if self.should(:owner) and ! self.should(:owner).is_a?(Symbol) writeable = Puppet::Util::SUIDManager.asuser(self.should(:owner)) { FileTest.writable?(::File.dirname(self[:path])) } # If the parent directory is writeable, then we execute # as the user in question. Otherwise we'll rely on # the 'owner' property to do things. asuser = self.should(:owner) if writeable end asuser end def bucket return @bucket if @bucket backup = self[:backup] return nil unless backup return nil if backup =~ /^\./ unless catalog or backup == "puppet" fail "Can not find filebucket for backups without a catalog" end unless catalog and filebucket = catalog.resource(:filebucket, backup) or backup == "puppet" fail "Could not find filebucket #{backup} specified in backup" end return default_bucket unless filebucket @bucket = filebucket.bucket @bucket end def default_bucket Puppet::Type.type(:filebucket).mkdefaultbucket.bucket end # Does the file currently exist? Just checks for whether # we have a stat def exist? stat ? true : false end # We have to do some extra finishing, to retrieve our bucket if # there is one. def finish # Look up our bucket, if there is one bucket super end # Create any children via recursion or whatever. def eval_generate return [] unless self.recurse? recurse #recurse.reject do |resource| # catalog.resource(:file, resource[:path]) #end.each do |child| # catalog.add_resource child # catalog.relationship_graph.add_edge self, child #end end def flush # We want to make sure we retrieve metadata anew on each transaction. @parameters.each do |name, param| param.flush if param.respond_to?(:flush) end @stat = :needs_stat end def initialize(hash) # Used for caching clients @clients = {} super # If they've specified a source, we get our 'should' values # from it. unless self[:ensure] if self[:target] self[:ensure] = :symlink elsif self[:content] self[:ensure] = :file end end @stat = :needs_stat end # Configure discovered resources to be purged. def mark_children_for_purging(children) children.each do |name, child| next if child[:source] child[:ensure] = :absent end end # Create a new file or directory object as a child to the current # object. def newchild(path) full_path = ::File.join(self[:path], path) # Add some new values to our original arguments -- these are the ones # set at initialization. We specifically want to exclude any param # values set by the :source property or any default values. # LAK:NOTE This is kind of silly, because the whole point here is that # the values set at initialization should live as long as the resource # but values set by default or by :source should only live for the transaction # or so. Unfortunately, we don't have a straightforward way to manage # the different lifetimes of this data, so we kludge it like this. # The right-side hash wins in the merge. options = @original_parameters.merge(:path => full_path).reject { |param, value| value.nil? } # These should never be passed to our children. [:parent, :ensure, :recurse, :recurselimit, :target, :alias, :source].each do |param| options.delete(param) if options.include?(param) end self.class.new(options) end # Files handle paths specially, because they just lengthen their # path names, rather than including the full parent's title each # time. def pathbuilder # We specifically need to call the method here, so it looks # up our parent in the catalog graph. if parent = parent() # We only need to behave specially when our parent is also # a file if parent.is_a?(self.class) # Remove the parent file name list = parent.pathbuilder list.pop # remove the parent's path info return list << self.ref else return super end else return [self.ref] end end # Should we be purging? def purge? @parameters.include?(:purge) and (self[:purge] == :true or self[:purge] == "true") end # Recursively generate a list of file resources, which will # be used to copy remote files, manage local files, and/or make links # to map to another directory. def recurse children = (self[:recurse] == :remote) ? {} : recurse_local if self[:target] recurse_link(children) elsif self[:source] recurse_remote(children) end # If we're purging resources, then delete any resource that isn't on the # remote system. mark_children_for_purging(children) if self.purge? result = children.values.sort { |a, b| a[:path] <=> b[:path] } remove_less_specific_files(result) end # This is to fix bug #2296, where two files recurse over the same # set of files. It's a rare case, and when it does happen you're # not likely to have many actual conflicts, which is good, because # this is a pretty inefficient implementation. def remove_less_specific_files(files) mypath = self[:path].split(::File::Separator) other_paths = catalog.vertices. select { |r| r.is_a?(self.class) and r[:path] != self[:path] }. collect { |r| r[:path].split(::File::Separator) }. select { |p| p[0,mypath.length] == mypath } return files if other_paths.empty? files.reject { |file| path = file[:path].split(::File::Separator) other_paths.any? { |p| path[0,p.length] == p } } end # A simple method for determining whether we should be recursing. def recurse? self[:recurse] == true or self[:recurse] == :remote end # Recurse the target of the link. def recurse_link(children) perform_recursion(self[:target]).each do |meta| if meta.relative_path == "." self[:ensure] = :directory next end children[meta.relative_path] ||= newchild(meta.relative_path) if meta.ftype == "directory" children[meta.relative_path][:ensure] = :directory else children[meta.relative_path][:ensure] = :link children[meta.relative_path][:target] = meta.full_path end end children end # Recurse the file itself, returning a Metadata instance for every found file. def recurse_local result = perform_recursion(self[:path]) return {} unless result result.inject({}) do |hash, meta| next hash if meta.relative_path == "." hash[meta.relative_path] = newchild(meta.relative_path) hash end end # Recurse against our remote file. def recurse_remote(children) sourceselect = self[:sourceselect] total = self[:source].collect do |source| next unless result = perform_recursion(source) return if top = result.find { |r| r.relative_path == "." } and top.ftype != "directory" result.each { |data| data.source = "#{source}/#{data.relative_path}" } break result if result and ! result.empty? and sourceselect == :first result end.flatten # This only happens if we have sourceselect == :all unless sourceselect == :first found = [] total.reject! do |data| result = found.include?(data.relative_path) found << data.relative_path unless found.include?(data.relative_path) result end end total.each do |meta| if meta.relative_path == "." parameter(:source).metadata = meta next end children[meta.relative_path] ||= newchild(meta.relative_path) children[meta.relative_path][:source] = meta.source children[meta.relative_path][:checksum] = :md5 if meta.ftype == "file" children[meta.relative_path].parameter(:source).metadata = meta end children end def perform_recursion(path) Puppet::FileServing::Metadata.indirection.search( path, :links => self[:links], :recurse => (self[:recurse] == :remote ? true : self[:recurse]), :recurselimit => self[:recurselimit], :ignore => self[:ignore], :checksum_type => (self[:source] || self[:content]) ? self[:checksum] : :none ) end # Remove any existing data. This is only used when dealing with # links or directories. def remove_existing(should) return unless s = stat self.fail "Could not back up; will not replace" unless perform_backup unless should.to_s == "link" return if s.ftype.to_s == should.to_s end case s.ftype when "directory" if self[:force] == :true debug "Removing existing directory for replacement with #{should}" FileUtils.rmtree(self[:path]) else notice "Not removing directory; use 'force' to override" end when "link", "file" debug "Removing existing #{s.ftype} for replacement with #{should}" ::File.unlink(self[:path]) else self.fail "Could not back up files of type #{s.ftype}" end @stat = :needs_stat end def retrieve if source = parameter(:source) source.copy_source_values end super end # Set the checksum, from another property. There are multiple # properties that modify the contents of a file, and they need the # ability to make sure that the checksum value is in sync. def setchecksum(sum = nil) if @parameters.include? :checksum if sum @parameters[:checksum].checksum = sum else # If they didn't pass in a sum, then tell checksum to # figure it out. currentvalue = @parameters[:checksum].retrieve @parameters[:checksum].checksum = currentvalue end end end # Should this thing be a normal file? This is a relatively complex # way of determining whether we're trying to create a normal file, # and it's here so that the logic isn't visible in the content property. def should_be_file? return true if self[:ensure] == :file # I.e., it's set to something like "directory" return false if e = self[:ensure] and e != :present # The user doesn't really care, apparently if self[:ensure] == :present return true unless s = stat return(s.ftype == "file" ? true : false) end # If we've gotten here, then :ensure isn't set return true if self[:content] return true if stat and stat.ftype == "file" false end # Stat our file. Depending on the value of the 'links' attribute, we # use either 'stat' or 'lstat', and we expect the properties to use the # resulting stat object accordingly (mostly by testing the 'ftype' # value). # # We use the initial value :needs_stat to ensure we only stat the file once, # but can also keep track of a failed stat (@stat == nil). This also allows # us to re-stat on demand by setting @stat = :needs_stat. def stat return @stat unless @stat == :needs_stat method = :stat # Files are the only types that support links if (self.class.name == :file and self[:links] != :follow) or self.class.name == :tidy method = :lstat end @stat = begin ::File.send(method, self[:path]) rescue Errno::ENOENT => error nil rescue Errno::EACCES => error warning "Could not stat; permission denied" nil end end # We have to hack this just a little bit, because otherwise we'll get # an error when the target and the contents are created as properties on # the far side. def to_trans(retrieve = true) obj = super obj.delete(:target) if obj[:target] == :notlink obj end # Write out the file. Requires the property name for logging. # Write will be done by the content property, along with checksum computation def write(property) remove_existing(:file) use_temporary_file = write_temporary_file? if use_temporary_file path = "#{self[:path]}.puppettmp_#{rand(10000)}" path = "#{self[:path]}.puppettmp_#{rand(10000)}" while ::File.exists?(path) or ::File.symlink?(path) else path = self[:path] end mode = self.should(:mode) # might be nil umask = mode ? 000 : 022 mode_int = mode ? mode.to_i(8) : nil content_checksum = Puppet::Util.withumask(umask) { ::File.open(path, 'w', mode_int ) { |f| write_content(f) } } # And put our new file in place if use_temporary_file # This is only not true when our file is empty. begin fail_if_checksum_is_wrong(path, content_checksum) if validate_checksum? ::File.rename(path, self[:path]) rescue => detail fail "Could not rename temporary file #{path} to #{self[:path]}: #{detail}" ensure # Make sure the created file gets removed ::File.unlink(path) if FileTest.exists?(path) end end # make sure all of the modes are actually correct property_fix end private # Should we validate the checksum of the file we're writing? def validate_checksum? self[:checksum] !~ /time/ end # Make sure the file we wrote out is what we think it is. def fail_if_checksum_is_wrong(path, content_checksum) newsum = parameter(:checksum).sum_file(path) return if [:absent, nil, content_checksum].include?(newsum) self.fail "File written to disk did not match checksum; discarding changes (#{content_checksum} vs #{newsum})" end # write the current content. Note that if there is no content property # simply opening the file with 'w' as done in write is enough to truncate # or write an empty length file. def write_content(file) (content = property(:content)) && content.write(file) end private def write_temporary_file? # unfortunately we don't know the source file size before fetching it # so let's assume the file won't be empty (c = property(:content) and c.length) || (s = @parameters[:source] and 1) end # There are some cases where all of the work does not get done on # file creation/modification, so we have to do some extra checking. def property_fix properties.each do |thing| next unless [:mode, :owner, :group, :seluser, :selrole, :seltype, :selrange].include?(thing.name) # Make sure we get a new stat objct @stat = :needs_stat currentvalue = thing.retrieve thing.sync unless thing.safe_insync?(currentvalue) end end end # We put all of the properties in separate files, because there are so many # of them. The order these are loaded is important, because it determines # the order they are in the property lit. require 'puppet/type/file/checksum' require 'puppet/type/file/content' # can create the file require 'puppet/type/file/source' # can create the file require 'puppet/type/file/target' # creates a different type of file require 'puppet/type/file/ensure' # can create the file require 'puppet/type/file/owner' require 'puppet/type/file/group' require 'puppet/type/file/mode' require 'puppet/type/file/type' require 'puppet/type/file/selcontext' # SELinux file context require 'puppet/type/file/ctime' require 'puppet/type/file/mtime' diff --git a/lib/puppet/type/file/source.rb b/lib/puppet/type/file/source.rb index 39f85e2ad..8653a8f7a 100755 --- a/lib/puppet/type/file/source.rb +++ b/lib/puppet/type/file/source.rb @@ -1,186 +1,188 @@ require 'puppet/file_serving/content' require 'puppet/file_serving/metadata' module Puppet # Copy files from a local or remote source. This state *only* does any work # when the remote file is an actual file; in that case, this state copies # the file down. If the remote file is a dir or a link or whatever, then # this state, during retrieval, modifies the appropriate other states # so that things get taken care of appropriately. Puppet::Type.type(:file).newparam(:source) do include Puppet::Util::Diff attr_accessor :source, :local desc "Copy a file over the current file. Uses `checksum` to determine when a file should be copied. Valid values are either fully qualified paths to files, or URIs. Currently supported URI types are *puppet* and *file*. This is one of the primary mechanisms for getting content into applications that Puppet does not directly support and is very useful for those configuration files that don't change much across sytems. For instance: class sendmail { file { \"/etc/mail/sendmail.cf\": source => \"puppet://server/modules/module_name/sendmail.cf\" } } You can also leave out the server name, in which case `puppet agent` will fill in the name of its configuration server and `puppet apply` will use the local filesystem. This makes it easy to use the same configuration in both local and centralized forms. Currently, only the `puppet` scheme is supported for source URL's. Puppet will connect to the file server running on `server` to retrieve the contents of the file. If the `server` part is empty, the behavior of the command-line interpreter (`puppet apply`) and the client demon (`puppet agent`) differs slightly: `apply` will look such a file up on the module path on the local host, whereas `agent` will connect to the puppet server that it received the manifest from. See the [fileserver configuration documentation](http://docs.puppetlabs.com/guides/file_serving.html) for information on how to configure and use file services within Puppet. If you specify multiple file sources for a file, then the first source that exists will be used. This allows you to specify what amount to search paths for files: file { \"/path/to/my/file\": source => [ \"/modules/nfs/files/file.$host\", \"/modules/nfs/files/file.$operatingsystem\", \"/modules/nfs/files/file\" ] } This will use the first found file as the source. You cannot currently copy links using this mechanism; set `links` to `follow` if any remote sources are links. " validate do |sources| sources = [sources] unless sources.is_a?(Array) sources.each do |source| begin uri = URI.parse(URI.escape(source)) rescue => detail self.fail "Could not understand source #{source}: #{detail}" end - self.fail "Cannot use URLs of type '#{uri.scheme}' as source for fileserving" unless uri.scheme.nil? or %w{file puppet}.include?(uri.scheme) + self.fail "Cannot use URLs of type '#{uri.scheme}' as source for fileserving" unless uri.scheme.nil? or %w{file puppet}.include?(uri.scheme) or (Puppet.features.microsoft_windows? and uri.scheme =~ /^[a-z]$/i) end end munge do |sources| sources = [sources] unless sources.is_a?(Array) sources.collect { |source| source.sub(/\/$/, '') } end def change_to_s(currentvalue, newvalue) # newvalue = "{md5}#{@metadata.checksum}" if @resource.property(:ensure).retrieve == :absent return "creating from source #{metadata.source} with contents #{metadata.checksum}" else return "replacing from source #{metadata.source} with contents #{metadata.checksum}" end end def checksum metadata && metadata.checksum end # Look up (if necessary) and return remote content. def content return @content if @content raise Puppet::DevError, "No source for content was stored with the metadata" unless metadata.source unless tmp = Puppet::FileServing::Content.indirection.find(metadata.source) fail "Could not find any content at %s" % metadata.source end @content = tmp.content end # Copy the values from the source to the resource. Yay. def copy_source_values devfail "Somehow got asked to copy source values without any metadata" unless metadata # Take each of the stats and set them as states on the local file # if a value has not already been provided. [:owner, :mode, :group, :checksum].each do |metadata_method| param_name = (metadata_method == :checksum) ? :content : metadata_method next if metadata_method == :owner and !Puppet.features.root? next if metadata_method == :checksum and metadata.ftype == "directory" next if metadata_method == :checksum and metadata.ftype == "link" and metadata.links == :manage if resource[param_name].nil? or resource[param_name] == :absent resource[param_name] = metadata.send(metadata_method) end end if resource[:ensure] == :absent # We know all we need to elsif metadata.ftype != "link" resource[:ensure] = metadata.ftype elsif @resource[:links] == :follow resource[:ensure] = :present else resource[:ensure] = "link" resource[:target] = metadata.destination end end def found? ! (metadata.nil? or metadata.ftype.nil?) end attr_writer :metadata # Provide, and retrieve if necessary, the metadata for this file. Fail # if we can't find data about this host, and fail if there are any # problems in our query. def metadata return @metadata if @metadata return nil unless value value.each do |source| begin if data = Puppet::FileServing::Metadata.indirection.find(source) @metadata = data @metadata.source = source break end rescue => detail fail detail, "Could not retrieve file metadata for #{source}: #{detail}" end end fail "Could not retrieve information from environment #{Puppet[:environment]} source(s) #{value.join(", ")}" unless @metadata @metadata end def local? found? and uri and (uri.scheme || "file") == "file" end def full_path URI.unescape(uri.path) if found? and uri end def server (uri and uri.host) or Puppet.settings[:server] end def port (uri and uri.port) or Puppet.settings[:masterport] end private def uri + return nil if metadata.source =~ /^[a-z]:[\/\\]/i # Abspath for Windows + @uri ||= URI.parse(URI.escape(metadata.source)) end end end diff --git a/spec/integration/type/file_spec.rb b/spec/integration/type/file_spec.rb index 241861cc5..9814c4539 100755 --- a/spec/integration/type/file_spec.rb +++ b/spec/integration/type/file_spec.rb @@ -1,511 +1,511 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet_spec/files' describe Puppet::Type.type(:file) do include PuppetSpec::Files before do # stub this to not try to create state.yaml Puppet::Util::Storage.stubs(:store) end it "should not attempt to manage files that do not exist if no means of creating the file is specified" do file = Puppet::Type.type(:file).new :path => make_absolute("/my/file"), :mode => "755" catalog = Puppet::Resource::Catalog.new catalog.add_resource file file.parameter(:mode).expects(:retrieve).never transaction = Puppet::Transaction.new(catalog) transaction.resource_harness.evaluate(file).should_not be_failed end describe "when writing files" do it "should backup files to a filebucket when one is configured", :fails_on_windows => true do bucket = Puppet::Type.type(:filebucket).new :path => tmpfile("filebucket"), :name => "mybucket" file = Puppet::Type.type(:file).new :path => tmpfile("bucket_backs"), :backup => "mybucket", :content => "foo" catalog = Puppet::Resource::Catalog.new catalog.add_resource file catalog.add_resource bucket File.open(file[:path], "w") { |f| f.puts "bar" } md5 = Digest::MD5.hexdigest(File.read(file[:path])) catalog.apply bucket.bucket.getfile(md5).should == "bar\n" end it "should backup files in the local directory when a backup string is provided" do file = Puppet::Type.type(:file).new :path => tmpfile("bucket_backs"), :backup => ".bak", :content => "foo" catalog = Puppet::Resource::Catalog.new catalog.add_resource file File.open(file[:path], "w") { |f| f.puts "bar" } catalog.apply backup = file[:path] + ".bak" FileTest.should be_exist(backup) File.read(backup).should == "bar\n" end it "should fail if no backup can be performed" do dir = tmpfile("backups") Dir.mkdir(dir) path = File.join(dir, "testfile") file = Puppet::Type.type(:file).new :path => path, :backup => ".bak", :content => "foo" catalog = Puppet::Resource::Catalog.new catalog.add_resource file File.open(file[:path], "w") { |f| f.puts "bar" } # Create a directory where the backup should be so that writing to it fails Dir.mkdir(File.join(dir, "testfile.bak")) Puppet::Util::Log.stubs(:newmessage) catalog.apply File.read(file[:path]).should == "bar\n" end - it "should not backup symlinks", :fails_on_windows => true do + it "should not backup symlinks", :unless => Puppet.features.microsoft_windows? do link = tmpfile("link") dest1 = tmpfile("dest1") dest2 = tmpfile("dest2") bucket = Puppet::Type.type(:filebucket).new :path => tmpfile("filebucket"), :name => "mybucket" file = Puppet::Type.type(:file).new :path => link, :target => dest2, :ensure => :link, :backup => "mybucket" catalog = Puppet::Resource::Catalog.new catalog.add_resource file catalog.add_resource bucket File.open(dest1, "w") { |f| f.puts "whatever" } File.symlink(dest1, link) md5 = Digest::MD5.hexdigest(File.read(file[:path])) catalog.apply File.readlink(link).should == dest2 Find.find(bucket[:path]) { |f| File.file?(f) }.should be_nil end it "should backup directories to the local filesystem by copying the whole directory" do file = Puppet::Type.type(:file).new :path => tmpfile("bucket_backs"), :backup => ".bak", :content => "foo", :force => true catalog = Puppet::Resource::Catalog.new catalog.add_resource file Dir.mkdir(file[:path]) otherfile = File.join(file[:path], "foo") File.open(otherfile, "w") { |f| f.print "yay" } catalog.apply backup = file[:path] + ".bak" FileTest.should be_directory(backup) File.read(File.join(backup, "foo")).should == "yay" end it "should backup directories to filebuckets by backing up each file separately", :fails_on_windows => true do bucket = Puppet::Type.type(:filebucket).new :path => tmpfile("filebucket"), :name => "mybucket" file = Puppet::Type.type(:file).new :path => tmpfile("bucket_backs"), :backup => "mybucket", :content => "foo", :force => true catalog = Puppet::Resource::Catalog.new catalog.add_resource file catalog.add_resource bucket Dir.mkdir(file[:path]) foofile = File.join(file[:path], "foo") barfile = File.join(file[:path], "bar") File.open(foofile, "w") { |f| f.print "fooyay" } File.open(barfile, "w") { |f| f.print "baryay" } foomd5 = Digest::MD5.hexdigest(File.read(foofile)) barmd5 = Digest::MD5.hexdigest(File.read(barfile)) catalog.apply bucket.bucket.getfile(foomd5).should == "fooyay" bucket.bucket.getfile(barmd5).should == "baryay" end it "should propagate failures encountered when renaming the temporary file" do file = Puppet::Type.type(:file).new :path => tmpfile("fail_rename"), :content => "foo" file.stubs(:remove_existing) # because it tries to make a backup catalog = Puppet::Resource::Catalog.new catalog.add_resource file File.open(file[:path], "w") { |f| f.print "bar" } File.expects(:rename).raises ArgumentError lambda { file.write(:content) }.should raise_error(Puppet::Error) File.read(file[:path]).should == "bar" end end describe "when recursing" do def build_path(dir) Dir.mkdir(dir) File.chmod(0750, dir) @dirs = [dir] @files = [] %w{one two}.each do |subdir| fdir = File.join(dir, subdir) Dir.mkdir(fdir) File.chmod(0750, fdir) @dirs << fdir %w{three}.each do |file| ffile = File.join(fdir, file) @files << ffile File.open(ffile, "w") { |f| f.puts "test #{file}" } File.chmod(0640, ffile) end end end it "should be able to recurse over a nonexistent file", :fails_on_windows => true do @path = tmpfile("file_integration_tests") @file = Puppet::Type::File.new( :name => @path, :mode => 0644, :recurse => true, :backup => false ) @catalog = Puppet::Resource::Catalog.new @catalog.add_resource @file lambda { @file.eval_generate }.should_not raise_error end it "should be able to recursively set properties on existing files" do @path = tmpfile("file_integration_tests") build_path(@path) @file = Puppet::Type::File.new( :name => @path, :mode => 0644, :recurse => true, :backup => false ) @catalog = Puppet::Resource::Catalog.new @catalog.add_resource @file @catalog.apply @dirs.each do |path| (File.stat(path).mode & 007777).should == 0755 end @files.each do |path| (File.stat(path).mode & 007777).should == 0644 end end - it "should be able to recursively make links to other files", :fails_on_windows => true do + it "should be able to recursively make links to other files", :unless => Puppet.features.microsoft_windows? do source = tmpfile("file_link_integration_source") build_path(source) dest = tmpfile("file_link_integration_dest") @file = Puppet::Type::File.new(:name => dest, :target => source, :recurse => true, :ensure => :link, :backup => false) @catalog = Puppet::Resource::Catalog.new @catalog.add_resource @file @catalog.apply @dirs.each do |path| link_path = path.sub(source, dest) File.lstat(link_path).should be_directory end @files.each do |path| link_path = path.sub(source, dest) File.lstat(link_path).ftype.should == "link" end end it "should be able to recursively copy files", :fails_on_windows => true do source = tmpfile("file_source_integration_source") build_path(source) dest = tmpfile("file_source_integration_dest") @file = Puppet::Type::File.new(:name => dest, :source => source, :recurse => true, :backup => false) @catalog = Puppet::Resource::Catalog.new @catalog.add_resource @file @catalog.apply @dirs.each do |path| newpath = path.sub(source, dest) File.lstat(newpath).should be_directory end @files.each do |path| newpath = path.sub(source, dest) File.lstat(newpath).ftype.should == "file" end end it "should not recursively manage files managed by a more specific explicit file" do dir = tmpfile("recursion_vs_explicit_1") subdir = File.join(dir, "subdir") file = File.join(subdir, "file") FileUtils.mkdir_p(subdir) File.open(file, "w") { |f| f.puts "" } base = Puppet::Type::File.new(:name => dir, :recurse => true, :backup => false, :mode => "755") sub = Puppet::Type::File.new(:name => subdir, :recurse => true, :backup => false, :mode => "644") @catalog = Puppet::Resource::Catalog.new @catalog.add_resource base @catalog.add_resource sub @catalog.apply (File.stat(file).mode & 007777).should == 0644 end - it "should recursively manage files even if there is an explicit file whose name is a prefix of the managed file", :fails_on_windows => true do + it "should recursively manage files even if there is an explicit file whose name is a prefix of the managed file" do dir = tmpfile("recursion_vs_explicit_2") - managed = File.join(dir, "file") - generated = File.join(dir, "file_with_a_name_starting_with_the_word_file") + managed = File.join(dir, "file") + generated = File.join(dir, "file_with_a_name_starting_with_the_word_file") + managed_mode = Puppet.features.microsoft_windows? ? 0444 : 0700 FileUtils.mkdir_p(dir) File.open(managed, "w") { |f| f.puts "" } File.open(generated, "w") { |f| f.puts "" } @catalog = Puppet::Resource::Catalog.new - @catalog.add_resource Puppet::Type::File.new(:name => dir, :recurse => true, :backup => false, :mode => "755") + @catalog.add_resource Puppet::Type::File.new(:name => dir, :recurse => true, :backup => false, :mode => managed_mode) @catalog.add_resource Puppet::Type::File.new(:name => managed, :recurse => true, :backup => false, :mode => "644") @catalog.apply - (File.stat(generated).mode & 007777).should == 0755 + (File.stat(generated).mode & 007777).should == managed_mode end end describe "when generating resources", :fails_on_windows => true do before do @source = tmpfile("generating_in_catalog_source") @dest = tmpfile("generating_in_catalog_dest") Dir.mkdir(@source) s1 = File.join(@source, "one") s2 = File.join(@source, "two") File.open(s1, "w") { |f| f.puts "uno" } File.open(s2, "w") { |f| f.puts "dos" } @file = Puppet::Type::File.new(:name => @dest, :source => @source, :recurse => true, :backup => false) @catalog = Puppet::Resource::Catalog.new @catalog.add_resource @file end it "should add each generated resource to the catalog" do @catalog.apply do |trans| @catalog.resource(:file, File.join(@dest, "one")).should be_instance_of(@file.class) @catalog.resource(:file, File.join(@dest, "two")).should be_instance_of(@file.class) end end it "should have an edge to each resource in the relationship graph" do @catalog.apply do |trans| one = @catalog.resource(:file, File.join(@dest, "one")) @catalog.relationship_graph.edge?(@file, one).should be two = @catalog.resource(:file, File.join(@dest, "two")) @catalog.relationship_graph.edge?(@file, two).should be end end end describe "when copying files" do # Ticket #285. - it "should be able to copy files with pound signs in their names", :fails_on_windows => true do + it "should be able to copy files with pound signs in their names" do source = tmpfile("filewith#signs") dest = tmpfile("destwith#signs") File.open(source, "w") { |f| f.print "foo" } file = Puppet::Type::File.new(:name => dest, :source => source) catalog = Puppet::Resource::Catalog.new catalog.add_resource file catalog.apply File.read(dest).should == "foo" end - it "should be able to copy files with spaces in their names", :fails_on_windows => true do + it "should be able to copy files with spaces in their names" do source = tmpfile("filewith spaces") dest = tmpfile("destwith spaces") File.open(source, "w") { |f| f.print "foo" } File.chmod(0755, source) file = Puppet::Type::File.new(:path => dest, :source => source) catalog = Puppet::Resource::Catalog.new catalog.add_resource file catalog.apply + expected_mode = Puppet.features.microsoft_windows? ? 0644 : 0755 File.read(dest).should == "foo" - (File.stat(dest).mode & 007777).should == 0755 + (File.stat(dest).mode & 007777).should == expected_mode end - it "should be able to copy individual files even if recurse has been specified", :fails_on_windows => true do + it "should be able to copy individual files even if recurse has been specified" do source = tmpfile("source") dest = tmpfile("dest") File.open(source, "w") { |f| f.print "foo" } file = Puppet::Type::File.new(:name => dest, :source => source, :recurse => true) catalog = Puppet::Resource::Catalog.new catalog.add_resource file catalog.apply File.read(dest).should == "foo" end end it "should be able to create files when 'content' is specified but 'ensure' is not" do dest = tmpfile("files_with_content") file = Puppet::Type.type(:file).new( :name => dest, :content => "this is some content, yo" ) catalog = Puppet::Resource::Catalog.new catalog.add_resource file catalog.apply File.read(dest).should == "this is some content, yo" end it "should create files with content if both 'content' and 'ensure' are set" do dest = tmpfile("files_with_content") file = Puppet::Type.type(:file).new( :name => dest, :ensure => "file", :content => "this is some content, yo" ) catalog = Puppet::Resource::Catalog.new catalog.add_resource file catalog.apply File.read(dest).should == "this is some content, yo" end - it "should delete files with sources but that are set for deletion", :fails_on_windows => true do + it "should delete files with sources but that are set for deletion" do dest = tmpfile("dest_source_with_ensure") source = tmpfile("source_source_with_ensure") File.open(source, "w") { |f| f.puts "yay" } File.open(dest, "w") { |f| f.puts "boo" } file = Puppet::Type.type(:file).new( :name => make_absolute(dest), :ensure => :absent, :source => source, :backup => false ) catalog = Puppet::Resource::Catalog.new catalog.add_resource file catalog.apply File.should_not be_exist(dest) end - describe "when purging files", :fails_on_windows => true do + describe "when purging files" do before do @sourcedir = tmpfile("purge_source") @destdir = tmpfile("purge_dest") Dir.mkdir(@sourcedir) Dir.mkdir(@destdir) @sourcefile = File.join(@sourcedir, "sourcefile") @copiedfile = File.join(@destdir, "sourcefile") @localfile = File.join(@destdir, "localfile") @purgee = File.join(@destdir, "to_be_purged") File.open(@localfile, "w") { |f| f.puts "rahtest" } File.open(@sourcefile, "w") { |f| f.puts "funtest" } # this file should get removed File.open(@purgee, "w") { |f| f.puts "footest" } - @lfobj = Puppet::Type.newfile( :title => "localfile", :path => @localfile, :content => "rahtest\n", :ensure => :file, :backup => false ) - @destobj = Puppet::Type.newfile( :title => "destdir", :path => @destdir, :source => @sourcedir, :backup => false, :purge => true, :recurse => true ) @catalog = Puppet::Resource::Catalog.new @catalog.add_resource @lfobj, @destobj end it "should still copy remote files" do @catalog.apply FileTest.should be_exist(@copiedfile) end it "should not purge managed, local files" do @catalog.apply FileTest.should be_exist(@localfile) end it "should purge files that are neither remote nor otherwise managed" do @catalog.apply FileTest.should_not be_exist(@purgee) end end end diff --git a/spec/lib/puppet_spec/files.rb b/spec/lib/puppet_spec/files.rb index 9e75d3142..725bf2af9 100755 --- a/spec/lib/puppet_spec/files.rb +++ b/spec/lib/puppet_spec/files.rb @@ -1,63 +1,61 @@ require 'fileutils' require 'tempfile' +require 'pathname' # A support module for testing files. module PuppetSpec::Files # This code exists only to support tests that run as root, pretty much. # Once they have finally been eliminated this can all go... --daniel 2011-04-08 - if Puppet.features.posix? then - def self.in_tmp(path) - path =~ /^\/tmp/ or path =~ /^\/var\/folders/ - end - elsif Puppet.features.microsoft_windows? - def self.in_tmp(path) - tempdir = File.expand_path(File.join(Dir::LOCAL_APPDATA, "Temp")) - path =~ /^#{tempdir}/ + def self.in_tmp(path) + tempdir = Dir.tmpdir + + Pathname.new(path).ascend do |dir| + return true if File.identical?(tempdir, dir) end - else - fail "Help! Can't find in_tmp for this platform" + + false end def self.cleanup $global_tempfiles ||= [] while path = $global_tempfiles.pop do fail "Not deleting tmpfile #{path} outside regular tmpdir" unless in_tmp(path) begin FileUtils.rm_r path, :secure => true rescue Errno::ENOENT # nothing to do end end end def make_absolute(path) return path unless Puppet.features.microsoft_windows? # REMIND UNC return path if path =~ /^[A-Za-z]:/ pwd = Dir.getwd return "#{pwd[0,2]}#{path}" if pwd.length > 2 and pwd =~ /^[A-Za-z]:/ return "C:#{path}" end def tmpfile(name) # Generate a temporary file, just for the name... source = Tempfile.new(name) path = source.path source.close! # ...record it for cleanup, $global_tempfiles ||= [] $global_tempfiles << File.expand_path(path) # ...and bam. path end def tmpdir(name) path = tmpfile(name) FileUtils.mkdir_p(path) path end end diff --git a/spec/unit/file_bucket/dipper_spec.rb b/spec/unit/file_bucket/dipper_spec.rb index 431b12371..605719324 100755 --- a/spec/unit/file_bucket/dipper_spec.rb +++ b/spec/unit/file_bucket/dipper_spec.rb @@ -1,113 +1,113 @@ #!/usr/bin/env rspec require 'spec_helper' require 'pathname' require 'puppet/file_bucket/dipper' require 'puppet/indirector/file_bucket_file/rest' describe Puppet::FileBucket::Dipper do include PuppetSpec::Files def make_tmp_file(contents) file = tmpfile("file_bucket_file") File.open(file, 'w') { |f| f.write(contents) } file end it "should fail in an informative way when there are failures checking for the file on the server" do @dipper = Puppet::FileBucket::Dipper.new(:Path => make_absolute("/my/bucket")) file = make_tmp_file('contents') Puppet::FileBucket::File.indirection.expects(:head).raises ArgumentError lambda { @dipper.backup(file) }.should raise_error(Puppet::Error) end it "should fail in an informative way when there are failures backing up to the server" do @dipper = Puppet::FileBucket::Dipper.new(:Path => make_absolute("/my/bucket")) file = make_tmp_file('contents') Puppet::FileBucket::File.indirection.expects(:head).returns false Puppet::FileBucket::File.indirection.expects(:save).raises ArgumentError lambda { @dipper.backup(file) }.should raise_error(Puppet::Error) end it "should backup files to a local bucket", :fails_on_windows => true do Puppet[:bucketdir] = "/non/existent/directory" file_bucket = tmpdir("bucket") @dipper = Puppet::FileBucket::Dipper.new(:Path => file_bucket) file = make_tmp_file('my contents') checksum = "2975f560750e71c478b8e3b39a956adb" Digest::MD5.hexdigest('my contents').should == checksum @dipper.backup(file).should == checksum File.exists?("#{file_bucket}/2/9/7/5/f/5/6/0/2975f560750e71c478b8e3b39a956adb/contents").should == true end it "should not backup a file that is already in the bucket" do @dipper = Puppet::FileBucket::Dipper.new(:Path => "/my/bucket") file = make_tmp_file('my contents') checksum = Digest::MD5.hexdigest('my contents') Puppet::FileBucket::File.indirection.expects(:head).returns true Puppet::FileBucket::File.indirection.expects(:save).never @dipper.backup(file).should == checksum end it "should retrieve files from a local bucket" do @dipper = Puppet::FileBucket::Dipper.new(:Path => "/my/bucket") checksum = Digest::MD5.hexdigest('my contents') request = nil Puppet::FileBucketFile::File.any_instance.expects(:find).with{ |r| request = r }.once.returns(Puppet::FileBucket::File.new('my contents')) @dipper.getfile(checksum).should == 'my contents' request.key.should == "md5/#{checksum}" end it "should backup files to a remote server" do @dipper = Puppet::FileBucket::Dipper.new(:Server => "puppetmaster", :Port => "31337") file = make_tmp_file('my contents') checksum = Digest::MD5.hexdigest('my contents') real_path = Pathname.new(file).realpath request1 = nil request2 = nil Puppet::FileBucketFile::Rest.any_instance.expects(:head).with { |r| request1 = r }.once.returns(nil) Puppet::FileBucketFile::Rest.any_instance.expects(:save).with { |r| request2 = r }.once @dipper.backup(file).should == checksum [request1, request2].each do |r| r.server.should == 'puppetmaster' r.port.should == 31337 - r.key.should == "md5/#{checksum}#{real_path}" + r.key.should == "md5/#{checksum}/#{real_path}" end end it "should retrieve files from a remote server" do @dipper = Puppet::FileBucket::Dipper.new(:Server => "puppetmaster", :Port => "31337") checksum = Digest::MD5.hexdigest('my contents') request = nil Puppet::FileBucketFile::Rest.any_instance.expects(:find).with { |r| request = r }.returns(Puppet::FileBucket::File.new('my contents')) @dipper.getfile(checksum).should == "my contents" request.server.should == 'puppetmaster' request.port.should == 31337 request.key.should == "md5/#{checksum}" end end diff --git a/spec/unit/file_bucket/file_spec.rb b/spec/unit/file_bucket/file_spec.rb index c4444ae77..ebf02438c 100755 --- a/spec/unit/file_bucket/file_spec.rb +++ b/spec/unit/file_bucket/file_spec.rb @@ -1,106 +1,112 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/file_bucket/file' require 'digest/md5' require 'digest/sha1' describe Puppet::FileBucket::File do include PuppetSpec::Files before do # this is the default from spec_helper, but it keeps getting reset at odd times @bucketdir = tmpdir('bucket') Puppet[:bucketdir] = @bucketdir @digest = "4a8ec4fa5f01b4ab1a0ab8cbccb709f0" @checksum = "{md5}4a8ec4fa5f01b4ab1a0ab8cbccb709f0" @dir = File.join(@bucketdir, '4/a/8/e/c/4/f/a/4a8ec4fa5f01b4ab1a0ab8cbccb709f0') @contents = "file contents" end it "should have a to_s method to return the contents" do Puppet::FileBucket::File.new(@contents).to_s.should == @contents end it "should raise an error if changing content" do x = Puppet::FileBucket::File.new("first") - proc { x.contents = "new" }.should raise_error + expect { x.contents = "new" }.to raise_error(NoMethodError, /undefined method .contents=/) end it "should require contents to be a string" do - proc { Puppet::FileBucket::File.new(5) }.should raise_error(ArgumentError) + expect { Puppet::FileBucket::File.new(5) }.to raise_error(ArgumentError, /contents must be a String, got a Fixnum$/) + end + + it "should complain about options other than :bucket_path" do + expect { + Puppet::FileBucket::File.new('5', :crazy_option => 'should not be passed') + }.to raise_error(ArgumentError, /Unknown option\(s\): crazy_option/) end it "should set the contents appropriately" do Puppet::FileBucket::File.new(@contents).contents.should == @contents end it "should default to 'md5' as the checksum algorithm if the algorithm is not in the name" do Puppet::FileBucket::File.new(@contents).checksum_type.should == "md5" end it "should calculate the checksum" do Puppet::FileBucket::File.new(@contents).checksum.should == @checksum end describe "when using back-ends" do it "should redirect using Puppet::Indirector" do Puppet::Indirector::Indirection.instance(:file_bucket_file).model.should equal(Puppet::FileBucket::File) end it "should have a :save instance method" do Puppet::FileBucket::File.indirection.should respond_to(:save) end end it "should return a url-ish name" do Puppet::FileBucket::File.new(@contents).name.should == "md5/4a8ec4fa5f01b4ab1a0ab8cbccb709f0" end it "should reject a url-ish name with an invalid checksum" do bucket = Puppet::FileBucket::File.new(@contents) - lambda { bucket.name = "sha1/4a8ec4fa5f01b4ab1a0ab8cbccb709f0/new/path" }.should raise_error + expect { bucket.name = "sha1/4a8ec4fa5f01b4ab1a0ab8cbccb709f0/new/path" }.to raise_error(NoMethodError, /undefined method .name=/) end it "should convert the contents to PSON" do Puppet::FileBucket::File.new(@contents).to_pson.should == '{"contents":"file contents"}' end it "should load from PSON" do Puppet::FileBucket::File.from_pson({"contents"=>"file contents"}).contents.should == "file contents" end def make_bucketed_file FileUtils.mkdir_p(@dir) File.open("#{@dir}/contents", 'w') { |f| f.write @contents } end describe "using the indirector's find method" do it "should return nil if a file doesn't exist" do bucketfile = Puppet::FileBucket::File.indirection.find("md5/#{@digest}") bucketfile.should == nil end it "should find a filebucket if the file exists" do make_bucketed_file bucketfile = Puppet::FileBucket::File.indirection.find("md5/#{@digest}") bucketfile.should_not == nil end describe "using RESTish digest notation" do it "should return nil if a file doesn't exist" do bucketfile = Puppet::FileBucket::File.indirection.find("md5/#{@digest}") bucketfile.should == nil end it "should find a filebucket if the file exists" do make_bucketed_file bucketfile = Puppet::FileBucket::File.indirection.find("md5/#{@digest}") bucketfile.should_not == nil end end end end diff --git a/spec/unit/indirector/file_bucket_file/file_spec.rb b/spec/unit/indirector/file_bucket_file/file_spec.rb index eb86eee85..808da17d8 100755 --- a/spec/unit/indirector/file_bucket_file/file_spec.rb +++ b/spec/unit/indirector/file_bucket_file/file_spec.rb @@ -1,273 +1,273 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/indirector/file_bucket_file/file' describe Puppet::FileBucketFile::File do include PuppetSpec::Files it "should be a subclass of the Code terminus class" do Puppet::FileBucketFile::File.superclass.should equal(Puppet::Indirector::Code) end it "should have documentation" do Puppet::FileBucketFile::File.doc.should be_instance_of(String) end describe "non-stubbing tests" do include PuppetSpec::Files before do Puppet[:bucketdir] = tmpdir('bucketdir') end def save_bucket_file(contents, path = "/who_cares") bucket_file = Puppet::FileBucket::File.new(contents) Puppet::FileBucket::File.indirection.save(bucket_file, "md5/#{Digest::MD5.hexdigest(contents)}#{path}") bucket_file.checksum_data end describe "when servicing a save request" do describe "when supplying a path" do it "should store the path if not already stored" do checksum = save_bucket_file("stuff", "/foo/bar") dir_path = "#{Puppet[:bucketdir]}/c/1/3/d/8/8/c/b/c13d88cb4cb02003daedb8a84e5d272a" File.read("#{dir_path}/contents").should == "stuff" File.read("#{dir_path}/paths").should == "foo/bar\n" end it "should leave the paths file alone if the path is already stored" do checksum = save_bucket_file("stuff", "/foo/bar") checksum = save_bucket_file("stuff", "/foo/bar") dir_path = "#{Puppet[:bucketdir]}/c/1/3/d/8/8/c/b/c13d88cb4cb02003daedb8a84e5d272a" File.read("#{dir_path}/contents").should == "stuff" File.read("#{dir_path}/paths").should == "foo/bar\n" end it "should store an additional path if the new path differs from those already stored" do checksum = save_bucket_file("stuff", "/foo/bar") checksum = save_bucket_file("stuff", "/foo/baz") dir_path = "#{Puppet[:bucketdir]}/c/1/3/d/8/8/c/b/c13d88cb4cb02003daedb8a84e5d272a" File.read("#{dir_path}/contents").should == "stuff" File.read("#{dir_path}/paths").should == "foo/bar\nfoo/baz\n" end end describe "when not supplying a path" do it "should save the file and create an empty paths file" do checksum = save_bucket_file("stuff", "") dir_path = "#{Puppet[:bucketdir]}/c/1/3/d/8/8/c/b/c13d88cb4cb02003daedb8a84e5d272a" File.read("#{dir_path}/contents").should == "stuff" File.read("#{dir_path}/paths").should == "" end end end describe "when servicing a head/find request" do describe "when supplying a path" do it "should return false/nil if the file isn't bucketed" do Puppet::FileBucket::File.indirection.head("md5/0ae2ec1980410229885fe72f7b44fe55/foo/bar").should == false Puppet::FileBucket::File.indirection.find("md5/0ae2ec1980410229885fe72f7b44fe55/foo/bar").should == nil end it "should return false/nil if the file is bucketed but with a different path" do checksum = save_bucket_file("I'm the contents of a file", '/foo/bar') Puppet::FileBucket::File.indirection.head("md5/#{checksum}/foo/baz").should == false Puppet::FileBucket::File.indirection.find("md5/#{checksum}/foo/baz").should == nil end it "should return true/file if the file is already bucketed with the given path" do contents = "I'm the contents of a file" checksum = save_bucket_file(contents, '/foo/bar') Puppet::FileBucket::File.indirection.head("md5/#{checksum}/foo/bar").should == true find_result = Puppet::FileBucket::File.indirection.find("md5/#{checksum}/foo/bar") find_result.should be_a(Puppet::FileBucket::File) find_result.checksum.should == "{md5}#{checksum}" find_result.to_s.should == contents end end describe "when not supplying a path" do [false, true].each do |trailing_slash| describe "#{trailing_slash ? 'with' : 'without'} a trailing slash" do trailing_string = trailing_slash ? '/' : '' it "should return false/nil if the file isn't bucketed" do Puppet::FileBucket::File.indirection.head("md5/0ae2ec1980410229885fe72f7b44fe55#{trailing_string}").should == false Puppet::FileBucket::File.indirection.find("md5/0ae2ec1980410229885fe72f7b44fe55#{trailing_string}").should == nil end it "should return true/file if the file is already bucketed" do contents = "I'm the contents of a file" checksum = save_bucket_file(contents, '/foo/bar') Puppet::FileBucket::File.indirection.head("md5/#{checksum}#{trailing_string}").should == true find_result = Puppet::FileBucket::File.indirection.find("md5/#{checksum}#{trailing_string}") find_result.should be_a(Puppet::FileBucket::File) find_result.checksum.should == "{md5}#{checksum}" find_result.to_s.should == contents end end end end end - describe "when diffing files" do + describe "when diffing files", :unless => Puppet.features.microsoft_windows? do it "should generate an empty string if there is no diff" do checksum = save_bucket_file("I'm the contents of a file") Puppet::FileBucket::File.indirection.find("md5/#{checksum}", :diff_with => checksum).should == '' end it "should generate a proper diff if there is a diff" do checksum1 = save_bucket_file("foo\nbar\nbaz") checksum2 = save_bucket_file("foo\nbiz\nbaz") diff = Puppet::FileBucket::File.indirection.find("md5/#{checksum1}", :diff_with => checksum2) diff.should == < biz HERE end it "should raise an exception if the hash to diff against isn't found" do checksum = save_bucket_file("whatever") bogus_checksum = "d1bf072d0e2c6e20e3fbd23f022089a1" lambda { Puppet::FileBucket::File.indirection.find("md5/#{checksum}", :diff_with => bogus_checksum) }.should raise_error "could not find diff_with #{bogus_checksum}" end it "should return nil if the hash to diff from isn't found" do checksum = save_bucket_file("whatever") bogus_checksum = "d1bf072d0e2c6e20e3fbd23f022089a1" Puppet::FileBucket::File.indirection.find("md5/#{bogus_checksum}", :diff_with => checksum).should == nil end end end describe "when initializing" do it "should use the filebucket settings section" do Puppet.settings.expects(:use).with(:filebucket) Puppet::FileBucketFile::File.new end end [true, false].each do |override_bucket_path| describe "when bucket path #{if override_bucket_path then 'is' else 'is not' end} overridden" do [true, false].each do |supply_path| describe "when #{supply_path ? 'supplying' : 'not supplying'} a path" do before :each do Puppet.settings.stubs(:use) @store = Puppet::FileBucketFile::File.new @contents = "my content" @digest = "f2bfa7fc155c4f42cb91404198dda01f" @digest.should == Digest::MD5.hexdigest(@contents) @bucket_dir = tmpdir("bucket") if override_bucket_path Puppet[:bucketdir] = "/bogus/path" # should not be used else Puppet[:bucketdir] = @bucket_dir end @dir = "#{@bucket_dir}/f/2/b/f/a/7/f/c/f2bfa7fc155c4f42cb91404198dda01f" @contents_path = "#{@dir}/contents" end describe "when retrieving files" do before :each do request_options = {} if override_bucket_path request_options[:bucket_path] = @bucket_dir end key = "md5/#{@digest}" if supply_path key += "/path/to/file" end @request = Puppet::Indirector::Request.new(:indirection_name, :find, key, request_options) end def make_bucketed_file FileUtils.mkdir_p(@dir) File.open(@contents_path, 'w') { |f| f.write @contents } end it "should return an instance of Puppet::FileBucket::File created with the content if the file exists" do make_bucketed_file if supply_path @store.find(@request).should == nil @store.head(@request).should == false # because path didn't match else bucketfile = @store.find(@request) bucketfile.should be_a(Puppet::FileBucket::File) bucketfile.contents.should == @contents @store.head(@request).should == true end end it "should return nil if no file is found" do @store.find(@request).should be_nil @store.head(@request).should == false end end describe "when saving files" do it "should save the contents to the calculated path" do options = {} if override_bucket_path options[:bucket_path] = @bucket_dir end key = "md5/#{@digest}" if supply_path key += "//path/to/file" end file_instance = Puppet::FileBucket::File.new(@contents, options) request = Puppet::Indirector::Request.new(:indirection_name, :save, key, file_instance) @store.save(request) File.read("#{@dir}/contents").should == @contents end end end end end end describe "when verifying identical files" do before do # this is the default from spec_helper, but it keeps getting reset at odd times Puppet[:bucketdir] = make_absolute("/dev/null/bucket") @digest = "4a8ec4fa5f01b4ab1a0ab8cbccb709f0" @checksum = "{md5}4a8ec4fa5f01b4ab1a0ab8cbccb709f0" @dir = make_absolute('/dev/null/bucket/4/a/8/e/c/4/f/a/4a8ec4fa5f01b4ab1a0ab8cbccb709f0') @contents = "file contents" @bucket = stub "bucket file" @bucket.stubs(:bucket_path) @bucket.stubs(:checksum).returns(@checksum) @bucket.stubs(:checksum_data).returns(@digest) @bucket.stubs(:path).returns(nil) @bucket.stubs(:contents).returns("file contents") end it "should raise an error if the files don't match" do File.expects(:read).with("#{@dir}/contents").returns("corrupt contents") lambda{ Puppet::FileBucketFile::File.new.send(:verify_identical_file!, @bucket) }.should raise_error(Puppet::FileBucket::BucketError) end it "should do nothing if the files match" do File.expects(:read).with("#{@dir}/contents").returns("file contents") Puppet::FileBucketFile::File.new.send(:verify_identical_file!, @bucket) end end end diff --git a/spec/unit/type/file_spec.rb b/spec/unit/type/file_spec.rb index 6be8acfca..65c7a091c 100755 --- a/spec/unit/type/file_spec.rb +++ b/spec/unit/type/file_spec.rb @@ -1,1240 +1,1188 @@ #!/usr/bin/env rspec require 'spec_helper' describe Puppet::Type.type(:file) do before do Puppet.settings.stubs(:use) @real_posix = Puppet.features.posix? Puppet.features.stubs("posix?").returns(true) @path = Tempfile.new("puppetspec") pathname = @path.path @path.close!() @path = pathname @file = Puppet::Type::File.new(:name => @path) @catalog = Puppet::Resource::Catalog.new @file.catalog = @catalog end describe "when determining if recursion is enabled" do it "should default to recursion being disabled" do @file.should_not be_recurse end [true, "true", 10, "inf", "remote"].each do |value| it "should consider #{value} to enable recursion" do @file[:recurse] = value @file.must be_recurse end end [false, "false", 0].each do |value| it "should consider #{value} to disable recursion" do @file[:recurse] = value @file.should_not be_recurse end end end describe "#write" do - it "should propagate failures encountered when renaming the temporary file" do File.stubs(:open) File.expects(:rename).raises ArgumentError file = Puppet::Type::File.new(:name => "/my/file", :backup => "puppet") file.stubs(:validate_checksum?).returns(false) property = stub('content_property', :actual_content => "something", :length => "something".length) file.stubs(:property).with(:content).returns(property) lambda { file.write(:content) }.should raise_error(Puppet::Error) end it "should delegate writing to the content property" do filehandle = stub_everything 'fh' File.stubs(:open).yields(filehandle) File.stubs(:rename) property = stub('content_property', :actual_content => "something", :length => "something".length) file = Puppet::Type::File.new(:name => "/my/file", :backup => "puppet") file.stubs(:validate_checksum?).returns(false) file.stubs(:property).with(:content).returns(property) property.expects(:write).with(filehandle) file.write(:content) end describe "when validating the checksum" do before { @file.stubs(:validate_checksum?).returns(true) } it "should fail if the checksum parameter and content checksums do not match" do checksum = stub('checksum_parameter', :sum => 'checksum_b', :sum_file => 'checksum_b') @file.stubs(:parameter).with(:checksum).returns(checksum) property = stub('content_property', :actual_content => "something", :length => "something".length, :write => 'checksum_a') @file.stubs(:property).with(:content).returns(property) lambda { @file.write :NOTUSED }.should raise_error(Puppet::Error) end end describe "when not validating the checksum" do before { @file.stubs(:validate_checksum?).returns(false) } it "should not fail if the checksum property and content checksums do not match" do checksum = stub('checksum_parameter', :sum => 'checksum_b') @file.stubs(:parameter).with(:checksum).returns(checksum) property = stub('content_property', :actual_content => "something", :length => "something".length, :write => 'checksum_a') @file.stubs(:property).with(:content).returns(property) lambda { @file.write :NOTUSED }.should_not raise_error(Puppet::Error) end - end end it "should have a method for determining if the file is present" do @file.must respond_to(:exist?) end it "should be considered existent if it can be stat'ed" do @file.expects(:stat).returns mock('stat') @file.must be_exist end it "should be considered nonexistent if it can not be stat'ed" do @file.expects(:stat).returns nil @file.must_not be_exist end it "should have a method for determining if the file should be a normal file" do @file.must respond_to(:should_be_file?) end it "should be a file if :ensure is set to :file" do @file[:ensure] = :file @file.must be_should_be_file end it "should be a file if :ensure is set to :present and the file exists as a normal file" do @file.stubs(:stat).returns(mock('stat', :ftype => "file")) @file[:ensure] = :present @file.must be_should_be_file end it "should not be a file if :ensure is set to something other than :file" do @file[:ensure] = :directory @file.must_not be_should_be_file end it "should not be a file if :ensure is set to :present and the file exists but is not a normal file" do @file.stubs(:stat).returns(mock('stat', :ftype => "directory")) @file[:ensure] = :present @file.must_not be_should_be_file end it "should be a file if :ensure is not set and :content is" do @file[:content] = "foo" @file.must be_should_be_file end it "should be a file if neither :ensure nor :content is set but the file exists as a normal file" do @file.stubs(:stat).returns(mock("stat", :ftype => "file")) @file.must be_should_be_file end it "should not be a file if neither :ensure nor :content is set but the file exists but not as a normal file" do @file.stubs(:stat).returns(mock("stat", :ftype => "directory")) @file.must_not be_should_be_file end describe "when using POSIX filenames" do - describe "on POSIX systems" do - before do - Puppet.features.stubs(:posix?).returns(true) - Puppet.features.stubs(:microsoft_windows?).returns(false) - end - - it "should autorequire its parent directory" do - file = Puppet::Type::File.new(:path => "/foo/bar") - dir = Puppet::Type::File.new(:path => "/foo") - @catalog.add_resource file - @catalog.add_resource dir - reqs = file.autorequire - reqs[0].source.must == dir - reqs[0].target.must == file - end - - it "should autorequire its nearest ancestor directory" do - file = Puppet::Type::File.new(:path => "/foo/bar/baz") - dir = Puppet::Type::File.new(:path => "/foo") - root = Puppet::Type::File.new(:path => "/") - @catalog.add_resource file - @catalog.add_resource dir - @catalog.add_resource root - reqs = file.autorequire - reqs.length.must == 1 - reqs[0].source.must == dir - reqs[0].target.must == file - end - - it "should not autorequire anything when there is no nearest ancestor directory" do - file = Puppet::Type::File.new(:path => "/foo/bar/baz") - @catalog.add_resource file - file.autorequire.should be_empty - end - - it "should not autorequire its parent dir if its parent dir is itself" do - file = Puppet::Type::File.new(:path => "/") - @catalog.add_resource file - file.autorequire.should be_empty - end - - it "should remove trailing slashes" do - file = Puppet::Type::File.new(:path => "/foo/bar/baz/") - file[:path].should == "/foo/bar/baz" - end - - it "should remove double slashes" do - file = Puppet::Type::File.new(:path => "/foo/bar//baz") - file[:path].should == "/foo/bar/baz" - end + it "should autorequire its parent directory" do + file = Puppet::Type::File.new(:path => "/foo/bar") + dir = Puppet::Type::File.new(:path => "/foo") + @catalog.add_resource file + @catalog.add_resource dir + reqs = file.autorequire + reqs[0].source.must == dir + reqs[0].target.must == file + end - it "should remove trailing double slashes" do - file = Puppet::Type::File.new(:path => "/foo/bar/baz//") - file[:path].should == "/foo/bar/baz" - end + it "should autorequire its nearest ancestor directory" do + file = Puppet::Type::File.new(:path => "/foo/bar/baz") + dir = Puppet::Type::File.new(:path => "/foo") + root = Puppet::Type::File.new(:path => "/") + @catalog.add_resource file + @catalog.add_resource dir + @catalog.add_resource root + reqs = file.autorequire + reqs.length.must == 1 + reqs[0].source.must == dir + reqs[0].target.must == file + end - it "should leave a single slash alone" do - file = Puppet::Type::File.new(:path => "/") - file[:path].should == "/" - end + it "should not autorequire anything when there is no nearest ancestor directory" do + file = Puppet::Type::File.new(:path => "/foo/bar/baz") + @catalog.add_resource file + file.autorequire.should be_empty + end - it "should accept a double-slash at the start of the path" do - expect { - file = Puppet::Type::File.new(:path => "//tmp/xxx") - # REVISIT: This should be wrong, later. See the next test. - # --daniel 2011-01-31 - file[:path].should == '/tmp/xxx' - }.should_not raise_error - end + it "should not autorequire its parent dir if its parent dir is itself" do + file = Puppet::Type::File.new(:path => "/") + @catalog.add_resource file + file.autorequire.should be_empty + end - # REVISIT: This is pending, because I don't want to try and audit the - # entire codebase to make sure we get this right. POSIX treats two (and - # exactly two) '/' characters at the start of the path specially. - # - # See sections 3.2 and 4.11, which allow DomainOS to be all special like - # and still have the POSIX branding and all. --daniel 2011-01-31 - it "should preserve the double-slash at the start of the path" + it "should remove trailing slashes" do + file = Puppet::Type::File.new(:path => "/foo/bar/baz/") + file[:path].should == "/foo/bar/baz" end - describe "on Microsoft Windows systems" do - before do - Puppet.features.stubs(:posix?).returns(false) - Puppet.features.stubs(:microsoft_windows?).returns(true) - end + it "should remove double slashes" do + file = Puppet::Type::File.new(:path => "/foo/bar//baz") + file[:path].should == "/foo/bar/baz" + end - it "should refuse to work" do - lambda { Puppet::Type::File.new(:path => "/foo/bar") }.should raise_error(Puppet::Error) - end + it "should remove trailing double slashes" do + file = Puppet::Type::File.new(:path => "/foo/bar/baz//") + file[:path].should == "/foo/bar/baz" end - end - describe "when using Microsoft Windows filenames", :if => Puppet.features.microsoft_windows? do - describe "on Microsoft Windows systems" do - before do - Puppet.features.stubs(:posix?).returns(false) - Puppet.features.stubs(:microsoft_windows?).returns(true) - end + it "should leave a single slash alone" do + file = Puppet::Type::File.new(:path => "/") + file[:path].should == "/" + end - it "should autorequire its parent directory" do - file = Puppet::Type::File.new(:path => "X:/foo/bar") - dir = Puppet::Type::File.new(:path => "X:/foo") - @catalog.add_resource file - @catalog.add_resource dir - reqs = file.autorequire - reqs[0].source.must == dir - reqs[0].target.must == file - end + it "should accept a double-slash at the start of the path" do + expect { + file = Puppet::Type::File.new(:path => "//tmp/xxx") + # REVISIT: This should be wrong, later. See the next test. + # --daniel 2011-01-31 + file[:path].should == '/tmp/xxx' + }.should_not raise_error + end - it "should autorequire its nearest ancestor directory" do - file = Puppet::Type::File.new(:path => "X:/foo/bar/baz") - dir = Puppet::Type::File.new(:path => "X:/foo") - root = Puppet::Type::File.new(:path => "X:/") - @catalog.add_resource file - @catalog.add_resource dir - @catalog.add_resource root - reqs = file.autorequire - reqs.length.must == 1 - reqs[0].source.must == dir - reqs[0].target.must == file - end + # REVISIT: This is pending, because I don't want to try and audit the + # entire codebase to make sure we get this right. POSIX treats two (and + # exactly two) '/' characters at the start of the path specially. + # + # See sections 3.2 and 4.11, which allow DomainOS to be all special like + # and still have the POSIX branding and all. --daniel 2011-01-31 + it "should preserve the double-slash at the start of the path" + end - it "should not autorequire anything when there is no nearest ancestor directory" do - file = Puppet::Type::File.new(:path => "X:/foo/bar/baz") - @catalog.add_resource file - file.autorequire.should be_empty - end + describe "when using Microsoft Windows filenames" do + it "should autorequire its parent directory" do + file = Puppet::Type::File.new(:path => "X:/foo/bar") + dir = Puppet::Type::File.new(:path => "X:/foo") + @catalog.add_resource file + @catalog.add_resource dir + reqs = file.autorequire + reqs[0].source.must == dir + reqs[0].target.must == file + end - it "should not autorequire its parent dir if its parent dir is itself" do - file = Puppet::Type::File.new(:path => "X:/") - @catalog.add_resource file - file.autorequire.should be_empty - end + it "should autorequire its nearest ancestor directory" do + file = Puppet::Type::File.new(:path => "X:/foo/bar/baz") + dir = Puppet::Type::File.new(:path => "X:/foo") + root = Puppet::Type::File.new(:path => "X:/") + @catalog.add_resource file + @catalog.add_resource dir + @catalog.add_resource root + reqs = file.autorequire + reqs.length.must == 1 + reqs[0].source.must == dir + reqs[0].target.must == file + end - it "should remove trailing slashes" do - file = Puppet::Type::File.new(:path => "X:/foo/bar/baz/") - file[:path].should == "X:/foo/bar/baz" - end + it "should not autorequire anything when there is no nearest ancestor directory" do + file = Puppet::Type::File.new(:path => "X:/foo/bar/baz") + @catalog.add_resource file + file.autorequire.should be_empty + end - it "should remove double slashes" do - file = Puppet::Type::File.new(:path => "X:/foo/bar//baz") - file[:path].should == "X:/foo/bar/baz" - end + it "should not autorequire its parent dir if its parent dir is itself" do + file = Puppet::Type::File.new(:path => "X:/") + @catalog.add_resource file + file.autorequire.should be_empty + end - it "should remove trailing double slashes" do - file = Puppet::Type::File.new(:path => "X:/foo/bar/baz//") - file[:path].should == "X:/foo/bar/baz" - end + it "should remove trailing slashes" do + file = Puppet::Type::File.new(:path => "X:/foo/bar/baz/") + file[:path].should == "X:/foo/bar/baz" + end - it "should leave a drive letter with a slash alone", :'fails_on_ruby_1.9.2' => true do - file = Puppet::Type::File.new(:path => "X:/") - file[:path].should == "X:/" - end + it "should remove double slashes" do + file = Puppet::Type::File.new(:path => "X:/foo/bar//baz") + file[:path].should == "X:/foo/bar/baz" + end - it "should not accept a drive letter without a slash", :'fails_on_ruby_1.9.2' => true do - lambda { Puppet::Type::File.new(:path => "X:") }.should raise_error(/File paths must be fully qualified/) - end + it "should remove trailing double slashes" do + file = Puppet::Type::File.new(:path => "X:/foo/bar/baz//") + file[:path].should == "X:/foo/bar/baz" end - describe "on POSIX systems" do - before do - Puppet.features.stubs(:posix?).returns(true) - Puppet.features.stubs(:microsoft_windows?).returns(false) - end + it "should leave a drive letter with a slash alone", :'fails_on_ruby_1.9.2' => true do + file = Puppet::Type::File.new(:path => "X:/") + file[:path].should == "X:/" + end - it "should refuse to work" do - lambda { Puppet::Type::File.new(:path => "X:/foo/bar") }.should raise_error(Puppet::Error) - end + it "should not accept a drive letter without a slash", :'fails_on_ruby_1.9.2' => true do + lambda { Puppet::Type::File.new(:path => "X:") }.should raise_error(/File paths must be fully qualified/) end end - describe "when using UNC filenames" do - describe "on Microsoft Windows systems", :if => Puppet.features.microsoft_windows?, :'fails_on_ruby_1.9.2' => true do - before do - Puppet.features.stubs(:posix?).returns(false) - Puppet.features.stubs(:microsoft_windows?).returns(true) - end - - it "should autorequire its parent directory" do - file = Puppet::Type::File.new(:path => "//server/foo/bar") - dir = Puppet::Type::File.new(:path => "//server/foo") - @catalog.add_resource file - @catalog.add_resource dir - reqs = file.autorequire - reqs[0].source.must == dir - reqs[0].target.must == file - end - - it "should autorequire its nearest ancestor directory" do - file = Puppet::Type::File.new(:path => "//server/foo/bar/baz/qux") - dir = Puppet::Type::File.new(:path => "//server/foo/bar") - root = Puppet::Type::File.new(:path => "//server/foo") - @catalog.add_resource file - @catalog.add_resource dir - @catalog.add_resource root - reqs = file.autorequire - reqs.length.must == 1 - reqs[0].source.must == dir - reqs[0].target.must == file - end + describe "when using UNC filenames", :'fails_on_ruby_1.9.2' => true do + before :each do + pending("UNC file paths not yet supported") + end - it "should not autorequire anything when there is no nearest ancestor directory" do - file = Puppet::Type::File.new(:path => "//server/foo/bar/baz/qux") - @catalog.add_resource file - file.autorequire.should be_empty - end + it "should autorequire its parent directory" do + file = Puppet::Type::File.new(:path => "//server/foo/bar") + dir = Puppet::Type::File.new(:path => "//server/foo") + @catalog.add_resource file + @catalog.add_resource dir + reqs = file.autorequire + reqs[0].source.must == dir + reqs[0].target.must == file + end - it "should not autorequire its parent dir if its parent dir is itself" do - file = Puppet::Type::File.new(:path => "//server/foo") - @catalog.add_resource file - puts file.autorequire - file.autorequire.should be_empty - end + it "should autorequire its nearest ancestor directory" do + file = Puppet::Type::File.new(:path => "//server/foo/bar/baz/qux") + dir = Puppet::Type::File.new(:path => "//server/foo/bar") + root = Puppet::Type::File.new(:path => "//server/foo") + @catalog.add_resource file + @catalog.add_resource dir + @catalog.add_resource root + reqs = file.autorequire + reqs.length.must == 1 + reqs[0].source.must == dir + reqs[0].target.must == file + end - it "should remove trailing slashes" do - file = Puppet::Type::File.new(:path => "//server/foo/bar/baz/") - file[:path].should == "//server/foo/bar/baz" - end + it "should not autorequire anything when there is no nearest ancestor directory" do + file = Puppet::Type::File.new(:path => "//server/foo/bar/baz/qux") + @catalog.add_resource file + file.autorequire.should be_empty + end - it "should remove double slashes" do - file = Puppet::Type::File.new(:path => "//server/foo/bar//baz") - file[:path].should == "//server/foo/bar/baz" - end + it "should not autorequire its parent dir if its parent dir is itself" do + file = Puppet::Type::File.new(:path => "//server/foo") + @catalog.add_resource file + puts file.autorequire + file.autorequire.should be_empty + end - it "should remove trailing double slashes" do - file = Puppet::Type::File.new(:path => "//server/foo/bar/baz//") - file[:path].should == "//server/foo/bar/baz" - end + it "should remove trailing slashes" do + file = Puppet::Type::File.new(:path => "//server/foo/bar/baz/") + file[:path].should == "//server/foo/bar/baz" + end - it "should remove a trailing slash from a sharename" do - file = Puppet::Type::File.new(:path => "//server/foo/") - file[:path].should == "//server/foo" - end + it "should remove double slashes" do + file = Puppet::Type::File.new(:path => "//server/foo/bar//baz") + file[:path].should == "//server/foo/bar/baz" + end - it "should not modify a sharename" do - file = Puppet::Type::File.new(:path => "//server/foo") - file[:path].should == "//server/foo" - end + it "should remove trailing double slashes" do + file = Puppet::Type::File.new(:path => "//server/foo/bar/baz//") + file[:path].should == "//server/foo/bar/baz" end - describe "on POSIX systems" do - before do - Puppet.features.stubs(:posix?).returns(true) - Puppet.features.stubs(:microsoft_windows?).returns(false) - end + it "should remove a trailing slash from a sharename" do + file = Puppet::Type::File.new(:path => "//server/foo/") + file[:path].should == "//server/foo" + end - it "should refuse to work" do - lambda { Puppet::Type::File.new(:path => "X:/foo/bar") }.should raise_error(Puppet::Error) - end + it "should not modify a sharename" do + file = Puppet::Type::File.new(:path => "//server/foo") + file[:path].should == "//server/foo" end end describe "when initializing" do it "should set a desired 'ensure' value if none is set and 'content' is set" do file = Puppet::Type::File.new(:name => "/my/file", :content => "/foo/bar") file[:ensure].should == :file end it "should set a desired 'ensure' value if none is set and 'target' is set" do file = Puppet::Type::File.new(:name => "/my/file", :target => "/foo/bar") file[:ensure].should == :symlink end end describe "when validating attributes" do %w{path checksum backup recurse recurselimit source replace force ignore links purge sourceselect}.each do |attr| it "should have a '#{attr}' parameter" do Puppet::Type.type(:file).attrtype(attr.intern).should == :param end end %w{content target ensure owner group mode type}.each do |attr| it "should have a '#{attr}' property" do Puppet::Type.type(:file).attrtype(attr.intern).should == :property end end it "should have its 'path' attribute set as its namevar" do Puppet::Type.type(:file).key_attributes.should == [:path] end end describe "when managing links" do require 'tempfile' if @real_posix describe "on POSIX systems" do before do @basedir = tempfile Dir.mkdir(@basedir) @file = File.join(@basedir, "file") @link = File.join(@basedir, "link") File.open(@file, "w", 0644) { |f| f.puts "yayness"; f.flush } File.symlink(@file, @link) @resource = Puppet::Type.type(:file).new(:path => @link, :mode => "755") @catalog.add_resource @resource end after do remove_tmp_files end it "should default to managing the link" do @catalog.apply # I convert them to strings so they display correctly if there's an error. ("%o" % (File.stat(@file).mode & 007777)).should == "%o" % 0644 end it "should be able to follow links" do @resource[:links] = :follow @catalog.apply ("%o" % (File.stat(@file).mode & 007777)).should == "%o" % 0755 end end else # @real_posix # should recode tests using expectations instead of using the filesystem end describe "on Microsoft Windows systems" do before do Puppet.features.stubs(:posix?).returns(false) Puppet.features.stubs(:microsoft_windows?).returns(true) end it "should refuse to work with links" end end it "should be able to retrieve a stat instance for the file it is managing" do Puppet::Type.type(:file).new(:path => "/foo/bar", :source => "/bar/foo").should respond_to(:stat) end describe "when stat'ing its file" do before do @resource = Puppet::Type.type(:file).new(:path => "/foo/bar") @resource[:links] = :manage # so we always use :lstat end it "should use :stat if it is following links" do @resource[:links] = :follow File.expects(:stat) @resource.stat end it "should use :lstat if is it not following links" do @resource[:links] = :manage File.expects(:lstat) @resource.stat end it "should stat the path of the file" do File.expects(:lstat).with("/foo/bar") @resource.stat end # This only happens in testing. it "should return nil if the stat does not exist" do File.expects(:lstat).returns nil @resource.stat.should be_nil end it "should return nil if the file does not exist" do File.expects(:lstat).raises(Errno::ENOENT) @resource.stat.should be_nil end it "should return nil if the file cannot be stat'ed" do File.expects(:lstat).raises(Errno::EACCES) @resource.stat.should be_nil end it "should return the stat instance" do File.expects(:lstat).returns "mystat" @resource.stat.should == "mystat" end it "should cache the stat instance if it has a catalog and is applying" do stat = mock 'stat' File.expects(:lstat).returns stat catalog = Puppet::Resource::Catalog.new @resource.catalog = catalog catalog.stubs(:applying?).returns true @resource.stat.should equal(@resource.stat) end end describe "when flushing" do it "should flush all properties that respond to :flush" do @resource = Puppet::Type.type(:file).new(:path => "/foo/bar", :source => "/bar/foo") @resource.parameter(:source).expects(:flush) @resource.flush end it "should reset its stat reference" do @resource = Puppet::Type.type(:file).new(:path => "/foo/bar") File.expects(:lstat).times(2).returns("stat1").then.returns("stat2") @resource.stat.should == "stat1" @resource.flush @resource.stat.should == "stat2" end end it "should have a method for performing recursion" do @file.must respond_to(:perform_recursion) end describe "when executing a recursive search" do it "should use Metadata to do its recursion" do Puppet::FileServing::Metadata.indirection.expects(:search) @file.perform_recursion(@file[:path]) end it "should use the provided path as the key to the search" do Puppet::FileServing::Metadata.indirection.expects(:search).with { |key, options| key == "/foo" } @file.perform_recursion("/foo") end it "should return the results of the metadata search" do Puppet::FileServing::Metadata.indirection.expects(:search).returns "foobar" @file.perform_recursion(@file[:path]).should == "foobar" end it "should pass its recursion value to the search" do @file[:recurse] = true Puppet::FileServing::Metadata.indirection.expects(:search).with { |key, options| options[:recurse] == true } @file.perform_recursion(@file[:path]) end it "should pass true if recursion is remote" do @file[:recurse] = :remote Puppet::FileServing::Metadata.indirection.expects(:search).with { |key, options| options[:recurse] == true } @file.perform_recursion(@file[:path]) end it "should pass its recursion limit value to the search" do @file[:recurselimit] = 10 Puppet::FileServing::Metadata.indirection.expects(:search).with { |key, options| options[:recurselimit] == 10 } @file.perform_recursion(@file[:path]) end it "should configure the search to ignore or manage links" do @file[:links] = :manage Puppet::FileServing::Metadata.indirection.expects(:search).with { |key, options| options[:links] == :manage } @file.perform_recursion(@file[:path]) end it "should pass its 'ignore' setting to the search if it has one" do @file[:ignore] = %w{.svn CVS} Puppet::FileServing::Metadata.indirection.expects(:search).with { |key, options| options[:ignore] == %w{.svn CVS} } @file.perform_recursion(@file[:path]) end end it "should have a method for performing local recursion" do @file.must respond_to(:recurse_local) end describe "when doing local recursion" do before do @metadata = stub 'metadata', :relative_path => "my/file" end it "should pass its path to the :perform_recursion method" do @file.expects(:perform_recursion).with(@file[:path]).returns [@metadata] @file.stubs(:newchild) @file.recurse_local end it "should return an empty hash if the recursion returns nothing" do @file.expects(:perform_recursion).returns nil @file.recurse_local.should == {} end it "should create a new child resource with each generated metadata instance's relative path" do @file.expects(:perform_recursion).returns [@metadata] @file.expects(:newchild).with(@metadata.relative_path).returns "fiebar" @file.recurse_local end it "should not create a new child resource for the '.' directory" do @metadata.stubs(:relative_path).returns "." @file.expects(:perform_recursion).returns [@metadata] @file.expects(:newchild).never @file.recurse_local end it "should return a hash of the created resources with the relative paths as the hash keys" do @file.expects(:perform_recursion).returns [@metadata] @file.expects(:newchild).with("my/file").returns "fiebar" @file.recurse_local.should == {"my/file" => "fiebar"} end it "should set checksum_type to none if this file checksum is none" do @file[:checksum] = :none Puppet::FileServing::Metadata.indirection.expects(:search).with { |path,params| params[:checksum_type] == :none }.returns [@metadata] @file.expects(:newchild).with("my/file").returns "fiebar" @file.recurse_local end end it "should have a method for performing link recursion" do @file.must respond_to(:recurse_link) end describe "when doing link recursion" do before do @first = stub 'first', :relative_path => "first", :full_path => "/my/first", :ftype => "directory" @second = stub 'second', :relative_path => "second", :full_path => "/my/second", :ftype => "file" @resource = stub 'file', :[]= => nil end it "should pass its target to the :perform_recursion method" do @file[:target] = "mylinks" @file.expects(:perform_recursion).with("mylinks").returns [@first] @file.stubs(:newchild).returns @resource @file.recurse_link({}) end it "should ignore the recursively-found '.' file and configure the top-level file to create a directory" do @first.stubs(:relative_path).returns "." @file[:target] = "mylinks" @file.expects(:perform_recursion).with("mylinks").returns [@first] @file.stubs(:newchild).never @file.expects(:[]=).with(:ensure, :directory) @file.recurse_link({}) end it "should create a new child resource for each generated metadata instance's relative path that doesn't already exist in the children hash" do @file.expects(:perform_recursion).returns [@first, @second] @file.expects(:newchild).with(@first.relative_path).returns @resource @file.recurse_link("second" => @resource) end it "should not create a new child resource for paths that already exist in the children hash" do @file.expects(:perform_recursion).returns [@first] @file.expects(:newchild).never @file.recurse_link("first" => @resource) end it "should set the target to the full path of discovered file and set :ensure to :link if the file is not a directory" do file = stub 'file' file.expects(:[]=).with(:target, "/my/second") file.expects(:[]=).with(:ensure, :link) @file.stubs(:perform_recursion).returns [@first, @second] @file.recurse_link("first" => @resource, "second" => file) end it "should :ensure to :directory if the file is a directory" do file = stub 'file' file.expects(:[]=).with(:ensure, :directory) @file.stubs(:perform_recursion).returns [@first, @second] @file.recurse_link("first" => file, "second" => @resource) end it "should return a hash with both created and existing resources with the relative paths as the hash keys" do file = stub 'file', :[]= => nil @file.expects(:perform_recursion).returns [@first, @second] @file.stubs(:newchild).returns file @file.recurse_link("second" => @resource).should == {"second" => @resource, "first" => file} end end it "should have a method for performing remote recursion" do @file.must respond_to(:recurse_remote) end describe "when doing remote recursion" do before do @file[:source] = "puppet://foo/bar" @first = Puppet::FileServing::Metadata.new("/my", :relative_path => "first") @second = Puppet::FileServing::Metadata.new("/my", :relative_path => "second") @first.stubs(:ftype).returns "directory" @second.stubs(:ftype).returns "directory" @parameter = stub 'property', :metadata= => nil @resource = stub 'file', :[]= => nil, :parameter => @parameter end it "should pass its source to the :perform_recursion method" do data = Puppet::FileServing::Metadata.new("/whatever", :relative_path => "foobar") @file.expects(:perform_recursion).with("puppet://foo/bar").returns [data] @file.stubs(:newchild).returns @resource @file.recurse_remote({}) end it "should not recurse when the remote file is not a directory" do data = Puppet::FileServing::Metadata.new("/whatever", :relative_path => ".") data.stubs(:ftype).returns "file" @file.expects(:perform_recursion).with("puppet://foo/bar").returns [data] @file.expects(:newchild).never @file.recurse_remote({}) end it "should set the source of each returned file to the searched-for URI plus the found relative path" do @first.expects(:source=).with File.join("puppet://foo/bar", @first.relative_path) @file.expects(:perform_recursion).returns [@first] @file.stubs(:newchild).returns @resource @file.recurse_remote({}) end it "should create a new resource for any relative file paths that do not already have a resource" do @file.stubs(:perform_recursion).returns [@first] @file.expects(:newchild).with("first").returns @resource @file.recurse_remote({}).should == {"first" => @resource} end it "should not create a new resource for any relative file paths that do already have a resource" do @file.stubs(:perform_recursion).returns [@first] @file.expects(:newchild).never @file.recurse_remote("first" => @resource) end it "should set the source of each resource to the source of the metadata" do @file.stubs(:perform_recursion).returns [@first] @resource.stubs(:[]=) @resource.expects(:[]=).with(:source, File.join("puppet://foo/bar", @first.relative_path)) @file.recurse_remote("first" => @resource) end # LAK:FIXME This is a bug, but I can't think of a fix for it. Fortunately it's already # filed, and when it's fixed, we'll just fix the whole flow. it "should set the checksum type to :md5 if the remote file is a file" do @first.stubs(:ftype).returns "file" @file.stubs(:perform_recursion).returns [@first] @resource.stubs(:[]=) @resource.expects(:[]=).with(:checksum, :md5) @file.recurse_remote("first" => @resource) end it "should store the metadata in the source property for each resource so the source does not have to requery the metadata" do @file.stubs(:perform_recursion).returns [@first] @resource.expects(:parameter).with(:source).returns @parameter @parameter.expects(:metadata=).with(@first) @file.recurse_remote("first" => @resource) end it "should not create a new resource for the '.' file" do @first.stubs(:relative_path).returns "." @file.stubs(:perform_recursion).returns [@first] @file.expects(:newchild).never @file.recurse_remote({}) end it "should store the metadata in the main file's source property if the relative path is '.'" do @first.stubs(:relative_path).returns "." @file.stubs(:perform_recursion).returns [@first] @file.parameter(:source).expects(:metadata=).with @first @file.recurse_remote("first" => @resource) end describe "and multiple sources are provided" do describe "and :sourceselect is set to :first" do it "should create file instances for the results for the first source to return any values" do data = Puppet::FileServing::Metadata.new("/whatever", :relative_path => "foobar") @file[:source] = %w{/one /two /three /four} @file.expects(:perform_recursion).with("/one").returns nil @file.expects(:perform_recursion).with("/two").returns [] @file.expects(:perform_recursion).with("/three").returns [data] @file.expects(:perform_recursion).with("/four").never @file.expects(:newchild).with("foobar").returns @resource @file.recurse_remote({}) end end describe "and :sourceselect is set to :all" do before do @file[:sourceselect] = :all end it "should return every found file that is not in a previous source" do klass = Puppet::FileServing::Metadata @file[:source] = %w{/one /two /three /four} @file.stubs(:newchild).returns @resource one = [klass.new("/one", :relative_path => "a")] @file.expects(:perform_recursion).with("/one").returns one @file.expects(:newchild).with("a").returns @resource two = [klass.new("/two", :relative_path => "a"), klass.new("/two", :relative_path => "b")] @file.expects(:perform_recursion).with("/two").returns two @file.expects(:newchild).with("b").returns @resource three = [klass.new("/three", :relative_path => "a"), klass.new("/three", :relative_path => "c")] @file.expects(:perform_recursion).with("/three").returns three @file.expects(:newchild).with("c").returns @resource @file.expects(:perform_recursion).with("/four").returns [] @file.recurse_remote({}) end end end end describe "when specifying both source, and content properties" do before do @file[:source] = '/one' @file[:content] = 'file contents' end it "should raise an exception" do lambda {@file.validate }.should raise_error(/You cannot specify more than one of/) end end describe "when using source" do before do @file[:source] = '/one' end Puppet::Type::File::ParameterChecksum.value_collection.values.reject {|v| v == :none}.each do |checksum_type| describe "with checksum '#{checksum_type}'" do before do @file[:checksum] = checksum_type end it 'should validate' do lambda { @file.validate }.should_not raise_error end end end describe "with checksum 'none'" do before do @file[:checksum] = :none end it 'should raise an exception when validating' do lambda { @file.validate }.should raise_error(/You cannot specify source when using checksum 'none'/) end end end describe "when using content" do before do @file[:content] = 'file contents' end (Puppet::Type::File::ParameterChecksum.value_collection.values - SOURCE_ONLY_CHECKSUMS).each do |checksum_type| describe "with checksum '#{checksum_type}'" do before do @file[:checksum] = checksum_type end it 'should validate' do lambda { @file.validate }.should_not raise_error end end end SOURCE_ONLY_CHECKSUMS.each do |checksum_type| describe "with checksum '#{checksum_type}'" do it 'should raise an exception when validating' do @file[:checksum] = checksum_type lambda { @file.validate }.should raise_error(/You cannot specify content when using checksum '#{checksum_type}'/) end end end end describe "when returning resources with :eval_generate" do before do @graph = stub 'graph', :add_edge => nil @catalog.stubs(:relationship_graph).returns @graph @file.catalog = @catalog @file[:recurse] = true end it "should recurse if recursion is enabled" do resource = stub('resource', :[] => "resource") @file.expects(:recurse?).returns true @file.expects(:recurse).returns [resource] @file.eval_generate.should == [resource] end it "should not recurse if recursion is disabled" do @file.expects(:recurse?).returns false @file.expects(:recurse).never @file.eval_generate.should == [] end it "should return each resource found through recursion" do foo = stub 'foo', :[] => "/foo" bar = stub 'bar', :[] => "/bar" bar2 = stub 'bar2', :[] => "/bar" @file.expects(:recurse).returns [foo, bar] @file.eval_generate.should == [foo, bar] end end describe "when recursing" do before do @file[:recurse] = true @metadata = Puppet::FileServing::Metadata end describe "and a source is set" do before { @file[:source] = "/my/source" } it "should pass the already-discovered resources to recurse_remote" do @file.stubs(:recurse_local).returns(:foo => "bar") @file.expects(:recurse_remote).with(:foo => "bar").returns [] @file.recurse end end describe "and a target is set" do before { @file[:target] = "/link/target" } it "should use recurse_link" do @file.stubs(:recurse_local).returns(:foo => "bar") @file.expects(:recurse_link).with(:foo => "bar").returns [] @file.recurse end end it "should use recurse_local if recurse is not remote" do @file.expects(:recurse_local).returns({}) @file.recurse end it "should not use recurse_local if recurse remote" do @file[:recurse] = :remote @file.expects(:recurse_local).never @file.recurse end it "should return the generated resources as an array sorted by file path" do one = stub 'one', :[] => "/one" two = stub 'two', :[] => "/one/two" three = stub 'three', :[] => "/three" @file.expects(:recurse_local).returns(:one => one, :two => two, :three => three) @file.recurse.should == [one, two, three] end describe "and purging is enabled" do before do @file[:purge] = true end it "should configure each file to be removed" do local = stub 'local' local.stubs(:[]).with(:source).returns nil # Thus, a local file local.stubs(:[]).with(:path).returns "foo" @file.expects(:recurse_local).returns("local" => local) local.expects(:[]=).with(:ensure, :absent) @file.recurse end it "should not remove files that exist in the remote repository" do @file["source"] = "/my/file" @file.expects(:recurse_local).returns({}) remote = stub 'remote' remote.stubs(:[]).with(:source).returns "/whatever" # Thus, a remote file remote.stubs(:[]).with(:path).returns "foo" @file.expects(:recurse_remote).with { |hash| hash["remote"] = remote } remote.expects(:[]=).with(:ensure, :absent).never @file.recurse end end describe "and making a new child resource" do it "should not copy the parent resource's parent" do Puppet::Type.type(:file).expects(:new).with { |options| ! options.include?(:parent) } @file.newchild("my/path") end {:recurse => true, :target => "/foo/bar", :ensure => :present, :alias => "yay", :source => "/foo/bar"}.each do |param, value| it "should not pass on #{param} to the sub resource" do @file = Puppet::Type::File.new(:name => @path, param => value, :catalog => @catalog) @file.class.expects(:new).with { |params| params[param].nil? } @file.newchild("sub/file") end end it "should copy all of the parent resource's 'should' values that were set at initialization" do file = @file.class.new(:path => "/foo/bar", :owner => "root", :group => "wheel") @catalog.add_resource(file) file.class.expects(:new).with { |options| options[:owner] == "root" and options[:group] == "wheel" } file.newchild("my/path") end it "should not copy default values to the new child" do @file.class.expects(:new).with { |params| params[:backup].nil? } @file.newchild("my/path") end it "should not copy values to the child which were set by the source" do @file[:source] = "/foo/bar" metadata = stub 'metadata', :owner => "root", :group => "root", :mode => 0755, :ftype => "file", :checksum => "{md5}whatever" @file.parameter(:source).stubs(:metadata).returns metadata @file.parameter(:source).copy_source_values @file.class.expects(:new).with { |params| params[:group].nil? } @file.newchild("my/path") end end end describe "when setting the backup" do it "should default to 'puppet'" do Puppet::Type::File.new(:name => "/my/file")[:backup].should == "puppet" end it "should allow setting backup to 'false'" do (!Puppet::Type::File.new(:name => "/my/file", :backup => false)[:backup]).should be_true end it "should set the backup to '.puppet-bak' if it is set to true" do Puppet::Type::File.new(:name => "/my/file", :backup => true)[:backup].should == ".puppet-bak" end it "should support any other backup extension" do Puppet::Type::File.new(:name => "/my/file", :backup => ".bak")[:backup].should == ".bak" end it "should set the filebucket when backup is set to a string matching the name of a filebucket in the catalog" do catalog = Puppet::Resource::Catalog.new bucket_resource = Puppet::Type.type(:filebucket).new :name => "foo", :path => "/my/file/bucket" catalog.add_resource bucket_resource file = Puppet::Type::File.new(:name => "/my/file") catalog.add_resource file file[:backup] = "foo" file.bucket.should == bucket_resource.bucket end it "should find filebuckets added to the catalog after the file resource was created" do catalog = Puppet::Resource::Catalog.new file = Puppet::Type::File.new(:name => "/my/file", :backup => "foo") catalog.add_resource file bucket_resource = Puppet::Type.type(:filebucket).new :name => "foo", :path => "/my/file/bucket" catalog.add_resource bucket_resource file.bucket.should == bucket_resource.bucket end it "should have a nil filebucket if backup is false" do catalog = Puppet::Resource::Catalog.new bucket_resource = Puppet::Type.type(:filebucket).new :name => "foo", :path => "/my/file/bucket" catalog.add_resource bucket_resource file = Puppet::Type::File.new(:name => "/my/file", :backup => false) catalog.add_resource file file.bucket.should be_nil end it "should have a nil filebucket if backup is set to a string starting with '.'" do catalog = Puppet::Resource::Catalog.new bucket_resource = Puppet::Type.type(:filebucket).new :name => "foo", :path => "/my/file/bucket" catalog.add_resource bucket_resource file = Puppet::Type::File.new(:name => "/my/file", :backup => ".foo") catalog.add_resource file file.bucket.should be_nil end it "should fail if there's no catalog and backup is not false" do file = Puppet::Type::File.new(:name => "/my/file", :backup => "foo") lambda { file.bucket }.should raise_error(Puppet::Error) end it "should fail if a non-existent catalog is specified" do file = Puppet::Type::File.new(:name => "/my/file", :backup => "foo") catalog = Puppet::Resource::Catalog.new catalog.add_resource file lambda { file.bucket }.should raise_error(Puppet::Error) end it "should be able to use the default filebucket without a catalog" do file = Puppet::Type::File.new(:name => "/my/file", :backup => "puppet") file.bucket.should be_instance_of(Puppet::FileBucket::Dipper) end it "should look up the filebucket during finish()" do file = Puppet::Type::File.new(:name => "/my/file", :backup => ".foo") file.expects(:bucket) file.finish end end describe "when retrieving the current file state" do it "should copy the source values if the 'source' parameter is set" do file = Puppet::Type::File.new(:name => "/my/file", :source => "/foo/bar") file.parameter(:source).expects(:copy_source_values) file.retrieve end end describe ".title_patterns" do before do @type_class = Puppet::Type.type(:file) end it "should have a regexp that captures the entire string, except for a terminating slash" do patterns = @type_class.title_patterns string = "abc/\n\tdef/" patterns[0][0] =~ string $1.should == "abc/\n\tdef" end end describe "when auditing" do it "should not fail if creating a new file if group is not set" do File.exists?(@path).should == false file = Puppet::Type::File.new(:name => @path, :audit => "all", :content => "content") catalog = Puppet::Resource::Catalog.new catalog.add_resource(file) Puppet::Util::Storage.stubs(:store) # to prevent the catalog from trying to write state.yaml transaction = catalog.apply transaction.report.resource_statuses["File[#{@path}]"].failed.should == false File.exists?(@path).should == true end it "should not log errors if creating a new file with ensure present and no content" do File.exists?(@path).should == false file = Puppet::Type::File.new(:name => @path, :audit => "content", :ensure => "present") catalog = Puppet::Resource::Catalog.new catalog.add_resource(file) Puppet::Util::Storage.stubs(:store) # to prevent the catalog from trying to write state.yaml catalog.apply @logs.reject {|l| l.level == :notice }.should be_empty end end describe "when specifying both source and checksum" do it 'should use the specified checksum when source is first' do @file[:source] = '/foo' @file[:checksum] = :md5lite @file[:checksum].should be :md5lite end it 'should use the specified checksum when source is last' do @file[:checksum] = :md5lite @file[:source] = '/foo' @file[:checksum].should be :md5lite end end end