~ubuntu-branches/ubuntu/oneiric/puppet/oneiric-security

« back to all changes in this revision

Viewing changes to lib/puppet/type/cron.rb

  • Committer: Bazaar Package Importer
  • Author(s): Micah Anderson
  • Date: 2008-07-26 15:43:45 UTC
  • mto: (3.1.1 lenny) (1.3.1 upstream)
  • mto: This revision was merged to the branch mainline in revision 16.
  • Revision ID: james.westby@ubuntu.com-20080726154345-1fmgo76b4l72ulvc
ImportĀ upstreamĀ versionĀ 0.24.5

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
require 'etc'
2
2
require 'facter'
3
 
require 'puppet/type/state'
4
 
require 'puppet/filetype'
5
 
require 'puppet/type/parsedtype'
6
 
 
7
 
module Puppet
8
 
    # Model the actual cron jobs.  Supports all of the normal cron job fields
9
 
    # as parameters, with the 'command' as the single state.  Also requires a
10
 
    # completely symbolic 'name' paremeter, which gets written to the file
11
 
    # and is used to manage the job.
12
 
    newtype(:cron) do
13
 
 
14
 
        # A stupid hack because Cron is the only parsed type that I haven't
15
 
        # converted to using providers.  This whole stupid type needs to
16
 
        # be rewritten.
17
 
        class CronHackParam < Puppet::State::ParsedParam
18
 
            # Normally this would retrieve the current value, but our state is not
19
 
            # actually capable of doing so.
20
 
            def retrieve
21
 
                # If we've synced, then just copy the values over and return.
22
 
                # This allows this state to behave like any other state.
23
 
                if defined? @synced and @synced
24
 
                    # by default, we only copy over the first value.
25
 
                    @is = @synced
26
 
                    @synced = false
27
 
                    return
28
 
                end
29
 
 
30
 
                unless defined? @is and ! @is.nil?
31
 
                    @is = :absent
32
 
                end
33
 
            end
34
 
 
35
 
            # If the ensure state is out of sync, it will always be called
36
 
            # first, so I don't need to worry about that.
37
 
            def sync(nostore = false)
38
 
                ebase = @parent.class.name.to_s
39
 
 
40
 
                tail = nil
41
 
                if self.class.name == :ensure
42
 
                    # We're either creating or destroying the object
43
 
                    if @is == :absent
44
 
                        #@is = self.should
45
 
                        tail = "created"
46
 
 
47
 
                        # If we're creating it, then sync all of the other states
48
 
                        # but tell them not to store (we'll store just once,
49
 
                        # at the end).
50
 
                        unless nostore
51
 
                            @parent.eachstate { |state|
52
 
                                next if state == self or state.name == :ensure
53
 
                                state.sync(true)
54
 
                            }
55
 
                        end
56
 
                    elsif self.should == :absent
57
 
                        @parent.remove(true)
58
 
                        tail = "deleted"
59
 
                    end
60
 
                else
61
 
                    # We don't do the work here, it gets done in 'store'
62
 
                    tail = "changed"
63
 
                end
64
 
                @synced = self.should
65
 
 
66
 
                # This should really only be done once per run, rather than
67
 
                # every time.  I guess we need some kind of 'flush' mechanism.
68
 
                if nostore
69
 
                    self.retrieve
70
 
                else
71
 
                    @parent.store
72
 
                end
73
 
                
74
 
                return (ebase + "_" + tail).intern
75
 
            end
76
 
        end
77
 
 
78
 
        # A base class for all of the Cron parameters, since they all have
79
 
        # similar argument checking going on.  We're stealing the base class
80
 
        # from parsedtype, and we should probably subclass Cron from there,
81
 
        # but it was just too annoying to do.
82
 
        class CronParam < CronHackParam
83
 
            class << self
84
 
                attr_accessor :boundaries, :default
85
 
            end
86
 
 
87
 
            # We have to override the parent method, because we consume the entire
88
 
            # "should" array
89
 
            def insync?
90
 
                if defined? @should and @should
91
 
                    self.is_to_s == self.should_to_s
92
 
                else
93
 
                    true
94
 
                end
95
 
            end
96
 
 
97
 
            # A method used to do parameter input handling.  Converts integers
98
 
            # in string form to actual integers, and returns the value if it's
99
 
            # an integer or false if it's just a normal string.
100
 
            def numfix(num)
101
 
                if num =~ /^\d+$/
102
 
                    return num.to_i
103
 
                elsif num.is_a?(Integer)
104
 
                    return num
105
 
                else
106
 
                    return false
107
 
                end
108
 
            end
109
 
 
110
 
            # Verify that a number is within the specified limits.  Return the
111
 
            # number if it is, or false if it is not.
112
 
            def limitcheck(num, lower, upper)
113
 
                if num >= lower and num <= upper
114
 
                    return num
115
 
                else
116
 
                    return false
117
 
                end
118
 
            end
119
 
 
120
 
            # Verify that a value falls within the specified array.  Does case
121
 
            # insensitive matching, and supports matching either the entire word
122
 
            # or the first three letters of the word.
123
 
            def alphacheck(value, ary)
124
 
                tmp = value.downcase
125
 
                
126
 
                # If they specified a shortened version of the name, then see
127
 
                # if we can lengthen it (e.g., mon => monday).
128
 
                if tmp.length == 3
129
 
                    ary.each_with_index { |name, index|
130
 
                        if name =~ /#{tmp}/i
131
 
                            return index
132
 
                        end
133
 
                    }
134
 
                else
135
 
                    if ary.include?(tmp)
136
 
                        return ary.index(tmp)
137
 
                    end
138
 
                end
139
 
 
140
 
                return false
141
 
            end
142
 
 
143
 
            def should_to_s
144
 
                if @should
145
 
                    if self.name == :command or @should[0].is_a? Symbol
146
 
                        @should[0]
147
 
                    else
148
 
                        @should.join(",")
149
 
                    end
150
 
                else
151
 
                    nil
152
 
                end
153
 
            end
154
 
 
155
 
            def is_to_s
156
 
                if @is
157
 
                    unless @is.is_a?(Array)
158
 
                        return @is
159
 
                    end
160
 
 
161
 
                    if self.name == :command or @is[0].is_a? Symbol
162
 
                        @is[0]
163
 
                    else
164
 
                        @is.join(",")
165
 
                    end
166
 
                else
167
 
                    nil
168
 
                end
169
 
            end
170
 
 
171
 
            # The method that does all of the actual parameter value
172
 
            # checking; called by all of the +param<name>=+ methods.
173
 
            # Requires the value, type, and bounds, and optionally supports
174
 
            # a boolean of whether to do alpha checking, and if so requires
175
 
            # the ary against which to do the checking.
176
 
            munge do |value|
177
 
                # Support 'absent' as a value, so that they can remove
178
 
                # a value
179
 
                if value == "absent" or value == :absent
180
 
                    return :absent
181
 
                end
182
 
 
183
 
                # Allow the */2 syntax
184
 
                if value =~ /^\*\/[0-9]+$/
185
 
                    return value
186
 
                end
187
 
 
188
 
                # Allow ranges
189
 
                if value =~ /^[0-9]+-[0-9]+$/
190
 
                    return value
191
 
                end
192
 
 
193
 
                if value == "*"
194
 
                    return value
195
 
                end
196
 
 
197
 
                return value unless self.class.boundaries
198
 
                lower, upper = self.class.boundaries
199
 
                retval = nil
200
 
                if num = numfix(value)
201
 
                    retval = limitcheck(num, lower, upper)
202
 
                elsif respond_to?(:alpha)
203
 
                    # If it has an alpha method defined, then we check
204
 
                    # to see if our value is in that list and if so we turn
205
 
                    # it into a number
206
 
                    retval = alphacheck(value, alpha())
207
 
                end
208
 
 
209
 
                if retval
210
 
                    return retval.to_s
211
 
                else
212
 
                    self.fail "%s is not a valid %s" %
213
 
                        [value, self.class.name]
214
 
                end
215
 
            end
216
 
        end
217
 
 
218
 
 
219
 
        # Override 'newstate' so that all states default to having the
220
 
        # correct parent type
221
 
        def self.newstate(name, options = {}, &block)
222
 
            options[:parent] ||= Puppet::State::CronParam
223
 
            super(name, options, &block)
224
 
        end
225
 
 
226
 
        # Somewhat uniquely, this state does not actually change anything -- it
227
 
        # just calls +@parent.sync+, which writes out the whole cron tab for
228
 
        # the user in question.  There is no real way to change individual cron
229
 
        # jobs without rewriting the entire cron file.
230
 
        #
231
 
        # Note that this means that managing many cron jobs for a given user
232
 
        # could currently result in multiple write sessions for that user.
233
 
        newstate(:command, :parent => CronParam) do
234
 
            desc "The command to execute in the cron job.  The environment
235
 
                provided to the command varies by local system rules, and it is
236
 
                best to always provide a fully qualified command.  The user's
237
 
                profile is not sourced when the command is run, so if the
238
 
                user's environment is desired it should be sourced manually.
239
 
                
240
 
                All cron parameters support ``absent`` as a value; this will
241
 
                remove any existing values for that field."
242
 
 
243
 
            def should
244
 
                if @should
245
 
                    if @should.is_a? Array
246
 
                        @should[0]
247
 
                    else
248
 
                        devfail "command is not an array"
249
 
                    end
250
 
                else
251
 
                    nil
252
 
                end
253
 
            end
254
 
        end
255
 
 
256
 
        newstate(:special, :parent => CronHackParam) do
257
 
            desc "Special schedules only supported on FreeBSD."
258
 
 
259
 
            def specials
260
 
                %w{reboot yearly annually monthly weekly daily midnight hourly}
261
 
            end
262
 
 
263
 
            validate do |value|
264
 
                unless specials().include?(value)
265
 
                    raise ArgumentError, "Invalid special schedule %s" %
266
 
                        value.inspect
267
 
                end
268
 
            end
269
 
        end
270
 
 
271
 
        newstate(:minute) do
272
 
            self.boundaries = [0, 59]
273
 
            desc "The minute at which to run the cron job.
274
 
                Optional; if specified, must be between 0 and 59, inclusive."
275
 
        end
276
 
 
277
 
        newstate(:hour) do
278
 
            self.boundaries = [0, 23]
279
 
            desc "The hour at which to run the cron job. Optional;
280
 
                if specified, must be between 0 and 23, inclusive."
281
 
        end
282
 
 
283
 
        newstate(:weekday) do
284
 
            def alpha
285
 
                %w{sunday monday tuesday wednesday thursday friday saturday}
286
 
            end
287
 
            self.boundaries = [0, 6]
288
 
            desc "The weekday on which to run the command.
289
 
                Optional; if specified, must be between 0 and 6, inclusive, with
290
 
                0 being Sunday, or must be the name of the day (e.g., Tuesday)."
291
 
        end
292
 
 
293
 
        newstate(:month) do
294
 
            def alpha
295
 
                %w{january february march april may june july
296
 
                    august september october november december}
297
 
            end
298
 
            self.boundaries = [1, 12]
299
 
            desc "The month of the year.  Optional; if specified
300
 
                must be between 1 and 12 or the month name (e.g., December)."
301
 
        end
302
 
 
303
 
        newstate(:monthday) do
304
 
            self.boundaries = [1, 31]
305
 
            desc "The day of the month on which to run the
306
 
                command.  Optional; if specified, must be between 1 and 31."
307
 
        end
308
 
 
309
 
        newstate(:environment, :parent => CronHackParam) do
310
 
            desc "Any environment settings associated with this cron job.  They
311
 
                will be stored between the header and the job in the crontab.  There
312
 
                can be no guarantees that other, earlier settings will not also
313
 
                affect a given cron job.
314
 
 
315
 
                Also, Puppet cannot automatically determine whether an existing,
316
 
                unmanaged environment setting is associated with a given cron
317
 
                job.  If you already have cron jobs with environment settings,
318
 
                then Puppet will keep those settings in the same place in the file,
319
 
                but will not associate them with a specific job.
320
 
                
321
 
                Settings should be specified exactly as they should appear in
322
 
                the crontab, e.g., 'PATH=/bin:/usr/bin:/usr/sbin'.  Multiple
323
 
                settings should be specified as an array."
324
 
 
325
 
            validate do |value|
326
 
                unless value =~ /^\s*(\w+)\s*=\s*(.+)\s*$/
327
 
                    raise ArgumentError, "Invalid environment setting %s" %
328
 
                        value.inspect
329
 
                end
330
 
            end
331
 
 
332
 
            def insync?
333
 
                if @is.is_a? Array
334
 
                    return @is.sort == @should.sort
335
 
                else
336
 
                    return @is == @should
337
 
                end
338
 
            end
339
 
 
340
 
            def should
341
 
                @should
342
 
            end
343
 
        end
344
 
 
345
 
        newparam(:name) do
346
 
            desc "The symbolic name of the cron job.  This name
347
 
                is used for human reference only and is generated automatically
348
 
                for cron jobs found on the system.  This generally won't
349
 
                matter, as Puppet will do its best to match existing cron jobs
350
 
                against specified jobs (and Puppet adds a comment to cron jobs it
351
 
                adds), but it is at least possible that converting from
352
 
                unmanaged jobs to managed jobs might require manual
353
 
                intervention.
354
 
                
355
 
                The names can only have alphanumeric characters plus the '-'
356
 
                character."
357
 
 
358
 
            isnamevar
359
 
 
360
 
            validate do |value|
361
 
                unless value =~ /^[-\w]+$/
362
 
                    raise ArgumentError, "Invalid name format '%s'" % value
363
 
                end
364
 
            end
365
 
        end
366
 
 
367
 
        newparam(:user) do
368
 
            desc "The user to run the command as.  This user must
369
 
                be allowed to run cron jobs, which is not currently checked by
370
 
                Puppet.
371
 
                
372
 
                The user defaults to whomever Puppet is running as."
373
 
 
374
 
            defaultto { ENV["USER"] }
375
 
 
376
 
            def value=(value)
377
 
                super
378
 
 
379
 
                # Make sure the user is not an array
380
 
                if @value.is_a? Array
381
 
                    @value = @value[0]
382
 
                end
383
 
            end
384
 
        end
385
 
 
386
 
        @doc = "Installs and manages cron jobs.  All fields except the command 
387
 
            and the user are optional, although specifying no periodic
388
 
            fields would result in the command being executed every
389
 
            minute.  While the name of the cron job is not part of the actual
390
 
            job, it is used by Puppet to store and retrieve it.
391
 
            
392
 
            If you specify a cron job that matches an existing job in every way
393
 
            except name, then the jobs will be considered equivalent and the
394
 
            new name will be permanently associated with that job.  Once this
395
 
            association is made and synced to disk, you can then manage the job
396
 
            normally (e.g., change the schedule of the job).
397
 
            
398
 
            Example:
399
 
                
400
 
                cron { logrotate:
401
 
                    command => \"/usr/sbin/logrotate\",
402
 
                    user => root,
403
 
                    hour => 2,
404
 
                    minute => 0
405
 
                }
406
 
            "
407
 
 
408
 
        @instances = {}
409
 
        @tabs = {}
410
 
 
 
3
require 'puppet/util/filetype'
 
4
 
 
5
Puppet::Type.newtype(:cron) do
 
6
    @doc = "Installs and manages cron jobs.  All fields except the command 
 
7
        and the user are optional, although specifying no periodic
 
8
        fields would result in the command being executed every
 
9
        minute.  While the name of the cron job is not part of the actual
 
10
        job, it is used by Puppet to store and retrieve it.
 
11
        
 
12
        If you specify a cron job that matches an existing job in every way
 
13
        except name, then the jobs will be considered equivalent and the
 
14
        new name will be permanently associated with that job.  Once this
 
15
        association is made and synced to disk, you can then manage the job
 
16
        normally (e.g., change the schedule of the job).
 
17
        
 
18
        Example::
 
19
            
 
20
            cron { logrotate:
 
21
                command => \"/usr/sbin/logrotate\",
 
22
                user => root,
 
23
                hour => 2,
 
24
                minute => 0
 
25
            }
 
26
 
 
27
        Note that all cron values can be specified as an array of values::
 
28
 
 
29
            cron { logrotate:
 
30
                command => \"/usr/sbin/logrotate\",
 
31
                user => root,
 
32
                hour => [2, 4]
 
33
            }
 
34
 
 
35
        Or using ranges, or the step syntax ``*/2`` (although there's no guarantee that
 
36
        your ``cron`` daemon supports it)::
 
37
 
 
38
            cron { logrotate:
 
39
                command => \"/usr/sbin/logrotate\",
 
40
                user => root,
 
41
                hour => ['2-4'],
 
42
                minute => '*/10'
 
43
            }
 
44
        "
 
45
    ensurable
 
46
 
 
47
    # A base class for all of the Cron parameters, since they all have
 
48
    # similar argument checking going on.
 
49
    class CronParam < Puppet::Property
411
50
        class << self
412
 
            attr_accessor :filetype
413
 
 
414
 
            def cronobj(name)
415
 
                if defined? @tabs
416
 
                    return @tabs[name]
417
 
                else
418
 
                    return nil
419
 
                end
420
 
            end
421
 
        end
422
 
 
423
 
        attr_accessor :uid
424
 
 
425
 
        # In addition to removing the instances in @objects, Cron has to remove
426
 
        # per-user cron tab information.
427
 
        def self.clear
428
 
            @instances = {}
429
 
            @tabs = {}
430
 
            super
431
 
        end
432
 
 
433
 
        def self.defaulttype
434
 
            case Facter["operatingsystem"].value
435
 
            when "Solaris":
436
 
                return Puppet::FileType.filetype(:suntab)
437
 
            else
438
 
                return Puppet::FileType.filetype(:crontab)
439
 
            end
440
 
        end
441
 
 
442
 
        self.filetype = self.defaulttype()
443
 
 
444
 
        # Override the default Puppet::Type method, because instances
445
 
        # also need to be deleted from the @instances hash
446
 
        def self.delete(child)
447
 
            if @instances.include?(child[:user])
448
 
                if @instances[child[:user]].include?(child)
449
 
                    @instances[child[:user]].delete(child)
450
 
                end
451
 
            end
452
 
            super
453
 
        end
454
 
 
455
 
        # Return the fields found in the cron tab.
456
 
        def self.fields
457
 
            return [:minute, :hour, :monthday, :month, :weekday, :command]
458
 
        end
459
 
 
460
 
        # Convert our hash to an object
461
 
        def self.hash2obj(hash)
462
 
            obj = nil
463
 
            namevar = self.namevar
464
 
            unless hash.include?(namevar) and hash[namevar]
465
 
                Puppet.info "Autogenerating name for %s" % hash[:command]
466
 
                hash[:name] = "autocron-%s" % hash.object_id
467
 
            end
468
 
 
469
 
            unless hash.include?(:command)
470
 
                raise Puppet::DevError, "No command for %s" % name
471
 
            end
472
 
            # if the cron already exists with that name...
473
 
            if obj = (self[hash[:name]] || match(hash))
474
 
                # Mark the cron job as present
475
 
                obj.is = [:ensure, :present]
476
 
 
477
 
                # Mark all of the values appropriately
478
 
                hash.each { |param, value|
479
 
                    if state = obj.state(param)
480
 
                        state.is = value
481
 
                    elsif val = obj[param]
482
 
                        obj[param] = val
483
 
                    else    
484
 
                        # There is a value on disk, but it should go away
485
 
                        obj.is = [param, value]
486
 
                        obj[param] = :absent
487
 
                    end
488
 
                }
489
 
            else
490
 
                # create a new cron job, since no existing one
491
 
                # seems to match
492
 
                obj = self.create(
493
 
                    :name => hash[namevar]
494
 
                )
495
 
 
496
 
                obj.is = [:ensure, :present]
497
 
 
498
 
                obj.notice "created"
499
 
 
500
 
                hash.delete(namevar)
501
 
                hash.each { |param, value|
502
 
                    obj.is = [param, value]
503
 
                }
504
 
            end
505
 
 
506
 
            instance(obj)
507
 
        end
508
 
 
509
 
        # Return the header placed at the top of each generated file, warning
510
 
        # users that modifying this file manually is probably a bad idea.
511
 
        def self.header
512
 
%{# HEADER This file was autogenerated at #{Time.now} by puppet.  While it
513
 
# HEADER can still be managed manually, it is definitely not recommended.
514
 
# HEADER Note particularly that the comments starting with 'Puppet Name' should
515
 
# HEADER not be deleted, as doing so could cause duplicate cron jobs.\n}
516
 
        end
517
 
 
518
 
        def self.instance(obj)
519
 
            user = obj[:user]
520
 
            unless @instances.include?(user)
521
 
                @instances[user] = []
522
 
            end
523
 
 
524
 
            @instances[user] << obj
525
 
        end
526
 
 
527
 
        def self.list
528
 
            # Look for cron jobs for each user
529
 
            Puppet::Type.type(:user).list_by_name.each { |user|
530
 
                self.retrieve(user, false)
531
 
            }
532
 
 
533
 
            self.collect { |c| c }
534
 
        end
535
 
 
536
 
        # See if we can match the hash against an existing cron job.
537
 
        def self.match(hash)
538
 
            self.find_all { |obj|
539
 
                obj[:user] == hash[:user] and obj.value(:command) == hash[:command][0]
540
 
            }.each do |obj|
541
 
                # we now have a cron job whose command exactly matches
542
 
                # let's see if the other fields match
543
 
 
544
 
                # First check the @special stuff
545
 
                if hash[:special]
546
 
                    next unless obj.value(:special) == hash[:special]
547
 
                end
548
 
 
549
 
                # Then the normal fields.
550
 
                matched = true
551
 
                fields().each do |field|
552
 
                    next if field == :command
553
 
                    if hash[field] and ! obj.value(field)
554
 
                        #Puppet.info "Cron is missing %s: %s and %s" %
555
 
                        #    [field, hash[field].inspect, obj.value(field).inspect]
556
 
                        matched = false
557
 
                        break
558
 
                    end
559
 
 
560
 
                    if ! hash[field] and obj.value(field)
561
 
                        #Puppet.info "Hash is missing %s: %s and %s" %
562
 
                        #    [field, obj.value(field).inspect, hash[field].inspect]
563
 
                        matched = false
564
 
                        break
565
 
                    end
566
 
 
567
 
                    # FIXME It'd be great if I could somehow reuse how the
568
 
                    # fields are turned into text, but....
569
 
                    next if (hash[field] == [:absent] and obj.value(field) == "*")
570
 
                    next if (hash[field].join(",") == obj.value(field))
571
 
                    #Puppet.info "Did not match %s: %s vs %s" %
572
 
                    #    [field, obj.value(field).inspect, hash[field].inspect]
573
 
                    matched = false 
574
 
                    break
575
 
                end
576
 
                next unless matched
577
 
                return obj
 
51
            attr_accessor :boundaries, :default
 
52
        end
 
53
 
 
54
        # We have to override the parent method, because we consume the entire
 
55
        # "should" array
 
56
        def insync?(is)
 
57
            if defined? @should and @should
 
58
                self.is_to_s(is) == self.should_to_s
 
59
            else
 
60
                true
 
61
            end
 
62
        end
 
63
 
 
64
        # A method used to do parameter input handling.  Converts integers
 
65
        # in string form to actual integers, and returns the value if it's
 
66
        # an integer or false if it's just a normal string.
 
67
        def numfix(num)
 
68
            if num =~ /^\d+$/
 
69
                return num.to_i
 
70
            elsif num.is_a?(Integer)
 
71
                return num
 
72
            else
 
73
                return false
 
74
            end
 
75
        end
 
76
 
 
77
        # Verify that a number is within the specified limits.  Return the
 
78
        # number if it is, or false if it is not.
 
79
        def limitcheck(num, lower, upper)
 
80
            if num >= lower and num <= upper
 
81
                return num
 
82
            else
 
83
                return false
 
84
            end
 
85
        end
 
86
 
 
87
        # Verify that a value falls within the specified array.  Does case
 
88
        # insensitive matching, and supports matching either the entire word
 
89
        # or the first three letters of the word.
 
90
        def alphacheck(value, ary)
 
91
            tmp = value.downcase
 
92
            
 
93
            # If they specified a shortened version of the name, then see
 
94
            # if we can lengthen it (e.g., mon => monday).
 
95
            if tmp.length == 3
 
96
                ary.each_with_index { |name, index|
 
97
                    if name =~ /#{tmp}/i
 
98
                        return index
 
99
                    end
 
100
                }
 
101
            else
 
102
                if ary.include?(tmp)
 
103
                    return ary.index(tmp)
 
104
                end
578
105
            end
579
106
 
580
107
            return false
581
108
        end
582
109
 
583
 
        # Parse a user's cron job into individual cron objects.
584
 
        #
585
 
        # Autogenerates names for any jobs that don't already have one; these
586
 
        # names will get written back to the file.
587
 
        #
588
 
        # This method also stores existing comments, and it stores all cron
589
 
        # jobs in order, mostly so that comments are retained in the order
590
 
        # they were written and in proximity to the same jobs.
591
 
        def self.parse(user, text)
592
 
            count = 0
593
 
            hash = {}
594
 
 
595
 
            envs = []
596
 
            text.chomp.split("\n").each { |line|
597
 
                case line
598
 
                when /^# Puppet Name: (.+)$/
599
 
                    hash[:name] = $1
600
 
                    next
601
 
                when /^#/:
602
 
                    # add other comments to the list as they are
603
 
                    @instances[user] << line 
604
 
                    next
605
 
                when /^\s*(\w+)\s*=\s*(.+)\s*$/:
606
 
                    # Match env settings.
607
 
                    if hash[:name]
608
 
                        envs << line
609
 
                    else
610
 
                        @instances[user] << line 
611
 
                    end
612
 
                    next
613
 
                when /^@(\w+)\s+(.+)/ # FreeBSD special cron crap
614
 
                    fields().each do |field|
615
 
                        next if field == :command
616
 
                        hash[field] = :absent
617
 
                    end
618
 
                    hash[:special] = $1
619
 
                    hash[:command] = $2
620
 
                else
621
 
                    if match = /^(\S+) (\S+) (\S+) (\S+) (\S+) (.+)$/.match(line)
622
 
                        fields().zip(match.captures).each { |param, value|
623
 
                            if value == "*"
624
 
                                hash[param] = [:absent]
625
 
                            else
626
 
                                if param == :command
627
 
                                    hash[param] = [value]
628
 
                                else
629
 
                                    # We always want the 'is' value to be an
630
 
                                    # array
631
 
                                    hash[param] = value.split(",")
632
 
                                end
633
 
                            end
634
 
                        }
635
 
                    else
636
 
                        # Don't fail on unmatched lines, just warn on them
637
 
                        # and skip them.
638
 
                        Puppet.warning "Could not match '%s'" % line
639
 
                        next
640
 
                    end
641
 
                end
642
 
 
643
 
                unless envs.empty?
644
 
                    # We have to dup here so that we don't remove the settings
645
 
                    # in @is on the object.
646
 
                    hash[:environment] = envs.dup
647
 
                end
648
 
 
649
 
                hash[:user] = user
650
 
 
651
 
                # Now convert our hash to an object.
652
 
                hash2obj(hash)
653
 
 
654
 
                hash = {}
655
 
                envs.clear
656
 
                count += 1
657
 
            }
658
 
        end
659
 
 
660
 
        # Retrieve a given user's cron job, using the @filetype's +retrieve+
661
 
        # method.  Returns nil if there was no cron job; else, returns the
662
 
        # number of cron instances found.
663
 
        def self.retrieve(user, checkuser = true)
664
 
            # First make sure the user exists, unless told not to
665
 
            if checkuser
666
 
                begin
667
 
                    Puppet::Util.uid(user)
668
 
                rescue ArgumentError
669
 
                    raise Puppet::Error,  "User %s not found" % user
670
 
                end
671
 
            end
672
 
 
673
 
            @tabs[user] ||= @filetype.new(user)
674
 
            text = @tabs[user].read
675
 
            if $? != 0
676
 
                # there is no cron file
677
 
                return nil
678
 
            else
679
 
                # Preemptively mark everything absent, so that retrieving it
680
 
                # can mark it present again.
681
 
                self.find_all { |obj|
682
 
                    obj[:user] == user
683
 
                }.each { |obj|
684
 
                    obj.is = [:ensure, :absent]
685
 
                }
686
 
 
687
 
                # Get rid of the old instances, so we don't get duplicates
688
 
                if @instances.include?(user)
689
 
                    @instances[user].clear
690
 
                else
691
 
                    @instances[user] = []
692
 
                end
693
 
 
694
 
                self.parse(user, text)
695
 
            end
696
 
        end
697
 
 
698
 
        # Remove a user's cron tab.
699
 
        def self.remove(user)
700
 
            @tabs[user] ||= @filetype.new(user)
701
 
            @tabs[user].remove
702
 
        end
703
 
 
704
 
        # Store the user's cron tab.  Collects the text of the new tab and
705
 
        # sends it to the +@filetype+ module's +write+ function.  Also adds
706
 
        # header, warning users not to modify the file directly.
707
 
        def self.store(user)
708
 
            unless @instances.include?(user) or @objects.find do |n,o|
709
 
                o[:user] == user
710
 
            end
711
 
                Puppet.notice "No cron instances for %s" % user
712
 
                return
713
 
            end
714
 
 
715
 
            @tabs[user] ||= @filetype.new(user)
716
 
 
717
 
            self.each do |inst|
718
 
                next unless inst[:user] == user
719
 
                unless (@instances[user] and @instances[user].include? inst)
720
 
                    @instances[user] ||= []
721
 
                    @instances[user] << inst
722
 
                end
723
 
            end
724
 
            @tabs[user].write(self.tab(user))
725
 
        end
726
 
 
727
 
        # Collect all Cron instances for a given user and convert them
728
 
        # into literal text.
729
 
        def self.tab(user)
730
 
            Puppet.info "Writing cron tab for %s" % user
731
 
            if @instances.include?(user)
732
 
                tab = @instances[user].reject { |obj|
733
 
                    if obj.is_a?(self) and obj.should(:ensure) == :absent
734
 
                        true
735
 
                    else
736
 
                        false
737
 
                    end
738
 
                }.collect { |obj|
739
 
                    if obj.is_a? self
740
 
                        obj.to_record
741
 
                    else
742
 
                        obj.to_s
743
 
                    end
744
 
                }.join("\n") + "\n"
745
 
 
746
 
                # Apparently Freebsd will "helpfully" add a new TZ line to every
747
 
                # single cron line, but not in all cases (e.g., it doesn't do it
748
 
                # on my machine.  This is my attempt to fix it so the TZ lines don't
749
 
                # multiply.
750
 
                if tab =~ /^TZ=.+$/
751
 
                    return tab.sub(/\n/, "\n" + self.header)
752
 
                else
753
 
                    return self.header() + tab
754
 
                end
755
 
 
756
 
            else
757
 
                Puppet.notice "No cron instances for %s" % user
758
 
            end
759
 
        end
760
 
 
761
 
        # Return the tab object itself.  Pretty much just used for testing.
762
 
        def self.tabobj(user)
763
 
            @tabs[user]
764
 
        end
765
 
 
766
 
        # Return the last time a given user's cron tab was loaded.  Could
767
 
        # be used for reducing writes, but currently is not.
768
 
        def self.loaded?(user)
769
 
            if @tabs.include?(user)
770
 
                return @loaded[user].loaded
771
 
            else
772
 
                return nil
773
 
            end
774
 
        end
775
 
 
776
 
        def create
777
 
            # nothing
778
 
            self.store
779
 
        end
780
 
 
781
 
        def destroy
782
 
            # nothing, since the 'Cron.tab' method just doesn't write out
783
 
            # crons whose 'ensure' states are set to 'absent'.
784
 
            self.store
785
 
        end
786
 
 
787
 
        def exists?
788
 
            @states.include?(:ensure) and @states[:ensure].is == :present
789
 
        end
790
 
 
791
 
        # Override the default Puppet::Type method because we need to call
792
 
        # the +@filetype+ retrieve method.
793
 
        def retrieve
794
 
            unless @parameters.include?(:user)
795
 
                self.fail "You must specify the cron user"
796
 
            end
797
 
 
798
 
            self.class.retrieve(self[:user])
799
 
            if withtab = self.class["testwithtab"]
800
 
                Puppet.info withtab.is(:ensure).inspect
801
 
            end
802
 
            self.eachstate { |st|
803
 
                st.retrieve
804
 
            }
805
 
            if withtab = self.class["testwithtab"]
806
 
                Puppet.info withtab.is(:ensure).inspect
807
 
            end
808
 
        end
809
 
 
810
 
        # Write the entire user's cron tab out.
811
 
        def store
812
 
            self.class.store(self[:user])
813
 
        end
814
 
 
815
 
        # Convert the current object a cron-style string.  Adds the cron name
816
 
        # as a comment above the cron job, in the form '# Puppet Name: <name>'.
817
 
        def to_record
818
 
            hash = {}
819
 
 
820
 
            # Collect all of the values that we have
821
 
            self.class.fields().each { |param|
822
 
                hash[param] = self.value(param)
823
 
 
824
 
                unless hash[param]
825
 
                    devfail "Got no value for %s" % param
826
 
                end
827
 
            }
828
 
 
829
 
            str = ""
830
 
 
831
 
            str = "# Puppet Name: %s\n" % self.name
832
 
 
833
 
            if @states.include?(:environment) and
834
 
                @states[:environment].should != :absent
835
 
                    envs = @states[:environment].should
836
 
                    unless envs.is_a? Array
837
 
                        envs = [envs]
838
 
                    end
839
 
 
840
 
                    envs.each do |line| str += (line + "\n") end
841
 
            end
842
 
 
843
 
            line = nil
844
 
            if special = self.value(:special)
845
 
                line = str + "@%s %s" %
846
 
                    [special, self.value(:command)]
847
 
            else
848
 
                line = str + self.class.fields.collect { |f|
849
 
                    if hash[f] and hash[f] != :absent
850
 
                        hash[f]
851
 
                    else
852
 
                        "*"
853
 
                    end
854
 
                }.join(" ")
855
 
            end
856
 
 
857
 
            return line
858
 
        end
859
 
 
860
 
        def value(name)
861
 
            name = name.intern if name.is_a? String
862
 
            ret = nil
863
 
            if @states.include?(name)
864
 
                ret = @states[name].should_to_s
865
 
 
866
 
                if ret.nil?
867
 
                    ret = @states[name].is_to_s
868
 
                end
869
 
 
870
 
                if ret == :absent
871
 
                    ret = nil
872
 
                end
873
 
            end
874
 
 
875
 
            unless ret
876
 
                case name
877
 
                when :command
878
 
                    devfail "No command, somehow"
879
 
                when :special
880
 
                    # nothing
881
 
                else
882
 
                    #ret = (self.class.validstate?(name).default || "*").to_s
883
 
                    ret = "*"
884
 
                end
885
 
            end
886
 
 
887
 
            ret
888
 
        end
 
110
        def should_to_s(newvalue = @should)
 
111
            if newvalue
 
112
                unless newvalue.is_a?(Array)
 
113
                    newvalue = [newvalue]
 
114
                end
 
115
                if self.name == :command or newvalue[0].is_a? Symbol
 
116
                    newvalue[0]
 
117
                else
 
118
                    newvalue.join(",")
 
119
                end
 
120
            else
 
121
                nil
 
122
            end
 
123
        end
 
124
 
 
125
        def is_to_s(currentvalue = @is)
 
126
            if currentvalue
 
127
                unless currentvalue.is_a?(Array)
 
128
                    return currentvalue
 
129
                end
 
130
 
 
131
                if self.name == :command or currentvalue[0].is_a? Symbol
 
132
                    currentvalue[0]
 
133
                else
 
134
                    currentvalue.join(",")
 
135
                end
 
136
            else
 
137
                nil
 
138
            end
 
139
        end
 
140
 
 
141
        def should
 
142
            if @should and @should[0] == :absent
 
143
                :absent
 
144
            else
 
145
                @should
 
146
            end
 
147
        end
 
148
 
 
149
        def should=(ary)
 
150
            super
 
151
            @should.flatten!
 
152
        end
 
153
 
 
154
        # The method that does all of the actual parameter value
 
155
        # checking; called by all of the +param<name>=+ methods.
 
156
        # Requires the value, type, and bounds, and optionally supports
 
157
        # a boolean of whether to do alpha checking, and if so requires
 
158
        # the ary against which to do the checking.
 
159
        munge do |value|
 
160
            # Support 'absent' as a value, so that they can remove
 
161
            # a value
 
162
            if value == "absent" or value == :absent
 
163
                return :absent
 
164
            end
 
165
 
 
166
            # Allow the */2 syntax
 
167
            if value =~ /^\*\/[0-9]+$/
 
168
                return value
 
169
            end
 
170
 
 
171
            # Allow ranges
 
172
            if value =~ /^[0-9]+-[0-9]+$/
 
173
                return value
 
174
            end
 
175
 
 
176
            if value == "*"
 
177
                return value
 
178
            end
 
179
 
 
180
            return value unless self.class.boundaries
 
181
            lower, upper = self.class.boundaries
 
182
            retval = nil
 
183
            if num = numfix(value)
 
184
                retval = limitcheck(num, lower, upper)
 
185
            elsif respond_to?(:alpha)
 
186
                # If it has an alpha method defined, then we check
 
187
                # to see if our value is in that list and if so we turn
 
188
                # it into a number
 
189
                retval = alphacheck(value, alpha())
 
190
            end
 
191
 
 
192
            if retval
 
193
                return retval.to_s
 
194
            else
 
195
                self.fail "%s is not a valid %s" %
 
196
                    [value, self.class.name]
 
197
            end
 
198
        end
 
199
    end
 
200
 
 
201
    # Somewhat uniquely, this property does not actually change anything -- it
 
202
    # just calls +@resource.sync+, which writes out the whole cron tab for
 
203
    # the user in question.  There is no real way to change individual cron
 
204
    # jobs without rewriting the entire cron file.
 
205
    #
 
206
    # Note that this means that managing many cron jobs for a given user
 
207
    # could currently result in multiple write sessions for that user.
 
208
    newproperty(:command, :parent => CronParam) do
 
209
        desc "The command to execute in the cron job.  The environment
 
210
            provided to the command varies by local system rules, and it is
 
211
            best to always provide a fully qualified command.  The user's
 
212
            profile is not sourced when the command is run, so if the
 
213
            user's environment is desired it should be sourced manually.
 
214
            
 
215
            All cron parameters support ``absent`` as a value; this will
 
216
            remove any existing values for that field."
 
217
 
 
218
        def retrieve 
 
219
          return_value = super
 
220
          if return_value && return_value.is_a?(Array)
 
221
            return_value = return_value[0]
 
222
          end
 
223
            
 
224
          return return_value
 
225
        end
 
226
 
 
227
        def should
 
228
            if @should
 
229
                if @should.is_a? Array
 
230
                    @should[0]
 
231
                else
 
232
                    devfail "command is not an array"
 
233
                end
 
234
            else
 
235
                nil
 
236
            end
 
237
        end
 
238
    end
 
239
 
 
240
    newproperty(:special) do
 
241
        desc "Special schedules only supported on FreeBSD."
 
242
 
 
243
        def specials
 
244
            %w{reboot yearly annually monthly weekly daily midnight hourly}
 
245
        end
 
246
 
 
247
        validate do |value|
 
248
            unless specials().include?(value)
 
249
                raise ArgumentError, "Invalid special schedule %s" %
 
250
                    value.inspect
 
251
            end
 
252
        end
 
253
    end
 
254
 
 
255
    newproperty(:minute, :parent => CronParam) do
 
256
        self.boundaries = [0, 59]
 
257
        desc "The minute at which to run the cron job.
 
258
            Optional; if specified, must be between 0 and 59, inclusive."
 
259
    end
 
260
 
 
261
    newproperty(:hour, :parent => CronParam) do
 
262
        self.boundaries = [0, 23]
 
263
        desc "The hour at which to run the cron job. Optional;
 
264
            if specified, must be between 0 and 23, inclusive."
 
265
    end
 
266
 
 
267
    newproperty(:weekday, :parent => CronParam) do
 
268
        def alpha
 
269
            %w{sunday monday tuesday wednesday thursday friday saturday}
 
270
        end
 
271
        self.boundaries = [0, 6]
 
272
        desc "The weekday on which to run the command.
 
273
            Optional; if specified, must be between 0 and 6, inclusive, with
 
274
            0 being Sunday, or must be the name of the day (e.g., Tuesday)."
 
275
    end
 
276
 
 
277
    newproperty(:month, :parent => CronParam) do
 
278
        def alpha
 
279
            %w{january february march april may june july
 
280
                august september october november december}
 
281
        end
 
282
        self.boundaries = [1, 12]
 
283
        desc "The month of the year.  Optional; if specified
 
284
            must be between 1 and 12 or the month name (e.g., December)."
 
285
    end
 
286
 
 
287
    newproperty(:monthday, :parent => CronParam) do
 
288
        self.boundaries = [1, 31]
 
289
        desc "The day of the month on which to run the
 
290
            command.  Optional; if specified, must be between 1 and 31."
 
291
    end
 
292
 
 
293
    newproperty(:environment) do
 
294
        desc "Any environment settings associated with this cron job.  They
 
295
            will be stored between the header and the job in the crontab.  There
 
296
            can be no guarantees that other, earlier settings will not also
 
297
            affect a given cron job.
 
298
 
 
299
 
 
300
            Also, Puppet cannot automatically determine whether an existing,
 
301
            unmanaged environment setting is associated with a given cron
 
302
            job.  If you already have cron jobs with environment settings,
 
303
            then Puppet will keep those settings in the same place in the file,
 
304
            but will not associate them with a specific job.
 
305
            
 
306
            Settings should be specified exactly as they should appear in
 
307
            the crontab, e.g., ``PATH=/bin:/usr/bin:/usr/sbin``."
 
308
 
 
309
        validate do |value|
 
310
            unless value =~ /^\s*(\w+)\s*=\s*(.+)\s*$/ or value == :absent or value == "absent"
 
311
                raise ArgumentError, "Invalid environment setting %s" %
 
312
                    value.inspect
 
313
            end
 
314
        end
 
315
 
 
316
        def insync?(is)
 
317
            if is.is_a? Array
 
318
                return is.sort == @should.sort
 
319
            else
 
320
                return is == @should
 
321
            end
 
322
        end
 
323
 
 
324
        def is_to_s(newvalue)
 
325
            if newvalue 
 
326
                if newvalue.is_a?(Array)
 
327
                    newvalue.join(",")
 
328
                else
 
329
                    newvalue
 
330
                end
 
331
            else
 
332
                nil
 
333
            end
 
334
        end
 
335
 
 
336
        def should
 
337
            @should
 
338
        end
 
339
 
 
340
        def should_to_s(newvalue = @should)
 
341
            if newvalue
 
342
                newvalue.join(",")
 
343
            else
 
344
                nil
 
345
            end
 
346
        end
 
347
    end
 
348
 
 
349
    newparam(:name) do
 
350
        desc "The symbolic name of the cron job.  This name
 
351
            is used for human reference only and is generated automatically
 
352
            for cron jobs found on the system.  This generally won't
 
353
            matter, as Puppet will do its best to match existing cron jobs
 
354
            against specified jobs (and Puppet adds a comment to cron jobs it
 
355
            adds), but it is at least possible that converting from
 
356
            unmanaged jobs to managed jobs might require manual
 
357
            intervention."
 
358
 
 
359
        isnamevar
 
360
    end
 
361
 
 
362
    newproperty(:user) do
 
363
        desc "The user to run the command as.  This user must
 
364
            be allowed to run cron jobs, which is not currently checked by
 
365
            Puppet.
 
366
            
 
367
            The user defaults to whomever Puppet is running as."
 
368
 
 
369
        defaultto { Etc.getpwuid(Process.uid).name || "root" }
 
370
    end
 
371
 
 
372
    newproperty(:target) do
 
373
        desc "Where the cron job should be stored.  For crontab-style
 
374
            entries this is the same as the user and defaults that way.
 
375
            Other providers default accordingly."
 
376
 
 
377
        defaultto {
 
378
            if provider.is_a?(@resource.class.provider(:crontab))
 
379
                if val = @resource.should(:user)
 
380
                    val
 
381
                else
 
382
                    raise ArgumentError,
 
383
                        "You must provide a user with crontab entries"
 
384
                end
 
385
            elsif provider.class.ancestors.include?(Puppet::Provider::ParsedFile)
 
386
                provider.class.default_target
 
387
            else
 
388
                nil
 
389
            end
 
390
        }
 
391
    end
 
392
 
 
393
    # We have to reorder things so that :provide is before :target
 
394
 
 
395
    attr_accessor :uid
 
396
 
 
397
    def value(name)
 
398
        name = symbolize(name)
 
399
        ret = nil
 
400
        if obj = @parameters[name]
 
401
            ret = obj.should
 
402
 
 
403
            if ret.nil?
 
404
                ret = obj.retrieve
 
405
            end
 
406
 
 
407
            if ret == :absent
 
408
                ret = nil
 
409
            end
 
410
        end
 
411
 
 
412
        unless ret
 
413
            case name
 
414
            when :command
 
415
                devfail "No command, somehow"
 
416
            when :special
 
417
                # nothing
 
418
            else
 
419
                #ret = (self.class.validproperty?(name).default || "*").to_s
 
420
                ret = "*"
 
421
            end
 
422
        end
 
423
 
 
424
        ret
889
425
    end
890
426
end
891
427
 
892
 
# $Id: cron.rb 1807 2006-10-18 04:03:01Z luke $
 
428
 
 
429