diff --git a/conf/auth.conf b/conf/auth.conf index cb202a989..ba389c076 100644 --- a/conf/auth.conf +++ b/conf/auth.conf @@ -1,99 +1,100 @@ # This is an example auth.conf file, it mimics the puppetmasterd defaults # # The ACL are checked in order of appearance in this file. # # Supported syntax: # This file supports two different syntax depending on how # you want to express the ACL. # # Path syntax (the one used below): # --------------------------------- # path /path/to/resource # [environment envlist] # [method methodlist] # [auth[enthicated] {yes|no|on|off|any}] # allow [host|ip|*] # deny [host|ip] # # The path is matched as a prefix. That is /file match at # the same time /file_metadat and /file_content. # # Regex syntax: # ------------- # This one is differenciated from the path one by a '~' # # path ~ regex # [environment envlist] # [method methodlist] # [auth[enthicated] {yes|no|on|off|any}] # allow [host|ip|*] # deny [host|ip] # # The regex syntax is the same as ruby ones. # # Ex: # path ~ .pp$ # will match every resource ending in .pp (manifests files for instance) # # path ~ ^/path/to/resource # is essentially equivalent to path /path/to/resource # # environment:: restrict an ACL to a specific set of environments # method:: restrict an ACL to a specific set of methods # auth:: restrict an ACL to an authenticated or unauthenticated request # the default when unspecified is to restrict the ACL to authenticated requests # (ie exactly as if auth yes was present). # ### Authenticated ACL - those applies only when the client ### has a valid certificate and is thus authenticated # allow nodes to retrieve their own catalog (ie their configuration) path ~ ^/catalog/([^/]+)$ method find allow $1 # allow nodes to retrieve their own node definition path ~ ^/node/([^/]+)$ method find allow $1 # allow all nodes to access the certificates services path /certificate_revocation_list/ca method find allow * # allow all nodes to store their reports path /report method save allow * # inconditionnally allow access to all files services # which means in practice that fileserver.conf will # still be used path /file allow * ### Unauthenticated ACL, for clients for which the current master doesn't -### have a valid certificate +### have a valid certificate; we allow authenticated users, too, because +### there isn't a great harm in letting that request through. # allow access to the master CA path /certificate/ca -auth no +auth any method find allow * path /certificate/ -auth no +auth any method find allow * path /certificate_request -auth no +auth any method find, save allow * # this one is not stricly necessary, but it has the merit # to show the default policy which is deny everything else path / auth any diff --git a/lib/puppet/network/rest_authconfig.rb b/lib/puppet/network/rest_authconfig.rb index 7dcc81ef4..80167103b 100644 --- a/lib/puppet/network/rest_authconfig.rb +++ b/lib/puppet/network/rest_authconfig.rb @@ -1,94 +1,102 @@ require 'puppet/network/authconfig' module Puppet class Network::RestAuthConfig < Network::AuthConfig extend MonitorMixin attr_accessor :rights DEFAULT_ACL = [ { :acl => "~ ^\/catalog\/([^\/]+)$", :method => :find, :allow => '$1', :authenticated => true }, { :acl => "~ ^\/node\/([^\/]+)$", :method => :find, :allow => '$1', :authenticated => true }, # this one will allow all file access, and thus delegate # to fileserver.conf { :acl => "/file" }, { :acl => "/certificate_revocation_list/ca", :method => :find, :authenticated => true }, { :acl => "/report", :method => :save, :authenticated => true }, - { :acl => "/certificate/ca", :method => :find, :authenticated => false }, - { :acl => "/certificate/", :method => :find, :authenticated => false }, - { :acl => "/certificate_request", :method => [:find, :save], :authenticated => false }, + # These allow `auth any`, because if you can do them anonymously you + # should probably also be able to do them when trusted. + { :acl => "/certificate/ca", :method => :find, :authenticated => :any }, + { :acl => "/certificate/", :method => :find, :authenticated => :any }, + { :acl => "/certificate_request", :method => [:find, :save], :authenticated => :any }, { :acl => "/status", :method => [:find], :authenticated => true }, ] def self.main synchronize do add_acl = @main.nil? super @main.insert_default_acl if add_acl and !@main.exists? end @main end def allowed?(request) Puppet.deprecation_warning "allowed? should not be called for REST authorization - use check_authorization instead" check_authorization(request) end # check wether this request is allowed in our ACL # raise an Puppet::Network::AuthorizedError if the request # is denied. def check_authorization(indirection, method, key, params) read # we're splitting the request in part because # fail_on_deny could as well be called in the XMLRPC context # with a ClientRequest. if authorization_failure_exception = @rights.is_request_forbidden_and_why?(indirection, method, key, params) Puppet.warning("Denying access: #{authorization_failure_exception}") raise authorization_failure_exception end end def initialize(file = nil, parsenow = true) super(file || Puppet[:rest_authconfig], parsenow) # if we didn't read a file (ie it doesn't exist) # make sure we can create some default rights @rights ||= Puppet::Network::Rights.new end def parse super() insert_default_acl end # force regular ACLs to be present def insert_default_acl + if exists? then + reason = "none were found in '#{@file}'" + else + reason = "#{Puppet[:rest_authconfig]} doesn't exist" + end + DEFAULT_ACL.each do |acl| unless rights[acl[:acl]] - Puppet.info "Inserting default '#{acl[:acl]}'(#{acl[:authenticated] ? "auth" : "non-auth"}) ACL because #{( !exists? ? "#{Puppet[:rest_authconfig]} doesn't exist" : "none were found in '#{@file}'")}" + Puppet.info "Inserting default '#{acl[:acl]}' (auth #{acl[:authenticated]}) ACL because #{reason}" mk_acl(acl) end end # queue an empty (ie deny all) right for every other path # actually this is not strictly necessary as the rights system # denies not explicitely allowed paths unless rights["/"] rights.newright("/") rights.restrict_authenticated("/", :any) end end def mk_acl(acl) @rights.newright(acl[:acl]) @rights.allow(acl[:acl], acl[:allow] || "*") if method = acl[:method] method = [method] unless method.is_a?(Array) method.each { |m| @rights.restrict_method(acl[:acl], m) } end @rights.restrict_authenticated(acl[:acl], acl[:authenticated]) unless acl[:authenticated].nil? end end end diff --git a/lib/puppet/provider/augeas/augeas.rb b/lib/puppet/provider/augeas/augeas.rb index eb9c69ac8..7214a8b33 100644 --- a/lib/puppet/provider/augeas/augeas.rb +++ b/lib/puppet/provider/augeas/augeas.rb @@ -1,410 +1,410 @@ # # Copyright 2011 Bryan Kearney # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. require 'augeas' if Puppet.features.augeas? require 'strscan' require 'puppet/util' require 'puppet/util/diff' Puppet::Type.type(:augeas).provide(:augeas) do include Puppet::Util include Puppet::Util::Diff confine :true => Puppet.features.augeas? has_features :parse_commands, :need_to_run?,:execute_changes SAVE_NOOP = "noop" SAVE_OVERWRITE = "overwrite" SAVE_NEWFILE = "newfile" SAVE_BACKUP = "backup" COMMANDS = { "set" => [ :path, :string ], "setm" => [ :path, :string, :string ], "rm" => [ :path ], "clear" => [ :path ], "mv" => [ :path, :path ], "insert" => [ :string, :string, :path ], "get" => [ :path, :comparator, :string ], "defvar" => [ :string, :path ], "defnode" => [ :string, :path, :string ], "match" => [ :path, :glob ], "size" => [:comparator, :int], "include" => [:string], "not_include" => [:string], "==" => [:glob], "!=" => [:glob] } COMMANDS["ins"] = COMMANDS["insert"] COMMANDS["remove"] = COMMANDS["rm"] COMMANDS["move"] = COMMANDS["mv"] attr_accessor :aug # Extracts an 2 dimensional array of commands which are in the # form of command path value. # The input can be # - A string with one command # - A string with many commands per line # - An array of strings. def parse_commands(data) context = resource[:context] # Add a trailing / if it is not there if (context.length > 0) context << "/" if context[-1, 1] != "/" end data = data.split($/) if data.is_a?(String) data = data.flatten args = [] data.each do |line| line.strip! next if line.nil? || line.empty? argline = [] sc = StringScanner.new(line) cmd = sc.scan(/\w+|==|!=/) formals = COMMANDS[cmd] fail("Unknown command #{cmd}") unless formals argline << cmd narg = 0 formals.each do |f| sc.skip(/\s+/) narg += 1 if f == :path start = sc.pos nbracket = 0 inSingleTick = false inDoubleTick = false begin sc.skip(/([^\]\[\s\\'"]|\\.)+/) ch = sc.getch nbracket += 1 if ch == "[" nbracket -= 1 if ch == "]" inSingleTick = !inSingleTick if ch == "'" inDoubleTick = !inDoubleTick if ch == "\"" fail("unmatched [") if nbracket < 0 end until ((nbracket == 0 && !inSingleTick && !inDoubleTick && (ch =~ /\s/)) || sc.eos?) len = sc.pos - start len -= 1 unless sc.eos? unless p = sc.string[start, len] fail("missing path argument #{narg} for #{cmd}") end # Rip off any ticks if they are there. p = p[1, (p.size - 2)] if p[0,1] == "'" || p[0,1] == "\"" p.chomp!("/") if p[0,1] != '$' && p[0,1] != "/" argline << context + p else argline << p end elsif f == :string delim = sc.peek(1) if delim == "'" || delim == "\"" sc.getch argline << sc.scan(/([^\\#{delim}]|(\\.))*/) sc.getch else argline << sc.scan(/[^\s]+/) end fail("missing string argument #{narg} for #{cmd}") unless argline[-1] elsif f == :comparator argline << sc.scan(/(==|!=|=~|<|<=|>|>=)/) unless argline[-1] puts sc.rest fail("invalid comparator for command #{cmd}") end elsif f == :int argline << sc.scan(/\d+/).to_i elsif f== :glob argline << sc.rest end end args << argline end args end def open_augeas unless @aug flags = Augeas::NONE flags = Augeas::TYPE_CHECK if resource[:type_check] == :true flags |= Augeas::NO_MODL_AUTOLOAD if resource[:incl] root = resource[:root] load_path = resource[:load_path] debug("Opening augeas with root #{root}, lens path #{load_path}, flags #{flags}") @aug = Augeas::open(root, load_path,flags) debug("Augeas version #{get_augeas_version} is installed") if get_augeas_version >= "0.3.6" if resource[:incl] aug.set("/augeas/load/Xfm/lens", resource[:lens]) aug.set("/augeas/load/Xfm/incl", resource[:incl]) aug.load end end @aug end def close_augeas if @aug @aug.close debug("Closed the augeas connection") @aug = nil end end # Used by the need_to_run? method to process get filters. Returns # true if there is a match, false if otherwise # Assumes a syntax of get /files/path [COMPARATOR] value def process_get(cmd_array) return_value = false #validate and tear apart the command fail ("Invalid command: #{cmd_array.join(" ")}") if cmd_array.length < 4 cmd = cmd_array.shift path = cmd_array.shift comparator = cmd_array.shift arg = cmd_array.join(" ") #check the value in augeas result = @aug.get(path) || '' case comparator when "!=" return_value = (result != arg) when "=~" regex = Regexp.new(arg) return_value = (result =~ regex) else return_value = (result.send(comparator, arg)) end !!return_value end # Used by the need_to_run? method to process match filters. Returns # true if there is a match, false if otherwise def process_match(cmd_array) return_value = false #validate and tear apart the command fail("Invalid command: #{cmd_array.join(" ")}") if cmd_array.length < 3 cmd = cmd_array.shift path = cmd_array.shift # Need to break apart the clause clause_array = parse_commands(cmd_array.shift)[0] verb = clause_array.shift #Get the values from augeas result = @aug.match(path) || [] fail("Error trying to match path '#{path}'") if (result == -1) # Now do the work case verb when "size" fail("Invalid command: #{cmd_array.join(" ")}") if clause_array.length != 2 comparator = clause_array.shift arg = clause_array.shift case comparator when "!=" return_value = !(result.size.send(:==, arg)) else return_value = (result.size.send(comparator, arg)) end when "include" arg = clause_array.shift return_value = result.include?(arg) when "not_include" arg = clause_array.shift return_value = !result.include?(arg) when "==" begin arg = clause_array.shift new_array = eval arg return_value = (result == new_array) rescue fail("Invalid array in command: #{cmd_array.join(" ")}") end when "!=" begin arg = clause_array.shift new_array = eval arg return_value = (result != new_array) rescue fail("Invalid array in command: #{cmd_array.join(" ")}") end end !!return_value end def get_augeas_version @aug.get("/augeas/version") || "" end def set_augeas_save_mode(mode) @aug.set("/augeas/save", mode) end # Determines if augeas acutally needs to run. def need_to_run? force = resource[:force] return_value = true begin open_augeas filter = resource[:onlyif] unless filter == "" cmd_array = parse_commands(filter)[0] command = cmd_array[0]; begin case command when "get"; return_value = process_get(cmd_array) when "match"; return_value = process_match(cmd_array) end rescue SystemExit,NoMemoryError raise rescue Exception => e fail("Error sending command '#{command}' with params #{cmd_array[1..-1].inspect}/#{e.message}") end end unless force # If we have a verison of augeas which is at least 0.3.6 then we # can make the changes now, see if changes were made, and # actually do the save. if return_value and get_augeas_version >= "0.3.6" debug("Will attempt to save and only run if files changed") set_augeas_save_mode(SAVE_NEWFILE) do_execute_changes save_result = @aug.save fail("Save failed with return code #{save_result}") unless save_result saved_files = @aug.match("/augeas/events/saved") if saved_files.size > 0 root = resource[:root].sub(/^\/$/, "") saved_files.each do |key| saved_file = @aug.get(key).to_s.sub(/^\/files/, root) if Puppet[:show_diff] - print diff(saved_file, saved_file + ".augnew") + notice "\n" + diff(saved_file, saved_file + ".augnew") end if resource.noop? File.delete(saved_file + ".augnew") end end debug("Files changed, should execute") return_value = true else debug("Skipping because no files were changed") return_value = false end end end ensure if not return_value or resource.noop? or not save_result close_augeas end end return_value end def execute_changes # Re-connect to augeas, and re-execute the changes begin open_augeas saved_files = @aug.match("/augeas/events/saved") if saved_files saved_files.each do |key| root = resource[:root].sub(/^\/$/, "") saved_file = @aug.get(key).to_s.sub(/^\/files/, root) if File.exists?(saved_file + ".augnew") success = File.rename(saved_file + ".augnew", saved_file) debug(saved_file + ".augnew moved to " + saved_file) fail("Rename failed with return code #{success}") if success != 0 end end else debug("No saved files, re-executing augeas") set_augeas_save_mode(SAVE_OVERWRITE) if get_augeas_version >= "0.3.6" do_execute_changes success = @aug.save fail("Save failed with return code #{success}") if success != true end ensure close_augeas end :executed end # Actually execute the augeas changes. def do_execute_changes commands = parse_commands(resource[:changes]) commands.each do |cmd_array| fail("invalid command #{cmd_array.join[" "]}") if cmd_array.length < 2 command = cmd_array[0] cmd_array.shift begin case command when "set" debug("sending command '#{command}' with params #{cmd_array.inspect}") rv = aug.set(cmd_array[0], cmd_array[1]) fail("Error sending command '#{command}' with params #{cmd_array.inspect}") if (!rv) when "setm" debug("sending command '#{command}' with params #{cmd_array.inspect}") rv = aug.setm(cmd_array[0], cmd_array[1], cmd_array[2]) fail("Error sending command '#{command}' with params #{cmd_array.inspect}") if (rv == -1) when "rm", "remove" debug("sending command '#{command}' with params #{cmd_array.inspect}") rv = aug.rm(cmd_array[0]) fail("Error sending command '#{command}' with params #{cmd_array.inspect}") if (rv == -1) when "clear" debug("sending command '#{command}' with params #{cmd_array.inspect}") rv = aug.clear(cmd_array[0]) fail("Error sending command '#{command}' with params #{cmd_array.inspect}") if (!rv) when "insert", "ins" label = cmd_array[0] where = cmd_array[1] path = cmd_array[2] case where when "before"; before = true when "after"; before = false else fail("Invalid value '#{where}' for where param") end debug("sending command '#{command}' with params #{[label, where, path].inspect}") rv = aug.insert(path, label, before) fail("Error sending command '#{command}' with params #{cmd_array.inspect}") if (rv == -1) when "defvar" debug("sending command '#{command}' with params #{cmd_array.inspect}") rv = aug.defvar(cmd_array[0], cmd_array[1]) fail("Error sending command '#{command}' with params #{cmd_array.inspect}") if (!rv) when "defnode" debug("sending command '#{command}' with params #{cmd_array.inspect}") rv = aug.defnode(cmd_array[0], cmd_array[1], cmd_array[2]) fail("Error sending command '#{command}' with params #{cmd_array.inspect}") if (!rv) when "mv", "move" debug("sending command '#{command}' with params #{cmd_array.inspect}") rv = aug.mv(cmd_array[0], cmd_array[1]) fail("Error sending command '#{command}' with params #{cmd_array.inspect}") if (rv == -1) else fail("Command '#{command}' is not supported") end rescue SystemExit,NoMemoryError raise rescue Exception => e fail("Error sending command '#{command}' with params #{cmd_array.inspect}/#{e.message}") end end end end diff --git a/lib/puppet/type/file.rb b/lib/puppet/type/file.rb index 282ebf418..20c774abf 100644 --- a/lib/puppet/type/file.rb +++ b/lib/puppet/type/file.rb @@ -1,812 +1,812 @@ require 'digest/md5' require 'cgi' require 'etc' require 'uri' require 'fileutils' require 'enumerator' require 'pathname' require 'puppet/network/handler' require 'puppet/util/diff' require 'puppet/util/checksums' 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| unless Puppet::Util.absolute_path?(value) fail Puppet::Error, "File paths must be fully qualified, not '#{value}'" end end # convert the current path in an index into the collection and the last # path name. The aim is to use less storage for all common paths in a hierarchy munge do |value| # We know the value is absolute, so expanding it will just standardize it. path, name = ::File.split(::File.expand_path(value)) { :index => Puppet::FileCollection.collection.index(path), :name => name } end # and the reverse unmunge do |value| basedir = Puppet::FileCollection.collection.path(value[:index]) ::File.join( basedir, value[:name] ) end end newparam(:backup) do desc "Whether files should be backed up before being replaced. The preferred method of backing files up is via a `filebucket`, which stores files by their MD5 sums and allows easy retrieval without littering directories with backups. You can specify a local filebucket or a network-accessible server-based filebucket by setting `backup => bucket-name`. Alternatively, if you specify any value that begins with a `.` (e.g., `.puppet-bak`), then Puppet will use copy the file in the same directory with that value as the extension of the backup. Setting `backup => false` disables all backups of the file in question. Puppet automatically creates a local filebucket named `puppet` and defaults to backing up there. To use a server-based filebucket, you must specify one in your configuration. filebucket { main: server => puppet, path => false, # The path => false line works around a known issue with the filebucket type. } The `puppet master` daemon creates a filebucket by default, so you can usually back up to your main server with this configuration. Once you've described the bucket in your configuration, you can use it in any file's backup attribute: file { \"/my/file\": source => \"/path/in/nfs/or/something\", backup => main } This will back the file up to the central server. At this point, the benefits of using a central filebucket are that you do not have backup files lying around on each of your machines, a given version of a file is only backed up once, you can restore any given file manually (no matter how old), and you can use Puppet Dashboard to view file contents. Eventually, transactional support will be able to automatically restore filebucketed files. " defaultto "puppet" munge do |value| # I don't really know how this is happening. value = value.shift if value.is_a?(Array) case value when false, "false", :false false when true, "true", ".puppet-bak", :true ".puppet-bak" when String value else self.fail "Invalid backup type #{value.inspect}" end end end newparam(:recurse) do desc "Whether and how deeply to do recursive management. Options are: * `inf,true` --- Regular style recursion on both remote and local directory structure. * `remote` --- Descends recursively into the remote directory but not the local directory. Allows copying of a few files into a directory containing many unmanaged files without scanning all the local files. * `false` --- Default of no recursion. * `[0-9]+` --- Same as true, but limit recursion. Warning: this syntax has been deprecated in favor of the `recurselimit` attribute. " newvalues(:true, :false, :inf, :remote, /^[0-9]+$/) # Replace the validation so that we allow numbers in # addition to string representations of them. validate { |arg| } munge do |value| newval = super(value) case newval when :true, :inf; true when :false; false when :remote; :remote when Integer, Fixnum, Bignum self.warning "Setting recursion depth with the recurse parameter is now deprecated, please use recurselimit" # recurse == 0 means no recursion return false if value == 0 resource[:recurselimit] = value true when /^\d+$/ self.warning "Setting recursion depth with the recurse parameter is now deprecated, please use recurselimit" value = Integer(value) # recurse == 0 means no recursion return false if value == 0 resource[:recurselimit] = value true else self.fail "Invalid recurse value #{value.inspect}" end end end newparam(:recurselimit) do desc "How deeply to do recursive management." newvalues(/^[0-9]+$/) munge do |value| newval = super(value) case newval when Integer, Fixnum, Bignum; value when /^\d+$/; Integer(value) else self.fail "Invalid recurselimit value #{value.inspect}" end end end newparam(:replace, :boolean => true) do desc "Whether or not to replace a file that is sourced but exists. This is useful for using file sources purely for initialization." newvalues(:true, :false) aliasvalue(:yes, :true) aliasvalue(:no, :false) defaultto :true end newparam(:force, :boolean => true) do desc "Force the file operation. Currently only used when replacing directories with links." newvalues(:true, :false) defaultto false end newparam(:ignore) do desc "A parameter which omits action on files matching specified patterns during recursion. Uses Ruby's builtin globbing engine, so shell metacharacters are fully supported, e.g. `[a-z]*`. Matches that would descend into the directory structure are ignored, e.g., `*/*`." validate do |value| unless value.is_a?(Array) or value.is_a?(String) or value == false self.devfail "Ignore must be a string or an Array" end end end newparam(:links) do desc "How to handle links during file actions. During file copying, `follow` will copy the target file instead of the link, `manage` will copy the link itself, and `ignore` will just pass it by. When not copying, `manage` and `ignore` behave equivalently (because you cannot really ignore links entirely during local recursion), and `follow` will manage the file to which the link points." newvalues(:follow, :manage) defaultto :manage end newparam(:purge, :boolean => true) do desc "Whether unmanaged files should be purged. If you have a filebucket configured the purged files will be uploaded, but if you do not, this will destroy data. Only use this option for generated files unless you really know what you are doing. This option only makes sense when recursively managing directories. Note that when using `purge` with `source`, Puppet will purge any files that are not on the remote system." defaultto :false newvalues(:true, :false) end newparam(:sourceselect) do desc "Whether to copy all valid sources, or just the first one. This parameter is only used in recursive copies; by default, the first valid source is the only one used as a recursive source, but if this parameter is set to `all`, then all valid sources will have all of their contents copied to the local host, and for sources that have the same file, the source earlier in the list will be used." defaultto :first newvalues(:first, :all) end # Autorequire the nearest ancestor directory found in the catalog. autorequire(:file) do - path = Pathname(self[:path]) + path = Pathname.new(self[:path]) if !path.root? # Start at our parent, to avoid autorequiring ourself parents = path.parent.enum_for(:ascend) found = parents.find { |p| catalog.resource(:file, p.to_s) } found and found.to_s 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] provider.validate if provider.respond_to?(:validate) end def self.[](path) return nil unless path super(path.gsub(/\/+/, '/').sub(/\/$/, '')) end def self.instances return [] end # Determine the user to write files as. def asuser if self.should(:owner) and ! self.should(:owner).is_a?(Symbol) writeable = Puppet::Util::SUIDManager.asuser(self.should(:owner)) { FileTest.writable?(::File.dirname(self[:path])) } # If the parent directory is writeable, then we execute # as the user in question. Otherwise we'll rely on # the 'owner' property to do things. asuser = self.should(:owner) if writeable end asuser end def bucket return @bucket if @bucket backup = self[:backup] return nil unless backup return nil if backup =~ /^\./ unless catalog or backup == "puppet" fail "Can not find filebucket for backups without a catalog" end unless catalog and filebucket = catalog.resource(:filebucket, backup) or backup == "puppet" fail "Could not find filebucket #{backup} specified in backup" end return default_bucket unless filebucket @bucket = filebucket.bucket @bucket end def default_bucket Puppet::Type.type(:filebucket).mkdefaultbucket.bucket end # Does the file currently exist? Just checks for whether # we have a stat def exist? stat ? true : false end # We have to do some extra finishing, to retrieve our bucket if # there is one. def finish # Look up our bucket, if there is one bucket super end # Create any children via recursion or whatever. def eval_generate return [] unless self.recurse? recurse #recurse.reject do |resource| # catalog.resource(:file, resource[:path]) #end.each do |child| # catalog.add_resource child # catalog.relationship_graph.add_edge self, child #end end def ancestors ancestors = Pathname.new(self[:path]).enum_for(:ascend).map(&:to_s) ancestors.delete(self[:path]) ancestors end def flush # We want to make sure we retrieve metadata anew on each transaction. @parameters.each do |name, param| param.flush if param.respond_to?(:flush) end @stat = :needs_stat end def initialize(hash) # Used for caching clients @clients = {} super # If they've specified a source, we get our 'should' values # from it. unless self[:ensure] if self[:target] self[:ensure] = :symlink elsif self[:content] self[:ensure] = :file end end @stat = :needs_stat end # Configure discovered resources to be purged. def mark_children_for_purging(children) children.each do |name, child| next if child[:source] child[:ensure] = :absent end end # Create a new file or directory object as a child to the current # object. def newchild(path) full_path = ::File.join(self[:path], path) # Add some new values to our original arguments -- these are the ones # set at initialization. We specifically want to exclude any param # values set by the :source property or any default values. # LAK:NOTE This is kind of silly, because the whole point here is that # the values set at initialization should live as long as the resource # but values set by default or by :source should only live for the transaction # or so. Unfortunately, we don't have a straightforward way to manage # the different lifetimes of this data, so we kludge it like this. # The right-side hash wins in the merge. options = @original_parameters.merge(:path => full_path).reject { |param, value| value.nil? } # These should never be passed to our children. [:parent, :ensure, :recurse, :recurselimit, :target, :alias, :source].each do |param| options.delete(param) if options.include?(param) end self.class.new(options) end # Files handle paths specially, because they just lengthen their # path names, rather than including the full parent's title each # time. def pathbuilder # We specifically need to call the method here, so it looks # up our parent in the catalog graph. if parent = parent() # We only need to behave specially when our parent is also # a file if parent.is_a?(self.class) # Remove the parent file name list = parent.pathbuilder list.pop # remove the parent's path info return list << self.ref else return super end else return [self.ref] end end # Should we be purging? def purge? @parameters.include?(:purge) and (self[:purge] == :true or self[:purge] == "true") end # Recursively generate a list of file resources, which will # be used to copy remote files, manage local files, and/or make links # to map to another directory. def recurse children = (self[:recurse] == :remote) ? {} : recurse_local if self[:target] recurse_link(children) elsif self[:source] recurse_remote(children) end # If we're purging resources, then delete any resource that isn't on the # remote system. mark_children_for_purging(children) if self.purge? result = children.values.sort { |a, b| a[:path] <=> b[:path] } remove_less_specific_files(result) end # This is to fix bug #2296, where two files recurse over the same # set of files. It's a rare case, and when it does happen you're # not likely to have many actual conflicts, which is good, because # this is a pretty inefficient implementation. def remove_less_specific_files(files) mypath = self[:path].split(::File::Separator) other_paths = catalog.vertices. select { |r| r.is_a?(self.class) and r[:path] != self[:path] }. collect { |r| r[:path].split(::File::Separator) }. select { |p| p[0,mypath.length] == mypath } return files if other_paths.empty? files.reject { |file| path = file[:path].split(::File::Separator) other_paths.any? { |p| path[0,p.length] == p } } end # A simple method for determining whether we should be recursing. def recurse? self[:recurse] == true or self[:recurse] == :remote end # Recurse the target of the link. def recurse_link(children) perform_recursion(self[:target]).each do |meta| if meta.relative_path == "." self[:ensure] = :directory next end children[meta.relative_path] ||= newchild(meta.relative_path) if meta.ftype == "directory" children[meta.relative_path][:ensure] = :directory else children[meta.relative_path][:ensure] = :link children[meta.relative_path][:target] = meta.full_path end end children end # Recurse the file itself, returning a Metadata instance for every found file. def recurse_local result = perform_recursion(self[:path]) return {} unless result result.inject({}) do |hash, meta| next hash if meta.relative_path == "." hash[meta.relative_path] = newchild(meta.relative_path) hash end end # Recurse against our remote file. def recurse_remote(children) sourceselect = self[:sourceselect] total = self[:source].collect do |source| next unless result = perform_recursion(source) return if top = result.find { |r| r.relative_path == "." } and top.ftype != "directory" result.each { |data| data.source = "#{source}/#{data.relative_path}" } break result if result and ! result.empty? and sourceselect == :first result end.flatten # This only happens if we have sourceselect == :all unless sourceselect == :first found = [] total.reject! do |data| result = found.include?(data.relative_path) found << data.relative_path unless found.include?(data.relative_path) result end end total.each do |meta| if meta.relative_path == "." parameter(:source).metadata = meta next end children[meta.relative_path] ||= newchild(meta.relative_path) children[meta.relative_path][:source] = meta.source children[meta.relative_path][:checksum] = :md5 if meta.ftype == "file" children[meta.relative_path].parameter(:source).metadata = meta end children end def perform_recursion(path) Puppet::FileServing::Metadata.indirection.search( path, :links => self[:links], :recurse => (self[:recurse] == :remote ? true : self[:recurse]), :recurselimit => self[:recurselimit], :ignore => self[:ignore], :checksum_type => (self[:source] || self[:content]) ? self[:checksum] : :none ) end # Remove any existing data. This is only used when dealing with # links or directories. def remove_existing(should) return unless s = stat self.fail "Could not back up; will not replace" unless perform_backup unless should.to_s == "link" return if s.ftype.to_s == should.to_s end case s.ftype when "directory" if self[:force] == :true debug "Removing existing directory for replacement with #{should}" FileUtils.rmtree(self[:path]) else notice "Not removing directory; use 'force' to override" return end when "link", "file" debug "Removing existing #{s.ftype} for replacement with #{should}" ::File.unlink(self[:path]) else self.fail "Could not back up files of type #{s.ftype}" end @stat = :needs_stat true end def retrieve if source = parameter(:source) source.copy_source_values end super end # Set the checksum, from another property. There are multiple # properties that modify the contents of a file, and they need the # ability to make sure that the checksum value is in sync. def setchecksum(sum = nil) if @parameters.include? :checksum if sum @parameters[:checksum].checksum = sum else # If they didn't pass in a sum, then tell checksum to # figure it out. currentvalue = @parameters[:checksum].retrieve @parameters[:checksum].checksum = currentvalue end end end # Should this thing be a normal file? This is a relatively complex # way of determining whether we're trying to create a normal file, # and it's here so that the logic isn't visible in the content property. def should_be_file? return true if self[:ensure] == :file # I.e., it's set to something like "directory" return false if e = self[:ensure] and e != :present # The user doesn't really care, apparently if self[:ensure] == :present return true unless s = stat return(s.ftype == "file" ? true : false) end # If we've gotten here, then :ensure isn't set return true if self[:content] return true if stat and stat.ftype == "file" false end # Stat our file. Depending on the value of the 'links' attribute, we # use either 'stat' or 'lstat', and we expect the properties to use the # resulting stat object accordingly (mostly by testing the 'ftype' # value). # # We use the initial value :needs_stat to ensure we only stat the file once, # but can also keep track of a failed stat (@stat == nil). This also allows # us to re-stat on demand by setting @stat = :needs_stat. def stat return @stat unless @stat == :needs_stat method = :stat # Files are the only types that support links if (self.class.name == :file and self[:links] != :follow) or self.class.name == :tidy method = :lstat end @stat = begin ::File.send(method, self[:path]) rescue Errno::ENOENT => error nil rescue Errno::EACCES => error warning "Could not stat; permission denied" nil end end # We have to hack this just a little bit, because otherwise we'll get # an error when the target and the contents are created as properties on # the far side. def to_trans(retrieve = true) obj = super obj.delete(:target) if obj[:target] == :notlink obj end # Write out the file. Requires the property name for logging. # Write will be done by the content property, along with checksum computation def write(property) remove_existing(:file) use_temporary_file = write_temporary_file? if use_temporary_file path = "#{self[:path]}.puppettmp_#{rand(10000)}" path = "#{self[:path]}.puppettmp_#{rand(10000)}" while ::File.exists?(path) or ::File.symlink?(path) else path = self[:path] end mode = self.should(:mode) # might be nil umask = mode ? 000 : 022 mode_int = mode ? mode.to_i(8) : nil content_checksum = Puppet::Util.withumask(umask) { ::File.open(path, 'wb', 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/content.rb b/lib/puppet/type/file/content.rb index e3795fe65..8f3b8b48a 100755 --- a/lib/puppet/type/file/content.rb +++ b/lib/puppet/type/file/content.rb @@ -1,225 +1,225 @@ require 'net/http' require 'uri' require 'tempfile' require 'puppet/util/checksums' require 'puppet/network/http/api/v1' require 'puppet/network/http/compression' module Puppet Puppet::Type.type(:file).newproperty(:content) do include Puppet::Util::Diff include Puppet::Util::Checksums include Puppet::Network::HTTP::API::V1 include Puppet::Network::HTTP::Compression.module attr_reader :actual_content desc "Specify the contents of a file as a string. Newlines, tabs, and spaces can be specified using standard escaped syntax in double-quoted strings (e.g., \\n for a newline). With very small files, you can construct strings directly... define resolve(nameserver1, nameserver2, domain, search) { $str = \"search $search domain $domain nameserver $nameserver1 nameserver $nameserver2 \" file { \"/etc/resolv.conf\": content => $str } } ...but for larger files, this attribute is more useful when combined with the [template](http://docs.puppetlabs.com/references/latest/function.html#template) function." # Store a checksum as the value, rather than the actual content. # Simplifies everything. munge do |value| if value == :absent value elsif checksum?(value) # XXX This is potentially dangerous because it means users can't write a file whose # entire contents are a plain checksum value else @actual_content = value resource.parameter(:checksum).sum(value) end end # Checksums need to invert how changes are printed. def change_to_s(currentvalue, newvalue) # Our "new" checksum value is provided by the source. if source = resource.parameter(:source) and tmp = source.checksum newvalue = tmp end if currentvalue == :absent return "defined content as '#{newvalue}'" elsif newvalue == :absent return "undefined content from '#{currentvalue}'" else return "content changed '#{currentvalue}' to '#{newvalue}'" end end def checksum_type if source = resource.parameter(:source) result = source.checksum else checksum = resource.parameter(:checksum) result = resource[:checksum] end if result =~ /^\{(\w+)\}.+/ return $1.to_sym else return result end end def length (actual_content and actual_content.length) || 0 end def content self.should end # Override this method to provide diffs if asked for. # Also, fix #872: when content is used, and replace is true, the file # should be insync when it exists def insync?(is) if resource.should_be_file? return false if is == :absent else return true end return true if ! @resource.replace? result = super if ! result and Puppet[:show_diff] write_temporarily do |path| - print diff(@resource[:path], path) + notice "\n" + diff(@resource[:path], path) end end result end def retrieve return :absent unless stat = @resource.stat ftype = stat.ftype # Don't even try to manage the content on directories or links return nil if ["directory","link"].include?(ftype) begin resource.parameter(:checksum).sum_file(resource[:path]) rescue => detail raise Puppet::Error, "Could not read #{ftype} #{@resource.title}: #{detail}" end end # Make sure we're also managing the checksum property. def should=(value) @resource.newattr(:checksum) unless @resource.parameter(:checksum) super end # Just write our content out to disk. def sync return_event = @resource.stat ? :file_changed : :file_created # We're safe not testing for the 'source' if there's no 'should' # because we wouldn't have gotten this far if there weren't at least # one valid value somewhere. @resource.write(:content) return_event end def write_temporarily tempfile = Tempfile.new("puppet-file") tempfile.open write(tempfile) tempfile.close yield tempfile.path tempfile.delete end def write(file) resource.parameter(:checksum).sum_stream { |sum| each_chunk_from(actual_content || resource.parameter(:source)) { |chunk| sum << chunk file.print chunk } } end def self.standalone? Puppet.settings[:name] == "apply" end # the content is munged so if it's a checksum source_or_content is nil # unless the checksum indirectly comes from source def each_chunk_from(source_or_content) if source_or_content.is_a?(String) yield source_or_content elsif content_is_really_a_checksum? && source_or_content.nil? yield read_file_from_filebucket elsif source_or_content.nil? yield '' elsif self.class.standalone? yield source_or_content.content elsif source_or_content.local? chunk_file_from_disk(source_or_content) { |chunk| yield chunk } else chunk_file_from_source(source_or_content) { |chunk| yield chunk } end end private def content_is_really_a_checksum? checksum?(should) end def chunk_file_from_disk(source_or_content) File.open(source_or_content.full_path, "rb") do |src| while chunk = src.read(8192) yield chunk end end end def chunk_file_from_source(source_or_content) request = Puppet::Indirector::Request.new(:file_content, :find, source_or_content.full_path.sub(/^\//,'')) connection = Puppet::Network::HttpPool.http_instance(source_or_content.server, source_or_content.port) connection.request_get(indirection2uri(request), add_accept_encoding({"Accept" => "raw"})) do |response| case response.code when /^2/; uncompress(response) { |uncompressor| response.read_body { |chunk| yield uncompressor.uncompress(chunk) } } else # Raise the http error if we didn't get a 'success' of some kind. message = "Error #{response.code} on SERVER: #{(response.body||'').empty? ? response.message : uncompress_body(response)}" raise Net::HTTPError.new(message, response) end end end def read_file_from_filebucket raise "Could not get filebucket from file" unless dipper = resource.bucket sum = should.sub(/\{\w+\}/, '') dipper.getfile(sum) rescue => detail fail "Could not retrieve content for #{should} from filebucket: #{detail}" end end end diff --git a/lib/puppet/util/diff.rb b/lib/puppet/util/diff.rb index 73e1bc014..131241b73 100644 --- a/lib/puppet/util/diff.rb +++ b/lib/puppet/util/diff.rb @@ -1,74 +1,74 @@ # Provide a diff between two strings. module Puppet::Util::Diff include Puppet::Util require 'tempfile' def diff(old, new) command = [Puppet[:diff]] if args = Puppet[:diff_args] and args != "" command << args end command << old << new execute(command, :failonfail => false) end module_function :diff # return diff string of two input strings # format defaults to unified # context defaults to 3 lines def lcs_diff(data_old, data_new, format=:unified, context_lines=3) unless Puppet.features.diff? Puppet.warning "Cannot provide diff without the diff/lcs Ruby library" return "" end data_old = data_old.split(/\n/).map! { |e| e.chomp } data_new = data_new.split(/\n/).map! { |e| e.chomp } output = "" diffs = ::Diff::LCS.diff(data_old, data_new) return output if diffs.empty? oldhunk = hunk = nil file_length_difference = 0 diffs.each do |piece| begin hunk = ::Diff::LCS::Hunk.new( data_old, data_new, piece, context_lines, file_length_difference) file_length_difference = hunk.file_length_difference next unless oldhunk # Hunks may overlap, which is why we need to be careful when our # diff includes lines of context. Otherwise, we might print # redundant lines. if (context_lines > 0) and hunk.overlaps?(oldhunk) hunk.unshift(oldhunk) else output << oldhunk.diff(format) end ensure oldhunk = hunk output << "\n" end end # Handle the last remaining hunk output << oldhunk.diff(format) << "\n" end def string_file_diff(path, string) require 'tempfile' tempfile = Tempfile.new("puppet-diffing") tempfile.open tempfile.print string tempfile.close - print diff(path, tempfile.path) + notice "\n" + diff(path, tempfile.path) tempfile.delete end end diff --git a/spec/unit/type/file/content_spec.rb b/spec/unit/type/file/content_spec.rb index a08f64cc7..108acc005 100755 --- a/spec/unit/type/file/content_spec.rb +++ b/spec/unit/type/file/content_spec.rb @@ -1,437 +1,437 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/network/http_pool' content = Puppet::Type.type(:file).attrclass(:content) describe content do include PuppetSpec::Files before do @filename = tmpfile('testfile') @resource = Puppet::Type.type(:file).new :path => @filename File.open(@filename, 'w') {|f| f.write "initial file content"} content.stubs(:standalone?).returns(false) end describe "when determining the checksum type" do it "should use the type specified in the source checksum if a source is set" do @resource[:source] = File.expand_path("/foo") @resource.parameter(:source).expects(:checksum).returns "{md5lite}eh" @content = content.new(:resource => @resource) @content.checksum_type.should == :md5lite end it "should use the type specified by the checksum parameter if no source is set" do @resource[:checksum] = :md5lite @content = content.new(:resource => @resource) @content.checksum_type.should == :md5lite end end describe "when determining the actual content to write" do it "should use the set content if available" do @content = content.new(:resource => @resource) @content.should = "ehness" @content.actual_content.should == "ehness" end it "should not use the content from the source if the source is set" do source = mock 'source' @resource.expects(:parameter).never.with(:source).returns source @content = content.new(:resource => @resource) @content.actual_content.should be_nil end end describe "when setting the desired content" do it "should make the actual content available via an attribute" do @content = content.new(:resource => @resource) @content.stubs(:checksum_type).returns "md5" @content.should = "this is some content" @content.actual_content.should == "this is some content" end it "should store the checksum as the desired content" do @content = content.new(:resource => @resource) digest = Digest::MD5.hexdigest("this is some content") @content.stubs(:checksum_type).returns "md5" @content.should = "this is some content" @content.should.must == "{md5}#{digest}" end it "should not checksum 'absent'" do @content = content.new(:resource => @resource) @content.should = :absent @content.should.must == :absent end it "should accept a checksum as the desired content" do @content = content.new(:resource => @resource) digest = Digest::MD5.hexdigest("this is some content") string = "{md5}#{digest}" @content.should = string @content.should.must == string end end describe "when retrieving the current content" do it "should return :absent if the file does not exist" do @content = content.new(:resource => @resource) @resource.expects(:stat).returns nil @content.retrieve.should == :absent end it "should not manage content on directories" do @content = content.new(:resource => @resource) stat = mock 'stat', :ftype => "directory" @resource.expects(:stat).returns stat @content.retrieve.should be_nil end it "should not manage content on links" do @content = content.new(:resource => @resource) stat = mock 'stat', :ftype => "link" @resource.expects(:stat).returns stat @content.retrieve.should be_nil end it "should always return the checksum as a string" do @content = content.new(:resource => @resource) @resource[:checksum] = :mtime stat = mock 'stat', :ftype => "file" @resource.expects(:stat).returns stat time = Time.now @resource.parameter(:checksum).expects(:mtime_file).with(@resource[:path]).returns time @content.retrieve.should == "{mtime}#{time}" end it "should return the checksum of the file if it exists and is a normal file" do @content = content.new(:resource => @resource) stat = mock 'stat', :ftype => "file" @resource.expects(:stat).returns stat @resource.parameter(:checksum).expects(:md5_file).with(@resource[:path]).returns "mysum" @content.retrieve.should == "{md5}mysum" end end describe "when testing whether the content is in sync" do before do @resource[:ensure] = :file @content = content.new(:resource => @resource) end it "should return true if the resource shouldn't be a regular file" do @resource.expects(:should_be_file?).returns false @content.should = "foo" @content.must be_safe_insync("whatever") end it "should return false if the current content is :absent" do @content.should = "foo" @content.should_not be_safe_insync(:absent) end it "should return false if the file should be a file but is not present" do @resource.expects(:should_be_file?).returns true @content.should = "foo" @content.should_not be_safe_insync(:absent) end describe "and the file exists" do before do @resource.stubs(:stat).returns mock("stat") end it "should return false if the current contents are different from the desired content" do @content.should = "some content" @content.should_not be_safe_insync("other content") end it "should return true if the sum for the current contents is the same as the sum for the desired content" do @content.should = "some content" @content.must be_safe_insync("{md5}" + Digest::MD5.hexdigest("some content")) end describe "and Puppet[:show_diff] is set" do before do Puppet[:show_diff] = true end it "should display a diff if the current contents are different from the desired content" do @content.should = "some content" @content.expects(:diff).returns("my diff").once - @content.expects(:print).with("my diff").once + @content.expects(:notice).with("\nmy diff").once @content.safe_insync?("other content") end it "should not display a diff if the sum for the current contents is the same as the sum for the desired content" do @content.should = "some content" @content.expects(:diff).never @content.safe_insync?("{md5}" + Digest::MD5.hexdigest("some content")) end end end describe "and :replace is false" do before do @resource.stubs(:replace?).returns false end it "should be insync if the file exists and the content is different" do @resource.stubs(:stat).returns mock('stat') @content.must be_safe_insync("whatever") end it "should be insync if the file exists and the content is right" do @resource.stubs(:stat).returns mock('stat') @content.must be_safe_insync("something") end it "should not be insync if the file does not exist" do @content.should = "foo" @content.should_not be_safe_insync(:absent) end end end describe "when changing the content" do before do @content = content.new(:resource => @resource) @content.should = "some content" @resource.stubs(:[]).with(:path).returns "/boo" @resource.stubs(:stat).returns "eh" end it "should use the file's :write method to write the content" do @resource.expects(:write).with(:content) @content.sync end it "should return :file_changed if the file already existed" do @resource.expects(:stat).returns "something" @resource.stubs(:write) @content.sync.should == :file_changed end it "should return :file_created if the file did not exist" do @resource.expects(:stat).returns nil @resource.stubs(:write) @content.sync.should == :file_created end end describe "when writing" do before do @content = content.new(:resource => @resource) end it "should attempt to read from the filebucket if no actual content nor source exists" do @fh = File.open(@filename, 'wb') @content.should = "{md5}foo" @content.resource.bucket.class.any_instance.stubs(:getfile).returns "foo" @content.write(@fh) @fh.close end describe "from actual content" do before(:each) do @content.stubs(:actual_content).returns("this is content") end it "should write to the given file handle" do @fh.expects(:print).with("this is content") @content.write(@fh) end it "should return the current checksum value" do @resource.parameter(:checksum).expects(:sum_stream).returns "checksum" @content.write(@fh).should == "checksum" end end describe "from a file bucket" do it "should fail if a file bucket cannot be retrieved" do @content.should = "{md5}foo" @content.resource.expects(:bucket).returns nil lambda { @content.write(@fh) }.should raise_error(Puppet::Error) end it "should fail if the file bucket cannot find any content" do @content.should = "{md5}foo" bucket = stub 'bucket' @content.resource.expects(:bucket).returns bucket bucket.expects(:getfile).with("foo").raises "foobar" lambda { @content.write(@fh) }.should raise_error(Puppet::Error) end it "should write the returned content to the file" do @content.should = "{md5}foo" bucket = stub 'bucket' @content.resource.expects(:bucket).returns bucket bucket.expects(:getfile).with("foo").returns "mycontent" @fh.expects(:print).with("mycontent") @content.write(@fh) end end describe "from local source" do before(:each) do @sourcename = tmpfile('source') @resource = Puppet::Type.type(:file).new :path => @filename, :backup => false, :source => @sourcename @source_content = "source file content\r\n"*10000 @sourcefile = File.open(@sourcename, 'wb') {|f| f.write @source_content} @content = @resource.newattr(:content) @source = @resource.parameter :source #newattr(:source) end it "should copy content from the source to the file" do @resource.write(@source) Puppet::Util.binread(@filename).should == @source_content end it "should return the checksum computed" do File.open(@filename, 'wb') do |file| @content.write(file).should == "{md5}#{Digest::MD5.hexdigest(@source_content)}" end end end describe "from remote source" do before(:each) do @resource = Puppet::Type.type(:file).new :path => @filename, :backup => false @response = stub_everything 'response', :code => "200" @source_content = "source file content\n"*10000 @response.stubs(:read_body).multiple_yields(*(["source file content\n"]*10000)) @conn = stub_everything 'connection' @conn.stubs(:request_get).yields(@response) Puppet::Network::HttpPool.stubs(:http_instance).returns @conn @content = @resource.newattr(:content) @sourcename = "puppet:///test/foo" @source = @resource.newattr(:source) @source.stubs(:metadata).returns stub_everything('metadata', :source => @sourcename, :ftype => 'file') end it "should write the contents to the file" do @resource.write(@source) Puppet::Util.binread(@filename).should == @source_content end it "should not write anything if source is not found" do @response.stubs(:code).returns("404") lambda {@resource.write(@source)}.should raise_error(Net::HTTPError) { |e| e.message =~ /404/ } File.read(@filename).should == "initial file content" end it "should raise an HTTP error in case of server error" do @response.stubs(:code).returns("500") lambda { @content.write(@fh) }.should raise_error { |e| e.message.include? @source_content } end it "should return the checksum computed" do File.open(@filename, 'w') do |file| @content.write(file).should == "{md5}#{Digest::MD5.hexdigest(@source_content)}" end end end # These are testing the implementation rather than the desired behaviour; while that bites, there are a whole # pile of other methods in the File type that depend on intimate details of this implementation and vice-versa. # If these blow up, you are gonna have to review the callers to make sure they don't explode! --daniel 2011-02-01 describe "each_chunk_from should work" do before do @content = content.new(:resource => @resource) end it "when content is a string" do @content.each_chunk_from('i_am_a_string') { |chunk| chunk.should == 'i_am_a_string' } end # The following manifest is a case where source and content.should are both set # file { "/tmp/mydir" : # source => '/tmp/sourcedir', # recurse => true, # } it "when content checksum comes from source" do source_param = Puppet::Type.type(:file).attrclass(:source) source = source_param.new(:resource => @resource) @content.should = "{md5}123abcd" @content.expects(:chunk_file_from_source).returns('from_source') @content.each_chunk_from(source) { |chunk| chunk.should == 'from_source' } end it "when no content, source, but ensure present" do @resource[:ensure] = :present @content.each_chunk_from(nil) { |chunk| chunk.should == '' } end # you might do this if you were just auditing it "when no content, source, but ensure file" do @resource[:ensure] = :file @content.each_chunk_from(nil) { |chunk| chunk.should == '' } end it "when source_or_content is nil and content not a checksum" do @content.each_chunk_from(nil) { |chunk| chunk.should == '' } end # the content is munged so that if it's a checksum nil gets passed in it "when content is a checksum it should try to read from filebucket" do @content.should = "{md5}123abcd" @content.expects(:read_file_from_filebucket).once.returns('im_a_filebucket') @content.each_chunk_from(nil) { |chunk| chunk.should == 'im_a_filebucket' } end it "when running as puppet apply" do @content.class.expects(:standalone?).returns true source_or_content = stubs('source_or_content') source_or_content.expects(:content).once.returns :whoo @content.each_chunk_from(source_or_content) { |chunk| chunk.should == :whoo } end it "when running from source with a local file" do source_or_content = stubs('source_or_content') source_or_content.expects(:local?).returns true @content.expects(:chunk_file_from_disk).with(source_or_content).once.yields 'woot' @content.each_chunk_from(source_or_content) { |chunk| chunk.should == 'woot' } end it "when running from source with a remote file" do source_or_content = stubs('source_or_content') source_or_content.expects(:local?).returns false @content.expects(:chunk_file_from_source).with(source_or_content).once.yields 'woot' @content.each_chunk_from(source_or_content) { |chunk| chunk.should == 'woot' } end end end end