1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310
|
require_relative 'filetype'
require 'puppet/provider/parsedfile'
Puppet::Type.type(:cron).provide(:crontab, parent: Puppet::Provider::ParsedFile, default_target: ENV['USER'] || 'root', raise_prefetch_errors: true) do
desc 'The crontab provider'
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('os.family')
when 'Solaris'
:suntab
when 'AIX'
:aixtab
else
:crontab
end
Puppet::Provider::Cron::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
# 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('os.family')
when 'Debian', 'HP-UX', 'Solaris'
'/var/spool/cron/crontabs'
when %r{BSD}
'/var/cron/tabs'
when 'Darwin'
'/usr/lib/cron/tabs/'
else
'/var/spool/cron'
end
# Return the directory holding crontab files stored on the local system.
#
# @api private
def self.crontab_dir
CRONTAB_DIR
end
# Yield the names of all crontab files stored on the local system.
#
# @note Ignores files that are not writable for the puppet process and hidden
# files that start with .keep
#
# @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 = File.join(CRONTAB_DIR, file)
# Gentoo creates .keep_PACKAGE-SLOT files to make sure the directory is not
# removed
yield(file) if File.file?(path) && File.writable?(path) && !file.start_with?('.keep_')
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
|