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
|
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
|