diff --git a/lib/puppet/type/cron.rb b/lib/puppet/type/cron.rb index b3f5e608d..ebbef72a1 100755 --- a/lib/puppet/type/cron.rb +++ b/lib/puppet/type/cron.rb @@ -1,414 +1,414 @@ require 'etc' require 'facter' require 'puppet/util/filetype' Puppet::Type.newtype(:cron) do @doc = "Installs and manages cron jobs. All fields except the command and the user are optional, although specifying no periodic fields would result in the command being executed every minute. While the name of the cron job is not part of the actual job, it is used by Puppet to store and retrieve it. If you specify a cron job that matches an existing job in every way except name, then the jobs will be considered equivalent and the new name will be permanently associated with that job. Once this association is made and synced to disk, you can then manage the job normally (e.g., change the schedule of the job). - Example:: - - cron { logrotate: - command => \"/usr/sbin/logrotate\", - user => root, - hour => 2, - minute => 0 - } - - Note that all cron values can be specified as an array of values:: - - cron { logrotate: - command => \"/usr/sbin/logrotate\", - user => root, - hour => [2, 4] - } - - Or using ranges, or the step syntax ``*/2`` (although there's no guarantee that - your ``cron`` daemon supports it):: - - cron { logrotate: - command => \"/usr/sbin/logrotate\", - user => root, - hour => ['2-4'], - minute => '*/10' - } + Example + + cron { logrotate: + command => \"/usr/sbin/logrotate\", + user => root, + hour => 2, + minute => 0 + } + + Note that all cron values can be specified as an array of values + + cron { logrotate: + command => \"/usr/sbin/logrotate\", + user => root, + hour => [2, 4] + } + + Or using ranges, or the step syntax `*/2` (although there's no guarantee that + your `cron` daemon supports it) + + cron { logrotate: + command => \"/usr/sbin/logrotate\", + user => root, + hour => ['2-4'], + minute => '*/10' + } " ensurable # A base class for all of the Cron parameters, since they all have # similar argument checking going on. class CronParam < Puppet::Property class << self attr_accessor :boundaries, :default end # We have to override the parent method, because we consume the entire # "should" array def insync?(is) if @should self.is_to_s(is) == self.should_to_s else true end end # A method used to do parameter input handling. Converts integers # in string form to actual integers, and returns the value if it's # an integer or false if it's just a normal string. def numfix(num) if num =~ /^\d+$/ return num.to_i elsif num.is_a?(Integer) return num else return false end end # Verify that a number is within the specified limits. Return the # number if it is, or false if it is not. def limitcheck(num, lower, upper) (num >= lower and num <= upper) && num end # Verify that a value falls within the specified array. Does case # insensitive matching, and supports matching either the entire word # or the first three letters of the word. def alphacheck(value, ary) tmp = value.downcase # If they specified a shortened version of the name, then see # if we can lengthen it (e.g., mon => monday). if tmp.length == 3 ary.each_with_index { |name, index| if name =~ /#{tmp}/i return index end } else return ary.index(tmp) if ary.include?(tmp) end false end def should_to_s(newvalue = @should) if newvalue newvalue = [newvalue] unless newvalue.is_a?(Array) if self.name == :command or newvalue[0].is_a? Symbol newvalue[0] else newvalue.join(",") end else nil end end def is_to_s(currentvalue = @is) if currentvalue return currentvalue unless currentvalue.is_a?(Array) if self.name == :command or currentvalue[0].is_a? Symbol currentvalue[0] else currentvalue.join(",") end else nil end end def should if @should and @should[0] == :absent :absent else @should end end def should=(ary) super @should.flatten! end # The method that does all of the actual parameter value # checking; called by all of the +param=+ methods. # Requires the value, type, and bounds, and optionally supports # a boolean of whether to do alpha checking, and if so requires # the ary against which to do the checking. munge do |value| # Support 'absent' as a value, so that they can remove # a value if value == "absent" or value == :absent return :absent end # Allow the */2 syntax if value =~ /^\*\/[0-9]+$/ return value end # Allow ranges if value =~ /^[0-9]+-[0-9]+$/ return value end # Allow ranges + */2 if value =~ /^[0-9]+-[0-9]+\/[0-9]+$/ return value end if value == "*" return value end return value unless self.class.boundaries lower, upper = self.class.boundaries retval = nil if num = numfix(value) retval = limitcheck(num, lower, upper) elsif respond_to?(:alpha) # If it has an alpha method defined, then we check # to see if our value is in that list and if so we turn # it into a number retval = alphacheck(value, alpha) end if retval return retval.to_s else self.fail "#{value} is not a valid #{self.class.name}" end end end # Somewhat uniquely, this property does not actually change anything -- it # just calls +@resource.sync+, which writes out the whole cron tab for # the user in question. There is no real way to change individual cron # jobs without rewriting the entire cron file. # # Note that this means that managing many cron jobs for a given user # could currently result in multiple write sessions for that user. newproperty(:command, :parent => CronParam) do desc "The command to execute in the cron job. The environment provided to the command varies by local system rules, and it is best to always provide a fully qualified command. The user's profile is not sourced when the command is run, so if the user's environment is desired it should be sourced manually. - All cron parameters support ``absent`` as a value; this will + All cron parameters support `absent` as a value; this will remove any existing values for that field." def retrieve return_value = super return_value = return_value[0] if return_value && return_value.is_a?(Array) return_value end def should if @should if @should.is_a? Array @should[0] else devfail "command is not an array" end else nil end end end newproperty(:special) do desc "Special schedules only supported on FreeBSD." def specials %w{reboot yearly annually monthly weekly daily midnight hourly} end validate do |value| raise ArgumentError, "Invalid special schedule #{value.inspect}" unless specials.include?(value) end end newproperty(:minute, :parent => CronParam) do self.boundaries = [0, 59] desc "The minute at which to run the cron job. Optional; if specified, must be between 0 and 59, inclusive." end newproperty(:hour, :parent => CronParam) do self.boundaries = [0, 23] desc "The hour at which to run the cron job. Optional; if specified, must be between 0 and 23, inclusive." end newproperty(:weekday, :parent => CronParam) do def alpha %w{sunday monday tuesday wednesday thursday friday saturday} end self.boundaries = [0, 7] desc "The weekday on which to run the command. Optional; if specified, must be between 0 and 7, inclusive, with 0 (or 7) being Sunday, or must be the name of the day (e.g., Tuesday)." end newproperty(:month, :parent => CronParam) do def alpha %w{january february march april may june july august september october november december} end self.boundaries = [1, 12] desc "The month of the year. Optional; if specified must be between 1 and 12 or the month name (e.g., December)." end newproperty(:monthday, :parent => CronParam) do self.boundaries = [1, 31] desc "The day of the month on which to run the command. Optional; if specified, must be between 1 and 31." end newproperty(:environment) do desc "Any environment settings associated with this cron job. They will be stored between the header and the job in the crontab. There can be no guarantees that other, earlier settings will not also affect a given cron job. Also, Puppet cannot automatically determine whether an existing, unmanaged environment setting is associated with a given cron job. If you already have cron jobs with environment settings, then Puppet will keep those settings in the same place in the file, but will not associate them with a specific job. Settings should be specified exactly as they should appear in - the crontab, e.g., ``PATH=/bin:/usr/bin:/usr/sbin``." + the crontab, e.g., `PATH=/bin:/usr/bin:/usr/sbin`." validate do |value| unless value =~ /^\s*(\w+)\s*=\s*(.*)\s*$/ or value == :absent or value == "absent" raise ArgumentError, "Invalid environment setting #{value.inspect}" end end def insync?(is) if is.is_a? Array return is.sort == @should.sort else return is == @should end end def is_to_s(newvalue) if newvalue if newvalue.is_a?(Array) newvalue.join(",") else newvalue end else nil end end def should @should end def should_to_s(newvalue = @should) if newvalue newvalue.join(",") else nil end end end newparam(:name) do desc "The symbolic name of the cron job. This name is used for human reference only and is generated automatically for cron jobs found on the system. This generally won't matter, as Puppet will do its best to match existing cron jobs against specified jobs (and Puppet adds a comment to cron jobs it adds), but it is at least possible that converting from unmanaged jobs to managed jobs might require manual intervention." isnamevar end newproperty(:user) do desc "The user to run the command as. This user must be allowed to run cron jobs, which is not currently checked by Puppet. The user defaults to whomever Puppet is running as." defaultto { Etc.getpwuid(Process.uid).name || "root" } end newproperty(:target) do desc "Where the cron job should be stored. For crontab-style entries this is the same as the user and defaults that way. Other providers default accordingly." defaultto { if provider.is_a?(@resource.class.provider(:crontab)) if val = @resource.should(:user) val else raise ArgumentError, "You must provide a user with crontab entries" end elsif provider.class.ancestors.include?(Puppet::Provider::ParsedFile) provider.class.default_target else nil end } end # We have to reorder things so that :provide is before :target attr_accessor :uid def value(name) name = symbolize(name) ret = nil if obj = @parameters[name] ret = obj.should ret ||= obj.retrieve if ret == :absent ret = nil end end unless ret case name when :command devfail "No command, somehow" when :special # nothing else #ret = (self.class.validproperty?(name).default || "*").to_s ret = "*" end end ret end end diff --git a/lib/puppet/type/file.rb b/lib/puppet/type/file.rb index 71f2756bc..f35a26408 100644 --- a/lib/puppet/type/file.rb +++ b/lib/puppet/type/file.rb @@ -1,798 +1,798 @@ require 'digest/md5' require 'cgi' require 'etc' require 'uri' require 'fileutils' require 'puppet/network/handler' require 'puppet/util/diff' require 'puppet/util/checksums' require 'puppet/network/client' require 'puppet/util/backups' Puppet::Type.newtype(:file) do include Puppet::Util::MethodHelper include Puppet::Util::Checksums include Puppet::Util::Backups @doc = "Manages local files, including setting ownership and permissions, creation of both files and directories, and retrieving entire files from remote servers. As Puppet matures, it - expected that the ``file`` resource will be used less and less to + 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." def self.title_patterns [ [ /^(.*?)\/*\Z/m, [ [ :path, lambda{|x| x} ] ] ] ] end newparam(:path) do desc "The path to the file to manage. Must be fully qualified." isnamevar validate do |value| # accept various path syntaxes: lone slash, posix, win32, unc unless (Puppet.features.posix? and (value =~ /^\/$/ or value =~ /^\/[^\/]/)) or (Puppet.features.microsoft_windows? and (value =~ /^.:\// or value =~ /^\/\/[^\/]+\/[^\/]+/)) fail Puppet::Error, "File paths must be fully qualified, not '#{value}'" end end # convert the current path in an index into the collection and the last # path name. The aim is to use less storage for all common paths in a hierarchy munge do |value| path, name = File.split(value.gsub(/\/+/,'/')) { :index => Puppet::FileCollection.collection.index(path), :name => name } end # and the reverse unmunge do |value| basedir = Puppet::FileCollection.collection.path(value[:index]) # a lone slash as :name indicates a root dir on windows if value[:name] == '/' basedir else File.join( basedir, value[:name] ) end end end newparam(:backup) do desc "Whether files should be backed up before being replaced. The preferred method of backing files up is via - a ``filebucket``, which stores files by their MD5 sums and allows + 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 + 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 + backup. Setting `backup => false` disables all backups of the file in question. - Puppet automatically creates a local filebucket named ``puppet`` and + 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:: + you must specify one in your configuration - filebucket { main: - server => puppet - } + filebucket { main: + server => puppet + } - The ``puppet master`` daemon creates a filebucket by default, + 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:: + configuration, you can use it in any file - file { \"/my/file\": - source => \"/path/in/nfs/or/something\", - backup => main - } + file { \"/my/file\": + source => \"/path/in/nfs/or/something\", + backup => main + } This will back the file up to the central server. At this point, the benefits of using a filebucket are that you do not have backup files lying around on each of your machines, a given version of a file is only backed up once, and you can restore any given file manually, no matter how old. Eventually, transactional support will be able to automatically restore filebucketed files. " defaultto "puppet" munge do |value| # I don't really know how this is happening. value = value.shift if value.is_a?(Array) case value when false, "false", :false false when true, "true", ".puppet-bak", :true ".puppet-bak" when String value else self.fail "Invalid backup type #{value.inspect}" end end end newparam(:recurse) do desc "Whether and how deeply to do recursive management." 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]*``. + engine, so shell metacharacters are fully supported, e.g. `[a-z]*`. Matches that would descend into the directory structure are ignored, - e.g., ``*/*``." + 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 + `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 + 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``, + 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 any parent directories. autorequire(:file) do basedir = File.dirname(self[:path]) if basedir != self[:path] basedir else nil end end # Autorequire the owner and group of the file. {:user => :owner, :group => :group}.each do |type, property| autorequire(type) do if @parameters.include?(property) # The user/group property automatically converts to IDs next unless should = @parameters[property].shouldorig val = should[0] if val.is_a?(Integer) or val =~ /^\d+$/ nil else val end end end end CREATORS = [:content, :source, :target] validate do count = 0 CREATORS.each do |param| count += 1 if self.should(param) end count += 1 if @parameters.include?(:source) self.fail "You cannot specify more than one of #{CREATORS.collect { |p| p.to_s}.join(", ")}" if count > 1 self.fail "You cannot specify a remote recursion without a source" if !self[:source] and self[:recurse] == :remote self.warning "Possible error: recurselimit is set but not recurse, no recursion will happen" if !self[:recurse] and self[:recurselimit] end def self.[](path) return nil unless path super(path.gsub(/\/+/, '/').sub(/\/$/, '')) end # List files, but only one level deep. def self.instances(base = "/") return [] unless FileTest.directory?(base) files = [] Dir.entries(base).reject { |e| e == "." or e == ".." }.each do |name| path = File.join(base, name) if obj = self[path] obj[:audit] = :all files << obj else files << self.new( :name => path, :audit => :all ) end end files end @depthfirst = false # Determine the user to write files as. def asuser if self.should(:owner) and ! self.should(:owner).is_a?(Symbol) writeable = Puppet::Util::SUIDManager.asuser(self.should(:owner)) { FileTest.writable?(File.dirname(self[:path])) } # If the parent directory is writeable, then we execute # as the user in question. Otherwise we'll rely on # the 'owner' property to do things. asuser = self.should(:owner) if writeable end asuser end def bucket return @bucket if @bucket backup = self[:backup] return nil unless backup return nil if backup =~ /^\./ unless catalog or backup == "puppet" fail "Can not find filebucket for backups without a catalog" end unless catalog and filebucket = catalog.resource(:filebucket, backup) or backup == "puppet" fail "Could not find filebucket #{backup} specified in backup" end return default_bucket unless filebucket @bucket = filebucket.bucket @bucket end def default_bucket Puppet::Type.type(:filebucket).mkdefaultbucket.bucket end # Does the file currently exist? Just checks for whether # we have a stat def exist? stat ? true : false end # We have to do some extra finishing, to retrieve our bucket if # there is one. def finish # Look up our bucket, if there is one bucket super end # Create any children via recursion or whatever. def eval_generate return [] unless self.recurse? recurse #recurse.reject do |resource| # catalog.resource(:file, resource[:path]) #end.each do |child| # catalog.add_resource child # catalog.relationship_graph.add_edge self, child #end end def flush # We want to make sure we retrieve metadata anew on each transaction. @parameters.each do |name, param| param.flush if param.respond_to?(:flush) end @stat = nil 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 = nil 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 = {} children = recurse_local if self[:recurse] != :remote 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? return false unless @parameters.include?(:recurse) val = @parameters[:recurse].value !!(val and (val == true or val == :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.search( path, :links => self[:links], :recurse => (self[:recurse] == :remote ? true : self[:recurse]), :recurselimit => self[:recurselimit], :ignore => self[:ignore], :checksum_type => (self[:source] || self[:content]) ? self[:checksum] : :none ) end # Remove any existing data. This is only used when dealing with # links or directories. def remove_existing(should) return unless s = stat self.fail "Could not back up; will not replace" unless perform_backup unless should.to_s == "link" return if s.ftype.to_s == should.to_s end case s.ftype when "directory" if self[:force] == :true debug "Removing existing directory for replacement with #{should}" FileUtils.rmtree(self[:path]) else notice "Not removing directory; use 'force' to override" end when "link", "file" debug "Removing existing #{s.ftype} for replacement with #{should}" File.unlink(self[:path]) else self.fail "Could not back up files of type #{s.ftype}" end expire 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). cached_attr(:stat) do method = :stat # Files are the only types that support links if (self.class.name == :file and self[:links] != :follow) or self.class.name == :tidy method = :lstat end path = self[:path] begin File.send(method, self[:path]) rescue Errno::ENOENT => error return nil rescue Errno::EACCES => error warning "Could not stat; permission denied" return 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 content_checksum = Puppet::Util.withumask(umask) { File.open(path, 'w', mode) { |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 expire currentvalue = thing.retrieve thing.sync unless thing.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 diff --git a/lib/puppet/type/group.rb b/lib/puppet/type/group.rb index 9f26d2243..19753d15b 100755 --- a/lib/puppet/type/group.rb +++ b/lib/puppet/type/group.rb @@ -1,100 +1,100 @@ require 'etc' require 'facter' module Puppet newtype(:group) do @doc = "Manage groups. On most platforms this can only create groups. Group membership must be managed on individual users. On some platforms such as OS X, group membership is managed as an attribute of the group, not the user record. Providers must have the feature 'manages_members' to manage the 'members' property of a group record." feature :manages_members, "For directories where membership is an attribute of groups not users." ensurable do desc "Create or remove the group." newvalue(:present) do provider.create end newvalue(:absent) do provider.delete end end newproperty(:gid) do desc "The group ID. Must be specified numerically. If not specified, a number will be picked, which can result in ID differences across systems and thus is not recommended. The GID is picked according to local system standards." def retrieve provider.gid end def sync if self.should == :absent raise Puppet::DevError, "GID cannot be deleted" else provider.gid = self.should end end munge do |gid| case gid when String if gid =~ /^[-0-9]+$/ gid = Integer(gid) else self.fail "Invalid GID #{gid}" end when Symbol unless gid == :absent self.devfail "Invalid GID #{gid}" end end return gid end end newproperty(:members, :array_matching => :all, :required_features => :manages_members) do desc "The members of the group. For directory services where group membership is stored in the group objects, not the users." def change_to_s(currentvalue, newvalue) currentvalue = currentvalue.join(",") if currentvalue != :absent newvalue = newvalue.join(",") super(currentvalue, newvalue) end end newparam(:auth_membership) do desc "whether the provider is authoritative for group membership." defaultto true end newparam(:name) do desc "The group name. While naming limitations vary by system, it is advisable to keep the name to the degenerate limitations, which is a maximum of 8 characters beginning with a letter." isnamevar end newparam(:allowdupe, :boolean => true) do desc "Whether to allow duplicate GIDs. This option does not work on - FreeBSD (contract to the ``pw`` man page)." + FreeBSD (contract to the `pw` man page)." newvalues(:true, :false) defaultto false end end end diff --git a/lib/puppet/type/host.rb b/lib/puppet/type/host.rb index 929542d7c..7169d12c4 100755 --- a/lib/puppet/type/host.rb +++ b/lib/puppet/type/host.rb @@ -1,101 +1,101 @@ module Puppet newtype(:host) do ensurable newproperty(:ip) do desc "The host's IP address, IPv4 or IPv6." validate do |value| unless value =~ /((([0-9a-fA-F]+:){7}[0-9a-fA-F]+)|(([0-9a-fA-F]+:)*[0-9a-fA-F]+)?::(([0-9a-fA-F]+:)*[0-9a-fA-F]+)?)|((25[0-5]|2[0-4][\d]|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3})/ raise Puppet::Error, "Invalid IP address" end end end newproperty(:host_aliases) do desc 'Any aliases the host might have. Multiple values must be specified as an array. Note that this property is not the same as the "alias" metaparam; use this property to add aliases to a host on disk, and "alias" to aliases for use in your Puppet scripts.' def insync?(is) is == @should end def is_to_s(currentvalue = @is) currentvalue = [currentvalue] unless currentvalue.is_a? Array currentvalue.join(" ") end def retrieve is = super case is when String is = is.split(/\s*,\s*/) when Symbol is = [is] when Array # nothing else raise Puppet::DevError, "Invalid @is type #{is.class}" end is end # We actually want to return the whole array here, not just the first # value. def should if defined?(@should) if @should == [:absent] return :absent else return @should end else return nil end end def should_to_s(newvalue = @should) newvalue.join(" ") end validate do |value| raise Puppet::Error, "Host aliases cannot include whitespace" if value =~ /\s/ end end newproperty(:target) do desc "The file in which to store service information. Only used by those providers that write to disk." defaultto { if @resource.class.defaultprovider.ancestors.include?(Puppet::Provider::ParsedFile) @resource.class.defaultprovider.default_target else nil end } end newparam(:name) do desc "The host name." isnamevar validate do |value| # LAK:NOTE See http://snurl.com/21zf8 [groups_google_com] x = value.split('.').each do |hostpart| unless hostpart =~ /^([\d\w]+|[\d\w][\d\w\-]+[\d\w])$/ raise Puppet::Error, "Invalid host name" end end end end @doc = "Installs and manages host entries. For most systems, these - entries will just be in ``/etc/hosts``, but some systems (notably OS X) + entries will just be in `/etc/hosts`, but some systems (notably OS X) will have different solutions." end end diff --git a/lib/puppet/type/mount.rb b/lib/puppet/type/mount.rb index 6ee5dd174..78bb5002f 100755 --- a/lib/puppet/type/mount.rb +++ b/lib/puppet/type/mount.rb @@ -1,225 +1,225 @@ module Puppet # We want the mount to refresh when it changes. newtype(:mount, :self_refresh => true) do @doc = "Manages mounted filesystems, including putting mount information into the mount table. The actual behavior depends on the value of the 'ensure' parameter. - Note that if a ``mount`` receives an event from another resource, - it will try to remount the filesystems if ``ensure`` is set to ``mounted``." + Note that if a `mount` receives an event from another resource, + it will try to remount the filesystems if `ensure` is set to `mounted`." feature :refreshable, "The provider can remount the filesystem.", :methods => [:remount] # Use the normal parent class, because we actually want to # call code when sync is called. newproperty(:ensure) do desc "Control what to do with this mount. Set this attribute to - ``umounted`` to make sure the filesystem is in the filesystem table - but not mounted (if the filesystem is currently mounted, it will be unmounted). Set it to ``absent`` to unmount (if necessary) and remove - the filesystem from the fstab. Set to ``mounted`` to add it to the - fstab and mount it. Set to ``present`` to add to fstab but not change + `umounted` to make sure the filesystem is in the filesystem table + but not mounted (if the filesystem is currently mounted, it will be unmounted). Set it to `absent` to unmount (if necessary) and remove + the filesystem from the fstab. Set to `mounted` to add it to the + fstab and mount it. Set to `present` to add to fstab but not change mount/unmount status" newvalue(:defined) do provider.create return :mount_created end aliasvalue :present, :defined newvalue(:unmounted) do if provider.mounted? syncothers provider.unmount return :mount_unmounted else provider.create return :mount_created end end newvalue(:absent, :event => :mount_deleted) do provider.unmount if provider.mounted? provider.destroy end newvalue(:mounted, :event => :mount_mounted) do # Create the mount point if it does not already exist. current_value = self.retrieve provider.create if current_value.nil? or current_value == :absent syncothers # The fs can be already mounted if it was absent but mounted provider.mount unless provider.mounted? end def insync?(is) if should == :defined and is != :absent true else super end end def retrieve # We need to special case :mounted; if we're absent, we still # want curval = super() if curval == :absent return :absent elsif provider.mounted? return :mounted else return :unmounted end end def syncothers # We have to flush any changes to disk. currentvalues = @resource.retrieve_resource # Determine if there are any out-of-sync properties. oos = @resource.send(:properties).find_all do |prop| unless currentvalues.include?(prop) raise Puppet::DevError, "Parent has property %s but it doesn't appear in the current values", [prop.name] end if prop.name == :ensure false else ! prop.insync?(currentvalues[prop]) end end.each { |prop| prop.sync }.length @resource.flush if oos > 0 end end newproperty(:device) do desc "The device providing the mount. This can be whatever device is supporting by the mount, including network devices or devices specified by UUID rather than device path, depending on the operating system." end # Solaris specifies two devices, not just one. newproperty(:blockdevice) do desc "The device to fsck. This is property is only valid on Solaris, and in most cases will default to the correct value." # Default to the device but with "dsk" replaced with "rdsk". defaultto do if Facter["operatingsystem"].value == "Solaris" device = @resource.value(:device) if device =~ %r{/dsk/} device.sub(%r{/dsk/}, "/rdsk/") else nil end else nil end end end newproperty(:fstype) do desc "The mount type. Valid values depend on the operating system. This is a required option." end newproperty(:options) do desc "Mount options for the mounts, as they would appear in the fstab." end newproperty(:pass) do desc "The pass in which the mount is checked." defaultto { 0 if @resource.managed? } end newproperty(:atboot) do desc "Whether to mount the mount at boot. Not all platforms support this." end newproperty(:dump) do desc "Whether to dump the mount. Not all platform support this. - Valid values are ``1`` or ``0``. or ``2`` on FreeBSD, Default is ``0``." + Valid values are `1` or `0`. or `2` on FreeBSD, Default is `0`." if Facter["operatingsystem"].value == "FreeBSD" newvalue(%r{(0|1|2)}) else newvalue(%r{(0|1)}) end newvalue(%r{(0|1)}) defaultto { 0 if @resource.managed? } end newproperty(:target) do desc "The file in which to store the mount table. Only used by those providers that write to disk." defaultto { if @resource.class.defaultprovider.ancestors.include?(Puppet::Provider::ParsedFile) @resource.class.defaultprovider.default_target else nil end } end newparam(:name) do desc "The mount path for the mount." isnamevar end newparam(:path) do - desc "The deprecated name for the mount point. Please use ``name`` now." + desc "The deprecated name for the mount point. Please use `name` now." def value=(value) warning "'path' is deprecated for mounts. Please use 'name'." @resource[:name] = value super end end newparam(:remounts) do - desc "Whether the mount can be remounted ``mount -o remount``. If + desc "Whether the mount can be remounted `mount -o remount`. If this is false, then the filesystem will be unmounted and remounted manually, which is prone to failure." newvalues(:true, :false) defaultto do case Facter.value(:operatingsystem) when "FreeBSD", "Darwin" false else true end end end def refresh # Only remount if we're supposed to be mounted. provider.remount if self.should(:fstype) != "swap" and provider.mounted? end def value(name) name = symbolize(name) ret = nil if property = @parameters[name] return property.value end end end end