diff --git a/lib/puppet/type/file.rb b/lib/puppet/type/file.rb index fe426a821..a7608c9e8 100644 --- a/lib/puppet/type/file.rb +++ b/lib/puppet/type/file.rb @@ -1,817 +1,820 @@ 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' +require 'puppet/util/symbolic_file_mode' Puppet::Type.newtype(:file) do include Puppet::Util::MethodHelper include Puppet::Util::Checksums include Puppet::Util::Backups + include Puppet::Util::SymbolicFileMode + @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 req = [] path = Pathname.new(self[:path]) if !path.root? # Start at our parent, to avoid autorequiring ourself parents = path.parent.enum_for(:ascend) if found = parents.find { |p| catalog.resource(:file, p.to_s) } req << found.to_s end end # if the resource is a link, make sure the target is created first req << self[:target] if self[:target] req 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 + mode_int = mode ? symbolic_mode_to_int(mode, 0644) : 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/ensure.rb b/lib/puppet/type/file/ensure.rb index a846856c8..b7614f3fb 100755 --- a/lib/puppet/type/file/ensure.rb +++ b/lib/puppet/type/file/ensure.rb @@ -1,167 +1,171 @@ + module Puppet Puppet::Type.type(:file).ensurable do require 'etc' + require 'puppet/util/symbolic_file_mode' + include Puppet::Util::SymbolicFileMode + desc <<-EOT Whether to create files that don't currently exist. Possible values are *absent*, *present*, *file*, and *directory*. Specifying `present` will match any form of file existence, and if the file is missing will create an empty file. Specifying `absent` will delete the file (and directory if `recurse => true`). Anything other than those values will create a symlink. In the interest of readability and clarity, you should use `ensure => link` and explicitly specify a target; however, if a `target` attribute isn't provided, the value of the `ensure` attribute will be used as the symlink target. The following two declarations are equivalent: # (Useful on Solaris) # Less maintainable: file { "/etc/inetd.conf": ensure => "/etc/inet/inetd.conf", } # More maintainable: file { "/etc/inetd.conf": ensure => link, target => "/etc/inet/inetd.conf", } EOT # Most 'ensure' properties have a default, but with files we, um, don't. nodefault newvalue(:absent) do File.unlink(@resource[:path]) end aliasvalue(:false, :absent) newvalue(:file, :event => :file_created) do # Make sure we're not managing the content some other way if property = @resource.property(:content) property.sync else @resource.write(:ensure) mode = @resource.should(:mode) end end #aliasvalue(:present, :file) newvalue(:present, :event => :file_created) do # Make a file if they want something, but this will match almost # anything. set_file end newvalue(:directory, :event => :directory_created) do mode = @resource.should(:mode) parent = File.dirname(@resource[:path]) unless FileTest.exists? parent raise Puppet::Error, "Cannot create #{@resource[:path]}; parent directory #{parent} does not exist" end if mode Puppet::Util.withumask(000) do - Dir.mkdir(@resource[:path], mode.to_i(8)) + Dir.mkdir(@resource[:path], symbolic_mode_to_int(mode, 755, true)) end else Dir.mkdir(@resource[:path]) end @resource.send(:property_fix) return :directory_created end newvalue(:link, :event => :link_created) do fail "Cannot create a symlink without a target" unless property = resource.property(:target) property.retrieve property.mklink end # Symlinks. newvalue(/./) do # This code never gets executed. We need the regex to support # specifying it, but the work is done in the 'symlink' code block. end munge do |value| value = super(value) value,resource[:target] = :link,value unless value.is_a? Symbol resource[:links] = :manage if value == :link and resource[:links] != :follow value end def change_to_s(currentvalue, newvalue) return super unless newvalue.to_s == "file" return super unless property = @resource.property(:content) # We know that content is out of sync if we're here, because # it's essentially equivalent to 'ensure' in the transaction. if source = @resource.parameter(:source) should = source.checksum else should = property.should end if should == :absent is = property.retrieve else is = :absent end property.change_to_s(is, should) end # Check that we can actually create anything def check basedir = File.dirname(@resource[:path]) if ! FileTest.exists?(basedir) raise Puppet::Error, "Can not create #{@resource.title}; parent directory does not exist" elsif ! FileTest.directory?(basedir) raise Puppet::Error, "Can not create #{@resource.title}; #{dirname} is not a directory" end end # We have to treat :present specially, because it works with any # type of file. def insync?(currentvalue) unless currentvalue == :absent or resource.replace? return true end if self.should == :present return !(currentvalue.nil? or currentvalue == :absent) else return super(currentvalue) end end def retrieve if stat = @resource.stat return stat.ftype.intern else if self.should == :false return :false else return :absent end end end def sync @resource.remove_existing(self.should) if self.should == :absent return :file_removed end event = super event end end end diff --git a/lib/puppet/type/file/mode.rb b/lib/puppet/type/file/mode.rb index b246652a0..8c7020ba4 100755 --- a/lib/puppet/type/file/mode.rb +++ b/lib/puppet/type/file/mode.rb @@ -1,85 +1,120 @@ # Manage file modes. This state should support different formats # for specification (e.g., u+rwx, or -0011), but for now only supports # specifying the full mode. module Puppet Puppet::Type.type(:file).newproperty(:mode) do + require 'puppet/util/symbolic_file_mode' + include Puppet::Util::SymbolicFileMode + desc "Mode the file should be. Currently relatively limited: you must specify the exact mode the file should be. Note that when you set the mode of a directory, Puppet always sets the search/traverse (1) bit anywhere the read (4) bit is set. This is almost always what you want: read allows you to list the entries in a directory, and search/traverse allows you to access (read/write/execute) those entries.) Because of this feature, you can recursively make a directory and all of the files in it world-readable by setting e.g.: file { '/some/dir': mode => 644, recurse => true, } In this case all of the files underneath `/some/dir` will have mode 644, and all of the directories will have mode 755." validate do |value| - if value.is_a?(String) and value !~ /^[0-7]+$/ - raise Puppet::Error, "File modes can only be octal numbers, not #{should.inspect}" + unless value.nil? or valid_symbolic_mode?(value) + raise Puppet::Error, "The file mode specification is invalid: #{value.inspect}" end end - munge do |should| - if should.is_a?(String) - should.to_i(8).to_s(8) - else - should.to_s(8) + munge do |value| + return nil if value.nil? + + unless valid_symbolic_mode?(value) + raise Puppet::Error, "The file mode specification is invalid: #{value.inspect}" end + + normalize_symbolic_mode(value) + end + + def desired_mode_from_current(desired, current) + current = current.to_i(8) if current.is_a? String + is_a_directory = @resource.stat and @resource.stat.directory? + symbolic_mode_to_int(desired, current, is_a_directory) end # If we're a directory, we need to be executable for all cases # that are readable. This should probably be selectable, but eh. def dirmask(value) - if FileTest.directory?(resource[:path]) + orig = value + if FileTest.directory?(resource[:path]) and value =~ /^\d+$/ then value = value.to_i(8) value |= 0100 if value & 0400 != 0 value |= 010 if value & 040 != 0 value |= 01 if value & 04 != 0 value = value.to_s(8) end value end # If we're not following links and we're a link, then we just turn # off mode management entirely. def insync?(currentvalue) if stat = @resource.stat and stat.ftype == "link" and @resource[:links] != :follow self.debug "Not managing symlink mode" return true else return super(currentvalue) end end + def property_matches?(current, desired) + return false unless current + current_bits = normalize_symbolic_mode(current) + desired_bits = desired_mode_from_current(desired, current).to_s(8) + current_bits == desired_bits + end + # Ideally, dirmask'ing could be done at munge time, but we don't know if 'ensure' # will eventually be a directory or something else. And unfortunately, that logic # depends on the ensure, source, and target properties. So rather than duplicate # that logic, and get it wrong, we do dirmask during retrieve, after 'ensure' has # been synced. def retrieve if @resource.stat @should &&= @should.collect { |s| self.dirmask(s) } end super end + # Finally, when we sync the mode out we need to transform it; since we + # don't have access to the calculated "desired" value here, or the + # "current" value, only the "should" value we need to retrieve again. + def sync + current = @resource.stat ? @resource.stat.mode : 0644 + set(desired_mode_from_current(@should[0], current).to_s(8)) + end + + def change_to_s(old_value, desired) + return super if desired =~ /^\d+$/ + + old_bits = normalize_symbolic_mode(old_value) + new_bits = normalize_symbolic_mode(desired_mode_from_current(desired, old_bits)) + super(old_bits, new_bits) + " (#{desired})" + end + def should_to_s(should_value) - should_value.rjust(4,"0") + should_value.rjust(4, "0") end def is_to_s(currentvalue) - currentvalue.rjust(4,"0") + currentvalue.rjust(4, "0") end end end diff --git a/lib/puppet/util/symbolic_file_mode.rb b/lib/puppet/util/symbolic_file_mode.rb new file mode 100644 index 000000000..de07b061a --- /dev/null +++ b/lib/puppet/util/symbolic_file_mode.rb @@ -0,0 +1,140 @@ +require 'puppet/util' + +module Puppet::Util::SymbolicFileMode + SetUIDBit = ReadBit = 4 + SetGIDBit = WriteBit = 2 + StickyBit = ExecBit = 1 + SymbolicMode = { 'x' => ExecBit, 'w' => WriteBit, 'r' => ReadBit } + SymbolicSpecialToBit = { + 't' => { 'u' => StickyBit, 'g' => StickyBit, 'o' => StickyBit }, + 's' => { 'u' => SetUIDBit, 'g' => SetGIDBit, 'o' => StickyBit } + } + + def valid_symbolic_mode?(value) + value = normalize_symbolic_mode(value) + return true if value =~ /^0?[0-7]{1,4}$/ + return true if value =~ /^([ugoa]*[-=+][-=+rstwxXugo]*)(,[ugoa]*[-=+][-=+rstwxXugo]*)*$/ + return false + end + + def normalize_symbolic_mode(value) + return nil if value.nil? + + # We need to treat integers as octal numbers. + if value.is_a? Numeric then + return value.to_s(8) + elsif value =~ /^0?[0-7]{1,4}$/ then + return value.to_i(8).to_s(8) + else + return value + end + end + + def symbolic_mode_to_int(modification, to_mode = 0, is_a_directory = false) + if modification.nil? or modification == '' then + raise Puppet::Error, "An empty mode string is illegal" + end + if modification =~ /^[0-7]+$/ then return modification.to_i(8) end + if modification =~ /^\d+$/ then + raise Puppet::Error, "Numeric modes must be in octal, not decimal!" + end + + fail "non-numeric current mode (#{to_mode.inspect})" unless to_mode.is_a?(Numeric) + + original_mode = { + 's' => (to_mode & 07000) >> 9, + 'u' => (to_mode & 00700) >> 6, + 'g' => (to_mode & 00070) >> 3, + 'o' => (to_mode & 00007) >> 0, + # Are there any execute bits set in the original mode? + 'any x?' => (to_mode & 00111) != 0 + } + final_mode = { + 's' => original_mode['s'], + 'u' => original_mode['u'], + 'g' => original_mode['g'], + 'o' => original_mode['o'], + } + + modification.split(/\s*,\s*/).each do |part| + begin + _, to, dsl = /^([ugoa]*)([-+=].*)$/.match(part).to_a + if dsl.nil? then raise Puppet::Error, 'Missing action' end + to = "a" unless to and to.length > 0 + + # We want a snapshot of the mode before we start messing with it to + # make actions like 'a-g' atomic. Various parts of the DSL refer to + # the original mode, the final mode, or the current snapshot of the + # mode, for added fun. + snapshot_mode = {} + final_mode.each {|k,v| snapshot_mode[k] = v } + + to.gsub('a', 'ugo').split('').uniq.each do |who| + value = snapshot_mode[who] + + action = '!' + actions = { + '!' => lambda {|_,_| raise Puppet::Error, 'Missing operation (-, =, or +)' }, + '=' => lambda {|m,v| m | v }, + '+' => lambda {|m,v| m | v }, + '-' => lambda {|m,v| m & ~v }, + } + + dsl.split('').each do |op| + case op + when /[-+=]/ then + action = op + # Clear all bits, if this is assignment + value = 0 if op == '=' + + when /[ugo]/ then + value = actions[action].call(value, snapshot_mode[op]) + + when /[rwx]/ then + value = actions[action].call(value, SymbolicMode[op]) + + when 'X' then + # Only meaningful in combination with "set" actions. + if action != '+' then + raise Puppet::Error, "X only works with the '+' operator" + end + + # As per the BSD manual page, set if this is a directory, or if + # any execute bit is set on the original (unmodified) mode. + # Ignored otherwise; it is "add if", not "add or clear". + if is_a_directory or original_mode['any x?'] then + value = actions[action].call(value, ExecBit) + end + + when /[st]/ then + bit = SymbolicSpecialToBit[op][who] or fail "internal error" + final_mode['s'] = actions[action].call(final_mode['s'], bit) + + else + raise Puppet::Error, 'Unknown operation' + end + end + + # Now, assign back the value. + final_mode[who] = value + end + + rescue Puppet::Error => e + if part.inspect != modification.inspect then + rest = " at #{part.inspect}" + else + rest = '' + end + + raise Puppet::Error, "#{e}#{rest} in symbolic mode #{modification.inspect}" + end + end + + result = + final_mode['s'] << 9 | + final_mode['u'] << 6 | + final_mode['g'] << 3 | + final_mode['o'] << 0 + return result + end +end diff --git a/spec/unit/type/file/mode_spec.rb b/spec/unit/type/file/mode_spec.rb index 2c6772aba..88881043c 100755 --- a/spec/unit/type/file/mode_spec.rb +++ b/spec/unit/type/file/mode_spec.rb @@ -1,142 +1,142 @@ #!/usr/bin/env rspec require 'spec_helper' describe Puppet::Type.type(:file).attrclass(:mode) do include PuppetSpec::Files let(:path) { tmpfile('mode_spec') } let(:resource) { Puppet::Type.type(:file).new :path => path, :mode => 0644 } let(:mode) { resource.property(:mode) } describe "#validate" do it "should accept values specified as integers" do expect { mode.value = 0755 }.not_to raise_error end it "should accept values specified as octal numbers in strings" do expect { mode.value = '0755' }.not_to raise_error end it "should not accept strings other than octal numbers" do expect do mode.value = 'readable please!' - end.to raise_error(Puppet::Error, /File modes can only be octal numbers/) + end.to raise_error(Puppet::Error, /The file mode specification is invalid/) end end describe "#munge" do # This is sort of a redundant test, but its spec is important. it "should return the value as a string" do mode.munge('0644').should be_a(String) end it "should accept strings as arguments" do mode.munge('0644').should == '644' end it "should accept integers are arguments" do mode.munge(0644).should == '644' end end describe "#dirmask" do before :each do Dir.mkdir(path) end it "should add execute bits corresponding to read bits for directories" do mode.dirmask('0644').should == '755' end it "should not add an execute bit when there is no read bit" do mode.dirmask('0600').should == '700' end it "should not add execute bits for files that aren't directories" do resource[:path] = tmpfile('other_file') mode.dirmask('0644').should == '0644' end end describe "#insync?" do it "should return true if the mode is correct" do FileUtils.touch(path) mode.must be_insync('644') end it "should return false if the mode is incorrect" do FileUtils.touch(path) mode.must_not be_insync('755') end it "should return true if the file is a link and we are managing links", :unless => Puppet.features.microsoft_windows? do File.symlink('anything', path) mode.must be_insync('644') end end describe "#retrieve" do it "should return absent if the resource doesn't exist" do resource[:path] = File.expand_path("/does/not/exist") mode.retrieve.should == :absent end it "should retrieve the directory mode from the provider" do Dir.mkdir(path) mode.expects(:dirmask).with('644').returns '755' resource.provider.expects(:mode).returns '755' mode.retrieve.should == '755' end it "should retrieve the file mode from the provider" do FileUtils.touch(path) mode.expects(:dirmask).with('644').returns '644' resource.provider.expects(:mode).returns '644' mode.retrieve.should == '644' end end describe '#should_to_s' do describe 'with a 3-digit mode' do it 'returns a 4-digit mode with a leading zero' do mode.should_to_s('755').should == '0755' end end describe 'with a 4-digit mode' do it 'returns the 4-digit mode when the first digit is a zero' do mode.should_to_s('0755').should == '0755' end it 'returns the 4-digit mode when the first digit is not a zero' do mode.should_to_s('1755').should == '1755' end end end describe '#is_to_s' do describe 'with a 3-digit mode' do it 'returns a 4-digit mode with a leading zero' do mode.is_to_s('755').should == '0755' end end describe 'with a 4-digit mode' do it 'returns the 4-digit mode when the first digit is a zero' do mode.is_to_s('0755').should == '0755' end it 'returns the 4-digit mode when the first digit is not a zero' do mode.is_to_s('1755').should == '1755' end end end end diff --git a/spec/unit/util/symbolic_file_mode_spec.rb b/spec/unit/util/symbolic_file_mode_spec.rb new file mode 100755 index 000000000..a6e9509f7 --- /dev/null +++ b/spec/unit/util/symbolic_file_mode_spec.rb @@ -0,0 +1,182 @@ +#!/usr/bin/env rspec +require 'spec_helper' + +require 'puppet/util/symbolic_file_mode' + +describe Puppet::Util::SymbolicFileMode do + include Puppet::Util::SymbolicFileMode + + describe "#valid_symbolic_mode?" do + %w{ + 0 0000 1 1 7 11 77 111 777 11 + 0 00000 01 01 07 011 077 0111 0777 011 + = - + u= g= o= a= u+ g+ o+ a+ u- g- o- a- ugo= ugoa= ugugug= + a=,u=,g= a=,g+ + =rwx +rwx -rwx + 644 go-w =rw,+X +X 755 u=rwx,go=rx u=rwx,go=u-w go= g=u-w + 755 0755 + }.each do |input| + it "should treat #{input.inspect} as valid" do + valid_symbolic_mode?(input).should be_true + end + end + + [0000, 0111, 0640, 0755, 0777].each do |input| + it "should treat the int #{input.to_s(8)} as value" do + valid_symbolic_mode?(input).should be_true + end + end + + %w{ + -1 -8 8 9 18 19 91 81 000000 11111 77777 + 0-1 0-8 08 09 018 019 091 081 0000000 011111 077777 + u g o a ug uo ua ag + }.each do |input| + it "should treat #{input.inspect} as invalid" do + valid_symbolic_mode?(input).should be_false + end + end + end + + describe "#normalize_symbolic_mode" do + it "should turn an int into a string" do + normalize_symbolic_mode(12).should be_an_instance_of String + end + + it "should not add a leading zero to an int" do + normalize_symbolic_mode(12).should_not =~ /^0/ + end + + it "should not add a leading zero to a string with a number" do + normalize_symbolic_mode("12").should_not =~ /^0/ + end + + it "should string a leading zero from a number" do + normalize_symbolic_mode("012").should == '12' + end + + it "should pass through any other string" do + normalize_symbolic_mode("u=rwx").should == 'u=rwx' + end + end + + describe "#symbolic_mode_to_int" do + { + "0654" => 00654, + "u+r" => 00400, + "g+r" => 00040, + "a+r" => 00444, + "a+x" => 00111, + "o+t" => 01000, + "o+t" => 01000, + ["o-t", 07777] => 06777, + ["a-x", 07777] => 07666, + ["a-rwx", 07777] => 07000, + ["ug-rwx", 07777] => 07007, + "a+x,ug-rwx" => 00001, + # My experimentation on debian suggests that +g ignores the sgid flag + ["a+g", 02060] => 02666, + # My experimentation on debian suggests that -g ignores the sgid flag + ["a-g", 02666] => 02000, + "g+x,a+g" => 00111, + # +X without exec set in the original should not set anything + "u+x,g+X" => 00100, + "g+X" => 00000, + # +X only refers to the original, *unmodified* file mode! + ["u+x,a+X", 0600] => 00700, + # Examples from the MacOS chmod(1) manpage + "0644" => 00644, + ["go-w", 07777] => 07755, + ["=rw,+X", 07777] => 07777, + ["=rw,+X", 07766] => 07777, + ["=rw,+X", 07676] => 07777, + ["=rw,+X", 07667] => 07777, + ["=rw,+X", 07666] => 07666, + "0755" => 00755, + "u=rwx,go=rx" => 00755, + "u=rwx,go=u-w" => 00755, + ["go=", 07777] => 07700, + ["g=u-w", 07777] => 07757, + ["g=u-w", 00700] => 00750, + ["g=u-w", 00600] => 00640, + ["g=u-w", 00500] => 00550, + ["g=u-w", 00400] => 00440, + ["g=u-w", 00300] => 00310, + ["g=u-w", 00200] => 00200, + ["g=u-w", 00100] => 00110, + ["g=u-w", 00000] => 00000, + # Cruel, but legal, use of the action set. + ["g=u+r-w", 0300] => 00350, + # Empty assignments. + ["u=", 00000] => 00000, + ["u=", 00600] => 00000, + ["ug=", 00000] => 00000, + ["ug=", 00600] => 00000, + ["ug=", 00660] => 00000, + ["ug=", 00666] => 00006, + ["=", 00000] => 00000, + ["=", 00666] => 00000, + ["+", 00000] => 00000, + ["+", 00124] => 00124, + ["-", 00000] => 00000, + ["-", 00124] => 00124, + }.each do |input, result| + from = input.is_a?(Array) ? "#{input[0]}, 0#{input[1].to_s(8)}" : input + it "should map #{from.inspect} to #{result.inspect}" do + symbolic_mode_to_int(*input).should == result + end + end + + # Now, test some failure modes. + it "should fail if no mode is given" do + expect { symbolic_mode_to_int('') }. + to raise_error Puppet::Error, /empty mode string/ + end + + %w{u g o ug uo go ugo a uu u/x u!x u=r,,g=r}.each do |input| + it "should fail if no (valid) action is given: #{input.inspect}" do + expect { symbolic_mode_to_int(input) }. + to raise_error Puppet::Error, /Missing action/ + end + end + + %w{u+q u-rwF u+rw,g+rw,o+RW}.each do |input| + it "should fail with unknown op #{input.inspect}" do + expect { symbolic_mode_to_int(input) }. + to raise_error Puppet::Error, /Unknown operation/ + end + end + + it "should refuse to subtract the conditional execute op" do + expect { symbolic_mode_to_int("o-rwX") }. + to raise_error Puppet::Error, /only works with/ + end + + it "should refuse to set to the conditional execute op" do + expect { symbolic_mode_to_int("o=rwX") }. + to raise_error Puppet::Error, /only works with/ + end + + %w{8 08 9 09 118 119}.each do |input| + it "should fail for decimal modes: #{input.inspect}" do + expect { symbolic_mode_to_int(input) }. + to raise_error Puppet::Error, /octal/ + end + end + + it "should set the execute bit on a directory, without exec in original" do + symbolic_mode_to_int("u+X", 0444, true).to_s(8).should == "544" + symbolic_mode_to_int("g+X", 0444, true).to_s(8).should == "454" + symbolic_mode_to_int("o+X", 0444, true).to_s(8).should == "445" + symbolic_mode_to_int("+X", 0444, true).to_s(8).should == "555" + end + + it "should set the execute bit on a file with exec in the original" do + symbolic_mode_to_int("+X", 0544).to_s(8).should == "555" + end + + it "should not set the execute bit on a file without exec on the original even if set by earlier DSL" do + symbolic_mode_to_int("u+x,go+X", 0444).to_s(8).should == "544" + end + end +end