summaryrefslogtreecommitdiffstats
path: root/lib/puppet/property.rb
blob: 9e8bae7a463c48d2d3f57fd83de97e9a4096e13e (plain)
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
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
 # The virtual base class for properties, which are the self-contained building
# blocks for actually doing work on the system.

require 'puppet'
require 'puppet/parameter'

module Puppet
class Property < Puppet::Parameter

    # Because 'should' uses an array, we have a special method for handling
    # it.  We also want to keep copies of the original values, so that
    # they can be retrieved and compared later when merging.
    attr_reader :shouldorig

    attr_writer :noop

    class << self
        attr_accessor :unmanaged
        attr_reader :name

        # Return array matching info, defaulting to just matching
        # the first value.
        def array_matching
            unless defined?(@array_matching)
                @array_matching = :first
            end
            @array_matching
        end

        # Set whether properties should match all values or just the first one.
        def array_matching=(value)
            value = value.intern if value.is_a?(String)
            unless [:first, :all].include?(value)
                raise ArgumentError, "Supported values for Property#array_matching are 'first' and 'all'"
            end
            @array_matching = value
        end

        def checkable
            @checkable = true
        end

        def uncheckable
            @checkable = false
        end

        def checkable?
            if defined? @checkable
                return @checkable
            else
                return true
            end
        end
    end

    # Look up a value's name, so we can find options and such.
    def self.value_name(value)
        if value != '' and name = symbolize(value) and @parametervalues.include?(name)
            return name
        elsif ary = self.match?(value)
            return ary[0]
        else
            return nil
        end
    end

    # Retrieve an option set when a value was defined.
    def self.value_option(name, option)
        option = option.to_sym
        if hash = @parameteroptions[name]
            hash[option]
        else
            nil
        end
    end

    # Create the value management variables.
    def self.initvars
        @parametervalues = {}
        @aliasvalues = {}
        @parameterregexes = {}
        @parameteroptions = {}
    end

    # Define a new valid value for a property.  You must provide the value itself,
    # usually as a symbol, or a regex to match the value.
    #
    # The first argument to the method is either the value itself or a regex.
    # The second argument is an option hash; valid options are:
    # * <tt>:event</tt>: The event that should be returned when this value is set.
    # * <tt>:call</tt>: When to call any associated block.  The default value
    #   is ``instead``, which means to call the value instead of calling the
    #   provider.  You can also specify ``before`` or ``after``, which will 
    #   call both the block and the provider, according to the order you specify
    #   (the ``first`` refers to when the block is called, not the provider).
    def self.newvalue(name, options = {}, &block)
        name = name.intern if name.is_a? String

        @parameteroptions[name] = {}
        paramopts = @parameteroptions[name]

        # Symbolize everything
        options.each do |opt, val|
            paramopts[symbolize(opt)] = symbolize(val)
        end

        # By default, call the block instead of the provider.
        if block_given?
            paramopts[:call] ||= :instead
        else
            paramopts[:call] ||= :none
        end
        # If there was no block given, we still want to store the information
        # for validation, but we won't be defining a method
        block ||= true

        case name
        when Symbol
            if @parametervalues.include?(name)
                Puppet.warning "%s reassigning value %s" % [self.name, name]
            end
            @parametervalues[name] = block

            if block_given?
                method = "set_" + name.to_s
                settor = paramopts[:settor] || (self.name.to_s + "=")
                define_method(method, &block)
                paramopts[:method] = method
            end
        when Regexp
            # The regexes are handled in parameter.rb.  This value is used
            # for validation.
            @parameterregexes[name] = block

            # This is used for looking up the block for execution.
            if block_given?
                paramopts[:block] = block
            end
        else
            raise ArgumentError, "Invalid value %s of type %s" %
                [name, name.class]
        end
    end

    # Call the provider method.
    def call_provider(value)
        begin
            provider.send(self.class.name.to_s + "=", value)
        rescue NoMethodError
            self.fail "The %s provider can not handle attribute %s" %
                [provider.class.name, self.class.name]
        end
    end

    # Call the dynamically-created method associated with our value, if
    # there is one.
    def call_valuemethod(name, value)
        event = nil
        if method = self.class.value_option(name, :method) and self.respond_to?(method)
            #self.debug "setting %s (currently %s)" % [value, self.retrieve]

            begin
                event = self.send(method)
            rescue Puppet::Error
                raise
            rescue => detail
                if Puppet[:trace]
                    puts detail.backtrace
                end
                error = Puppet::Error.new("Could not set %s on %s: %s" %
                    [value, self.class.name, detail], @resource.line, @resource.file)
                error.set_backtrace detail.backtrace
                raise error
            end
        elsif block = self.class.value_option(name, :block)
            # FIXME It'd be better here to define a method, so that
            # the blocks could return values.
            # If the regex was defined with no associated block, then just pass
            # through and the correct event will be passed back.
            event = self.instance_eval(&block)
        end
        return event, name
    end

    # How should a property change be printed as a string?
    def change_to_s(currentvalue, newvalue)
        begin
            if currentvalue == :absent
                return "defined '%s' as '%s'" %
                    [self.name, self.should_to_s(newvalue)]
            elsif newvalue == :absent or newvalue == [:absent]
                return "undefined %s from '%s'" %
                    [self.name, self.is_to_s(currentvalue)]
            else
                return "%s changed '%s' to '%s'" %
                    [self.name, self.is_to_s(currentvalue), self.should_to_s(newvalue)]
            end
        rescue Puppet::Error, Puppet::DevError
            raise
        rescue => detail
            raise Puppet::DevError, "Could not convert change %s to string: %s" %
                [self.name, detail]
        end
    end

    # Figure out which event to return.
    def event(name, event = nil)
        if value_event = self.class.value_option(name, :event)
            return value_event 
        end

        if event and event.is_a?(Symbol)
            if event == :nochange
                return nil
            else
                return event
            end
        end

        if self.class.name == :ensure
            event = case self.should
            when :present: (@resource.class.name.to_s + "_created").intern
            when :absent: (@resource.class.name.to_s + "_removed").intern
            else
                (@resource.class.name.to_s + "_changed").intern
            end
        else
            event = (@resource.class.name.to_s + "_changed").intern
        end

        return event
    end
    
    # initialize our property
    def initialize(hash = {})
        super
    end

    def inspect
        str = "Property('%s', " % self.name

        if defined? @should and @should
            str += "@should = '%s')" % @should.join(", ")
        else
            str += "@should = nil)"
        end
    end

    # Determine whether the property is in-sync or not.  If @should is
    # not defined or is set to a non-true value, then we do not have
    # a valid value for it and thus consider the property to be in-sync
    # since we cannot fix it.  Otherwise, we expect our should value
    # to be an array, and if @is matches any of those values, then
    # we consider it to be in-sync.
    def insync?(is)
        #debug "%s value is '%s', should be '%s'" %
        #    [self,self.is.inspect,self.should.inspect]
        unless defined? @should and @should
            return true
        end

        unless @should.is_a?(Array)
            self.devfail "%s's should is not array" % self.class.name
        end

        # an empty array is analogous to no should values
        if @should.empty?
            return true
        end

        # Look for a matching value
        if match_all?
            return (is == @should or is == @should.collect { |v| v.to_s })
        else
            @should.each { |val|
                if is == val or is == val.to_s
                    return true
                end
            }
        end

        # otherwise, return false
        return false
    end

    # because the @should and @is vars might be in weird formats,
    # we need to set up a mechanism for pretty printing of the values
    # default to just the values, but this way individual properties can
    # override these methods
    def is_to_s(currentvalue)
        currentvalue
    end

    # Send a log message.
    def log(msg)
        unless @resource[:loglevel]
            self.devfail "Parent %s has no loglevel" % @resource.name
        end
        Puppet::Util::Log.create(
            :level => @resource[:loglevel],
            :message => msg,
            :source => self
        )
    end

    # Should we match all values, or just the first?
    def match_all?
        self.class.array_matching == :all
    end

    # each property class must define the name() method, and property instances
    # do not change that name
    # this implicitly means that a given object can only have one property
    # instance of a given property class
    def name
        return self.class.name
    end

    # for testing whether we should actually do anything
    def noop
        # This is only here to make testing easier.
        if @resource.respond_to?(:noop?)
            @resource.noop?
        else
            if defined?(@noop)
                @noop
            else
                Puppet[:noop]
            end
        end
    end

    # By default, call the method associated with the property name on our
    # provider.  In other words, if the property name is 'gid', we'll call
    # 'provider.gid' to retrieve the current value.
    def retrieve
        provider.send(self.class.name)
    end

    # Set our value, using the provider, an associated block, or both.
    def set(value)
        # Set a name for looking up associated options like the event.
        name = self.class.value_name(value)

        call = self.class.value_option(name, :call)

        # If we're supposed to call the block first or instead, call it now
        if call == :before or call == :instead
            event, tmp = call_valuemethod(name, value) 
        end
        unless call == :instead
            if @resource.provider
                call_provider(value)
            else
                # They haven't provided a block, and our parent does not have
                # a provider, so we have no idea how to handle this.
                self.fail "%s cannot handle values of type %s" %
                    [self.class.name, value.inspect]
            end
        end
        if call == :after
            event, tmp = call_valuemethod(name, value) 
        end

        return event(name, event)
    end

    # Only return the first value
    def should
        if defined? @should
            unless @should.is_a?(Array)
                self.devfail "should for %s on %s is not an array" %
                    [self.class.name, @resource.name]
            end
            if match_all?
                return @should
            else
                return @should[0]
            end
        else
            return nil
        end
    end

    # Set the should value.
    def should=(values)
        unless values.is_a?(Array)
            values = [values]
        end

        @shouldorig = values

        if self.respond_to?(:validate)
            values.each { |val|
                validate(val)
            }
        end
        if self.respond_to?(:munge)
            @should = values.collect { |val|
                self.munge(val)
            }
        else
            @should = values
        end
    end

    def should_to_s(newvalue)
        newvalue = [newvalue] unless newvalue.is_a? Array
        if defined? newvalue
            newvalue.join(" ")
        else
            return nil
        end
    end

    # The default 'sync' method only selects among a list of registered # values.
    def sync
        self.devfail("No values defined for %s" % self.class.name) unless self.class.values

        if value = self.should
            set(value)
        else
            self.devfail "Got a nil value for should"
        end
    end

    # The properties need to return tags so that logs correctly collect them.
    def tags
        unless defined? @tags
            @tags = []
            # This might not be true in testing
            if @resource.respond_to? :tags
                @tags = @resource.tags
            end
            @tags << self.name
        end
        @tags
    end

    def to_s
        return "%s(%s)" % [@resource.name,self.name]
    end

    # Provide a common hook for setting @should, just like params.
    def value=(value)
        self.should = value
    end

    # This property will get automatically added to any type that responds
    # to the methods 'exists?', 'create', and 'destroy'.
    class Ensure < Puppet::Property
        @name = :ensure

        def self.defaultvalues
            newvalue(:present) do
                if @resource.provider and @resource.provider.respond_to?(:create)
                    @resource.provider.create
                else
                    @resource.create
                end
                nil # return nil so the event is autogenerated
            end

            newvalue(:absent) do
                if @resource.provider and @resource.provider.respond_to?(:destroy)
                    @resource.provider.destroy
                else
                    @resource.destroy
                end
                nil # return nil so the event is autogenerated
            end

            defaultto do
                if @resource.managed?
                    :present
                else
                    nil
                end
            end

            # This doc will probably get overridden
            @doc ||= "The basic property that the object should be in."
        end

        def self.inherited(sub)
            # Add in the two properties that everyone will have.
            sub.class_eval do
            end
        end

        def change_to_s(currentvalue, newvalue)
            begin
                if currentvalue == :absent or currentvalue.nil?
                    return "created"
                elsif newvalue == :absent
                    return "removed"
                else
                    return "%s changed '%s' to '%s'" %
                        [self.name, self.is_to_s(currentvalue), self.should_to_s(newvalue)]
                end
            rescue Puppet::Error, Puppet::DevError
                raise
            rescue => detail
                raise Puppet::DevError, "Could not convert change %s to string: %s" %
                    [self.name, detail]
            end
        end

        def retrieve
            # XXX This is a problem -- whether the object exists or not often
            # depends on the results of other properties, yet we're the first property
            # to get checked, which means that those other properties do not have
            # @is values set.  This seems to be the source of quite a few bugs,
            # although they're mostly logging bugs, not functional ones.
            if prov = @resource.provider and prov.respond_to?(:exists?)
                result = prov.exists?
            elsif @resource.respond_to?(:exists?)
                result = @resource.exists?
            else
                raise Puppet::DevError, "No ability to determine if %s exists" %
                    @resource.class.name
            end
            if result
                return :present
            else
                return :absent
            end
        end

        # If they're talking about the thing at all, they generally want to
        # say it should exist.
        #defaultto :present
        defaultto do
            if @resource.managed?
                :present
            else
                nil
            end
        end
    end
end
end