aboutsummaryrefslogtreecommitdiff
path: root/lib/puppet/provider/cron/crontab.rb
diff options
context:
space:
mode:
Diffstat (limited to 'lib/puppet/provider/cron/crontab.rb')
-rw-r--r--lib/puppet/provider/cron/crontab.rb298
1 files changed, 298 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..fec5511
--- /dev/null
+++ b/lib/puppet/provider/cron/crontab.rb
@@ -0,0 +1,298 @@
+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] = Regexp.last_match(1) if record[:line] =~ %r{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].freeze
+
+ record_line :crontab,
+ fields: ['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)
+ match = %r{@(\S+)}.match(time)
+ if match
+ # is there another way to access the constant?
+ Puppet::Type::Cron::ProviderCrontab::TIME_FIELDS.each { |f| record[f] = :absent }
+ record[:special] = match.captures[0]
+ return record
+ end
+
+ match = %r{(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)}.match(time)
+ if match
+ record[:special] = :absent
+ Puppet::Type::Cron::ProviderCrontab::TIME_FIELDS.zip(match.captures).each do |field, value|
+ record[field] = if value == absent
+ :absent
+ else
+ value.split(',')
+ end
+ end
+
+ return record
+ end
+
+ 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
+
+ def pre_gen(record)
+ if record[:special] && record[:special] != :absent
+ record[:special] = "@#{record[:special]}"
+ end
+
+ Puppet::Type::Cron::ProviderCrontab::TIME_FIELDS.each do |field|
+ if (vals = record[field]) && 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] && record[:environment] != :absent
+ str += record[:environment].map { |line| "#{line}\n" }.join('')
+ end
+ fields = if record[:special] && record[:special] != :absent
+ [:special, :command]
+ else
+ Puppet::Type::Cron::ProviderCrontab::TIME_FIELDS + [:command]
+ end
+ str += record.values_at(*fields).map { |field|
+ if field.nil? || field == :absent
+ absent
+ else
+ field
+ end
+ }.join(joiner)
+ str
+ end
+ end
+
+ def create
+ if resource.should(:command)
+ 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
+
+ target = resource[:target] || resource[:user] if resource
+ return resource if record[:target] == target
+ 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
+ %r{# 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] && !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]) && (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 == '*' && 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 do |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 # rubocop: disable Lint/EmptyWhen
+ # nothing
+ else
+ if name
+ record[:name] = name
+ name = nil
+ else
+ cmd_string = record[:command].gsub(%r{\s+}, '_')
+ index = (@name_index += 1)
+ record[:name] = "unmanaged:#{cmd_string}-#{index}"
+ record[:unmanaged] = true
+ end
+ if envs.nil? || 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
+ result << record unless record[:skip]
+ end
+ 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 =~ %r{(^TZ=.+\n)}
+ tz = Regexp.last_match(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 %r{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) && 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