aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorJorie Tappa <jorie@jorietappa.com>2018-07-31 14:56:46 -0500
committerJorie Tappa <jorie@jorietappa.com>2018-07-31 16:07:37 -0500
commita2af7dd0b9713f279724d2c7e6f17bfd8ce2d95b (patch)
tree6a90efd716578c7449ec69051279150308884414 /lib
parente74dce11298298889a40879aad1e2fcc27fa0559 (diff)
downloadpuppet-cron_core-a2af7dd0b9713f279724d2c7e6f17bfd8ce2d95b.tar.gz
puppet-cron_core-a2af7dd0b9713f279724d2c7e6f17bfd8ce2d95b.tar.bz2
Initial cron import from puppet 7a4c5f07bdf61a7bc7aa32a50e99489a604eac52
Diffstat (limited to 'lib')
-rw-r--r--lib/puppet/provider/cron/crontab.rb297
-rw-r--r--lib/puppet/type/cron.rb480
2 files changed, 777 insertions, 0 deletions
diff --git a/lib/puppet/provider/cron/crontab.rb b/lib/puppet/provider/cron/crontab.rb
new file mode 100644
index 0000000..b24ed14
--- /dev/null
+++ b/lib/puppet/provider/cron/crontab.rb
@@ -0,0 +1,297 @@
+require 'puppet/provider/parsedfile'
+
+Puppet::Type.type(:cron).provide(:crontab, :parent => Puppet::Provider::ParsedFile, :default_target => ENV["USER"] || "root") do
+ commands :crontab => "crontab"
+
+ text_line :comment, :match => %r{^\s*#}, :post_parse => proc { |record|
+ record[:name] = $1 if record[:line] =~ /Puppet Name: (.+)\s*$/
+ }
+
+ text_line :blank, :match => %r{^\s*$}
+
+ text_line :environment, :match => %r{^\s*\w+\s*=}
+
+ def self.filetype
+ tabname = case Facter.value(:osfamily)
+ when "Solaris"
+ :suntab
+ when "AIX"
+ :aixtab
+ else
+ :crontab
+ end
+
+ Puppet::Util::FileType.filetype(tabname)
+ end
+
+ self::TIME_FIELDS = [:minute, :hour, :monthday, :month, :weekday]
+
+ record_line :crontab,
+ :fields => %w{time command},
+ :match => %r{^\s*(@\w+|\S+\s+\S+\s+\S+\s+\S+\s+\S+)\s+(.+)$},
+ :absent => '*',
+ :block_eval => :instance do
+
+ def post_parse(record)
+ time = record.delete(:time)
+ if match = /@(\S+)/.match(time)
+ # is there another way to access the constant?
+ Puppet::Type::Cron::ProviderCrontab::TIME_FIELDS.each { |f| record[f] = :absent }
+ record[:special] = match.captures[0]
+ elsif match = /(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)/.match(time)
+ record[:special] = :absent
+ Puppet::Type::Cron::ProviderCrontab::TIME_FIELDS.zip(match.captures).each do |field,value|
+ if value == self.absent
+ record[field] = :absent
+ else
+ record[field] = value.split(",")
+ end
+ end
+ else
+ raise Puppet::Error, _("Line got parsed as a crontab entry but cannot be handled. Please file a bug with the contents of your crontab")
+ end
+ record
+ end
+
+ def pre_gen(record)
+ if record[:special] and record[:special] != :absent
+ record[:special] = "@#{record[:special]}"
+ end
+
+ Puppet::Type::Cron::ProviderCrontab::TIME_FIELDS.each do |field|
+ if vals = record[field] and vals.is_a?(Array)
+ record[field] = vals.join(",")
+ end
+ end
+ record
+ end
+
+ def to_line(record)
+ str = ""
+ record[:name] = nil if record[:unmanaged]
+ str = "# Puppet Name: #{record[:name]}\n" if record[:name]
+ if record[:environment] and record[:environment] != :absent
+ str += record[:environment].map {|line| "#{line}\n"}.join('')
+ end
+ if record[:special] and record[:special] != :absent
+ fields = [:special, :command]
+ else
+ fields = Puppet::Type::Cron::ProviderCrontab::TIME_FIELDS + [:command]
+ end
+ str += record.values_at(*fields).map do |field|
+ if field.nil? or field == :absent
+ self.absent
+ else
+ field
+ end
+ end.join(self.joiner)
+ str
+ end
+ end
+
+ def create
+ if resource.should(:command) then
+ super
+ else
+ resource.err _("no command specified, cannot create")
+ end
+ end
+
+ # Look up a resource with a given name whose user matches a record target
+ #
+ # @api private
+ #
+ # @note This overrides the ParsedFile method for finding resources by name,
+ # so that only records for a given user are matched to resources of the
+ # same user so that orphaned records in other crontabs don't get falsely
+ # matched (#2251)
+ #
+ # @param [Hash<Symbol, Object>] record
+ # @param [Array<Puppet::Resource>] resources
+ #
+ # @return [Puppet::Resource, nil] The resource if found, else nil
+ def self.resource_for_record(record, resources)
+ resource = super
+
+ if resource
+ target = resource[:target] || resource[:user]
+ if record[:target] == target
+ resource
+ end
+ end
+ end
+
+ # Return the header placed at the top of each generated file, warning
+ # users that modifying this file manually is probably a bad idea.
+ def self.header
+%{# HEADER: This file was autogenerated at #{Time.now} by puppet.
+# HEADER: While it can still be managed manually, it is definitely not recommended.
+# HEADER: Note particularly that the comments starting with 'Puppet Name' should
+# HEADER: not be deleted, as doing so could cause duplicate cron jobs.\n}
+ end
+
+ # Regex for finding one vixie cron header.
+ def self.native_header_regex
+ /# DO NOT EDIT THIS FILE.*?Cron version.*?vixie.*?\n/m
+ end
+
+ # If a vixie cron header is found, it should be dropped, cron will insert
+ # a new one in any case, so we need to avoid duplicates.
+ def self.drop_native_header
+ true
+ end
+
+ # See if we can match the record against an existing cron job.
+ def self.match(record, resources)
+ # if the record is named, do not even bother (#19876)
+ # except the resource name was implicitly generated (#3220)
+ return false if record[:name] and !record[:unmanaged]
+ resources.each do |name, resource|
+ # Match the command first, since it's the most important one.
+ next unless record[:target] == resource[:target]
+ next unless record[:command] == resource.value(:command)
+
+ # Now check the time fields
+ compare_fields = self::TIME_FIELDS + [:special]
+
+ matched = true
+ compare_fields.each do |field|
+ # If the resource does not manage a property (say monthday) it should
+ # always match. If it is the other way around (e.g. resource defines
+ # a should value for :special but the record does not have it, we do
+ # not match
+ next unless resource[field]
+ unless record.include?(field)
+ matched = false
+ break
+ end
+
+ if record_value = record[field] and resource_value = resource.value(field)
+ # The record translates '*' into absent in the post_parse hook and
+ # the resource type does exactly the opposite (alias :absent to *)
+ next if resource_value == '*' and record_value == :absent
+ next if resource_value == record_value
+ end
+ matched =false
+ break
+ end
+ return resource if matched
+ end
+ false
+ end
+
+ @name_index = 0
+
+ # Collapse name and env records.
+ def self.prefetch_hook(records)
+ name = nil
+ envs = nil
+ result = records.each { |record|
+ case record[:record_type]
+ when :comment
+ if record[:name]
+ name = record[:name]
+ record[:skip] = true
+
+ # Start collecting env values
+ envs = []
+ end
+ when :environment
+ # If we're collecting env values (meaning we're in a named cronjob),
+ # store the line and skip the record.
+ if envs
+ envs << record[:line]
+ record[:skip] = true
+ end
+ when :blank
+ # nothing
+ else
+ if name
+ record[:name] = name
+ name = nil
+ else
+ cmd_string = record[:command].gsub(/\s+/, "_")
+ index = ( @name_index += 1 )
+ record[:name] = "unmanaged:#{cmd_string}-#{ index.to_s }"
+ record[:unmanaged] = true
+ end
+ if envs.nil? or envs.empty?
+ record[:environment] = :absent
+ else
+ # Collect all of the environment lines, and mark the records to be skipped,
+ # since their data is included in our crontab record.
+ record[:environment] = envs
+
+ # And turn off env collection again
+ envs = nil
+ end
+ end
+ }.reject { |record| record[:skip] }
+ result
+ end
+
+ def self.to_file(records)
+ text = super
+ # Apparently Freebsd will "helpfully" add a new TZ line to every
+ # single cron line, but not in all cases (e.g., it doesn't do it
+ # on my machine). This is my attempt to fix it so the TZ lines don't
+ # multiply.
+ if text =~ /(^TZ=.+\n)/
+ tz = $1
+ text.sub!(tz, '')
+ text = tz + text
+ end
+ text
+ end
+
+ def user=(user)
+ # we have to mark the target as modified first, to make sure that if
+ # we move a cronjob from userA to userB, userA's crontab will also
+ # be rewritten
+ mark_target_modified
+ @property_hash[:user] = user
+ @property_hash[:target] = user
+ end
+
+ def user
+ @property_hash[:user] || @property_hash[:target]
+ end
+
+ CRONTAB_DIR = case Facter.value("osfamily")
+ when "Debian", "HP-UX"
+ "/var/spool/cron/crontabs"
+ when /BSD/
+ "/var/cron/tabs"
+ when "Darwin"
+ "/usr/lib/cron/tabs/"
+ else
+ "/var/spool/cron"
+ end
+
+ # Yield the names of all crontab files stored on the local system.
+ #
+ # @note Ignores files that are not writable for the puppet process.
+ #
+ # @api private
+ def self.enumerate_crontabs
+ Puppet.debug "looking for crontabs in #{CRONTAB_DIR}"
+ return unless File.readable?(CRONTAB_DIR)
+ Dir.foreach(CRONTAB_DIR) do |file|
+ path = "#{CRONTAB_DIR}/#{file}"
+ yield(file) if File.file?(path) and File.writable?(path)
+ end
+ end
+
+
+ # Include all plausible crontab files on the system
+ # in the list of targets (#11383 / PUP-1381)
+ def self.targets(resources = nil)
+ targets = super(resources)
+ enumerate_crontabs do |target|
+ targets << target
+ end
+ targets.uniq
+ end
+
+end
+
diff --git a/lib/puppet/type/cron.rb b/lib/puppet/type/cron.rb
new file mode 100644
index 0000000..a4f1f91
--- /dev/null
+++ b/lib/puppet/type/cron.rb
@@ -0,0 +1,480 @@
+require 'etc'
+require 'facter'
+require 'puppet/util/filetype'
+
+Puppet::Type.newtype(:cron) do
+ @doc = <<-'EOT'
+ Installs and manages cron jobs. Every cron resource created by Puppet
+ requires a command and at least one periodic attribute (hour, minute,
+ month, monthday, weekday, or special). While the name of the cron job is
+ not part of the actual job, the name is stored in a comment beginning with
+ `# Puppet Name: `. These comments are used to match crontab entries created
+ by Puppet with cron resources.
+
+ If an existing crontab entry happens to match the scheduling and command of a
+ cron resource that has never been synced, Puppet will defer to the existing
+ crontab entry and will not create a new entry tagged with the `# Puppet Name: `
+ comment.
+
+ Example:
+
+ cron { 'logrotate':
+ command => '/usr/sbin/logrotate',
+ user => 'root',
+ hour => 2,
+ minute => 0,
+ }
+
+ Note that all periodic attributes 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 these):
+
+ cron { 'logrotate':
+ command => '/usr/sbin/logrotate',
+ user => 'root',
+ hour => ['2-4'],
+ minute => '*/10',
+ }
+
+ An important note: _the Cron type will not reset parameters that are
+ removed from a manifest_. For example, removing a `minute => 10` parameter
+ will not reset the minute component of the associated cronjob to `*`.
+ These changes must be expressed by setting the parameter to
+ `minute => absent` because Puppet only manages parameters that are out of
+ sync with manifest entries.
+
+ **Autorequires:** If Puppet is managing the user account specified by the
+ `user` property of a cron resource, then the cron resource will autorequire
+ that user.
+ EOT
+ 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)
+ self.is_to_s(is) == self.should_to_s
+ 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 tmp.upcase == name[0..2].upcase
+ return index
+ end
+ }
+ else
+ return ary.index(tmp) if ary.include?(tmp)
+ end
+
+ false
+ end
+
+ def should_to_s(value = @should)
+ if value
+ if value.is_a?(Array) && (name == :command || value[0].is_a?(Symbol))
+ value = value[0]
+ end
+ super(value)
+ else
+ nil
+ end
+ end
+
+ def is_to_s(value = @is)
+ if value
+ if value.is_a?(Array) && (name == :command || value[0].is_a?(Symbol))
+ value = value[0]
+ end
+ super(value)
+ 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<name>=+ 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 :absent
+ 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 %{name}") % { value: value, name: 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
+ 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
+
+ def munge(value)
+ value.strip
+ end
+ end
+
+ newproperty(:special) do
+ desc "A special value such as 'reboot' or 'annually'.
+ Only available on supported systems such as Vixie Cron.
+ Overrides more specific time of day/week settings.
+ Set to 'absent' to make puppet revert to a plain numeric schedule."
+
+ def specials
+ %w{reboot yearly annually monthly weekly daily midnight hourly absent} +
+ [ :absent ]
+ end
+
+ validate do |value|
+ raise ArgumentError, _("Invalid special schedule %{value}") % { value: value.inspect } unless specials.include?(value)
+ end
+
+ def munge(value)
+ # Support value absent so that a schedule can be
+ # forced to change to numeric.
+ if value == "absent" or value == :absent
+ return :absent
+ end
+ 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 either:
+
+ - A number between 0 and 7, inclusive, with 0 or 7 being Sunday
+ - The name of the day, such as 'Tuesday'."
+ end
+
+ newproperty(:month, :parent => CronParam) do
+ def alpha
+ # The ___placeholder accounts for the fact that month is unique among
+ # "nameable" crontab entries in that it does not use 0-based indexing.
+ # Padding the array with a placeholder introduces the appropriate shift
+ # in indices.
+ %w{___placeholder 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 either:
+
+ - A number between 1 and 12, inclusive, with 1 being January
+ - The name of the month, such as '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, like `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}") % { value: value.inspect }
+ end
+ end
+
+ def insync?(is)
+ if is.is_a? Array
+ return is.sort == @should.sort
+ else
+ return is == @should
+ 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 who owns the cron job. This user must
+ be allowed to run cron jobs, which is not currently checked by
+ Puppet.
+
+ This property defaults to the user running Puppet or `root`.
+
+ The default crontab provider executes the system `crontab` using
+ the user account specified by this property."
+
+ defaultto {
+ if not provider.is_a?(@resource.class.provider(:crontab))
+ struct = Etc.getpwuid(Process.uid)
+ struct.respond_to?(:name) && struct.name or 'root'
+ end
+ }
+ end
+
+ # Autorequire the owner of the crontab entry.
+ autorequire(:user) do
+ self[:user]
+ end
+
+ newproperty(:target) do
+ desc "The name of the crontab file in which the cron job should be stored.
+
+ This property defaults to the value of the `user` property if set, the
+ user running Puppet or `root`.
+
+ For the default crontab provider, this property is functionally
+ equivalent to the `user` property and should be avoided. In particular,
+ setting both `user` and `target` to different values will result in
+ undefined behavior."
+
+ defaultto {
+ if provider.is_a?(@resource.class.provider(:crontab))
+ if val = @resource.should(:user)
+ val
+ else
+ struct = Etc.getpwuid(Process.uid)
+ struct.respond_to?(:name) && struct.name or 'root'
+ end
+ elsif provider.class.ancestors.include?(Puppet::Provider::ParsedFile)
+ provider.class.default_target
+ else
+ nil
+ end
+ }
+ end
+
+ validate do
+ return true unless self[:special]
+ return true if self[:special] == :absent
+ # there is a special schedule in @should, so we don't want to see
+ # any numeric should values
+ [ :minute, :hour, :weekday, :monthday, :month ].each do |field|
+ next unless self[field]
+ next if self[field] == :absent
+ raise ArgumentError, _("%{cron} cannot specify both a special schedule and a value for %{field}") % { cron: self.ref, field: field }
+ end
+ end
+
+ # We have to reorder things so that :provide is before :target
+
+ attr_accessor :uid
+
+ # Marks the resource as "being purged".
+ #
+ # @api public
+ #
+ # @note This overrides the Puppet::Type method in order to handle
+ # an edge case that has so far been observed during testing only.
+ # Without forcing the should-value for the user property to be
+ # identical to the original cron file, purging from a fixture
+ # will not work, because the user property defaults to the user
+ # running the test. It is not clear whether this scenario can apply
+ # during normal operation.
+ #
+ # @note Also, when not forcing the should-value for the target
+ # property, unpurged file content (such as comments) can end up
+ # being written to the default target (i.e. the current login name).
+ def purging
+ self[:target] = provider.target
+ self[:user] = provider.target
+ super
+ end
+
+ def value(name)
+ name = name.intern
+ 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
+ when :special
+ # nothing
+ else
+ #ret = (self.class.validproperty?(name).default || "*").to_s
+ ret = "*"
+ end
+ end
+
+ ret
+ end
+end
+